Compare commits

...

28 Commits

Author SHA1 Message Date
Davidson Gomes
1665654676 feat: typebot send list 2024-10-29 19:30:17 -03:00
Davidson Gomes
133eddd742 docker-compose 2024-10-29 18:22:45 -03:00
Davidson Gomes
c55312d206 feat: typebot send list 2024-10-29 17:51:06 -03:00
Davidson Gomes
52216ec08e fix: receive buttons and list response in integrations 2024-10-29 16:11:53 -03:00
Davidson Gomes
7a01cdf0ef fix: receive buttons and list response in integrations 2024-10-29 16:11:29 -03:00
Davidson Gomes
65a9c78d86 feat: typebot send buttons 2024-10-29 16:03:00 -03:00
Davidson Gomes
fbccf2eb2a feat: send pix button 2024-10-29 10:00:32 -03:00
Davidson Gomes
23640a71b8 fix: fetch instances 2024-10-29 07:43:48 -03:00
Davidson Gomes
fce3e55e91 feat: send pix button 2024-10-29 07:36:24 -03:00
Davidson Gomes
9f39ec2110 feat: send ptv message 2024-10-28 18:02:42 -03:00
Davidson Gomes
89c4c194df Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2024-10-28 18:02:24 -03:00
Davidson Gomes
a4e7baa41c feat: send ptv message 2024-10-28 18:02:17 -03:00
Davidson Gomes
e22ff6c0d9 Merge pull request #1009 from yousseefspires/fix/chats-messages
fix: received messages but chat doesnt exists
2024-10-28 17:42:44 -03:00
yousseefs
11d31123ac fix: received messages but chat doesnt exists 2024-10-28 20:23:20 +00:00
Davidson Gomes
0fdc47e8f0 Merge pull request #1001 from robjean9/patch-1
FIX: Update instance.controller.ts to filter by instanceName
2024-10-28 11:13:51 -03:00
Robson Jean Penteado
60db8081bd FIX: Update instance.controller.ts to filter by instanceName
This commit should fix the filter by instanceName
2024-10-24 09:49:43 -03:00
Davidson Gomes
37b003f169 Merge pull request #1000 from fmedeiros95/develop
remove o arquivo public/images/cover (1).png:Zone.Identifier
2024-10-24 09:05:10 -03:00
Felipe Medeiros
891c3eb5d3 remove o arquivo 2024-10-24 08:52:56 -03:00
Davidson Gomes
3b99699f1a fix: var API_AUDIO_CONVERTER with default value 2024-10-22 11:29:43 -03:00
Davidson Gomes
e1de70542b feat: convert audio with api 2024-10-22 09:43:58 -03:00
Davidson Gomes
c10680df41 brand 2024-10-21 18:53:21 -03:00
Davidson Gomes
171f460f3b fix: ignoreJids in integrations dont work 2024-10-21 13:52:08 -03:00
Davidson Gomes
6d0ad5f3db feat: convert audio with api 2024-10-21 12:04:38 -03:00
Davidson Gomes
f34115fdcb feat: convert audio with api 2024-10-21 11:59:45 -03:00
Davidson Gomes
f9705c07dc feat: convert audio with api 2024-10-21 11:59:20 -03:00
Davidson Gomes
e986768716 feat: convert audio with api 2024-10-21 11:39:21 -03:00
Davidson Gomes
34769e2293 fix: receive medias on chatwoot 2024-10-18 19:49:14 -03:00
Davidson Gomes
5401ecd2c4 fix: send buttons cloud api oficial 2024-10-18 19:23:15 -03:00
24 changed files with 973 additions and 332 deletions

View File

@@ -248,6 +248,10 @@ S3_USE_SSL=true
# S3_USE_SSL=true
# S3_REGION=eu-south
# Evolution Audio Converter - Environment variables - https://github.com/EvolutionAPI/evolution-audio-converter
# API_AUDIO_CONVERTER=http://localhost:4040/process-audio
# API_AUDIO_CONVERTER_KEY=429683C4C977415CAAFCCE10F7D57E11
# Define a global apikey to access all instances.
# OBS: This key must be inserted in the request header to create an instance.
AUTHENTICATION_API_KEY=429683C4C977415CAAFCCE10F7D57E11

View File

@@ -8,6 +8,8 @@
* Added unreadMessages to chats
* Pusher event integration
* Add support for splitMessages and timePerChar in Integrations
* Audio Converter via API
* Send PTV messages with Baileys
### Fixed
@@ -19,6 +21,8 @@
* Add indexes to improve performance in Evolution
* Add logical or permanent message deletion based on env config
* Add support for fetching multiple instances by key
* Update instance.controller.ts to filter by instanceName
* Receive template button reply message
# 2.1.2 (2024-10-06 10:09)

View File

