@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.
Files changed (261) hide show
  1. package/.env.example +58 -13
  2. package/.github/workflows/ci.yml +5 -5
  3. package/.github/workflows/codeql.yml +1 -1
  4. package/.github/workflows/db-migration-check.yml +2 -2
  5. package/.github/workflows/dependency-review.yml +1 -1
  6. package/.github/workflows/deploy.yml +2 -2
  7. package/.github/workflows/release.yml +2 -2
  8. package/.github/workflows/security-attest-provenance.yml +2 -2
  9. package/.github/workflows/security-gitleaks.yml +13 -4
  10. package/.github/workflows/security-runner-hardening.yml +2 -2
  11. package/.github/workflows/security-scorecard.yml +1 -1
  12. package/.github/workflows/security-zap-baseline.yml +1 -1
  13. package/.github/workflows/security-zap-full-scan.yml +2 -1
  14. package/.github/workflows/security-zizmor.yml +1 -1
  15. package/.github/workflows/wiki-sync.yml +1 -1
  16. package/.gitleaksignore +9 -0
  17. package/CODE_OF_CONDUCT.md +2 -2
  18. package/GEMINI.md +64 -0
  19. package/README.md +52 -82
  20. package/SECURITY.md +1 -1
  21. package/app/config/index.js +2 -0
  22. package/app/configParts/adminIdentity.js +5 -5
  23. package/app/configParts/baileysConfig.js +230 -58
  24. package/app/configParts/groupUtils.js +5 -0
  25. package/app/configParts/messagePersistenceService.js +145 -4
  26. package/app/configParts/sessionConfig.js +157 -0
  27. package/app/connection/baileysCompatibility.test.js +1 -1
  28. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  29. package/app/connection/socketController.js +660 -158
  30. package/app/connection/socketController.multiSession.test.js +108 -0
  31. package/app/controllers/messageController.js +1 -1
  32. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  33. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  34. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  35. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  36. package/app/controllers/messageProcessingPipeline.js +93 -13
  37. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  38. package/app/modules/adminModule/AGENT.md +1 -1
  39. package/app/modules/adminModule/commandConfig.json +3318 -1347
  40. package/app/modules/adminModule/groupCommandHandlers.js +858 -15
  41. package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
  42. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  43. package/app/modules/aiModule/AGENT.md +47 -30
  44. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  45. package/app/modules/aiModule/catCommand.js +135 -27
  46. package/app/modules/aiModule/commandConfig.json +114 -28
  47. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  48. package/app/modules/gameModule/AGENT.md +1 -1
  49. package/app/modules/gameModule/commandConfig.json +29 -0
  50. package/app/modules/menuModule/AGENT.md +1 -1
  51. package/app/modules/menuModule/commandConfig.json +45 -10
  52. package/app/modules/menuModule/menuCatalogService.js +190 -0
  53. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  54. package/app/modules/menuModule/menuDynamicService.js +511 -0
  55. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  56. package/app/modules/menuModule/menus.js +36 -5
  57. package/app/modules/playModule/AGENT.md +10 -5
  58. package/app/modules/playModule/commandConfig.json +140 -12
  59. package/app/modules/playModule/playCommand.js +1 -1417
  60. package/app/modules/playModule/playCommandConstants.js +80 -0
  61. package/app/modules/playModule/playCommandCore.js +361 -0
  62. package/app/modules/playModule/playCommandHandlers.js +41 -0
  63. package/app/modules/playModule/playCommandMediaClient.js +1872 -0
  64. package/app/modules/playModule/playConfigRuntime.js +245 -4
  65. package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
  66. package/app/modules/quoteModule/AGENT.md +1 -1
  67. package/app/modules/quoteModule/commandConfig.json +29 -0
  68. package/app/modules/quoteModule/quoteCommand.js +3 -2
  69. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  70. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  71. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
  72. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
  73. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
  74. package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
  75. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
  76. package/app/modules/statsModule/AGENT.md +1 -1
  77. package/app/modules/statsModule/commandConfig.json +58 -0
  78. package/app/modules/statsModule/rankingCommon.js +5 -4
  79. package/app/modules/stickerModule/AGENT.md +1 -1
  80. package/app/modules/stickerModule/addStickerMetadata.js +4 -3
  81. package/app/modules/stickerModule/commandConfig.json +145 -0
  82. package/app/modules/stickerModule/stickerCommand.js +1 -1
  83. package/app/modules/stickerPackModule/AGENT.md +1 -1
  84. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  85. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  86. package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
  87. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
  88. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
  89. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
  90. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
  91. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
  92. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
  93. package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
  94. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
  95. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  96. package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
  97. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
  98. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  99. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  100. package/app/modules/systemMetricsModule/pingCommand.js +6 -5
  101. package/app/modules/tiktokModule/AGENT.md +1 -1
  102. package/app/modules/tiktokModule/commandConfig.json +29 -0
  103. package/app/modules/tiktokModule/tiktokCommand.js +2 -1
  104. package/app/modules/userModule/AGENT.md +1 -1
  105. package/app/modules/userModule/commandConfig.json +29 -0
  106. package/app/modules/userModule/userCommand.js +72 -23
  107. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  108. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  109. package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
  110. package/app/observability/metrics.js +136 -0
  111. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  112. package/app/services/ai/conversationRouterService.js +4 -3
  113. package/app/services/ai/geminiService.js +132 -7
  114. package/app/services/ai/geminiService.test.js +59 -2
  115. package/app/services/ai/globalModuleAiHelpService.js +3 -2
  116. package/app/services/ai/messageCommandExecutionService.js +2 -1
  117. package/app/services/ai/moduleAiHelpCoreService.js +45 -14
  118. package/app/services/ai/moduleToolExecutorService.js +3 -2
  119. package/app/services/ai/moduleToolRegistryService.js +2 -1
  120. package/app/services/ai/toolCandidateSelectorService.js +6 -5
  121. package/app/services/auth/googleWebLinkService.js +3 -2
  122. package/app/services/auth/whatsappLoginLinkService.js +3 -2
  123. package/app/services/external/pokeApiService.js +4 -3
  124. package/app/services/group/groupMetadataService.js +24 -1
  125. package/app/services/infra/dbWriteQueue.js +57 -26
  126. package/app/services/infra/featureFlagService.js +2 -1
  127. package/app/services/messaging/captchaService.js +3 -2
  128. package/app/services/messaging/newsBroadcastService.js +846 -29
  129. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  130. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  131. package/app/services/multiSession/groupOwnershipService.js +890 -0
  132. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  133. package/app/services/multiSession/sessionRegistryService.js +293 -0
  134. package/app/services/sticker/stickerFocusService.js +11 -10
  135. package/app/store/aiPromptStore.js +36 -19
  136. package/app/store/conversationSessionStore.js +7 -6
  137. package/app/store/groupConfigStore.js +41 -5
  138. package/app/store/premiumUserStore.js +21 -7
  139. package/app/utils/antiLink/antiLinkModule.js +352 -16
  140. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  141. package/app/workers/aiLearningWorker.js +6 -5
  142. package/app/workers/commandConfigEnrichmentWorker.js +4 -3
  143. package/database/index.js +14 -8
  144. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  145. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  146. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  147. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  148. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  149. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  150. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  151. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  152. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  153. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  154. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  155. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  156. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  157. package/database/schema.sql +102 -1
  158. package/docker-compose.yml +4 -1
  159. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  160. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
  161. package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
  162. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  163. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
  164. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  165. package/docs/security/omnizap-static-security-headers.conf +25 -0
  166. package/docs/wiki/Home.md +1 -1
  167. package/ecosystem.prod.config.cjs +32 -12
  168. package/index.js +57 -23
  169. package/observability/alert-rules.yml +20 -0
  170. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  171. package/observability/mysql-setup.sql +4 -4
  172. package/observability/system-admin-observability.md +26 -0
  173. package/package.json +20 -6
  174. package/public/apple-touch-icon.png +0 -0
  175. package/public/comandos/commands-catalog.json +2853 -3326
  176. package/public/favicon-16x16.png +0 -0
  177. package/public/favicon-32x32.png +0 -0
  178. package/public/favicon.ico +0 -0
  179. package/public/js/apps/apiDocsApp.js +3 -2
  180. package/public/js/apps/commandsReactApp.js +280 -99
  181. package/public/js/apps/createPackApp.js +11 -10
  182. package/public/js/apps/homeReactApp.js +181 -130
  183. package/public/js/apps/loginReactApp.js +1 -1
  184. package/public/js/apps/stickersApp.js +263 -110
  185. package/public/js/apps/termsReactApp.js +73 -24
  186. package/public/js/apps/userApp.js +4 -3
  187. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  188. package/public/js/apps/userReactApp.js +355 -280
  189. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  190. package/public/pages/api-docs.html +1 -1
  191. package/public/pages/aup.html +2 -2
  192. package/public/pages/dpa.html +3 -3
  193. package/public/pages/licenca.html +4 -4
  194. package/public/pages/login.html +1 -1
  195. package/public/pages/notice-and-takedown.html +2 -2
  196. package/public/pages/politica-de-privacidade.html +6 -6
  197. package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
  198. package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
  199. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
  200. package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
  201. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
  202. package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
  203. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
  204. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
  205. package/public/pages/stickers-admin.html +1 -1
  206. package/public/pages/stickers-create.html +1 -1
  207. package/public/pages/stickers.html +6 -6
  208. package/public/pages/suboperadores.html +2 -2
  209. package/public/pages/termos-de-uso-texto-integral.html +6 -6
  210. package/public/pages/termos-de-uso.html +3 -3
  211. package/public/pages/user-password-reset.html +4 -5
  212. package/public/pages/user-systemadm.html +9 -463
  213. package/public/pages/user.html +2 -2
  214. package/scripts/clear-whatsapp-session.sh +123 -0
  215. package/scripts/core-ai-mode.mjs +163 -0
  216. package/scripts/deploy.sh +11 -1
  217. package/scripts/email-broadcast-terms-update.mjs +2 -1
  218. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  219. package/scripts/generate-commands-catalog.mjs +166 -2
  220. package/scripts/generate-module-agents.mjs +2 -1
  221. package/scripts/generate-seo-satellite-pages.mjs +5 -4
  222. package/scripts/github-deploy-notify.mjs +2 -1
  223. package/scripts/github-release-notify.mjs +25 -10
  224. package/scripts/new-whatsapp-session.sh +317 -0
  225. package/scripts/release.sh +2 -19
  226. package/scripts/security-smoketest.mjs +6 -5
  227. package/scripts/security-web-surface-check.mjs +218 -0
  228. package/scripts/sticker-catalog-loadtest.mjs +5 -4
  229. package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
  230. package/server/auth/jwt/webJwtService.js +1 -1
  231. package/server/auth/stickerCatalogAuthContext.js +2 -1
  232. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
  233. package/server/auth/userPassword/userPasswordAuthService.js +2 -1
  234. package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
  235. package/server/auth/webAccount/webAccountHandlers.js +9 -10
  236. package/server/controllers/admin/adminPanelHandlers.js +267 -16
  237. package/server/controllers/admin/systemAdminController.js +267 -0
  238. package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
  239. package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
  240. package/server/controllers/sticker/stickerCatalogController.js +23 -36
  241. package/server/controllers/system/contactController.js +9 -17
  242. package/server/controllers/system/githubController.js +3 -2
  243. package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
  244. package/server/controllers/system/systemController.js +254 -1
  245. package/server/controllers/system/systemMetricsController.js +2 -1
  246. package/server/controllers/userController.js +6 -0
  247. package/server/email/emailTemplateService.js +5 -3
  248. package/server/http/httpServer.js +11 -6
  249. package/server/middleware/rateLimit.js +2 -1
  250. package/server/middleware/securityHeaders.js +20 -1
  251. package/server/routes/admin/systemAdminRouter.js +6 -0
  252. package/server/routes/indexRouter.js +30 -6
  253. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  254. package/server/routes/static/staticPageRouter.js +27 -1
  255. package/server/utils/publicContact.js +31 -0
  256. package/utils/time/timeModule.js +135 -0
  257. package/utils/time/timeModule.test.js +65 -0
  258. package/utils/whatsapp/contactEnv.js +39 -0
  259. package/vite.config.mjs +7 -1
  260. package/public/assets/images/brand-icon-192.png +0 -0
  261. package/scripts/sync-readme-snapshot.mjs +0 -133
