@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
@@ -0,0 +1,128 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { createGroupOwnerWriteStateResolver } from './groupOwnerWriteStateResolver.js';
5
+
6
+ /**
7
+ * Cria um cache em memória simples para testes.
8
+ * @returns {{get: (key: string) => any, set: (key: string, value: any) => boolean, del: (key: string) => boolean, keys: () => string[]}}
9
+ */
10
+ const createCache = () => {
11
+ const map = new Map();
12
+ return {
13
+ get: (key) => map.get(key),
14
+ set: (key, value) => {
15
+ map.set(key, value);
16
+ return true;
17
+ },
18
+ del: (key) => map.delete(key),
19
+ keys: () => Array.from(map.keys()),
20
+ };
21
+ };
22
+
23
+ /**
24
+ * Monta a chave de cache por sessão e grupo.
25
+ * @param {string} groupJid
26
+ * @param {string} sessionId
27
+ * @returns {string}
28
+ */
29
+ const buildCacheKey = (groupJid, sessionId) => `${sessionId}:${groupJid}`;
30
+
31
+ /**
32
+ * Normaliza o ID da sessão para os cenários de teste.
33
+ * @param {string | null | undefined} value
34
+ * @returns {string}
35
+ */
36
+ const normalizeSessionId = (value) => String(value || '').trim() || 'default';
37
+ /**
38
+ * Verifica se o JID pertence a grupo.
39
+ * @param {string | null | undefined} jid
40
+ * @returns {boolean}
41
+ */
42
+ const isGroupJid = (jid) => String(jid || '').endsWith('@g.us');
43
+
44
+ test('socketController multi-session: fencing token por assignment_version invalida writer stale', async () => {
45
+ let ownerState = {
46
+ ownerSessionId: 'session-a',
47
+ assignmentVersion: 1,
48
+ };
49
+
50
+ const resolver = createGroupOwnerWriteStateResolver({
51
+ buildCacheKeyImpl: buildCacheKey,
52
+ getOwnerImpl: async () => ownerState,
53
+ tryAcquireImpl: async () => ({ acquired: false, reason: 'claim_disabled' }),
54
+ cacheImpl: createCache(),
55
+ isGroupJidImpl: isGroupJid,
56
+ normalizeSessionIdImpl: normalizeSessionId,
57
+ loggerImpl: { warn: () => {} },
58
+ });
59
+
60
+ const first = await resolver('120363555555555555@g.us', 'session-a', {
61
+ allowClaim: false,
62
+ source: 'test_first',
63
+ });
64
+ assert.equal(first.allowed, true);
65
+ assert.equal(first.assignmentVersion, 1);
66
+
67
+ ownerState = {
68
+ ownerSessionId: 'session-a',
69
+ assignmentVersion: 2,
70
+ };
71
+
72
+ const stale = await resolver('120363555555555555@g.us', 'session-a', {
73
+ allowClaim: false,
74
+ source: 'test_stale',
75
+ expectedAssignmentVersion: 1,
76
+ enforceFence: true,
77
+ });
78
+ assert.equal(stale.allowed, false);
79
+ assert.equal(stale.reason, 'fence_token_mismatch');
80
+ assert.equal(stale.assignmentVersion, 2);
81
+ });
82
+
83
+ test('socketController multi-session: sessão antiga perde escrita após failover de owner', async () => {
84
+ let ownerState = {
85
+ ownerSessionId: 'session-a',
86
+ assignmentVersion: 7,
87
+ };
88
+
89
+ const resolver = createGroupOwnerWriteStateResolver({
90
+ buildCacheKeyImpl: buildCacheKey,
91
+ getOwnerImpl: async () => ownerState,
92
+ tryAcquireImpl: async () => ({ acquired: false, reason: 'claim_disabled' }),
93
+ cacheImpl: createCache(),
94
+ isGroupJidImpl: isGroupJid,
95
+ normalizeSessionIdImpl: normalizeSessionId,
96
+ loggerImpl: { warn: () => {} },
97
+ });
98
+
99
+ const beforeFailover = await resolver('120363666666666666@g.us', 'session-a', {
100
+ allowClaim: false,
101
+ source: 'before_failover',
102
+ });
103
+ assert.equal(beforeFailover.allowed, true);
104
+ assert.equal(beforeFailover.assignmentVersion, 7);
105
+
106
+ ownerState = {
107
+ ownerSessionId: 'session-b',
108
+ assignmentVersion: 8,
109
+ };
110
+
111
+ const staleOwner = await resolver('120363666666666666@g.us', 'session-a', {
112
+ allowClaim: false,
113
+ source: 'after_failover_old_owner',
114
+ expectedAssignmentVersion: 7,
115
+ enforceFence: true,
116
+ });
117
+ assert.equal(staleOwner.allowed, false);
118
+ assert.equal(staleOwner.reason, 'owned_by_other');
119
+
120
+ const newOwner = await resolver('120363666666666666@g.us', 'session-b', {
121
+ allowClaim: false,
122
+ source: 'after_failover_new_owner',
123
+ expectedAssignmentVersion: 8,
124
+ enforceFence: true,
125
+ });
126
+ assert.equal(newOwner.allowed, true);
127
+ assert.equal(newOwner.assignmentVersion, 8);
128
+ });
@@ -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', allowSelfCommandsOnAppend = true, 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
 
