@omnizap-system/omnizap 2.6.1 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.env.example +78 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +6 -0
  6. package/app/configParts/adminIdentity.js +36 -7
  7. package/app/configParts/baileysConfig.js +343 -56
  8. package/app/configParts/groupUtils.js +226 -0
  9. package/app/configParts/loggerConfig.js +185 -0
  10. package/app/configParts/messagePersistenceService.js +307 -5
  11. package/app/configParts/sessionConfig.js +242 -0
  12. package/app/connection/baileysCompatibility.test.js +10 -1
  13. package/app/connection/baileysDbAuthState.js +205 -9
  14. package/app/connection/baileysLibsignalPatch.js +210 -0
  15. package/app/connection/groupOwnerWriteStateResolver.js +141 -0
  16. package/app/connection/socketController.js +694 -123
  17. package/app/connection/socketController.multiSession.test.js +128 -0
  18. package/app/controllers/messageController.js +1 -1
  19. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  20. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  21. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  22. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
  23. package/app/controllers/messageProcessingPipeline.js +90 -9
  24. package/app/controllers/messageProcessingPipeline.test.js +202 -0
  25. package/app/modules/adminModule/AGENT.md +1 -1
  26. package/app/modules/adminModule/commandConfig.json +3318 -1347
  27. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  28. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  29. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  30. package/app/modules/aiModule/AGENT.md +47 -30
  31. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  32. package/app/modules/aiModule/catCommand.js +132 -25
  33. package/app/modules/aiModule/commandConfig.json +114 -28
  34. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  35. package/app/modules/gameModule/AGENT.md +1 -1
  36. package/app/modules/gameModule/commandConfig.json +29 -0
  37. package/app/modules/menuModule/AGENT.md +1 -1
  38. package/app/modules/menuModule/commandConfig.json +45 -10
  39. package/app/modules/menuModule/menuCatalogService.js +190 -0
  40. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  41. package/app/modules/menuModule/menuDynamicService.js +511 -0
  42. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  43. package/app/modules/menuModule/menus.js +36 -5
  44. package/app/modules/playModule/AGENT.md +10 -5
  45. package/app/modules/playModule/commandConfig.json +74 -16
  46. package/app/modules/playModule/playCommandConstants.js +13 -7
  47. package/app/modules/playModule/playCommandCore.js +4 -6
  48. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  49. package/app/modules/playModule/playConfigRuntime.js +5 -6
  50. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  51. package/app/modules/quoteModule/AGENT.md +1 -1
  52. package/app/modules/quoteModule/commandConfig.json +29 -0
  53. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  54. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  55. package/app/modules/statsModule/AGENT.md +1 -1
  56. package/app/modules/statsModule/commandConfig.json +58 -0
  57. package/app/modules/stickerModule/AGENT.md +1 -1
  58. package/app/modules/stickerModule/commandConfig.json +145 -0
  59. package/app/modules/stickerPackModule/AGENT.md +1 -1
  60. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  61. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  62. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  63. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  64. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  65. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  66. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  67. package/app/modules/tiktokModule/AGENT.md +1 -1
  68. package/app/modules/tiktokModule/commandConfig.json +29 -0
  69. package/app/modules/userModule/AGENT.md +1 -1
  70. package/app/modules/userModule/commandConfig.json +29 -0
  71. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  72. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  73. package/app/observability/metrics.js +136 -0
  74. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  75. package/app/services/ai/geminiService.js +131 -7
  76. package/app/services/ai/geminiService.test.js +59 -2
  77. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  78. package/app/services/group/groupMetadataService.js +24 -1
  79. package/app/services/infra/dbWriteQueue.js +51 -21
  80. package/app/services/messaging/newsBroadcastService.js +843 -27
  81. package/app/services/multiSession/assignmentBalancerService.js +452 -0
  82. package/app/services/multiSession/groupOwnershipRepository.js +346 -0
  83. package/app/services/multiSession/groupOwnershipService.js +809 -0
  84. package/app/services/multiSession/groupOwnershipService.test.js +317 -0
  85. package/app/services/multiSession/sessionRegistryService.js +239 -0
  86. package/app/store/aiPromptStore.js +36 -19
  87. package/app/store/groupConfigStore.js +41 -5
  88. package/app/store/premiumUserStore.js +21 -7
  89. package/app/utils/antiLink/antiLinkModule.js +391 -25
  90. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  91. package/database/index.js +6 -0
  92. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  93. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  94. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  95. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  96. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  97. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  98. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  99. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  100. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  101. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  102. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  103. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  104. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  105. package/database/schema.sql +102 -1
  106. package/docker-compose.yml +4 -1
  107. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  108. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  109. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  110. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  111. package/docs/security/omnizap-static-security-headers.conf +25 -0
  112. package/ecosystem.prod.config.cjs +31 -11
  113. package/index.js +52 -18
  114. package/observability/alert-rules.yml +20 -0
  115. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  116. package/observability/mysql-setup.sql +4 -4
  117. package/observability/system-admin-observability.md +26 -0
  118. package/package.json +14 -6
  119. package/public/comandos/commands-catalog.json +2253 -78
  120. package/public/css/payments-react.css +478 -0
  121. package/public/js/apps/commandsReactApp.js +267 -87
  122. package/public/js/apps/createPackApp.js +3 -3
  123. package/public/js/apps/homeReactApp.js +2 -2
  124. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  125. package/public/js/apps/paymentsReactApp.js +399 -0
  126. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  127. package/public/js/apps/stickersApp.js +255 -103
  128. package/public/js/apps/termsReactApp.js +57 -8
  129. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  130. package/public/js/apps/userReactApp.js +96 -47
  131. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  132. package/public/pages/pagamentos-cancelado.html +21 -0
  133. package/public/pages/pagamentos-sucesso.html +21 -0
  134. package/public/pages/pagamentos.html +30 -0
  135. package/public/pages/politica-de-privacidade.html +1 -1
  136. package/public/pages/stickers.html +5 -5
  137. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  138. package/public/pages/termos-de-uso.html +1 -1
  139. package/public/pages/user-password-reset.html +3 -4
  140. package/public/pages/user-systemadm.html +8 -462
  141. package/public/pages/user.html +1 -1
  142. package/scripts/clear-whatsapp-session.sh +123 -0
  143. package/scripts/core-ai-mode.mjs +163 -0
  144. package/scripts/deploy.sh +13 -0
  145. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  146. package/scripts/generate-commands-catalog.mjs +155 -0
  147. package/scripts/new-whatsapp-session.sh +564 -0
  148. package/scripts/security-web-surface-check.mjs +218 -0
  149. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  150. package/server/controllers/admin/systemAdminController.js +254 -0
  151. package/server/controllers/payments/paymentsController.js +731 -0
  152. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  153. package/server/controllers/system/contactController.js +9 -17
  154. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  155. package/server/controllers/system/systemController.js +228 -1
  156. package/server/controllers/userController.js +6 -0
  157. package/server/email/emailAutomationRuntime.js +36 -1
  158. package/server/email/emailAutomationService.js +42 -1
  159. package/server/email/emailTemplateService.js +140 -33
  160. package/server/http/httpRequestUtils.js +18 -14
  161. package/server/http/httpServer.js +8 -4
  162. package/server/middleware/securityHeaders.js +35 -3
  163. package/server/routes/admin/systemAdminRouter.js +6 -0
  164. package/server/routes/indexRouter.js +50 -6
  165. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  166. package/server/routes/payments/paymentsRouter.js +47 -0
  167. package/server/routes/static/staticPageRouter.js +30 -1
  168. package/server/utils/publicContact.js +31 -0
  169. package/utils/whatsapp/contactEnv.js +39 -0
  170. package/vite.config.mjs +5 -1
  171. package/app/modules/playModule/local/installYtDlp.js +0 -25
  172. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -4,7 +4,7 @@ import 'dotenv/config';
