@omnizap-system/omnizap 2.6.0 → 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 +58 -13
- package/.github/workflows/ci.yml +5 -5
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/db-migration-check.yml +2 -2
- package/.github/workflows/dependency-review.yml +1 -1
- package/.github/workflows/deploy.yml +2 -2
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/security-attest-provenance.yml +2 -2
- package/.github/workflows/security-gitleaks.yml +13 -4
- package/.github/workflows/security-runner-hardening.yml +2 -2
- package/.github/workflows/security-scorecard.yml +1 -1
- package/.github/workflows/security-zap-baseline.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +2 -1
- package/.github/workflows/security-zizmor.yml +1 -1
- package/.github/workflows/wiki-sync.yml +1 -1
- package/.gitleaksignore +9 -0
- package/CODE_OF_CONDUCT.md +2 -2
- package/GEMINI.md +64 -0
- package/README.md +52 -82
- package/SECURITY.md +1 -1
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +230 -58
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +145 -4
- 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 +660 -158
- 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 +93 -13
- 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 +858 -15
- package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
- 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 +135 -27
- 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 +140 -12
- package/app/modules/playModule/playCommand.js +1 -1417
- package/app/modules/playModule/playCommandConstants.js +80 -0
- package/app/modules/playModule/playCommandCore.js +361 -0
- package/app/modules/playModule/playCommandHandlers.js +41 -0
- package/app/modules/playModule/playCommandMediaClient.js +1872 -0
- package/app/modules/playModule/playConfigRuntime.js +245 -4
- package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/quoteModule/quoteCommand.js +3 -2
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/statsModule/rankingCommon.js +5 -4
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/addStickerMetadata.js +4 -3
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerModule/stickerCommand.js +1 -1
- 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/semanticThemeClusterService.js +7 -6
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
- package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/systemMetricsModule/pingCommand.js +6 -5
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/tiktokCommand.js +2 -1
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/userModule/userCommand.js +72 -23
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/conversationRouterService.js +4 -3
- package/app/services/ai/geminiService.js +132 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/globalModuleAiHelpService.js +3 -2
- package/app/services/ai/messageCommandExecutionService.js +2 -1
- package/app/services/ai/moduleAiHelpCoreService.js +45 -14
- package/app/services/ai/moduleToolExecutorService.js +3 -2
- package/app/services/ai/moduleToolRegistryService.js +2 -1
- package/app/services/ai/toolCandidateSelectorService.js +6 -5
- package/app/services/auth/googleWebLinkService.js +3 -2
- package/app/services/auth/whatsappLoginLinkService.js +3 -2
- package/app/services/external/pokeApiService.js +4 -3
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +57 -26
- package/app/services/infra/featureFlagService.js +2 -1
- package/app/services/messaging/captchaService.js +3 -2
- package/app/services/messaging/newsBroadcastService.js +846 -29
- 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/services/sticker/stickerFocusService.js +11 -10
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/conversationSessionStore.js +7 -6
- 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/app/workers/aiLearningWorker.js +6 -5
- package/app/workers/commandConfigEnrichmentWorker.js +4 -3
- package/database/index.js +14 -8
- 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/dpa-b2b-standard-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/incident-response-lgpd-anpd-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/docs/wiki/Home.md +1 -1
- package/ecosystem.prod.config.cjs +32 -12
- package/index.js +57 -23
- 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 +20 -6
- package/public/apple-touch-icon.png +0 -0
- package/public/comandos/commands-catalog.json +2853 -3326
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/js/apps/apiDocsApp.js +3 -2
- package/public/js/apps/commandsReactApp.js +280 -99
- package/public/js/apps/createPackApp.js +11 -10
- package/public/js/apps/homeReactApp.js +181 -130
- package/public/js/apps/loginReactApp.js +1 -1
- package/public/js/apps/stickersApp.js +263 -110
- package/public/js/apps/termsReactApp.js +73 -24
- package/public/js/apps/userApp.js +4 -3
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +355 -280
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/api-docs.html +1 -1
- package/public/pages/aup.html +2 -2
- package/public/pages/dpa.html +3 -3
- package/public/pages/licenca.html +4 -4
- package/public/pages/login.html +1 -1
- package/public/pages/notice-and-takedown.html +2 -2
- package/public/pages/politica-de-privacidade.html +6 -6
- package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
- package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
- package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
- package/public/pages/stickers-admin.html +1 -1
- package/public/pages/stickers-create.html +1 -1
- package/public/pages/stickers.html +6 -6
- package/public/pages/suboperadores.html +2 -2
- package/public/pages/termos-de-uso-texto-integral.html +6 -6
- package/public/pages/termos-de-uso.html +3 -3
- package/public/pages/user-password-reset.html +4 -5
- package/public/pages/user-systemadm.html +9 -463
- package/public/pages/user.html +2 -2
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +11 -1
- package/scripts/email-broadcast-terms-update.mjs +2 -1
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +166 -2
- package/scripts/generate-module-agents.mjs +2 -1
- package/scripts/generate-seo-satellite-pages.mjs +5 -4
- package/scripts/github-deploy-notify.mjs +2 -1
- package/scripts/github-release-notify.mjs +25 -10
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/release.sh +2 -19
- package/scripts/security-smoketest.mjs +6 -5
- package/scripts/security-web-surface-check.mjs +218 -0
- package/scripts/sticker-catalog-loadtest.mjs +5 -4
- package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
- package/server/auth/jwt/webJwtService.js +1 -1
- package/server/auth/stickerCatalogAuthContext.js +2 -1
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
- package/server/auth/userPassword/userPasswordAuthService.js +2 -1
- package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
- package/server/auth/webAccount/webAccountHandlers.js +9 -10
- package/server/controllers/admin/adminPanelHandlers.js +267 -16
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
- package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
- package/server/controllers/sticker/stickerCatalogController.js +23 -36
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/githubController.js +3 -2
- package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/system/systemMetricsController.js +2 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +5 -3
- package/server/http/httpServer.js +11 -6
- package/server/middleware/rateLimit.js +2 -1
- 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/time/timeModule.js +135 -0
- package/utils/time/timeModule.test.js +65 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +7 -1
- package/public/assets/images/brand-icon-192.png +0 -0
- package/scripts/sync-readme-snapshot.mjs +0 -133
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
|
|
1
2
|
import makeWASocket, { DisconnectReason, Browsers, getAggregateVotesInPollMessage, areJidsSameUser, WAMessageStatus, WAMessageStubType, delayCancellable, getStatusFromReceiptType, promiseTimeout } from '@whiskeysockets/baileys';
|
|
2
3
|
|
|
3
4
|
import NodeCache from 'node-cache';
|
|
4
|
-
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';
|
|
5
6
|
|
|
6
7
|
import { Boom } from '@hapi/boom';
|
|
7
8
|
import qrcode from 'qrcode-terminal';
|
|
@@ -15,11 +16,18 @@ import { resolveCaptchaByReaction } from '../services/messaging/captchaService.j
|
|
|
15
16
|
|
|
16
17
|
import { handleGroupUpdate as handleGroupParticipantsEvent, handleGroupJoinRequest } from '../modules/adminModule/groupEventHandlers.js';
|
|
17
18
|
|
|
18
|
-
import { dbConfig, executeQuery, findBy, findById, pool, remove } from '../../database/index.js';
|
|
19
|
+
import { dbConfig, executeQuery, findBy, findById, pool, remove, TABLES } from '../../database/index.js';
|
|
19
20
|
import { extractSenderInfoFromMessage, primeLidCache, resolveUserIdCached, isLidUserId, isWhatsAppUserId } from '../config/index.js';
|
|
20
21
|
import { queueBaileysEventInsert, queueChatUpdate, queueLidUpdate, queueMessageInsert } from '../services/infra/dbWriteQueue.js';
|
|
21
22
|
import { buildGroupMetadataFromGroup, buildGroupMetadataFromUpdate, upsertGroupMetadata, parseParticipantsFromDb } from '../services/group/groupMetadataService.js';
|
|
22
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';
|
|
23
31
|
import { useDbAuthState } from './baileysDbAuthState.js';
|
|
24
32
|
|
|
25
33
|
import { fileURLToPath } from 'node:url';
|
|
@@ -135,10 +143,10 @@ const BAILEYS_GROUP_METADATA_CACHE_CHECKPERIOD_SECONDS = parseEnvInt(process.env
|
|
|
135
143
|
* Permite isolar múltiplas sessões no mesmo banco.
|
|
136
144
|
* @type {string}
|
|
137
145
|
*/
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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';
|
|
142
150
|
/**
|
|
143
151
|
* Habilita bootstrap inicial do auth state no MySQL usando os arquivos locais legados.
|
|
144
152
|
* @type {boolean}
|
|
@@ -163,12 +171,80 @@ const BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS = parseEnvInt(process.env.BAILEY
|
|
|
163
171
|
* Nome do lock de escritor único usado no MySQL.
|
|
164
172
|
* @type {string}
|
|
165
173
|
*/
|
|
166
|
-
const
|
|
174
|
+
const BAILEYS_SINGLE_WRITER_LOCK_NAME_BASE = (() => {
|
|
167
175
|
const raw = String(process.env.BAILEYS_SINGLE_WRITER_LOCK_NAME || '').trim();
|
|
168
176
|
if (raw) return raw;
|
|
169
177
|
const dbLabel = String(dbConfig?.database || 'db').replace(/[^a-zA-Z0-9:_-]+/g, '_');
|
|
170
178
|
return `omnizap:baileys:writer:${dbLabel}`;
|
|
171
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
|
+
});
|
|
172
248
|
/**
|
|
173
249
|
* Habilita ou desabilita o diário de eventos do Baileys.
|
|
174
250
|
* @type {boolean}
|
|
@@ -266,15 +342,63 @@ const normalizeMessageReceiptType = (receiptType) => {
|
|
|
266
342
|
*/
|
|
267
343
|
let activeSocket = null;
|
|
268
344
|
/**
|
|
269
|
-
*
|
|
270
|
-
* @
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
};
|
|
278
402
|
/**
|
|
279
403
|
* Cache para contadores de retentativa de mensagens.
|
|
280
404
|
* @type {NodeCache}
|
|
@@ -321,26 +445,6 @@ const MAX_CONNECTION_ATTEMPTS = 5;
|
|
|
321
445
|
* @type {number}
|
|
322
446
|
*/
|
|
323
447
|
const INITIAL_RECONNECT_DELAY = 3000;
|
|
324
|
-
/**
|
|
325
|
-
* Timeout para reconexão.
|
|
326
|
-
* @type {ReturnType<typeof delayCancellable> | null}
|
|
327
|
-
*/
|
|
328
|
-
let reconnectTimeout = null;
|
|
329
|
-
/**
|
|
330
|
-
* Promessa de conexão ativa.
|
|
331
|
-
* @type {Promise<void> | null}
|
|
332
|
-
*/
|
|
333
|
-
let connectPromise = null;
|
|
334
|
-
/**
|
|
335
|
-
* Geração atual do socket (incrementado a cada nova conexão).
|
|
336
|
-
* @type {number}
|
|
337
|
-
*/
|
|
338
|
-
let socketGeneration = 0;
|
|
339
|
-
/**
|
|
340
|
-
* Conexão MySQL dedicada para manter lock de escritor único do Baileys.
|
|
341
|
-
* @type {import('mysql2/promise').PoolConnection | null}
|
|
342
|
-
*/
|
|
343
|
-
let baileysWriterLockConnection = null;
|
|
344
448
|
/**
|
|
345
449
|
* Nomes de todos os eventos do Baileys que são monitorados.
|
|
346
450
|
* @type {string[]}
|
|
@@ -715,7 +819,7 @@ const registerBaileysEventLoggers = (sock) => {
|
|
|
715
819
|
action: 'baileys_event',
|
|
716
820
|
event: eventName,
|
|
717
821
|
...summary,
|
|
718
|
-
timestamp:
|
|
822
|
+
timestamp: __timeNowIso(),
|
|
719
823
|
});
|
|
720
824
|
});
|
|
721
825
|
}
|
|
@@ -883,12 +987,15 @@ const queueContactsLidUpdates = (contacts, source) => {
|
|
|
883
987
|
* Eventos selecionados são enfileirados para persistência.
|
|
884
988
|
* @param {import('@whiskeysockets/baileys').WASocket} sock - A instância do socket do Baileys.
|
|
885
989
|
* @param {number} generation - A geração atual do socket.
|
|
990
|
+
* @param {string} sessionId - Sessão associada ao socket.
|
|
886
991
|
* @returns {void}
|
|
887
992
|
*/
|
|
888
|
-
const registerBaileysEventJournal = (sock, generation) => {
|
|
993
|
+
const registerBaileysEventJournal = (sock, generation, sessionId) => {
|
|
994
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
889
995
|
if (!BAILEYS_EVENT_JOURNAL_ENABLED) {
|
|
890
996
|
logger.debug('Journal de eventos Baileys desativado por configuração.', {
|
|
891
997
|
action: 'baileys_event_journal_disabled',
|
|
998
|
+
sessionId: safeSessionId,
|
|
892
999
|
});
|
|
893
1000
|
return;
|
|
894
1001
|
}
|
|
@@ -897,6 +1004,7 @@ const registerBaileysEventJournal = (sock, generation) => {
|
|
|
897
1004
|
if (unknownEvents.length > 0) {
|
|
898
1005
|
logger.warn('Alguns eventos configurados para journal não existem na lista conhecida do Baileys.', {
|
|
899
1006
|
action: 'baileys_event_journal_unknown_events',
|
|
1007
|
+
sessionId: safeSessionId,
|
|
900
1008
|
unknownEvents,
|
|
901
1009
|
});
|
|
902
1010
|
}
|
|
@@ -905,28 +1013,53 @@ const registerBaileysEventJournal = (sock, generation) => {
|
|
|
905
1013
|
if (eventsToPersist.length === 0) {
|
|
906
1014
|
logger.warn('Journal de eventos Baileys habilitado sem eventos válidos para persistir.', {
|
|
907
1015
|
action: 'baileys_event_journal_empty',
|
|
1016
|
+
sessionId: safeSessionId,
|
|
908
1017
|
configuredEvents: BAILEYS_EVENT_JOURNAL_EVENT_LIST,
|
|
909
1018
|
});
|
|
910
1019
|
return;
|
|
911
1020
|
}
|
|
912
1021
|
|
|
913
1022
|
for (const eventName of eventsToPersist) {
|
|
914
|
-
sock.ev.on(eventName, (payload) => {
|
|
1023
|
+
sock.ev.on(eventName, async (payload) => {
|
|
915
1024
|
try {
|
|
916
1025
|
const summary = summarizeBaileysEventPayload(eventName, payload);
|
|
917
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
|
+
}
|
|
918
1049
|
queueBaileysEventInsert({
|
|
1050
|
+
session_id: safeSessionId,
|
|
919
1051
|
event_name: eventName,
|
|
920
1052
|
socket_generation: generation,
|
|
921
1053
|
chat_id: refs.chatId,
|
|
922
1054
|
message_id: refs.messageId,
|
|
923
1055
|
participant_id: refs.participantId,
|
|
924
1056
|
payload_summary: summary,
|
|
925
|
-
event_timestamp:
|
|
1057
|
+
event_timestamp: __timeNow(),
|
|
926
1058
|
});
|
|
927
1059
|
} catch (error) {
|
|
928
1060
|
logger.warn('Falha ao enfileirar evento Baileys para journal.', {
|
|
929
1061
|
action: 'baileys_event_journal_enqueue_failed',
|
|
1062
|
+
sessionId: safeSessionId,
|
|
930
1063
|
eventName,
|
|
931
1064
|
error: error?.message,
|
|
932
1065
|
});
|
|
@@ -936,6 +1069,7 @@ const registerBaileysEventJournal = (sock, generation) => {
|
|
|
936
1069
|
|
|
937
1070
|
logger.info('Journal de eventos Baileys habilitado.', {
|
|
938
1071
|
action: 'baileys_event_journal_ready',
|
|
1072
|
+
sessionId: safeSessionId,
|
|
939
1073
|
generation,
|
|
940
1074
|
eventsCount: eventsToPersist.length,
|
|
941
1075
|
events: eventsToPersist,
|
|
@@ -977,11 +1111,13 @@ const safeJsonParse = (value, fallback) => {
|
|
|
977
1111
|
* @param {'append' | 'notify' | string} type - Tipo do evento de upsert.
|
|
978
1112
|
* @returns {Promise<void>} Conclusão da persistência.
|
|
979
1113
|
*/
|
|
980
|
-
async function persistIncomingMessages(incomingMessages, type) {
|
|
1114
|
+
async function persistIncomingMessages(incomingMessages, type, sessionId = BAILEYS_PRIMARY_SESSION_ID) {
|
|
981
1115
|
if (type !== 'append' && type !== 'notify') return;
|
|
982
1116
|
|
|
983
1117
|
const entries = [];
|
|
984
1118
|
const lidsToPrime = new Set();
|
|
1119
|
+
const groupWriteStateByJid = new Map();
|
|
1120
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
985
1121
|
|
|
986
1122
|
for (const msg of incomingMessages) {
|
|
987
1123
|
if (!msg.message || msg.key.remoteJid === 'status@broadcast') continue;
|
|
@@ -1017,7 +1153,36 @@ async function persistIncomingMessages(incomingMessages, type) {
|
|
|
1017
1153
|
|
|
1018
1154
|
const canonicalSenderId = resolveUserIdCached(senderInfo) || msg.key.participant || msg.key.remoteJid;
|
|
1019
1155
|
|
|
1020
|
-
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
|
+
|
|
1021
1186
|
queueMessageInsert(messageData);
|
|
1022
1187
|
}
|
|
1023
1188
|
}
|
|
@@ -1028,17 +1193,36 @@ async function persistIncomingMessages(incomingMessages, type) {
|
|
|
1028
1193
|
* @param {import('@whiskeysockets/baileys').WAMessageKey} key - Chave da mensagem.
|
|
1029
1194
|
* @returns {Promise<import('@whiskeysockets/baileys').proto.IMessage | undefined>} Conteúdo da mensagem armazenada.
|
|
1030
1195
|
*/
|
|
1031
|
-
async function getStoredMessage(key) {
|
|
1196
|
+
async function getStoredMessage(key, sessionId = BAILEYS_PRIMARY_SESSION_ID) {
|
|
1032
1197
|
const messageId = key?.id;
|
|
1033
1198
|
const remoteJid = key?.remoteJid;
|
|
1034
1199
|
if (!messageId || !remoteJid) return undefined;
|
|
1035
1200
|
|
|
1036
1201
|
try {
|
|
1037
|
-
const
|
|
1038
|
-
|
|
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
|
+
|
|
1039
1222
|
const stored = safeJsonParse(record?.raw_message, null);
|
|
1040
1223
|
if (record?.raw_message && !stored) {
|
|
1041
1224
|
logger.error('Falha ao interpretar raw_message armazenado.', {
|
|
1225
|
+
sessionId: safeSessionId,
|
|
1042
1226
|
messageId,
|
|
1043
1227
|
remoteJid,
|
|
1044
1228
|
});
|
|
@@ -1046,6 +1230,7 @@ async function getStoredMessage(key) {
|
|
|
1046
1230
|
return stored?.message ?? undefined;
|
|
1047
1231
|
} catch (error) {
|
|
1048
1232
|
logger.error('Erro ao buscar mensagem armazenada no banco:', {
|
|
1233
|
+
sessionId: normalizeSessionId(sessionId),
|
|
1049
1234
|
error: error.message,
|
|
1050
1235
|
messageId,
|
|
1051
1236
|
remoteJid,
|
|
@@ -1056,64 +1241,75 @@ async function getStoredMessage(key) {
|
|
|
1056
1241
|
|
|
1057
1242
|
/**
|
|
1058
1243
|
* Limpa o timeout de reconexão agendado, se houver.
|
|
1244
|
+
* @param {string} sessionId
|
|
1059
1245
|
* @returns {void}
|
|
1060
1246
|
*/
|
|
1061
|
-
const clearReconnectTimeout = () => {
|
|
1062
|
-
|
|
1063
|
-
reconnectTimeout
|
|
1064
|
-
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;
|
|
1065
1252
|
};
|
|
1066
1253
|
|
|
1067
1254
|
/**
|
|
1068
1255
|
* Reseta o estado das tentativas de reconexão.
|
|
1256
|
+
* @param {string} sessionId
|
|
1069
1257
|
* @returns {void}
|
|
1070
1258
|
*/
|
|
1071
|
-
const resetReconnectState = () => {
|
|
1072
|
-
|
|
1073
|
-
|
|
1259
|
+
const resetReconnectState = (sessionId = BAILEYS_PRIMARY_SESSION_ID) => {
|
|
1260
|
+
const context = getSessionContext(sessionId);
|
|
1261
|
+
context.connectionAttempts = 0;
|
|
1262
|
+
context.reconnectWindowStartedAt = 0;
|
|
1074
1263
|
};
|
|
1075
1264
|
|
|
1076
1265
|
/**
|
|
1077
1266
|
* Calcula o número da próxima tentativa de reconexão.
|
|
1078
1267
|
* Reseta a contagem de tentativas se a janela de reconexão expirou.
|
|
1268
|
+
* @param {string} sessionId
|
|
1079
1269
|
* @returns {number} O número da próxima tentativa.
|
|
1080
1270
|
*/
|
|
1081
|
-
const getNextReconnectAttempt = () => {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1271
|
+
const getNextReconnectAttempt = (sessionId = BAILEYS_PRIMARY_SESSION_ID) => {
|
|
1272
|
+
const context = getSessionContext(sessionId);
|
|
1273
|
+
const now = __timeNowMs();
|
|
1274
|
+
if (!context.reconnectWindowStartedAt || now - context.reconnectWindowStartedAt >= BAILEYS_RECONNECT_ATTEMPT_RESET_MS) {
|
|
1275
|
+
context.reconnectWindowStartedAt = now;
|
|
1276
|
+
context.connectionAttempts = 0;
|
|
1086
1277
|
}
|
|
1087
|
-
connectionAttempts += 1;
|
|
1088
|
-
return connectionAttempts;
|
|
1278
|
+
context.connectionAttempts += 1;
|
|
1279
|
+
return context.connectionAttempts;
|
|
1089
1280
|
};
|
|
1090
1281
|
|
|
1091
1282
|
/**
|
|
1092
1283
|
* Agenda uma reconexão com o WhatsApp após um determinado atraso.
|
|
1093
1284
|
* Evita agendar múltiplas reconexões.
|
|
1285
|
+
* @param {string} sessionId
|
|
1094
1286
|
* @param {number} delay - O atraso em milissegundos antes de tentar a reconexão.
|
|
1095
1287
|
* @returns {void}
|
|
1096
1288
|
*/
|
|
1097
|
-
const scheduleReconnect = (delay) => {
|
|
1098
|
-
|
|
1289
|
+
const scheduleReconnect = (sessionId, delay) => {
|
|
1290
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1291
|
+
const context = getSessionContext(safeSessionId);
|
|
1292
|
+
if (context.reconnectTimeout) return;
|
|
1293
|
+
|
|
1099
1294
|
const pendingReconnect = delayCancellable(Math.max(0, Number(delay) || 0));
|
|
1100
|
-
reconnectTimeout = pendingReconnect;
|
|
1295
|
+
context.reconnectTimeout = pendingReconnect;
|
|
1101
1296
|
pendingReconnect.delay
|
|
1102
1297
|
.then(() => {
|
|
1103
|
-
if (reconnectTimeout !== pendingReconnect) return;
|
|
1104
|
-
reconnectTimeout = null;
|
|
1105
|
-
connectToWhatsApp().catch((error) => {
|
|
1298
|
+
if (context.reconnectTimeout !== pendingReconnect) return;
|
|
1299
|
+
context.reconnectTimeout = null;
|
|
1300
|
+
connectToWhatsApp(safeSessionId).catch((error) => {
|
|
1106
1301
|
logger.error('Falha ao executar reconexão agendada.', {
|
|
1107
1302
|
action: 'reconnect_schedule_failure',
|
|
1303
|
+
sessionId: safeSessionId,
|
|
1108
1304
|
errorMessage: error?.message,
|
|
1109
1305
|
stack: error?.stack,
|
|
1110
|
-
timestamp:
|
|
1306
|
+
timestamp: __timeNowIso(),
|
|
1111
1307
|
});
|
|
1112
1308
|
});
|
|
1113
1309
|
})
|
|
1114
1310
|
.catch((error) => {
|
|
1115
|
-
if (reconnectTimeout === pendingReconnect) {
|
|
1116
|
-
reconnectTimeout = null;
|
|
1311
|
+
if (context.reconnectTimeout === pendingReconnect) {
|
|
1312
|
+
context.reconnectTimeout = null;
|
|
1117
1313
|
}
|
|
1118
1314
|
if (
|
|
1119
1315
|
String(error?.message || '')
|
|
@@ -1124,39 +1320,149 @@ const scheduleReconnect = (delay) => {
|
|
|
1124
1320
|
}
|
|
1125
1321
|
logger.warn('Falha ao aguardar atraso da reconexão agendada.', {
|
|
1126
1322
|
action: 'reconnect_schedule_delay_error',
|
|
1323
|
+
sessionId: safeSessionId,
|
|
1127
1324
|
errorMessage: error?.message,
|
|
1128
1325
|
});
|
|
1129
1326
|
});
|
|
1130
1327
|
};
|
|
1131
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
|
+
|
|
1132
1433
|
/**
|
|
1133
1434
|
* Libera lock de escritor único do Baileys, se estiver ativo.
|
|
1435
|
+
* @param {string} sessionId
|
|
1134
1436
|
* @param {string} reason
|
|
1135
1437
|
* @returns {Promise<void>}
|
|
1136
1438
|
*/
|
|
1137
|
-
const releaseBaileysWriterLock = async (reason = 'unknown') => {
|
|
1138
|
-
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;
|
|
1139
1443
|
if (!connection) return;
|
|
1140
|
-
|
|
1141
|
-
|
|
1444
|
+
const lockName = getWriterLockNameBySession(safeSessionId);
|
|
1445
|
+
context.writerLockConnection = null;
|
|
1142
1446
|
|
|
1143
1447
|
try {
|
|
1144
|
-
const rows = await executeQuery('SELECT RELEASE_LOCK(?) AS released', [
|
|
1448
|
+
const rows = await executeQuery('SELECT RELEASE_LOCK(?) AS released', [lockName], connection);
|
|
1145
1449
|
const released = Number(rows?.[0]?.released) === 1;
|
|
1146
1450
|
logger.info('Lock de escritor do Baileys liberado.', {
|
|
1147
1451
|
action: 'baileys_writer_lock_released',
|
|
1452
|
+
sessionId: safeSessionId,
|
|
1148
1453
|
reason,
|
|
1149
1454
|
released,
|
|
1150
|
-
lockName
|
|
1151
|
-
timestamp:
|
|
1455
|
+
lockName,
|
|
1456
|
+
timestamp: __timeNowIso(),
|
|
1152
1457
|
});
|
|
1153
1458
|
} catch (error) {
|
|
1154
1459
|
logger.warn('Falha ao liberar lock de escritor do Baileys.', {
|
|
1155
1460
|
action: 'baileys_writer_lock_release_error',
|
|
1461
|
+
sessionId: safeSessionId,
|
|
1156
1462
|
reason,
|
|
1157
|
-
lockName
|
|
1463
|
+
lockName,
|
|
1158
1464
|
errorMessage: error?.message,
|
|
1159
|
-
timestamp:
|
|
1465
|
+
timestamp: __timeNowIso(),
|
|
1160
1466
|
});
|
|
1161
1467
|
} finally {
|
|
1162
1468
|
try {
|
|
@@ -1172,41 +1478,48 @@ const releaseBaileysWriterLock = async (reason = 'unknown') => {
|
|
|
1172
1478
|
|
|
1173
1479
|
/**
|
|
1174
1480
|
* Garante lock de escritor único para a sessão do Baileys.
|
|
1481
|
+
* @param {string} sessionId
|
|
1175
1482
|
* @returns {Promise<boolean>}
|
|
1176
1483
|
*/
|
|
1177
|
-
const ensureBaileysWriterLock = async () => {
|
|
1484
|
+
const ensureBaileysWriterLock = async (sessionId) => {
|
|
1178
1485
|
if (!BAILEYS_SINGLE_WRITER_LOCK_ENABLED) {
|
|
1179
1486
|
return true;
|
|
1180
1487
|
}
|
|
1181
1488
|
|
|
1182
|
-
|
|
1489
|
+
const safeSessionId = normalizeSessionId(sessionId);
|
|
1490
|
+
const context = getSessionContext(safeSessionId);
|
|
1491
|
+
const lockName = getWriterLockNameBySession(safeSessionId);
|
|
1492
|
+
|
|
1493
|
+
if (context.writerLockConnection) {
|
|
1183
1494
|
return true;
|
|
1184
1495
|
}
|
|
1185
1496
|
|
|
1186
1497
|
const connection = await pool.getConnection();
|
|
1187
1498
|
|
|
1188
1499
|
try {
|
|
1189
|
-
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);
|
|
1190
1501
|
const lockStatus = Number(rows?.[0]?.lock_status);
|
|
1191
1502
|
if (lockStatus !== 1) {
|
|
1192
1503
|
connection.release();
|
|
1193
1504
|
logger.warn('Nao foi possivel adquirir lock de escritor do Baileys nesta tentativa.', {
|
|
1194
1505
|
action: 'baileys_writer_lock_busy',
|
|
1195
|
-
|
|
1506
|
+
sessionId: safeSessionId,
|
|
1507
|
+
lockName,
|
|
1196
1508
|
timeoutSeconds: BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS,
|
|
1197
1509
|
status: Number.isFinite(lockStatus) ? lockStatus : null,
|
|
1198
1510
|
retryAfterMs: BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS,
|
|
1199
|
-
timestamp:
|
|
1511
|
+
timestamp: __timeNowIso(),
|
|
1200
1512
|
});
|
|
1201
1513
|
return false;
|
|
1202
1514
|
}
|
|
1203
1515
|
|
|
1204
|
-
|
|
1516
|
+
context.writerLockConnection = connection;
|
|
1205
1517
|
logger.info('Lock de escritor do Baileys adquirido com sucesso.', {
|
|
1206
1518
|
action: 'baileys_writer_lock_acquired',
|
|
1207
|
-
|
|
1519
|
+
sessionId: safeSessionId,
|
|
1520
|
+
lockName,
|
|
1208
1521
|
timeoutSeconds: BAILEYS_SINGLE_WRITER_LOCK_TIMEOUT_SECONDS,
|
|
1209
|
-
timestamp:
|
|
1522
|
+
timestamp: __timeNowIso(),
|
|
1210
1523
|
});
|
|
1211
1524
|
return true;
|
|
1212
1525
|
} catch (error) {
|
|
@@ -1219,16 +1532,25 @@ const ensureBaileysWriterLock = async () => {
|
|
|
1219
1532
|
}
|
|
1220
1533
|
};
|
|
1221
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
|
+
|
|
1222
1544
|
process.once('beforeExit', () => {
|
|
1223
|
-
|
|
1545
|
+
releaseAllBaileysWriterLocks('before_exit').catch(() => {});
|
|
1224
1546
|
});
|
|
1225
1547
|
|
|
1226
1548
|
process.once('SIGINT', () => {
|
|
1227
|
-
|
|
1549
|
+
releaseAllBaileysWriterLocks('sigint').catch(() => {});
|
|
1228
1550
|
});
|
|
1229
1551
|
|
|
1230
1552
|
process.once('SIGTERM', () => {
|
|
1231
|
-
|
|
1553
|
+
releaseAllBaileysWriterLocks('sigterm').catch(() => {});
|
|
1232
1554
|
});
|
|
1233
1555
|
|
|
1234
1556
|
/**
|
|
@@ -1264,7 +1586,7 @@ const syncGroupsOnConnectionOpen = async (sock) => {
|
|
|
1264
1586
|
if (!GROUP_SYNC_ON_CONNECT) {
|
|
1265
1587
|
logger.info('Sincronização de grupos no connect desativada por configuração.', {
|
|
1266
1588
|
action: 'groups_sync_disabled',
|
|
1267
|
-
timestamp:
|
|
1589
|
+
timestamp: __timeNowIso(),
|
|
1268
1590
|
});
|
|
1269
1591
|
return;
|
|
1270
1592
|
}
|
|
@@ -1303,7 +1625,7 @@ const syncGroupsOnConnectionOpen = async (sock) => {
|
|
|
1303
1625
|
batchSize: GROUP_SYNC_BATCH_SIZE,
|
|
1304
1626
|
maxGroups: GROUP_SYNC_MAX_GROUPS > 0 ? GROUP_SYNC_MAX_GROUPS : null,
|
|
1305
1627
|
timeoutMs: GROUP_SYNC_TIMEOUT_MS,
|
|
1306
|
-
timestamp:
|
|
1628
|
+
timestamp: __timeNowIso(),
|
|
1307
1629
|
});
|
|
1308
1630
|
};
|
|
1309
1631
|
|
|
@@ -1315,31 +1637,36 @@ const syncGroupsOnConnectionOpen = async (sock) => {
|
|
|
1315
1637
|
* @returns {Promise<void>} Conclusão da inicialização e do registro de handlers.
|
|
1316
1638
|
* @throws {Error} Lança erro se a conexão inicial falhar.
|
|
1317
1639
|
*/
|
|
1318
|
-
export async function connectToWhatsApp() {
|
|
1319
|
-
|
|
1320
|
-
|
|
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;
|
|
1321
1646
|
}
|
|
1322
1647
|
|
|
1323
|
-
if (isSocketOpen(
|
|
1648
|
+
if (isSocketOpen(context.socket)) {
|
|
1324
1649
|
return;
|
|
1325
1650
|
}
|
|
1326
1651
|
|
|
1327
1652
|
logger.info('Iniciando conexão com o WhatsApp...', {
|
|
1328
1653
|
action: 'connect_init',
|
|
1329
|
-
|
|
1654
|
+
sessionId: safeSessionId,
|
|
1655
|
+
timestamp: __timeNowIso(),
|
|
1330
1656
|
});
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1657
|
+
|
|
1658
|
+
const currentConnectPromise = (async () => {
|
|
1659
|
+
clearReconnectTimeout(safeSessionId);
|
|
1660
|
+
const isWriterReady = await ensureBaileysWriterLock(safeSessionId);
|
|
1334
1661
|
if (!isWriterReady) {
|
|
1335
|
-
scheduleReconnect(BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS);
|
|
1662
|
+
scheduleReconnect(safeSessionId, BAILEYS_SINGLE_WRITER_LOCK_RETRY_DELAY_MS);
|
|
1336
1663
|
return;
|
|
1337
1664
|
}
|
|
1338
1665
|
|
|
1339
|
-
const generation = ++socketGeneration;
|
|
1666
|
+
const generation = ++context.socketGeneration;
|
|
1340
1667
|
const legacyAuthPath = path.join(__dirname, 'auth');
|
|
1341
1668
|
const { state, saveCreds } = await useDbAuthState({
|
|
1342
|
-
sessionId:
|
|
1669
|
+
sessionId: safeSessionId,
|
|
1343
1670
|
bootstrapFromDir: legacyAuthPath,
|
|
1344
1671
|
bootstrapFromFiles: BAILEYS_AUTH_BOOTSTRAP_FROM_FILES,
|
|
1345
1672
|
});
|
|
@@ -1347,7 +1674,8 @@ export async function connectToWhatsApp() {
|
|
|
1347
1674
|
const version = await resolveBaileysVersion();
|
|
1348
1675
|
|
|
1349
1676
|
logger.debug('Dados de autenticação carregados com sucesso.', {
|
|
1350
|
-
|
|
1677
|
+
sessionId: safeSessionId,
|
|
1678
|
+
authSessionId: safeSessionId,
|
|
1351
1679
|
bootstrappedFromFiles: BAILEYS_AUTH_BOOTSTRAP_FROM_FILES,
|
|
1352
1680
|
version,
|
|
1353
1681
|
generation,
|
|
@@ -1365,7 +1693,7 @@ export async function connectToWhatsApp() {
|
|
|
1365
1693
|
msgRetryCounterCache,
|
|
1366
1694
|
maxMsgRetryCount: 5,
|
|
1367
1695
|
retryRequestDelayMs: 250,
|
|
1368
|
-
getMessage: getStoredMessage,
|
|
1696
|
+
getMessage: (key) => getStoredMessage(key, safeSessionId),
|
|
1369
1697
|
userDevicesCache,
|
|
1370
1698
|
mediaCache,
|
|
1371
1699
|
cachedGroupMetadata: resolveCachedGroupMetadata,
|
|
@@ -1376,33 +1704,40 @@ export async function connectToWhatsApp() {
|
|
|
1376
1704
|
};
|
|
1377
1705
|
|
|
1378
1706
|
const sock = makeWASocket(socketConfig);
|
|
1707
|
+
sock.__omnizapSessionId = safeSessionId;
|
|
1379
1708
|
|
|
1380
|
-
|
|
1381
|
-
storeActiveSocket(sock);
|
|
1709
|
+
context.socket = sock;
|
|
1710
|
+
storeActiveSocket(sock, safeSessionId);
|
|
1711
|
+
syncLegacyActiveSocketReference();
|
|
1382
1712
|
|
|
1383
|
-
const isCurrentSocket = () =>
|
|
1713
|
+
const isCurrentSocket = () => {
|
|
1714
|
+
const latest = getSessionContext(safeSessionId, { createIfMissing: false });
|
|
1715
|
+
return Boolean(latest && latest.socket === sock && latest.socketGeneration === generation);
|
|
1716
|
+
};
|
|
1384
1717
|
|
|
1385
1718
|
sock.ev.on('creds.update', async () => {
|
|
1386
1719
|
if (!isCurrentSocket()) return;
|
|
1387
1720
|
logger.debug('Atualizando credenciais de autenticação...', {
|
|
1388
1721
|
action: 'creds_update',
|
|
1389
|
-
|
|
1722
|
+
sessionId: safeSessionId,
|
|
1723
|
+
timestamp: __timeNowIso(),
|
|
1390
1724
|
});
|
|
1391
1725
|
await saveCreds();
|
|
1392
1726
|
});
|
|
1393
1727
|
|
|
1394
1728
|
sock.ev.on('connection.update', (update) => {
|
|
1395
1729
|
if (!isCurrentSocket()) return;
|
|
1396
|
-
handleConnectionUpdate(update, sock);
|
|
1730
|
+
handleConnectionUpdate(update, sock, safeSessionId, generation);
|
|
1397
1731
|
if (update.connection === 'open') {
|
|
1398
1732
|
syncNewsBroadcastService();
|
|
1399
1733
|
}
|
|
1400
1734
|
logger.debug('Estado da conexão atualizado.', {
|
|
1401
1735
|
action: 'connection_update',
|
|
1736
|
+
sessionId: safeSessionId,
|
|
1402
1737
|
status: update.connection,
|
|
1403
1738
|
lastDisconnect: update.lastDisconnect?.error?.message || null,
|
|
1404
1739
|
isNewLogin: update.isNewLogin || false,
|
|
1405
|
-
timestamp:
|
|
1740
|
+
timestamp: __timeNowIso(),
|
|
1406
1741
|
});
|
|
1407
1742
|
});
|
|
1408
1743
|
|
|
@@ -1414,17 +1749,19 @@ export async function connectToWhatsApp() {
|
|
|
1414
1749
|
try {
|
|
1415
1750
|
logger.debug('Novo(s) evento(s) em messages.upsert', {
|
|
1416
1751
|
action: 'messages_upsert',
|
|
1752
|
+
sessionId: safeSessionId,
|
|
1417
1753
|
type: update.type,
|
|
1418
1754
|
messagesCount: update.messages.length,
|
|
1419
1755
|
remoteJid: update.messages[0]?.key.remoteJid || null,
|
|
1420
1756
|
});
|
|
1421
|
-
const persistPromise = persistIncomingMessages(update.messages, update.type).catch((error) => {
|
|
1757
|
+
const persistPromise = persistIncomingMessages(update.messages, update.type, safeSessionId).catch((error) => {
|
|
1422
1758
|
logger.error('Erro ao persistir mensagens no banco de dados:', {
|
|
1759
|
+
sessionId: safeSessionId,
|
|
1423
1760
|
error: error.message,
|
|
1424
1761
|
});
|
|
1425
1762
|
recordError('messages_upsert');
|
|
1426
1763
|
});
|
|
1427
|
-
const handlePromise = handleMessages(update, sock).catch((error) => {
|
|
1764
|
+
const handlePromise = handleMessages(update, sock, { sessionId: safeSessionId }).catch((error) => {
|
|
1428
1765
|
recordError('messages_upsert');
|
|
1429
1766
|
throw error;
|
|
1430
1767
|
});
|
|
@@ -1441,6 +1778,7 @@ export async function connectToWhatsApp() {
|
|
|
1441
1778
|
});
|
|
1442
1779
|
} catch (error) {
|
|
1443
1780
|
logger.error('Erro no evento messages.upsert:', {
|
|
1781
|
+
sessionId: safeSessionId,
|
|
1444
1782
|
error: error.message,
|
|
1445
1783
|
stack: error.stack,
|
|
1446
1784
|
action: 'messages_upsert_error',
|
|
@@ -1475,6 +1813,7 @@ export async function connectToWhatsApp() {
|
|
|
1475
1813
|
for (const chatId of deletions) {
|
|
1476
1814
|
remove('chats', chatId).catch((error) => {
|
|
1477
1815
|
logger.error('Erro ao remover chat do banco:', {
|
|
1816
|
+
sessionId: safeSessionId,
|
|
1478
1817
|
error: error.message,
|
|
1479
1818
|
chatId,
|
|
1480
1819
|
});
|
|
@@ -1492,6 +1831,7 @@ export async function connectToWhatsApp() {
|
|
|
1492
1831
|
invalidateCachedGroupMetadata(group.id);
|
|
1493
1832
|
} catch (error) {
|
|
1494
1833
|
logger.error('Erro no upsert do grupo:', {
|
|
1834
|
+
sessionId: safeSessionId,
|
|
1495
1835
|
error: error.message,
|
|
1496
1836
|
groupId: group.id,
|
|
1497
1837
|
});
|
|
@@ -1518,6 +1858,7 @@ export async function connectToWhatsApp() {
|
|
|
1518
1858
|
queueLidUpdate(lid, pnJid, 'lid-mapping');
|
|
1519
1859
|
} catch (error) {
|
|
1520
1860
|
logger.warn('Falha ao processar lid-mapping.update para lid_map.', {
|
|
1861
|
+
sessionId: safeSessionId,
|
|
1521
1862
|
error: error.message,
|
|
1522
1863
|
});
|
|
1523
1864
|
}
|
|
@@ -1528,11 +1869,13 @@ export async function connectToWhatsApp() {
|
|
|
1528
1869
|
try {
|
|
1529
1870
|
logger.debug('Atualização de mensagens recebida.', {
|
|
1530
1871
|
action: 'messages_update',
|
|
1872
|
+
sessionId: safeSessionId,
|
|
1531
1873
|
updatesCount: update.length,
|
|
1532
1874
|
});
|
|
1533
1875
|
handleMessageUpdate(update, sock);
|
|
1534
1876
|
} catch (error) {
|
|
1535
1877
|
logger.error('Erro no evento messages.update:', {
|
|
1878
|
+
sessionId: safeSessionId,
|
|
1536
1879
|
error: error.message,
|
|
1537
1880
|
stack: error.stack,
|
|
1538
1881
|
action: 'messages_update_error',
|
|
@@ -1549,6 +1892,7 @@ export async function connectToWhatsApp() {
|
|
|
1549
1892
|
const firstError = erroredUpdates[0]?.error;
|
|
1550
1893
|
logger.warn('Falha reportada em atualização de mídia.', {
|
|
1551
1894
|
action: 'messages_media_update_error',
|
|
1895
|
+
sessionId: safeSessionId,
|
|
1552
1896
|
updatesCount: updates.length,
|
|
1553
1897
|
errorCount: erroredUpdates.length,
|
|
1554
1898
|
firstMessageId: erroredUpdates[0]?.key?.id || null,
|
|
@@ -1560,6 +1904,7 @@ export async function connectToWhatsApp() {
|
|
|
1560
1904
|
|
|
1561
1905
|
logger.debug('Atualização de mídia de mensagem recebida.', {
|
|
1562
1906
|
action: 'messages_media_update',
|
|
1907
|
+
sessionId: safeSessionId,
|
|
1563
1908
|
updatesCount: updates.length,
|
|
1564
1909
|
});
|
|
1565
1910
|
});
|
|
@@ -1581,6 +1926,7 @@ export async function connectToWhatsApp() {
|
|
|
1581
1926
|
|
|
1582
1927
|
logger.debug('Atualização de recibos de mensagem recebida.', {
|
|
1583
1928
|
action: 'message_receipt_update',
|
|
1929
|
+
sessionId: safeSessionId,
|
|
1584
1930
|
updatesCount: updates.length,
|
|
1585
1931
|
receiptTypes: Array.from(receiptTypes),
|
|
1586
1932
|
invalidReceiptTypeCount,
|
|
@@ -1620,6 +1966,7 @@ export async function connectToWhatsApp() {
|
|
|
1620
1966
|
}
|
|
1621
1967
|
} catch (error) {
|
|
1622
1968
|
logger.error('Erro no evento messages.reaction:', {
|
|
1969
|
+
sessionId: safeSessionId,
|
|
1623
1970
|
error: error.message,
|
|
1624
1971
|
stack: error.stack,
|
|
1625
1972
|
action: 'messages_reaction_error',
|
|
@@ -1632,12 +1979,14 @@ export async function connectToWhatsApp() {
|
|
|
1632
1979
|
try {
|
|
1633
1980
|
logger.debug('Grupo(s) atualizado(s).', {
|
|
1634
1981
|
action: 'groups_update',
|
|
1982
|
+
sessionId: safeSessionId,
|
|
1635
1983
|
groupCount: updates.length,
|
|
1636
1984
|
groupIds: updates.map((u) => u.id),
|
|
1637
1985
|
});
|
|
1638
1986
|
handleGroupUpdate(updates);
|
|
1639
1987
|
} catch (err) {
|
|
1640
1988
|
logger.error('Erro no evento groups.update:', {
|
|
1989
|
+
sessionId: safeSessionId,
|
|
1641
1990
|
error: err.message,
|
|
1642
1991
|
stack: err.stack,
|
|
1643
1992
|
action: 'groups_update_error',
|
|
@@ -1650,6 +1999,7 @@ export async function connectToWhatsApp() {
|
|
|
1650
1999
|
try {
|
|
1651
2000
|
logger.debug('Participantes do grupo atualizados.', {
|
|
1652
2001
|
action: 'group_participants_update',
|
|
2002
|
+
sessionId: safeSessionId,
|
|
1653
2003
|
groupId: update.id,
|
|
1654
2004
|
actionType: update.action,
|
|
1655
2005
|
participants: update.participants,
|
|
@@ -1658,6 +2008,7 @@ export async function connectToWhatsApp() {
|
|
|
1658
2008
|
handleGroupParticipantsEvent(sock, update.id, update.participants, update.action);
|
|
1659
2009
|
} catch (err) {
|
|
1660
2010
|
logger.error('Erro no evento group-participants.update:', {
|
|
2011
|
+
sessionId: safeSessionId,
|
|
1661
2012
|
error: err.message,
|
|
1662
2013
|
stack: err.stack,
|
|
1663
2014
|
action: 'group_participants_update_error',
|
|
@@ -1670,6 +2021,7 @@ export async function connectToWhatsApp() {
|
|
|
1670
2021
|
try {
|
|
1671
2022
|
logger.debug('Solicitação de entrada no grupo recebida.', {
|
|
1672
2023
|
action: 'group_join_request',
|
|
2024
|
+
sessionId: safeSessionId,
|
|
1673
2025
|
groupId: update?.id,
|
|
1674
2026
|
participant: update?.participant,
|
|
1675
2027
|
method: update?.method,
|
|
@@ -1678,6 +2030,7 @@ export async function connectToWhatsApp() {
|
|
|
1678
2030
|
handleGroupJoinRequest(sock, update);
|
|
1679
2031
|
} catch (err) {
|
|
1680
2032
|
logger.error('Erro no evento group.join-request:', {
|
|
2033
|
+
sessionId: safeSessionId,
|
|
1681
2034
|
error: err.message,
|
|
1682
2035
|
stack: err.stack,
|
|
1683
2036
|
action: 'group_join_request_error',
|
|
@@ -1703,15 +2056,17 @@ export async function connectToWhatsApp() {
|
|
|
1703
2056
|
await sock.rejectCall(call.id, call.from);
|
|
1704
2057
|
logger.info('Chamada recebida rejeitada automaticamente.', {
|
|
1705
2058
|
action: 'call_auto_reject',
|
|
2059
|
+
sessionId: safeSessionId,
|
|
1706
2060
|
callId: call.id,
|
|
1707
2061
|
from: call.from,
|
|
1708
2062
|
isGroup: call.isGroup || false,
|
|
1709
2063
|
isVideo: call.isVideo || false,
|
|
1710
|
-
timestamp:
|
|
2064
|
+
timestamp: __timeNowIso(),
|
|
1711
2065
|
});
|
|
1712
2066
|
} catch (error) {
|
|
1713
2067
|
logger.warn('Falha ao rejeitar chamada automaticamente.', {
|
|
1714
2068
|
action: 'call_auto_reject_failed',
|
|
2069
|
+
sessionId: safeSessionId,
|
|
1715
2070
|
callId: call?.id || null,
|
|
1716
2071
|
from: call?.from || null,
|
|
1717
2072
|
error: error?.message,
|
|
@@ -1721,19 +2076,45 @@ export async function connectToWhatsApp() {
|
|
|
1721
2076
|
});
|
|
1722
2077
|
|
|
1723
2078
|
registerBaileysEventLoggers(sock);
|
|
1724
|
-
registerBaileysEventJournal(sock, generation);
|
|
2079
|
+
registerBaileysEventJournal(sock, generation, safeSessionId);
|
|
1725
2080
|
|
|
1726
2081
|
logger.info('Conexão com o WhatsApp estabelecida com sucesso.', {
|
|
1727
2082
|
action: 'connect_success',
|
|
2083
|
+
sessionId: safeSessionId,
|
|
1728
2084
|
generation,
|
|
1729
|
-
timestamp:
|
|
2085
|
+
timestamp: __timeNowIso(),
|
|
1730
2086
|
});
|
|
1731
2087
|
})();
|
|
1732
2088
|
|
|
2089
|
+
context.connectPromise = currentConnectPromise;
|
|
2090
|
+
|
|
1733
2091
|
try {
|
|
1734
|
-
await
|
|
2092
|
+
await currentConnectPromise;
|
|
1735
2093
|
} finally {
|
|
1736
|
-
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;
|
|
1737
2118
|
}
|
|
1738
2119
|
}
|
|
1739
2120
|
|
|
@@ -1743,16 +2124,24 @@ export async function connectToWhatsApp() {
|
|
|
1743
2124
|
* @async
|
|
1744
2125
|
* @param {import('@whiskeysockets/baileys').ConnectionState} update - Objeto contendo o estado atual da conexão.
|
|
1745
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.
|
|
1746
2129
|
* @returns {Promise<void>} Uma promessa que resolve quando o processamento do estado da conexão é concluído.
|
|
1747
2130
|
*/
|
|
1748
|
-
async function handleConnectionUpdate(update, sock) {
|
|
1749
|
-
|
|
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
|
+
|
|
1750
2138
|
const { connection, lastDisconnect, qr } = update;
|
|
1751
2139
|
|
|
1752
2140
|
if (qr) {
|
|
1753
2141
|
logger.info('📱 QR Code gerado! Escaneie com seu WhatsApp.', {
|
|
1754
2142
|
action: 'qr_code_generated',
|
|
1755
|
-
|
|
2143
|
+
sessionId: safeSessionId,
|
|
2144
|
+
timestamp: __timeNowIso(),
|
|
1756
2145
|
});
|
|
1757
2146
|
qrcode.generate(qr, { small: true });
|
|
1758
2147
|
}
|
|
@@ -1762,66 +2151,109 @@ async function handleConnectionUpdate(update, sock) {
|
|
|
1762
2151
|
const errorMessage = lastDisconnect?.error?.message || 'Sem mensagem de erro';
|
|
1763
2152
|
|
|
1764
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
|
+
});
|
|
1765
2172
|
|
|
1766
2173
|
if (shouldReconnect) {
|
|
1767
|
-
const attempt = getNextReconnectAttempt();
|
|
2174
|
+
const attempt = getNextReconnectAttempt(safeSessionId);
|
|
1768
2175
|
if (attempt <= MAX_CONNECTION_ATTEMPTS) {
|
|
1769
2176
|
const reconnectDelay = INITIAL_RECONNECT_DELAY * Math.pow(2, attempt - 1);
|
|
1770
2177
|
logger.warn(`⚠️ Conexão perdida. Tentando reconectar...`, {
|
|
1771
2178
|
action: 'reconnect_attempt',
|
|
2179
|
+
sessionId: safeSessionId,
|
|
1772
2180
|
attempt,
|
|
1773
2181
|
maxAttempts: MAX_CONNECTION_ATTEMPTS,
|
|
1774
2182
|
delay: reconnectDelay,
|
|
1775
2183
|
reasonCode: disconnectCode,
|
|
1776
2184
|
errorMessage,
|
|
1777
|
-
timestamp:
|
|
2185
|
+
timestamp: __timeNowIso(),
|
|
1778
2186
|
});
|
|
1779
|
-
|
|
1780
|
-
storeActiveSocket(null);
|
|
1781
|
-
|
|
2187
|
+
context.socket = null;
|
|
2188
|
+
storeActiveSocket(null, safeSessionId);
|
|
2189
|
+
syncLegacyActiveSocketReference();
|
|
2190
|
+
scheduleReconnect(safeSessionId, reconnectDelay);
|
|
1782
2191
|
} else {
|
|
1783
2192
|
logger.error('❌ Limite de tentativas atingido; aguardando janela para novo retry.', {
|
|
1784
2193
|
action: 'reconnect_backoff_window',
|
|
2194
|
+
sessionId: safeSessionId,
|
|
1785
2195
|
totalAttempts: attempt,
|
|
1786
2196
|
maxAttempts: MAX_CONNECTION_ATTEMPTS,
|
|
1787
2197
|
retryAfterMs: BAILEYS_RECONNECT_ATTEMPT_RESET_MS,
|
|
1788
2198
|
reasonCode: disconnectCode,
|
|
1789
2199
|
errorMessage,
|
|
1790
|
-
timestamp:
|
|
2200
|
+
timestamp: __timeNowIso(),
|
|
1791
2201
|
});
|
|
1792
|
-
|
|
1793
|
-
storeActiveSocket(null);
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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);
|
|
1797
2208
|
}
|
|
1798
2209
|
} else {
|
|
1799
2210
|
logger.error('❌ Conexão fechada definitivamente.', {
|
|
1800
2211
|
action: 'connection_closed',
|
|
2212
|
+
sessionId: safeSessionId,
|
|
1801
2213
|
reasonCode: disconnectCode,
|
|
1802
2214
|
errorMessage,
|
|
1803
|
-
timestamp:
|
|
2215
|
+
timestamp: __timeNowIso(),
|
|
1804
2216
|
});
|
|
1805
|
-
|
|
1806
|
-
storeActiveSocket(null);
|
|
1807
|
-
|
|
2217
|
+
context.socket = null;
|
|
2218
|
+
storeActiveSocket(null, safeSessionId);
|
|
2219
|
+
syncLegacyActiveSocketReference();
|
|
2220
|
+
await releaseBaileysWriterLock(safeSessionId, 'connection_closed_no_reconnect');
|
|
1808
2221
|
}
|
|
1809
2222
|
}
|
|
1810
2223
|
|
|
1811
2224
|
if (connection === 'open') {
|
|
1812
2225
|
logger.info('✅ Conectado com sucesso ao WhatsApp!', {
|
|
1813
2226
|
action: 'connection_open',
|
|
1814
|
-
|
|
2227
|
+
sessionId: safeSessionId,
|
|
2228
|
+
timestamp: __timeNowIso(),
|
|
1815
2229
|
});
|
|
1816
2230
|
|
|
1817
|
-
resetReconnectState();
|
|
1818
|
-
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
|
+
});
|
|
1819
2250
|
|
|
1820
2251
|
if (process.send) {
|
|
1821
2252
|
process.send('ready');
|
|
1822
2253
|
logger.info('🟢 Sinal de "ready" enviado ao PM2.', {
|
|
1823
2254
|
action: 'pm2_ready_signal',
|
|
1824
|
-
|
|
2255
|
+
sessionId: safeSessionId,
|
|
2256
|
+
timestamp: __timeNowIso(),
|
|
1825
2257
|
});
|
|
1826
2258
|
}
|
|
1827
2259
|
|
|
@@ -1830,10 +2262,11 @@ async function handleConnectionUpdate(update, sock) {
|
|
|
1830
2262
|
} catch (error) {
|
|
1831
2263
|
logger.error('❌ Erro ao carregar metadados de grupos na conexão.', {
|
|
1832
2264
|
action: 'groups_load_error',
|
|
2265
|
+
sessionId: safeSessionId,
|
|
1833
2266
|
errorMessage: error.message,
|
|
1834
2267
|
stack: error.stack,
|
|
1835
2268
|
timeoutMs: GROUP_SYNC_TIMEOUT_MS,
|
|
1836
|
-
timestamp:
|
|
2269
|
+
timestamp: __timeNowIso(),
|
|
1837
2270
|
});
|
|
1838
2271
|
}
|
|
1839
2272
|
}
|
|
@@ -1864,7 +2297,7 @@ async function handleMessageUpdate(updates, sock) {
|
|
|
1864
2297
|
statusName: status?.name ?? null,
|
|
1865
2298
|
stubTypeCode: stubType?.code ?? null,
|
|
1866
2299
|
stubTypeName: stubType?.name ?? null,
|
|
1867
|
-
timestamp:
|
|
2300
|
+
timestamp: __timeNowIso(),
|
|
1868
2301
|
});
|
|
1869
2302
|
}
|
|
1870
2303
|
|
|
@@ -1885,13 +2318,13 @@ async function handleMessageUpdate(updates, sock) {
|
|
|
1885
2318
|
participant: key.participant || null,
|
|
1886
2319
|
votesCount: Object.values(aggregatedVotes || {}).reduce((a, b) => a + b, 0),
|
|
1887
2320
|
votes: aggregatedVotes,
|
|
1888
|
-
timestamp:
|
|
2321
|
+
timestamp: __timeNowIso(),
|
|
1889
2322
|
});
|
|
1890
2323
|
} else {
|
|
1891
2324
|
logger.warn('⚠️ Mensagem da enquete não encontrada.', {
|
|
1892
2325
|
action: 'poll_message_not_found',
|
|
1893
2326
|
key,
|
|
1894
|
-
timestamp:
|
|
2327
|
+
timestamp: __timeNowIso(),
|
|
1895
2328
|
});
|
|
1896
2329
|
}
|
|
1897
2330
|
} catch (error) {
|
|
@@ -1900,7 +2333,7 @@ async function handleMessageUpdate(updates, sock) {
|
|
|
1900
2333
|
errorMessage: error.message,
|
|
1901
2334
|
stack: error.stack,
|
|
1902
2335
|
key,
|
|
1903
|
-
timestamp:
|
|
2336
|
+
timestamp: __timeNowIso(),
|
|
1904
2337
|
});
|
|
1905
2338
|
}
|
|
1906
2339
|
}
|
|
@@ -1932,7 +2365,7 @@ async function handleGroupUpdate(updates) {
|
|
|
1932
2365
|
groupId,
|
|
1933
2366
|
groupName: updatedData.subject || oldData.subject || 'Desconhecido',
|
|
1934
2367
|
changedFields,
|
|
1935
|
-
timestamp:
|
|
2368
|
+
timestamp: __timeNowIso(),
|
|
1936
2369
|
});
|
|
1937
2370
|
} catch (error) {
|
|
1938
2371
|
logger.error('❌ Erro ao atualizar metadados do grupo', {
|
|
@@ -1940,7 +2373,7 @@ async function handleGroupUpdate(updates) {
|
|
|
1940
2373
|
errorMessage: error.message,
|
|
1941
2374
|
stack: error.stack,
|
|
1942
2375
|
event,
|
|
1943
|
-
timestamp:
|
|
2376
|
+
timestamp: __timeNowIso(),
|
|
1944
2377
|
});
|
|
1945
2378
|
}
|
|
1946
2379
|
}),
|
|
@@ -1952,14 +2385,26 @@ async function handleGroupUpdate(updates) {
|
|
|
1952
2385
|
* @returns {import('@whiskeysockets/baileys').WASocket | null} O objeto socket do Baileys ativo ou `null` se não houver conexão ativa.
|
|
1953
2386
|
*/
|
|
1954
2387
|
export function getActiveSocket() {
|
|
2388
|
+
syncLegacyActiveSocketReference();
|
|
1955
2389
|
logger.debug('🔍 Recuperando instância do socket ativo.', {
|
|
1956
2390
|
action: 'get_active_socket',
|
|
1957
2391
|
socketExists: !!activeSocket,
|
|
1958
|
-
timestamp:
|
|
2392
|
+
timestamp: __timeNowIso(),
|
|
1959
2393
|
});
|
|
1960
2394
|
return activeSocket;
|
|
1961
2395
|
}
|
|
1962
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
|
+
|
|
1963
2408
|
/**
|
|
1964
2409
|
* Executa um método centralizado no socket ativo, tratando erros e mapeando-os para respostas HTTP.
|
|
1965
2410
|
* @async
|
|
@@ -1969,6 +2414,7 @@ export function getActiveSocket() {
|
|
|
1969
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.
|
|
1970
2415
|
*/
|
|
1971
2416
|
async function runControllerSocketMethod(methodName, ...args) {
|
|
2417
|
+
const socket = getActiveSocket();
|
|
1972
2418
|
try {
|
|
1973
2419
|
return await runActiveSocketMethod(methodName, ...args);
|
|
1974
2420
|
} catch (error) {
|
|
@@ -1976,9 +2422,9 @@ async function runControllerSocketMethod(methodName, ...args) {
|
|
|
1976
2422
|
if (message.includes('Socket do WhatsApp indisponível')) {
|
|
1977
2423
|
logger.warn('Socket ativo indisponível para operação.', {
|
|
1978
2424
|
action: methodName,
|
|
1979
|
-
socketExists: !!
|
|
1980
|
-
socketOpen: isSocketOpen(
|
|
1981
|
-
timestamp:
|
|
2425
|
+
socketExists: !!socket,
|
|
2426
|
+
socketOpen: isSocketOpen(socket),
|
|
2427
|
+
timestamp: __timeNowIso(),
|
|
1982
2428
|
});
|
|
1983
2429
|
throw new Boom('Socket do WhatsApp indisponível no momento.', { statusCode: 503 });
|
|
1984
2430
|
}
|
|
@@ -2206,29 +2652,85 @@ export async function rejectCall(callId, callFrom) {
|
|
|
2206
2652
|
* Encerra o socket ativo atual, se existir, para disparar a lógica de reconexão.
|
|
2207
2653
|
* Se nenhum socket estiver ativo, inicia uma nova conexão.
|
|
2208
2654
|
* @async
|
|
2655
|
+
* @param {string} [sessionId=BAILEYS_PRIMARY_SESSION_ID]
|
|
2209
2656
|
* @returns {Promise<void>} Uma promessa que resolve quando o fluxo de reconexão é iniciado ou uma nova conexão é tentada.
|
|
2210
2657
|
*/
|
|
2211
|
-
export async function reconnectToWhatsApp() {
|
|
2212
|
-
|
|
2213
|
-
|
|
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)) {
|
|
2214
2662
|
logger.info('♻️ Forçando fechamento do socket para reconectar...', {
|
|
2215
2663
|
action: 'force_reconnect',
|
|
2216
|
-
|
|
2664
|
+
sessionId: safeSessionId,
|
|
2665
|
+
timestamp: __timeNowIso(),
|
|
2217
2666
|
});
|
|
2218
|
-
|
|
2667
|
+
targetSocket.ws?.close?.();
|
|
2219
2668
|
} else {
|
|
2220
2669
|
logger.warn('⚠️ Nenhum socket ativo detectado. Iniciando nova conexão manualmente.', {
|
|
2221
2670
|
action: 'reconnect_no_active_socket',
|
|
2222
|
-
|
|
2671
|
+
sessionId: safeSessionId,
|
|
2672
|
+
timestamp: __timeNowIso(),
|
|
2223
2673
|
});
|
|
2224
|
-
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');
|
|
2225
2726
|
}
|
|
2727
|
+
syncLegacyActiveSocketReference();
|
|
2226
2728
|
}
|
|
2227
2729
|
|
|
2228
2730
|
if (process.argv[1] === __filename) {
|
|
2229
2731
|
logger.info('🚀 Socket Controller iniciado diretamente via CLI.', {
|
|
2230
2732
|
action: 'module_direct_execution',
|
|
2231
|
-
timestamp:
|
|
2733
|
+
timestamp: __timeNowIso(),
|
|
2232
2734
|
});
|
|
2233
2735
|
|
|
2234
2736
|
connectToWhatsApp().catch((err) => {
|
|
@@ -2236,7 +2738,7 @@ if (process.argv[1] === __filename) {
|
|
|
2236
2738
|
action: 'direct_connection_failure',
|
|
2237
2739
|
errorMessage: err.message,
|
|
2238
2740
|
stack: err.stack,
|
|
2239
|
-
timestamp:
|
|
2741
|
+
timestamp: __timeNowIso(),
|
|
2240
2742
|
});
|
|
2241
2743
|
process.exit(1);
|
|
2242
2744
|
});
|