@@ -79,12 +156,26 @@ export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJ
79
156
 
80
157
  const detectCommandIntentMiddleware = async (ctx) => {
81
158
  ctx.hasCommandPrefix = ctx.extractedText.startsWith(ctx.commandPrefix);
82
- ctx.isCommandMessage = ctx.hasCommandPrefix && ctx.isNotifyUpsert;
159
+ const isSelfAppendCommand =
160
+ allowSelfCommandsOnAppend &&
161
+ ctx.hasCommandPrefix &&
162
+ !ctx.isNotifyUpsert &&
163
+ String(ctx.upsertType || '')
164
+ .trim()
165
+ .toLowerCase() === 'append' &&
166
+ ctx.isMessageFromBot;
167
+ ctx.isCommandMessage = ctx.hasCommandPrefix && (ctx.isNotifyUpsert || isSelfAppendCommand);
83
168
 
84
169
  ctx.analysisPayload.isCommand = ctx.isCommandMessage;
85
170
  ctx.analysisPayload.commandPrefix = ctx.commandPrefix;
86
171
 
87
- if (ctx.hasCommandPrefix && !ctx.isNotifyUpsert) {
172
+ if (isSelfAppendCommand) {
173
+ mergeAnalysisMetadata(ctx.analysisPayload, {
174
+ command_detected_via: 'append_from_me',
175
+ });
176
+ }
177
+
178
+ if (ctx.hasCommandPrefix && !ctx.isCommandMessage) {
88
179
  mergeAnalysisMetadata(ctx.analysisPayload, {
89
180
  command_suppressed_reason: 'non_notify_upsert',
90
181
  });
@@ -129,7 +220,7 @@ export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJ
129
220
  if (shouldSendStickerFocusWarning({ groupId: ctx.remoteJid, senderJid: ctx.senderJid })) {
130
221
  try {
131
222
  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*.`,
223
+ 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
224
  });
134
225
  } catch (error) {
135
226
  logger.warn('Falha ao enviar aviso de sticker focus.', {
@@ -157,6 +248,7 @@ export const createPreProcessingMiddlewares = ({ executeQuery, TABLES, isStatusJ
157
248
  return {
158
249
  touchSenderLastSeenMiddleware,
159
250
  ignoreUnprocessableMessageMiddleware,
251
+ enforceGroupOwnerMiddleware,
160
252
  applyGroupPolicyMiddleware,
161
253
  resolveCaptchaMiddleware,
162
254
  handleStartLoginTriggerMiddleware,