@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.
- package/.env.example +54 -9
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/security-runner-hardening.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +1 -0
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +226 -55
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +143 -3
- package/app/configParts/sessionConfig.js +157 -0
- package/app/connection/baileysCompatibility.test.js +1 -1
- package/app/connection/groupOwnerWriteStateResolver.js +109 -0
- package/app/connection/socketController.js +625 -124
- package/app/connection/socketController.multiSession.test.js +108 -0
- package/app/controllers/messageController.js +1 -1
- package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
- package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
- package/app/controllers/messageProcessingPipeline.js +88 -9
- package/app/controllers/messageProcessingPipeline.test.js +200 -0
- package/app/modules/adminModule/AGENT.md +1 -1
- package/app/modules/adminModule/commandConfig.json +3318 -1347
- package/app/modules/adminModule/groupCommandHandlers.js +856 -14
- package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
- package/app/modules/adminModule/groupWarningRepository.js +152 -0
- package/app/modules/aiModule/AGENT.md +47 -30
- package/app/modules/aiModule/aiConfigRuntime.js +1 -0
- package/app/modules/aiModule/catCommand.js +132 -25
- package/app/modules/aiModule/commandConfig.json +114 -28
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
- package/app/modules/gameModule/AGENT.md +1 -1
- package/app/modules/gameModule/commandConfig.json +29 -0
- package/app/modules/menuModule/AGENT.md +1 -1
- package/app/modules/menuModule/commandConfig.json +45 -10
- package/app/modules/menuModule/menuCatalogService.js +190 -0
- package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
- package/app/modules/menuModule/menuDynamicService.js +511 -0
- package/app/modules/menuModule/menuDynamicService.test.js +141 -0
- package/app/modules/menuModule/menus.js +36 -5
- package/app/modules/playModule/AGENT.md +10 -5
- package/app/modules/playModule/commandConfig.json +74 -16
- package/app/modules/playModule/playCommandConstants.js +13 -7
- package/app/modules/playModule/playCommandCore.js +4 -6
- package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
- package/app/modules/playModule/playConfigRuntime.js +5 -6
- package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerPackModule/AGENT.md +1 -1
- package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
- package/app/modules/stickerPackModule/commandConfig.json +29 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/geminiService.js +131 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/moduleAiHelpCoreService.js +33 -4
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +51 -21
- package/app/services/messaging/newsBroadcastService.js +843 -27
- package/app/services/multiSession/assignmentBalancerService.js +457 -0
- package/app/services/multiSession/groupOwnershipRepository.js +381 -0
- package/app/services/multiSession/groupOwnershipService.js +890 -0
- package/app/services/multiSession/groupOwnershipService.test.js +309 -0
- package/app/services/multiSession/sessionRegistryService.js +293 -0
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/groupConfigStore.js +41 -5
- package/app/store/premiumUserStore.js +21 -7
- package/app/utils/antiLink/antiLinkModule.js +352 -16
- package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
- package/database/index.js +6 -0
- package/database/migrations/20260307_d0_hardening_down.sql +1 -1
- package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
- package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
- package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
- package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
- package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
- package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
- package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
- package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
- package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
- package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
- package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
- package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
- package/database/schema.sql +102 -1
- package/docker-compose.yml +4 -1
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
- package/docs/security/omnizap-static-security-headers.conf +25 -0
- package/ecosystem.prod.config.cjs +31 -11
- package/index.js +52 -18
- package/observability/alert-rules.yml +20 -0
- package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
- package/observability/mysql-setup.sql +4 -4
- package/observability/system-admin-observability.md +26 -0
- package/package.json +12 -5
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/stickersApp.js +255 -103
- package/public/js/apps/termsReactApp.js +57 -8
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +96 -47
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/politica-de-privacidade.html +1 -1
- package/public/pages/stickers.html +5 -5
- package/public/pages/termos-de-uso-texto-integral.html +1 -1
- package/public/pages/termos-de-uso.html +1 -1
- package/public/pages/user-password-reset.html +3 -4
- package/public/pages/user-systemadm.html +8 -462
- package/public/pages/user.html +1 -1
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +10 -0
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +155 -0
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/sticker/stickerCatalogController.js +9 -23
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +3 -2
- package/server/http/httpServer.js +8 -4
- package/server/middleware/securityHeaders.js +20 -1
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +30 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/static/staticPageRouter.js +27 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +2 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -2,7 +2,7 @@ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } fro
|
|
|
2
2
|
import makeWASocket, { DisconnectReason, Browsers, getAggregateVotesInPollMessage, areJidsSameUser, WAMessageStatus, WAMessageStubType, delayCancellable, getStatusFromReceiptType, promiseTimeout } from '@whiskeysockets/baileys';
|
|
3
3
|
|
|
4
4
|
import NodeCache from 'node-cache';
|
|
5
|
-
import { parseEnvBool, parseEnvCsv, parseEnvInt, resolveBaileysVersion, resolveAddressingModeFromMessageKey, normalizeAddressingMode, normalizePnToJid, normalizeWAPresence, baileysConnectionLogger as logger, baileysSocketLogger } from '../config/index.js';
|
|
5
|
+
import { parseEnvBool, parseEnvCsv, parseEnvInt, resolveBaileysVersion, resolveAddressingModeFromMessageKey, normalizeAddressingMode, normalizeJid, normalizePnToJid, normalizeWAPresence, isGroupJid, getMultiSessionRuntimeConfig, baileysConnectionLogger as logger, baileysSocketLogger } from '../config/index.js';
|
|
6
6
|
|
|
7
7
|
import { Boom } from '@hapi/boom';
|
|
8
8
|
import qrcode from 'qrcode-terminal';
|
|
@@ -16,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
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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
|
-
*
|
|
271
|
-
* @
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
|
|
278
|
-
|
|
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 =
|
|
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
|
|
1039
|
-
|
|
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
|
-
|
|
1064
|
-
reconnectTimeout
|
|
1065
|
-
reconnectTimeout
|
|
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
|
-
|
|
1074
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1444
|
+
const lockName = getWriterLockNameBySession(safeSessionId);
|
|
1445
|
+
context.writerLockConnection = null;
|
|
1143
1446
|
|
|
1144
1447
|
try {
|
|
1145
|
-
const rows = await executeQuery('SELECT RELEASE_LOCK(?) AS released', [
|
|
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
|
|
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
|
|
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
|
-
|
|
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', [
|
|
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
|
-
|
|
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
|
-
|
|
1516
|
+
context.writerLockConnection = connection;
|
|
1206
1517
|
logger.info('Lock de escritor do Baileys adquirido com sucesso.', {
|
|
1207
1518
|
action: 'baileys_writer_lock_acquired',
|
|
1208
|
-
|
|
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
|
-
|
|
1545
|
+
releaseAllBaileysWriterLocks('before_exit').catch(() => {});
|
|
1225
1546
|
});
|
|
1226
1547
|
|
|
1227
1548
|
process.once('SIGINT', () => {
|
|
1228
|
-
|
|
1549
|
+
releaseAllBaileysWriterLocks('sigint').catch(() => {});
|
|
1229
1550
|
});
|
|
1230
1551
|
|
|
1231
1552
|
process.once('SIGTERM', () => {
|
|
1232
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
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(
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1382
|
-
storeActiveSocket(sock);
|
|
1709
|
+
context.socket = sock;
|
|
1710
|
+
storeActiveSocket(sock, safeSessionId);
|
|
1711
|
+
syncLegacyActiveSocketReference();
|
|
1383
1712
|
|
|
1384
|
-
const isCurrentSocket = () =>
|
|
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
|
|
2092
|
+
await currentConnectPromise;
|
|
1736
2093
|
} finally {
|
|
1737
|
-
connectPromise
|
|
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
|
-
|
|
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
|
-
|
|
1781
|
-
storeActiveSocket(null);
|
|
1782
|
-
|
|
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
|
-
|
|
1794
|
-
storeActiveSocket(null);
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
-
|
|
1807
|
-
storeActiveSocket(null);
|
|
1808
|
-
|
|
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: !!
|
|
1981
|
-
socketOpen: isSocketOpen(
|
|
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
|
-
|
|
2214
|
-
|
|
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
|
-
|
|
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) {
|