mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-20 12:22:21 -06:00
refactor: integrations folder structure
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { waMonitor } from '@api/server.module';
|
||||
import { CacheService } from '@api/services/cache.service';
|
||||
import { CacheEngine } from '@cache/cacheengine';
|
||||
import { Chatwoot, ConfigService, HttpServer } from '@config/env.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import { isURL } from 'class-validator';
|
||||
|
||||
export class ChatwootController {
|
||||
constructor(
|
||||
private readonly chatwootService: ChatwootService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
) {}
|
||||
|
||||
public async createChatwoot(instance: InstanceDto, data: ChatwootDto) {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||
|
||||
if (data.enabled) {
|
||||
if (!isURL(data.url, { require_tld: false })) {
|
||||
throw new BadRequestException('url is not valid');
|
||||
}
|
||||
|
||||
if (!data.accountId) {
|
||||
throw new BadRequestException('accountId is required');
|
||||
}
|
||||
|
||||
if (!data.token) {
|
||||
throw new BadRequestException('token is required');
|
||||
}
|
||||
|
||||
if (data.signMsg !== true && data.signMsg !== false) {
|
||||
throw new BadRequestException('signMsg is required');
|
||||
}
|
||||
if (data.signMsg === false) data.signDelimiter = null;
|
||||
}
|
||||
|
||||
if (!data.nameInbox || data.nameInbox === '') {
|
||||
data.nameInbox = instance.instanceName;
|
||||
}
|
||||
|
||||
const result = await this.chatwootService.create(instance, data);
|
||||
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
const response = {
|
||||
...result,
|
||||
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async findChatwoot(instance: InstanceDto) {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||
|
||||
const result = await this.chatwootService.find(instance);
|
||||
|
||||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||||
|
||||
if (Object.keys(result || {}).length === 0) {
|
||||
return {
|
||||
enabled: false,
|
||||
url: '',
|
||||
accountId: '',
|
||||
token: '',
|
||||
signMsg: false,
|
||||
nameInbox: '',
|
||||
webhook_url: '',
|
||||
};
|
||||
}
|
||||
|
||||
const response = {
|
||||
...result,
|
||||
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async receiveWebhook(instance: InstanceDto, data: any) {
|
||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||
|
||||
const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine());
|
||||
const chatwootService = new ChatwootService(waMonitor, this.configService, this.prismaRepository, chatwootCache);
|
||||
|
||||
return chatwootService.receiveWebhook(instance, data);
|
||||
}
|
||||
}
|
||||
20
src/api/integrations/chatbot/chatwoot/dto/chatwoot.dto.ts
Normal file
20
src/api/integrations/chatbot/chatwoot/dto/chatwoot.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export class ChatwootDto {
|
||||
enabled?: boolean;
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
url?: string;
|
||||
nameInbox?: string;
|
||||
signMsg?: boolean;
|
||||
signDelimiter?: string;
|
||||
number?: string;
|
||||
reopenConversation?: boolean;
|
||||
conversationPending?: boolean;
|
||||
mergeBrazilContacts?: boolean;
|
||||
importContacts?: boolean;
|
||||
importMessages?: boolean;
|
||||
daysLimitImportMessages?: number;
|
||||
autoCreate?: boolean;
|
||||
organization?: string;
|
||||
logo?: string;
|
||||
ignoreJids?: string[];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Chatwoot, configService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import postgresql from 'pg';
|
||||
|
||||
const { Pool } = postgresql;
|
||||
|
||||
class Postgres {
|
||||
private logger = new Logger('Postgres');
|
||||
private pool;
|
||||
private connected = false;
|
||||
|
||||
getConnection(connectionString: string) {
|
||||
if (this.connected) {
|
||||
return this.pool;
|
||||
} else {
|
||||
this.pool = new Pool({
|
||||
connectionString,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
this.pool.on('error', () => {
|
||||
this.logger.error('postgres disconnected');
|
||||
this.connected = false;
|
||||
});
|
||||
|
||||
try {
|
||||
this.connected = true;
|
||||
} catch (e) {
|
||||
this.connected = false;
|
||||
this.logger.error('postgres connect exception caught: ' + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pool;
|
||||
}
|
||||
}
|
||||
|
||||
getChatwootConnection() {
|
||||
const uri = configService.get<Chatwoot>('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI;
|
||||
|
||||
return this.getConnection(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export const postgresClient = new Postgres();
|
||||
@@ -0,0 +1,46 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { chatwootController } from '@api/server.module';
|
||||
import { chatwootSchema, instanceSchema } from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class ChatwootRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('set'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<ChatwootDto>({
|
||||
request: req,
|
||||
schema: chatwootSchema,
|
||||
ClassRef: ChatwootDto,
|
||||
execute: (instance, data) => chatwootController.createChatwoot(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) => chatwootController.findChatwoot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('webhook'), async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => chatwootController.receiveWebhook(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
2346
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Normal file
2346
src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,524 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||||
import { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
|
||||
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
|
||||
import { Chatwoot, configService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { inbox } from '@figuro/chatwoot-sdk';
|
||||
import { Chatwoot as ChatwootModel, Contact, Message } from '@prisma/client';
|
||||
import { proto } from 'baileys';
|
||||
|
||||
type ChatwootUser = {
|
||||
user_type: string;
|
||||
user_id: number;
|
||||
};
|
||||
|
||||
type FksChatwoot = {
|
||||
phone_number: string;
|
||||
contact_id: string;
|
||||
conversation_id: string;
|
||||
};
|
||||
|
||||
type firstLastTimestamp = {
|
||||
first: number;
|
||||
last: number;
|
||||
};
|
||||
|
||||
type IWebMessageInfo = Omit<proto.IWebMessageInfo, 'key'> & Partial<Pick<proto.IWebMessageInfo, 'key'>>;
|
||||
|
||||
class ChatwootImport {
|
||||
private logger = new Logger('ChatwootImport');
|
||||
private repositoryMessagesCache = new Map<string, Set<string>>();
|
||||
private historyMessages = new Map<string, Message[]>();
|
||||
private historyContacts = new Map<string, Contact[]>();
|
||||
|
||||
public getRepositoryMessagesCache(instance: InstanceDto) {
|
||||
return this.repositoryMessagesCache.has(instance.instanceName)
|
||||
? this.repositoryMessagesCache.get(instance.instanceName)
|
||||
: null;
|
||||
}
|
||||
|
||||
public setRepositoryMessagesCache(instance: InstanceDto, repositoryMessagesCache: Set<string>) {
|
||||
this.repositoryMessagesCache.set(instance.instanceName, repositoryMessagesCache);
|
||||
}
|
||||
|
||||
public deleteRepositoryMessagesCache(instance: InstanceDto) {
|
||||
this.repositoryMessagesCache.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public addHistoryMessages(instance: InstanceDto, messagesRaw: Message[]) {
|
||||
const actualValue = this.historyMessages.has(instance.instanceName)
|
||||
? this.historyMessages.get(instance.instanceName)
|
||||
: [];
|
||||
this.historyMessages.set(instance.instanceName, actualValue.concat(messagesRaw));
|
||||
}
|
||||
|
||||
public addHistoryContacts(instance: InstanceDto, contactsRaw: Contact[]) {
|
||||
const actualValue = this.historyContacts.has(instance.instanceName)
|
||||
? this.historyContacts.get(instance.instanceName)
|
||||
: [];
|
||||
this.historyContacts.set(instance.instanceName, actualValue.concat(contactsRaw));
|
||||
}
|
||||
|
||||
public deleteHistoryMessages(instance: InstanceDto) {
|
||||
this.historyMessages.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public deleteHistoryContacts(instance: InstanceDto) {
|
||||
this.historyContacts.delete(instance.instanceName);
|
||||
}
|
||||
|
||||
public clearAll(instance: InstanceDto) {
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteHistoryContacts(instance);
|
||||
}
|
||||
|
||||
public getHistoryMessagesLenght(instance: InstanceDto) {
|
||||
return this.historyMessages.get(instance.instanceName)?.length ?? 0;
|
||||
}
|
||||
|
||||
public async importHistoryContacts(instance: InstanceDto, provider: ChatwootDto) {
|
||||
try {
|
||||
if (this.getHistoryMessagesLenght(instance) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
let totalContactsImported = 0;
|
||||
|
||||
const contacts = this.historyContacts.get(instance.instanceName) || [];
|
||||
if (contacts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let contactsChunk: Contact[] = this.sliceIntoChunks(contacts, 3000);
|
||||
while (contactsChunk.length > 0) {
|
||||
const labelSql = `SELECT id FROM labels WHERE title = '${provider.nameInbox}' AND account_id = ${provider.accountId} LIMIT 1`;
|
||||
|
||||
let labelId = (await pgClient.query(labelSql))?.rows[0]?.id;
|
||||
|
||||
if (!labelId) {
|
||||
// creating label in chatwoot db and getting the id
|
||||
const sqlLabel = `INSERT INTO labels (title, color, show_on_sidebar, account_id, created_at, updated_at) VALUES ('${provider.nameInbox}', '#34039B', true, ${provider.accountId}, NOW(), NOW()) RETURNING id`;
|
||||
|
||||
labelId = (await pgClient.query(sqlLabel))?.rows[0]?.id;
|
||||
}
|
||||
|
||||
// inserting contacts in chatwoot db
|
||||
let sqlInsert = `INSERT INTO contacts
|
||||
(name, phone_number, account_id, identifier, created_at, updated_at) VALUES `;
|
||||
const bindInsert = [provider.accountId];
|
||||
|
||||
for (const contact of contactsChunk) {
|
||||
bindInsert.push(contact.pushName);
|
||||
const bindName = `$${bindInsert.length}`;
|
||||
|
||||
bindInsert.push(`+${contact.remoteJid.split('@')[0]}`);
|
||||
const bindPhoneNumber = `$${bindInsert.length}`;
|
||||
|
||||
bindInsert.push(contact.remoteJid);
|
||||
const bindIdentifier = `$${bindInsert.length}`;
|
||||
|
||||
sqlInsert += `(${bindName}, ${bindPhoneNumber}, $1, ${bindIdentifier}, NOW(), NOW()),`;
|
||||
}
|
||||
if (sqlInsert.slice(-1) === ',') {
|
||||
sqlInsert = sqlInsert.slice(0, -1);
|
||||
}
|
||||
sqlInsert += ` ON CONFLICT (identifier, account_id)
|
||||
DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
phone_number = EXCLUDED.phone_number,
|
||||
identifier = EXCLUDED.identifier`;
|
||||
|
||||
totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0;
|
||||
|
||||
const sqlTags = `SELECT id FROM tags WHERE name = '${provider.nameInbox}' LIMIT 1`;
|
||||
|
||||
const tagData = (await pgClient.query(sqlTags))?.rows[0];
|
||||
let tagId = tagData?.id;
|
||||
|
||||
const sqlTag = `INSERT INTO tags (name, taggings_count) VALUES ('${provider.nameInbox}', ${totalContactsImported}) ON CONFLICT (name) DO UPDATE SET taggings_count = tags.taggings_count + ${totalContactsImported} RETURNING id`;
|
||||
|
||||
tagId = (await pgClient.query(sqlTag))?.rows[0]?.id;
|
||||
|
||||
await pgClient.query(sqlTag);
|
||||
|
||||
let sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at) VALUES `;
|
||||
|
||||
contactsChunk.forEach((contact) => {
|
||||
const bindTaggableId = `(SELECT id FROM contacts WHERE identifier = '${contact.remoteJid}' AND account_id = ${provider.accountId})`;
|
||||
sqlInsertLabel += `($1, $2, ${bindTaggableId}, $3, NOW()),`;
|
||||
});
|
||||
|
||||
if (sqlInsertLabel.slice(-1) === ',') {
|
||||
sqlInsertLabel = sqlInsertLabel.slice(0, -1);
|
||||
}
|
||||
|
||||
await pgClient.query(sqlInsertLabel, [tagId, 'Contact', 'labels']);
|
||||
|
||||
contactsChunk = this.sliceIntoChunks(contacts, 3000);
|
||||
}
|
||||
|
||||
this.deleteHistoryContacts(instance);
|
||||
|
||||
return totalContactsImported;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on import history contacts: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async importHistoryMessages(
|
||||
instance: InstanceDto,
|
||||
chatwootService: ChatwootService,
|
||||
inbox: inbox,
|
||||
provider: ChatwootModel,
|
||||
) {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const chatwootUser = await this.getChatwootUser(provider);
|
||||
if (!chatwootUser) {
|
||||
throw new Error('User not found to import messages.');
|
||||
}
|
||||
|
||||
let totalMessagesImported = 0;
|
||||
|
||||
const messagesOrdered = this.historyMessages.get(instance.instanceName) || [];
|
||||
if (messagesOrdered.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ordering messages by number and timestamp asc
|
||||
messagesOrdered.sort((a, b) => {
|
||||
const aKey = a.key as {
|
||||
remoteJid: string;
|
||||
};
|
||||
|
||||
const bKey = b.key as {
|
||||
remoteJid: string;
|
||||
};
|
||||
|
||||
const aMessageTimestamp = a.messageTimestamp as any as number;
|
||||
const bMessageTimestamp = b.messageTimestamp as any as number;
|
||||
|
||||
return parseInt(aKey.remoteJid) - parseInt(bKey.remoteJid) || aMessageTimestamp - bMessageTimestamp;
|
||||
});
|
||||
|
||||
const allMessagesMappedByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesOrdered);
|
||||
// Map structure: +552199999999 => { first message timestamp from number, last message timestamp from number}
|
||||
const phoneNumbersWithTimestamp = new Map<string, firstLastTimestamp>();
|
||||
allMessagesMappedByPhoneNumber.forEach((messages: Message[], phoneNumber: string) => {
|
||||
phoneNumbersWithTimestamp.set(phoneNumber, {
|
||||
first: messages[0]?.messageTimestamp as any as number,
|
||||
last: messages[messages.length - 1]?.messageTimestamp as any as number,
|
||||
});
|
||||
});
|
||||
|
||||
// processing messages in batch
|
||||
const batchSize = 4000;
|
||||
let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||
while (messagesChunk.length > 0) {
|
||||
// Map structure: +552199999999 => Message[]
|
||||
const messagesByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesChunk);
|
||||
|
||||
if (messagesByPhoneNumber.size > 0) {
|
||||
const fksByNumber = await this.selectOrCreateFksFromChatwoot(
|
||||
provider,
|
||||
inbox,
|
||||
phoneNumbersWithTimestamp,
|
||||
messagesByPhoneNumber,
|
||||
);
|
||||
|
||||
// inserting messages in chatwoot db
|
||||
let sqlInsertMsg = `INSERT INTO messages
|
||||
(content, account_id, inbox_id, conversation_id, message_type, private, content_type,
|
||||
sender_type, sender_id, created_at, updated_at) VALUES `;
|
||||
const bindInsertMsg = [provider.accountId, inbox.id];
|
||||
|
||||
messagesByPhoneNumber.forEach((messages: any[], phoneNumber: string) => {
|
||||
const fksChatwoot = fksByNumber.get(phoneNumber);
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (!message.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fksChatwoot?.conversation_id || !fksChatwoot?.contact_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentMessage = this.getContentMessage(chatwootService, message);
|
||||
if (!contentMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
bindInsertMsg.push(contentMessage);
|
||||
const bindContent = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(fksChatwoot.conversation_id);
|
||||
const bindConversationId = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? '1' : '0');
|
||||
const bindMessageType = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_type : 'Contact');
|
||||
const bindSenderType = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_id : fksChatwoot.contact_id);
|
||||
const bindSenderId = `$${bindInsertMsg.length}`;
|
||||
|
||||
bindInsertMsg.push(message.messageTimestamp as number);
|
||||
const bindmessageTimestamp = `$${bindInsertMsg.length}`;
|
||||
|
||||
sqlInsertMsg += `(${bindContent}, $1, $2, ${bindConversationId}, ${bindMessageType}, FALSE, 0,
|
||||
${bindSenderType},${bindSenderId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`;
|
||||
});
|
||||
});
|
||||
if (bindInsertMsg.length > 2) {
|
||||
if (sqlInsertMsg.slice(-1) === ',') {
|
||||
sqlInsertMsg = sqlInsertMsg.slice(0, -1);
|
||||
}
|
||||
totalMessagesImported += (await pgClient.query(sqlInsertMsg, bindInsertMsg))?.rowCount ?? 0;
|
||||
}
|
||||
}
|
||||
messagesChunk = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||
}
|
||||
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
|
||||
const providerData: ChatwootDto = {
|
||||
...provider,
|
||||
ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [],
|
||||
};
|
||||
|
||||
this.importHistoryContacts(instance, providerData);
|
||||
|
||||
return totalMessagesImported;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on import history messages: ${error.toString()}`);
|
||||
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public async selectOrCreateFksFromChatwoot(
|
||||
provider: ChatwootModel,
|
||||
inbox: inbox,
|
||||
phoneNumbersWithTimestamp: Map<string, firstLastTimestamp>,
|
||||
messagesByPhoneNumber: Map<string, Message[]>,
|
||||
): Promise<Map<string, FksChatwoot>> {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const bindValues = [provider.accountId, inbox.id];
|
||||
const phoneNumberBind = Array.from(messagesByPhoneNumber.keys())
|
||||
.map((phoneNumber) => {
|
||||
const phoneNumberTimestamp = phoneNumbersWithTimestamp.get(phoneNumber);
|
||||
|
||||
if (phoneNumberTimestamp) {
|
||||
bindValues.push(phoneNumber);
|
||||
let bindStr = `($${bindValues.length},`;
|
||||
|
||||
bindValues.push(phoneNumberTimestamp.first);
|
||||
bindStr += `$${bindValues.length},`;
|
||||
|
||||
bindValues.push(phoneNumberTimestamp.last);
|
||||
return `${bindStr}$${bindValues.length})`;
|
||||
}
|
||||
})
|
||||
.join(',');
|
||||
|
||||
// select (or insert when necessary) data from tables contacts, contact_inboxes, conversations from chatwoot db
|
||||
const sqlFromChatwoot = `WITH
|
||||
phone_number AS (
|
||||
SELECT phone_number, created_at::INTEGER, last_activity_at::INTEGER FROM (
|
||||
VALUES
|
||||
${phoneNumberBind}
|
||||
) as t (phone_number, created_at, last_activity_at)
|
||||
),
|
||||
|
||||
only_new_phone_number AS (
|
||||
SELECT * FROM phone_number
|
||||
WHERE phone_number NOT IN (
|
||||
SELECT phone_number
|
||||
FROM contacts
|
||||
JOIN contact_inboxes ci ON ci.contact_id = contacts.id AND ci.inbox_id = $2
|
||||
JOIN conversations con ON con.contact_inbox_id = ci.id
|
||||
AND con.account_id = $1
|
||||
AND con.inbox_id = $2
|
||||
AND con.contact_id = contacts.id
|
||||
WHERE contacts.account_id = $1
|
||||
)
|
||||
),
|
||||
|
||||
new_contact AS (
|
||||
INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at)
|
||||
SELECT REPLACE(p.phone_number, '+', ''), p.phone_number, $1, CONCAT(REPLACE(p.phone_number, '+', ''),
|
||||
'@s.whatsapp.net'), to_timestamp(p.created_at), to_timestamp(p.last_activity_at)
|
||||
FROM only_new_phone_number AS p
|
||||
ON CONFLICT(identifier, account_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
|
||||
RETURNING id, phone_number, created_at, updated_at
|
||||
),
|
||||
|
||||
new_contact_inbox AS (
|
||||
INSERT INTO contact_inboxes (contact_id, inbox_id, source_id, created_at, updated_at)
|
||||
SELECT new_contact.id, $2, gen_random_uuid(), new_contact.created_at, new_contact.updated_at
|
||||
FROM new_contact
|
||||
RETURNING id, contact_id, created_at, updated_at
|
||||
),
|
||||
|
||||
new_conversation AS (
|
||||
INSERT INTO conversations (account_id, inbox_id, status, contact_id,
|
||||
contact_inbox_id, uuid, last_activity_at, created_at, updated_at)
|
||||
SELECT $1, $2, 0, new_contact_inbox.contact_id, new_contact_inbox.id, gen_random_uuid(),
|
||||
new_contact_inbox.updated_at, new_contact_inbox.created_at, new_contact_inbox.updated_at
|
||||
FROM new_contact_inbox
|
||||
RETURNING id, contact_id
|
||||
)
|
||||
|
||||
SELECT new_contact.phone_number, new_conversation.contact_id, new_conversation.id AS conversation_id
|
||||
FROM new_conversation
|
||||
JOIN new_contact ON new_conversation.contact_id = new_contact.id
|
||||
|
||||
UNION
|
||||
|
||||
SELECT p.phone_number, c.id contact_id, con.id conversation_id
|
||||
FROM phone_number p
|
||||
JOIN contacts c ON c.phone_number = p.phone_number
|
||||
JOIN contact_inboxes ci ON ci.contact_id = c.id AND ci.inbox_id = $2
|
||||
JOIN conversations con ON con.contact_inbox_id = ci.id AND con.account_id = $1
|
||||
AND con.inbox_id = $2 AND con.contact_id = c.id`;
|
||||
|
||||
const fksFromChatwoot = await pgClient.query(sqlFromChatwoot, bindValues);
|
||||
|
||||
return new Map(fksFromChatwoot.rows.map((item: FksChatwoot) => [item.phone_number, item]));
|
||||
}
|
||||
|
||||
public async getChatwootUser(provider: ChatwootModel): Promise<ChatwootUser> {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const sqlUser = `SELECT owner_type AS user_type, owner_id AS user_id
|
||||
FROM access_tokens
|
||||
WHERE token = $1`;
|
||||
|
||||
return (await pgClient.query(sqlUser, [provider.token]))?.rows[0] || false;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on getChatwootUser: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public createMessagesMapByPhoneNumber(messages: Message[]): Map<string, Message[]> {
|
||||
return messages.reduce((acc: Map<string, Message[]>, message: Message) => {
|
||||
const key = message?.key as {
|
||||
remoteJid: string;
|
||||
};
|
||||
if (!this.isIgnorePhoneNumber(key?.remoteJid)) {
|
||||
const phoneNumber = key?.remoteJid?.split('@')[0];
|
||||
if (phoneNumber) {
|
||||
const phoneNumberPlus = `+${phoneNumber}`;
|
||||
const messages = acc.has(phoneNumberPlus) ? acc.get(phoneNumberPlus) : [];
|
||||
messages.push(message);
|
||||
acc.set(phoneNumberPlus, messages);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
public async getContactsOrderByRecentConversations(
|
||||
inbox: inbox,
|
||||
provider: ChatwootModel,
|
||||
limit = 50,
|
||||
): Promise<{ id: number; phone_number: string; identifier: string }[]> {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const sql = `SELECT contacts.id, contacts.identifier, contacts.phone_number
|
||||
FROM conversations
|
||||
JOIN contacts ON contacts.id = conversations.contact_id
|
||||
WHERE conversations.account_id = $1
|
||||
AND inbox_id = $2
|
||||
ORDER BY conversations.last_activity_at DESC
|
||||
LIMIT $3`;
|
||||
|
||||
return (await pgClient.query(sql, [provider.accountId, inbox.id, limit]))?.rows;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on get recent conversations: ${error.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getContentMessage(chatwootService: ChatwootService, msg: IWebMessageInfo) {
|
||||
const contentMessage = chatwootService.getConversationMessage(msg.message);
|
||||
if (contentMessage) {
|
||||
return contentMessage;
|
||||
}
|
||||
|
||||
if (!configService.get<Chatwoot>('CHATWOOT').IMPORT.PLACEHOLDER_MEDIA_MESSAGE) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const types = {
|
||||
documentMessage: msg.message.documentMessage,
|
||||
documentWithCaptionMessage: msg.message.documentWithCaptionMessage?.message?.documentMessage,
|
||||
imageMessage: msg.message.imageMessage,
|
||||
videoMessage: msg.message.videoMessage,
|
||||
audioMessage: msg.message.audioMessage,
|
||||
stickerMessage: msg.message.stickerMessage,
|
||||
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
|
||||
};
|
||||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
|
||||
|
||||
switch (typeKey) {
|
||||
case 'documentMessage':
|
||||
return `_<File: ${msg.message.documentMessage.fileName}${
|
||||
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
|
||||
}>_`;
|
||||
|
||||
case 'documentWithCaptionMessage':
|
||||
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${
|
||||
msg.message.documentWithCaptionMessage.message.documentMessage.caption
|
||||
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}`
|
||||
: ''
|
||||
}>_`;
|
||||
|
||||
case 'templateMessage':
|
||||
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText
|
||||
? `*${msg.message.templateMessage.hydratedTemplate.hydratedTitleText}*\\n`
|
||||
: '' + msg.message.templateMessage.hydratedTemplate.hydratedContentText;
|
||||
|
||||
case 'imageMessage':
|
||||
return '_<Image Message>_';
|
||||
|
||||
case 'videoMessage':
|
||||
return '_<Video Message>_';
|
||||
|
||||
case 'audioMessage':
|
||||
return '_<Audio Message>_';
|
||||
|
||||
case 'stickerMessage':
|
||||
return '_<Sticker Message>_';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public sliceIntoChunks(arr: any[], chunkSize: number) {
|
||||
return arr.splice(0, chunkSize);
|
||||
}
|
||||
|
||||
public isGroup(remoteJid: string) {
|
||||
return remoteJid.includes('@g.us');
|
||||
}
|
||||
|
||||
public isIgnorePhoneNumber(remoteJid: string) {
|
||||
return this.isGroup(remoteJid) || remoteJid === 'status@broadcast' || remoteJid === '0@s.whatsapp.net';
|
||||
}
|
||||
}
|
||||
|
||||
export const chatwootImport = new ChatwootImport();
|
||||
@@ -0,0 +1,45 @@
|
||||
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 chatwootSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', enum: [true, false] },
|
||||
accountId: { type: 'string' },
|
||||
token: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
signMsg: { type: 'boolean', enum: [true, false] },
|
||||
signDelimiter: { type: ['string', 'null'] },
|
||||
nameInbox: { type: ['string', 'null'] },
|
||||
reopenConversation: { type: 'boolean', enum: [true, false] },
|
||||
conversationPending: { type: 'boolean', enum: [true, false] },
|
||||
autoCreate: { type: 'boolean', enum: [true, false] },
|
||||
importContacts: { type: 'boolean', enum: [true, false] },
|
||||
mergeBrazilContacts: { type: 'boolean', enum: [true, false] },
|
||||
importMessages: { type: 'boolean', enum: [true, false] },
|
||||
daysLimitImportMessages: { type: 'number' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['enabled', 'accountId', 'token', 'url', 'signMsg', 'reopenConversation', 'conversationPending'],
|
||||
...isNotEmpty('enabled', 'accountId', 'token', 'url', 'signMsg', 'reopenConversation', 'conversationPending'),
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { DifyDto, DifyIgnoreJidDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
|
||||
import { DifyService } from '@api/integrations/chatbot/dify/services/dify.service';
|
||||
import { configService, Dify } from '@config/env.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
|
||||
export class DifyController {
|
||||
constructor(private readonly difyService: DifyService) {}
|
||||
|
||||
public async createDify(instance: InstanceDto, data: DifyDto) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findDify(instance: InstanceDto) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.find(instance);
|
||||
}
|
||||
|
||||
public async fetchDify(instance: InstanceDto, difyId: string) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.fetch(instance, difyId);
|
||||
}
|
||||
|
||||
public async updateDify(instance: InstanceDto, difyId: string, data: DifyDto) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.update(instance, difyId, data);
|
||||
}
|
||||
|
||||
public async deleteDify(instance: InstanceDto, difyId: string) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.delete(instance, difyId);
|
||||
}
|
||||
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.setDefaultSettings(instance, data);
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.fetchDefaultSettings(instance);
|
||||
}
|
||||
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.changeStatus(instance, data);
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, difyId: string) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.fetchSessions(instance, difyId);
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: DifyIgnoreJidDto) {
|
||||
if (!configService.get<Dify>('DIFY').ENABLED) throw new BadRequestException('Dify is disabled');
|
||||
|
||||
return this.difyService.ignoreJid(instance, data);
|
||||
}
|
||||
}
|
||||
47
src/api/integrations/chatbot/dify/dto/dify.dto.ts
Normal file
47
src/api/integrations/chatbot/dify/dto/dify.dto.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export class Session {
|
||||
remoteJid?: string;
|
||||
sessionId?: string;
|
||||
status?: string;
|
||||
createdAt?: number;
|
||||
updateAt?: number;
|
||||
}
|
||||
|
||||
export class DifyDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
botType?: $Enums.DifyBotType;
|
||||
apiUrl?: string;
|
||||
apiKey?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
}
|
||||
|
||||
export class DifySettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
difyIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
}
|
||||
|
||||
export class DifyIgnoreJidDto {
|
||||
remoteJid?: string;
|
||||
action?: string;
|
||||
}
|
||||
122
src/api/integrations/chatbot/dify/routes/dify.router.ts
Normal file
122
src/api/integrations/chatbot/dify/routes/dify.router.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { DifyDto, DifyIgnoreJidDto, DifySettingDto } from '@api/integrations/chatbot/dify/dto/dify.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { difyController } from '@api/server.module';
|
||||
import {
|
||||
difyIgnoreJidSchema,
|
||||
difySchema,
|
||||
difySettingSchema,
|
||||
difyStatusSchema,
|
||||
instanceSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class DifyRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<DifyDto>({
|
||||
request: req,
|
||||
schema: difySchema,
|
||||
ClassRef: DifyDto,
|
||||
execute: (instance, data) => difyController.createDify(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) => difyController.findDify(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:difyId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => difyController.fetchDify(instance, req.params.difyId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:difyId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<DifyDto>({
|
||||
request: req,
|
||||
schema: difySchema,
|
||||
ClassRef: DifyDto,
|
||||
execute: (instance, data) => difyController.updateDify(instance, req.params.difyId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:difyId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => difyController.deleteDify(instance, req.params.difyId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<DifySettingDto>({
|
||||
request: req,
|
||||
schema: difySettingSchema,
|
||||
ClassRef: DifySettingDto,
|
||||
execute: (instance, data) => difyController.settings(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => difyController.fetchSettings(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: difyStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => difyController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:difyId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => difyController.fetchSessions(instance, req.params.difyId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<DifyIgnoreJidDto>({
|
||||
request: req,
|
||||
schema: difyIgnoreJidSchema,
|
||||
ClassRef: DifyIgnoreJidDto,
|
||||
execute: (instance, data) => difyController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
2100
src/api/integrations/chatbot/dify/services/dify.service.ts
Normal file
2100
src/api/integrations/chatbot/dify/services/dify.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
108
src/api/integrations/chatbot/dify/validate/dify.schema.ts
Normal file
108
src/api/integrations/chatbot/dify/validate/dify.schema.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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 difySchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
botType: { type: 'string', enum: ['chatBot', 'textGenerator', 'agent', 'workflow'] },
|
||||
apiUrl: { type: 'string' },
|
||||
apiKey: { type: 'string' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['enabled', 'botType', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'botType', 'triggerType'),
|
||||
};
|
||||
|
||||
export const difyStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const difySettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
difyIdFallback: { type: 'string' },
|
||||
},
|
||||
required: [
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
),
|
||||
};
|
||||
|
||||
export const difyIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { OpenaiCredsDto, OpenaiDto, OpenaiIgnoreJidDto } from '@api/integrations/chatbot/openai/dto/openai.dto';
|
||||
import { OpenaiService } from '@api/integrations/chatbot/openai/services/openai.service';
|
||||
import { configService, Openai } from '@config/env.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
|
||||
export class OpenaiController {
|
||||
constructor(private readonly openaiService: OpenaiService) {}
|
||||
|
||||
public async createOpenaiCreds(instance: InstanceDto, data: OpenaiCredsDto) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.createCreds(instance, data);
|
||||
}
|
||||
|
||||
public async findOpenaiCreds(instance: InstanceDto) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.findCreds(instance);
|
||||
}
|
||||
|
||||
public async deleteCreds(instance: InstanceDto, openaiCredsId: string) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.deleteCreds(instance, openaiCredsId);
|
||||
}
|
||||
|
||||
public async createOpenai(instance: InstanceDto, data: OpenaiDto) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findOpenai(instance: InstanceDto) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.find(instance);
|
||||
}
|
||||
|
||||
public async fetchOpenai(instance: InstanceDto, openaiBotId: string) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.fetch(instance, openaiBotId);
|
||||
}
|
||||
|
||||
public async updateOpenai(instance: InstanceDto, openaiBotId: string, data: OpenaiDto) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.update(instance, openaiBotId, data);
|
||||
}
|
||||
|
||||
public async deleteOpenai(instance: InstanceDto, openaiBotId: string) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.delete(instance, openaiBotId);
|
||||
}
|
||||
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.setDefaultSettings(instance, data);
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.fetchDefaultSettings(instance);
|
||||
}
|
||||
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.changeStatus(instance, data);
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, openaiBotId: string) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.fetchSessions(instance, openaiBotId);
|
||||
}
|
||||
|
||||
public async getModels(instance: InstanceDto) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.getModels(instance);
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: OpenaiIgnoreJidDto) {
|
||||
if (!configService.get<Openai>('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled');
|
||||
|
||||
return this.openaiService.ignoreJid(instance, data);
|
||||
}
|
||||
}
|
||||
60
src/api/integrations/chatbot/openai/dto/openai.dto.ts
Normal file
60
src/api/integrations/chatbot/openai/dto/openai.dto.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export class Session {
|
||||
remoteJid?: string;
|
||||
sessionId?: string;
|
||||
status?: string;
|
||||
createdAt?: number;
|
||||
updateAt?: number;
|
||||
}
|
||||
|
||||
export class OpenaiCredsDto {
|
||||
name: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export class OpenaiDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
openaiCredsId: string;
|
||||
botType?: string;
|
||||
assistantId?: string;
|
||||
functionUrl?: string;
|
||||
model?: string;
|
||||
systemMessages?: string[];
|
||||
assistantMessages?: string[];
|
||||
userMessages?: string[];
|
||||
maxTokens?: number;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
}
|
||||
|
||||
export class OpenaiSettingDto {
|
||||
openaiCredsId?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
openaiIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
speechToText?: boolean;
|
||||
}
|
||||
|
||||
export class OpenaiIgnoreJidDto {
|
||||
remoteJid?: string;
|
||||
action?: string;
|
||||
}
|
||||
168
src/api/integrations/chatbot/openai/routes/openai.router.ts
Normal file
168
src/api/integrations/chatbot/openai/routes/openai.router.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import {
|
||||
OpenaiCredsDto,
|
||||
OpenaiDto,
|
||||
OpenaiIgnoreJidDto,
|
||||
OpenaiSettingDto,
|
||||
} from '@api/integrations/chatbot/openai/dto/openai.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { openaiController } from '@api/server.module';
|
||||
import {
|
||||
instanceSchema,
|
||||
openaiCredsSchema,
|
||||
openaiIgnoreJidSchema,
|
||||
openaiSchema,
|
||||
openaiSettingSchema,
|
||||
openaiStatusSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class OpenaiRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('creds'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiCredsDto>({
|
||||
request: req,
|
||||
schema: openaiCredsSchema,
|
||||
ClassRef: OpenaiCredsDto,
|
||||
execute: (instance, data) => openaiController.createOpenaiCreds(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.get(this.routerPath('creds'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.findOpenaiCreds(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('creds/:openaiCredsId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.deleteCreds(instance, req.params.openaiCredsId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiDto>({
|
||||
request: req,
|
||||
schema: openaiSchema,
|
||||
ClassRef: OpenaiDto,
|
||||
execute: (instance, data) => openaiController.createOpenai(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) => openaiController.findOpenai(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:openaiBotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.fetchOpenai(instance, req.params.openaiBotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:openaiBotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiDto>({
|
||||
request: req,
|
||||
schema: openaiSchema,
|
||||
ClassRef: OpenaiDto,
|
||||
execute: (instance, data) => openaiController.updateOpenai(instance, req.params.openaiBotId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:openaiBotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.deleteOpenai(instance, req.params.openaiBotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiSettingDto>({
|
||||
request: req,
|
||||
schema: openaiSettingSchema,
|
||||
ClassRef: OpenaiSettingDto,
|
||||
execute: (instance, data) => openaiController.settings(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.fetchSettings(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: openaiStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => openaiController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:openaiBotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.fetchSessions(instance, req.params.openaiBotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<OpenaiIgnoreJidDto>({
|
||||
request: req,
|
||||
schema: openaiIgnoreJidSchema,
|
||||
ClassRef: OpenaiIgnoreJidDto,
|
||||
execute: (instance, data) => openaiController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('getModels'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => openaiController.getModels(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
2247
src/api/integrations/chatbot/openai/services/openai.service.ts
Normal file
2247
src/api/integrations/chatbot/openai/services/openai.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
129
src/api/integrations/chatbot/openai/validate/openai.schema.ts
Normal file
129
src/api/integrations/chatbot/openai/validate/openai.schema.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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 openaiSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
openaiCredsId: { type: 'string' },
|
||||
botType: { type: 'string', enum: ['assistant', 'chatCompletion'] },
|
||||
assistantId: { type: 'string' },
|
||||
functionUrl: { type: 'string' },
|
||||
model: { type: 'string' },
|
||||
systemMessages: { type: 'array', items: { type: 'string' } },
|
||||
assistantMessages: { type: 'array', items: { type: 'string' } },
|
||||
userMessages: { type: 'array', items: { type: 'string' } },
|
||||
maxTokens: { type: 'integer' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['enabled', 'openaiCredsId', 'botType', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'openaiCredsId', 'botType', 'triggerType'),
|
||||
};
|
||||
|
||||
export const openaiCredsSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
apiKey: { type: 'string' },
|
||||
},
|
||||
required: ['name', 'apiKey'],
|
||||
...isNotEmpty('name', 'apiKey'),
|
||||
};
|
||||
|
||||
export const openaiStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const openaiSettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
openaiCredsId: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
speechToText: { type: 'boolean' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
openaiIdFallback: { type: 'string' },
|
||||
},
|
||||
required: [
|
||||
'openaiCredsId',
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
],
|
||||
...isNotEmpty(
|
||||
'openaiCredsId',
|
||||
'expire',
|
||||
'keywordFinish',
|
||||
'delayMessage',
|
||||
'unknownMessage',
|
||||
'listeningFromMe',
|
||||
'stopBotFromMe',
|
||||
'keepOpen',
|
||||
'debounceTime',
|
||||
'ignoreJids',
|
||||
),
|
||||
};
|
||||
|
||||
export const openaiIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { TypebotDto, TypebotIgnoreJidDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
|
||||
import { TypebotService } from '@api/integrations/chatbot/typebot/services/typebot.service';
|
||||
import { configService, Typebot } from '@config/env.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
|
||||
export class TypebotController {
|
||||
constructor(private readonly typebotService: TypebotService) {}
|
||||
|
||||
public async createTypebot(instance: InstanceDto, data: TypebotDto) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findTypebot(instance: InstanceDto) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.find(instance);
|
||||
}
|
||||
|
||||
public async fetchTypebot(instance: InstanceDto, typebotId: string) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.fetch(instance, typebotId);
|
||||
}
|
||||
|
||||
public async updateTypebot(instance: InstanceDto, typebotId: string, data: TypebotDto) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.update(instance, typebotId, data);
|
||||
}
|
||||
|
||||
public async deleteTypebot(instance: InstanceDto, typebotId: string) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.delete(instance, typebotId);
|
||||
}
|
||||
|
||||
public async startTypebot(instance: InstanceDto, data: any) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.startTypebot(instance, data);
|
||||
}
|
||||
|
||||
public async settings(instance: InstanceDto, data: any) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.setDefaultSettings(instance, data);
|
||||
}
|
||||
|
||||
public async fetchSettings(instance: InstanceDto) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.fetchDefaultSettings(instance);
|
||||
}
|
||||
|
||||
public async changeStatus(instance: InstanceDto, data: any) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.changeStatus(instance, data);
|
||||
}
|
||||
|
||||
public async fetchSessions(instance: InstanceDto, typebotId: string) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.fetchSessions(instance, typebotId);
|
||||
}
|
||||
|
||||
public async ignoreJid(instance: InstanceDto, data: TypebotIgnoreJidDto) {
|
||||
if (!configService.get<Typebot>('TYPEBOT').ENABLED) throw new BadRequestException('Typebot is disabled');
|
||||
|
||||
return this.typebotService.ignoreJid(instance, data);
|
||||
}
|
||||
}
|
||||
54
src/api/integrations/chatbot/typebot/dto/typebot.dto.ts
Normal file
54
src/api/integrations/chatbot/typebot/dto/typebot.dto.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export class Session {
|
||||
remoteJid?: string;
|
||||
sessionId?: string;
|
||||
status?: string;
|
||||
createdAt?: number;
|
||||
updateAt?: number;
|
||||
prefilledVariables?: PrefilledVariables;
|
||||
}
|
||||
|
||||
export class PrefilledVariables {
|
||||
remoteJid?: string;
|
||||
pushName?: string;
|
||||
messageType?: string;
|
||||
additionalData?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export class TypebotDto {
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
url: string;
|
||||
typebot?: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType?: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: any;
|
||||
}
|
||||
|
||||
export class TypebotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
typebotIdFallback?: string;
|
||||
ignoreJids?: any;
|
||||
}
|
||||
|
||||
export class TypebotIgnoreJidDto {
|
||||
remoteJid?: string;
|
||||
action?: string;
|
||||
}
|
||||
133
src/api/integrations/chatbot/typebot/routes/typebot.router.ts
Normal file
133
src/api/integrations/chatbot/typebot/routes/typebot.router.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { TypebotDto, TypebotIgnoreJidDto, TypebotSettingDto } from '@api/integrations/chatbot/typebot/dto/typebot.dto';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { typebotController } from '@api/server.module';
|
||||
import {
|
||||
instanceSchema,
|
||||
typebotIgnoreJidSchema,
|
||||
typebotSchema,
|
||||
typebotSettingSchema,
|
||||
typebotStartSchema,
|
||||
typebotStatusSchema,
|
||||
} from '@validate/validate.schema';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class TypebotRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('create'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<TypebotDto>({
|
||||
request: req,
|
||||
schema: typebotSchema,
|
||||
ClassRef: TypebotDto,
|
||||
execute: (instance, data) => typebotController.createTypebot(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) => typebotController.findTypebot(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetch/:typebotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => typebotController.fetchTypebot(instance, req.params.typebotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.put(this.routerPath('update/:typebotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<TypebotDto>({
|
||||
request: req,
|
||||
schema: typebotSchema,
|
||||
ClassRef: TypebotDto,
|
||||
execute: (instance, data) => typebotController.updateTypebot(instance, req.params.typebotId, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.delete(this.routerPath('delete/:typebotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => typebotController.deleteTypebot(instance, req.params.typebotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('settings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<TypebotSettingDto>({
|
||||
request: req,
|
||||
schema: typebotSettingSchema,
|
||||
ClassRef: TypebotSettingDto,
|
||||
execute: (instance, data) => typebotController.settings(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSettings'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => typebotController.fetchSettings(instance),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('start'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: typebotStartSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => typebotController.startTypebot(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('changeStatus'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: typebotStatusSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance, data) => typebotController.changeStatus(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.get(this.routerPath('fetchSessions/:typebotId'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: instanceSchema,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => typebotController.fetchSessions(instance, req.params.typebotId),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('ignoreJid'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<TypebotIgnoreJidDto>({
|
||||
request: req,
|
||||
schema: typebotIgnoreJidSchema,
|
||||
ClassRef: TypebotIgnoreJidDto,
|
||||
execute: (instance, data) => typebotController.ignoreJid(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
2041
src/api/integrations/chatbot/typebot/services/typebot.service.ts
Normal file
2041
src/api/integrations/chatbot/typebot/services/typebot.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
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 typebotSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
typebot: { type: 'string' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['enabled', 'url', 'typebot', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'url', 'typebot', 'triggerType'),
|
||||
};
|
||||
|
||||
export const typebotStatusSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
status: { type: 'string', enum: ['opened', 'closed', 'paused', 'delete'] },
|
||||
},
|
||||
required: ['remoteJid', 'status'],
|
||||
...isNotEmpty('remoteJid', 'status'),
|
||||
};
|
||||
|
||||
export const typebotStartSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
typebot: { type: 'string' },
|
||||
},
|
||||
required: ['remoteJid', 'url', 'typebot'],
|
||||
...isNotEmpty('remoteJid', 'url', 'typebot'),
|
||||
};
|
||||
|
||||
export const typebotSettingSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
typebotIdFallback: { type: 'string' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'],
|
||||
...isNotEmpty('expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'),
|
||||
};
|
||||
|
||||
export const typebotIgnoreJidSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
remoteJid: { type: 'string' },
|
||||
action: { type: 'string', enum: ['add', 'remove'] },
|
||||
},
|
||||
required: ['remoteJid', 'action'],
|
||||
...isNotEmpty('remoteJid', 'action'),
|
||||
};
|
||||
Reference in New Issue
Block a user