@omnizap-system/omnizap 2.6.1 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +78 -9
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/security-runner-hardening.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +1 -0
- package/app/config/index.js +6 -0
- package/app/configParts/adminIdentity.js +36 -7
- package/app/configParts/baileysConfig.js +343 -56
- package/app/configParts/groupUtils.js +226 -0
- package/app/configParts/loggerConfig.js +185 -0
- package/app/configParts/messagePersistenceService.js +307 -5
- package/app/configParts/sessionConfig.js +242 -0
- package/app/connection/baileysCompatibility.test.js +10 -1
- package/app/connection/baileysDbAuthState.js +205 -9
- package/app/connection/baileysLibsignalPatch.js +210 -0
- package/app/connection/groupOwnerWriteStateResolver.js +141 -0
- package/app/connection/socketController.js +694 -123
- package/app/connection/socketController.multiSession.test.js +128 -0
- package/app/controllers/messageController.js +1 -1
- package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
- package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
- package/app/controllers/messageProcessingPipeline.js +90 -9
- package/app/controllers/messageProcessingPipeline.test.js +202 -0
- package/app/modules/adminModule/AGENT.md +1 -1
- package/app/modules/adminModule/commandConfig.json +3318 -1347
- package/app/modules/adminModule/groupCommandHandlers.js +856 -14
- package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
- package/app/modules/adminModule/groupWarningRepository.js +152 -0
- package/app/modules/aiModule/AGENT.md +47 -30
- package/app/modules/aiModule/aiConfigRuntime.js +1 -0
- package/app/modules/aiModule/catCommand.js +132 -25
- package/app/modules/aiModule/commandConfig.json +114 -28
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
- package/app/modules/gameModule/AGENT.md +1 -1
- package/app/modules/gameModule/commandConfig.json +29 -0
- package/app/modules/menuModule/AGENT.md +1 -1
- package/app/modules/menuModule/commandConfig.json +45 -10
- package/app/modules/menuModule/menuCatalogService.js +190 -0
- package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
- package/app/modules/menuModule/menuDynamicService.js +511 -0
- package/app/modules/menuModule/menuDynamicService.test.js +141 -0
- package/app/modules/menuModule/menus.js +36 -5
- package/app/modules/playModule/AGENT.md +10 -5
- package/app/modules/playModule/commandConfig.json +74 -16
- package/app/modules/playModule/playCommandConstants.js +13 -7
- package/app/modules/playModule/playCommandCore.js +4 -6
- package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
- package/app/modules/playModule/playConfigRuntime.js +5 -6
- package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerPackModule/AGENT.md +1 -1
- package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
- package/app/modules/stickerPackModule/commandConfig.json +29 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/geminiService.js +131 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/moduleAiHelpCoreService.js +33 -4
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +51 -21
- package/app/services/messaging/newsBroadcastService.js +843 -27
- package/app/services/multiSession/assignmentBalancerService.js +452 -0
- package/app/services/multiSession/groupOwnershipRepository.js +346 -0
- package/app/services/multiSession/groupOwnershipService.js +809 -0
- package/app/services/multiSession/groupOwnershipService.test.js +317 -0
- package/app/services/multiSession/sessionRegistryService.js +239 -0
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/groupConfigStore.js +41 -5
- package/app/store/premiumUserStore.js +21 -7
- package/app/utils/antiLink/antiLinkModule.js +391 -25
- package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
- package/database/index.js +6 -0
- package/database/migrations/20260307_d0_hardening_down.sql +1 -1
- package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
- package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
- package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
- package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
- package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
- package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
- package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
- package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
- package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
- package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
- package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
- package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
- package/database/schema.sql +102 -1
- package/docker-compose.yml +4 -1
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
- package/docs/security/omnizap-static-security-headers.conf +25 -0
- package/ecosystem.prod.config.cjs +31 -11
- package/index.js +52 -18
- package/observability/alert-rules.yml +20 -0
- package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
- package/observability/mysql-setup.sql +4 -4
- package/observability/system-admin-observability.md +26 -0
- package/package.json +14 -6
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/css/payments-react.css +478 -0
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/homeReactApp.js +2 -2
- package/public/js/apps/paymentsCancelReactApp.js +45 -0
- package/public/js/apps/paymentsReactApp.js +399 -0
- package/public/js/apps/paymentsSuccessReactApp.js +148 -0
- package/public/js/apps/stickersApp.js +255 -103
- package/public/js/apps/termsReactApp.js +57 -8
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +96 -47
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/pagamentos-cancelado.html +21 -0
- package/public/pages/pagamentos-sucesso.html +21 -0
- package/public/pages/pagamentos.html +30 -0
- package/public/pages/politica-de-privacidade.html +1 -1
- package/public/pages/stickers.html +5 -5
- package/public/pages/termos-de-uso-texto-integral.html +1 -1
- package/public/pages/termos-de-uso.html +1 -1
- package/public/pages/user-password-reset.html +3 -4
- package/public/pages/user-systemadm.html +8 -462
- package/public/pages/user.html +1 -1
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +13 -0
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +155 -0
- package/scripts/new-whatsapp-session.sh +564 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +254 -0
- package/server/controllers/payments/paymentsController.js +731 -0
- package/server/controllers/sticker/stickerCatalogController.js +9 -23
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
- package/server/controllers/system/systemController.js +228 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailAutomationRuntime.js +36 -1
- package/server/email/emailAutomationService.js +42 -1
- package/server/email/emailTemplateService.js +140 -33
- package/server/http/httpRequestUtils.js +18 -14
- package/server/http/httpServer.js +8 -4
- package/server/middleware/securityHeaders.js +35 -3
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +50 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/payments/paymentsRouter.js +47 -0
- package/server/routes/static/staticPageRouter.js +30 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +5 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -2,7 +2,7 @@ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } fro
|
|
|
2
2
|
import makeWASocket, { DisconnectReason, Browsers, getAggregateVotesInPollMessage, areJidsSameUser, WAMessageStatus, WAMessageStubType, delayCancellable, getStatusFromReceiptType, promiseTimeout } from '@whiskeysockets/baileys';
|
|
3
3
|
|
|
4
4
|
import NodeCache from 'node-cache';
|
|
5
|
-
import { parseEnvBool, parseEnvCsv, parseEnvInt, resolveBaileysVersion, resolveAddressingModeFromMessageKey, normalizeAddressingMode, normalizePnToJid, normalizeWAPresence, baileysConnectionLogger as logger, baileysSocketLogger } from '../config/index.js';
|
|
5
|
+
import { parseEnvBool, parseEnvCsv, parseEnvInt, resolveBaileysVersion, resolveAddressingModeFromMessageKey, normalizeAddressingMode, normalizeJid, normalizePnToJid, normalizeWAPresence, isGroupJid, getMultiSessionRuntimeConfig, baileysConnectionLogger as logger, baileysSocketLogger } from '../config/index.js';
|
|
6
6
|
|
|
7
7
|
import { Boom } from '@hapi/boom';
|
|
8
8
|
import qrcode from 'qrcode-terminal';
|
|
@@ -16,18 +16,24 @@ import { resolveCaptchaByReaction } from '../services/messaging/captchaService.j
|
|
|
16
16
|
|
|
17
17
|
import { handleGroupUpdate as handleGroupParticipantsEvent, handleGroupJoinRequest } from '../modules/adminModule/groupEventHandlers.js';
|
|
18
18
|
|
|
19
|
-
import { dbConfig, executeQuery, findBy, findById, pool, remove } from '../../database/index.js';
|
|
19
|
+
import { dbConfig, executeQuery, findBy, findById, pool, remove, TABLES } from '../../database/index.js';
|
|
20
20
|
import { extractSenderInfoFromMessage, primeLidCache, resolveUserIdCached, isLidUserId, isWhatsAppUserId } from '../config/index.js';
|
|
21
21
|
import { queueBaileysEventInsert, queueChatUpdate, queueLidUpdate, queueMessageInsert } from '../services/infra/dbWriteQueue.js';
|
|
22
22
|
import { buildGroupMetadataFromGroup, buildGroupMetadataFromUpdate, upsertGroupMetadata, parseParticipantsFromDb } from '../services/group/groupMetadataService.js';
|
|
23
23
|
import { buildMessageData } from '../configParts/messagePersistenceService.js';
|
|
24
|
+
import { getOwner as getGroupOwner, tryAcquire as tryAcquireGroupOwner, heartbeatOwnerSession as heartbeatGroupOwnerSession } from '../services/multiSession/groupOwnershipService.js';
|
|
25
|
+
import sessionRegistryService from '../services/multiSession/sessionRegistryService.js';
|
|
26
|
+
import { createGroupOwnerWriteStateResolver, normalizeAssignmentVersion } from './groupOwnerWriteStateResolver.js';
|
|
24
27
|
import { useDbAuthState } from './baileysDbAuthState.js';
|
|
28
|
+
import { applyLibsignalRuntimePatch } from './baileysLibsignalPatch.js';
|
|
25
29
|
|
|
26
30
|
import { fileURLToPath } from 'node:url';
|
|
27
31
|
|
|
28
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
33
|
const __dirname = path.dirname(__filename);
|
|
30
34
|
|
|
35
|
+
applyLibsignalRuntimePatch();
|
|
36
|
+
|
|
31
37
|
/**
|
|
32
38
|
* Indica se o ambiente de execução é de produção.
|
|
33
39
|
* @type {boolean}
|
|
@@ -132,14 +138,31 @@ const BAILEYS_GROUP_METADATA_CACHE_TTL_SECONDS = parseEnvInt(process.env.BAILEYS
|
|
|
132
138
|
*/
|
|
133
139
|
const BAILEYS_GROUP_METADATA_CACHE_CHECKPERIOD_SECONDS = parseEnvInt(process.env.BAILEYS_GROUP_METADATA_CACHE_CHECKPERIOD_SECONDS, 60, 10, 1800);
|
|
134
140
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
141
|
+
* Configuração de runtime para múltiplas sessões do Baileys.
|
|
142
|
+
* @type {{
|
|
143
|
+
* sessionIds?: string[],
|
|
144
|
+
* primarySessionId?: string,
|
|
145
|
+
* ownerLeaseMs?: number,
|
|
146
|
+
* ownerHeartbeatMs?: number,
|
|
147
|
+
* sessionWeights?: Record<string, number>
|
|
148
|
+
* }}
|
|
149
|
+
*/
|
|
150
|
+
const MULTI_SESSION_RUNTIME_CONFIG = getMultiSessionRuntimeConfig();
|
|
151
|
+
/**
|
|
152
|
+
* Lista imutável de IDs de sessão habilitadas.
|
|
153
|
+
* @type {readonly string[]}
|
|
154
|
+
*/
|
|
155
|
+
const BAILEYS_SESSION_IDS = Object.freeze(Array.isArray(MULTI_SESSION_RUNTIME_CONFIG?.sessionIds) && MULTI_SESSION_RUNTIME_CONFIG.sessionIds.length > 0 ? [...MULTI_SESSION_RUNTIME_CONFIG.sessionIds] : [String(process.env.BAILEYS_AUTH_SESSION_ID || 'default').trim() || 'default']);
|
|
156
|
+
/**
|
|
157
|
+
* Conjunto para consultas rápidas de sessão válida.
|
|
158
|
+
* @type {Set<string>}
|
|
159
|
+
*/
|
|
160
|
+
const BAILEYS_SESSION_ID_SET = new Set(BAILEYS_SESSION_IDS);
|
|
161
|
+
/**
|
|
162
|
+
* ID de sessão principal usado como fallback.
|
|
137
163
|
* @type {string}
|
|
138
164
|
*/
|
|
139
|
-
const
|
|
140
|
-
const raw = String(process.env.BAILEYS_AUTH_SESSION_ID || '').trim();
|
|
141
|
-
return raw || 'default';
|
|
142
|
-
})();
|
|
165
|
+
const BAILEYS_PRIMARY_SESSION_ID = String(MULTI_SESSION_RUNTIME_CONFIG?.primarySessionId || BAILEYS_SESSION_IDS[0] || 'default').trim() || 'default';
|
|
143
166
|
/**
|
|
144
167
|
* Habilita bootstrap inicial do auth state no MySQL usando os arquivos locais legados.
|
|
145
168
|
* @type {boolean}
|
|
@@ -164,12 +187,111 @@ const BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS = parseEnvInt(process.env.BAILEY
|
|
|
164
187
|
* Nome do lock de escritor único usado no MySQL.
|
|
165
188
|
* @type {string}
|
|
166
189
|
*/
|
|
167
|
-
const
|
|
190
|
+
const BAILEYS_SINGLE_WRITER_LOCK_NAME_BASE = (() => {
|
|
168
191
|
const raw = String(process.env.BAILEYS_SINGLE_WRITER_LOCK_NAME || '').trim();
|
|
169
192
|
if (raw) return raw;
|
|
170
193
|
const dbLabel = String(dbConfig?.database || 'db').replace(/[^a-zA-Z0-9:_-]+/g, '_');
|
|
171
194
|
return `omnizap:baileys:writer:${dbLabel}`;
|
|
172
195
|
})();
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Normaliza um ID de sessão, garantindo fallback para a sessão principal.
|
|
199
|
+
* @param {string | null | undefined} sessionId
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
const normalizeSessionId = (sessionId) => {
|
|
203
|
+
const normalized = String(sessionId || '').trim();
|
|
204
|
+
if (!normalized) return BAILEYS_PRIMARY_SESSION_ID;
|
|
205
|
+
if (!BAILEYS_SESSION_ID_SET.has(normalized)) return BAILEYS_PRIMARY_SESSION_ID;
|
|
206
|
+
return normalized;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolve o nome final do lock de escritor único para uma sessão.
|
|
211
|
+
* @param {string | null | undefined} sessionId
|
|
212
|
+
* @returns {string}
|
|
213
|
+
*/
|
|
214
|
+
const getWriterLockNameBySession = (sessionId) => {
|
|
215
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
216
|
+
const base = BAILEYS_SINGLE_WRITER_LOCK_NAME_BASE;
|
|
217
|
+
if (base.includes('{sessionId}')) {
|
|
218
|
+
return base.replace(/\{sessionId\}/g, safeSessionId);
|
|
219
|
+
}
|
|
220
|
+
return `${base}:${safeSessionId}`;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* TTL do cache local de ownership de grupo (em milissegundos).
|
|
225
|
+
* @type {number}
|
|
226
|
+
*/
|
|
227
|
+
const GROUP_OWNER_WRITE_CACHE_TTL_MS = parseEnvInt(process.env.GROUP_OWNER_WRITE_CACHE_TTL_MS, Math.max(2_000, Math.floor((Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerHeartbeatMs) || 30_000) / 3)), 1_000, 60_000);
|
|
228
|
+
/**
|
|
229
|
+
* Permite tentar reivindicar ownership no miss de cache.
|
|
230
|
+
* @type {boolean}
|
|
231
|
+
*/
|
|
232
|
+
const GROUP_OWNER_WRITE_CLAIM_ON_MISS = parseEnvBool(process.env.GROUP_OWNER_WRITE_CLAIM_ON_MISS, true);
|
|
233
|
+
/**
|
|
234
|
+
* Duração da lease de ownership por sessão (em milissegundos).
|
|
235
|
+
* @type {number}
|
|
236
|
+
*/
|
|
237
|
+
const GROUP_OWNER_LEASE_MS = Math.max(5_000, Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerLeaseMs) || 120_000);
|
|
238
|
+
/**
|
|
239
|
+
* Intervalo de heartbeat de ownership (em milissegundos).
|
|
240
|
+
* @type {number}
|
|
241
|
+
*/
|
|
242
|
+
let GROUP_OWNER_HEARTBEAT_MS = parseEnvInt(process.env.GROUP_OWNER_HEARTBEAT_RUNTIME_MS, Math.max(1_000, Math.min(GROUP_OWNER_LEASE_MS - 500, Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerHeartbeatMs) || 30_000)), 1_000, 5 * 60 * 1000);
|
|
243
|
+
if (GROUP_OWNER_HEARTBEAT_MS >= GROUP_OWNER_LEASE_MS) {
|
|
244
|
+
GROUP_OWNER_HEARTBEAT_MS = Math.max(1_000, Math.floor(GROUP_OWNER_LEASE_MS / 2));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Cache local de decisões de escrita por ownership de grupo.
|
|
248
|
+
* @type {NodeCache}
|
|
249
|
+
*/
|
|
250
|
+
const groupOwnerWriteStateCache = new NodeCache({
|
|
251
|
+
stdTTL: Math.max(1, Math.ceil(GROUP_OWNER_WRITE_CACHE_TTL_MS / 1000)),
|
|
252
|
+
checkperiod: Math.max(1, Math.ceil(GROUP_OWNER_WRITE_CACHE_TTL_MS / 1000)),
|
|
253
|
+
useClones: false,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Monta a chave de cache de ownership para uma combinação sessão+grupo.
|
|
258
|
+
* @param {string | null | undefined} groupJid
|
|
259
|
+
* @param {string | null | undefined} sessionId
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
const buildGroupOwnerWriteCacheKey = (groupJid, sessionId) => {
|
|
263
|
+
const safeGroupJid = String(groupJid || '').trim();
|
|
264
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
265
|
+
if (!safeGroupJid || !safeSessionId) return '';
|
|
266
|
+
return `${safeSessionId}:${safeGroupJid}`;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Remove do cache todas as entradas de ownership da sessão informada.
|
|
271
|
+
* @param {string | null | undefined} sessionId
|
|
272
|
+
* @returns {void}
|
|
273
|
+
*/
|
|
274
|
+
const clearGroupOwnerWriteCacheForSession = (sessionId) => {
|
|
275
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
276
|
+
const prefix = `${safeSessionId}:`;
|
|
277
|
+
const keys = groupOwnerWriteStateCache.keys();
|
|
278
|
+
for (const key of keys) {
|
|
279
|
+
if (String(key || '').startsWith(prefix)) {
|
|
280
|
+
groupOwnerWriteStateCache.del(key);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const resolveGroupOwnerWriteState = createGroupOwnerWriteStateResolver({
|
|
286
|
+
getOwnerImpl: getGroupOwner,
|
|
287
|
+
tryAcquireImpl: tryAcquireGroupOwner,
|
|
288
|
+
cacheImpl: groupOwnerWriteStateCache,
|
|
289
|
+
isGroupJidImpl: isGroupJid,
|
|
290
|
+
normalizeSessionIdImpl: normalizeSessionId,
|
|
291
|
+
buildCacheKeyImpl: buildGroupOwnerWriteCacheKey,
|
|
292
|
+
loggerImpl: logger,
|
|
293
|
+
defaultAllowClaim: GROUP_OWNER_WRITE_CLAIM_ON_MISS,
|
|
294
|
+
});
|
|
173
295
|
/**
|
|
174
296
|
* Habilita ou desabilita o diário de eventos do Baileys.
|
|
175
297
|
* @type {boolean}
|
|
@@ -267,15 +389,83 @@ const normalizeMessageReceiptType = (receiptType) => {
|
|
|
267
389
|
*/
|
|
268
390
|
let activeSocket = null;
|
|
269
391
|
/**
|
|
270
|
-
*
|
|
271
|
-
* @
|
|
392
|
+
* Contexto runtime de cada sessão de WhatsApp.
|
|
393
|
+
* @typedef {{
|
|
394
|
+
* sessionId: string,
|
|
395
|
+
* socket: import('@whiskeysockets/baileys').WASocket|null,
|
|
396
|
+
* connectPromise: Promise<void>|null,
|
|
397
|
+
* reconnectTimeout: ReturnType<typeof delayCancellable>|null,
|
|
398
|
+
* reconnectWindowStartedAt: number,
|
|
399
|
+
* connectionAttempts: number,
|
|
400
|
+
* socketGeneration: number,
|
|
401
|
+
* writerLockConnection: import('mysql2/promise').PoolConnection|null,
|
|
402
|
+
* ownerHeartbeatInterval: ReturnType<typeof setInterval>|null,
|
|
403
|
+
* ownerHeartbeatInFlight: boolean
|
|
404
|
+
* }} SessionContext
|
|
405
|
+
*/
|
|
406
|
+
/**
|
|
407
|
+
* Registry em memória de contexto por sessão.
|
|
408
|
+
* @type {Map<string, SessionContext>}
|
|
409
|
+
*/
|
|
410
|
+
const sessionContexts = new Map();
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Cria o contexto de runtime inicial para uma sessão.
|
|
414
|
+
* @param {string} sessionId
|
|
415
|
+
* @returns {SessionContext}
|
|
416
|
+
*/
|
|
417
|
+
const createSessionContext = (sessionId) => ({
|
|
418
|
+
sessionId,
|
|
419
|
+
socket: null,
|
|
420
|
+
connectPromise: null,
|
|
421
|
+
reconnectTimeout: null,
|
|
422
|
+
reconnectWindowStartedAt: 0,
|
|
423
|
+
connectionAttempts: 0,
|
|
424
|
+
socketGeneration: 0,
|
|
425
|
+
writerLockConnection: null,
|
|
426
|
+
ownerHeartbeatInterval: null,
|
|
427
|
+
ownerHeartbeatInFlight: false,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Obtém o contexto de runtime de uma sessão.
|
|
432
|
+
* @param {string | null | undefined} sessionId
|
|
433
|
+
* @param {{ createIfMissing?: boolean }} [options]
|
|
434
|
+
* @returns {SessionContext | null}
|
|
272
435
|
*/
|
|
273
|
-
|
|
436
|
+
const getSessionContext = (sessionId, { createIfMissing = true } = {}) => {
|
|
437
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
438
|
+
let context = sessionContexts.get(safeSessionId);
|
|
439
|
+
if (!context && createIfMissing) {
|
|
440
|
+
context = createSessionContext(safeSessionId);
|
|
441
|
+
sessionContexts.set(safeSessionId, context);
|
|
442
|
+
}
|
|
443
|
+
return context || null;
|
|
444
|
+
};
|
|
445
|
+
|
|
274
446
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
447
|
+
* Resolve qual socket deve ser exposto como "ativo" no runtime legado.
|
|
448
|
+
* Prioriza a sessão principal quando conectada.
|
|
449
|
+
* @returns {import('@whiskeysockets/baileys').WASocket | null}
|
|
277
450
|
*/
|
|
278
|
-
|
|
451
|
+
const resolvePreferredActiveSocket = () => {
|
|
452
|
+
const primaryContext = getSessionContext(BAILEYS_PRIMARY_SESSION_ID, { createIfMissing: false });
|
|
453
|
+
if (isSocketOpen(primaryContext?.socket)) return primaryContext.socket;
|
|
454
|
+
|
|
455
|
+
for (const context of sessionContexts.values()) {
|
|
456
|
+
if (isSocketOpen(context?.socket)) return context.socket;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return primaryContext?.socket || null;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Sincroniza a referência legada `activeSocket` com os contextos por sessão.
|
|
464
|
+
* @returns {void}
|
|
465
|
+
*/
|
|
466
|
+
const syncLegacyActiveSocketReference = () => {
|
|
467
|
+
activeSocket = resolvePreferredActiveSocket();
|
|
468
|
+
};
|
|
279
469
|
/**
|
|
280
470
|
* Cache para contadores de retentativa de mensagens.
|
|
281
471
|
* @type {NodeCache}
|
|
@@ -322,26 +512,6 @@ const MAX_CONNECTION_ATTEMPTS = 5;
|
|
|
322
512
|
* @type {number}
|
|
323
513
|
*/
|
|
324
514
|
const INITIAL_RECONNECT_DELAY = 3000;
|
|
325
|
-
/**
|
|
326
|
-
* Timeout para reconexão.
|
|
327
|
-
* @type {ReturnType<typeof delayCancellable> | null}
|
|
328
|
-
*/
|
|
329
|
-
let reconnectTimeout = null;
|
|
330
|
-
/**
|
|
331
|
-
* Promessa de conexão ativa.
|
|
332
|
-
* @type {Promise<void> | null}
|
|
333
|
-
*/
|
|
334
|
-
let connectPromise = null;
|
|
335
|
-
/**
|
|
336
|
-
* Geração atual do socket (incrementado a cada nova conexão).
|
|
337
|
-
* @type {number}
|
|
338
|
-
*/
|
|
339
|
-
let socketGeneration = 0;
|
|
340
|
-
/**
|
|
341
|
-
* Conexão MySQL dedicada para manter lock de escritor único do Baileys.
|
|
342
|
-
* @type {import('mysql2/promise').PoolConnection | null}
|
|
343
|
-
*/
|
|
344
|
-
let baileysWriterLockConnection = null;
|
|
345
515
|
/**
|
|
346
516
|
* Nomes de todos os eventos do Baileys que são monitorados.
|
|
347
517
|
* @type {string[]}
|
|
@@ -884,12 +1054,15 @@ const queueContactsLidUpdates = (contacts, source) => {
|
|
|
884
1054
|
* Eventos selecionados são enfileirados para persistência.
|
|
885
1055
|
* @param {import('@whiskeysockets/baileys').WASocket} sock - A instância do socket do Baileys.
|
|
886
1056
|
* @param {number} generation - A geração atual do socket.
|
|
1057
|
+
* @param {string} sessionId - Sessão associada ao socket.
|
|
887
1058
|
* @returns {void}
|
|
888
1059
|
*/
|
|
889
|
-
const registerBaileysEventJournal = (sock, generation) => {
|
|
1060
|
+
const registerBaileysEventJournal = (sock, generation, sessionId) => {
|
|
1061
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
890
1062
|
if (!BAILEYS_EVENT_JOURNAL_ENABLED) {
|
|
891
1063
|
logger.debug('Journal de eventos Baileys desativado por configuração.', {
|
|
892
1064
|
action: 'baileys_event_journal_disabled',
|
|
1065
|
+
sessionId: safeSessionId,
|
|
893
1066
|
});
|
|
894
1067
|
return;
|
|
895
1068
|
}
|
|
@@ -898,6 +1071,7 @@ const registerBaileysEventJournal = (sock, generation) => {
|
|
|
898
1071
|
if (unknownEvents.length > 0) {
|
|
899
1072
|
logger.warn('Alguns eventos configurados para journal não existem na lista conhecida do Baileys.', {
|
|
900
1073
|
action: 'baileys_event_journal_unknown_events',
|
|
1074
|
+
sessionId: safeSessionId,
|
|
901
1075
|
unknownEvents,
|
|
902
1076
|
});
|
|
903
1077
|
}
|
|
@@ -906,17 +1080,41 @@ const registerBaileysEventJournal = (sock, generation) => {
|
|
|
906
1080
|
if (eventsToPersist.length === 0) {
|
|
907
1081
|
logger.warn('Journal de eventos Baileys habilitado sem eventos válidos para persistir.', {
|
|
908
1082
|
action: 'baileys_event_journal_empty',
|
|
1083
|
+
sessionId: safeSessionId,
|
|
909
1084
|
configuredEvents: BAILEYS_EVENT_JOURNAL_EVENT_LIST,
|
|
910
1085
|
});
|
|
911
1086
|
return;
|
|
912
1087
|
}
|
|
913
1088
|
|
|
914
1089
|
for (const eventName of eventsToPersist) {
|
|
915
|
-
sock.ev.on(eventName, (payload) => {
|
|
1090
|
+
sock.ev.on(eventName, async (payload) => {
|
|
916
1091
|
try {
|
|
917
1092
|
const summary = summarizeBaileysEventPayload(eventName, payload);
|
|
918
1093
|
const refs = extractBaileysEventReferences(payload);
|
|
1094
|
+
if (isGroupJid(refs.chatId || '')) {
|
|
1095
|
+
const ownerWriteCacheKey = buildGroupOwnerWriteCacheKey(refs.chatId, safeSessionId);
|
|
1096
|
+
const expectedAssignmentVersion = normalizeAssignmentVersion(groupOwnerWriteStateCache.get(ownerWriteCacheKey)?.assignmentVersion);
|
|
1097
|
+
const ownerState = await resolveGroupOwnerWriteState(refs.chatId, safeSessionId, {
|
|
1098
|
+
source: `baileys_journal:${eventName}`,
|
|
1099
|
+
expectedAssignmentVersion,
|
|
1100
|
+
enforceFence: true,
|
|
1101
|
+
});
|
|
1102
|
+
if (!ownerState.allowed) {
|
|
1103
|
+
logger.debug('Evento Baileys de grupo ignorado para escrita por não-owner.', {
|
|
1104
|
+
action: 'baileys_event_group_write_skipped_non_owner',
|
|
1105
|
+
sessionId: safeSessionId,
|
|
1106
|
+
groupId: refs.chatId,
|
|
1107
|
+
ownerSessionId: ownerState.ownerSessionId,
|
|
1108
|
+
ownerAssignmentVersion: ownerState.assignmentVersion || null,
|
|
1109
|
+
expectedAssignmentVersion,
|
|
1110
|
+
reason: ownerState.reason,
|
|
1111
|
+
eventName,
|
|
1112
|
+
});
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
919
1116
|
queueBaileysEventInsert({
|
|
1117
|
+
session_id: safeSessionId,
|
|
920
1118
|
event_name: eventName,
|
|
921
1119
|
socket_generation: generation,
|
|
922
1120
|
chat_id: refs.chatId,
|
|
@@ -928,6 +1126,7 @@ const registerBaileysEventJournal = (sock, generation) => {
|
|
|
928
1126
|
} catch (error) {
|
|
929
1127
|
logger.warn('Falha ao enfileirar evento Baileys para journal.', {
|
|
930
1128
|
action: 'baileys_event_journal_enqueue_failed',
|
|
1129
|
+
sessionId: safeSessionId,
|
|
931
1130
|
eventName,
|
|
932
1131
|
error: error?.message,
|
|
933
1132
|
});
|
|
@@ -937,6 +1136,7 @@ const registerBaileysEventJournal = (sock, generation) => {
|
|
|
937
1136
|
|
|
938
1137
|
logger.info('Journal de eventos Baileys habilitado.', {
|
|
939
1138
|
action: 'baileys_event_journal_ready',
|
|
1139
|
+
sessionId: safeSessionId,
|
|
940
1140
|
generation,
|
|
941
1141
|
eventsCount: eventsToPersist.length,
|
|
942
1142
|
events: eventsToPersist,
|
|
@@ -978,11 +1178,13 @@ const safeJsonParse = (value, fallback) => {
|
|
|
978
1178
|
* @param {'append' | 'notify' | string} type - Tipo do evento de upsert.
|
|
979
1179
|
* @returns {Promise<void>} Conclusão da persistência.
|
|
980
1180
|
*/
|
|
981
|
-
async function persistIncomingMessages(incomingMessages, type) {
|
|
1181
|
+
async function persistIncomingMessages(incomingMessages, type, sessionId = BAILEYS_PRIMARY_SESSION_ID) {
|
|
982
1182
|
if (type !== 'append' && type !== 'notify') return;
|
|
983
1183
|
|
|
984
1184
|
const entries = [];
|
|
985
1185
|
const lidsToPrime = new Set();
|
|
1186
|
+
const groupWriteStateByJid = new Map();
|
|
1187
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
986
1188
|
|
|
987
1189
|
for (const msg of incomingMessages) {
|
|
988
1190
|
if (!msg.message || msg.key.remoteJid === 'status@broadcast') continue;
|
|
@@ -1018,7 +1220,36 @@ async function persistIncomingMessages(incomingMessages, type) {
|
|
|
1018
1220
|
|
|
1019
1221
|
const canonicalSenderId = resolveUserIdCached(senderInfo) || msg.key.participant || msg.key.remoteJid;
|
|
1020
1222
|
|
|
1021
|
-
const messageData =
|
|
1223
|
+
const messageData = {
|
|
1224
|
+
...buildMessageData(msg, canonicalSenderId, safeSessionId),
|
|
1225
|
+
};
|
|
1226
|
+
if (isGroupJid(messageData.chat_id || '')) {
|
|
1227
|
+
let ownerState = groupWriteStateByJid.get(messageData.chat_id);
|
|
1228
|
+
if (!ownerState) {
|
|
1229
|
+
const ownerWriteCacheKey = buildGroupOwnerWriteCacheKey(messageData.chat_id, safeSessionId);
|
|
1230
|
+
const expectedAssignmentVersion = normalizeAssignmentVersion(groupOwnerWriteStateCache.get(ownerWriteCacheKey)?.assignmentVersion);
|
|
1231
|
+
ownerState = await resolveGroupOwnerWriteState(messageData.chat_id, safeSessionId, {
|
|
1232
|
+
source: 'persist_incoming_messages',
|
|
1233
|
+
expectedAssignmentVersion,
|
|
1234
|
+
enforceFence: true,
|
|
1235
|
+
});
|
|
1236
|
+
groupWriteStateByJid.set(messageData.chat_id, ownerState);
|
|
1237
|
+
}
|
|
1238
|
+
if (!ownerState.allowed) {
|
|
1239
|
+
logger.debug('Persistência de mensagem de grupo ignorada para sessão não-owner.', {
|
|
1240
|
+
action: 'incoming_group_message_persistence_skipped_non_owner',
|
|
1241
|
+
sessionId: safeSessionId,
|
|
1242
|
+
groupId: messageData.chat_id,
|
|
1243
|
+
ownerSessionId: ownerState.ownerSessionId,
|
|
1244
|
+
ownerAssignmentVersion: ownerState.assignmentVersion || null,
|
|
1245
|
+
messageId: messageData.message_id,
|
|
1246
|
+
reason: ownerState.reason,
|
|
1247
|
+
});
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
messageData.allow_group_write = true;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1022
1253
|
queueMessageInsert(messageData);
|
|
1023
1254
|
}
|
|
1024
1255
|
}
|
|
@@ -1029,17 +1260,36 @@ async function persistIncomingMessages(incomingMessages, type) {
|
|
|
1029
1260
|
* @param {import('@whiskeysockets/baileys').WAMessageKey} key - Chave da mensagem.
|
|
1030
1261
|
* @returns {Promise<import('@whiskeysockets/baileys').proto.IMessage | undefined>} Conteúdo da mensagem armazenada.
|
|
1031
1262
|
*/
|
|
1032
|
-
async function getStoredMessage(key) {
|
|
1263
|
+
async function getStoredMessage(key, sessionId = BAILEYS_PRIMARY_SESSION_ID) {
|
|
1033
1264
|
const messageId = key?.id;
|
|
1034
1265
|
const remoteJid = key?.remoteJid;
|
|
1035
1266
|
if (!messageId || !remoteJid) return undefined;
|
|
1036
1267
|
|
|
1037
1268
|
try {
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1269
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1270
|
+
let record = null;
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
const rows = await executeQuery(
|
|
1274
|
+
`SELECT raw_message
|
|
1275
|
+
FROM ${TABLES.MESSAGES}
|
|
1276
|
+
WHERE session_id = ? AND message_id = ? AND chat_id = ?
|
|
1277
|
+
LIMIT 1`,
|
|
1278
|
+
[safeSessionId, messageId, remoteJid],
|
|
1279
|
+
);
|
|
1280
|
+
record = rows?.[0] || null;
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
if (String(error?.code || '') !== 'ER_BAD_FIELD_ERROR') {
|
|
1283
|
+
throw error;
|
|
1284
|
+
}
|
|
1285
|
+
const fallbackRows = await findBy('messages', { message_id: messageId, chat_id: remoteJid }, { limit: 1 });
|
|
1286
|
+
record = fallbackRows?.[0] || null;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1040
1289
|
const stored = safeJsonParse(record?.raw_message, null);
|
|
1041
1290
|
if (record?.raw_message && !stored) {
|
|
1042
1291
|
logger.error('Falha ao interpretar raw_message armazenado.', {
|
|
1292
|
+
sessionId: safeSessionId,
|
|
1043
1293
|
messageId,
|
|
1044
1294
|
remoteJid,
|
|
1045
1295
|
});
|
|
@@ -1047,6 +1297,7 @@ async function getStoredMessage(key) {
|
|
|
1047
1297
|
return stored?.message ?? undefined;
|
|
1048
1298
|
} catch (error) {
|
|
1049
1299
|
logger.error('Erro ao buscar mensagem armazenada no banco:', {
|
|
1300
|
+
sessionId: normalizeSessionId(sessionId),
|
|
1050
1301
|
error: error.message,
|
|
1051
1302
|
messageId,
|
|
1052
1303
|
remoteJid,
|
|
@@ -1057,55 +1308,66 @@ async function getStoredMessage(key) {
|
|
|
1057
1308
|
|
|
1058
1309
|
/**
|
|
1059
1310
|
* Limpa o timeout de reconexão agendado, se houver.
|
|
1311
|
+
* @param {string} sessionId
|
|
1060
1312
|
* @returns {void}
|
|
1061
1313
|
*/
|
|
1062
|
-
const clearReconnectTimeout = () => {
|
|
1063
|
-
|
|
1064
|
-
reconnectTimeout
|
|
1065
|
-
reconnectTimeout
|
|
1314
|
+
const clearReconnectTimeout = (sessionId = BAILEYS_PRIMARY_SESSION_ID) => {
|
|
1315
|
+
const context = getSessionContext(sessionId, { createIfMissing: false });
|
|
1316
|
+
if (!context?.reconnectTimeout) return;
|
|
1317
|
+
context.reconnectTimeout.cancel();
|
|
1318
|
+
context.reconnectTimeout = null;
|
|
1066
1319
|
};
|
|
1067
1320
|
|
|
1068
1321
|
/**
|
|
1069
1322
|
* Reseta o estado das tentativas de reconexão.
|
|
1323
|
+
* @param {string} sessionId
|
|
1070
1324
|
* @returns {void}
|
|
1071
1325
|
*/
|
|
1072
|
-
const resetReconnectState = () => {
|
|
1073
|
-
|
|
1074
|
-
|
|
1326
|
+
const resetReconnectState = (sessionId = BAILEYS_PRIMARY_SESSION_ID) => {
|
|
1327
|
+
const context = getSessionContext(sessionId);
|
|
1328
|
+
context.connectionAttempts = 0;
|
|
1329
|
+
context.reconnectWindowStartedAt = 0;
|
|
1075
1330
|
};
|
|
1076
1331
|
|
|
1077
1332
|
/**
|
|
1078
1333
|
* Calcula o número da próxima tentativa de reconexão.
|
|
1079
1334
|
* Reseta a contagem de tentativas se a janela de reconexão expirou.
|
|
1335
|
+
* @param {string} sessionId
|
|
1080
1336
|
* @returns {number} O número da próxima tentativa.
|
|
1081
1337
|
*/
|
|
1082
|
-
const getNextReconnectAttempt = () => {
|
|
1338
|
+
const getNextReconnectAttempt = (sessionId = BAILEYS_PRIMARY_SESSION_ID) => {
|
|
1339
|
+
const context = getSessionContext(sessionId);
|
|
1083
1340
|
const now = __timeNowMs();
|
|
1084
|
-
if (!reconnectWindowStartedAt || now - reconnectWindowStartedAt >= BAILEYS_RECONNECT_ATTEMPT_RESET_MS) {
|
|
1085
|
-
reconnectWindowStartedAt = now;
|
|
1086
|
-
connectionAttempts = 0;
|
|
1341
|
+
if (!context.reconnectWindowStartedAt || now - context.reconnectWindowStartedAt >= BAILEYS_RECONNECT_ATTEMPT_RESET_MS) {
|
|
1342
|
+
context.reconnectWindowStartedAt = now;
|
|
1343
|
+
context.connectionAttempts = 0;
|
|
1087
1344
|
}
|
|
1088
|
-
connectionAttempts += 1;
|
|
1089
|
-
return connectionAttempts;
|
|
1345
|
+
context.connectionAttempts += 1;
|
|
1346
|
+
return context.connectionAttempts;
|
|
1090
1347
|
};
|
|
1091
1348
|
|
|
1092
1349
|
/**
|
|
1093
1350
|
* Agenda uma reconexão com o WhatsApp após um determinado atraso.
|
|
1094
1351
|
* Evita agendar múltiplas reconexões.
|
|
1352
|
+
* @param {string} sessionId
|
|
1095
1353
|
* @param {number} delay - O atraso em milissegundos antes de tentar a reconexão.
|
|
1096
1354
|
* @returns {void}
|
|
1097
1355
|
*/
|
|
1098
|
-
const scheduleReconnect = (delay) => {
|
|
1099
|
-
|
|
1356
|
+
const scheduleReconnect = (sessionId, delay) => {
|
|
1357
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1358
|
+
const context = getSessionContext(safeSessionId);
|
|
1359
|
+
if (context.reconnectTimeout) return;
|
|
1360
|
+
|
|
1100
1361
|
const pendingReconnect = delayCancellable(Math.max(0, Number(delay) || 0));
|
|
1101
|
-
reconnectTimeout = pendingReconnect;
|
|
1362
|
+
context.reconnectTimeout = pendingReconnect;
|
|
1102
1363
|
pendingReconnect.delay
|
|
1103
1364
|
.then(() => {
|
|
1104
|
-
if (reconnectTimeout !== pendingReconnect) return;
|
|
1105
|
-
reconnectTimeout = null;
|
|
1106
|
-
connectToWhatsApp().catch((error) => {
|
|
1365
|
+
if (context.reconnectTimeout !== pendingReconnect) return;
|
|
1366
|
+
context.reconnectTimeout = null;
|
|
1367
|
+
connectToWhatsApp(safeSessionId).catch((error) => {
|
|
1107
1368
|
logger.error('Falha ao executar reconexão agendada.', {
|
|
1108
1369
|
action: 'reconnect_schedule_failure',
|
|
1370
|
+
sessionId: safeSessionId,
|
|
1109
1371
|
errorMessage: error?.message,
|
|
1110
1372
|
stack: error?.stack,
|
|
1111
1373
|
timestamp: __timeNowIso(),
|
|
@@ -1113,8 +1375,8 @@ const scheduleReconnect = (delay) => {
|
|
|
1113
1375
|
});
|
|
1114
1376
|
})
|
|
1115
1377
|
.catch((error) => {
|
|
1116
|
-
if (reconnectTimeout === pendingReconnect) {
|
|
1117
|
-
reconnectTimeout = null;
|
|
1378
|
+
if (context.reconnectTimeout === pendingReconnect) {
|
|
1379
|
+
context.reconnectTimeout = null;
|
|
1118
1380
|
}
|
|
1119
1381
|
if (
|
|
1120
1382
|
String(error?.message || '')
|
|
@@ -1125,37 +1387,146 @@ const scheduleReconnect = (delay) => {
|
|
|
1125
1387
|
}
|
|
1126
1388
|
logger.warn('Falha ao aguardar atraso da reconexão agendada.', {
|
|
1127
1389
|
action: 'reconnect_schedule_delay_error',
|
|
1390
|
+
sessionId: safeSessionId,
|
|
1128
1391
|
errorMessage: error?.message,
|
|
1129
1392
|
});
|
|
1130
1393
|
});
|
|
1131
1394
|
};
|
|
1132
1395
|
|
|
1396
|
+
/**
|
|
1397
|
+
* Interrompe o heartbeat de ownership por sessão.
|
|
1398
|
+
* @param {string} sessionId
|
|
1399
|
+
* @param {string} reason
|
|
1400
|
+
* @returns {void}
|
|
1401
|
+
*/
|
|
1402
|
+
const stopGroupOwnerHeartbeat = (sessionId, reason = 'unknown') => {
|
|
1403
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1404
|
+
const context = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
1405
|
+
if (!context) return;
|
|
1406
|
+
|
|
1407
|
+
if (context.ownerHeartbeatInterval) {
|
|
1408
|
+
clearInterval(context.ownerHeartbeatInterval);
|
|
1409
|
+
context.ownerHeartbeatInterval = null;
|
|
1410
|
+
}
|
|
1411
|
+
context.ownerHeartbeatInFlight = false;
|
|
1412
|
+
clearGroupOwnerWriteCacheForSession(safeSessionId);
|
|
1413
|
+
|
|
1414
|
+
logger.debug('Heartbeat de ownership por grupo interrompido.', {
|
|
1415
|
+
action: 'group_owner_heartbeat_stopped',
|
|
1416
|
+
sessionId: safeSessionId,
|
|
1417
|
+
reason,
|
|
1418
|
+
});
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Inicia heartbeat de ownership por sessão para renovar lease dos grupos de que a sessão é owner.
|
|
1423
|
+
* @param {string} sessionId
|
|
1424
|
+
* @param {number} generation
|
|
1425
|
+
* @returns {void}
|
|
1426
|
+
*/
|
|
1427
|
+
const startGroupOwnerHeartbeat = (sessionId, generation) => {
|
|
1428
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1429
|
+
const context = getSessionContext(safeSessionId);
|
|
1430
|
+
|
|
1431
|
+
stopGroupOwnerHeartbeat(safeSessionId, 'restart');
|
|
1432
|
+
|
|
1433
|
+
const runTick = async () => {
|
|
1434
|
+
const latestContext = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
1435
|
+
if (!latestContext) return;
|
|
1436
|
+
if (latestContext.ownerHeartbeatInFlight) return;
|
|
1437
|
+
if (latestContext.socketGeneration !== generation) return;
|
|
1438
|
+
if (!isSocketOpen(latestContext.socket)) return;
|
|
1439
|
+
|
|
1440
|
+
latestContext.ownerHeartbeatInFlight = true;
|
|
1441
|
+
try {
|
|
1442
|
+
const socket = latestContext.socket;
|
|
1443
|
+
const botJid = normalizeJid(socket?.user?.id || socket?.authState?.creds?.me?.id || socket?.authState?.creds?.me?.lid) || undefined;
|
|
1444
|
+
const sessionWeight = Math.max(1, Number(MULTI_SESSION_RUNTIME_CONFIG?.sessionWeights?.[safeSessionId] || 1));
|
|
1445
|
+
const heartbeatOutcome = await heartbeatGroupOwnerSession({
|
|
1446
|
+
sessionId: safeSessionId,
|
|
1447
|
+
leaseMs: GROUP_OWNER_LEASE_MS,
|
|
1448
|
+
reason: 'owner_lease_heartbeat',
|
|
1449
|
+
botJid,
|
|
1450
|
+
metadata: {
|
|
1451
|
+
source: 'socket_controller',
|
|
1452
|
+
socketGeneration: generation,
|
|
1453
|
+
},
|
|
1454
|
+
capacityWeight: sessionWeight,
|
|
1455
|
+
currentScore: 0,
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
logger.debug('Heartbeat de ownership executado.', {
|
|
1459
|
+
action: 'group_owner_heartbeat_tick',
|
|
1460
|
+
sessionId: safeSessionId,
|
|
1461
|
+
generation,
|
|
1462
|
+
renewedAssignments: heartbeatOutcome?.renewedAssignments || 0,
|
|
1463
|
+
heartbeatMs: GROUP_OWNER_HEARTBEAT_MS,
|
|
1464
|
+
leaseMs: GROUP_OWNER_LEASE_MS,
|
|
1465
|
+
});
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
logger.warn('Falha no heartbeat de ownership da sessão.', {
|
|
1468
|
+
action: 'group_owner_heartbeat_failed',
|
|
1469
|
+
sessionId: safeSessionId,
|
|
1470
|
+
generation,
|
|
1471
|
+
error: error?.message,
|
|
1472
|
+
});
|
|
1473
|
+
} finally {
|
|
1474
|
+
const current = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
1475
|
+
if (current) {
|
|
1476
|
+
current.ownerHeartbeatInFlight = false;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
context.ownerHeartbeatInterval = setInterval(() => {
|
|
1482
|
+
void runTick();
|
|
1483
|
+
}, GROUP_OWNER_HEARTBEAT_MS);
|
|
1484
|
+
if (typeof context.ownerHeartbeatInterval.unref === 'function') {
|
|
1485
|
+
context.ownerHeartbeatInterval.unref();
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
logger.info('Heartbeat de ownership por grupo iniciado.', {
|
|
1489
|
+
action: 'group_owner_heartbeat_started',
|
|
1490
|
+
sessionId: safeSessionId,
|
|
1491
|
+
generation,
|
|
1492
|
+
heartbeatMs: GROUP_OWNER_HEARTBEAT_MS,
|
|
1493
|
+
leaseMs: GROUP_OWNER_LEASE_MS,
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
void runTick();
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1133
1499
|
/**
|
|
1134
1500
|
* Libera lock de escritor único do Baileys, se estiver ativo.
|
|
1501
|
+
* @param {string} sessionId
|
|
1135
1502
|
* @param {string} reason
|
|
1136
1503
|
* @returns {Promise<void>}
|
|
1137
1504
|
*/
|
|
1138
|
-
const releaseBaileysWriterLock = async (reason = 'unknown') => {
|
|
1139
|
-
const
|
|
1505
|
+
const releaseBaileysWriterLock = async (sessionId, reason = 'unknown') => {
|
|
1506
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1507
|
+
const context = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
1508
|
+
const connection = context?.writerLockConnection;
|
|
1140
1509
|
if (!connection) return;
|
|
1141
|
-
|
|
1142
|
-
|
|
1510
|
+
const lockName = getWriterLockNameBySession(safeSessionId);
|
|
1511
|
+
context.writerLockConnection = null;
|
|
1143
1512
|
|
|
1144
1513
|
try {
|
|
1145
|
-
const rows = await executeQuery('SELECT RELEASE_LOCK(?) AS released', [
|
|
1514
|
+
const rows = await executeQuery('SELECT RELEASE_LOCK(?) AS released', [lockName], connection);
|
|
1146
1515
|
const released = Number(rows?.[0]?.released) === 1;
|
|
1147
1516
|
logger.info('Lock de escritor do Baileys liberado.', {
|
|
1148
1517
|
action: 'baileys_writer_lock_released',
|
|
1518
|
+
sessionId: safeSessionId,
|
|
1149
1519
|
reason,
|
|
1150
1520
|
released,
|
|
1151
|
-
lockName
|
|
1521
|
+
lockName,
|
|
1152
1522
|
timestamp: __timeNowIso(),
|
|
1153
1523
|
});
|
|
1154
1524
|
} catch (error) {
|
|
1155
1525
|
logger.warn('Falha ao liberar lock de escritor do Baileys.', {
|
|
1156
1526
|
action: 'baileys_writer_lock_release_error',
|
|
1527
|
+
sessionId: safeSessionId,
|
|
1157
1528
|
reason,
|
|
1158
|
-
lockName
|
|
1529
|
+
lockName,
|
|
1159
1530
|
errorMessage: error?.message,
|
|
1160
1531
|
timestamp: __timeNowIso(),
|
|
1161
1532
|
});
|
|
@@ -1173,27 +1544,33 @@ const releaseBaileysWriterLock = async (reason = 'unknown') => {
|
|
|
1173
1544
|
|
|
1174
1545
|
/**
|
|
1175
1546
|
* Garante lock de escritor único para a sessão do Baileys.
|
|
1547
|
+
* @param {string} sessionId
|
|
1176
1548
|
* @returns {Promise<boolean>}
|
|
1177
1549
|
*/
|
|
1178
|
-
const ensureBaileysWriterLock = async () => {
|
|
1550
|
+
const ensureBaileysWriterLock = async (sessionId) => {
|
|
1179
1551
|
if (!BAILEYS_SINGLE_WRITER_LOCK_ENABLED) {
|
|
1180
1552
|
return true;
|
|
1181
1553
|
}
|
|
1182
1554
|
|
|
1183
|
-
|
|
1555
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1556
|
+
const context = getSessionContext(safeSessionId);
|
|
1557
|
+
const lockName = getWriterLockNameBySession(safeSessionId);
|
|
1558
|
+
|
|
1559
|
+
if (context.writerLockConnection) {
|
|
1184
1560
|
return true;
|
|
1185
1561
|
}
|
|
1186
1562
|
|
|
1187
1563
|
const connection = await pool.getConnection();
|
|
1188
1564
|
|
|
1189
1565
|
try {
|
|
1190
|
-
const rows = await executeQuery('SELECT GET_LOCK(?, ?) AS lock_status', [
|
|
1566
|
+
const rows = await executeQuery('SELECT GET_LOCK(?, ?) AS lock_status', [lockName, BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS], connection);
|
|
1191
1567
|
const lockStatus = Number(rows?.[0]?.lock_status);
|
|
1192
1568
|
if (lockStatus !== 1) {
|
|
1193
1569
|
connection.release();
|
|
1194
1570
|
logger.warn('Nao foi possivel adquirir lock de escritor do Baileys nesta tentativa.', {
|
|
1195
1571
|
action: 'baileys_writer_lock_busy',
|
|
1196
|
-
|
|
1572
|
+
sessionId: safeSessionId,
|
|
1573
|
+
lockName,
|
|
1197
1574
|
timeoutSeconds: BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS,
|
|
1198
1575
|
status: Number.isFinite(lockStatus) ? lockStatus : null,
|
|
1199
1576
|
retryAfterMs: BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS,
|
|
@@ -1202,10 +1579,11 @@ const ensureBaileysWriterLock = async () => {
|
|
|
1202
1579
|
return false;
|
|
1203
1580
|
}
|
|
1204
1581
|
|
|
1205
|
-
|
|
1582
|
+
context.writerLockConnection = connection;
|
|
1206
1583
|
logger.info('Lock de escritor do Baileys adquirido com sucesso.', {
|
|
1207
1584
|
action: 'baileys_writer_lock_acquired',
|
|
1208
|
-
|
|
1585
|
+
sessionId: safeSessionId,
|
|
1586
|
+
lockName,
|
|
1209
1587
|
timeoutSeconds: BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS,
|
|
1210
1588
|
timestamp: __timeNowIso(),
|
|
1211
1589
|
});
|
|
@@ -1220,16 +1598,30 @@ const ensureBaileysWriterLock = async () => {
|
|
|
1220
1598
|
}
|
|
1221
1599
|
};
|
|
1222
1600
|
|
|
1601
|
+
/**
|
|
1602
|
+
* Libera todos os locks de escritor mantidos pelo processo atual.
|
|
1603
|
+
* @param {string} [reason='unknown']
|
|
1604
|
+
* @returns {Promise<void>}
|
|
1605
|
+
*/
|
|
1606
|
+
const releaseAllBaileysWriterLocks = async (reason = 'unknown') => {
|
|
1607
|
+
const targets = Array.from(sessionContexts.keys());
|
|
1608
|
+
if (!targets.length) {
|
|
1609
|
+
await releaseBaileysWriterLock(BAILEYS_PRIMARY_SESSION_ID, reason).catch(() => {});
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
await Promise.allSettled(targets.map((sessionId) => releaseBaileysWriterLock(sessionId, reason)));
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1223
1615
|
process.once('beforeExit', () => {
|
|
1224
|
-
|
|
1616
|
+
releaseAllBaileysWriterLocks('before_exit').catch(() => {});
|
|
1225
1617
|
});
|
|
1226
1618
|
|
|
1227
1619
|
process.once('SIGINT', () => {
|
|
1228
|
-
|
|
1620
|
+
releaseAllBaileysWriterLocks('sigint').catch(() => {});
|
|
1229
1621
|
});
|
|
1230
1622
|
|
|
1231
1623
|
process.once('SIGTERM', () => {
|
|
1232
|
-
|
|
1624
|
+
releaseAllBaileysWriterLocks('sigterm').catch(() => {});
|
|
1233
1625
|
});
|
|
1234
1626
|
|
|
1235
1627
|
/**
|
|
@@ -1313,34 +1705,40 @@ const syncGroupsOnConnectionOpen = async (sock) => {
|
|
|
1313
1705
|
* Configura autenticação, cria o socket e registra handlers de eventos.
|
|
1314
1706
|
* Gerencia a lógica de reconexão e a distribuição de eventos.
|
|
1315
1707
|
* @async
|
|
1708
|
+
* @param {string} [sessionId=BAILEYS_PRIMARY_SESSION_ID] - Sessão alvo da conexão.
|
|
1316
1709
|
* @returns {Promise<void>} Conclusão da inicialização e do registro de handlers.
|
|
1317
1710
|
* @throws {Error} Lança erro se a conexão inicial falhar.
|
|
1318
1711
|
*/
|
|
1319
|
-
export async function connectToWhatsApp() {
|
|
1320
|
-
|
|
1321
|
-
|
|
1712
|
+
export async function connectToWhatsApp(sessionId = BAILEYS_PRIMARY_SESSION_ID) {
|
|
1713
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1714
|
+
const context = getSessionContext(safeSessionId);
|
|
1715
|
+
|
|
1716
|
+
if (context.connectPromise) {
|
|
1717
|
+
return context.connectPromise;
|
|
1322
1718
|
}
|
|
1323
1719
|
|
|
1324
|
-
if (isSocketOpen(
|
|
1720
|
+
if (isSocketOpen(context.socket)) {
|
|
1325
1721
|
return;
|
|
1326
1722
|
}
|
|
1327
1723
|
|
|
1328
1724
|
logger.info('Iniciando conexão com o WhatsApp...', {
|
|
1329
1725
|
action: 'connect_init',
|
|
1726
|
+
sessionId: safeSessionId,
|
|
1330
1727
|
timestamp: __timeNowIso(),
|
|
1331
1728
|
});
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1729
|
+
|
|
1730
|
+
const currentConnectPromise = (async () => {
|
|
1731
|
+
clearReconnectTimeout(safeSessionId);
|
|
1732
|
+
const isWriterReady = await ensureBaileysWriterLock(safeSessionId);
|
|
1335
1733
|
if (!isWriterReady) {
|
|
1336
|
-
scheduleReconnect(BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS);
|
|
1734
|
+
scheduleReconnect(safeSessionId, BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS);
|
|
1337
1735
|
return;
|
|
1338
1736
|
}
|
|
1339
1737
|
|
|
1340
|
-
const generation = ++socketGeneration;
|
|
1738
|
+
const generation = ++context.socketGeneration;
|
|
1341
1739
|
const legacyAuthPath = path.join(__dirname, 'auth');
|
|
1342
1740
|
const { state, saveCreds } = await useDbAuthState({
|
|
1343
|
-
sessionId:
|
|
1741
|
+
sessionId: safeSessionId,
|
|
1344
1742
|
bootstrapFromDir: legacyAuthPath,
|
|
1345
1743
|
bootstrapFromFiles: BAILEYS_AUTH_BOOTSTRAP_FROM_FILES,
|
|
1346
1744
|
});
|
|
@@ -1348,7 +1746,8 @@ export async function connectToWhatsApp() {
|
|
|
1348
1746
|
const version = await resolveBaileysVersion();
|
|
1349
1747
|
|
|
1350
1748
|
logger.debug('Dados de autenticação carregados com sucesso.', {
|
|
1351
|
-
|
|
1749
|
+
sessionId: safeSessionId,
|
|
1750
|
+
authSessionId: safeSessionId,
|
|
1352
1751
|
bootstrappedFromFiles: BAILEYS_AUTH_BOOTSTRAP_FROM_FILES,
|
|
1353
1752
|
version,
|
|
1354
1753
|
generation,
|
|
@@ -1366,7 +1765,7 @@ export async function connectToWhatsApp() {
|
|
|
1366
1765
|
msgRetryCounterCache,
|
|
1367
1766
|
maxMsgRetryCount: 5,
|
|
1368
1767
|
retryRequestDelayMs: 250,
|
|
1369
|
-
getMessage: getStoredMessage,
|
|
1768
|
+
getMessage: (key) => getStoredMessage(key, safeSessionId),
|
|
1370
1769
|
userDevicesCache,
|
|
1371
1770
|
mediaCache,
|
|
1372
1771
|
cachedGroupMetadata: resolveCachedGroupMetadata,
|
|
@@ -1377,16 +1776,22 @@ export async function connectToWhatsApp() {
|
|
|
1377
1776
|
};
|
|
1378
1777
|
|
|
1379
1778
|
const sock = makeWASocket(socketConfig);
|
|
1779
|
+
sock.__omnizapSessionId = safeSessionId;
|
|
1380
1780
|
|
|
1381
|
-
|
|
1382
|
-
storeActiveSocket(sock);
|
|
1781
|
+
context.socket = sock;
|
|
1782
|
+
storeActiveSocket(sock, safeSessionId);
|
|
1783
|
+
syncLegacyActiveSocketReference();
|
|
1383
1784
|
|
|
1384
|
-
const isCurrentSocket = () =>
|
|
1785
|
+
const isCurrentSocket = () => {
|
|
1786
|
+
const latest = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
1787
|
+
return Boolean(latest && latest.socket === sock && latest.socketGeneration === generation);
|
|
1788
|
+
};
|
|
1385
1789
|
|
|
1386
1790
|
sock.ev.on('creds.update', async () => {
|
|
1387
1791
|
if (!isCurrentSocket()) return;
|
|
1388
1792
|
logger.debug('Atualizando credenciais de autenticação...', {
|
|
1389
1793
|
action: 'creds_update',
|
|
1794
|
+
sessionId: safeSessionId,
|
|
1390
1795
|
timestamp: __timeNowIso(),
|
|
1391
1796
|
});
|
|
1392
1797
|
await saveCreds();
|
|
@@ -1394,12 +1799,13 @@ export async function connectToWhatsApp() {
|
|
|
1394
1799
|
|
|
1395
1800
|
sock.ev.on('connection.update', (update) => {
|
|
1396
1801
|
if (!isCurrentSocket()) return;
|
|
1397
|
-
handleConnectionUpdate(update, sock);
|
|
1802
|
+
handleConnectionUpdate(update, sock, safeSessionId, generation);
|
|
1398
1803
|
if (update.connection === 'open') {
|
|
1399
1804
|
syncNewsBroadcastService();
|
|
1400
1805
|
}
|
|
1401
1806
|
logger.debug('Estado da conexão atualizado.', {
|
|
1402
1807
|
action: 'connection_update',
|
|
1808
|
+
sessionId: safeSessionId,
|
|
1403
1809
|
status: update.connection,
|
|
1404
1810
|
lastDisconnect: update.lastDisconnect?.error?.message || null,
|
|
1405
1811
|
isNewLogin: update.isNewLogin || false,
|
|
@@ -1415,17 +1821,19 @@ export async function connectToWhatsApp() {
|
|
|
1415
1821
|
try {
|
|
1416
1822
|
logger.debug('Novo(s) evento(s) em messages.upsert', {
|
|
1417
1823
|
action: 'messages_upsert',
|
|
1824
|
+
sessionId: safeSessionId,
|
|
1418
1825
|
type: update.type,
|
|
1419
1826
|
messagesCount: update.messages.length,
|
|
1420
1827
|
remoteJid: update.messages[0]?.key.remoteJid || null,
|
|
1421
1828
|
});
|
|
1422
|
-
const persistPromise = persistIncomingMessages(update.messages, update.type).catch((error) => {
|
|
1829
|
+
const persistPromise = persistIncomingMessages(update.messages, update.type, safeSessionId).catch((error) => {
|
|
1423
1830
|
logger.error('Erro ao persistir mensagens no banco de dados:', {
|
|
1831
|
+
sessionId: safeSessionId,
|
|
1424
1832
|
error: error.message,
|
|
1425
1833
|
});
|
|
1426
1834
|
recordError('messages_upsert');
|
|
1427
1835
|
});
|
|
1428
|
-
const handlePromise = handleMessages(update, sock).catch((error) => {
|
|
1836
|
+
const handlePromise = handleMessages(update, sock, { sessionId: safeSessionId }).catch((error) => {
|
|
1429
1837
|
recordError('messages_upsert');
|
|
1430
1838
|
throw error;
|
|
1431
1839
|
});
|
|
@@ -1442,6 +1850,7 @@ export async function connectToWhatsApp() {
|
|
|
1442
1850
|
});
|
|
1443
1851
|
} catch (error) {
|
|
1444
1852
|
logger.error('Erro no evento messages.upsert:', {
|
|
1853
|
+
sessionId: safeSessionId,
|
|
1445
1854
|
error: error.message,
|
|
1446
1855
|
stack: error.stack,
|
|
1447
1856
|
action: 'messages_upsert_error',
|
|
@@ -1476,6 +1885,7 @@ export async function connectToWhatsApp() {
|
|
|
1476
1885
|
for (const chatId of deletions) {
|
|
1477
1886
|
remove('chats', chatId).catch((error) => {
|
|
1478
1887
|
logger.error('Erro ao remover chat do banco:', {
|
|
1888
|
+
sessionId: safeSessionId,
|
|
1479
1889
|
error: error.message,
|
|
1480
1890
|
chatId,
|
|
1481
1891
|
});
|
|
@@ -1493,6 +1903,7 @@ export async function connectToWhatsApp() {
|
|
|
1493
1903
|
invalidateCachedGroupMetadata(group.id);
|
|
1494
1904
|
} catch (error) {
|
|
1495
1905
|
logger.error('Erro no upsert do grupo:', {
|
|
1906
|
+
sessionId: safeSessionId,
|
|
1496
1907
|
error: error.message,
|
|
1497
1908
|
groupId: group.id,
|
|
1498
1909
|
});
|
|
@@ -1519,6 +1930,7 @@ export async function connectToWhatsApp() {
|
|
|
1519
1930
|
queueLidUpdate(lid, pnJid, 'lid-mapping');
|
|
1520
1931
|
} catch (error) {
|
|
1521
1932
|
logger.warn('Falha ao processar lid-mapping.update para lid_map.', {
|
|
1933
|
+
sessionId: safeSessionId,
|
|
1522
1934
|
error: error.message,
|
|
1523
1935
|
});
|
|
1524
1936
|
}
|
|
@@ -1529,11 +1941,13 @@ export async function connectToWhatsApp() {
|
|
|
1529
1941
|
try {
|
|
1530
1942
|
logger.debug('Atualização de mensagens recebida.', {
|
|
1531
1943
|
action: 'messages_update',
|
|
1944
|
+
sessionId: safeSessionId,
|
|
1532
1945
|
updatesCount: update.length,
|
|
1533
1946
|
});
|
|
1534
1947
|
handleMessageUpdate(update, sock);
|
|
1535
1948
|
} catch (error) {
|
|
1536
1949
|
logger.error('Erro no evento messages.update:', {
|
|
1950
|
+
sessionId: safeSessionId,
|
|
1537
1951
|
error: error.message,
|
|
1538
1952
|
stack: error.stack,
|
|
1539
1953
|
action: 'messages_update_error',
|
|
@@ -1550,6 +1964,7 @@ export async function connectToWhatsApp() {
|
|
|
1550
1964
|
const firstError = erroredUpdates[0]?.error;
|
|
1551
1965
|
logger.warn('Falha reportada em atualização de mídia.', {
|
|
1552
1966
|
action: 'messages_media_update_error',
|
|
1967
|
+
sessionId: safeSessionId,
|
|
1553
1968
|
updatesCount: updates.length,
|
|
1554
1969
|
errorCount: erroredUpdates.length,
|
|
1555
1970
|
firstMessageId: erroredUpdates[0]?.key?.id || null,
|
|
@@ -1561,6 +1976,7 @@ export async function connectToWhatsApp() {
|
|
|
1561
1976
|
|
|
1562
1977
|
logger.debug('Atualização de mídia de mensagem recebida.', {
|
|
1563
1978
|
action: 'messages_media_update',
|
|
1979
|
+
sessionId: safeSessionId,
|
|
1564
1980
|
updatesCount: updates.length,
|
|
1565
1981
|
});
|
|
1566
1982
|
});
|
|
@@ -1582,6 +1998,7 @@ export async function connectToWhatsApp() {
|
|
|
1582
1998
|
|
|
1583
1999
|
logger.debug('Atualização de recibos de mensagem recebida.', {
|
|
1584
2000
|
action: 'message_receipt_update',
|
|
2001
|
+
sessionId: safeSessionId,
|
|
1585
2002
|
updatesCount: updates.length,
|
|
1586
2003
|
receiptTypes: Array.from(receiptTypes),
|
|
1587
2004
|
invalidReceiptTypeCount,
|
|
@@ -1621,6 +2038,7 @@ export async function connectToWhatsApp() {
|
|
|
1621
2038
|
}
|
|
1622
2039
|
} catch (error) {
|
|
1623
2040
|
logger.error('Erro no evento messages.reaction:', {
|
|
2041
|
+
sessionId: safeSessionId,
|
|
1624
2042
|
error: error.message,
|
|
1625
2043
|
stack: error.stack,
|
|
1626
2044
|
action: 'messages_reaction_error',
|
|
@@ -1633,12 +2051,14 @@ export async function connectToWhatsApp() {
|
|
|
1633
2051
|
try {
|
|
1634
2052
|
logger.debug('Grupo(s) atualizado(s).', {
|
|
1635
2053
|
action: 'groups_update',
|
|
2054
|
+
sessionId: safeSessionId,
|
|
1636
2055
|
groupCount: updates.length,
|
|
1637
2056
|
groupIds: updates.map((u) => u.id),
|
|
1638
2057
|
});
|
|
1639
2058
|
handleGroupUpdate(updates);
|
|
1640
2059
|
} catch (err) {
|
|
1641
2060
|
logger.error('Erro no evento groups.update:', {
|
|
2061
|
+
sessionId: safeSessionId,
|
|
1642
2062
|
error: err.message,
|
|
1643
2063
|
stack: err.stack,
|
|
1644
2064
|
action: 'groups_update_error',
|
|
@@ -1651,6 +2071,7 @@ export async function connectToWhatsApp() {
|
|
|
1651
2071
|
try {
|
|
1652
2072
|
logger.debug('Participantes do grupo atualizados.', {
|
|
1653
2073
|
action: 'group_participants_update',
|
|
2074
|
+
sessionId: safeSessionId,
|
|
1654
2075
|
groupId: update.id,
|
|
1655
2076
|
actionType: update.action,
|
|
1656
2077
|
participants: update.participants,
|
|
@@ -1659,6 +2080,7 @@ export async function connectToWhatsApp() {
|
|
|
1659
2080
|
handleGroupParticipantsEvent(sock, update.id, update.participants, update.action);
|
|
1660
2081
|
} catch (err) {
|
|
1661
2082
|
logger.error('Erro no evento group-participants.update:', {
|
|
2083
|
+
sessionId: safeSessionId,
|
|
1662
2084
|
error: err.message,
|
|
1663
2085
|
stack: err.stack,
|
|
1664
2086
|
action: 'group_participants_update_error',
|
|
@@ -1671,6 +2093,7 @@ export async function connectToWhatsApp() {
|
|
|
1671
2093
|
try {
|
|
1672
2094
|
logger.debug('Solicitação de entrada no grupo recebida.', {
|
|
1673
2095
|
action: 'group_join_request',
|
|
2096
|
+
sessionId: safeSessionId,
|
|
1674
2097
|
groupId: update?.id,
|
|
1675
2098
|
participant: update?.participant,
|
|
1676
2099
|
method: update?.method,
|
|
@@ -1679,6 +2102,7 @@ export async function connectToWhatsApp() {
|
|
|
1679
2102
|
handleGroupJoinRequest(sock, update);
|
|
1680
2103
|
} catch (err) {
|
|
1681
2104
|
logger.error('Erro no evento group.join-request:', {
|
|
2105
|
+
sessionId: safeSessionId,
|
|
1682
2106
|
error: err.message,
|
|
1683
2107
|
stack: err.stack,
|
|
1684
2108
|
action: 'group_join_request_error',
|
|
@@ -1704,6 +2128,7 @@ export async function connectToWhatsApp() {
|
|
|
1704
2128
|
await sock.rejectCall(call.id, call.from);
|
|
1705
2129
|
logger.info('Chamada recebida rejeitada automaticamente.', {
|
|
1706
2130
|
action: 'call_auto_reject',
|
|
2131
|
+
sessionId: safeSessionId,
|
|
1707
2132
|
callId: call.id,
|
|
1708
2133
|
from: call.from,
|
|
1709
2134
|
isGroup: call.isGroup || false,
|
|
@@ -1713,6 +2138,7 @@ export async function connectToWhatsApp() {
|
|
|
1713
2138
|
} catch (error) {
|
|
1714
2139
|
logger.warn('Falha ao rejeitar chamada automaticamente.', {
|
|
1715
2140
|
action: 'call_auto_reject_failed',
|
|
2141
|
+
sessionId: safeSessionId,
|
|
1716
2142
|
callId: call?.id || null,
|
|
1717
2143
|
from: call?.from || null,
|
|
1718
2144
|
error: error?.message,
|
|
@@ -1722,19 +2148,43 @@ export async function connectToWhatsApp() {
|
|
|
1722
2148
|
});
|
|
1723
2149
|
|
|
1724
2150
|
registerBaileysEventLoggers(sock);
|
|
1725
|
-
registerBaileysEventJournal(sock, generation);
|
|
2151
|
+
registerBaileysEventJournal(sock, generation, safeSessionId);
|
|
1726
2152
|
|
|
1727
2153
|
logger.info('Conexão com o WhatsApp estabelecida com sucesso.', {
|
|
1728
2154
|
action: 'connect_success',
|
|
2155
|
+
sessionId: safeSessionId,
|
|
1729
2156
|
generation,
|
|
1730
2157
|
timestamp: __timeNowIso(),
|
|
1731
2158
|
});
|
|
1732
2159
|
})();
|
|
1733
2160
|
|
|
2161
|
+
context.connectPromise = currentConnectPromise;
|
|
2162
|
+
|
|
1734
2163
|
try {
|
|
1735
|
-
await
|
|
2164
|
+
await currentConnectPromise;
|
|
1736
2165
|
} finally {
|
|
1737
|
-
connectPromise
|
|
2166
|
+
if (context.connectPromise === currentConnectPromise) {
|
|
2167
|
+
context.connectPromise = null;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Conecta todas as sessões configuradas no runtime.
|
|
2174
|
+
* @returns {Promise<void>}
|
|
2175
|
+
*/
|
|
2176
|
+
export async function connectAllWhatsAppSessions() {
|
|
2177
|
+
const results = await Promise.allSettled(BAILEYS_SESSION_IDS.map((sessionId) => connectToWhatsApp(sessionId)));
|
|
2178
|
+
const failures = results.map((result, index) => ({ result, sessionId: BAILEYS_SESSION_IDS[index] })).filter(({ result }) => result.status === 'rejected');
|
|
2179
|
+
|
|
2180
|
+
if (failures.length > 0) {
|
|
2181
|
+
const error = new Error(`Falha ao conectar ${failures.length}/${BAILEYS_SESSION_IDS.length} sessões do WhatsApp.`);
|
|
2182
|
+
// @ts-ignore enrich error object for logs
|
|
2183
|
+
error.failures = failures.map(({ sessionId, result }) => ({
|
|
2184
|
+
sessionId,
|
|
2185
|
+
message: result.reason?.message || String(result.reason || ''),
|
|
2186
|
+
}));
|
|
2187
|
+
throw error;
|
|
1738
2188
|
}
|
|
1739
2189
|
}
|
|
1740
2190
|
|
|
@@ -1744,15 +2194,23 @@ export async function connectToWhatsApp() {
|
|
|
1744
2194
|
* @async
|
|
1745
2195
|
* @param {import('@whiskeysockets/baileys').ConnectionState} update - Objeto contendo o estado atual da conexão.
|
|
1746
2196
|
* @param {import('@whiskeysockets/baileys').WASocket} sock - Instância do socket do WhatsApp que disparou a atualização.
|
|
2197
|
+
* @param {string} sessionId - Sessão do socket.
|
|
2198
|
+
* @param {number} generation - Geração do socket.
|
|
1747
2199
|
* @returns {Promise<void>} Uma promessa que resolve quando o processamento do estado da conexão é concluído.
|
|
1748
2200
|
*/
|
|
1749
|
-
async function handleConnectionUpdate(update, sock) {
|
|
1750
|
-
|
|
2201
|
+
async function handleConnectionUpdate(update, sock, sessionId, generation) {
|
|
2202
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
2203
|
+
const context = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
2204
|
+
if (!context) return;
|
|
2205
|
+
if (context.socket !== sock) return;
|
|
2206
|
+
if (context.socketGeneration !== generation) return;
|
|
2207
|
+
|
|
1751
2208
|
const { connection, lastDisconnect, qr } = update;
|
|
1752
2209
|
|
|
1753
2210
|
if (qr) {
|
|
1754
2211
|
logger.info('📱 QR Code gerado! Escaneie com seu WhatsApp.', {
|
|
1755
2212
|
action: 'qr_code_generated',
|
|
2213
|
+
sessionId: safeSessionId,
|
|
1756
2214
|
timestamp: __timeNowIso(),
|
|
1757
2215
|
});
|
|
1758
2216
|
qrcode.generate(qr, { small: true });
|
|
@@ -1763,13 +2221,32 @@ async function handleConnectionUpdate(update, sock) {
|
|
|
1763
2221
|
const errorMessage = lastDisconnect?.error?.message || 'Sem mensagem de erro';
|
|
1764
2222
|
|
|
1765
2223
|
const shouldReconnect = lastDisconnect?.error instanceof Boom && disconnectCode !== DisconnectReason.loggedOut;
|
|
2224
|
+
stopGroupOwnerHeartbeat(safeSessionId, shouldReconnect ? 'connection_close_reconnect' : 'connection_close_final');
|
|
2225
|
+
void sessionRegistryService
|
|
2226
|
+
.markSessionDisconnected(safeSessionId, {
|
|
2227
|
+
status: shouldReconnect ? 'reconnecting' : 'offline',
|
|
2228
|
+
metadata: {
|
|
2229
|
+
reasonCode: disconnectCode,
|
|
2230
|
+
errorMessage,
|
|
2231
|
+
shouldReconnect,
|
|
2232
|
+
},
|
|
2233
|
+
})
|
|
2234
|
+
.catch((error) => {
|
|
2235
|
+
logger.warn('Falha ao registrar sessao offline no registry.', {
|
|
2236
|
+
action: 'session_registry_mark_disconnected_failed',
|
|
2237
|
+
sessionId: safeSessionId,
|
|
2238
|
+
reasonCode: disconnectCode,
|
|
2239
|
+
error: error?.message,
|
|
2240
|
+
});
|
|
2241
|
+
});
|
|
1766
2242
|
|
|
1767
2243
|
if (shouldReconnect) {
|
|
1768
|
-
const attempt = getNextReconnectAttempt();
|
|
2244
|
+
const attempt = getNextReconnectAttempt(safeSessionId);
|
|
1769
2245
|
if (attempt <= MAX_CONNECTION_ATTEMPTS) {
|
|
1770
2246
|
const reconnectDelay = INITIAL_RECONNECT_DELAY * Math.pow(2, attempt - 1);
|
|
1771
2247
|
logger.warn(`⚠️ Conexão perdida. Tentando reconectar...`, {
|
|
1772
2248
|
action: 'reconnect_attempt',
|
|
2249
|
+
sessionId: safeSessionId,
|
|
1773
2250
|
attempt,
|
|
1774
2251
|
maxAttempts: MAX_CONNECTION_ATTEMPTS,
|
|
1775
2252
|
delay: reconnectDelay,
|
|
@@ -1777,12 +2254,14 @@ async function handleConnectionUpdate(update, sock) {
|
|
|
1777
2254
|
errorMessage,
|
|
1778
2255
|
timestamp: __timeNowIso(),
|
|
1779
2256
|
});
|
|
1780
|
-
|
|
1781
|
-
storeActiveSocket(null);
|
|
1782
|
-
|
|
2257
|
+
context.socket = null;
|
|
2258
|
+
storeActiveSocket(null, safeSessionId);
|
|
2259
|
+
syncLegacyActiveSocketReference();
|
|
2260
|
+
scheduleReconnect(safeSessionId, reconnectDelay);
|
|
1783
2261
|
} else {
|
|
1784
2262
|
logger.error('❌ Limite de tentativas atingido; aguardando janela para novo retry.', {
|
|
1785
2263
|
action: 'reconnect_backoff_window',
|
|
2264
|
+
sessionId: safeSessionId,
|
|
1786
2265
|
totalAttempts: attempt,
|
|
1787
2266
|
maxAttempts: MAX_CONNECTION_ATTEMPTS,
|
|
1788
2267
|
retryAfterMs: BAILEYS_RECONNECT_ATTEMPT_RESET_MS,
|
|
@@ -1790,38 +2269,60 @@ async function handleConnectionUpdate(update, sock) {
|
|
|
1790
2269
|
errorMessage,
|
|
1791
2270
|
timestamp: __timeNowIso(),
|
|
1792
2271
|
});
|
|
1793
|
-
|
|
1794
|
-
storeActiveSocket(null);
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2272
|
+
context.socket = null;
|
|
2273
|
+
storeActiveSocket(null, safeSessionId);
|
|
2274
|
+
syncLegacyActiveSocketReference();
|
|
2275
|
+
context.connectionAttempts = 0;
|
|
2276
|
+
context.reconnectWindowStartedAt = __timeNowMs();
|
|
2277
|
+
scheduleReconnect(safeSessionId, BAILEYS_RECONNECT_ATTEMPT_RESET_MS);
|
|
1798
2278
|
}
|
|
1799
2279
|
} else {
|
|
1800
2280
|
logger.error('❌ Conexão fechada definitivamente.', {
|
|
1801
2281
|
action: 'connection_closed',
|
|
2282
|
+
sessionId: safeSessionId,
|
|
1802
2283
|
reasonCode: disconnectCode,
|
|
1803
2284
|
errorMessage,
|
|
1804
2285
|
timestamp: __timeNowIso(),
|
|
1805
2286
|
});
|
|
1806
|
-
|
|
1807
|
-
storeActiveSocket(null);
|
|
1808
|
-
|
|
2287
|
+
context.socket = null;
|
|
2288
|
+
storeActiveSocket(null, safeSessionId);
|
|
2289
|
+
syncLegacyActiveSocketReference();
|
|
2290
|
+
await releaseBaileysWriterLock(safeSessionId, 'connection_closed_no_reconnect');
|
|
1809
2291
|
}
|
|
1810
2292
|
}
|
|
1811
2293
|
|
|
1812
2294
|
if (connection === 'open') {
|
|
1813
2295
|
logger.info('✅ Conectado com sucesso ao WhatsApp!', {
|
|
1814
2296
|
action: 'connection_open',
|
|
2297
|
+
sessionId: safeSessionId,
|
|
1815
2298
|
timestamp: __timeNowIso(),
|
|
1816
2299
|
});
|
|
1817
2300
|
|
|
1818
|
-
resetReconnectState();
|
|
1819
|
-
clearReconnectTimeout();
|
|
2301
|
+
resetReconnectState(safeSessionId);
|
|
2302
|
+
clearReconnectTimeout(safeSessionId);
|
|
2303
|
+
startGroupOwnerHeartbeat(safeSessionId, generation);
|
|
2304
|
+
void sessionRegistryService
|
|
2305
|
+
.markSessionConnected(safeSessionId, {
|
|
2306
|
+
botJid: normalizeJid(sock?.user?.id || sock?.authState?.creds?.me?.id || sock?.authState?.creds?.me?.lid) || undefined,
|
|
2307
|
+
metadata: {
|
|
2308
|
+
source: 'connection_open',
|
|
2309
|
+
socketGeneration: generation,
|
|
2310
|
+
},
|
|
2311
|
+
capacityWeight: Math.max(1, Number(MULTI_SESSION_RUNTIME_CONFIG?.sessionWeights?.[safeSessionId] || 1)),
|
|
2312
|
+
})
|
|
2313
|
+
.catch((error) => {
|
|
2314
|
+
logger.warn('Falha ao registrar sessao online no registry.', {
|
|
2315
|
+
action: 'session_registry_mark_connected_failed',
|
|
2316
|
+
sessionId: safeSessionId,
|
|
2317
|
+
error: error?.message,
|
|
2318
|
+
});
|
|
2319
|
+
});
|
|
1820
2320
|
|
|
1821
2321
|
if (process.send) {
|
|
1822
2322
|
process.send('ready');
|
|
1823
2323
|
logger.info('🟢 Sinal de "ready" enviado ao PM2.', {
|
|
1824
2324
|
action: 'pm2_ready_signal',
|
|
2325
|
+
sessionId: safeSessionId,
|
|
1825
2326
|
timestamp: __timeNowIso(),
|
|
1826
2327
|
});
|
|
1827
2328
|
}
|
|
@@ -1831,6 +2332,7 @@ async function handleConnectionUpdate(update, sock) {
|
|
|
1831
2332
|
} catch (error) {
|
|
1832
2333
|
logger.error('❌ Erro ao carregar metadados de grupos na conexão.', {
|
|
1833
2334
|
action: 'groups_load_error',
|
|
2335
|
+
sessionId: safeSessionId,
|
|
1834
2336
|
errorMessage: error.message,
|
|
1835
2337
|
stack: error.stack,
|
|
1836
2338
|
timeoutMs: GROUP_SYNC_TIMEOUT_MS,
|
|
@@ -1953,6 +2455,7 @@ async function handleGroupUpdate(updates) {
|
|
|
1953
2455
|
* @returns {import('@whiskeysockets/baileys').WASocket | null} O objeto socket do Baileys ativo ou `null` se não houver conexão ativa.
|
|
1954
2456
|
*/
|
|
1955
2457
|
export function getActiveSocket() {
|
|
2458
|
+
syncLegacyActiveSocketReference();
|
|
1956
2459
|
logger.debug('🔍 Recuperando instância do socket ativo.', {
|
|
1957
2460
|
action: 'get_active_socket',
|
|
1958
2461
|
socketExists: !!activeSocket,
|
|
@@ -1961,6 +2464,17 @@ export function getActiveSocket() {
|
|
|
1961
2464
|
return activeSocket;
|
|
1962
2465
|
}
|
|
1963
2466
|
|
|
2467
|
+
/**
|
|
2468
|
+
* Retorna o socket de uma sessão específica.
|
|
2469
|
+
* @param {string} sessionId
|
|
2470
|
+
* @returns {import('@whiskeysockets/baileys').WASocket | null}
|
|
2471
|
+
*/
|
|
2472
|
+
export function getSocketBySession(sessionId = BAILEYS_PRIMARY_SESSION_ID) {
|
|
2473
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
2474
|
+
const context = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
2475
|
+
return context?.socket || null;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
1964
2478
|
/**
|
|
1965
2479
|
* Executa um método centralizado no socket ativo, tratando erros e mapeando-os para respostas HTTP.
|
|
1966
2480
|
* @async
|
|
@@ -1970,6 +2484,7 @@ export function getActiveSocket() {
|
|
|
1970
2484
|
* @throws {Boom} Retorna um erro HTTP 503 se o socket não estiver disponível, ou 501 se o método não existir.
|
|
1971
2485
|
*/
|
|
1972
2486
|
async function runControllerSocketMethod(methodName, ...args) {
|
|
2487
|
+
const socket = getActiveSocket();
|
|
1973
2488
|
try {
|
|
1974
2489
|
return await runActiveSocketMethod(methodName, ...args);
|
|
1975
2490
|
} catch (error) {
|
|
@@ -1977,8 +2492,8 @@ async function runControllerSocketMethod(methodName, ...args) {
|
|
|
1977
2492
|
if (message.includes('Socket do WhatsApp indisponível')) {
|
|
1978
2493
|
logger.warn('Socket ativo indisponível para operação.', {
|
|
1979
2494
|
action: methodName,
|
|
1980
|
-
socketExists: !!
|
|
1981
|
-
socketOpen: isSocketOpen(
|
|
2495
|
+
socketExists: !!socket,
|
|
2496
|
+
socketOpen: isSocketOpen(socket),
|
|
1982
2497
|
timestamp: __timeNowIso(),
|
|
1983
2498
|
});
|
|
1984
2499
|
throw new Boom('Socket do WhatsApp indisponível no momento.', { statusCode: 503 });
|
|
@@ -2207,23 +2722,79 @@ export async function rejectCall(callId, callFrom) {
|
|
|
2207
2722
|
* Encerra o socket ativo atual, se existir, para disparar a lógica de reconexão.
|
|
2208
2723
|
* Se nenhum socket estiver ativo, inicia uma nova conexão.
|
|
2209
2724
|
* @async
|
|
2725
|
+
* @param {string} [sessionId=BAILEYS_PRIMARY_SESSION_ID]
|
|
2210
2726
|
* @returns {Promise<void>} Uma promessa que resolve quando o fluxo de reconexão é iniciado ou uma nova conexão é tentada.
|
|
2211
2727
|
*/
|
|
2212
|
-
export async function reconnectToWhatsApp() {
|
|
2213
|
-
|
|
2214
|
-
|
|
2728
|
+
export async function reconnectToWhatsApp(sessionId = BAILEYS_PRIMARY_SESSION_ID) {
|
|
2729
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
2730
|
+
const targetSocket = getSocketBySession(safeSessionId);
|
|
2731
|
+
if (targetSocket && isSocketOpen(targetSocket)) {
|
|
2215
2732
|
logger.info('♻️ Forçando fechamento do socket para reconectar...', {
|
|
2216
2733
|
action: 'force_reconnect',
|
|
2734
|
+
sessionId: safeSessionId,
|
|
2217
2735
|
timestamp: __timeNowIso(),
|
|
2218
2736
|
});
|
|
2219
|
-
|
|
2737
|
+
targetSocket.ws?.close?.();
|
|
2220
2738
|
} else {
|
|
2221
2739
|
logger.warn('⚠️ Nenhum socket ativo detectado. Iniciando nova conexão manualmente.', {
|
|
2222
2740
|
action: 'reconnect_no_active_socket',
|
|
2741
|
+
sessionId: safeSessionId,
|
|
2223
2742
|
timestamp: __timeNowIso(),
|
|
2224
2743
|
});
|
|
2225
|
-
await connectToWhatsApp();
|
|
2744
|
+
await connectToWhatsApp(safeSessionId);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
/**
|
|
2749
|
+
* Encerra todas as sessões ativas no processo.
|
|
2750
|
+
* @param {{ releaseLocks?: boolean }} [options]
|
|
2751
|
+
* @returns {Promise<void>}
|
|
2752
|
+
*/
|
|
2753
|
+
export async function disconnectAllWhatsAppSessions(options = {}) {
|
|
2754
|
+
const { releaseLocks = true } = options;
|
|
2755
|
+
const targetSessionIds = Array.from(new Set([...BAILEYS_SESSION_IDS, ...sessionContexts.keys()]));
|
|
2756
|
+
|
|
2757
|
+
await Promise.allSettled(
|
|
2758
|
+
targetSessionIds.map(async (sessionId) => {
|
|
2759
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
2760
|
+
const context = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
2761
|
+
if (!context) return;
|
|
2762
|
+
|
|
2763
|
+
clearReconnectTimeout(safeSessionId);
|
|
2764
|
+
stopGroupOwnerHeartbeat(safeSessionId, 'disconnect_all_sessions');
|
|
2765
|
+
|
|
2766
|
+
const socket = context.socket;
|
|
2767
|
+
context.socket = null;
|
|
2768
|
+
context.connectPromise = null;
|
|
2769
|
+
storeActiveSocket(null, safeSessionId);
|
|
2770
|
+
|
|
2771
|
+
if (socket && typeof socket.end === 'function') {
|
|
2772
|
+
try {
|
|
2773
|
+
await socket.end();
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
logger.warn('Falha ao encerrar sessão do WhatsApp.', {
|
|
2776
|
+
action: 'disconnect_session_failed',
|
|
2777
|
+
sessionId: safeSessionId,
|
|
2778
|
+
errorMessage: error?.message,
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
void sessionRegistryService
|
|
2784
|
+
.markSessionDisconnected(safeSessionId, {
|
|
2785
|
+
status: 'offline',
|
|
2786
|
+
metadata: {
|
|
2787
|
+
reason: 'disconnect_all_sessions',
|
|
2788
|
+
},
|
|
2789
|
+
})
|
|
2790
|
+
.catch(() => {});
|
|
2791
|
+
}),
|
|
2792
|
+
);
|
|
2793
|
+
|
|
2794
|
+
if (releaseLocks) {
|
|
2795
|
+
await releaseAllBaileysWriterLocks('disconnect_all_sessions');
|
|
2226
2796
|
}
|
|
2797
|
+
syncLegacyActiveSocketReference();
|
|
2227
2798
|
}
|
|
2228
2799
|
|
|
2229
2800
|
if (process.argv[1] === __filename) {
|