@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.
Files changed (172) hide show
  1. package/.env.example +78 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +6 -0
  6. package/app/configParts/adminIdentity.js +36 -7
  7. package/app/configParts/baileysConfig.js +343 -56
  8. package/app/configParts/groupUtils.js +226 -0
  9. package/app/configParts/loggerConfig.js +185 -0
  10. package/app/configParts/messagePersistenceService.js +307 -5
  11. package/app/configParts/sessionConfig.js +242 -0
  12. package/app/connection/baileysCompatibility.test.js +10 -1
  13. package/app/connection/baileysDbAuthState.js +205 -9
  14. package/app/connection/baileysLibsignalPatch.js +210 -0
  15. package/app/connection/groupOwnerWriteStateResolver.js +141 -0
  16. package/app/connection/socketController.js +694 -123
  17. package/app/connection/socketController.multiSession.test.js +128 -0
  18. package/app/controllers/messageController.js +1 -1
  19. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  20. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  21. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  22. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
  23. package/app/controllers/messageProcessingPipeline.js +90 -9
  24. package/app/controllers/messageProcessingPipeline.test.js +202 -0
  25. package/app/modules/adminModule/AGENT.md +1 -1
  26. package/app/modules/adminModule/commandConfig.json +3318 -1347
  27. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  28. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  29. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  30. package/app/modules/aiModule/AGENT.md +47 -30
  31. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  32. package/app/modules/aiModule/catCommand.js +132 -25
  33. package/app/modules/aiModule/commandConfig.json +114 -28
  34. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  35. package/app/modules/gameModule/AGENT.md +1 -1
  36. package/app/modules/gameModule/commandConfig.json +29 -0
  37. package/app/modules/menuModule/AGENT.md +1 -1
  38. package/app/modules/menuModule/commandConfig.json +45 -10
  39. package/app/modules/menuModule/menuCatalogService.js +190 -0
  40. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  41. package/app/modules/menuModule/menuDynamicService.js +511 -0
  42. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  43. package/app/modules/menuModule/menus.js +36 -5
  44. package/app/modules/playModule/AGENT.md +10 -5
  45. package/app/modules/playModule/commandConfig.json +74 -16
  46. package/app/modules/playModule/playCommandConstants.js +13 -7
  47. package/app/modules/playModule/playCommandCore.js +4 -6
  48. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  49. package/app/modules/playModule/playConfigRuntime.js +5 -6
  50. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  51. package/app/modules/quoteModule/AGENT.md +1 -1
  52. package/app/modules/quoteModule/commandConfig.json +29 -0
  53. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  54. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  55. package/app/modules/statsModule/AGENT.md +1 -1
  56. package/app/modules/statsModule/commandConfig.json +58 -0
  57. package/app/modules/stickerModule/AGENT.md +1 -1
  58. package/app/modules/stickerModule/commandConfig.json +145 -0
  59. package/app/modules/stickerPackModule/AGENT.md +1 -1
  60. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  61. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  62. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  63. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  64. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  65. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  66. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  67. package/app/modules/tiktokModule/AGENT.md +1 -1
  68. package/app/modules/tiktokModule/commandConfig.json +29 -0
  69. package/app/modules/userModule/AGENT.md +1 -1
  70. package/app/modules/userModule/commandConfig.json +29 -0
  71. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  72. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  73. package/app/observability/metrics.js +136 -0
  74. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  75. package/app/services/ai/geminiService.js +131 -7
  76. package/app/services/ai/geminiService.test.js +59 -2
  77. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  78. package/app/services/group/groupMetadataService.js +24 -1
  79. package/app/services/infra/dbWriteQueue.js +51 -21
  80. package/app/services/messaging/newsBroadcastService.js +843 -27
  81. package/app/services/multiSession/assignmentBalancerService.js +452 -0
  82. package/app/services/multiSession/groupOwnershipRepository.js +346 -0
  83. package/app/services/multiSession/groupOwnershipService.js +809 -0
  84. package/app/services/multiSession/groupOwnershipService.test.js +317 -0
  85. package/app/services/multiSession/sessionRegistryService.js +239 -0
  86. package/app/store/aiPromptStore.js +36 -19
  87. package/app/store/groupConfigStore.js +41 -5
  88. package/app/store/premiumUserStore.js +21 -7
  89. package/app/utils/antiLink/antiLinkModule.js +391 -25
  90. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  91. package/database/index.js +6 -0
  92. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  93. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  94. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  95. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  96. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  97. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  98. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  99. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  100. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  101. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  102. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  103. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  104. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  105. package/database/schema.sql +102 -1
  106. package/docker-compose.yml +4 -1
  107. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  108. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  109. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  110. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  111. package/docs/security/omnizap-static-security-headers.conf +25 -0
  112. package/ecosystem.prod.config.cjs +31 -11
  113. package/index.js +52 -18
  114. package/observability/alert-rules.yml +20 -0
  115. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  116. package/observability/mysql-setup.sql +4 -4
  117. package/observability/system-admin-observability.md +26 -0
  118. package/package.json +14 -6
  119. package/public/comandos/commands-catalog.json +2253 -78
  120. package/public/css/payments-react.css +478 -0
  121. package/public/js/apps/commandsReactApp.js +267 -87
  122. package/public/js/apps/createPackApp.js +3 -3
  123. package/public/js/apps/homeReactApp.js +2 -2
  124. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  125. package/public/js/apps/paymentsReactApp.js +399 -0
  126. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  127. package/public/js/apps/stickersApp.js +255 -103
  128. package/public/js/apps/termsReactApp.js +57 -8
  129. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  130. package/public/js/apps/userReactApp.js +96 -47
  131. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  132. package/public/pages/pagamentos-cancelado.html +21 -0
  133. package/public/pages/pagamentos-sucesso.html +21 -0
  134. package/public/pages/pagamentos.html +30 -0
  135. package/public/pages/politica-de-privacidade.html +1 -1
  136. package/public/pages/stickers.html +5 -5
  137. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  138. package/public/pages/termos-de-uso.html +1 -1
  139. package/public/pages/user-password-reset.html +3 -4
  140. package/public/pages/user-systemadm.html +8 -462
  141. package/public/pages/user.html +1 -1
  142. package/scripts/clear-whatsapp-session.sh +123 -0
  143. package/scripts/core-ai-mode.mjs +163 -0
  144. package/scripts/deploy.sh +13 -0
  145. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  146. package/scripts/generate-commands-catalog.mjs +155 -0
  147. package/scripts/new-whatsapp-session.sh +564 -0
  148. package/scripts/security-web-surface-check.mjs +218 -0
  149. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  150. package/server/controllers/admin/systemAdminController.js +254 -0
  151. package/server/controllers/payments/paymentsController.js +731 -0
  152. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  153. package/server/controllers/system/contactController.js +9 -17
  154. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  155. package/server/controllers/system/systemController.js +228 -1
  156. package/server/controllers/userController.js +6 -0
  157. package/server/email/emailAutomationRuntime.js +36 -1
  158. package/server/email/emailAutomationService.js +42 -1
  159. package/server/email/emailTemplateService.js +140 -33
  160. package/server/http/httpRequestUtils.js +18 -14
  161. package/server/http/httpServer.js +8 -4
  162. package/server/middleware/securityHeaders.js +35 -3
  163. package/server/routes/admin/systemAdminRouter.js +6 -0
  164. package/server/routes/indexRouter.js +50 -6
  165. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  166. package/server/routes/payments/paymentsRouter.js +47 -0
  167. package/server/routes/static/staticPageRouter.js +30 -1
  168. package/server/utils/publicContact.js +31 -0
  169. package/utils/whatsapp/contactEnv.js +39 -0
  170. package/vite.config.mjs +5 -1
  171. package/app/modules/playModule/local/installYtDlp.js +0 -25
  172. 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
