From 9cedf31eeda7e22243202131b81ed2f679b7722f Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 21 May 2025 17:55:00 -0300 Subject: [PATCH] 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. --- .env.example | 14 +- .../channel/meta/whatsapp.business.service.ts | 156 ++++++++++++++---- .../chatbot/base-chatbot.service.ts | 6 +- .../chatwoot/utils/chatwoot-import-helper.ts | 17 +- .../integrations/chatbot/dify/dto/dify.dto.ts | 2 +- .../chatbot/evoai/dto/evoai.dto.ts | 2 - .../services/evolutionBot.service.ts | 9 +- .../chatbot/openai/dto/openai.dto.ts | 2 - .../event/webhook/webhook.controller.ts | 55 +++++- src/api/services/channel.service.ts | 46 +++--- src/config/env.config.ts | 29 +++- src/main.ts | 10 +- 12 files changed, 268 insertions(+), 80 deletions(-) diff --git a/.env.example b/.env.example index b2720d20..055b1008 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,9 @@ SERVER_PORT=8080 # Server URL - Set your application url SERVER_URL=http://localhost:8080 +SSL_CONF_PRIVKEY=/path/to/cert.key +SSL_CONF_FULLCHAIN=/path/to/cert.crt + SENTRY_DSN= # 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_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 CONFIG_SESSION_PHONE_CLIENT=Evolution API # Browser Name = Chrome | Firefox | Edge | Opera | Safari @@ -275,4 +287,4 @@ LANGUAGE=en # PROXY_PORT=80 # PROXY_PROTOCOL=http # PROXY_USERNAME= -# PROXY_PASSWORD= \ No newline at end of file +# PROXY_PASSWORD= diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index bcfd1ce4..9db19e84 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -192,17 +192,63 @@ export class BusinessStartupService extends ChannelStartupService { } 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]; + 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) { content = { 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 { 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; } @@ -300,6 +346,9 @@ export class BusinessStartupService extends ChannelStartupService { case 'location': messageType = 'locationMessage'; break; + case 'sticker': + messageType = 'stickerMessage'; + break; default: messageType = 'conversation'; break; @@ -316,12 +365,28 @@ export class BusinessStartupService extends ChannelStartupService { if (received.contacts) pushName = received.contacts[0].profile.name; if (received.messages) { + const message = received.messages[0]; // Añadir esta línea para definir message + const key = { - id: received.messages[0].id, + id: message.id, 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 = { key, pushName, @@ -455,17 +520,6 @@ export class BusinessStartupService extends ChannelStartupService { source: 'unknown', 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 { messageRaw = { key, @@ -501,7 +555,7 @@ export class BusinessStartupService extends ChannelStartupService { openAiDefaultSettings.speechToText && audioMessage ) { - messageRaw.message.speechToText = await this.openaiService.speechToText({ + messageRaw.message.speechToText = await this.openaiService.speechToText(openAiDefaultSettings.OpenaiCreds, { message: { mediaUrl: messageRaw.message.mediaUrl, ...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({ data: messageRaw, }); @@ -738,10 +792,48 @@ export class BusinessStartupService extends ChannelStartupService { } protected async eventHandler(content: any) { - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); + try { + // Registro para depuración + this.logger.log('Contenido recibido en eventHandler:'); + this.logger.log(JSON.stringify(content, null, 2)); - this.messageHandle(content, database, settings); + const database = this.configService.get('DATABASE'); + 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); + } 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) { @@ -823,7 +915,6 @@ export class BusinessStartupService extends ChannelStartupService { } if (message['media']) { const isImage = message['mimetype']?.startsWith('image/'); - const isVideo = message['mimetype']?.startsWith('video/'); content = { messaging_product: 'whatsapp', @@ -833,7 +924,7 @@ export class BusinessStartupService extends ChannelStartupService { [message['mediaType']]: { [message['type']]: message['id'], preview_url: Boolean(options?.linkPreview), - ...(message['fileName'] && !isImage && !isVideo && { filename: message['fileName'] }), + ...(message['fileName'] && !isImage && { filename: message['fileName'] }), caption: message['caption'], }, }; @@ -1001,10 +1092,7 @@ export class BusinessStartupService extends ChannelStartupService { private async getIdMedia(mediaMessage: any) { const formData = new FormData(); - const media = mediaMessage.media || mediaMessage.audio; - if (!media) throw new Error('Media or audio not found'); - - const fileStream = createReadStream(media); + const fileStream = createReadStream(mediaMessage.media); formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype }); formData.append('typeFile', mediaMessage.mimetype); @@ -1105,7 +1193,7 @@ export class BusinessStartupService extends ChannelStartupService { const prepareMedia: any = { fileName: `${hash}.mp3`, mediaType: 'audio', - audio, + media: audio, }; if (isURL(audio)) { @@ -1127,7 +1215,15 @@ export class BusinessStartupService extends ChannelStartupService { public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { 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); diff --git a/src/api/integrations/chatbot/base-chatbot.service.ts b/src/api/integrations/chatbot/base-chatbot.service.ts index 5931f6fe..3d14ec98 100644 --- a/src/api/integrations/chatbot/base-chatbot.service.ts +++ b/src/api/integrations/chatbot/base-chatbot.service.ts @@ -223,7 +223,7 @@ export abstract class BaseChatbotService { ): Promise { if (!message) return; - const linkRegex = /(!?)\[(.*?)\]\((.*?)\)/g; + const linkRegex = /!?\[(.*?)\]\((.*?)\)/g; let textBuffer = ''; let lastIndex = 0; let match: RegExpExecArray | null; @@ -231,7 +231,7 @@ export abstract class BaseChatbotService { const splitMessages = (settings as any)?.splitMessages ?? false; while ((match = linkRegex.exec(message)) !== null) { - const [, , altText, url] = match; + const [fullMatch, altText, url] = match; const mediaType = this.getMediaType(url); const beforeText = message.slice(lastIndex, match.index); @@ -276,7 +276,7 @@ export abstract class BaseChatbotService { } } else { // It's a regular link, keep it in the text - textBuffer += `[${altText}](${url})`; + textBuffer += fullMatch; } lastIndex = linkRegex.lastIndex; diff --git a/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts b/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts index 4be403e6..147ff936 100644 --- a/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts +++ b/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts @@ -180,10 +180,10 @@ class ChatwootImport { const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890 let query: string; if (conversationId) { - 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'; } @@ -508,9 +508,7 @@ class ChatwootImport { templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText, }; - const typeKey = Object.keys(types).find( - (key) => types[key] !== undefined && types[key] !== null - ); + const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null); switch (typeKey) { case 'documentMessage': { const doc = msg.message.documentMessage; @@ -526,10 +524,13 @@ class ChatwootImport { return `__`; } - case 'templateMessage': + case 'templateMessage': { const template = msg.message.templateMessage?.hydratedTemplate; - return (template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') + - (template?.hydratedContentText || ''); + return ( + (template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') + + (template?.hydratedContentText || '') + ); + } case 'imageMessage': return '__'; diff --git a/src/api/integrations/chatbot/dify/dto/dify.dto.ts b/src/api/integrations/chatbot/dify/dto/dify.dto.ts index ab3bf438..07e7b265 100644 --- a/src/api/integrations/chatbot/dify/dto/dify.dto.ts +++ b/src/api/integrations/chatbot/dify/dto/dify.dto.ts @@ -1,4 +1,4 @@ -import { $Enums, TriggerOperator, TriggerType } from '@prisma/client'; +import { $Enums } from '@prisma/client'; import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; diff --git a/src/api/integrations/chatbot/evoai/dto/evoai.dto.ts b/src/api/integrations/chatbot/evoai/dto/evoai.dto.ts index 342a8863..735df3d2 100644 --- a/src/api/integrations/chatbot/evoai/dto/evoai.dto.ts +++ b/src/api/integrations/chatbot/evoai/dto/evoai.dto.ts @@ -1,5 +1,3 @@ -import { TriggerOperator, TriggerType } from '@prisma/client'; - import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto'; export class EvoaiDto extends BaseChatbotDto { diff --git a/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts b/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts index 58c53441..0a048a1a 100644 --- a/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts +++ b/src/api/integrations/chatbot/evolutionBot/services/evolutionBot.service.ts @@ -86,7 +86,14 @@ export class EvolutionBotService extends BaseChatbotService | undefined, + timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000, }); await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl); @@ -166,7 +167,10 @@ export class WebhookController extends EventController implements EventControlle try { 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( httpService, @@ -200,12 +204,20 @@ export class WebhookController extends EventController implements EventControlle origin: string, baseURL: string, serverUrl: string, - maxRetries = 10, - delaySeconds = 30, + maxRetries?: number, + delaySeconds?: number, ): Promise { + const webhookConfig = configService.get('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; - while (attempts < maxRetries) { + while (attempts < maxRetryAttempts) { try { await httpService.post('', webhookData); if (attempts > 0) { @@ -219,12 +231,27 @@ export class WebhookController extends EventController implements EventControlle } catch (error) { attempts++; + const isTimeout = error.code === 'ECONNABORTED'; + + if (error?.response?.status && nonRetryableStatusCodes.includes(error.response.status)) { + this.logger.error({ + local: `${origin}`, + 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}/${maxRetries} falhou: ${error?.message}`, + message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, + isTimeout, + statusCode: error?.response?.status, error: error?.errno, stack: error?.stack, name: error?.name, @@ -232,11 +259,25 @@ export class WebhookController extends EventController implements EventControlle server_url: serverUrl, }); - if (attempts === maxRetries) { + if (attempts === maxRetryAttempts) { 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)); } } } diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index ee8e83b2..2dc97f70 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -761,36 +761,36 @@ export class ChannelStartupService { `; if (results && isArray(results) && results.length > 0) { - const mappedResults = results.map((item) => { - const lastMessage = item.lastMessageId + const mappedResults = results.map((contact) => { + const lastMessage = contact.lastmessageid ? { - id: item.lastMessageId, - key: item.lastMessage_key, - pushName: item.lastMessagePushName, - participant: item.lastMessageParticipant, - messageType: item.lastMessageMessageType, - message: item.lastMessageMessage, - contextInfo: item.lastMessageContextInfo, - source: item.lastMessageSource, - messageTimestamp: item.lastMessageMessageTimestamp, - instanceId: item.lastMessageInstanceId, - sessionId: item.lastMessageSessionId, - status: item.lastMessageStatus, + id: contact.lastmessageid, + key: contact.lastmessage_key, + pushName: contact.lastmessagepushname, + participant: contact.lastmessageparticipant, + messageType: contact.lastmessagemessagetype, + message: contact.lastmessagemessage, + contextInfo: contact.lastmessagecontextinfo, + source: contact.lastmessagesource, + messageTimestamp: contact.lastmessagemessagetimestamp, + instanceId: contact.lastmessageinstanceid, + sessionId: contact.lastmessagesessionid, + status: contact.lastmessagestatus, } : undefined; return { - id: item.contactId || null, - remoteJid: item.remoteJid, - pushName: item.pushName, - profilePicUrl: item.profilePicUrl, - updatedAt: item.updatedAt, - windowStart: item.windowStart, - windowExpires: item.windowExpires, - windowActive: item.windowActive, + id: contact.contactid || null, + remoteJid: contact.remotejid, + pushName: contact.pushname, + profilePicUrl: contact.profilepicurl, + updatedAt: contact.updatedat, + windowStart: contact.windowstart, + windowExpires: contact.windowexpires, + windowActive: contact.windowactive, lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined, unreadCount: 0, - isSaved: !!item.contactId, + isSaved: !!contact.contactid, }; }); diff --git a/src/config/env.config.ts b/src/config/env.config.ts index b9dd74ef..57a84472 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -232,7 +232,21 @@ export type CacheConfLocal = { TTL: number; }; 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 ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string }; export type QrCode = { LIMIT: number; COLOR: string }; @@ -555,6 +569,19 @@ export class ConfigService { ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true', 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: { CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', diff --git a/src/main.ts b/src/main.ts index 938d51d2..cf787f32 100644 --- a/src/main.ts +++ b/src/main.ts @@ -128,7 +128,15 @@ async function bootstrap() { const httpServer = configService.get('SERVER'); 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);