4
4
  import { isAdminCommand } from '../modules/adminModule/groupCommandHandlers.js';
5
5
  import { explicarComandoGlobal, registerGlobalHelpCommandExecution } from '../services/ai/globalModuleAiHelpService.js';
6
6
  import { extractSupportedStickerMediaDetails, processSticker } from '../modules/stickerModule/stickerCommand.js';
7
- import { detectAllMediaTypes, extractMessageContent, getExpiration, getJidServer, isGroupJid, isStatusJid, isSameJidUser, normalizeJid, normalizeWAPresence, resolveBotJid, extractSenderInfoFromMessage, resolveUserId, resolveAddressingModeFromMessageKey, resolveCanonicalWhatsAppJid, parseEnvBool, parseEnvInt } from '../config/index.js';
7
+ import { detectAllMediaTypes, extractMessageContent, getExpiration, getJidServer, isGroupJid, isStatusJid, isSameJidUser, normalizeJid, normalizeWAPresence, resolveBotJid, extractSenderInfoFromMessage, resolveUserId, resolveAddressingModeFromMessageKey, resolveCanonicalWhatsAppJid, parseEnvBool, parseEnvInt, getMultiSessionRuntimeConfig } from '../config/index.js';
8
8
  import { isUserAdmin } from '../config/index.js';
