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.
This commit is contained in:
Davidson Gomes 2025-05-21 17:55:00 -03:00
parent f9567fbeaa
commit 9cedf31eed
12 changed files with 268 additions and 80 deletions

View File

@ -3,6 +3,9 @@ SERVER_PORT=8080
# Server URL - Set your application url # Server URL - Set your application url
SERVER_URL=http://localhost:8080 SERVER_URL=http://localhost:8080
SSL_CONF_PRIVKEY=/path/to/cert.key
SSL_CONF_FULLCHAIN=/path/to/cert.crt
SENTRY_DSN= SENTRY_DSN=
# Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com' # Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com'
@ -176,6 +179,15 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
WEBHOOK_EVENTS_ERRORS=false WEBHOOK_EVENTS_ERRORS=false
WEBHOOK_EVENTS_ERRORS_WEBHOOK= WEBHOOK_EVENTS_ERRORS_WEBHOOK=
WEBHOOK_REQUEST_TIMEOUT_MS=60000
WEBHOOK_RETRY_MAX_ATTEMPTS=10
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
WEBHOOK_RETRY_JITTER_FACTOR=0.2
# Comma separated list of HTTP status codes that should not trigger retries
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
# Name that will be displayed on smartphone connection # Name that will be displayed on smartphone connection
CONFIG_SESSION_PHONE_CLIENT=Evolution API CONFIG_SESSION_PHONE_CLIENT=Evolution API
# Browser Name = Chrome | Firefox | Edge | Opera | Safari # Browser Name = Chrome | Firefox | Edge | Opera | Safari

View File