@@ -0,0 +1,108 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { createGroupOwnerWriteStateResolver } from './groupOwnerWriteStateResolver.js';
5
+
6
+ const createCache = () => {
7
+ const map = new Map();
8
+ return {
9
+ get: (key) => map.get(key),
10
+ set: (key, value) => {
11
+ map.set(key, value);
12
+ return true;
13
+ },
14
+ del: (key) => map.delete(key),
15
+ keys: () => Array.from(map.keys()),
16
+ };
17
+ };
18
+
19
+ const buildCacheKey = (groupJid, sessionId) => `${sessionId}:${groupJid}`;
20
+
21
+ const normalizeSessionId = (value) => String(value || '').trim() || 'default';
22
+ const isGroupJid = (jid) => String(jid || '').endsWith('@g.us');
23
+
24
+ test('socketController multi-session: fencing token por assignment_version invalida writer stale', async () => {
25
+ let ownerState = {
26
+ ownerSessionId: 'session-a',
27
+ assignmentVersion: 1,
28
+ };
29
+
30
+ const resolver = createGroupOwnerWriteStateResolver({
31
+ buildCacheKeyImpl: buildCacheKey,
32
+ getOwnerImpl: async () => ownerState,
33
+ tryAcquireImpl: async () => ({ acquired: false, reason: 'claim_disabled' }),
34
+ cacheImpl: createCache(),
35
+ isGroupJidImpl: isGroupJid,
36
+ normalizeSessionIdImpl: normalizeSessionId,
37
+ loggerImpl: { warn: () => {} },
38
+ });
39
+
40
+ const first = await resolver('120363555555555555@g.us', 'session-a', {
41
+ allowClaim: false,
42
+ source: 'test_first',
43
+ });
44
+ assert.equal(first.allowed, true);
45
+ assert.equal(first.assignmentVersion, 1);
46
+
47
+ ownerState = {
48
+ ownerSessionId: 'session-a',
49
+ assignmentVersion: 2,
50
+ };
51
+
52
+ const stale = await resolver('120363555555555555@g.us', 'session-a', {
53
+ allowClaim: false,
54
+ source: 'test_stale',
55
+ expectedAssignmentVersion: 1,
56
+ enforceFence: true,
57
+ });
58
+ assert.equal(stale.allowed, false);
59
+ assert.equal(stale.reason, 'fence_token_mismatch');
60
+ assert.equal(stale.assignmentVersion, 2);
61
+ });
62
+
63
+ test('socketController multi-session: sessão antiga perde escrita após failover de owner', async () => {
64
+ let ownerState = {
65
+ ownerSessionId: 'session-a',
66
+ assignmentVersion: 7,
67
+ };
68
+
69
+ const resolver = createGroupOwnerWriteStateResolver({
70
+ buildCacheKeyImpl: buildCacheKey,
71
+ getOwnerImpl: async () => ownerState,
72
+ tryAcquireImpl: async () => ({ acquired: false, reason: 'claim_disabled' }),
73
+ cacheImpl: createCache(),
74
+ isGroupJidImpl: isGroupJid,
75
+ normalizeSessionIdImpl: normalizeSessionId,
76
+ loggerImpl: { warn: () => {} },
77
+ });
78
+
79
+ const beforeFailover = await resolver('120363666666666666@g.us', 'session-a', {
80
+ allowClaim: false,
81
+ source: 'before_failover',
82
+ });
83
+ assert.equal(beforeFailover.allowed, true);
84
+ assert.equal(beforeFailover.assignmentVersion, 7);
85
+
86
+ ownerState = {
87
+ ownerSessionId: 'session-b',
88
+ assignmentVersion: 8,
89
+ };
90
+
91
+ const staleOwner = await resolver('120363666666666666@g.us', 'session-a', {
92
+ allowClaim: false,
93
+ source: 'after_failover_old_owner',
94
+ expectedAssignmentVersion: 7,
95
+ enforceFence: true,
96
+ });
97
+ assert.equal(staleOwner.allowed, false);
98
+ assert.equal(staleOwner.reason, 'owned_by_other');
99
+
100
+ const newOwner = await resolver('120363666666666666@g.us', 'session-b', {
101
+ allowClaim: false,
102
+ source: 'after_failover_new_owner',
103
+ expectedAssignmentVersion: 8,
104
+ enforceFence: true,
105
+ });
106
+ assert.equal(newOwner.allowed, true);
107
+ assert.equal(newOwner.assignmentVersion, 8);
108
+ });
@@ -4,4 +4,4 @@ import { handleMessagesThroughPipeline } from './messageProcessingPipeline.js';
4
4
  * Facade do controller de mensagens.
