Skip to content

Commit 9cedf31

Browse files
committed
feat(env): enhance webhook configuration and SSL support
- Added new environment variables for SSL configuration, including `SSL_CONF_PRIVKEY` and `SSL_CONF_FULLCHAIN`, to support secure connections. - Introduced additional webhook configuration options in the `.env.example` file, such as `WEBHOOK_REQUEST_TIMEOUT_MS`, `WEBHOOK_RETRY_MAX_ATTEMPTS`, and related retry settings to improve webhook resilience and error handling. - Updated the `bootstrap` function in `main.ts` to handle SSL certificate loading failures gracefully, falling back to HTTP if necessary. - Enhanced error handling and logging in the `BusinessStartupService` to ensure better traceability and robustness when processing messages. This commit focuses on improving the security and reliability of webhook interactions while ensuring that the application can handle SSL configurations effectively.
1 parent f9567fb commit 9cedf31

File tree

12 files changed

+269
-81
lines changed

12 files changed

+269
-81
lines changed

.env.example

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ SERVER_PORT=8080
33
# Server URL - Set your application url
44
SERVER_URL=http://localhost:8080
55

6+
SSL_CONF_PRIVKEY=/path/to/cert.key
7+
SSL_CONF_FULLCHAIN=/path/to/cert.crt
8+
69
SENTRY_DSN=
710

811
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
@@ -176,6 +179,15 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
176179
WEBHOOK_EVENTS_ERRORS=false
177180
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
178181

182+
WEBHOOK_REQUEST_TIMEOUT_MS=60000
183+
WEBHOOK_RETRY_MAX_ATTEMPTS=10
184+
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
185+
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
186+
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
187+
WEBHOOK_RETRY_JITTER_FACTOR=0.2
188+
# Comma separated list of HTTP status codes that should not trigger retries
189+
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
190+
179191
# Name that will be displayed on smartphone connection
180192
CONFIG_SESSION_PHONE_CLIENT=Evolution API
181193
# Browser Name = Chrome | Firefox | Edge | Opera | Safari
@@ -275,4 +287,4 @@ LANGUAGE=en
275287
# PROXY_PORT=80
276288
# PROXY_PROTOCOL=http
277289
# PROXY_USERNAME=
278-
# PROXY_PASSWORD=
290+
# PROXY_PASSWORD=

src/api/integrations/channel/meta/whatsapp.business.service.ts

Lines changed: 127 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -192,17 +192,63 @@ export class BusinessStartupService extends ChannelStartupService {
192192
}
193193

194194
private messageTextJson(received: any) {
195-
let content: any;
195+
// Verificar que received y received.messages existen
196+
if (!received || !received.messages || received.messages.length === 0) {
197+
this.logger.error('Error: received object or messages array is undefined or empty');
198+
return null;
199+
}
200+
196201
const message = received.messages[0];
202+
let content: any;
203+
204+
// Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
205+
if (!message.text) {
206+
// Si no hay texto, manejamos diferente según el tipo de mensaje
207+
if (message.type === 'sticker') {
208+
content = { stickerMessage: {} };
209+
} else if (message.type === 'location') {
210+
content = {
211+
locationMessage: {
212+
degreesLatitude: message.location?.latitude,
213+
degreesLongitude: message.location?.longitude,
214+
name: message.location?.name,
215+
address: message.location?.address,
216+
},
217+
};
218+
} else {
219+
// Para otros tipos de mensajes sin texto, creamos un contenido genérico
220+
this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
221+
content = { [message.type + 'Message']: message[message.type] || {} };
222+
}
223+
224+
// Añadir contexto si existe
225+
if (message.context) {
226+
content = { ...content, contextInfo: { stanzaId: message.context.id } };
227+
}
228+
229+
return content;
230+
}
231+
232+
// Si el mensaje tiene texto, procesamos normalmente
233+
if (!received.metadata || !received.metadata.phone_number_id) {
234+
this.logger.error('Error: metadata or phone_number_id is undefined');
235+
return null;
236+
}
237+
197238
if (message.from === received.metadata.phone_number_id) {
198239
content = {
199240
extendedTextMessage: { text: message.text.body },
200241
};
201-
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
242+
if (message.context) {
243+
content = { ...content, contextInfo: { stanzaId: message.context.id } };
244+
}
202245
} else {
203246
content = { conversation: message.text.body };
204-
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
247+
if (message.context) {
248+
content = { ...content, contextInfo: { stanzaId: message.context.id } };
249+
}
205250
}
251+
206252
return content;
207253
}
208254

