mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-14 01:41:24 -06:00
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:
parent
f9567fbeaa
commit
9cedf31eed
14
.env.example
14
.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=
|
||||
# PROXY_PASSWORD=
|
||||
|
@ -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>('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>('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);
|
||||
|
||||
|
@ -223,7 +223,7 @@ export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
||||
): Promise<void> {
|
||||
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<BotType = any, SettingsType = any> {
|
||||
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<BotType = any, SettingsType = any> {
|
||||
}
|
||||
} else {
|
||||
// It's a regular link, keep it in the text
|
||||
textBuffer += `[${altText}](${url})`;
|
||||
textBuffer += fullMatch;
|
||||
}
|
||||
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
|
@ -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 `_<File: ${fileName}${caption}>_`;
|
||||
}
|
||||
|
||||
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 '_<Image Message>_';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { $Enums, TriggerOperator, TriggerType } from '@prisma/client';
|
||||
import { $Enums } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class EvoaiDto extends BaseChatbotDto {
|
||||
|
@ -86,7 +86,14 @@ export class EvolutionBotService extends BaseChatbotService<EvolutionBot, Evolut
|
||||
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) {
|
||||
// Use the base class method to send the message to WhatsApp
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
import { BaseChatbotDto, BaseChatbotSettingDto } from '../../base-chatbot.dto';
|
||||
|
||||
export class OpenaiCredsDto {
|
||||
|
@ -125,6 +125,7 @@ export class WebhookController extends EventController implements EventControlle
|
||||
const httpService = axios.create({
|
||||
baseURL,
|
||||
headers: webhookHeaders as Record<string, string> | 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<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;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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',
|
||||
|
10
src/main.ts
10
src/main.ts
@ -128,7 +128,15 @@ async function bootstrap() {
|
||||
const httpServer = configService.get<HttpServer>('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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user