@ -192,17 +192,63 @@ export class BusinessStartupService extends ChannelStartupService {
} }
private messageTextJson(received: any) { private messageTextJson(received: any) {
let content: any; // Verificar que received y received.messages existen
if (!received || !received.messages || received.messages.length === 0) {
this.logger.error('Error: received object or messages array is undefined or empty');
return null;
}
const message = received.messages[0]; const message = received.messages[0];
let content: any;
// Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text
if (!message.text) {
// Si no hay texto, manejamos diferente según el tipo de mensaje
if (message.type === 'sticker') {
content = { stickerMessage: {} };
} else if (message.type === 'location') {
content = {
locationMessage: {
degreesLatitude: message.location?.latitude,
degreesLongitude: message.location?.longitude,
name: message.location?.name,
address: message.location?.address,
},
};
} else {
// Para otros tipos de mensajes sin texto, creamos un contenido genérico
this.logger.log(`Mensaje de tipo ${message.type} sin campo text`);
content = { [message.type + 'Message']: message[message.type] || {} };
}
// Añadir contexto si existe
if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
return content;
}
// Si el mensaje tiene texto, procesamos normalmente
if (!received.metadata || !received.metadata.phone_number_id) {
this.logger.error('Error: metadata or phone_number_id is undefined');
return null;
}
if (message.from === received.metadata.phone_number_id) { if (message.from === received.metadata.phone_number_id) {
content = { content = {
extendedTextMessage: { text: message.text.body }, extendedTextMessage: { text: message.text.body },
}; };
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content; if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
}
} else { } else {
content = { conversation: message.text.body }; content = { conversation: message.text.body };
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content; if (message.context) {
content = { ...content, contextInfo: { stanzaId: message.context.id } };
} }
}
return content; return content;
} }
@ -300,6 +346,9 @@ export class BusinessStartupService extends ChannelStartupService {
case 'location': case 'location':
messageType = 'locationMessage'; messageType = 'locationMessage';
break; break;
case 'sticker':
messageType = 'stickerMessage';
break;
default: default:
messageType = 'conversation'; messageType = 'conversation';
break; break;
@ -316,12 +365,28 @@ export class BusinessStartupService extends ChannelStartupService {
if (received.contacts) pushName = received.contacts[0].profile.name; if (received.contacts) pushName = received.contacts[0].profile.name;
if (received.messages) { if (received.messages) {
const message = received.messages[0]; // Añadir esta línea para definir message
const key = { const key = {
id: received.messages[0].id, id: message.id,
remoteJid: this.phoneNumber, remoteJid: this.phoneNumber,
fromMe: received.messages[0].from === received.metadata.phone_number_id, fromMe: message.from === received.metadata.phone_number_id,
}; };
if (this.isMediaMessage(received?.messages[0])) {
if (message.type === 'sticker') {
this.logger.log('Procesando mensaje de tipo sticker');
messageRaw = {
key,
pushName,
message: {
stickerMessage: message.sticker || {},
},
messageType: 'stickerMessage',
messageTimestamp: parseInt(message.timestamp) as number,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (this.isMediaMessage(message)) {
messageRaw = { messageRaw = {
key, key,
pushName, pushName,
@ -455,17 +520,6 @@ export class BusinessStartupService extends ChannelStartupService {
source: 'unknown', source: 'unknown',
instanceId: this.instanceId, instanceId: this.instanceId,
}; };
} else if (received?.messages[0].location) {
messageRaw = {
key,
pushName,
message: this.messageLocationJson(received),
contextInfo: this.messageLocationJson(received)?.contextInfo,
messageType: this.renderMessageType(received.messages[0].type),
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
source: 'unknown',
instanceId: this.instanceId,
};
} else { } else {
messageRaw = { messageRaw = {
key, key,
@ -501,7 +555,7 @@ export class BusinessStartupService extends ChannelStartupService {
openAiDefaultSettings.speechToText && openAiDefaultSettings.speechToText &&
audioMessage audioMessage
) { ) {
messageRaw.message.speechToText = await this.openaiService.speechToText({ messageRaw.message.speechToText = await this.openaiService.speechToText(openAiDefaultSettings.OpenaiCreds, {
message: { message: {
mediaUrl: messageRaw.message.mediaUrl, mediaUrl: messageRaw.message.mediaUrl,
...messageRaw, ...messageRaw,
@ -535,7 +589,7 @@ export class BusinessStartupService extends ChannelStartupService {
} }
} }
if (!this.isMediaMessage(received?.messages[0])) { if (!this.isMediaMessage(message) && message.type !== 'sticker') {
await this.prismaRepository.message.create({ await this.prismaRepository.message.create({
data: messageRaw, data: messageRaw,
}); });
@ -738,10 +792,48 @@ export class BusinessStartupService extends ChannelStartupService {
} }
protected async eventHandler(content: any) { protected async eventHandler(content: any) {
try {
// Registro para depuración
this.logger.log('Contenido recibido en eventHandler:');
this.logger.log(JSON.stringify(content, null, 2));
const database = this.configService.get<Database>('DATABASE'); const database = this.configService.get<Database>('DATABASE');
const settings = await this.findSettings(); const settings = await this.findSettings();
// Si hay mensajes, verificar primero el tipo
if (content.messages && content.messages.length > 0) {
const message = content.messages[0];
this.logger.log(`Tipo de mensaje recibido: ${message.type}`);
// Verificamos el tipo de mensaje antes de procesarlo
if (
message.type === 'text' ||
message.type === 'image' ||
message.type === 'video' ||
message.type === 'audio' ||
message.type === 'document' ||
message.type === 'sticker' ||
message.type === 'location' ||
message.type === 'contacts' ||
message.type === 'interactive' ||
message.type === 'button' ||
message.type === 'reaction'
) {
// Procesar el mensaje normalmente
this.messageHandle(content, database, settings); this.messageHandle(content, database, settings);
} else {
this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`);
}
} else if (content.statuses) {
// Procesar actualizaciones de estado
this.messageHandle(content, database, settings);
} else {
this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido');
}
} catch (error) {
this.logger.error('Error en eventHandler:');
this.logger.error(error);
}
} }
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) { protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
@ -823,7 +915,6 @@ export class BusinessStartupService extends ChannelStartupService {
} }
if (message['media']) { if (message['media']) {
const isImage = message['mimetype']?.startsWith('image/'); const isImage = message['mimetype']?.startsWith('image/');
const isVideo = message['mimetype']?.startsWith('video/');
content = { content = {
messaging_product: 'whatsapp', messaging_product: 'whatsapp',
@ -833,7 +924,7 @@ export class BusinessStartupService extends ChannelStartupService {
[message['mediaType']]: { [message['mediaType']]: {
[message['type']]: message['id'], [message['type']]: message['id'],
preview_url: Boolean(options?.linkPreview), preview_url: Boolean(options?.linkPreview),
...(message['fileName'] && !isImage && !isVideo && { filename: message['fileName'] }), ...(message['fileName'] && !isImage && { filename: message['fileName'] }),
caption: message['caption'], caption: message['caption'],
}, },
}; };
@ -1001,10 +1092,7 @@ export class BusinessStartupService extends ChannelStartupService {
private async getIdMedia(mediaMessage: any) { private async getIdMedia(mediaMessage: any) {
const formData = new FormData(); const formData = new FormData();
const media = mediaMessage.media || mediaMessage.audio; const fileStream = createReadStream(mediaMessage.media);
if (!media) throw new Error('Media or audio not found');
const fileStream = createReadStream(media);
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype }); formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
formData.append('typeFile', mediaMessage.mimetype); formData.append('typeFile', mediaMessage.mimetype);
@ -1105,7 +1193,7 @@ export class BusinessStartupService extends ChannelStartupService {
const prepareMedia: any = { const prepareMedia: any = {
fileName: `${hash}.mp3`, fileName: `${hash}.mp3`,
mediaType: 'audio', mediaType: 'audio',
audio, media: audio,
}; };
if (isURL(audio)) { if (isURL(audio)) {
@ -1127,7 +1215,15 @@ export class BusinessStartupService extends ChannelStartupService {
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
const mediaData: SendAudioDto = { ...data }; const mediaData: SendAudioDto = { ...data };
if (file) mediaData.audio = file.buffer.toString('base64'); if (file?.buffer) {
mediaData.audio = file.buffer.toString('base64');
} else if (isURL(mediaData.audio)) {
// DO NOTHING
// mediaData.audio = mediaData.audio;
} else {
console.error('El archivo no tiene buffer o file es undefined');
throw new Error('File or buffer is undefined');
}
const message = await this.processAudio(mediaData.audio, data.number); const message = await this.processAudio(mediaData.audio, data.number);

View File

@ -223,7 +223,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
): Promise<void> { ): Promise<void> {
if (!message) return; if (!message) return;
const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g; const linkRegex = /!?\[(.*?)\]\((.*?)\)/g;
let textBuffer = ''; let textBuffer = '';
let lastIndex = 0; let lastIndex = 0;
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
@ -231,7 +231,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
const splitMessages = (settings as any)?.splitMessages ?? false; const splitMessages = (settings as any)?.splitMessages ?? false;
while ((match = linkRegex.exec(message)) !== null) { while ((match = linkRegex.exec(message)) !== null) {
const [, , altText, url] = match; const [fullMatch, altText, url] = match;
const mediaType = this.getMediaType(url); const mediaType = this.getMediaType(url);
const beforeText = message.slice(lastIndex, match.index); const beforeText = message.slice(lastIndex, match.index);
@ -276,7 +276,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
} }
} else { } else {
// It's a regular link, keep it in the text // It's a regular link, keep it in the text
textBuffer += `[${altText}](${url})`; textBuffer += fullMatch;
} }
lastIndex = linkRegex.lastIndex; lastIndex = linkRegex.lastIndex;

View File

@ -183,7 +183,7 @@ class ChatwootImport {
query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)'; query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
} }
if(!conversationId) { if (!conversationId) {
query = 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2'; query = 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2';
} }
@ -508,9 +508,7 @@ class ChatwootImport {
templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText, templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText,
}; };
const typeKey = Object.keys(types).find( const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null);
(key) => types[key] !== undefined && types[key] !== null
);
switch (typeKey) { switch (typeKey) {
case 'documentMessage': { case 'documentMessage': {
const doc = msg.message.documentMessage; const doc = msg.message.documentMessage;
@ -526,10 +524,13 @@ class ChatwootImport {
return `_<File: ${fileName}${caption}>_`; return `_<File: ${fileName}${caption}>_`;
} }
case 'templateMessage': case 'templateMessage': {
const template = msg.message.templateMessage?.hydratedTemplate; const template = msg.message.templateMessage?.hydratedTemplate;
return (template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') + return (
(template?.hydratedContentText || ''); (template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') +
(template?.hydratedContentText || '')
);
}
case 'imageMessage': case 'imageMessage':
return '_<Image Message>_'; return '_<Image Message>_';

View File

@ -1,4 +1,4 @@
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client'; import { $Enums } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';

View File

@ -1,5 +1,3 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class EvoaiDto extends BaseChatbotDto { export class EvoaiDto extends BaseChatbotDto {

View File

@ -86,7 +86,14 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
await instance.client.sendPresenceUpdate('paused', remoteJid); await instance.client.sendPresenceUpdate('paused', remoteJid);
} }
const message = response?.data?.message; let message = response?.data?.message;
if (message && typeof message === 'string' && message.startsWith("'") && message.endsWith("'")) {
const innerContent = message.slice(1, -1);
if (!innerContent.includes("'")) {
message = innerContent;
}
}
if (message) { if (message) {
// Use the base class method to send the message to WhatsApp // Use the base class method to send the message to WhatsApp

View File

@ -1,5 +1,3 @@
import { TriggerOperator, TriggerType } from '@prisma/client';
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
export class OpenaiCredsDto { export class OpenaiCredsDto {

View File

@ -125,6 +125,7 @@ export class WebhookController extends EventController implements EventControlle
const httpService = axios.create({ const httpService = axios.create({
baseURL, baseURL,
headers: webhookHeaders as Record<string, string> | undefined, headers: webhookHeaders as Record<string, string> | undefined,
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
}); });
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl); await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
@ -166,7 +167,10 @@ export class WebhookController extends EventController implements EventControlle
try { try {
if (regex.test(globalURL)) { if (regex.test(globalURL)) {
const httpService = axios.create({ baseURL: globalURL }); const httpService = axios.create({
baseURL: globalURL,
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
});
await this.retryWebhookRequest( await this.retryWebhookRequest(
httpService, httpService,
@ -200,12 +204,20 @@ export class WebhookController extends EventController implements EventControlle
origin: string, origin: string,
baseURL: string, baseURL: string,
serverUrl: string, serverUrl: string,
maxRetries = 10, maxRetries?: number,
delaySeconds = 30, delaySeconds?: number,
): Promise<void> { ): Promise<void> {
const webhookConfig = configService.get<Webhook>('WEBHOOK');
const maxRetryAttempts = maxRetries ?? webhookConfig.RETRY?.MAX_ATTEMPTS ?? 10;
const initialDelay = delaySeconds ?? webhookConfig.RETRY?.INITIAL_DELAY_SECONDS ?? 5;
const useExponentialBackoff = webhookConfig.RETRY?.USE_EXPONENTIAL_BACKOFF ?? true;
const maxDelay = webhookConfig.RETRY?.MAX_DELAY_SECONDS ?? 300;
const jitterFactor = webhookConfig.RETRY?.JITTER_FACTOR ?? 0.2;
const nonRetryableStatusCodes = webhookConfig.RETRY?.NON_RETRYABLE_STATUS_CODES ?? [400, 401, 403, 404, 422];
let attempts = 0; let attempts = 0;
while (attempts < maxRetries) { while (attempts < maxRetryAttempts) {
try { try {
await httpService.post('', webhookData); await httpService.post('', webhookData);
if (attempts > 0) { if (attempts > 0) {
@ -219,12 +231,27 @@ export class WebhookController extends EventController implements EventControlle
} catch (error) { } catch (error) {
attempts++; attempts++;
const isTimeout = error.code === 'ECONNABORTED';
if (error?.response?.status && nonRetryableStatusCodes.includes(error.response.status)) {
this.logger.error({ this.logger.error({
local: `${origin}`, local: `${origin}`,
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`, message: `Erro não recuperável (${error.response.status}): ${error?.message}. Cancelando retentativas.`,
statusCode: error?.response?.status,
url: baseURL,
server_url: serverUrl,
});
throw error;
}
this.logger.error({
local: `${origin}`,
message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`,
hostName: error?.hostname, hostName: error?.hostname,
syscall: error?.syscall, syscall: error?.syscall,
code: error?.code, code: error?.code,
isTimeout,
statusCode: error?.response?.status,
error: error?.errno, error: error?.errno,
stack: error?.stack, stack: error?.stack,
name: error?.name, name: error?.name,
@ -232,11 +259,25 @@ export class WebhookController extends EventController implements EventControlle
server_url: serverUrl, server_url: serverUrl,
}); });
if (attempts === maxRetries) { if (attempts === maxRetryAttempts) {
throw error; throw error;
} }
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)); let nextDelay = initialDelay;
if (useExponentialBackoff) {
nextDelay = Math.min(initialDelay * Math.pow(2, attempts - 1), maxDelay);
const jitter = nextDelay * jitterFactor * (Math.random() * 2 - 1);
nextDelay = Math.max(initialDelay, nextDelay + jitter);
}
this.logger.log({
local: `${origin}`,
message: `Aguardando ${nextDelay.toFixed(1)} segundos antes da próxima tentativa`,
url: baseURL,
});
await new Promise((resolve) => setTimeout(resolve, nextDelay * 1000));
} }
} }
} }

View File

@ -761,36 +761,36 @@ export class ChannelStartupService {
`; `;
if (results && isArray(results) && results.length > 0) { if (results && isArray(results) && results.length > 0) {
const mappedResults = results.map((item) => { const mappedResults = results.map((contact) => {
const lastMessage = item.lastMessageId const lastMessage = contact.lastmessageid
? { ? {
id: item.lastMessageId, id: contact.lastmessageid,
key: item.lastMessage_key, key: contact.lastmessage_key,
pushName: item.lastMessagePushName, pushName: contact.lastmessagepushname,
participant: item.lastMessageParticipant, participant: contact.lastmessageparticipant,
messageType: item.lastMessageMessageType, messageType: contact.lastmessagemessagetype,
message: item.lastMessageMessage, message: contact.lastmessagemessage,
contextInfo: item.lastMessageContextInfo, contextInfo: contact.lastmessagecontextinfo,
source: item.lastMessageSource, source: contact.lastmessagesource,
messageTimestamp: item.lastMessageMessageTimestamp, messageTimestamp: contact.lastmessagemessagetimestamp,
instanceId: item.lastMessageInstanceId, instanceId: contact.lastmessageinstanceid,
sessionId: item.lastMessageSessionId, sessionId: contact.lastmessagesessionid,
status: item.lastMessageStatus, status: contact.lastmessagestatus,
} }
: undefined; : undefined;
return { return {
id: item.contactId || null, id: contact.contactid || null,
remoteJid: item.remoteJid, remoteJid: contact.remotejid,
pushName: item.pushName, pushName: contact.pushname,
profilePicUrl: item.profilePicUrl, profilePicUrl: contact.profilepicurl,
updatedAt: item.updatedAt, updatedAt: contact.updatedat,
windowStart: item.windowStart, windowStart: contact.windowstart,
windowExpires: item.windowExpires, windowExpires: contact.windowexpires,
windowActive: item.windowActive, windowActive: contact.windowactive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined, lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
unreadCount: 0, unreadCount: 0,
isSaved: !!item.contactId, isSaved: !!contact.contactid,
}; };
}); });

View File

@ -232,7 +232,21 @@ export type CacheConfLocal = {
TTL: number; TTL: number;
}; };
export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook }; export type Webhook = {
GLOBAL?: GlobalWebhook;
EVENTS: EventsWebhook;
REQUEST?: {
TIMEOUT_MS?: number;
};
RETRY?: {
MAX_ATTEMPTS?: number;
INITIAL_DELAY_SECONDS?: number;
USE_EXPONENTIAL_BACKOFF?: boolean;
MAX_DELAY_SECONDS?: number;
JITTER_FACTOR?: number;
NON_RETRYABLE_STATUS_CODES?: number[];
};
};
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher }; export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string }; export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
export type QrCode = { LIMIT: number; COLOR: string }; export type QrCode = { LIMIT: number; COLOR: string };
@ -555,6 +569,19 @@ export class ConfigService {
ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true', ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '', ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '',
}, },
REQUEST: {
TIMEOUT_MS: Number.parseInt(process.env?.WEBHOOK_REQUEST_TIMEOUT_MS) || 30000,
},
RETRY: {
MAX_ATTEMPTS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_ATTEMPTS) || 10,
INITIAL_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_INITIAL_DELAY_SECONDS) || 5,
USE_EXPONENTIAL_BACKOFF: process.env?.WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF !== 'false',
MAX_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_DELAY_SECONDS) || 300,
JITTER_FACTOR: Number.parseFloat(process.env?.WEBHOOK_RETRY_JITTER_FACTOR) || 0.2,
NON_RETRYABLE_STATUS_CODES: process.env?.WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES?.split(',').map(Number) || [
400, 401, 403, 404, 422,
],
},
}, },
CONFIG_SESSION_PHONE: { CONFIG_SESSION_PHONE: {
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',

View File

@ -128,7 +128,15 @@ async function bootstrap() {
const httpServer = configService.get<HttpServer>('SERVER'); const httpServer = configService.get<HttpServer>('SERVER');
ServerUP.app = app; ServerUP.app = app;
const server = ServerUP[httpServer.TYPE]; let server = ServerUP[httpServer.TYPE];
if (server === null) {
logger.warn('SSL cert load failed — falling back to HTTP.');
logger.info("Ensure 'SSL_CONF_PRIVKEY' and 'SSL_CONF_FULLCHAIN' env vars point to valid certificate files.");
httpServer.TYPE = 'http';
server = ServerUP[httpServer.TYPE];
}
eventManager.init(server); eventManager.init(server);