@@ -300,6 +346,9 @@ export class BusinessStartupService extends ChannelStartupService {
300346
case 'location':
301347
messageType = 'locationMessage';
302348
break;
349+
case 'sticker':
350+
messageType = 'stickerMessage';
351+
break;
303352
default:
304353
messageType = 'conversation';
305354
break;
@@ -316,12 +365,28 @@ export class BusinessStartupService extends ChannelStartupService {
316365
if (received.contacts) pushName = received.contacts[0].profile.name;
317366

318367
if (received.messages) {
368+
const message = received.messages[0]; // Añadir esta línea para definir message
369+
319370
const key = {
320-
id: received.messages[0].id,
371+
id: message.id,
321372
remoteJid: this.phoneNumber,
322-
fromMe: received.messages[0].from === received.metadata.phone_number_id,
373+
fromMe: message.from === received.metadata.phone_number_id,
323374
};
324-
if (this.isMediaMessage(received?.messages[0])) {
375+
376+
if (message.type === 'sticker') {
377+
this.logger.log('Procesando mensaje de tipo sticker');
378+
messageRaw = {
379+
key,
380+
pushName,
381+
message: {
382+
stickerMessage: message.sticker || {},
383+
},
384+
messageType: 'stickerMessage',
385+
messageTimestamp: parseInt(message.timestamp) as number,
386+
source: 'unknown',
387+
instanceId: this.instanceId,
388+
};
389+
} else if (this.isMediaMessage(message)) {
325390
messageRaw = {
326391
key,
327392
pushName,
@@ -455,17 +520,6 @@ export class BusinessStartupService extends ChannelStartupService {
455520
source: 'unknown',
456521
instanceId: this.instanceId,
457522
};
458-
} else if (received?.messages[0].location) {
459-
messageRaw = {
460-
key,
461-
pushName,
462-
message: this.messageLocationJson(received),
463-
contextInfo: this.messageLocationJson(received)?.contextInfo,
464-
messageType: this.renderMessageType(received.messages[0].type),
465-
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
466-
source: 'unknown',
467-
instanceId: this.instanceId,
468-
};
469523
} else {
470524
messageRaw = {
471525
key,
@@ -501,7 +555,7 @@ export class BusinessStartupService extends ChannelStartupService {
501555
openAiDefaultSettings.speechToText &&
502556
audioMessage
503557
) {
504-
messageRaw.message.speechToText = await this.openaiService.speechToText({
558+
messageRaw.message.speechToText = await this.openaiService.speechToText(openAiDefaultSettings.OpenaiCreds, {
505559
message: {
506560
mediaUrl: messageRaw.message.mediaUrl,
507561
...messageRaw,
@@ -535,7 +589,7 @@ export class BusinessStartupService extends ChannelStartupService {
535589
}
536590
}
537591

538-
if (!this.isMediaMessage(received?.messages[0])) {
592+
if (!this.isMediaMessage(message) && message.type !== 'sticker') {
539593
await this.prismaRepository.message.create({
540594
data: messageRaw,
541595
});
@@ -738,10 +792,48 @@ export class BusinessStartupService extends ChannelStartupService {
738792
}
739793

740794
protected async eventHandler(content: any) {
741-
const database = this.configService.get<Database>('DATABASE');
742-
const settings = await this.findSettings();
743-
744-
this.messageHandle(content, database, settings);
795+
try {
796+
// Registro para depuración
797+
this.logger.log('Contenido recibido en eventHandler:');
798+
this.logger.log(JSON.stringify(content, null, 2));
799+
800+
const database = this.configService.get<Database>('DATABASE');
801+
const settings = await this.findSettings();
802+
803+
// Si hay mensajes, verificar primero el tipo
804+
if (content.messages && content.messages.length > 0) {
805+
const message = content.messages[0];
806+
this.logger.log(`Tipo de mensaje recibido: ${message.type}`);
807+
808+
// Verificamos el tipo de mensaje antes de procesarlo
809+
if (
810+
message.type === 'text' ||
811+
message.type === 'image' ||
812+
message.type === 'video' ||
813+
message.type === 'audio' ||
814+
message.type === 'document' ||
815+
message.type === 'sticker' ||
816+
message.type === 'location' ||
817+
message.type === 'contacts' ||
818+
message.type === 'interactive' ||
819+
message.type === 'button' ||
820+
message.type === 'reaction'
821+
) {
822+
// Procesar el mensaje normalmente
823+
this.messageHandle(content, database, settings);
824+
} else {
825+
this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
826+
}
827+
} else if (content.statuses) {
828+
// Procesar actualizaciones de estado
829+
this.messageHandle(content, database, settings);
830+
} else {
831+
this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
832+
}
833+
} catch (error) {
834+
this.logger.error('Error en eventHandler:');
835+
this.logger.error(error);
836+
}
745837
}
746838

747839
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
@@ -823,7 +915,6 @@ export class BusinessStartupService extends ChannelStartupService {
823915
}
824916
if (message['media']) {
825917
const isImage = message['mimetype']?.startsWith('image/');
826-
const isVideo = message['mimetype']?.startsWith('video/');
827918

828919
content = {
829920
messaging_product: 'whatsapp',
@@ -833,7 +924,7 @@ export class BusinessStartupService extends ChannelStartupService {
833924
[message['mediaType']]: {
834925
[message['type']]: message['id'],
835926
preview_url: Boolean(options?.linkPreview),
836-
...(message['fileName'] && !isImage && !isVideo && { filename: message['fileName'] }),
927+
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
837928
caption: message['caption'],
838929
},
839930
};
@@ -1001,10 +1092,7 @@ export class BusinessStartupService extends ChannelStartupService {
10011092

10021093
private async getIdMedia(mediaMessage: any) {
10031094
const formData = new FormData();
1004-
const media = mediaMessage.media || mediaMessage.audio;
1005-
if (!media) throw new Error('Media or audio not found');
1006-
1007-
const fileStream = createReadStream(media);
1095+
const fileStream = createReadStream(mediaMessage.media);
10081096

10091097
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
10101098
formData.append('typeFile', mediaMessage.mimetype);
@@ -1105,7 +1193,7 @@ export class BusinessStartupService extends ChannelStartupService {
11051193
const prepareMedia: any = {
11061194
fileName: `${hash}.mp3`,
11071195
mediaType: 'audio',
1108-
audio,
1196+
media: audio,
11091197
};
11101198

11111199
if (isURL(audio)) {
@@ -1127,7 +1215,15 @@ export class BusinessStartupService extends ChannelStartupService {
11271215
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
11281216
const mediaData: SendAudioDto = { ...data };
11291217

1130-
if (file) mediaData.audio = file.buffer.toString('base64');
1218+
if (file?.buffer) {
1219+
mediaData.audio = file.buffer.toString('base64');
1220+
} else if (isURL(mediaData.audio)) {
1221+
// DO NOTHING
1222+
// mediaData.audio = mediaData.audio;
1223+
} else {
1224+
console.error('El archivo no tiene buffer o file es undefined');
1225+
throw new Error('File or buffer is undefined');
1226+
}
11311227

11321228
const message = await this.processAudio(mediaData.audio, data.number);
11331229

src/api/integrations/chatbot/base-chatbot.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,15 +223,15 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
223223
): Promise<void> {
224224
if (!message) return;
225225

226-
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g;
226+
const linkRegex = /!?\[(.*?)\]\((.*?)\)/g;
227227
let textBuffer = '';
228228
let lastIndex = 0;
229229
let match: RegExpExecArray | null;
230230

231231
const splitMessages = (settings as any)?.splitMessages ?? false;
232232

233233
while ((match = linkRegex.exec(message)) !== null) {
234-
const [, , altText, url] = match;
234+
const [fullMatch, altText, url] = match;
235235
const mediaType = this.getMediaType(url);
236236
const beforeText = message.slice(lastIndex, match.index);
237237

@@ -276,7 +276,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
276276
}
277277
} else {
278278
// It's a regular link, keep it in the text
279-
textBuffer += `[${altText}](${url})`;
279+
textBuffer += fullMatch;
280280
}
281281

282282
lastIndex = linkRegex.lastIndex;

src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ class ChatwootImport {
180180
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890
181181
let query: string;
182182
if (conversationId) {
183-
query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
183+
query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
184184
}
185185

186-
if(!conversationId) {
186+
if (!conversationId) {
187187
query = 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2';
188188
}
189189

@@ -508,9 +508,7 @@ class ChatwootImport {
508508
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
509509
};
510510

511-
const typeKey = Object.keys(types).find(
512-
(key) => types[key] !== undefined && types[key] !== null
513-
);
511+
const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null);
514512
switch (typeKey) {
515513
case 'documentMessage': {
516514
const doc = msg.message.documentMessage;
@@ -526,10 +524,13 @@ class ChatwootImport {
526524
return `_<File: ${fileName}${caption}>_`;
527525
}
528526

529-
case 'templateMessage':
527+
case 'templateMessage': {
530528
const template = msg.message.templateMessage?.hydratedTemplate;
531-
return (template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
532-
(template?.hydratedContentText || '');
529+
return (
530+
(template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
531+
(template?.hydratedContentText || '')
532+
);
533+
}
533534

534535
case 'imageMessage':
535536
return '_<Image Message>_';

src/api/integrations/chatbot/dify/dto/dify.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
1+
import { $Enums } from '@prisma/client';
22

33
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
44

src/api/integrations/chatbot/evoai/dto/evoai.dto.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { TriggerOperator, TriggerType } from '@prisma/client';
2-
31
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
42

53
export class EvoaiDto extends BaseChatbotDto {

src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,14 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
8686
await instance.client.sendPresenceUpdate('paused', remoteJid);
8787
}
8888

89-
const message = response?.data?.message;
89+
let message = response?.data?.message;
90+
91+
if (message && typeof message === 'string' && message.startsWith("'") && message.endsWith("'")) {
92+
const innerContent = message.slice(1, -1);
93+
if (!innerContent.includes("'")) {
94+
message = innerContent;
95+
}
96+
}
9097

9198
if (message) {
9299
// Use the base class method to send the message to WhatsApp

src/api/integrations/chatbot/openai/dto/openai.dto.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { TriggerOperator, TriggerType } from '@prisma/client';
2-
31
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
42

53
export class OpenaiCredsDto {

0 commit comments

Comments
 (0)