diff --git a/.gitmodules b/.gitmodules
index ef5b3e63cf..54a177465b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "evolution-manager-v2"]
path = evolution-manager-v2
- url = https://github.com/EvolutionAPI/evolution-manager-v2.git
+ url = https://github.com/evolution-foundation/evolution-manager-v2.git
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 595600cc05..8aa5df1c7f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,7 +12,7 @@ Harassment, discrimination, or abusive behavior will not be tolerated.
### Reporting Bugs
-1. Check existing [issues](https://github.com/EvolutionAPI/evolution-api/issues)
+1. Check existing [issues](https://github.com/evolution-foundation/evolution-api/issues)
to avoid duplicates
2. Open a new issue with:
- Clear, descriptive title
diff --git a/README.md b/README.md
index d9ea137e14..4586e4028a 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -25,8 +25,6 @@
Support
-
-
---
## About
@@ -39,7 +37,7 @@ Evolution API began as a WhatsApp controller API based on [CodeChat](https://git
## Part of the Evolution Foundation ecosystem
-Evolution API is one of the messaging engines maintained by Evolution Foundation. It is used as a WhatsApp provider by the [Evo CRM Community](https://github.com/EvolutionAPI/evo-crm-community) and other projects in the ecosystem.
+Evolution API is one of the messaging engines maintained by Evolution Foundation. It is used as a WhatsApp provider by the [Evo CRM Community](https://github.com/evolution-foundation/evo-crm-community) and other projects in the ecosystem.
---
@@ -82,7 +80,7 @@ Evolution API integrates natively with many platforms:
### Installation
```bash
-git clone git@github.com:EvolutionAPI/evolution-api.git
+git clone git@github.com:evolution-foundation/evolution-api.git
cd evolution-api
# Install dependencies
@@ -172,6 +170,14 @@ Local storage or S3/MinIO. Automatic media download from WhatsApp. Optional audi
---
+## Hosting
+
+Deploy Evolution API with optimized infrastructure through our HostGator partnership:
+
+[**Evolution API VPS — HostGator**](https://evolution-api.com/vps-evolution-api)
+
+---
+
## Telemetry
Evolution API collects anonymous telemetry data (routes used, most accessed routes, API version) to help improve the service. **No sensitive or personal data is collected.** This information helps us identify improvements and provide a better experience for users.
diff --git a/evolution-manager-v2 b/evolution-manager-v2
index f054b9bc28..3137df4695 160000
--- a/evolution-manager-v2
+++ b/evolution-manager-v2
@@ -1 +1 @@
-Subproject commit f054b9bc28083152d4948f835e3346fd0add39db
+Subproject commit 3137df469504ce211c68e7b35f0706497ac1b95f
diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts
index 6a69106881..a79ae21e61 100644
--- a/src/api/controllers/instance.controller.ts
+++ b/src/api/controllers/instance.controller.ts
@@ -391,6 +391,15 @@ export class InstanceController {
}
public async connectionState({ instanceName }: InstanceDto) {
+ // Cross-check the cached state against the real WebSocket before answering, so a
+ // "zombie open" instance (state cached as 'open' after the socket already dropped)
+ // is reconciled here too, not only on the next periodic health check.
+ try {
+ await this.waMonitor.reconcileInstanceConnection(instanceName);
+ } catch (error) {
+ this.logger.error(error);
+ }
+
return {
instance: {
instanceName: instanceName,
diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
index 906fff1881..408785724f 100644
--- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
+++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts
@@ -1381,7 +1381,12 @@ export class ChatwootService {
if (state !== 'open') {
const number = command.split(':')[1];
- await waInstance.connectToWhatsapp(number);
+ try {
+ await waInstance.connectToWhatsapp(number);
+ } catch (connectError) {
+ this.logger.error(connectError);
+ await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming');
+ }
} else {
await this.createBotMessage(
instance,
@@ -1601,6 +1606,13 @@ export class ChatwootService {
return { message: 'bot' };
} catch (error) {
this.logger.error(error);
+ this.logger.error(error?.stack ?? error);
+
+ try {
+ await this.createBotMessage(instance, i18next.t('cw.inbox.requestError'), 'incoming');
+ } catch (notifyError) {
+ this.logger.error(notifyError);
+ }
return { message: 'bot' };
}
@@ -1948,6 +1960,10 @@ export class ChatwootService {
}
public async eventWhatsapp(event: string, instance: InstanceDto, body: any) {
+ // Tracks whether createBotQr already succeeded, so a later failure in this same
+ // handler (e.g. posting the accompanying text message) doesn't also send a
+ // confusing "QR failed" notification into a conversation that just received the QR.
+ let qrPosted = false;
try {
const waInstance = this.waMonitor.waInstances[instance.instanceName];
@@ -2493,7 +2509,17 @@ export class ChatwootService {
const erroQRcode = `🚨 ${i18next.t('qrlimitreached')}`;
return await this.createBotMessage(instance, erroQRcode, 'incoming');
} else {
- const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64');
+ const qrBase64 = body?.qrcode?.base64;
+
+ if (!qrBase64) {
+ this.logger.error(
+ `qrcode.updated event received without a valid qrcode.base64 payload for instance ${instance?.instanceName}`,
+ );
+ await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming');
+ return;
+ }
+
+ const fileData = Buffer.from(qrBase64.replace('data:image/png;base64,', ''), 'base64');
const fileStream = new Readable();
fileStream._read = () => {};
@@ -2507,6 +2533,7 @@ export class ChatwootService {
fileStream,
`${instance.instanceName}.png`,
);
+ qrPosted = true;
let msgQrCode = `⚡️${i18next.t('qrgeneratedsuccesfully')}\n\n${i18next.t('scanqr')}`;
@@ -2524,6 +2551,15 @@ export class ChatwootService {
}
} catch (error) {
this.logger.error(error);
+ this.logger.error(error?.stack ?? error);
+
+ if (event === 'qrcode.updated' && !qrPosted) {
+ try {
+ await this.createBotMessage(instance, i18next.t('cw.inbox.qrError'), 'incoming');
+ } catch (notifyError) {
+ this.logger.error(notifyError);
+ }
+ }
}
}
diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts
index 438530b57e..23b92c1448 100644
--- a/src/api/services/monitor.service.ts
+++ b/src/api/services/monitor.service.ts
@@ -2,7 +2,7 @@ import { InstanceDto } from '@api/dto/instance.dto';
import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service';
import { channelController } from '@api/server.module';
-import { Events, Integration } from '@api/types/wa.types';
+import { Events, Integration, wa } from '@api/types/wa.types';
import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSession } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
@@ -26,6 +26,7 @@ export class WAMonitoringService {
) {
this.removeInstance();
this.noConnection();
+ this.startConnectionHealthCheck();
Object.assign(this.db, configService.get('DATABASE'));
Object.assign(this.redis, configService.get('CACHE'));
@@ -42,6 +43,12 @@ export class WAMonitoringService {
private readonly providerSession: ProviderSession;
+ // Health check: how often (ms) we probe instances cached as 'open' to confirm the
+ // underlying WebSocket is really still connected (mitigates the "zombie open" state,
+ // since connectionStatus is otherwise 100% event-driven from connection.update).
+ private readonly HEALTH_CHECK_INTERVAL_MS = 30_000;
+ private healthCheckInterval: NodeJS.Timeout;
+
public delInstanceTime(instance: string) {
const time = this.configService.get('DEL_INSTANCE');
if (typeof time === 'number' && time > 0) {
@@ -83,6 +90,87 @@ export class WAMonitoringService {
}
}
+ /**
+ * Starts the periodic reconciliation of cached connection state ("zombie open" mitigation).
+ * connectionStatus.state is otherwise only ever written from inside the Baileys
+ * 'connection.update' handler, so if that event is ever missed (process hiccup, socket
+ * torn down outside Baileys' own reconnection flow, etc.) an instance can stay marked
+ * 'open' in memory/DB forever even though the WebSocket is gone.
+ */
+ private startConnectionHealthCheck() {
+ this.healthCheckInterval = setInterval(() => {
+ this.checkInstancesConnection();
+ }, this.HEALTH_CHECK_INTERVAL_MS);
+ }
+
+ private async checkInstancesConnection() {
+ for (const instanceName of Object.keys(this.waInstances)) {
+ try {
+ await this.reconcileInstanceConnection(instanceName);
+ } catch (error) {
+ // A failure checking one instance must never abort the loop or affect the others.
+ this.logger.error(`Health check failed for instance "${instanceName}": ${error}`);
+ }
+ }
+ }
+
+ /**
+ * Confirms that an instance cached as 'open' still has a real, open WebSocket
+ * (client.ws.isOpen, per Baileys' AbstractSocketClient). If it does not, forces
+ * stateConnection to 'close', persists it via the same Prisma fields used by
+ * BaileysStartupService.connectionUpdate() on a genuine close, and re-emits
+ * CONNECTION_UPDATE through the instance's own sendDataWebhook (no duplicated logic).
+ *
+ * Used both by the periodic health check above and on-demand by
+ * InstanceController.connectionState() before answering a status request.
+ */
+ public async reconcileInstanceConnection(instanceName: string): Promise {
+ const waInstance = this.waInstances[instanceName];
+
+ if (!waInstance || waInstance.connectionStatus?.state !== 'open') {
+ return waInstance?.connectionStatus;
+ }
+
+ const client = waInstance.client;
+
+ // Only instances with an initialized Baileys client expose ws.isOpen; instances
+ // without one (still connecting, non-Baileys channel, etc.) are left untouched.
+ if (!client?.ws || client.ws.isOpen) {
+ return waInstance.connectionStatus;
+ }
+
+ this.logger.warn(
+ `Instance "${instanceName}" is cached as 'open' but its WebSocket is not open (isOpen=false). Reconciling status to 'close'.`,
+ );
+
+ waInstance.stateConnection = { state: 'close', statusReason: 428 };
+
+ try {
+ await this.prismaRepository.instance.update({
+ where: { id: waInstance.instanceId },
+ data: {
+ connectionStatus: 'close',
+ disconnectionAt: new Date(),
+ disconnectionReasonCode: 428,
+ disconnectionObject: JSON.stringify({ reason: 'health-check: websocket not open' }),
+ },
+ });
+ } catch (error) {
+ this.logger.error(`Failed to persist reconciled connection state for "${instanceName}": ${error}`);
+ }
+
+ try {
+ await waInstance.sendDataWebhook?.(Events.CONNECTION_UPDATE, {
+ instance: instanceName,
+ ...waInstance.stateConnection,
+ });
+ } catch (error) {
+ this.logger.error(`Failed to emit CONNECTION_UPDATE for "${instanceName}": ${error}`);
+ }
+
+ return waInstance.connectionStatus;
+ }
+
public async instanceInfo(instanceNames?: string[]): Promise {
if (instanceNames && instanceNames.length > 0) {
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
diff --git a/src/utils/translations/en.json b/src/utils/translations/en.json
index 43dfd8d213..0a31dc1b34 100644
--- a/src/utils/translations/en.json
+++ b/src/utils/translations/en.json
@@ -2,6 +2,8 @@
"qrgeneratedsuccesfully": "QRCode successfully generated!",
"scanqr": "Scan this QR code within the next 40 seconds.",
"qrlimitreached": "QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.",
+ "cw.inbox.qrError": "🚨 Failed to generate the QR Code. Please try again by sending 'init' in this conversation.",
+ "cw.inbox.requestError": "⚠️ An error occurred while processing your request. If you were trying to connect via QR Code, please try again by sending 'init'.",
"cw.inbox.connected": "🚀 Connection successfully established!",
"cw.inbox.disconnect": "🚨 Disconnecting WhatsApp from inbox *{{inboxName}}*.",
"cw.inbox.alreadyConnected": "🚨 {{inboxName}} instance is connected.",
diff --git a/src/utils/translations/es.json b/src/utils/translations/es.json
index be830885ed..1bf0f5a824 100644
--- a/src/utils/translations/es.json
+++ b/src/utils/translations/es.json
@@ -2,6 +2,8 @@
"qrgeneratedsuccesfully": "Código QR generado exitosamente!",
"scanqr": "Escanea este código QR en los próximos 40 segundos.",
"qrlimitreached": "🚨 Se alcanzó el límite de generación de QRCode. Para generar un nuevo QRCode, envíe el mensaje 'init' nuevamente.",
+ "cw.inbox.qrError": "🚨 No se pudo generar el código QR. Por favor, intente nuevamente enviando 'init' en esta conversación.",
+ "cw.inbox.requestError": "⚠️ Ocurrió un error al procesar su solicitud. Si estaba intentando conectar mediante código QR, intente nuevamente enviando 'init'.",
"cw.inbox.connected": "🚀 ¡Conexión establecida exitosamente!",
"cw.inbox.disconnect": "🚨 Instancia *{{inboxName}}* desconectado de Whatsapp.",
"cw.inbox.alreadyConnected": "🚨 La instancia {{inboxName}} está conectada.",
diff --git a/src/utils/translations/pt-BR.json b/src/utils/translations/pt-BR.json
index 31bf468a9b..f55557cf4d 100644
--- a/src/utils/translations/pt-BR.json
+++ b/src/utils/translations/pt-BR.json
@@ -2,6 +2,8 @@
"qrgeneratedsuccesfully": "QRCode gerado com sucesso!",
"scanqr": "Escaneie o QRCode com o WhatsApp nos próximos 40 segundos.",
"qrlimitreached": "Limite de geração de QRCode atingido! Para gerar um novo QRCode, envie o texto 'init' nesta conversa.",
+ "cw.inbox.qrError": "🚨 Falha ao gerar o QR Code. Por favor, tente novamente enviando 'init' nesta conversa.",
+ "cw.inbox.requestError": "⚠️ Ocorreu um erro ao processar sua solicitação. Se você estava tentando conectar via QR Code, tente novamente enviando 'init'.",
"cw.inbox.connected": "🚀 Conectado com sucesso!",
"cw.inbox.disconnect": "🚨 Instância *{{inboxName}}* desconectada do WhatsApp.",
"cw.inbox.alreadyConnected": "🚨 Instância *{{inboxName}}* já está conectada.",