@omnizap-system/omnizap 2.6.1 → 2.6.2

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 (156) hide show
  1. package/.env.example +54 -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 +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. 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,11 +16,18 @@ 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 {
25
+ getOwner as getGroupOwner,
26
+ tryAcquire as tryAcquireGroupOwner,
27
+ heartbeatOwnerSession as heartbeatGroupOwnerSession,
28
+ } from '../services/multiSession/groupOwnershipService.js';
29
+ import sessionRegistryService from '../services/multiSession/sessionRegistryService.js';
30
+ import { createGroupOwnerWriteStateResolver, normalizeAssignmentVersion } from './groupOwnerWriteStateResolver.js';
24
31
  import { useDbAuthState } from './baileysDbAuthState.js';
25
32
 
26
33
  import { fileURLToPath } from 'node:url';
@@ -136,10 +143,10 @@ const BAILEYS_GROUP_METADATA_CACHE_CHECKPERIOD_SECONDS = parseEnvInt(process.env
136
143
  * Permite isolar múltiplas sessões no mesmo banco.
137
144
  * @type {string}
138
145
  */
139
- const BAILEYS_AUTH_SESSION_ID = (() => {
140
- const raw = String(process.env.BAILEYS_AUTH_SESSION_ID || '').trim();
141
- return raw || 'default';
142
- })();
146
+ const MULTI_SESSION_RUNTIME_CONFIG = getMultiSessionRuntimeConfig();
147
+ 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']);
148
+ const BAILEYS_SESSION_ID_SET = new Set(BAILEYS_SESSION_IDS);
149
+ const BAILEYS_PRIMARY_SESSION_ID = String(MULTI_SESSION_RUNTIME_CONFIG?.primarySessionId || BAILEYS_SESSION_IDS[0] || 'default').trim() || 'default';
143
150
  /**
144
151
  * Habilita bootstrap inicial do auth state no MySQL usando os arquivos locais legados.
145
152
  * @type {boolean}
@@ -164,12 +171,80 @@ const BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS = parseEnvInt(process.env.BAILEY
164
171
  * Nome do lock de escritor único usado no MySQL.
165
172
  * @type {string}
166
173
  */
167
- const BAILEYS_SINGLE_WRITER_LOCK_NAME = (() => {
174
+ const BAILEYS_SINGLE_WRITER_LOCK_NAME_BASE = (() => {
168
175
  const raw = String(process.env.BAILEYS_SINGLE_WRITER_LOCK_NAME || '').trim();
169
176
  if (raw) return raw;
170
177
  const dbLabel = String(dbConfig?.database || 'db').replace(/[^a-zA-Z0-9:_-]+/g, '_');
171
178
  return `omnizap:baileys:writer:${dbLabel}`;
172
179
  })();
180
+
181
+ const normalizeSessionId = (sessionId) => {
182
+ const normalized = String(sessionId || '').trim();
183
+ if (!normalized) return BAILEYS_PRIMARY_SESSION_ID;
184
+ if (!BAILEYS_SESSION_ID_SET.has(normalized)) return BAILEYS_PRIMARY_SESSION_ID;
185
+ return normalized;
186
+ };
187
+
188
+ const getWriterLockNameBySession = (sessionId) => {
189
+ const safeSessionId = normalizeSessionId(sessionId);
190
+ const base = BAILEYS_SINGLE_WRITER_LOCK_NAME_BASE;
191
+ if (base.includes('{sessionId}')) {
192
+ return base.replace(/\{sessionId\}/g, safeSessionId);
193
+ }
194
+ return `${base}:${safeSessionId}`;
195
+ };
196
+
197
+ const GROUP_OWNER_WRITE_CACHE_TTL_MS = parseEnvInt(
198
+ process.env.GROUP_OWNER_WRITE_CACHE_TTL_MS,
199
+ Math.max(2_000, Math.floor((Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerHeartbeatMs) || 30_000) / 3)),
200
+ 1_000,
201
+ 60_000,
202
+ );
203
+ const GROUP_OWNER_WRITE_CLAIM_ON_MISS = parseEnvBool(process.env.GROUP_OWNER_WRITE_CLAIM_ON_MISS, true);
204
+ const GROUP_OWNER_LEASE_MS = Math.max(5_000, Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerLeaseMs) || 120_000);
205
+ let GROUP_OWNER_HEARTBEAT_MS = parseEnvInt(
206
+ process.env.GROUP_OWNER_HEARTBEAT_RUNTIME_MS,
207
+ Math.max(1_000, Math.min(GROUP_OWNER_LEASE_MS - 500, Number(MULTI_SESSION_RUNTIME_CONFIG?.ownerHeartbeatMs) || 30_000)),
208
+ 1_000,
209
+ 5 * 60 * 1000,
210
+ );
211
+ if (GROUP_OWNER_HEARTBEAT_MS >= GROUP_OWNER_LEASE_MS) {
212
+ GROUP_OWNER_HEARTBEAT_MS = Math.max(1_000, Math.floor(GROUP_OWNER_LEASE_MS / 2));
213
+ }
214
+ const groupOwnerWriteStateCache = new NodeCache({
215
+ stdTTL: Math.max(1, Math.ceil(GROUP_OWNER_WRITE_CACHE_TTL_MS / 1000)),
216
+ checkperiod: Math.max(1, Math.ceil(GROUP_OWNER_WRITE_CACHE_TTL_MS / 1000)),
217
+ useClones: false,
218
+ });
219
+
220
+ const buildGroupOwnerWriteCacheKey = (groupJid, sessionId) => {
221
+ const safeGroupJid = String(groupJid || '').trim();
222
+ const safeSessionId = normalizeSessionId(sessionId);
223
+ if (!safeGroupJid || !safeSessionId) return '';
224
+ return `${safeSessionId}:${safeGroupJid}`;
225
+ };
226
+
227
+ const clearGroupOwnerWriteCacheForSession = (sessionId) => {
228
+ const safeSessionId = normalizeSessionId(sessionId);
229
+ const prefix = `${safeSessionId}:`;
230
+ const keys = groupOwnerWriteStateCache.keys();
231
+ for (const key of keys) {
232
+ if (String(key || '').startsWith(prefix)) {
233
+ groupOwnerWriteStateCache.del(key);
234
+ }
235
+ }
236
+ };
237
+
238
+ const resolveGroupOwnerWriteState = createGroupOwnerWriteStateResolver({
239
+ getOwnerImpl: getGroupOwner,
240
+ tryAcquireImpl: tryAcquireGroupOwner,
241
+ cacheImpl: groupOwnerWriteStateCache,
242
+ isGroupJidImpl: isGroupJid,
243
+ normalizeSessionIdImpl: normalizeSessionId,
244
+ buildCacheKeyImpl: buildGroupOwnerWriteCacheKey,
245
+ loggerImpl: logger,
246
+ defaultAllowClaim: GROUP_OWNER_WRITE_CLAIM_ON_MISS,
247
+ });
173
248
  /**
174
249
  * Habilita ou desabilita o diário de eventos do Baileys.
175
250
  * @type {boolean}
@@ -267,15 +342,63 @@ const normalizeMessageReceiptType = (receiptType) => {
267
342
  */
268
343
  let activeSocket = null;
269
344
  /**
270
- * Contador de tentativas de conexão.
271
- * @type {number}
272
- */
273
- let connectionAttempts = 0;
274
- /**
275
- * Timestamp do início da janela de reconexão.
276
- * @type {number}
277
- */
278
- let reconnectWindowStartedAt = 0;
345
+ * Contexto runtime de cada sessão de WhatsApp.
346
+ * @typedef {{
347
+ * sessionId: string,
348
+ * socket: import('@whiskeysockets/baileys').WASocket|null,
349
+ * connectPromise: Promise<void>|null,
350
+ * reconnectTimeout: ReturnType<typeof delayCancellable>|null,
351
+ * reconnectWindowStartedAt: number,
352
+ * connectionAttempts: number,
353
+ * socketGeneration: number,
354
+ * writerLockConnection: import('mysql2/promise').PoolConnection|null,
355
+ * ownerHeartbeatInterval: ReturnType<typeof setInterval>|null,
356
+ * ownerHeartbeatInFlight: boolean
357
+ * }} SessionContext
358
+ */
359
+ /**
360
+ * Registry em memória de contexto por sessão.
361
+ * @type {Map<string, SessionContext>}
362
+ */
363
+ const sessionContexts = new Map();
364
+
365
+ const createSessionContext = (sessionId) => ({
366
+ sessionId,
367
+ socket: null,
368
+ connectPromise: null,
369
+ reconnectTimeout: null,
370
+ reconnectWindowStartedAt: 0,
371
+ connectionAttempts: 0,
372
+ socketGeneration: 0,
373
+ writerLockConnection: null,
374
+ ownerHeartbeatInterval: null,
375
+ ownerHeartbeatInFlight: false,
376
+ });
377
+
378
+ const getSessionContext = (sessionId, { createIfMissing = true } = {}) => {
379
+ const safeSessionId = normalizeSessionId(sessionId);
380
+ let context = sessionContexts.get(safeSessionId);
381
+ if (!context && createIfMissing) {
382
+ context = createSessionContext(safeSessionId);
383
+ sessionContexts.set(safeSessionId, context);
384
+ }
385
+ return context || null;
386
+ };
387
+
388
+ const resolvePreferredActiveSocket = () => {
389
+ const primaryContext = getSessionContext(BAILEYS_PRIMARY_SESSION_ID, { createIfMissing: false });
390
+ if (isSocketOpen(primaryContext?.socket)) return primaryContext.socket;
391
+
392
+ for (const context of sessionContexts.values()) {
393
+ if (isSocketOpen(context?.socket)) return context.socket;
394
+ }
395
+
396
+ return primaryContext?.socket || null;
397
+ };
398
+
399
+ const syncLegacyActiveSocketReference = () => {
400
+ activeSocket = resolvePreferredActiveSocket();
401
+ };
279
402
  /**
280
403
  * Cache para contadores de retentativa de mensagens.
281
404
  * @type {NodeCache}
@@ -322,26 +445,6 @@ const MAX_CONNECTION_ATTEMPTS = 5;
322
445
  * @type {number}
323
446
  */
324
447
  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
448
  /**
346
449
  * Nomes de todos os eventos do Baileys que são monitorados.
347
450
  * @type {string[]}
@@ -884,12 +987,15 @@ const queueContactsLidUpdates = (contacts, source) => {
884
987
  * Eventos selecionados são enfileirados para persistência.
885
988
  * @param {import('@whiskeysockets/baileys').WASocket} sock - A instância do socket do Baileys.
886
989
  * @param {number} generation - A geração atual do socket.
990
+ * @param {string} sessionId - Sessão associada ao socket.
887
991
  * @returns {void}
888
992
  */
889
- const registerBaileysEventJournal = (sock, generation) => {
993
+ const registerBaileysEventJournal = (sock, generation, sessionId) => {
994
+ const safeSessionId = normalizeSessionId(sessionId);
890
995
  if (!BAILEYS_EVENT_JOURNAL_ENABLED) {
891
996
  logger.debug('Journal de eventos Baileys desativado por configuração.', {
892
997
  action: 'baileys_event_journal_disabled',
998
+ sessionId: safeSessionId,
893
999
  });
894
1000
  return;
895
1001
  }
@@ -898,6 +1004,7 @@ const registerBaileysEventJournal = (sock, generation) => {
898
1004
  if (unknownEvents.length > 0) {
899
1005
  logger.warn('Alguns eventos configurados para journal não existem na lista conhecida do Baileys.', {
900
1006
  action: 'baileys_event_journal_unknown_events',
1007
+ sessionId: safeSessionId,
901
1008
  unknownEvents,
902
1009
  });
903
1010
  }
@@ -906,17 +1013,41 @@ const registerBaileysEventJournal = (sock, generation) => {
906
1013
  if (eventsToPersist.length === 0) {
907
1014
  logger.warn('Journal de eventos Baileys habilitado sem eventos válidos para persistir.', {
908
1015
  action: 'baileys_event_journal_empty',
1016
+ sessionId: safeSessionId,
909
1017
  configuredEvents: BAILEYS_EVENT_JOURNAL_EVENT_LIST,
910
1018
  });
911
1019
  return;
912
1020
  }
913
1021
 
914
1022
  for (const eventName of eventsToPersist) {
915
- sock.ev.on(eventName, (payload) => {
1023
+ sock.ev.on(eventName, async (payload) => {
916
1024
  try {
917
1025
  const summary = summarizeBaileysEventPayload(eventName, payload);
918
1026
  const refs = extractBaileysEventReferences(payload);
1027
+ if (isGroupJid(refs.chatId || '')) {
1028
+ const ownerWriteCacheKey = buildGroupOwnerWriteCacheKey(refs.chatId, safeSessionId);
1029
+ const expectedAssignmentVersion = normalizeAssignmentVersion(groupOwnerWriteStateCache.get(ownerWriteCacheKey)?.assignmentVersion);
1030
+ const ownerState = await resolveGroupOwnerWriteState(refs.chatId, safeSessionId, {
1031
+ source: `baileys_journal:${eventName}`,
1032
+ expectedAssignmentVersion,
1033
+ enforceFence: true,
1034
+ });
1035
+ if (!ownerState.allowed) {
1036
+ logger.debug('Evento Baileys de grupo ignorado para escrita por não-owner.', {
1037
+ action: 'baileys_event_group_write_skipped_non_owner',
1038
+ sessionId: safeSessionId,
1039
+ groupId: refs.chatId,
1040
+ ownerSessionId: ownerState.ownerSessionId,
1041
+ ownerAssignmentVersion: ownerState.assignmentVersion || null,
1042
+ expectedAssignmentVersion,
1043
+ reason: ownerState.reason,
1044
+ eventName,
1045
+ });
1046
+ return;
1047
+ }
1048
+ }
919
1049
  queueBaileysEventInsert({
1050
+ session_id: safeSessionId,
920
1051
  event_name: eventName,
921
1052
  socket_generation: generation,
922
1053
  chat_id: refs.chatId,
@@ -928,6 +1059,7 @@ const registerBaileysEventJournal = (sock, generation) => {
928
1059
  } catch (error) {
929
1060
  logger.warn('Falha ao enfileirar evento Baileys para journal.', {
930
1061
  action: 'baileys_event_journal_enqueue_failed',
1062
+ sessionId: safeSessionId,
931
1063
  eventName,
932
1064
  error: error?.message,
933
1065
  });
@@ -937,6 +1069,7 @@ const registerBaileysEventJournal = (sock, generation) => {
937
1069
 
938
1070
  logger.info('Journal de eventos Baileys habilitado.', {
939
1071
  action: 'baileys_event_journal_ready',
1072
+ sessionId: safeSessionId,
940
1073
  generation,
941
1074
  eventsCount: eventsToPersist.length,
942
1075
  events: eventsToPersist,
@@ -978,11 +1111,13 @@ const safeJsonParse = (value, fallback) => {
978
1111
  * @param {'append' | 'notify' | string} type - Tipo do evento de upsert.
979
1112
  * @returns {Promise<void>} Conclusão da persistência.
980
1113
  */
981
- async function persistIncomingMessages(incomingMessages, type) {
1114
+ async function persistIncomingMessages(incomingMessages, type, sessionId = BAILEYS_PRIMARY_SESSION_ID) {
982
1115
  if (type !== 'append' && type !== 'notify') return;
983
1116
 
984
1117
  const entries = [];
985
1118
  const lidsToPrime = new Set();
1119
+ const groupWriteStateByJid = new Map();
1120
+ const safeSessionId = normalizeSessionId(sessionId);
986
1121
 
987
1122
  for (const msg of incomingMessages) {
988
1123
  if (!msg.message || msg.key.remoteJid === 'status@broadcast') continue;
@@ -1018,7 +1153,36 @@ async function persistIncomingMessages(incomingMessages, type) {
1018
1153
 
1019
1154
  const canonicalSenderId = resolveUserIdCached(senderInfo) || msg.key.participant || msg.key.remoteJid;
1020
1155
 
1021
- const messageData = buildMessageData(msg, canonicalSenderId);
1156
+ const messageData = {
1157
+ ...buildMessageData(msg, canonicalSenderId, safeSessionId),
1158
+ };
1159
+ if (isGroupJid(messageData.chat_id || '')) {
1160
+ let ownerState = groupWriteStateByJid.get(messageData.chat_id);
1161
+ if (!ownerState) {
1162
+ const ownerWriteCacheKey = buildGroupOwnerWriteCacheKey(messageData.chat_id, safeSessionId);
1163
+ const expectedAssignmentVersion = normalizeAssignmentVersion(groupOwnerWriteStateCache.get(ownerWriteCacheKey)?.assignmentVersion);
1164
+ ownerState = await resolveGroupOwnerWriteState(messageData.chat_id, safeSessionId, {
1165
+ source: 'persist_incoming_messages',
1166
+ expectedAssignmentVersion,
1167
+ enforceFence: true,
1168
+ });
1169
+ groupWriteStateByJid.set(messageData.chat_id, ownerState);
1170
+ }
1171
+ if (!ownerState.allowed) {
1172
+ logger.debug('Persistência de mensagem de grupo ignorada para sessão não-owner.', {
1173
+ action: 'incoming_group_message_persistence_skipped_non_owner',
1174
+ sessionId: safeSessionId,
1175
+ groupId: messageData.chat_id,
1176
+ ownerSessionId: ownerState.ownerSessionId,
1177
+ ownerAssignmentVersion: ownerState.assignmentVersion || null,
1178
+ messageId: messageData.message_id,
1179
+ reason: ownerState.reason,
1180
+ });
1181
+ continue;
1182
+ }
1183
+ messageData.allow_group_write = true;
1184
+ }
1185
+
1022
1186
  queueMessageInsert(messageData);
1023
1187
  }
1024
1188
  }
@@ -1029,17 +1193,36 @@ async function persistIncomingMessages(incomingMessages, type) {
1029
1193
  * @param {import('@whiskeysockets/baileys').WAMessageKey} key - Chave da mensagem.
1030
1194
  * @returns {Promise<import('@whiskeysockets/baileys').proto.IMessage | undefined>} Conteúdo da mensagem armazenada.
1031
1195
  */
1032
- async function getStoredMessage(key) {
1196
+ async function getStoredMessage(key, sessionId = BAILEYS_PRIMARY_SESSION_ID) {
1033
1197
  const messageId = key?.id;
1034
1198
  const remoteJid = key?.remoteJid;
1035
1199
  if (!messageId || !remoteJid) return undefined;
1036
1200
 
1037
1201
  try {
1038
- const results = await findBy('messages', { message_id: messageId, chat_id: remoteJid }, { limit: 1 });
1039
- const record = results?.[0];
1202
+ const safeSessionId = normalizeSessionId(sessionId);
1203
+ let record = null;
1204
+
1205
+ try {
1206
+ const rows = await executeQuery(
1207
+ `SELECT raw_message
1208
+ FROM ${TABLES.MESSAGES}
1209
+ WHERE session_id = ? AND message_id = ? AND chat_id = ?
1210
+ LIMIT 1`,
1211
+ [safeSessionId, messageId, remoteJid],
1212
+ );
1213
+ record = rows?.[0] || null;
1214
+ } catch (error) {
1215
+ if (String(error?.code || '') !== 'ER_BAD_FIELD_ERROR') {
1216
+ throw error;
1217
+ }
1218
+ const fallbackRows = await findBy('messages', { message_id: messageId, chat_id: remoteJid }, { limit: 1 });
1219
+ record = fallbackRows?.[0] || null;
1220
+ }
1221
+
1040
1222
  const stored = safeJsonParse(record?.raw_message, null);
1041
1223
  if (record?.raw_message && !stored) {
1042
1224
  logger.error('Falha ao interpretar raw_message armazenado.', {
1225
+ sessionId: safeSessionId,
1043
1226
  messageId,
1044
1227
  remoteJid,
1045
1228
  });
@@ -1047,6 +1230,7 @@ async function getStoredMessage(key) {
1047
1230
  return stored?.message ?? undefined;
1048
1231
  } catch (error) {
1049
1232
  logger.error('Erro ao buscar mensagem armazenada no banco:', {
1233
+ sessionId: normalizeSessionId(sessionId),
1050
1234
  error: error.message,
1051
1235
  messageId,
1052
1236
  remoteJid,
@@ -1057,55 +1241,66 @@ async function getStoredMessage(key) {
1057
1241
 
1058
1242
  /**
1059
1243
  * Limpa o timeout de reconexão agendado, se houver.
1244
+ * @param {string} sessionId
1060
1245
  * @returns {void}
1061
1246
  */
1062
- const clearReconnectTimeout = () => {
1063
- if (!reconnectTimeout) return;
1064
- reconnectTimeout.cancel();
1065
- reconnectTimeout = null;
1247
+ const clearReconnectTimeout = (sessionId = BAILEYS_PRIMARY_SESSION_ID) => {
1248
+ const context = getSessionContext(sessionId, { createIfMissing: false });
1249
+ if (!context?.reconnectTimeout) return;
1250
+ context.reconnectTimeout.cancel();
1251
+ context.reconnectTimeout = null;
1066
1252
  };
1067
1253
 
1068
1254
  /**
1069
1255
  * Reseta o estado das tentativas de reconexão.
1256
+ * @param {string} sessionId
1070
1257
  * @returns {void}
1071
1258
  */
1072
- const resetReconnectState = () => {
1073
- connectionAttempts = 0;
1074
- reconnectWindowStartedAt = 0;
1259
+ const resetReconnectState = (sessionId = BAILEYS_PRIMARY_SESSION_ID) => {
1260
+ const context = getSessionContext(sessionId);
1261
+ context.connectionAttempts = 0;
1262
+ context.reconnectWindowStartedAt = 0;
1075
1263
  };
1076
1264
 
1077
1265
  /**
1078
1266
  * Calcula o número da próxima tentativa de reconexão.
1079
1267
  * Reseta a contagem de tentativas se a janela de reconexão expirou.
1268
+ * @param {string} sessionId
1080
1269
  * @returns {number} O número da próxima tentativa.
1081
1270
  */
1082
- const getNextReconnectAttempt = () => {
1271
+ const getNextReconnectAttempt = (sessionId = BAILEYS_PRIMARY_SESSION_ID) => {
1272
+ const context = getSessionContext(sessionId);
1083
1273
  const now = __timeNowMs();
1084
- if (!reconnectWindowStartedAt || now - reconnectWindowStartedAt >= BAILEYS_RECONNECT_ATTEMPT_RESET_MS) {
1085
- reconnectWindowStartedAt = now;
1086
- connectionAttempts = 0;
1274
+ if (!context.reconnectWindowStartedAt || now - context.reconnectWindowStartedAt >= BAILEYS_RECONNECT_ATTEMPT_RESET_MS) {
1275
+ context.reconnectWindowStartedAt = now;
1276
+ context.connectionAttempts = 0;
1087
1277
  }
1088
- connectionAttempts += 1;
1089
- return connectionAttempts;
1278
+ context.connectionAttempts += 1;
1279
+ return context.connectionAttempts;
1090
1280
  };
1091
1281
 
1092
1282
  /**
1093
1283
  * Agenda uma reconexão com o WhatsApp após um determinado atraso.
1094
1284
  * Evita agendar múltiplas reconexões.
1285
+ * @param {string} sessionId
1095
1286
  * @param {number} delay - O atraso em milissegundos antes de tentar a reconexão.
1096
1287
  * @returns {void}
1097
1288
  */
1098
- const scheduleReconnect = (delay) => {
1099
- if (reconnectTimeout) return;
1289
+ const scheduleReconnect = (sessionId, delay) => {
1290
+ const safeSessionId = normalizeSessionId(sessionId);
1291
+ const context = getSessionContext(safeSessionId);
1292
+ if (context.reconnectTimeout) return;
1293
+
1100
1294
  const pendingReconnect = delayCancellable(Math.max(0, Number(delay) || 0));
1101
- reconnectTimeout = pendingReconnect;
1295
+ context.reconnectTimeout = pendingReconnect;
1102
1296
  pendingReconnect.delay
1103
1297
  .then(() => {
1104
- if (reconnectTimeout !== pendingReconnect) return;
1105
- reconnectTimeout = null;
1106
- connectToWhatsApp().catch((error) => {
1298
+ if (context.reconnectTimeout !== pendingReconnect) return;
1299
+ context.reconnectTimeout = null;
1300
+ connectToWhatsApp(safeSessionId).catch((error) => {
1107
1301
  logger.error('Falha ao executar reconexão agendada.', {
1108
1302
  action: 'reconnect_schedule_failure',
1303
+ sessionId: safeSessionId,
1109
1304
  errorMessage: error?.message,
1110
1305
  stack: error?.stack,
1111
1306
  timestamp: __timeNowIso(),
@@ -1113,8 +1308,8 @@ const scheduleReconnect = (delay) => {
1113
1308
  });
1114
1309
  })
1115
1310
  .catch((error) => {
1116
- if (reconnectTimeout === pendingReconnect) {
1117
- reconnectTimeout = null;
1311
+ if (context.reconnectTimeout === pendingReconnect) {
1312
+ context.reconnectTimeout = null;
1118
1313
  }
1119
1314
  if (
1120
1315
  String(error?.message || '')
@@ -1125,37 +1320,147 @@ const scheduleReconnect = (delay) => {
1125
1320
  }
1126
1321
  logger.warn('Falha ao aguardar atraso da reconexão agendada.', {
1127
1322
  action: 'reconnect_schedule_delay_error',
1323
+ sessionId: safeSessionId,
1128
1324
  errorMessage: error?.message,
1129
1325
  });
1130
1326
  });
1131
1327
  };
1132
1328
 
1329
+ /**
1330
+ * Interrompe o heartbeat de ownership por sessão.
1331
+ * @param {string} sessionId
1332
+ * @param {string} reason
1333
+ * @returns {void}
1334
+ */
1335
+ const stopGroupOwnerHeartbeat = (sessionId, reason = 'unknown') => {
1336
+ const safeSessionId = normalizeSessionId(sessionId);
1337
+ const context = getSessionContext(safeSessionId, { createIfMissing: false });
1338
+ if (!context) return;
1339
+
1340
+ if (context.ownerHeartbeatInterval) {
1341
+ clearInterval(context.ownerHeartbeatInterval);
1342
+ context.ownerHeartbeatInterval = null;
1343
+ }
1344
+ context.ownerHeartbeatInFlight = false;
1345
+ clearGroupOwnerWriteCacheForSession(safeSessionId);
1346
+
1347
+ logger.debug('Heartbeat de ownership por grupo interrompido.', {
1348
+ action: 'group_owner_heartbeat_stopped',
1349
+ sessionId: safeSessionId,
1350
+ reason,
1351
+ });
1352
+ };
1353
+
1354
+ /**
1355
+ * Inicia heartbeat de ownership por sessão para renovar lease dos grupos de que a sessão é owner.
1356
+ * @param {string} sessionId
1357
+ * @param {number} generation
1358
+ * @returns {void}
1359
+ */
1360
+ const startGroupOwnerHeartbeat = (sessionId, generation) => {
1361
+ const safeSessionId = normalizeSessionId(sessionId);
1362
+ const context = getSessionContext(safeSessionId);
1363
+
1364
+ stopGroupOwnerHeartbeat(safeSessionId, 'restart');
1365
+
1366
+ const runTick = async () => {
1367
+ const latestContext = getSessionContext(safeSessionId, { createIfMissing: false });
1368
+ if (!latestContext) return;
1369
+ if (latestContext.ownerHeartbeatInFlight) return;
1370
+ if (latestContext.socketGeneration !== generation) return;
1371
+ if (!isSocketOpen(latestContext.socket)) return;
1372
+
1373
+ latestContext.ownerHeartbeatInFlight = true;
1374
+ try {
1375
+ const socket = latestContext.socket;
1376
+ const botJid =
1377
+ normalizeJid(socket?.user?.id || socket?.authState?.creds?.me?.id || socket?.authState?.creds?.me?.lid) || undefined;
1378
+ const sessionWeight = Math.max(1, Number(MULTI_SESSION_RUNTIME_CONFIG?.sessionWeights?.[safeSessionId] || 1));
1379
+ const heartbeatOutcome = await heartbeatGroupOwnerSession({
1380
+ sessionId: safeSessionId,
1381
+ leaseMs: GROUP_OWNER_LEASE_MS,
1382
+ reason: 'owner_lease_heartbeat',
1383
+ botJid,
1384
+ metadata: {
1385
+ source: 'socket_controller',
1386
+ socketGeneration: generation,
1387
+ },
1388
+ capacityWeight: sessionWeight,
1389
+ currentScore: 0,
1390
+ });
1391
+
1392
+ logger.debug('Heartbeat de ownership executado.', {
1393
+ action: 'group_owner_heartbeat_tick',
1394
+ sessionId: safeSessionId,
1395
+ generation,
1396
+ renewedAssignments: heartbeatOutcome?.renewedAssignments || 0,
1397
+ heartbeatMs: GROUP_OWNER_HEARTBEAT_MS,
1398
+ leaseMs: GROUP_OWNER_LEASE_MS,
1399
+ });
1400
+ } catch (error) {
1401
+ logger.warn('Falha no heartbeat de ownership da sessão.', {
1402
+ action: 'group_owner_heartbeat_failed',
1403
+ sessionId: safeSessionId,
1404
+ generation,
1405
+ error: error?.message,
1406
+ });
1407
+ } finally {
1408
+ const current = getSessionContext(safeSessionId, { createIfMissing: false });
1409
+ if (current) {
1410
+ current.ownerHeartbeatInFlight = false;
1411
+ }
1412
+ }
1413
+ };
1414
+
1415
+ context.ownerHeartbeatInterval = setInterval(() => {
1416
+ void runTick();
1417
+ }, GROUP_OWNER_HEARTBEAT_MS);
1418
+ if (typeof context.ownerHeartbeatInterval.unref === 'function') {
1419
+ context.ownerHeartbeatInterval.unref();
1420
+ }
1421
+
1422
+ logger.info('Heartbeat de ownership por grupo iniciado.', {
1423
+ action: 'group_owner_heartbeat_started',
1424
+ sessionId: safeSessionId,
1425
+ generation,
1426
+ heartbeatMs: GROUP_OWNER_HEARTBEAT_MS,
1427
+ leaseMs: GROUP_OWNER_LEASE_MS,
1428
+ });
1429
+
1430
+ void runTick();
1431
+ };
1432
+
1133
1433
  /**
1134
1434
  * Libera lock de escritor único do Baileys, se estiver ativo.
1435
+ * @param {string} sessionId
1135
1436
  * @param {string} reason
1136
1437
  * @returns {Promise<void>}
1137
1438
  */
1138
- const releaseBaileysWriterLock = async (reason = 'unknown') => {
1139
- const connection = baileysWriterLockConnection;
1439
+ const releaseBaileysWriterLock = async (sessionId, reason = 'unknown') => {
1440
+ const safeSessionId = normalizeSessionId(sessionId);
1441
+ const context = getSessionContext(safeSessionId, { createIfMissing: false });
1442
+ const connection = context?.writerLockConnection;
1140
1443
  if (!connection) return;
1141
-
1142
- baileysWriterLockConnection = null;
1444
+ const lockName = getWriterLockNameBySession(safeSessionId);
1445
+ context.writerLockConnection = null;
1143
1446
 
1144
1447
  try {
1145
- const rows = await executeQuery('SELECT RELEASE_LOCK(?) AS released', [BAILEYS_SINGLE_WRITER_LOCK_NAME], connection);
1448
+ const rows = await executeQuery('SELECT RELEASE_LOCK(?) AS released', [lockName], connection);
1146
1449
  const released = Number(rows?.[0]?.released) === 1;
1147
1450
  logger.info('Lock de escritor do Baileys liberado.', {
1148
1451
  action: 'baileys_writer_lock_released',
1452
+ sessionId: safeSessionId,
1149
1453
  reason,
1150
1454
  released,
1151
- lockName: BAILEYS_SINGLE_WRITER_LOCK_NAME,
1455
+ lockName,
1152
1456
  timestamp: __timeNowIso(),
1153
1457
  });
1154
1458
  } catch (error) {
1155
1459
  logger.warn('Falha ao liberar lock de escritor do Baileys.', {
1156
1460
  action: 'baileys_writer_lock_release_error',
1461
+ sessionId: safeSessionId,
1157
1462
  reason,
1158
- lockName: BAILEYS_SINGLE_WRITER_LOCK_NAME,
1463
+ lockName,
1159
1464
  errorMessage: error?.message,
1160
1465
  timestamp: __timeNowIso(),
1161
1466
  });
@@ -1173,27 +1478,33 @@ const releaseBaileysWriterLock = async (reason = 'unknown') => {
1173
1478
 
1174
1479
  /**
1175
1480
  * Garante lock de escritor único para a sessão do Baileys.
1481
+ * @param {string} sessionId
1176
1482
  * @returns {Promise<boolean>}
1177
1483
  */
1178
- const ensureBaileysWriterLock = async () => {
1484
+ const ensureBaileysWriterLock = async (sessionId) => {
1179
1485
  if (!BAILEYS_SINGLE_WRITER_LOCK_ENABLED) {
1180
1486
  return true;
1181
1487
  }
1182
1488
 
1183
- if (baileysWriterLockConnection) {
1489
+ const safeSessionId = normalizeSessionId(sessionId);
1490
+ const context = getSessionContext(safeSessionId);
1491
+ const lockName = getWriterLockNameBySession(safeSessionId);
1492
+
1493
+ if (context.writerLockConnection) {
1184
1494
  return true;
1185
1495
  }
1186
1496
 
1187
1497
  const connection = await pool.getConnection();
1188
1498
 
1189
1499
  try {
1190
- const rows = await executeQuery('SELECT GET_LOCK(?, ?) AS lock_status', [BAILEYS_SINGLE_WRITER_LOCK_NAME, BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS], connection);
1500
+ const rows = await executeQuery('SELECT GET_LOCK(?, ?) AS lock_status', [lockName, BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS], connection);
1191
1501
  const lockStatus = Number(rows?.[0]?.lock_status);
1192
1502
  if (lockStatus !== 1) {
1193
1503
  connection.release();
1194
1504
  logger.warn('Nao foi possivel adquirir lock de escritor do Baileys nesta tentativa.', {
1195
1505
  action: 'baileys_writer_lock_busy',
1196
- lockName: BAILEYS_SINGLE_WRITER_LOCK_NAME,
1506
+ sessionId: safeSessionId,
1507
+ lockName,
1197
1508
  timeoutSeconds: BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS,
1198
1509
  status: Number.isFinite(lockStatus) ? lockStatus : null,
1199
1510
  retryAfterMs: BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS,
@@ -1202,10 +1513,11 @@ const ensureBaileysWriterLock = async () => {
1202
1513
  return false;
1203
1514
  }
1204
1515
 
1205
- baileysWriterLockConnection = connection;
1516
+ context.writerLockConnection = connection;
1206
1517
  logger.info('Lock de escritor do Baileys adquirido com sucesso.', {
1207
1518
  action: 'baileys_writer_lock_acquired',
1208
- lockName: BAILEYS_SINGLE_WRITER_LOCK_NAME,
1519
+ sessionId: safeSessionId,
1520
+ lockName,
1209
1521
  timeoutSeconds: BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS,
1210
1522
  timestamp: __timeNowIso(),
1211
1523
  });
@@ -1220,16 +1532,25 @@ const ensureBaileysWriterLock = async () => {
1220
1532
  }
1221
1533
  };
1222
1534
 
1535
+ const releaseAllBaileysWriterLocks = async (reason = 'unknown') => {
1536
+ const targets = Array.from(sessionContexts.keys());
1537
+ if (!targets.length) {
1538
+ await releaseBaileysWriterLock(BAILEYS_PRIMARY_SESSION_ID, reason).catch(() => {});
1539
+ return;
1540
+ }
1541
+ await Promise.allSettled(targets.map((sessionId) => releaseBaileysWriterLock(sessionId, reason)));
1542
+ };
1543
+
1223
1544
  process.once('beforeExit', () => {
1224
- releaseBaileysWriterLock('before_exit').catch(() => {});
1545
+ releaseAllBaileysWriterLocks('before_exit').catch(() => {});
1225
1546
  });
1226
1547
 
1227
1548
  process.once('SIGINT', () => {
1228
- releaseBaileysWriterLock('sigint').catch(() => {});
1549
+ releaseAllBaileysWriterLocks('sigint').catch(() => {});
1229
1550
  });
1230
1551
 
1231
1552
  process.once('SIGTERM', () => {
1232
- releaseBaileysWriterLock('sigterm').catch(() => {});
1553
+ releaseAllBaileysWriterLocks('sigterm').catch(() => {});
1233
1554
  });
1234
1555
 
1235
1556
  /**
@@ -1316,31 +1637,36 @@ const syncGroupsOnConnectionOpen = async (sock) => {
1316
1637
  * @returns {Promise<void>} Conclusão da inicialização e do registro de handlers.
1317
1638
  * @throws {Error} Lança erro se a conexão inicial falhar.
1318
1639
  */
1319
- export async function connectToWhatsApp() {
1320
- if (connectPromise) {
1321
- return connectPromise;
1640
+ export async function connectToWhatsApp(sessionId = BAILEYS_PRIMARY_SESSION_ID) {
1641
+ const safeSessionId = normalizeSessionId(sessionId);
1642
+ const context = getSessionContext(safeSessionId);
1643
+
1644
+ if (context.connectPromise) {
1645
+ return context.connectPromise;
1322
1646
  }
1323
1647
 
1324
- if (isSocketOpen(activeSocket)) {
1648
+ if (isSocketOpen(context.socket)) {
1325
1649
  return;
1326
1650
  }
1327
1651
 
1328
1652
  logger.info('Iniciando conexão com o WhatsApp...', {
1329
1653
  action: 'connect_init',
1654
+ sessionId: safeSessionId,
1330
1655
  timestamp: __timeNowIso(),
1331
1656
  });
1332
- connectPromise = (async () => {
1333
- clearReconnectTimeout();
1334
- const isWriterReady = await ensureBaileysWriterLock();
1657
+
1658
+ const currentConnectPromise = (async () => {
1659
+ clearReconnectTimeout(safeSessionId);
1660
+ const isWriterReady = await ensureBaileysWriterLock(safeSessionId);
1335
1661
  if (!isWriterReady) {
1336
- scheduleReconnect(BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS);
1662
+ scheduleReconnect(safeSessionId, BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS);
1337
1663
  return;
1338
1664
  }
1339
1665
 
1340
- const generation = ++socketGeneration;
1666
+ const generation = ++context.socketGeneration;
1341
1667
  const legacyAuthPath = path.join(__dirname, 'auth');
1342
1668
  const { state, saveCreds } = await useDbAuthState({
1343
- sessionId: BAILEYS_AUTH_SESSION_ID,
1669
+ sessionId: safeSessionId,
1344
1670
  bootstrapFromDir: legacyAuthPath,
1345
1671
  bootstrapFromFiles: BAILEYS_AUTH_BOOTSTRAP_FROM_FILES,
1346
1672
  });
@@ -1348,7 +1674,8 @@ export async function connectToWhatsApp() {
1348
1674
  const version = await resolveBaileysVersion();
1349
1675
 
1350
1676
  logger.debug('Dados de autenticação carregados com sucesso.', {
1351
- authSessionId: BAILEYS_AUTH_SESSION_ID,
1677
+ sessionId: safeSessionId,
1678
+ authSessionId: safeSessionId,
1352
1679
  bootstrappedFromFiles: BAILEYS_AUTH_BOOTSTRAP_FROM_FILES,
1353
1680
  version,
1354
1681
  generation,
@@ -1366,7 +1693,7 @@ export async function connectToWhatsApp() {
1366
1693
  msgRetryCounterCache,
1367
1694
  maxMsgRetryCount: 5,
1368
1695
  retryRequestDelayMs: 250,
1369
- getMessage: getStoredMessage,
1696
+ getMessage: (key) => getStoredMessage(key, safeSessionId),
1370
1697
  userDevicesCache,
1371
1698
  mediaCache,
1372
1699
  cachedGroupMetadata: resolveCachedGroupMetadata,
@@ -1377,16 +1704,22 @@ export async function connectToWhatsApp() {
1377
1704
  };
1378
1705
 
1379
1706
  const sock = makeWASocket(socketConfig);
1707
+ sock.__omnizapSessionId = safeSessionId;
1380
1708
 
1381
- activeSocket = sock;
1382
- storeActiveSocket(sock);
1709
+ context.socket = sock;
1710
+ storeActiveSocket(sock, safeSessionId);
1711
+ syncLegacyActiveSocketReference();
1383
1712
 
1384
- const isCurrentSocket = () => activeSocket === sock && generation === socketGeneration;
1713
+ const isCurrentSocket = () => {
1714
+ const latest = getSessionContext(safeSessionId, { createIfMissing: false });
1715
+ return Boolean(latest && latest.socket === sock && latest.socketGeneration === generation);
1716
+ };
1385
1717
 
1386
1718
  sock.ev.on('creds.update', async () => {
1387
1719
  if (!isCurrentSocket()) return;
1388
1720
  logger.debug('Atualizando credenciais de autenticação...', {
1389
1721
  action: 'creds_update',
1722
+ sessionId: safeSessionId,
1390
1723
  timestamp: __timeNowIso(),
1391
1724
  });
1392
1725
  await saveCreds();
@@ -1394,12 +1727,13 @@ export async function connectToWhatsApp() {
1394
1727
 
1395
1728
  sock.ev.on('connection.update', (update) => {
1396
1729
  if (!isCurrentSocket()) return;
1397
- handleConnectionUpdate(update, sock);
1730
+ handleConnectionUpdate(update, sock, safeSessionId, generation);
1398
1731
  if (update.connection === 'open') {
1399
1732
  syncNewsBroadcastService();
1400
1733
  }
1401
1734
  logger.debug('Estado da conexão atualizado.', {
1402
1735
  action: 'connection_update',
1736
+ sessionId: safeSessionId,
1403
1737
  status: update.connection,
1404
1738
  lastDisconnect: update.lastDisconnect?.error?.message || null,
1405
1739
  isNewLogin: update.isNewLogin || false,
@@ -1415,17 +1749,19 @@ export async function connectToWhatsApp() {
1415
1749
  try {
1416
1750
  logger.debug('Novo(s) evento(s) em messages.upsert', {
1417
1751
  action: 'messages_upsert',
1752
+ sessionId: safeSessionId,
1418
1753
  type: update.type,
1419
1754
  messagesCount: update.messages.length,
1420
1755
  remoteJid: update.messages[0]?.key.remoteJid || null,
1421
1756
  });
1422
- const persistPromise = persistIncomingMessages(update.messages, update.type).catch((error) => {
1757
+ const persistPromise = persistIncomingMessages(update.messages, update.type, safeSessionId).catch((error) => {
1423
1758
  logger.error('Erro ao persistir mensagens no banco de dados:', {
1759
+ sessionId: safeSessionId,
1424
1760
  error: error.message,
1425
1761
  });
1426
1762
  recordError('messages_upsert');
1427
1763
  });
1428
- const handlePromise = handleMessages(update, sock).catch((error) => {
1764
+ const handlePromise = handleMessages(update, sock, { sessionId: safeSessionId }).catch((error) => {
1429
1765
  recordError('messages_upsert');
1430
1766
  throw error;
1431
1767
  });
@@ -1442,6 +1778,7 @@ export async function connectToWhatsApp() {
1442
1778
  });
1443
1779
  } catch (error) {
1444
1780
  logger.error('Erro no evento messages.upsert:', {
1781
+ sessionId: safeSessionId,
1445
1782
  error: error.message,
1446
1783
  stack: error.stack,
1447
1784
  action: 'messages_upsert_error',
@@ -1476,6 +1813,7 @@ export async function connectToWhatsApp() {
1476
1813
  for (const chatId of deletions) {
1477
1814
  remove('chats', chatId).catch((error) => {
1478
1815
  logger.error('Erro ao remover chat do banco:', {
1816
+ sessionId: safeSessionId,
1479
1817
  error: error.message,
1480
1818
  chatId,
1481
1819
  });
@@ -1493,6 +1831,7 @@ export async function connectToWhatsApp() {
1493
1831
  invalidateCachedGroupMetadata(group.id);
1494
1832
  } catch (error) {
1495
1833
  logger.error('Erro no upsert do grupo:', {
1834
+ sessionId: safeSessionId,
1496
1835
  error: error.message,
1497
1836
  groupId: group.id,
1498
1837
  });
@@ -1519,6 +1858,7 @@ export async function connectToWhatsApp() {
1519
1858
  queueLidUpdate(lid, pnJid, 'lid-mapping');
1520
1859
  } catch (error) {
1521
1860
  logger.warn('Falha ao processar lid-mapping.update para lid_map.', {
1861
+ sessionId: safeSessionId,
1522
1862
  error: error.message,
1523
1863
  });
1524
1864
  }
@@ -1529,11 +1869,13 @@ export async function connectToWhatsApp() {
1529
1869
  try {
1530
1870
  logger.debug('Atualização de mensagens recebida.', {
1531
1871
  action: 'messages_update',
1872
+ sessionId: safeSessionId,
1532
1873
  updatesCount: update.length,
1533
1874
  });
1534
1875
  handleMessageUpdate(update, sock);
1535
1876
  } catch (error) {
1536
1877
  logger.error('Erro no evento messages.update:', {
1878
+ sessionId: safeSessionId,
1537
1879
  error: error.message,
1538
1880
  stack: error.stack,
1539
1881
  action: 'messages_update_error',
@@ -1550,6 +1892,7 @@ export async function connectToWhatsApp() {
1550
1892
  const firstError = erroredUpdates[0]?.error;
1551
1893
  logger.warn('Falha reportada em atualização de mídia.', {
1552
1894
  action: 'messages_media_update_error',
1895
+ sessionId: safeSessionId,
1553
1896
  updatesCount: updates.length,
1554
1897
  errorCount: erroredUpdates.length,
1555
1898
  firstMessageId: erroredUpdates[0]?.key?.id || null,
@@ -1561,6 +1904,7 @@ export async function connectToWhatsApp() {
1561
1904
 
1562
1905
  logger.debug('Atualização de mídia de mensagem recebida.', {
1563
1906
  action: 'messages_media_update',
1907
+ sessionId: safeSessionId,
1564
1908
  updatesCount: updates.length,
1565
1909
  });
1566
1910
  });
@@ -1582,6 +1926,7 @@ export async function connectToWhatsApp() {
1582
1926
 
1583
1927
  logger.debug('Atualização de recibos de mensagem recebida.', {
1584
1928
  action: 'message_receipt_update',
1929
+ sessionId: safeSessionId,
1585
1930
  updatesCount: updates.length,
1586
1931
  receiptTypes: Array.from(receiptTypes),
1587
1932
  invalidReceiptTypeCount,
@@ -1621,6 +1966,7 @@ export async function connectToWhatsApp() {
1621
1966
  }
1622
1967
  } catch (error) {
1623
1968
  logger.error('Erro no evento messages.reaction:', {
1969
+ sessionId: safeSessionId,
1624
1970
  error: error.message,
1625
1971
  stack: error.stack,
1626
1972
  action: 'messages_reaction_error',
@@ -1633,12 +1979,14 @@ export async function connectToWhatsApp() {
1633
1979
  try {
1634
1980
  logger.debug('Grupo(s) atualizado(s).', {
1635
1981
  action: 'groups_update',
1982
+ sessionId: safeSessionId,
1636
1983
  groupCount: updates.length,
1637
1984
  groupIds: updates.map((u) => u.id),
1638
1985
  });
1639
1986
  handleGroupUpdate(updates);
1640
1987
  } catch (err) {
1641
1988
  logger.error('Erro no evento groups.update:', {
1989
+ sessionId: safeSessionId,
1642
1990
  error: err.message,
1643
1991
  stack: err.stack,
1644
1992
  action: 'groups_update_error',
@@ -1651,6 +1999,7 @@ export async function connectToWhatsApp() {
1651
1999
  try {
1652
2000
  logger.debug('Participantes do grupo atualizados.', {
1653
2001
  action: 'group_participants_update',
2002
+ sessionId: safeSessionId,
1654
2003
  groupId: update.id,
1655
2004
  actionType: update.action,
1656
2005
  participants: update.participants,
@@ -1659,6 +2008,7 @@ export async function connectToWhatsApp() {
1659
2008
  handleGroupParticipantsEvent(sock, update.id, update.participants, update.action);
1660
2009
  } catch (err) {
1661
2010
  logger.error('Erro no evento group-participants.update:', {
2011
+ sessionId: safeSessionId,
1662
2012
  error: err.message,
1663
2013
  stack: err.stack,
1664
2014
  action: 'group_participants_update_error',
@@ -1671,6 +2021,7 @@ export async function connectToWhatsApp() {
1671
2021
  try {
1672
2022
  logger.debug('Solicitação de entrada no grupo recebida.', {
1673
2023
  action: 'group_join_request',
2024
+ sessionId: safeSessionId,
1674
2025
  groupId: update?.id,
1675
2026
  participant: update?.participant,
1676
2027
  method: update?.method,
@@ -1679,6 +2030,7 @@ export async function connectToWhatsApp() {
1679
2030
  handleGroupJoinRequest(sock, update);
1680
2031
  } catch (err) {
1681
2032
  logger.error('Erro no evento group.join-request:', {
2033
+ sessionId: safeSessionId,
1682
2034
  error: err.message,
1683
2035
  stack: err.stack,
1684
2036
  action: 'group_join_request_error',
@@ -1704,6 +2056,7 @@ export async function connectToWhatsApp() {
1704
2056
  await sock.rejectCall(call.id, call.from);
1705
2057
  logger.info('Chamada recebida rejeitada automaticamente.', {
1706
2058
  action: 'call_auto_reject',
2059
+ sessionId: safeSessionId,
1707
2060
  callId: call.id,
1708
2061
  from: call.from,
1709
2062
  isGroup: call.isGroup || false,
@@ -1713,6 +2066,7 @@ export async function connectToWhatsApp() {
1713
2066
  } catch (error) {
1714
2067
  logger.warn('Falha ao rejeitar chamada automaticamente.', {
1715
2068
  action: 'call_auto_reject_failed',
2069
+ sessionId: safeSessionId,
1716
2070
  callId: call?.id || null,
1717
2071
  from: call?.from || null,
1718
2072
  error: error?.message,
@@ -1722,19 +2076,45 @@ export async function connectToWhatsApp() {
1722
2076
  });
1723
2077
 
1724
2078
  registerBaileysEventLoggers(sock);
1725
- registerBaileysEventJournal(sock, generation);
2079
+ registerBaileysEventJournal(sock, generation, safeSessionId);
1726
2080
 
1727
2081
  logger.info('Conexão com o WhatsApp estabelecida com sucesso.', {
1728
2082
  action: 'connect_success',
2083
+ sessionId: safeSessionId,
1729
2084
  generation,
1730
2085
  timestamp: __timeNowIso(),
1731
2086
  });
1732
2087
  })();
1733
2088
 
2089
+ context.connectPromise = currentConnectPromise;
2090
+
1734
2091
  try {
1735
- await connectPromise;
2092
+ await currentConnectPromise;
1736
2093
  } finally {
1737
- connectPromise = null;
2094
+ if (context.connectPromise === currentConnectPromise) {
2095
+ context.connectPromise = null;
2096
+ }
2097
+ }
2098
+ }
2099
+
2100
+ /**
2101
+ * Conecta todas as sessões configuradas no runtime.
2102
+ * @returns {Promise<void>}
2103
+ */
2104
+ export async function connectAllWhatsAppSessions() {
2105
+ const results = await Promise.allSettled(BAILEYS_SESSION_IDS.map((sessionId) => connectToWhatsApp(sessionId)));
2106
+ const failures = results
2107
+ .map((result, index) => ({ result, sessionId: BAILEYS_SESSION_IDS[index] }))
2108
+ .filter(({ result }) => result.status === 'rejected');
2109
+
2110
+ if (failures.length > 0) {
2111
+ const error = new Error(`Falha ao conectar ${failures.length}/${BAILEYS_SESSION_IDS.length} sessões do WhatsApp.`);
2112
+ // @ts-ignore enrich error object for logs
2113
+ error.failures = failures.map(({ sessionId, result }) => ({
2114
+ sessionId,
2115
+ message: result.reason?.message || String(result.reason || ''),
2116
+ }));
2117
+ throw error;
1738
2118
  }
1739
2119
  }
1740
2120
 
@@ -1744,15 +2124,23 @@ export async function connectToWhatsApp() {
1744
2124
  * @async
1745
2125
  * @param {import('@whiskeysockets/baileys').ConnectionState} update - Objeto contendo o estado atual da conexão.
1746
2126
  * @param {import('@whiskeysockets/baileys').WASocket} sock - Instância do socket do WhatsApp que disparou a atualização.
2127
+ * @param {string} sessionId - Sessão do socket.
2128
+ * @param {number} generation - Geração do socket.
1747
2129
  * @returns {Promise<void>} Uma promessa que resolve quando o processamento do estado da conexão é concluído.
1748
2130
  */
1749
- async function handleConnectionUpdate(update, sock) {
1750
- if (sock !== activeSocket) return;
2131
+ async function handleConnectionUpdate(update, sock, sessionId, generation) {
2132
+ const safeSessionId = normalizeSessionId(sessionId);
2133
+ const context = getSessionContext(safeSessionId, { createIfMissing: false });
2134
+ if (!context) return;
2135
+ if (context.socket !== sock) return;
2136
+ if (context.socketGeneration !== generation) return;
2137
+
1751
2138
  const { connection, lastDisconnect, qr } = update;
1752
2139
 
1753
2140
  if (qr) {
1754
2141
  logger.info('📱 QR Code gerado! Escaneie com seu WhatsApp.', {
1755
2142
  action: 'qr_code_generated',
2143
+ sessionId: safeSessionId,
1756
2144
  timestamp: __timeNowIso(),
1757
2145
  });
1758
2146
  qrcode.generate(qr, { small: true });
@@ -1763,13 +2151,32 @@ async function handleConnectionUpdate(update, sock) {
1763
2151
  const errorMessage = lastDisconnect?.error?.message || 'Sem mensagem de erro';
1764
2152
 
1765
2153
  const shouldReconnect = lastDisconnect?.error instanceof Boom && disconnectCode !== DisconnectReason.loggedOut;
2154
+ stopGroupOwnerHeartbeat(safeSessionId, shouldReconnect ? 'connection_close_reconnect' : 'connection_close_final');
2155
+ void sessionRegistryService
2156
+ .markSessionDisconnected(safeSessionId, {
2157
+ status: shouldReconnect ? 'reconnecting' : 'offline',
2158
+ metadata: {
2159
+ reasonCode: disconnectCode,
2160
+ errorMessage,
2161
+ shouldReconnect,
2162
+ },
2163
+ })
2164
+ .catch((error) => {
2165
+ logger.warn('Falha ao registrar sessao offline no registry.', {
2166
+ action: 'session_registry_mark_disconnected_failed',
2167
+ sessionId: safeSessionId,
2168
+ reasonCode: disconnectCode,
2169
+ error: error?.message,
2170
+ });
2171
+ });
1766
2172
 
1767
2173
  if (shouldReconnect) {
1768
- const attempt = getNextReconnectAttempt();
2174
+ const attempt = getNextReconnectAttempt(safeSessionId);
1769
2175
  if (attempt <= MAX_CONNECTION_ATTEMPTS) {
1770
2176
  const reconnectDelay = INITIAL_RECONNECT_DELAY * Math.pow(2, attempt - 1);
1771
2177
  logger.warn(`⚠️ Conexão perdida. Tentando reconectar...`, {
1772
2178
  action: 'reconnect_attempt',
2179
+ sessionId: safeSessionId,
1773
2180
  attempt,
1774
2181
  maxAttempts: MAX_CONNECTION_ATTEMPTS,
1775
2182
  delay: reconnectDelay,
@@ -1777,12 +2184,14 @@ async function handleConnectionUpdate(update, sock) {
1777
2184
  errorMessage,
1778
2185
  timestamp: __timeNowIso(),
1779
2186
  });
1780
- activeSocket = null;
1781
- storeActiveSocket(null);
1782
- scheduleReconnect(reconnectDelay);
2187
+ context.socket = null;
2188
+ storeActiveSocket(null, safeSessionId);
2189
+ syncLegacyActiveSocketReference();
2190
+ scheduleReconnect(safeSessionId, reconnectDelay);
1783
2191
  } else {
1784
2192
  logger.error('❌ Limite de tentativas atingido; aguardando janela para novo retry.', {
1785
2193
  action: 'reconnect_backoff_window',
2194
+ sessionId: safeSessionId,
1786
2195
  totalAttempts: attempt,
1787
2196
  maxAttempts: MAX_CONNECTION_ATTEMPTS,
1788
2197
  retryAfterMs: BAILEYS_RECONNECT_ATTEMPT_RESET_MS,
@@ -1790,38 +2199,60 @@ async function handleConnectionUpdate(update, sock) {
1790
2199
  errorMessage,
1791
2200
  timestamp: __timeNowIso(),
1792
2201
  });
1793
- activeSocket = null;
1794
- storeActiveSocket(null);
1795
- connectionAttempts = 0;
1796
- reconnectWindowStartedAt = __timeNowMs();
1797
- scheduleReconnect(BAILEYS_RECONNECT_ATTEMPT_RESET_MS);
2202
+ context.socket = null;
2203
+ storeActiveSocket(null, safeSessionId);
2204
+ syncLegacyActiveSocketReference();
2205
+ context.connectionAttempts = 0;
2206
+ context.reconnectWindowStartedAt = __timeNowMs();
2207
+ scheduleReconnect(safeSessionId, BAILEYS_RECONNECT_ATTEMPT_RESET_MS);
1798
2208
  }
1799
2209
  } else {
1800
2210
  logger.error('❌ Conexão fechada definitivamente.', {
1801
2211
  action: 'connection_closed',
2212
+ sessionId: safeSessionId,
1802
2213
  reasonCode: disconnectCode,
1803
2214
  errorMessage,
1804
2215
  timestamp: __timeNowIso(),
1805
2216
  });
1806
- activeSocket = null;
1807
- storeActiveSocket(null);
1808
- await releaseBaileysWriterLock('connection_closed_no_reconnect');
2217
+ context.socket = null;
2218
+ storeActiveSocket(null, safeSessionId);
2219
+ syncLegacyActiveSocketReference();
2220
+ await releaseBaileysWriterLock(safeSessionId, 'connection_closed_no_reconnect');
1809
2221
  }
1810
2222
  }
1811
2223
 
1812
2224
  if (connection === 'open') {
1813
2225
  logger.info('✅ Conectado com sucesso ao WhatsApp!', {
1814
2226
  action: 'connection_open',
2227
+ sessionId: safeSessionId,
1815
2228
  timestamp: __timeNowIso(),
1816
2229
  });
1817
2230
 
1818
- resetReconnectState();
1819
- clearReconnectTimeout();
2231
+ resetReconnectState(safeSessionId);
2232
+ clearReconnectTimeout(safeSessionId);
2233
+ startGroupOwnerHeartbeat(safeSessionId, generation);
2234
+ void sessionRegistryService
2235
+ .markSessionConnected(safeSessionId, {
2236
+ botJid: normalizeJid(sock?.user?.id || sock?.authState?.creds?.me?.id || sock?.authState?.creds?.me?.lid) || undefined,
2237
+ metadata: {
2238
+ source: 'connection_open',
2239
+ socketGeneration: generation,
2240
+ },
2241
+ capacityWeight: Math.max(1, Number(MULTI_SESSION_RUNTIME_CONFIG?.sessionWeights?.[safeSessionId] || 1)),
2242
+ })
2243
+ .catch((error) => {
2244
+ logger.warn('Falha ao registrar sessao online no registry.', {
2245
+ action: 'session_registry_mark_connected_failed',
2246
+ sessionId: safeSessionId,
2247
+ error: error?.message,
2248
+ });
2249
+ });
1820
2250
 
1821
2251
  if (process.send) {
1822
2252
  process.send('ready');
1823
2253
  logger.info('🟢 Sinal de "ready" enviado ao PM2.', {
1824
2254
  action: 'pm2_ready_signal',
2255
+ sessionId: safeSessionId,
1825
2256
  timestamp: __timeNowIso(),
1826
2257
  });
1827
2258
  }
@@ -1831,6 +2262,7 @@ async function handleConnectionUpdate(update, sock) {
1831
2262
  } catch (error) {
1832
2263
  logger.error('❌ Erro ao carregar metadados de grupos na conexão.', {
1833
2264
  action: 'groups_load_error',
2265
+ sessionId: safeSessionId,
1834
2266
  errorMessage: error.message,
1835
2267
  stack: error.stack,
1836
2268
  timeoutMs: GROUP_SYNC_TIMEOUT_MS,
@@ -1953,6 +2385,7 @@ async function handleGroupUpdate(updates) {
1953
2385
  * @returns {import('@whiskeysockets/baileys').WASocket | null} O objeto socket do Baileys ativo ou `null` se não houver conexão ativa.
1954
2386
  */
1955
2387
  export function getActiveSocket() {
2388
+ syncLegacyActiveSocketReference();
1956
2389
  logger.debug('🔍 Recuperando instância do socket ativo.', {
1957
2390
  action: 'get_active_socket',
1958
2391
  socketExists: !!activeSocket,
@@ -1961,6 +2394,17 @@ export function getActiveSocket() {
1961
2394
  return activeSocket;
1962
2395
  }
1963
2396
 
2397
+ /**
2398
+ * Retorna o socket de uma sessão específica.
2399
+ * @param {string} sessionId
2400
+ * @returns {import('@whiskeysockets/baileys').WASocket | null}
2401
+ */
2402
+ export function getSocketBySession(sessionId = BAILEYS_PRIMARY_SESSION_ID) {
2403
+ const safeSessionId = normalizeSessionId(sessionId);
2404
+ const context = getSessionContext(safeSessionId, { createIfMissing: false });
2405
+ return context?.socket || null;
2406
+ }
2407
+
1964
2408
  /**
1965
2409
  * Executa um método centralizado no socket ativo, tratando erros e mapeando-os para respostas HTTP.
1966
2410
  * @async
@@ -1970,6 +2414,7 @@ export function getActiveSocket() {
1970
2414
  * @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
2415
  */
1972
2416
  async function runControllerSocketMethod(methodName, ...args) {
2417
+ const socket = getActiveSocket();
1973
2418
  try {
1974
2419
  return await runActiveSocketMethod(methodName, ...args);
1975
2420
  } catch (error) {
@@ -1977,8 +2422,8 @@ async function runControllerSocketMethod(methodName, ...args) {
1977
2422
  if (message.includes('Socket do WhatsApp indisponível')) {
1978
2423
  logger.warn('Socket ativo indisponível para operação.', {
1979
2424
  action: methodName,
1980
- socketExists: !!activeSocket,
1981
- socketOpen: isSocketOpen(activeSocket),
2425
+ socketExists: !!socket,
2426
+ socketOpen: isSocketOpen(socket),
1982
2427
  timestamp: __timeNowIso(),
1983
2428
  });
1984
2429
  throw new Boom('Socket do WhatsApp indisponível no momento.', { statusCode: 503 });
@@ -2207,23 +2652,79 @@ export async function rejectCall(callId, callFrom) {
2207
2652
  * Encerra o socket ativo atual, se existir, para disparar a lógica de reconexão.
2208
2653
  * Se nenhum socket estiver ativo, inicia uma nova conexão.
2209
2654
  * @async
2655
+ * @param {string} [sessionId=BAILEYS_PRIMARY_SESSION_ID]
2210
2656
  * @returns {Promise<void>} Uma promessa que resolve quando o fluxo de reconexão é iniciado ou uma nova conexão é tentada.
2211
2657
  */
2212
- export async function reconnectToWhatsApp() {
2213
- // eslint-disable-next-line no-undef
2214
- if (activeSocket && activeSocket.ws?.readyState === WebSocket.OPEN) {
2658
+ export async function reconnectToWhatsApp(sessionId = BAILEYS_PRIMARY_SESSION_ID) {
2659
+ const safeSessionId = normalizeSessionId(sessionId);
2660
+ const targetSocket = getSocketBySession(safeSessionId);
2661
+ if (targetSocket && isSocketOpen(targetSocket)) {
2215
2662
  logger.info('♻️ Forçando fechamento do socket para reconectar...', {
2216
2663
  action: 'force_reconnect',
2664
+ sessionId: safeSessionId,
2217
2665
  timestamp: __timeNowIso(),
2218
2666
  });
2219
- activeSocket.ws.close();
2667
+ targetSocket.ws?.close?.();
2220
2668
  } else {
2221
2669
  logger.warn('⚠️ Nenhum socket ativo detectado. Iniciando nova conexão manualmente.', {
2222
2670
  action: 'reconnect_no_active_socket',
2671
+ sessionId: safeSessionId,
2223
2672
  timestamp: __timeNowIso(),
2224
2673
  });
2225
- await connectToWhatsApp();
2674
+ await connectToWhatsApp(safeSessionId);
2675
+ }
2676
+ }
2677
+
2678
+ /**
2679
+ * Encerra todas as sessões ativas no processo.
2680
+ * @param {{ releaseLocks?: boolean }} [options]
2681
+ * @returns {Promise<void>}
2682
+ */
2683
+ export async function disconnectAllWhatsAppSessions(options = {}) {
2684
+ const { releaseLocks = true } = options;
2685
+ const targetSessionIds = Array.from(new Set([...BAILEYS_SESSION_IDS, ...sessionContexts.keys()]));
2686
+
2687
+ await Promise.allSettled(
2688
+ targetSessionIds.map(async (sessionId) => {
2689
+ const safeSessionId = normalizeSessionId(sessionId);
2690
+ const context = getSessionContext(safeSessionId, { createIfMissing: false });
2691
+ if (!context) return;
2692
+
2693
+ clearReconnectTimeout(safeSessionId);
2694
+ stopGroupOwnerHeartbeat(safeSessionId, 'disconnect_all_sessions');
2695
+
2696
+ const socket = context.socket;
2697
+ context.socket = null;
2698
+ context.connectPromise = null;
2699
+ storeActiveSocket(null, safeSessionId);
2700
+
2701
+ if (socket && typeof socket.end === 'function') {
2702
+ try {
2703
+ await socket.end();
2704
+ } catch (error) {
2705
+ logger.warn('Falha ao encerrar sessão do WhatsApp.', {
2706
+ action: 'disconnect_session_failed',
2707
+ sessionId: safeSessionId,
2708
+ errorMessage: error?.message,
2709
+ });
2710
+ }
2711
+ }
2712
+
2713
+ void sessionRegistryService
2714
+ .markSessionDisconnected(safeSessionId, {
2715
+ status: 'offline',
2716
+ metadata: {
2717
+ reason: 'disconnect_all_sessions',
2718
+ },
2719
+ })
2720
+ .catch(() => {});
2721
+ }),
2722
+ );
2723
+
2724
+ if (releaseLocks) {
2725
+ await releaseAllBaileysWriterLocks('disconnect_all_sessions');
2226
2726
  }
2727
+ syncLegacyActiveSocketReference();
2227
2728
  }
2228
2729
 
2229
2730
  if (process.argv[1] === __filename) {