- * Identificador lógico da sessão de autenticação do Baileys no MySQL.
136
- * Permite isolar múltiplas sessões no mesmo banco.
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 BAILEYS_AUTH_SESSION_ID = (() => {
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 BAILEYS_SINGLE_WRITER_LOCK_NAME = (() => {
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
- * Contador de tentativas de conexão.
271
- * @type {number}
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
- let connectionAttempts = 0;
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
- * Timestamp do início da janela de reconexão.
276
- * @type {number}
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
- let reconnectWindowStartedAt = 0;
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 = buildMessageData(msg, canonicalSenderId);
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 results = await findBy('messages', { message_id: messageId, chat_id: remoteJid }, { limit: 1 });
1039
- const record = results?.[0];
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
- if (!reconnectTimeout) return;
1064
- reconnectTimeout.cancel();
1065
- reconnectTimeout = null;
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
- connectionAttempts = 0;
1074
- reconnectWindowStartedAt = 0;
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
- if (reconnectTimeout) return;
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 connection = baileysWriterLockConnection;
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
- baileysWriterLockConnection = null;
1510
+ const lockName = getWriterLockNameBySession(safeSessionId);
1511
+ context.writerLockConnection = null;
1143
1512
 
1144
1513
  try {
1145
- const rows = await executeQuery('SELECT RELEASE_LOCK(?) AS released', [BAILEYS_SINGLE_WRITER_LOCK_NAME], connection);
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: BAILEYS_SINGLE_WRITER_LOCK_NAME,
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: BAILEYS_SINGLE_WRITER_LOCK_NAME,
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
- if (baileysWriterLockConnection) {
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', [BAILEYS_SINGLE_WRITER_LOCK_NAME, BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS], connection);
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
- lockName: BAILEYS_SINGLE_WRITER_LOCK_NAME,
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
- baileysWriterLockConnection = connection;
1582
+ context.writerLockConnection = connection;
1206
1583
  logger.info('Lock de escritor do Baileys adquirido com sucesso.', {
1207
1584
  action: 'baileys_writer_lock_acquired',
1208
- lockName: BAILEYS_SINGLE_WRITER_LOCK_NAME,
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
- releaseBaileysWriterLock('before_exit').catch(() => {});
1616
+ releaseAllBaileysWriterLocks('before_exit').catch(() => {});
1225
1617
  });
1226
1618
 
1227
1619
  process.once('SIGINT', () => {
1228
- releaseBaileysWriterLock('sigint').catch(() => {});
1620
+ releaseAllBaileysWriterLocks('sigint').catch(() => {});
1229
1621
  });
1230
1622
 
1231
1623
  process.once('SIGTERM', () => {
1232
- releaseBaileysWriterLock('sigterm').catch(() => {});
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
- if (connectPromise) {
1321
- return connectPromise;
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(activeSocket)) {
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
- connectPromise = (async () => {
1333
- clearReconnectTimeout();
1334
- const isWriterReady = await ensureBaileysWriterLock();
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: BAILEYS_AUTH_SESSION_ID,
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
- authSessionId: BAILEYS_AUTH_SESSION_ID,
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
- activeSocket = sock;
1382
- storeActiveSocket(sock);
1781
+ context.socket = sock;
1782
+ storeActiveSocket(sock, safeSessionId);
1783
+ syncLegacyActiveSocketReference();
1383
1784
 
1384
- const isCurrentSocket = () => activeSocket === sock && generation === socketGeneration;
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 connectPromise;
2164
+ await currentConnectPromise;
1736
2165
  } finally {
1737
- connectPromise = null;
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
- if (sock !== activeSocket) return;
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
- activeSocket = null;
1781
- storeActiveSocket(null);
1782
- scheduleReconnect(reconnectDelay);
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
- activeSocket = null;
1794
- storeActiveSocket(null);
1795
- connectionAttempts = 0;
1796
- reconnectWindowStartedAt = __timeNowMs();
1797
- scheduleReconnect(BAILEYS_RECONNECT_ATTEMPT_RESET_MS);
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
- activeSocket = null;
1807
- storeActiveSocket(null);
1808
- await releaseBaileysWriterLock('connection_closed_no_reconnect');
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: !!activeSocket,
1981
- socketOpen: isSocketOpen(activeSocket),
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
- // eslint-disable-next-line no-undef
2214
- if (activeSocket && activeSocket.ws?.readyState === WebSocket.OPEN) {
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
- activeSocket.ws.close();
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) {