5
5
  * Mantem assinatura/compatibilidade enquanto delega ao pipeline modular.
6
6
  */
7
- export const handleMessages = async (update, sock) => handleMessagesThroughPipeline(update, sock);
7
+ export const handleMessages = async (update, sock, options = {}) => handleMessagesThroughPipeline(update, sock, options);
@@ -42,17 +42,19 @@ export const createCommandMiddleware = ({ isAdminCommand, isKnownNonAdminCommand
42
42
  }
43
43
  }
44
44
 
45
- if (COMMAND_REACT_EMOJI) {
46
- try {
47
- await sendAndStore(ctx.sock, ctx.remoteJid, {
48
- react: {
49
- text: COMMAND_REACT_EMOJI,
50
- key: ctx.key,
51
- },
45
+ if (COMMAND_REACT_EMOJI?.trim() && ctx?.key) {
46
+ sendAndStore(ctx.sock, ctx.remoteJid, {
47
+ react: {
48
+ text: COMMAND_REACT_EMOJI,
49
+ key: ctx.key,
50
+ },
51
+ }).catch((error) => {
52
+ logger.warn('Falha ao enviar reação de comando', {
53
+ error: error?.message,
54
+ jid: ctx.remoteJid,
55
+ messageId: ctx.key?.id,
52
56
  });
53
- } catch (error) {
54
- logger.warn('Falha ao enviar reação de comando:', error?.message);
55
- }
57
+ });
56
58
  }
57
59
 
58
60
  const execution = await executeMessageCommandRoute({
@@ -1,4 +1,4 @@
1
- export const createConversationMiddleware = ({ logger, resolveSenderAdminForContext, resolveSenderOwnerForContext, resolveHasGoogleLoginForContext, isUserAdmin, isAdminSenderAsync, resolveCanonicalSenderJidForContext, isWhatsAppUserLinkedToGoogleWebAccount, WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN, ensureUserHasGoogleWebLoginForCommand, executeMessageCommandRoute, isAdminCommand, runCommand, sendReply, routeConversationMessage, stopMessagePipeline }) => {
1
+ export const createConversationMiddleware = ({ logger, resolveSenderAdminForContext, resolveSenderOwnerForContext, resolveHasGoogleLoginForContext, isUserAdmin, isAdminSenderAsync, resolveCanonicalSenderJidForContext, isWhatsAppUserLinkedToGoogleWebAccount, WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN, ensureUserHasGoogleWebLoginForCommand, executeMessageCommandRoute, isAdminCommand, runCommand, sendReply, routeConversationMessage, stopMessagePipeline, conversationAutoReplyEnabled = true }) => {
2
2
  const resolveToolSecurityContextForConversation = async (ctx) => {
3
3
  if (ctx.memo.toolSecurityContext) return ctx.memo.toolSecurityContext;
4
4
 
@@ -133,6 +133,7 @@ export const createConversationMiddleware = ({ logger, resolveSenderAdminForCont
133
133
  };
134
134
 
135
135
  return async (ctx) => {
136
+ if (!conversationAutoReplyEnabled) return null;
136
137
  if (ctx.isCommandMessage || ctx.isMessageFromBot || !ctx.isNotifyUpsert) return null;
137
138
 
138
139
  try {
@@ -26,6 +26,8 @@ const createBaseContext = (overrides = {}) => ({
26
26
  mediaEntries: [],
27
27
  upsertType: 'notify',
28
28
  isNotifyUpsert: true,
29
+ sessionId: 'session-default',
30
+ ownerSessionId: null,
29
31
  isCommandMessage: false,
30
32
  hasCommandPrefix: false,
31
33
  analysisPayload: {
@@ -166,6 +168,108 @@ test('pre-processing trata trigger de iniciar login', async () => {
166
168
  assert.equal(stopSpy.calls[0].metadataPatch.flow, 'whatsapp_google_login');
167
169
  });
168
170
 
171
+ test('pre-processing owner gate bloqueia sessao nao-owner em modo enforce', async () => {
172
+ const stopSpy = createStopSpy();
173
+
174
+ const middlewares = createPreProcessingMiddlewares({
175
+ executeQuery: async () => [],
176
+ TABLES: { RPG_PLAYER: 'rpg_player' },
177
+ isStatusJid: () => false,
178
+ stopMessagePipeline: stopSpy.stopMessagePipeline,
179
+ handleAntiLink: async () => false,
180
+ ensureCommandPrefixForContext: async () => '/',
181
+ resolveCaptchaByMessage: async () => {},
182
+ maybeHandleStartLoginMessage: async () => false,
183
+ mergeAnalysisMetadata: (analysisPayload, patch) => {
184
+ analysisPayload.metadata = {
185
+ ...(analysisPayload.metadata || {}),
186
+ ...(patch || {}),
187
+ };
188
+ },
189
+ ensureGroupConfigForContext: async () => ({}),
190
+ resolveStickerFocusState: () => ({ enabled: false }),
191
+ resolveStickerFocusMessageClassification: () => ({ isThrottleCandidate: false }),
192
+ resolveGroupOwnerForContext: async (ctx) => {
193
+ ctx.ownerSessionId = 'session-owner';
194
+ return { ownerSessionId: 'session-owner' };
195
+ },
196
+ ownerEnforcementMode: 'enforce',
197
+ primarySessionId: 'session-primary',
198
+ isUserAdmin: async () => false,
199
+ canSendMessageInStickerFocus: () => ({ allowed: true, remainingMs: 0 }),
200
+ registerMessageUsageInStickerFocus: () => {},
201
+ shouldSendStickerFocusWarning: () => false,
202
+ sendReply: async () => {},
203
+ formatStickerFocusRuleLabel: () => '',
204
+ formatRemainingMinutesLabel: () => 1,
205
+ logger: { warn: () => {}, info: () => {} },
206
+ });
207
+
208
+ const ctx = createBaseContext({
209
+ sessionId: 'session-worker',
210
+ isGroupMessage: true,
211
+ remoteJid: '120363111111111111@g.us',
212
+ });
213
+ const result = await middlewares.enforceGroupOwnerMiddleware(ctx);
214
+
215
+ assert.deepEqual(result, { stop: true });
216
+ assert.equal(stopSpy.calls.length, 1);
217
+ assert.equal(stopSpy.calls[0].processingResult, 'blocked_group_owner_enforcement');
218
+ assert.equal(ctx.analysisPayload.metadata.owner_enforcement_result, 'blocked_non_owner');
219
+ assert.equal(ctx.analysisPayload.metadata.owner_session_id, 'session-owner');
220
+ });
221
+
222
+ test('pre-processing owner gate apenas loga em modo shadow para sessao nao-owner', async () => {
223
+ const stopSpy = createStopSpy();
224
+ const infoLogs = [];
225
+
226
+ const middlewares = createPreProcessingMiddlewares({
227
+ executeQuery: async () => [],
228
+ TABLES: { RPG_PLAYER: 'rpg_player' },
229
+ isStatusJid: () => false,
230
+ stopMessagePipeline: stopSpy.stopMessagePipeline,
231
+ handleAntiLink: async () => false,
232
+ ensureCommandPrefixForContext: async () => '/',
233
+ resolveCaptchaByMessage: async () => {},
234
+ maybeHandleStartLoginMessage: async () => false,
235
+ mergeAnalysisMetadata: (analysisPayload, patch) => {
236
+ analysisPayload.metadata = {
237
+ ...(analysisPayload.metadata || {}),
238
+ ...(patch || {}),
239
+ };
240
+ },
241
+ ensureGroupConfigForContext: async () => ({}),
242
+ resolveStickerFocusState: () => ({ enabled: false }),
243
+ resolveStickerFocusMessageClassification: () => ({ isThrottleCandidate: false }),
244
+ resolveGroupOwnerForContext: async (ctx) => {
245
+ ctx.ownerSessionId = 'session-owner';
246
+ return { ownerSessionId: 'session-owner' };
247
+ },
248
+ ownerEnforcementMode: 'shadow',
249
+ primarySessionId: 'session-primary',
250
+ isUserAdmin: async () => false,
251
+ canSendMessageInStickerFocus: () => ({ allowed: true, remainingMs: 0 }),
252
+ registerMessageUsageInStickerFocus: () => {},
253
+ shouldSendStickerFocusWarning: () => false,
254
+ sendReply: async () => {},
255
+ formatStickerFocusRuleLabel: () => '',
256
+ formatRemainingMinutesLabel: () => 1,
257
+ logger: { warn: () => {}, info: (msg, payload) => infoLogs.push({ msg, payload }) },
258
+ });
259
+
260
+ const ctx = createBaseContext({
261
+ sessionId: 'session-worker',
262
+ isGroupMessage: true,
263
+ });
264
+ const result = await middlewares.enforceGroupOwnerMiddleware(ctx);
265
+
266
+ assert.equal(result, null);
267
+ assert.equal(stopSpy.calls.length, 0);
268
+ assert.equal(ctx.pipelineStopped, false);
269
+ assert.equal(ctx.analysisPayload.metadata.owner_enforcement_result, 'shadow_non_owner');
270
+ assert.equal(infoLogs.length, 1);
271
+ });
272
+
169
273
  test('conversation middleware responde e interrompe pipeline', async () => {
170
274
  const stopSpy = createStopSpy();
171
275
  const replies = [];
@@ -1,4 +1,9 @@
1
- export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJid, stopMessagePipeline, handleAntiLink, ensureCommandPrefixForContext, resolveCaptchaByMessage, maybeHandleStartLoginMessage, mergeAnalysisMetadata, ensureGroupConfigForContext, resolveStickerFocusState, resolveStickerFocusMessageClassification, resolveSenderAdminForContext, isUserAdmin, canSendMessageInStickerFocus, registerMessageUsageInStickerFocus, shouldSendStickerFocusWarning, sendReply, formatStickerFocusRuleLabel, formatRemainingMinutesLabel, logger }) => {
1
+ export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJid, stopMessagePipeline, handleAntiLink, ensureCommandPrefixForContext, resolveCaptchaByMessage, maybeHandleStartLoginMessage, mergeAnalysisMetadata, ensureGroupConfigForContext, resolveStickerFocusState, resolveStickerFocusMessageClassification, resolveGroupOwnerForContext, ownerEnforcementMode = 'off', primarySessionId = 'default', resolveSenderAdminForContext, isUserAdmin, canSendMessageInStickerFocus, registerMessageUsageInStickerFocus, shouldSendStickerFocusWarning, sendReply, formatStickerFocusRuleLabel, formatRemainingMinutesLabel, logger }) => {
2
+ const normalizedOwnerEnforcementMode = String(ownerEnforcementMode || 'off')
3
+ .trim()
4
+ .toLowerCase();
5
+ const effectiveOwnerEnforcementMode = normalizedOwnerEnforcementMode === 'enforce' || normalizedOwnerEnforcementMode === 'shadow' ? normalizedOwnerEnforcementMode : 'off';
6
+
2
7
  const touchSenderLastSeenMiddleware = async (ctx) => {
3
8
  if (!ctx.senderJid || isStatusJid(ctx.remoteJid)) return;
4
9
 
@@ -21,6 +26,78 @@ export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJ
21
26
  });
22
27
  };
23
28
 
29
+ const enforceGroupOwnerMiddleware = async (ctx) => {
30
+ if (!ctx.isGroupMessage) return null;
31
+
32
+ mergeAnalysisMetadata(ctx.analysisPayload, {
33
+ owner_enforcement_mode: effectiveOwnerEnforcementMode,
34
+ processing_session_id: ctx.sessionId,
35
+ });
36
+
37
+ if (effectiveOwnerEnforcementMode === 'off') return null;
38
+
39
+ if (typeof resolveGroupOwnerForContext !== 'function') {
40
+ logger.warn('Middleware de owner enforcement sem resolver de owner configurado.', {
41
+ action: 'group_owner_enforcement_missing_resolver',
42
+ sessionId: ctx.sessionId,
43
+ groupId: ctx.remoteJid,
44
+ });
45
+ return null;
46
+ }
47
+
48
+ const ownerState = await resolveGroupOwnerForContext(ctx);
49
+ const ownerSessionId = String(ctx.ownerSessionId || ownerState?.ownerSessionId || '').trim() || null;
50
+ ctx.ownerSessionId = ownerSessionId;
51
+
52
+ mergeAnalysisMetadata(ctx.analysisPayload, {
53
+ owner_session_id: ownerSessionId,
54
+ });
55
+
56
+ if (!ownerSessionId) {
57
+ mergeAnalysisMetadata(ctx.analysisPayload, {
58
+ owner_enforcement_result: 'owner_not_found',
59
+ });
60
+ return null;
61
+ }
62
+
63
+ const currentSessionId = String(ctx.sessionId || '').trim() || primarySessionId;
64
+ const isOwnerSession = ownerSessionId === currentSessionId;
65
+ if (isOwnerSession) {
66
+ mergeAnalysisMetadata(ctx.analysisPayload, {
67
+ owner_enforcement_result: 'owner_match',
68
+ });
69
+ return null;
70
+ }
71
+
72
+ if (effectiveOwnerEnforcementMode === 'shadow') {
73
+ mergeAnalysisMetadata(ctx.analysisPayload, {
74
+ owner_enforcement_result: 'shadow_non_owner',
75
+ });
76
+ logger.info('Owner enforcement (shadow): sessao nao-owner detectada no grupo.', {
77
+ action: 'group_owner_enforcement_shadow_non_owner',
78
+ groupId: ctx.remoteJid,
79
+ sessionId: currentSessionId,
80
+ ownerSessionId,
81
+ messageId: ctx.key?.id || null,
82
+ });
83
+ return null;
84
+ }
85
+
86
+ mergeAnalysisMetadata(ctx.analysisPayload, {
87
+ owner_enforcement_result: 'blocked_non_owner',
88
+ blocked_by: 'group_owner_enforcement',
89
+ });
90
+ logger.info('Owner enforcement: mensagem bloqueada em sessao nao-owner.', {
91
+ action: 'group_owner_enforcement_blocked_non_owner',
92
+ groupId: ctx.remoteJid,
93
+ sessionId: currentSessionId,
94
+ ownerSessionId,
95
+ messageId: ctx.key?.id || null,
96
+ isCommand: ctx.isCommandMessage,
97
+ });
98
+ return stopMessagePipeline(ctx, 'blocked_group_owner_enforcement');
99
+ };
100
+
24
101
  const applyGroupPolicyMiddleware = async (ctx) => {
25
102
  if (!ctx.isGroupMessage) return null;
26
103
 
@@ -129,7 +206,7 @@ export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJ
129
206
  if (shouldSendStickerFocusWarning({ groupId: ctx.remoteJid, senderJid: ctx.senderJid })) {
130
207
  try {
131
208
  await sendReply(ctx.sock, ctx.remoteJid, ctx.messageInfo, ctx.expirationMessage, {
132
- text: '🖼️ Este chat está com *foco em sticker* ativo.\n' + 'Siga o padrão: envie apenas *imagens* ou *vídeos* para criação automática, ou compartilhe seus *stickers*.\n' + `Mensagens como texto e áudio seguem uma janela de tempo: *${formatStickerFocusRuleLabel(stickerFocusState)}*.\n` + `Tente novamente em ~${formatRemainingMinutesLabel(messageGate.remainingMs)} min ou peça para um admin abrir a janela com *${ctx.commandPrefix}chatwindow on*.`,
209
+ text: '🖼️ *Modo Sticker ativo!*\n\n' + 'Este chat está focado em *stickers automáticos*.\n' + '👉 Envie apenas *imagens* ou *vídeos* para gerar stickers,\n' + '👉 Ou compartilhe *stickers* normalmente.\n\n' + '⏳ *Texto e áudio estão temporariamente limitados*.\n' + `Janela atual: *${formatStickerFocusRuleLabel(stickerFocusState)}*\n` + `Tente novamente em ~${formatRemainingMinutesLabel(messageGate.remainingMs)}.\n\n` + `💡 Um admin pode liberar com: *${ctx.commandPrefix}chatwindow on*`,
133
210
  });
134
211
  } catch (error) {
135
212
  logger.warn('Falha ao enviar aviso de sticker focus.', {
@@ -157,6 +234,7 @@ export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJ
157
234
  return {
158
235
  touchSenderLastSeenMiddleware,
159
236
  ignoreUnprocessableMessageMiddleware,
237
+ enforceGroupOwnerMiddleware,
160
238
  applyGroupPolicyMiddleware,
161
239
  resolveCaptchaMiddleware,
162
240
  handleStartLoginTriggerMiddleware,
@@ -1,9 +1,10 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import 'dotenv/config';
2
3
 
3
4
  import { isAdminCommand } from '../modules/adminModule/groupCommandHandlers.js';
4
5
  import { explicarComandoGlobal, registerGlobalHelpCommandExecution } from '../services/ai/globalModuleAiHelpService.js';
5
6
  import { extractSupportedStickerMediaDetails, processSticker } from '../modules/stickerModule/stickerCommand.js';
6
- 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';
7
8
  import { isUserAdmin } from '../config/index.js';
8
9
  import { isAdminSenderAsync } from '../config/index.js';
9
10
  import { executeQuery, TABLES } from '../../database/index.js';
@@ -19,6 +20,7 @@ import { createMessageAnalysisEvent } from '../modules/analyticsModule/messageAn
19
20
  import { routeConversationMessage } from '../services/ai/conversationRouterService.js';
20
21
  import { executeMessageCommandRoute, isKnownNonAdminCommand } from '../services/ai/messageCommandExecutionService.js';
21
22
  import { canSendMessageInStickerFocus, registerMessageUsageInStickerFocus, resolveStickerFocusMessageClassification, resolveStickerFocusState, shouldSendStickerFocusWarning } from '../services/sticker/stickerFocusService.js';
23
+ import { getOwner as getGroupOwner } from '../services/multiSession/groupOwnershipService.js';
22
24
  import { createPreProcessingMiddlewares } from './messagePipeline/preProcessingMiddlewares.js';
23
25
  import { createConversationMiddleware } from './messagePipeline/conversationMiddleware.js';
24
26
  import { createCommandMiddleware } from './messagePipeline/commandMiddleware.js';
@@ -40,6 +42,7 @@ const MESSAGE_REPLY_PRESENCE_BEFORE = normalizeWAPresence(process.env.MESSAGE_RE
40
42
  const MESSAGE_REPLY_PRESENCE_AFTER = normalizeWAPresence(process.env.MESSAGE_REPLY_PRESENCE_AFTER, 'paused');
41
43
  const MESSAGE_REPLY_PRESENCE_DELAY_MS = parseEnvInt(process.env.MESSAGE_REPLY_PRESENCE_DELAY_MS, 280, 0, 3_000);
42
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);
43
46
  const WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN = parseEnvBool(process.env.WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN, true);
44
47
  const SITE_ORIGIN =
45
48
  String(process.env.SITE_ORIGIN || process.env.WHATSAPP_LOGIN_BASE_URL || 'https://omnizap.shop')
@@ -47,11 +50,24 @@ const SITE_ORIGIN =
47
50
  .replace(/\/+$/, '') || 'https://omnizap.shop';
48
51
  const SITE_LOGIN_URL = `${SITE_ORIGIN}/login/`;
49
52
  const SITE_GROUP_LOGIN_URL = `${SITE_ORIGIN}/login`;
53
+ const MULTI_SESSION_RUNTIME_CONFIG = getMultiSessionRuntimeConfig();
54
+ const PRIMARY_SESSION_ID = String(MULTI_SESSION_RUNTIME_CONFIG?.primarySessionId || 'default').trim() || 'default';
55
+ const VALID_SESSION_IDS = new Set(Array.isArray(MULTI_SESSION_RUNTIME_CONFIG?.sessionIds) ? MULTI_SESSION_RUNTIME_CONFIG.sessionIds : [PRIMARY_SESSION_ID]);
56
+ const GROUP_OWNER_ENFORCEMENT_MODE = String(MULTI_SESSION_RUNTIME_CONFIG?.ownerEnforcementMode || 'off')
57
+ .trim()
58
+ .toLowerCase();
59
+
60
+ const normalizeSessionId = (sessionId) => {
61
+ const normalized = String(sessionId || '').trim();
62
+ if (!normalized) return PRIMARY_SESSION_ID;
63
+ if (VALID_SESSION_IDS.has(normalized)) return normalized;
64
+ return PRIMARY_SESSION_ID;
65
+ };
50
66
 
51
67
  let messageAnalyticsTableMissingLogged = false;
52
68
  const recentCommandExecutions = new Map();
53
69
 
54
- const pruneRecentCommandExecutions = (nowMs = Date.now()) => {
70
+ const pruneRecentCommandExecutions = (nowMs = __timeNowMs()) => {
55
71
  for (const [cacheKey, expiresAt] of recentCommandExecutions.entries()) {
56
72
  if (expiresAt <= nowMs) {
57
73
  recentCommandExecutions.delete(cacheKey);
@@ -70,7 +86,7 @@ const isDuplicateCommandExecution = (chatId, messageId) => {
70
86
  const cacheKey = buildCommandExecutionCacheKey(chatId, messageId);
71
87
  if (!cacheKey) return false;
72
88
 
73
- const nowMs = Date.now();
89
+ const nowMs = __timeNowMs();
74
90
  const expiresAt = recentCommandExecutions.get(cacheKey) || 0;
75
91
  if (expiresAt <= nowMs) {
76
92
  recentCommandExecutions.delete(cacheKey);
@@ -83,8 +99,8 @@ const isDuplicateCommandExecution = (chatId, messageId) => {
83
99
  const markCommandExecution = (chatId, messageId) => {
84
100
  const cacheKey = buildCommandExecutionCacheKey(chatId, messageId);
85
101
  if (!cacheKey) return;
86
- pruneRecentCommandExecutions(Date.now());
87
- recentCommandExecutions.set(cacheKey, Date.now() + MESSAGE_COMMAND_DEDUPE_TTL_MS);
102
+ pruneRecentCommandExecutions(__timeNowMs());
103
+ recentCommandExecutions.set(cacheKey, __timeNowMs() + MESSAGE_COMMAND_DEDUPE_TTL_MS);
88
104
  };
89
105
 
90
106
  const normalizeTriggerText = (value) =>
@@ -169,7 +185,7 @@ const maybeHandleStartLoginMessage = async ({ sock, messageInfo, extractedText,
169
185
 
170
186
  if (isGroupMessage) {
171
187
  await sendReply(sock, remoteJid, messageInfo, expirationMessage, {
172
- text: 'Por seguranca, envie *iniciar* no privado do bot para receber seu link de login.',
188
+ 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.',
173
189
  });
174
190
  return true;
175
191
  }
@@ -198,7 +214,7 @@ const maybeHandleStartLoginMessage = async ({ sock, messageInfo, extractedText,
198
214
  const safeName = String(senderName || '').trim();
199
215
  const greeting = safeName ? `Oi, *${safeName}*!` : 'Oi!';
200
216
  await sendReply(sock, remoteJid, messageInfo, expirationMessage, {
201
- 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.',
217
+ 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.',
202
218
  });
203
219
 
204
220
  return true;
@@ -373,7 +389,7 @@ const ensureUserHasGoogleWebLoginForCommand = async ({ sock, messageInfo, sender
373
389
  }
374
390
 
375
391
  const loginUrl = isGroupMessage ? SITE_GROUP_LOGIN_URL : buildSiteLoginUrlForUser(resolvedCanonicalUserId || senderJid);
376
- 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).`;
392
+ 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*).`;
377
393
 
378
394
  await sendReply(sock, remoteJid, messageInfo, expirationMessage, {
379
395
  text: loginMessage,
@@ -458,6 +474,43 @@ const resolveSenderOwnerForContext = async (ctx) => {
458
474
  return ctx.memo.senderOwnerPromise;
459
475
  };
460
476
 
477
+ const resolveGroupOwnerForContext = async (ctx, { bypassCache = false } = {}) => {
478
+ if (!ctx.isGroupMessage) return null;
479
+
480
+ if (!bypassCache && ctx.memo.groupOwnerPromise) {
481
+ return ctx.memo.groupOwnerPromise;
482
+ }
483
+
484
+ const fetchOwnerPromise = (async () => {
485
+ try {
486
+ const ownerState = await getGroupOwner(ctx.remoteJid, { bypassCache });
487
+ const ownerSessionId = String(ownerState?.ownerSessionId || '').trim() || null;
488
+ ctx.ownerSessionId = ownerSessionId;
489
+ mergeAnalysisMetadata(ctx.analysisPayload, {
490
+ owner_session_id: ownerSessionId,
491
+ });
492
+ return ownerState;
493
+ } catch (error) {
494
+ logger.warn('Falha ao resolver owner de grupo para roteamento de sessão.', {
495
+ action: 'group_owner_resolution_failed',
496
+ sessionId: ctx.sessionId,
497
+ groupId: ctx.remoteJid,
498
+ error: error?.message,
499
+ });
500
+ ctx.ownerSessionId = null;
501
+ mergeAnalysisMetadata(ctx.analysisPayload, {
502
+ owner_resolution_error: String(error?.code || error?.name || 'lookup_failed')
503
+ .trim()
504
+ .toLowerCase(),
505
+ });
506
+ return null;
507
+ }
508
+ })();
509
+
510
+ ctx.memo.groupOwnerPromise = fetchOwnerPromise;
511
+ return fetchOwnerPromise;
512
+ };
513
+
461
514
  const resolveHasGoogleLoginForContext = async (ctx) => {
462
515
  if (ctx.isMessageFromBot || !WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN) {
463
516
  return undefined;
@@ -475,10 +528,11 @@ const resolveHasGoogleLoginForContext = async (ctx) => {
475
528
  return ctx.memo.hasGoogleLoginPromise;
476
529
  };
477
530
 
478
- const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyUpsert, sock }) => {
531
+ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyUpsert, sock, sessionId }) => {
479
532
  const key = messageInfo?.key || {};
480
533
  const remoteJid = key?.remoteJid;
481
534
  if (!remoteJid) return null;
535
+ const normalizedSessionId = normalizeSessionId(sessionId);
482
536
 
483
537
  const isGroupMessage = isGroupJid(remoteJid);
484
538
  const extractedText = extractMessageContent(messageInfo);
@@ -504,6 +558,7 @@ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyU
504
558
  .slice(0, 10);
505
559
 
506
560
  const analysisPayload = {
561
+ sessionId: normalizedSessionId,
507
562
  messageId: key?.id || null,
508
563
  chatId: remoteJid || null,
509
564
  senderId: senderJid || null,
@@ -529,6 +584,8 @@ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyU
529
584
  upsert_type: upsertType,
530
585
  is_notify_upsert: isNotifyUpsert,
531
586
  is_history_append: upsertType === 'append',
587
+ session_id: normalizedSessionId,
588
+ owner_session_id: null,
532
589
  addressing_mode: addressingMode || null,
533
590
  participant_alt: key?.participantAlt || null,
534
591
  remote_jid_alt: key?.remoteJidAlt || null,
@@ -549,6 +606,8 @@ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyU
549
606
  botJidCandidates,
550
607
  botJid,
551
608
  isMessageFromBot,
609
+ sessionId: normalizedSessionId,
610
+ ownerSessionId: null,
552
611
  commandPrefix: DEFAULT_COMMAND_PREFIX,
553
612
  groupConfig: null,
554
613
  groupConfigLoaded: false,
@@ -563,7 +622,7 @@ const createMessagePipelineContext = async ({ messageInfo, upsertType, isNotifyU
563
622
  };
564
623
  };
565
624
 
566
- const { touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, applyGroupPolicyMiddleware, resolveCaptchaMiddleware, handleStartLoginTriggerMiddleware, detectCommandIntentMiddleware, applyStickerFocusMiddleware } = createPreProcessingMiddlewares({
625
+ const { touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, enforceGroupOwnerMiddleware, applyGroupPolicyMiddleware, resolveCaptchaMiddleware, handleStartLoginTriggerMiddleware, detectCommandIntentMiddleware, applyStickerFocusMiddleware } = createPreProcessingMiddlewares({
567
626
  executeQuery,
568
627
  TABLES,
569
628
  isStatusJid,
@@ -576,6 +635,9 @@ const { touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, app
576
635
  ensureGroupConfigForContext,
577
636
  resolveStickerFocusState,
578
637
  resolveStickerFocusMessageClassification,
638
+ resolveGroupOwnerForContext,
639
+ ownerEnforcementMode: GROUP_OWNER_ENFORCEMENT_MODE,
640
+ primarySessionId: PRIMARY_SESSION_ID,
579
641
  resolveSenderAdminForContext,
580
642
  isUserAdmin,
581
643
  canSendMessageInStickerFocus,
@@ -604,6 +666,7 @@ const routeConversationMiddleware = createConversationMiddleware({
604
666
  sendReply,
605
667
  routeConversationMessage,
606
668
  stopMessagePipeline,
669
+ conversationAutoReplyEnabled: CONVERSATIONAL_AUTO_REPLY_ENABLED,
607
670
  });
608
671
 
609
672
  const executeCommandMiddleware = createCommandMiddleware({
@@ -641,7 +704,7 @@ const runPostProcessingMiddleware = createPostProcessingMiddleware({
641
704
  normalizeAnalysisErrorCode,
642
705
  });
643
706
 
644
- const MESSAGE_PIPELINE_MIDDLEWARES = [touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, applyGroupPolicyMiddleware, resolveCaptchaMiddleware, handleStartLoginTriggerMiddleware, detectCommandIntentMiddleware, applyStickerFocusMiddleware, routeConversationMiddleware, executeCommandMiddleware, runPostProcessingMiddleware];
707
+ const MESSAGE_PIPELINE_MIDDLEWARES = [touchSenderLastSeenMiddleware, ignoreUnprocessableMessageMiddleware, enforceGroupOwnerMiddleware, applyGroupPolicyMiddleware, resolveCaptchaMiddleware, handleStartLoginTriggerMiddleware, detectCommandIntentMiddleware, applyStickerFocusMiddleware, routeConversationMiddleware, executeCommandMiddleware, runPostProcessingMiddleware];
645
708
 
646
709
  const runMessagePipeline = async (ctx) => {
647
710
  for (const middleware of MESSAGE_PIPELINE_MIDDLEWARES) {
@@ -656,7 +719,8 @@ const runMessagePipeline = async (ctx) => {
656
719
  *
657
720
  * @param {Object} update - Objeto contendo a atualização do WhatsApp.
658
721
  */
659
- export const handleMessagesThroughPipeline = async (update, sock) => {
722
+ export const handleMessagesThroughPipeline = async (update, sock, options = {}) => {
723
+ const sessionId = normalizeSessionId(options?.sessionId);
660
724
  if (update.messages && Array.isArray(update.messages)) {
661
725
  try {
662
726
  const upsertType = update?.type || null;
@@ -668,6 +732,7 @@ export const handleMessagesThroughPipeline = async (update, sock) => {
668
732
  upsertType,
669
733
  isNotifyUpsert,
670
734
  sock,
735
+ sessionId,
671
736
  });
672
737
  if (!context) continue;
673
738
 
@@ -677,19 +742,34 @@ export const handleMessagesThroughPipeline = async (update, sock) => {
677
742
  context.analysisPayload.processingResult = 'error';
678
743
  context.analysisPayload.errorCode = normalizeAnalysisErrorCode(messageError);
679
744
  logger.error('Erro ao processar mensagem individual:', {
745
+ sessionId,
746
+ ownerSessionId: context.ownerSessionId || null,
680
747
  error: messageError?.message,
681
748
  messageId: context.key?.id || null,
682
749
  remoteJid: context.remoteJid,
683
750
  });
684
751
  } finally {
752
+ logger.debug('Mensagem processada pelo pipeline.', {
753
+ action: 'message_pipeline_processed',
754
+ sessionId: context.sessionId,
755
+ ownerSessionId: context.ownerSessionId || null,
756
+ messageId: context.key?.id || null,
757
+ remoteJid: context.remoteJid,
758
+ result: context.analysisPayload.processingResult,
759
+ isCommand: context.analysisPayload.isCommand,
760
+ });
685
761
  persistMessageAnalysisEvent(context.analysisPayload);
686
762
  }
687
763
  }
688
764
  } catch (error) {
689
- logger.error('Erro ao processar mensagens:', error?.message);
765
+ logger.error('Erro ao processar mensagens:', {
766
+ sessionId,
767
+ error: error?.message,
768
+ });
690
769
  }
691
770
  } else {
692
771
  logger.info('🔄 Processando evento recebido:', {
772
+ sessionId,
693
773
  eventType: update?.type || 'unknown',
694
774
  eventData: update,
695
775
  });