@@ -75,10 +75,6 @@ To continuously improve our services, we have implemented telemetry that collect
Join our Evolution Pro community for expert support and a weekly call to answer questions. Visit the link below to learn more and subscribe:
[Click here to learn more](https://evolution-api.com/suporte-pro)
<br>
<a href="https://evolution-api.com/suporte-pro">
<img src="./public/images/evolution-pro.png" alt="Subscribe" width="600">
</a>
# Donate to the project.

View File

@@ -1,8 +1,11 @@
services:
api:
container_name: evolution_api
image: atendai/evolution-api:v2.0.9-rc
image: atendai/evolution-api:homolog
restart: always
depends_on:
- redis
- postgres
ports:
- 8080:8080
volumes:
@@ -14,8 +17,38 @@ services:
expose:
- 8080
redis:
image: redis:latest
networks:
- evolution-net
container_name: redis
command: >
redis-server --port 6379 --appendonly yes
volumes:
- evolution_redis:/data
ports:
- 6379:6379
postgres:
container_name: postgres
image: postgres:15
networks:
- evolution-net
command: ["postgres", "-c", "max_connections=1000"]
restart: always
ports:
- 5432:5432
environment:
- POSTGRES_PASSWORD=PASSWORD
volumes:
- postgres_data:/var/lib/postgresql/data
expose:
- 5432
volumes:
evolution_instances:
evolution_redis:
postgres_data:
networks:

View File

@@ -8,8 +8,8 @@
"build": "tsc --noEmit && tsup",
"start": "tsnd -r tsconfig-paths/register --files --transpile-only ./src/main.ts",
"start:prod": "node dist/main",
"dev:server": "clear && tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
"test": "clear && tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
"dev:server": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
"test": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
"lint": "eslint --fix --ext .ts src",
"db:generate": "node runWithProvider.js \"npx prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
"db:deploy": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate deploy --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
@@ -73,6 +73,7 @@
"jsonschema": "^1.4.1",
"link-preview-js": "^3.0.4",
"long": "^5.2.3",
"mediainfo.js": "^0.3.2",
"mime": "^3.0.0",
"minio": "^8.0.1",
"multer": "^1.4.5-lts.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -382,7 +382,9 @@ export class InstanceController {
return this.waMonitor.instanceInfoById(instanceId, number);
}
return this.waMonitor.instanceInfo();
const instanceNames = instanceName ? [instanceName] : null;
return this.waMonitor.instanceInfo(instanceNames);
}
public async setPresence({ instanceName }: InstanceDto, data: SetPresenceDto) {

View File

@@ -7,6 +7,7 @@ import {
SendLocationDto,
SendMediaDto,
SendPollDto,
SendPtvDto,
SendReactionDto,
SendStatusDto,
SendStickerDto,
@@ -39,6 +40,13 @@ export class SendMessageController {
throw new BadRequestException('Owned media must be a url or base64');
}
public async sendPtv({ instanceName }: InstanceDto, data: SendPtvDto, file?: any) {
if (file || isURL(data?.video) || isBase64(data?.video)) {
return await this.waMonitor.waInstances[instanceName].ptvMessage(data, file);
}
throw new BadRequestException('Owned media must be a url or base64');
}
public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto, file?: any) {
if (file || isURL(data.sticker) || isBase64(data.sticker)) {
return await this.waMonitor.waInstances[instanceName].mediaSticker(data, file);

View File

@@ -70,7 +70,7 @@ export class SendPollDto extends Metadata {
messageSecret?: Uint8Array;
}
export type MediaType = 'image' | 'document' | 'video' | 'audio';
export type MediaType = 'image' | 'document' | 'video' | 'audio' | 'ptv';
export class SendMediaDto extends Metadata {
mediatype: MediaType;
@@ -82,6 +82,10 @@ export class SendMediaDto extends Metadata {
media: string;
}
export class SendPtvDto extends Metadata {
video: string;
}
export class SendStickerDto extends Metadata {
sticker: string;
}
@@ -90,15 +94,21 @@ export class SendAudioDto extends Metadata {
audio: string;
}
export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
export type TypeButton = 'reply' | 'copy' | 'url' | 'call' | 'pix';
export type KeyType = 'phone' | 'email' | 'cpf' | 'cnpj' | 'random';
export class Button {
type: TypeButton;
displayText: string;
displayText?: string;
id?: string;
url?: string;
copyCode?: string;
phoneNumber?: string;
currency?: string;
name?: string;
keyType?: KeyType;
key?: string;
}
export class SendButtonsDto extends Metadata {

View File

@@ -1,6 +1,5 @@
import { NumberBusiness } from '@api/dto/chat.dto';
import {
Button,
ContactMessage,
MediaMessage,
Options,
@@ -13,7 +12,6 @@ import {
SendReactionDto,
SendTemplateDto,
SendTextDto,
TypeButton,
} from '@api/dto/sendMessage.dto';
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
import { ProviderFiles } from '@api/provider/sessions';
@@ -26,14 +24,12 @@ import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@conf
import { BadRequestException, InternalServerErrorException } from '@exceptions';
import { status } from '@utils/renderStatus';
import axios from 'axios';
import { proto } from 'baileys';
import { arrayUnique, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
import FormData from 'form-data';
import { createReadStream } from 'fs';
import mime from 'mime';
import { join } from 'path';
import { v4 } from 'uuid';
export class BusinessStartupService extends ChannelStartupService {
constructor(
@@ -1112,97 +1108,42 @@ export class BusinessStartupService extends ChannelStartupService {
return audioSent;
}
private toJSONString(button: Button): string {
const toString = (obj: any) => JSON.stringify(obj);
const json = {
call: () => toString({ display_text: button.displayText, phone_number: button.phoneNumber }),
reply: () => toString({ display_text: button.displayText, id: button.id }),
copy: () => toString({ display_text: button.displayText, copy_code: button.copyCode }),
url: () =>
toString({
display_text: button.displayText,
url: button.url,
merchant_url: button.url,
}),
};
return json[button.type]?.() || '';
}
private readonly mapType = new Map<TypeButton, string>([
['reply', 'quick_reply'],
['copy', 'cta_copy'],
['url', 'cta_url'],
['call', 'cta_call'],
]);
public async buttonMessage(data: SendButtonsDto) {
const generate = await (async () => {
if (data?.thumbnailUrl) {
return await this.prepareMediaMessage({
mediatype: 'image',
media: data.thumbnailUrl,
});
}
})();
const embeddedMedia: any = {};
const buttons = data.buttons.map((value) => {
return {
name: this.mapType.get(value.type),
buttonParamsJson: this.toJSONString(value),
const btnItems = {
text: data.buttons.map((btn) => btn.displayText),
ids: data.buttons.map((btn) => btn.id),
};
});
const message: proto.IMessage = {
viewOnceMessage: {
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2,
},
interactiveMessage: {
body: {
text: (() => {
let t = '*' + data.title + '*';
if (data?.description) {
t += '\n\n';
t += data.description;
t += '\n';
if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) {
throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.');
}
return t;
})(),
},
footer: {
text: data?.footer,
},
header: (() => {
if (generate?.message?.imageMessage) {
return await this.sendMessageWithTyping(
data.number,
{
text: !embeddedMedia?.mediaKey ? data.title : undefined,
buttons: data.buttons.map((button) => {
return {
hasMediaAttachment: !!generate.message.imageMessage,
imageMessage: generate.message.imageMessage,
type: 'reply',
reply: {
title: button.displayText,
id: button.id,
},
};
}
})(),
nativeFlowMessage: {
buttons: buttons,
messageParamsJson: JSON.stringify({
from: 'api',
templateId: v4(),
}),
[embeddedMedia?.mediaKey]: embeddedMedia?.message,
},
},
},
},
};
return await this.sendMessageWithTyping(data.number, message, {
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
linkPreview: data?.linkPreview,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
});
},
);
}
public async locationMessage(data: SendLocationDto) {

View File

@@ -33,6 +33,7 @@ import { HandleLabelDto, LabelDto } from '@api/dto/label.dto';
import {
Button,
ContactMessage,
KeyType,
MediaMessage,
Options,
SendAudioDto,
@@ -42,6 +43,7 @@ import {
SendLocationDto,
SendMediaDto,
SendPollDto,
SendPtvDto,
SendReactionDto,
SendStatusDto,
SendStickerDto,
@@ -125,6 +127,7 @@ import { isArray, isBase64, isURL } from 'class-validator';
import { randomBytes } from 'crypto';
import EventEmitter2 from 'eventemitter2';
import ffmpeg from 'fluent-ffmpeg';
import FormData from 'form-data';
import { readFileSync } from 'fs';
import Long from 'long';
import mime from 'mime';
@@ -136,11 +139,71 @@ import P from 'pino';
import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
import qrcodeTerminal from 'qrcode-terminal';
import sharp from 'sharp';
import { PassThrough } from 'stream';
import { PassThrough, Readable } from 'stream';
import { v4 } from 'uuid';
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
// Adicione a função getVideoDuration no início do arquivo
async function getVideoDuration(input: Buffer | string | Readable): Promise<number> {
const MediaInfoFactory = (await import('mediainfo.js')).default;
const mediainfo = await MediaInfoFactory({ format: 'JSON' });
let fileSize: number;
let readChunk: (size: number, offset: number) => Promise<Buffer>;
if (Buffer.isBuffer(input)) {
fileSize = input.length;
readChunk = async (size: number, offset: number): Promise<Buffer> => {
return input.slice(offset, offset + size);
};
} else if (typeof input === 'string') {
const fs = await import('fs');
const stat = await fs.promises.stat(input);
fileSize = stat.size;
const fd = await fs.promises.open(input, 'r');
readChunk = async (size: number, offset: number): Promise<Buffer> => {
const buffer = Buffer.alloc(size);
await fd.read(buffer, 0, size, offset);
return buffer;
};
try {
const result = await mediainfo.analyzeData(() => fileSize, readChunk);
const jsonResult = JSON.parse(result);
const generalTrack = jsonResult.media.track.find((t: any) => t['@type'] === 'General');
const duration = generalTrack.Duration;
return Math.round(parseFloat(duration));
} finally {
await fd.close();
}
} else if (input instanceof Readable) {
const chunks: Buffer[] = [];
for await (const chunk of input) {
chunks.push(chunk);
}
const data = Buffer.concat(chunks);
fileSize = data.length;
readChunk = async (size: number, offset: number): Promise<Buffer> => {
return data.slice(offset, offset + size);
};
} else {
throw new Error('Tipo de entrada não suportado');
}
const result = await mediainfo.analyzeData(() => fileSize, readChunk);
const jsonResult = JSON.parse(result);
const generalTrack = jsonResult.media.track.find((t: any) => t['@type'] === 'General');
const duration = generalTrack.Duration;
return Math.round(parseFloat(duration));
}
export class BaileysStartupService extends ChannelStartupService {
constructor(
public readonly configService: ConfigService,
@@ -1070,6 +1133,25 @@ export class BaileysStartupService extends ChannelStartupService {
if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) {
continue;
}
const existingChat = await this.prismaRepository.chat.findFirst({
where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid },
});
if (existingChat) {
const chatToInsert = {
remoteJid: received.key.remoteJid,
instanceId: this.instanceId,
name: received.pushName || '',
unreadMessages: 0,
};
this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
await this.prismaRepository.chat.create({
data: chatToInsert,
});
}
}
const messageRaw = this.prepareMessage(received);
@@ -1079,6 +1161,7 @@ export class BaileysStartupService extends ChannelStartupService {
received?.message?.stickerMessage ||
received?.message?.documentMessage ||
received?.message?.documentWithCaptionMessage ||
received?.message?.ptvMessage ||
received?.message?.audioMessage;
if (this.localSettings.readMessages && received.key.id !== 'status@broadcast') {
@@ -1385,6 +1468,26 @@ export class BaileysStartupService extends ChannelStartupService {
await this.prismaRepository.messageUpdate.create({
data: message,
});
const existingChat = await this.prismaRepository.chat.findFirst({
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
});
if (existingChat) {
const chatToInsert = {
remoteJid: message.remoteJid,
instanceId: this.instanceId,
name: message.pushName || '',
unreadMessages: 0,
};
this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
await this.prismaRepository.chat.create({
data: chatToInsert,
});
}
}
}
}
@@ -1720,7 +1823,8 @@ export class BaileysStartupService extends ChannelStartupService {
website: business?.website?.shift(),
};
} else {
const info: Instance = await waMonitor.instanceInfo([instanceName]);
const instanceNames = instanceName ? [instanceName] : null;
const info: Instance = await waMonitor.instanceInfo(instanceNames);
const business = await this.fetchBusinessProfile(jid);
return {
@@ -2051,8 +2155,10 @@ export class BaileysStartupService extends ChannelStartupService {
messageSent?.message?.imageMessage ||
messageSent?.message?.videoMessage ||
messageSent?.message?.stickerMessage ||
messageSent?.message?.ptvMessage ||
messageSent?.message?.documentMessage ||
messageSent?.message?.documentWithCaptionMessage ||
messageSent?.message?.ptvMessage ||
messageSent?.message?.audioMessage;
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) {
@@ -2396,11 +2502,11 @@ export class BaileysStartupService extends ChannelStartupService {
private async prepareMediaMessage(mediaMessage: MediaMessage) {
try {
const type = mediaMessage.mediatype === 'ptv' ? 'video' : mediaMessage.mediatype;
const prepareMedia = await prepareWAMessageMedia(
{
[mediaMessage.mediatype]: isURL(mediaMessage.media)
? { url: mediaMessage.media }
: Buffer.from(mediaMessage.media, 'base64'),
[type]: isURL(mediaMessage.media) ? { url: mediaMessage.media } : Buffer.from(mediaMessage.media, 'base64'),
} as any,
{ upload: this.client.waUploadToServer },
);
@@ -2452,6 +2558,40 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (mediaMessage.mediatype === 'ptv') {
prepareMedia[mediaType] = prepareMedia[type + 'Message'];
mimetype = 'video/mp4';
if (!prepareMedia[mediaType]) {
throw new Error('Failed to prepare video message');
}
try {
let mediaInput;
if (isURL(mediaMessage.media)) {
mediaInput = mediaMessage.media;
} else {
const mediaBuffer = Buffer.from(mediaMessage.media, 'base64');
if (!mediaBuffer || mediaBuffer.length === 0) {
throw new Error('Invalid media buffer');
}
mediaInput = mediaBuffer;
}
const duration = await getVideoDuration(mediaInput);
if (!duration || duration <= 0) {
throw new Error('Invalid media duration');
}
this.logger.verbose(`Video duration: ${duration} seconds`);
prepareMedia[mediaType].seconds = duration;
} catch (error) {
this.logger.error('Error getting video duration:');
this.logger.error(error);
throw new Error(`Failed to get video duration: ${error.message}`);
}
}
prepareMedia[mediaType].caption = mediaMessage?.caption;
prepareMedia[mediaType].mimetype = mimetype;
prepareMedia[mediaType].fileName = mediaMessage.fileName;
@@ -2563,6 +2703,37 @@ export class BaileysStartupService extends ChannelStartupService {
return mediaSent;
}
public async ptvMessage(data: SendPtvDto, file?: any, isIntegration = false) {
const mediaData: SendMediaDto = {
number: data.number,
media: data.video,
mediatype: 'ptv',
delay: data?.delay,
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
};
if (file) mediaData.media = file.buffer.toString('base64');
const generate = await this.prepareMediaMessage(mediaData);
const mediaSent = await this.sendMessageWithTyping(
data.number,
{ ...generate.message },
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
isIntegration,
);
return mediaSent;
}
public async processAudioMp4(audio: string) {
let inputStream: PassThrough;
@@ -2631,6 +2802,30 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async processAudio(audio: string): Promise<Buffer> {
if (process.env.API_AUDIO_CONVERTER) {
this.logger.verbose('Using audio converter API');
const formData = new FormData();
if (isURL(audio)) {
formData.append('url', audio);
} else {
formData.append('base64', audio);
}
const { data } = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
headers: {
...formData.getHeaders(),
apikey: process.env.API_AUDIO_CONVERTER_KEY,
},
});
if (!data.audio) {
throw new InternalServerErrorException('Failed to convert audio');
}
this.logger.verbose('Audio converted');
return Buffer.from(data.audio, 'base64');
} else {
let inputAudioStream: PassThrough;
if (isURL(audio)) {
@@ -2679,6 +2874,7 @@ export class BaileysStartupService extends ChannelStartupService {
});
});
}
}
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
const mediaData: SendAudioDto = { ...data };
@@ -2727,6 +2923,15 @@ export class BaileysStartupService extends ChannelStartupService {
);
}
private generateRandomId(length = 11) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
private toJSONString(button: Button): string {
const toString = (obj: any) => JSON.stringify(obj);
@@ -2740,6 +2945,49 @@ export class BaileysStartupService extends ChannelStartupService {
url: button.url,
merchant_url: button.url,
}),
pix: () =>
toString({
currency: button.currency,
total_amount: {
value: 0,
offset: 100,
},
reference_id: this.generateRandomId(),
type: 'physical-goods',
order: {
status: 'pending',
subtotal: {
value: 0,
offset: 100,
},
order_type: 'ORDER',
items: [
{
name: '',
amount: {
value: 0,
offset: 100,
},
quantity: 0,
sale_amount: {
value: 0,
offset: 100,
},
},
],
},
payment_settings: [
{
type: 'pix_static_code',
pix_static_code: {
merchant_name: button.name,
key: button.key,
key_type: this.mapKeyType.get(button.keyType),
},
},
],
share_payment_status: false,
}),
};
return json[button.type]?.() || '';
@@ -2750,9 +2998,75 @@ export class BaileysStartupService extends ChannelStartupService {
['copy', 'cta_copy'],
['url', 'cta_url'],
['call', 'cta_call'],
['pix', 'payment_info'],
]);
private readonly mapKeyType = new Map<KeyType, string>([
['phone', 'PHONE'],
['email', 'EMAIL'],
['cpf', 'CPF'],
['cnpj', 'CNPJ'],
['random', 'EVP'],
]);
public async buttonMessage(data: SendButtonsDto) {
if (data.buttons.length === 0) {
throw new BadRequestException('At least one button is required');
}
const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply');
const hasPixButton = data.buttons.some((btn) => btn.type === 'pix');
const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix');
if (hasReplyButtons) {
if (data.buttons.length > 3) {
throw new BadRequestException('Maximum of 3 reply buttons allowed');
}
if (hasOtherButtons) {
throw new BadRequestException('Reply buttons cannot be mixed with other button types');
}
}
if (hasPixButton) {
if (data.buttons.length > 1) {
throw new BadRequestException('Only one PIX button is allowed');
}
if (hasOtherButtons) {
throw new BadRequestException('PIX button cannot be mixed with other button types');
}
const message: proto.IMessage = {
viewOnceMessage: {
message: {
interactiveMessage: {
nativeFlowMessage: {
buttons: [
{
name: this.mapType.get('pix'),
buttonParamsJson: this.toJSONString(data.buttons[0]),
},
],
messageParamsJson: JSON.stringify({
from: 'api',
templateId: v4(),
}),
},
},
},
},
};
return await this.sendMessageWithTyping(data.number, message, {
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
});
}
const generate = await (async () => {
if (data?.thumbnailUrl) {
return await this.prepareMediaMessage({
@@ -2807,8 +3121,6 @@ export class BaileysStartupService extends ChannelStartupService {
},
};
console.log(JSON.stringify(message));
return await this.sendMessageWithTyping(data.number, message, {
delay: data?.delay,
presence: 'composing',

View File

@@ -401,7 +401,6 @@ export class ChatwootService {
return true;
} catch (error) {
this.logger.error(error);
return false;
}
}
@@ -933,11 +932,13 @@ export class ChatwootService {
) {
if (sourceId && this.isImportHistoryAvailable()) {
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]);
if (messageAlreadySaved) {
if (messageAlreadySaved.size > 0) {
this.logger.warn('Message already saved on chatwoot');
return null;
}
}
}
const data = new FormData();
if (content) {
@@ -2442,6 +2443,7 @@ export class ChatwootService {
chatwootConfig: ChatwootDto,
prepareMessage: (message: any) => any,
) {
try {
if (!this.isImportHistoryAvailable()) {
return;
}
@@ -2494,5 +2496,8 @@ export class ChatwootService {
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
const waInstance = this.waMonitor.waInstances[instance.instanceName];
waInstance.clearCacheChatwoot();
} catch (error) {
return;
}
}
}

View File

@@ -170,6 +170,7 @@ class ChatwootImport {
}
public async getExistingSourceIds(sourceIds: string[]): Promise<Set<string>> {
try {
const existingSourceIdsSet = new Set<string>();
if (sourceIds.length === 0) {
@@ -186,6 +187,9 @@ class ChatwootImport {
}
return existingSourceIdsSet;
} catch (error) {
return null;
}
}
public async importHistoryMessages(

View File

@@ -64,17 +64,25 @@ export class DifyController extends ChatbotController implements ChatbotControll
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
@@ -788,15 +796,15 @@ export class DifyController extends ChatbotController implements ChatbotControll
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;

View File

@@ -60,17 +60,25 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
@@ -760,15 +768,15 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;

View File

@@ -60,17 +60,25 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
@@ -760,15 +768,15 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;

View File

@@ -201,18 +201,25 @@ export class OpenaiController extends ChatbotController implements ChatbotContro
},
});
if (!data.openaiCredsId) data.openaiCredsId = defaultSettingCheck?.openaiCredsId || null;
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!data.openaiCredsId) {
throw new Error('Openai Creds Id is required');
@@ -998,15 +1005,15 @@ export class OpenaiController extends ChatbotController implements ChatbotContro
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;

View File

@@ -570,6 +570,8 @@ export class TypebotController extends ChatbotController implements ChatbotContr
let listeningFromMe = data?.typebot?.listeningFromMe;
let stopBotFromMe = data?.typebot?.stopBotFromMe;
let keepOpen = data?.typebot?.keepOpen;
let debounceTime = data?.typebot?.debounceTime;
let ignoreJids = data?.typebot?.ignoreJids;
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
@@ -586,15 +588,20 @@ export class TypebotController extends ChatbotController implements ChatbotContr
!unknownMessage ||
!listeningFromMe ||
!stopBotFromMe ||
!keepOpen
!keepOpen ||
!debounceTime ||
!ignoreJids
) {
if (!expire) expire = defaultSettingCheck?.expire || 0;
if (!keywordFinish) keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR';
if (!delayMessage) delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!unknownMessage) unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi';
if (!listeningFromMe) listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!stopBotFromMe) stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!keepOpen) keepOpen = defaultSettingCheck?.keepOpen || false;
if (expire === undefined || expire === null) expire = defaultSettingCheck.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = defaultSettingCheck.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = defaultSettingCheck.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = defaultSettingCheck.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null)
listeningFromMe = defaultSettingCheck.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = defaultSettingCheck.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = defaultSettingCheck.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = defaultSettingCheck.ignoreJids;
if (!defaultSettingCheck) {
await this.settings(instance, {
@@ -605,6 +612,8 @@ export class TypebotController extends ChatbotController implements ChatbotContr
listeningFromMe: listeningFromMe,
stopBotFromMe: stopBotFromMe,
keepOpen: keepOpen,
debounceTime: debounceTime,
ignoreJids: ignoreJids,
});
}
}
@@ -980,15 +989,15 @@ export class TypebotController extends ChatbotController implements ChatbotContr
let debounceTime = findBot?.debounceTime;
let ignoreJids = findBot?.ignoreJids;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (!debounceTime) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (this.checkIgnoreJids(ignoreJids, remoteJid)) return;

View File

@@ -223,6 +223,121 @@ export class TypebotService {
formattedText = formattedText.replace(/\n$/, '');
if (formattedText.includes('[list]')) {
const listJson = {
number: remoteJid.split('@')[0],
title: '',
description: '',
buttonText: '',
footerText: '',
sections: [],
};
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[buttonText\])/);
const buttonTextMatch = formattedText.match(/\[buttonText\]([\s\S]*?)(?=\[footerText\])/);
const footerTextMatch = formattedText.match(/\[footerText\]([\s\S]*?)(?=\[menu\])/);
if (titleMatch) listJson.title = titleMatch[1].trim();
if (descriptionMatch) listJson.description = descriptionMatch[1].trim();
if (buttonTextMatch) listJson.buttonText = buttonTextMatch[1].trim();
if (footerTextMatch) listJson.footerText = footerTextMatch[1].trim();
const menuContent = formattedText.match(/\[menu\]([\s\S]*?)\[\/menu\]/)?.[1];
if (menuContent) {
const sections = menuContent.match(/\[section\]([\s\S]*?)(?=\[section\]|\[\/section\]|\[\/menu\])/g);
if (sections) {
sections.forEach((section) => {
const sectionTitle = section.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim();
const rows = section.match(/\[row\]([\s\S]*?)(?=\[row\]|\[\/row\]|\[\/section\]|\[\/menu\])/g);
const sectionData = {
title: sectionTitle,
rows:
rows?.map((row) => ({
title: row.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(),
description: row.match(/description: (.*?)(?:\n|$)/)?.[1]?.trim(),
rowId: row.match(/rowId: (.*?)(?:\n|$)/)?.[1]?.trim(),
})) || [],
};
listJson.sections.push(sectionData);
});
}
}
await instance.listMessage(listJson);
} else if (formattedText.includes('[buttons]')) {
const buttonJson = {
number: remoteJid.split('@')[0],
thumbnailUrl: undefined,
title: '',
description: '',
footer: '',
buttons: [],
};
const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/);
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/);
const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/);
if (titleMatch) buttonJson.title = titleMatch[1].trim();
if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim();
if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim();
if (footerMatch) buttonJson.footer = footerMatch[1].trim();
const buttonTypes = {
reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
};
for (const [type, pattern] of Object.entries(buttonTypes)) {
let match;
while ((match = pattern.exec(formattedText)) !== null) {
const content = match[1].trim();
const button: any = { type };
switch (type) {
case 'pix':
button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim();
button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim();
button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim();
button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'reply':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'copy':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'call':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'url':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
}
if (Object.keys(button).length > 1) {
buttonJson.buttons.push(button);
}
}
}
await instance.buttonMessage(buttonJson);
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
@@ -231,6 +346,7 @@ export class TypebotService {
},
false,
);
}
sendTelemetry('/message/sendText');
}
@@ -299,6 +415,121 @@ export class TypebotService {
formattedText = formattedText.replace(/\n$/, '');
if (formattedText.includes('[list]')) {
const listJson = {
number: remoteJid.split('@')[0],
title: '',
description: '',
buttonText: '',
footerText: '',
sections: [],
};
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[buttonText\])/);
const buttonTextMatch = formattedText.match(/\[buttonText\]([\s\S]*?)(?=\[footerText\])/);
const footerTextMatch = formattedText.match(/\[footerText\]([\s\S]*?)(?=\[menu\])/);
if (titleMatch) listJson.title = titleMatch[1].trim();
if (descriptionMatch) listJson.description = descriptionMatch[1].trim();
if (buttonTextMatch) listJson.buttonText = buttonTextMatch[1].trim();
if (footerTextMatch) listJson.footerText = footerTextMatch[1].trim();
const menuContent = formattedText.match(/\[menu\]([\s\S]*?)\[\/menu\]/)?.[1];
if (menuContent) {
const sections = menuContent.match(/\[section\]([\s\S]*?)(?=\[section\]|\[\/section\]|\[\/menu\])/g);
if (sections) {
sections.forEach((section) => {
const sectionTitle = section.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim();
const rows = section.match(/\[row\]([\s\S]*?)(?=\[row\]|\[\/row\]|\[\/section\]|\[\/menu\])/g);
const sectionData = {
title: sectionTitle,
rows:
rows?.map((row) => ({
title: row.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(),
description: row.match(/description: (.*?)(?:\n|$)/)?.[1]?.trim(),
rowId: row.match(/rowId: (.*?)(?:\n|$)/)?.[1]?.trim(),
})) || [],
};
listJson.sections.push(sectionData);
});
}
}
await instance.listMessage(listJson);
} else if (formattedText.includes('[buttons]')) {
const buttonJson = {
number: remoteJid.split('@')[0],
thumbnailUrl: undefined,
title: '',
description: '',
footer: '',
buttons: [],
};
const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/);
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/);
const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/);
if (titleMatch) buttonJson.title = titleMatch[1].trim();
if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim();
if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim();
if (footerMatch) buttonJson.footer = footerMatch[1].trim();
const buttonTypes = {
reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
};
for (const [type, pattern] of Object.entries(buttonTypes)) {
let match;
while ((match = pattern.exec(formattedText)) !== null) {
const content = match[1].trim();
const button: any = { type };
switch (type) {
case 'pix':
button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim();
button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim();
button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim();
button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'reply':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'copy':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'call':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'url':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
}
if (Object.keys(button).length > 1) {
buttonJson.buttons.push(button);
}
}
}
await instance.buttonMessage(buttonJson);
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
@@ -307,6 +538,7 @@ export class TypebotService {
},
false,
);
}
sendTelemetry('/message/sendText');
}