9
9
  import { isAdminSenderAsync } from '../config/index.js';
10
10
  import { executeQuery, TABLES } from '../../database/index.js';
@@ -20,6 +20,7 @@ import { createMessageAnalysisEvent } from '../modules/analyticsModule/messageAn
20
20
  import { routeConversationMessage } from '../services/ai/conversationRouterService.js';
21
21
  import { executeMessageCommandRoute, isKnownNonAdminCommand } from '../services/ai/messageCommandExecutionService.js';
22
22
  import { canSendMessageInStickerFocus, registerMessageUsageInStickerFocus, resolveStickerFocusMessageClassification, resolveStickerFocusState, shouldSendStickerFocusWarning } from '../services/sticker/stickerFocusService.js';
23
+ import { getOwner as getGroupOwner } from '../services/multiSession/groupOwnershipService.js';
23
24
  import { createPreProcessingMiddlewares } from './messagePipeline/preProcessingMiddlewares.js';
24
25
  import { createConversationMiddleware } from './messagePipeline/conversationMiddleware.js';
25
26
  import { createCommandMiddleware } from './messagePipeline/commandMiddleware.js';
@@ -41,13 +42,28 @@ const MESSAGE_REPLY_PRESENCE_BEFORE = normalizeWAPresence(process.env.MESSAGE_RE
41
42
  const MESSAGE_REPLY_PRESENCE_AFTER = normalizeWAPresence(process.env.MESSAGE_REPLY_PRESENCE_AFTER, 'paused');
42
43
  const MESSAGE_REPLY_PRESENCE_DELAY_MS = parseEnvInt(process.env.MESSAGE_REPLY_PRESENCE_DELAY_MS, 280, 0, 3_000);
43
44
  const MESSAGE_REPLY_PRESENCE_SUBSCRIBE = parseEnvBool(process.env.MESSAGE_REPLY_PRESENCE_SUBSCRIBE, true);
45
+ const CONVERSATIONAL_AUTO_REPLY_ENABLED = parseEnvBool(process.env.CONVERSATIONAL_AUTO_REPLY_ENABLED, false);
44
46
  const WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN = parseEnvBool(process.env.WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN, true);
47
+ const WHATSAPP_ALLOW_SELF_COMMANDS_ON_APPEND = parseEnvBool(process.env.WHATSAPP_ALLOW_SELF_COMMANDS_ON_APPEND, true);
45
48
  const SITE_ORIGIN =
46
49
  String(process.env.SITE_ORIGIN || process.env.WHATSAPP_LOGIN_BASE_URL || 'https://omnizap.shop')
47
50
  .trim()
48
51
  .replace(/\/+$/, '') || 'https://omnizap.shop';
49
52
  const SITE_LOGIN_URL = `${SITE_ORIGIN}/login/`;
50
53
  const SITE_GROUP_LOGIN_URL = `${SITE_ORIGIN}/login`;
54
+ const MULTI_SESSION_RUNTIME_CONFIG = getMultiSessionRuntimeConfig();
55
+ const PRIMARY_SESSION_ID = String(MULTI_SESSION_RUNTIME_CONFIG?.primarySessionId || 'default').trim() || 'default';
56
+ const VALID_SESSION_IDS = new Set(Array.isArray(MULTI_SESSION_RUNTIME_CONFIG?.sessionIds) ? MULTI_SESSION_RUNTIME_CONFIG.sessionIds : [PRIMARY_SESSION_ID]);
57
+ const GROUP_OWNER_ENFORCEMENT_MODE = String(MULTI_SESSION_RUNTIME_CONFIG?.ownerEnforcementMode || 'off')
58
+ .trim()
59
+ .toLowerCase();
60
+
61
+ const normalizeSessionId = (sessionId) => {
62
+ const normalized = String(sessionId || '').trim();
63
+ if (!normalized) return PRIMARY_SESSION_ID;
64
+ if (VALID_SESSION_IDS.has(normalized)) return normalized;
65
+ return PRIMARY_SESSION_ID;
66
+ };
51
67
 
52
68
  let messageAnalyticsTableMissingLogged = false;
53
69
  const recentCommandExecutions = new Map();
@@ -170,7 +186,7 @@ const maybeHandleStartLoginMessage = async ({ sock, messageInfo, extractedText,
170
186
 
171
187
  if (isGroupMessage) {
172
188
  await sendReply(sock, remoteJid, messageInfo, expirationMessage, {
173
- text: 'Por seguranca, envie *iniciar* no privado do bot para receber seu link de login.',
189
+ text: '🔐 *Segurança dos seus dados*\n\n' + 'Para proteger sua conta, o acesso é feito apenas no *privado*.\n' + 'Envie *iniciar* no chat privado do bot para receber seu link de login seguro.\n\n' + '⚠️ Por segurança, não enviamos links de acesso em grupos.',
174
190
  });
175
191
  return true;
176
192
  }
@@ -199,7 +215,7 @@ const maybeHandleStartLoginMessage = async ({ sock, messageInfo, extractedText,
199
215
  const safeName = String(senderName || '').trim();
200
216
  const greeting = safeName ? `Oi, *${safeName}*!` : 'Oi!';
201
217
  await sendReply(sock, remoteJid, messageInfo, expirationMessage, {
202
- text: `${greeting}\n\n` + 'Para continuar no OmniZap, faca login com Google neste link:\n' + `${loginUrl}\n\n` + 'Seu numero do WhatsApp sera vinculado automaticamente a conta logada.',
218
+ text: `${greeting}\n\n` + '🚀 *Bem-vindo ao OmniZap!*\n\n' + 'Para continuar, faça login com sua conta *Google* no link abaixo:\n\n' + `${loginUrl}\n\n` + '🔐 Seu número do WhatsApp será vinculado automaticamente à conta após o login.\n' + '⚠️ Use apenas este link e não compartilhe com outras pessoas.',
203
219
  });
204
220
 
205
221
  return true;
@@ -374,7 +390,7 @@ const ensureUserHasGoogleWebLoginForCommand = async ({ sock, messageInfo, sender
374
390
  }
375
391
 
376
392
  const loginUrl = isGroupMessage ? SITE_GROUP_LOGIN_URL : buildSiteLoginUrlForUser(resolvedCanonicalUserId || senderJid);
377
- const loginMessage = isGroupMessage ? `Para usar os comandos do bot, você precisa estar logado no site com sua conta Google.\n\nAcesse:\n${loginUrl}` : `Para usar os comandos do bot, você precisa estar logado no site com sua conta Google.\n\nCadastre-se / faça login em:\n${loginUrl}\n\nDepois volte aqui e envie o comando novamente (ex.: ${commandPrefix}menu).`;
393
+ const loginMessage = isGroupMessage ? '🔐 *Login necessário*\n\n' + 'Para usar os comandos do bot, você precisa estar logado com sua conta *Google*.\n\n' + 'Acesse o link abaixo para entrar:\n' + `${loginUrl}\n\n` + '⚠️ Por segurança, recomendamos abrir o link no privado.' : '🔐 *Login necessário*\n\n' + 'Para usar os comandos do bot, faça login com sua conta *Google* no link abaixo:\n\n' + `${loginUrl}\n\n` + '✅ Após entrar, volte aqui e envie o comando novamente\n' + `(ex.: *${commandPrefix}menu*).`;
378
394
 
379
395
  await sendReply(sock, remoteJid, messageInfo, expirationMessage, {
380
396
  text: loginMessage,
@@ -459,6 +475,43 @@ const resolveSenderOwnerForContext = async (ctx) => {
459
475
  return ctx.memo.senderOwnerPromise;
460
476
  };
461
477
 
478
+ const resolveGroupOwnerForContext = async (ctx, { bypassCache = false } = {}) => {
479
+ if (!ctx.isGroupMessage) return null;
480
+
481
+ if (!bypassCache && ctx.memo.groupOwnerPromise) {
482
+ return ctx.memo.groupOwnerPromise;
483
+ }
484
+
485
+ const fetchOwnerPromise = (async () => {
486
+ try {
487
+ const ownerState = await getGroupOwner(ctx.remoteJid, { bypassCache });
488
+ const ownerSessionId = String(ownerState?.ownerSessionId || '').trim() || null;
489
+ ctx.ownerSessionId = ownerSessionId;
490
+ mergeAnalysisMetadata(ctx.analysisPayload, {
491
+ owner_session_id: ownerSessionId,
492
+ });
493
+ return ownerState;
494
+ } catch (error) {
495
+ logger.warn('Falha ao resolver owner de grupo para roteamento de sessão.', {
496
+ action: 'group_owner_resolution_failed',
497
+ sessionId: ctx.sessionId,
498
+ groupId: ctx.remoteJid,
499
+ error: error?.message,
500
+ });
501
+ ctx.ownerSessionId = null;
502
+ mergeAnalysisMetadata(ctx.analysisPayload, {
503
+ owner_resolution_error: String(error?.code || error?.name || 'lookup_failed')
504
+ .trim()
505
+ .toLowerCase(),
506
+ });
507
+ return null;
508
+ }
509
+ })();
510
+
511
+ ctx.memo.groupOwnerPromise = fetchOwnerPromise;
512
+ return fetchOwnerPromise;
513
+ };
514
+
462
515
  const resolveHasGoogleLoginForContext = async (ctx) => {
463
516
  if (ctx.isMessageFromBot || !WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN) {
464
517
  return undefined;
@@ -476,10 +529,11 @@ const resolveHasGoogleLoginForContext = async (ctx) => {
476
529
  return ctx.memo.hasGoogleLoginPromise;
477
530
  };
478
531
 
479
- const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyUpsert, sock }) => {
532
+ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyUpsert, sock, sessionId }) => {
480
533
  const key = messageInfo?.key || {};
481
534
  const remoteJid = key?.remoteJid;
482
535
  if (!remoteJid) return null;
536
+ const normalizedSessionId = normalizeSessionId(sessionId);
483
537
 
484
538
  const isGroupMessage = isGroupJid(remoteJid);
485
539
  const extractedText = extractMessageContent(messageInfo);
@@ -505,6 +559,7 @@ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyU
505
559
  .slice(0, 10);
506
560
 
507
561
  const analysisPayload = {
562
+ sessionId: normalizedSessionId,
508
563
  messageId: key?.id || null,
509
564
  chatId: remoteJid || null,
510
565
  senderId: senderJid || null,
@@ -530,6 +585,8 @@ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyU
530
585
  upsert_type: upsertType,
531
586
  is_notify_upsert: isNotifyUpsert,
532
587
  is_history_append: upsertType === 'append',
588
+ session_id: normalizedSessionId,
589
+ owner_session_id: null,
533
590
  addressing_mode: addressingMode || null,
534
591
  participant_alt: key?.participantAlt || null,
535
592
  remote_jid_alt: key?.remoteJidAlt || null,
@@ -550,6 +607,8 @@ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyU
550
607
  botJidCandidates,
551
608
  botJid,
552
609
  isMessageFromBot,
610
+ sessionId: normalizedSessionId,
611
+ ownerSessionId: null,
553
612
  commandPrefix: DEFAULT_COMMAND_PREFIX,
554
613
  groupConfig: null,
555
614
  groupConfigLoaded: false,
@@ -564,7 +623,7 @@ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyU
564
623
  };
565
624
  };
566
625
 
567
- const { touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, applyGroupPolicyMiddleware, resolveCaptchaMiddleware, handleStartLoginTriggerMiddleware, detectCommandIntentMiddleware, applyStickerFocusMiddleware } = createPreProcessingMiddlewares({
626
+ const { touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, enforceGroupOwnerMiddleware, applyGroupPolicyMiddleware, resolveCaptchaMiddleware, handleStartLoginTriggerMiddleware, detectCommandIntentMiddleware, applyStickerFocusMiddleware } = createPreProcessingMiddlewares({
568
627
  executeQuery,
569
628
  TABLES,
570
629
  isStatusJid,
@@ -577,6 +636,10 @@ const { touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, app
577
636
  ensureGroupConfigForContext,
578
637
  resolveStickerFocusState,
579
638
  resolveStickerFocusMessageClassification,
639
+ resolveGroupOwnerForContext,
640
+ ownerEnforcementMode: GROUP_OWNER_ENFORCEMENT_MODE,
641
+ primarySessionId: PRIMARY_SESSION_ID,
642
+ allowSelfCommandsOnAppend: WHATSAPP_ALLOW_SELF_COMMANDS_ON_APPEND,
580
643
  resolveSenderAdminForContext,
581
644
  isUserAdmin,
582
645
  canSendMessageInStickerFocus,
@@ -605,6 +668,7 @@ const routeConversationMiddleware = createConversationMiddleware({
605
668
  sendReply,
606
669
  routeConversationMessage,
607
670
  stopMessagePipeline,
671
+ conversationAutoReplyEnabled: CONVERSATIONAL_AUTO_REPLY_ENABLED,
608
672
  });
609
673
 
610
674
  const executeCommandMiddleware = createCommandMiddleware({
@@ -642,7 +706,7 @@ const runPostProcessingMiddleware = createPostProcessingMiddleware({
642
706
  normalizeAnalysisErrorCode,
643
707
  });
644
708
 
645
- const MESSAGE_PIPELINE_MIDDLEWARES = [touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, applyGroupPolicyMiddleware, resolveCaptchaMiddleware, handleStartLoginTriggerMiddleware, detectCommandIntentMiddleware, applyStickerFocusMiddleware, routeConversationMiddleware, executeCommandMiddleware, runPostProcessingMiddleware];
709
+ const MESSAGE_PIPELINE_MIDDLEWARES = [touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, enforceGroupOwnerMiddleware, applyGroupPolicyMiddleware, resolveCaptchaMiddleware, handleStartLoginTriggerMiddleware, detectCommandIntentMiddleware, applyStickerFocusMiddleware, routeConversationMiddleware, executeCommandMiddleware, runPostProcessingMiddleware];
646
710
 
647
711
  const runMessagePipeline = async (ctx) => {
648
712
  for (const middleware of MESSAGE_PIPELINE_MIDDLEWARES) {
@@ -657,7 +721,8 @@ const runMessagePipeline = async (ctx) => {
657
721
  *
658
722
  * @param {Object} update - Objeto contendo a atualização do WhatsApp.
659
723
  */
660
- export const handleMessagesThroughPipeline = async (update, sock) => {
724
+ export const handleMessagesThroughPipeline = async (update, sock, options = {}) => {
725
+ const sessionId = normalizeSessionId(options?.sessionId);
661
726
  if (update.messages && Array.isArray(update.messages)) {
662
727
  try {
663
728
  const upsertType = update?.type || null;
@@ -669,6 +734,7 @@ export const handleMessagesThroughPipeline = async (update, sock) => {
669
734
  upsertType,
670
735
  isNotifyUpsert,
671
736
  sock,
737
+ sessionId,
672
738
  });
673
739
  if (!context) continue;
674
740
 
@@ -678,19 +744,34 @@ export const handleMessagesThroughPipeline = async (update, sock) => {
678
744
  context.analysisPayload.processingResult = 'error';
679
745
  context.analysisPayload.errorCode = normalizeAnalysisErrorCode(messageError);
680
746
  logger.error('Erro ao processar mensagem individual:', {
747
+ sessionId,
748
+ ownerSessionId: context.ownerSessionId || null,
681
749
  error: messageError?.message,
682
750
  messageId: context.key?.id || null,
683
751
  remoteJid: context.remoteJid,
684
752
  });
685
753
  } finally {
754
+ logger.debug('Mensagem processada pelo pipeline.', {
755
+ action: 'message_pipeline_processed',
756
+ sessionId: context.sessionId,
757
+ ownerSessionId: context.ownerSessionId || null,
758
+ messageId: context.key?.id || null,
759
+ remoteJid: context.remoteJid,
760
+ result: context.analysisPayload.processingResult,
761
+ isCommand: context.analysisPayload.isCommand,
762
+ });
686
763
  persistMessageAnalysisEvent(context.analysisPayload);
687
764
  }
688
765
  }
689
766
  } catch (error) {
690
- logger.error('Erro ao processar mensagens:', error?.message);
767
+ logger.error('Erro ao processar mensagens:', {
768
+ sessionId,
769
+ error: error?.message,
770
+ });
691
771
  }
692
772
  } else {
693
773
  logger.info('🔄 Processando evento recebido:', {
774
+ sessionId,
694
775
  eventType: update?.type || 'unknown',
695
776
  eventData: update,
696
777
  });
@@ -0,0 +1,202 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { createPreProcessingMiddlewares } from './messagePipeline/preProcessingMiddlewares.js';
5
+ import { createCommandMiddleware } from './messagePipeline/commandMiddleware.js';
6
+
7
+ const createContext = (overrides = {}) => ({
8
+ sock: {},
9
+ messageInfo: { key: { id: 'msg-1' }, message: { conversation: '/menu' } },
10
+ key: { id: 'msg-1' },
11
+ remoteJid: '120363111111111111@g.us',
12
+ isGroupMessage: true,
13
+ extractedText: '/menu',
14
+ senderJid: '5511999999999@s.whatsapp.net',
15
+ senderIdentity: '5511999999999@s.whatsapp.net',
16
+ senderName: 'Tester',
17
+ expirationMessage: 0,
18
+ botJid: '5511888888888@s.whatsapp.net',
19
+ isMessageFromBot: false,
20
+ commandPrefix: '/',
21
+ mediaEntries: [],
22
+ upsertType: 'notify',
23
+ isNotifyUpsert: true,
24
+ sessionId: 'session-a',
25
+ ownerSessionId: null,
26
+ isCommandMessage: false,
27
+ hasCommandPrefix: false,
28
+ pipelineStopped: false,
29
+ analysisPayload: {
30
+ processingResult: 'processed',
31
+ errorCode: null,
32
+ metadata: {},
33
+ isCommand: false,
34
+ commandPrefix: '/',
35
+ commandName: null,
36
+ commandArgsCount: 0,
37
+ commandKnown: null,
38
+ },
39
+ ...overrides,
40
+ });
41
+
42
+ const createStopMessagePipeline =
43
+ () =>
44
+ (ctx, processingResult = '', metadataPatch = null) => {
45
+ if (processingResult) {
46
+ ctx.analysisPayload.processingResult = processingResult;
47
+ }
48
+ if (metadataPatch) {
49
+ ctx.analysisPayload.metadata = {
50
+ ...(ctx.analysisPayload.metadata || {}),
51
+ ...(metadataPatch || {}),
52
+ };
53
+ }
54
+ ctx.pipelineStopped = true;
55
+ return { stop: true };
56
+ };
57
+
58
+ const mergeAnalysisMetadata = (analysisPayload, patch) => {
59
+ analysisPayload.metadata = {
60
+ ...(analysisPayload.metadata || {}),
61
+ ...(patch || {}),
62
+ };
63
+ };
64
+
65
+ const createSharedCommandDedupe = () => {
66
+ const cache = new Map();
67
+ return {
68
+ isDuplicateCommandExecution: (chatId, messageId) => cache.has(`${chatId}:${messageId}`),
69
+ markCommandExecution: (chatId, messageId) => {
70
+ cache.set(`${chatId}:${messageId}`, true);
71
+ },
72
+ };
73
+ };
74
+
75
+ const createCommandMiddlewareForTest = ({ executeRouteSpy, dedupe }) =>
76
+ createCommandMiddleware({
77
+ isAdminCommand: () => false,
78
+ isKnownNonAdminCommand: () => true,
79
+ isDuplicateCommandExecution: dedupe.isDuplicateCommandExecution,
80
+ markCommandExecution: dedupe.markCommandExecution,
81
+ MESSAGE_COMMAND_DEDUPE_TTL_MS: 120_000,
82
+ stopMessagePipeline: createStopMessagePipeline(),
83
+ WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN: false,
84
+ resolveCanonicalSenderJidForContext: async () => '5511999999999@s.whatsapp.net',
85
+ ensureUserHasGoogleWebLoginForCommand: async () => ({ allowed: true }),
86
+ SITE_LOGIN_URL: 'https://omnizap.shop/login',
87
+ COMMAND_REACT_EMOJI: '',
88
+ sendAndStore: async () => {},
89
+ executeMessageCommandRoute: async (payload) => {
90
+ executeRouteSpy.push(payload);
91
+ return {
92
+ commandRoute: payload.command || 'menu',
93
+ commandResult: { ok: true },
94
+ };
95
+ },
96
+ runCommand: async (_label, handler) => {
97
+ try {
98
+ await handler();
99
+ return { ok: true };
100
+ } catch (error) {
101
+ return { ok: false, error };
102
+ }
103
+ },
104
+ sendReply: async () => {},
105
+ registerGlobalHelpCommandExecution: async () => {},
106
+ logger: { warn: () => {} },
107
+ normalizeAnalysisErrorCode: () => 'processing_error',
108
+ resolveSenderAdminForContext: async () => false,
109
+ isUserAdmin: async () => false,
110
+ buildCommandErrorHelpText: async () => '',
111
+ mergeAnalysisMetadata,
112
+ });
113
+
114
+ test('messageProcessingPipeline: com owner enforcement ativo, apenas owner executa comando no grupo', async () => {
115
+ const pre = createPreProcessingMiddlewares({
116
+ executeQuery: async () => [],
117
+ TABLES: { RPG_PLAYER: 'rpg_player' },
118
+ isStatusJid: () => false,
119
+ stopMessagePipeline: createStopMessagePipeline(),
120
+ handleAntiLink: async () => false,
121
+ ensureCommandPrefixForContext: async () => '/',
122
+ resolveCaptchaByMessage: async () => {},
123
+ maybeHandleStartLoginMessage: async () => false,
124
+ mergeAnalysisMetadata,
125
+ ensureGroupConfigForContext: async () => ({}),
126
+ resolveStickerFocusState: () => ({ enabled: false }),
127
+ resolveStickerFocusMessageClassification: () => ({ isThrottleCandidate: false }),
128
+ resolveGroupOwnerForContext: async (ctx) => {
129
+ ctx.ownerSessionId = 'session-a';
130
+ return { ownerSessionId: 'session-a', assignmentVersion: 7 };
131
+ },
132
+ ownerEnforcementMode: 'enforce',
133
+ primarySessionId: 'session-a',
134
+ resolveSenderAdminForContext: async () => false,
135
+ isUserAdmin: async () => false,
136
+ canSendMessageInStickerFocus: () => ({ allowed: true, remainingMs: 0 }),
137
+ registerMessageUsageInStickerFocus: () => {},
138
+ shouldSendStickerFocusWarning: () => false,
139
+ sendReply: async () => {},
140
+ formatStickerFocusRuleLabel: () => '',
141
+ formatRemainingMinutesLabel: () => 1,
142
+ logger: { warn: () => {}, info: () => {} },
143
+ });
144
+
145
+ const executeRouteSpy = [];
146
+ const dedupe = createSharedCommandDedupe();
147
+ const commandMiddleware = createCommandMiddlewareForTest({ executeRouteSpy, dedupe });
148
+
149
+ const ownerCtx = createContext({
150
+ sessionId: 'session-a',
151
+ key: { id: 'msg-shared' },
152
+ messageInfo: { key: { id: 'msg-shared' }, message: { conversation: '/menu' } },
153
+ });
154
+ const nonOwnerCtx = createContext({
155
+ sessionId: 'session-b',
156
+ key: { id: 'msg-shared' },
157
+ messageInfo: { key: { id: 'msg-shared' }, message: { conversation: '/menu' } },
158
+ });
159
+
160
+ await pre.enforceGroupOwnerMiddleware(ownerCtx);
161
+ await pre.detectCommandIntentMiddleware(ownerCtx);
162
+ await commandMiddleware(ownerCtx);
163
+
164
+ const blockedResult = await pre.enforceGroupOwnerMiddleware(nonOwnerCtx);
165
+ assert.deepEqual(blockedResult, { stop: true });
166
+ await pre.detectCommandIntentMiddleware(nonOwnerCtx);
167
+ if (!nonOwnerCtx.pipelineStopped) {
168
+ await commandMiddleware(nonOwnerCtx);
169
+ }
170
+
171
+ assert.equal(executeRouteSpy.length, 1);
172
+ assert.equal(ownerCtx.analysisPayload.processingResult, 'command_executed');
173
+ assert.equal(nonOwnerCtx.analysisPayload.processingResult, 'blocked_group_owner_enforcement');
174
+ });
175
+
176
+ test('messageProcessingPipeline: dedupe de comando por chat+message impede execução dupla em duas sessões', async () => {
177
+ const executeRouteSpy = [];
178
+ const dedupe = createSharedCommandDedupe();
179
+ const commandMiddleware = createCommandMiddlewareForTest({ executeRouteSpy, dedupe });
180
+
181
+ const firstCtx = createContext({
182
+ sessionId: 'session-a',
183
+ isCommandMessage: true,
184
+ hasCommandPrefix: true,
185
+ key: { id: 'dup-msg-1' },
186
+ messageInfo: { key: { id: 'dup-msg-1' }, message: { conversation: '/menu' } },
187
+ });
188
+ const secondCtx = createContext({
189
+ sessionId: 'session-b',
190
+ isCommandMessage: true,
191
+ hasCommandPrefix: true,
192
+ key: { id: 'dup-msg-1' },
193
+ messageInfo: { key: { id: 'dup-msg-1' }, message: { conversation: '/menu' } },
194
+ });
195
+
196
+ await commandMiddleware(firstCtx);
197
+ await commandMiddleware(secondCtx);
198
+
199
+ assert.equal(executeRouteSpy.length, 1);
200
+ assert.equal(firstCtx.analysisPayload.processingResult, 'command_executed');
201
+ assert.equal(secondCtx.analysisPayload.processingResult, 'duplicate_command_ignored');
202
+ });
@@ -7,7 +7,7 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
7
7
  - arquivo_base: `app/modules/adminModule/commandConfig.json`
8
8
  - schema_version: `2.0.0`
9
9
  - module_enabled: `true`
10
- - generated_at: `2026-03-11T02:35:17.177Z`
10
+ - generated_at: `2026-03-17T04:04:14.195Z`
11
11
 
12
12
  ## Escopo do Modulo
13
13