View File

@@ -7,6 +7,7 @@ import {
SendLocationDto,
SendMediaDto,
SendPollDto,
SendPtvDto,
SendReactionDto,
SendStatusDto,
SendStickerDto,
@@ -22,6 +23,7 @@ import {
locationMessageSchema,
mediaMessageSchema,
pollMessageSchema,
ptvMessageSchema,
reactionMessageSchema,
statusMessageSchema,
stickerMessageSchema,
@@ -71,6 +73,18 @@ export class MessageRouter extends RouterBroker {
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendPtv'), ...guards, upload.single('file'), async (req, res) => {
const bodyData = req.body;
const response = await this.dataValidate<SendPtvDto>({
request: req,
schema: ptvMessageSchema,
ClassRef: SendPtvDto,
execute: (instance) => sendMessageController.sendPtv(instance, bodyData, req.file as any),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendWhatsAppAudio'), ...guards, upload.single('file'), async (req, res) => {
const bodyData = req.body;

View File

@@ -60,6 +60,7 @@ export class WAMonitoringService {
}
public async instanceInfo(instanceNames?: string[]): Promise<any> {
if (instanceNames && instanceNames.length > 0) {
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
if (inexistentInstances.length > 0) {
@@ -67,10 +68,11 @@ export class WAMonitoringService {
`Instance${inexistentInstances.length > 1 ? 's' : ''} "${inexistentInstances.join(', ')}" not found`,
);
}
}
const clientName = this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
const where = instanceNames
const where = instanceNames && instanceNames.length > 0
? {
name: {
in: instanceNames,
@@ -123,7 +125,9 @@ export class WAMonitoringService {
throw new NotFoundException(`Instance "${instanceName}" not found`);
}
return this.instanceInfo([instanceName]);
const instanceNames = instanceName ? [instanceName] : null;
return this.instanceInfo(instanceNames);
}
public async cleaningUp(instanceName: string) {

View File

@@ -131,7 +131,7 @@ export declare namespace wa {
export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED';
}
export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage'];
export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage', 'ptvMessage'];
export const MessageSubtype = [
'ephemeralMessage',

View File

@@ -15,8 +15,9 @@ const getTypeMessage = (msg: any) => {
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
listResponseMessage: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
listResponseMessage: msg?.message?.listResponseMessage?.title,
responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
templateButtonReplyMessage: msg?.message?.templateButtonReplyMessage?.selectedId,
// Medias
audioMessage: msg?.message?.speechToText
? msg?.message?.speechToText

View File

@@ -122,6 +122,32 @@ export const mediaMessageSchema: JSONSchema7 = {
required: ['number', 'mediatype'],
};
export const ptvMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { ...numberDefinition },
video: { type: 'string' },
delay: {
type: 'integer',
description: 'Enter a value in milliseconds',
},
quoted: { ...quotedOptionsSchema },
everyOne: { type: 'boolean', enum: [true, false] },
mentioned: {
type: 'array',
minItems: 1,
uniqueItems: true,
items: {
type: 'string',
pattern: '^\\d+',
description: '"mentioned" must be an array of numeric strings',
},
},
},
required: ['number'],
};
export const audioMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
@@ -387,14 +413,18 @@ export const buttonsMessageSchema: JSONSchema7 = {
properties: {
type: {
type: 'string',
enum: ['reply', 'copy', 'url', 'call'],
enum: ['reply', 'copy', 'url', 'call', 'pix'],
},
displayText: { type: 'string' },
id: { type: 'string' },
url: { type: 'string' },
phoneNumber: { type: 'string' },
currency: { type: 'string' },
name: { type: 'string' },
keyType: { type: 'string', enum: ['phone', 'email', 'cpf', 'cnpj', 'random'] },
key: { type: 'string' },
},
required: ['type', 'displayText'],
required: ['type'],
...isNotEmpty('id', 'url', 'phoneNumber'),
},
},