@omnizap-system/omnizap 2.6.1 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.env.example +54 -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 +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -2,8 +2,8 @@ import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { createGeminiTextService } from './geminiService.js';
4
4
 
5
- test('createGeminiTextService retorna null quando GEMINI_API_KEY nao existe', () => {
6
- const service = createGeminiTextService({ apiKey: '' });
5
+ test('createGeminiTextService retorna null no modo api_key quando GEMINI_API_KEY nao existe', () => {
6
+ const service = createGeminiTextService({ authMode: 'api_key', apiKey: '' });
7
7
  assert.equal(service, null);
8
8
  });
9
9
 
@@ -85,3 +85,60 @@ test('createGeminiTextService propaga erro detalhado da API', async (t) => {
85
85
  /Modelo invalido/,
86
86
  );
87
87
  });
88
+
89
+ test('createGeminiTextService usa Gemini CLI quando authMode=cli', async () => {
90
+ const calls = [];
91
+ const service = createGeminiTextService({
92
+ authMode: 'cli',
93
+ cliCommand: 'gemini',
94
+ defaultModel: 'gemini-2.5-flash',
95
+ isCliAvailableImpl: () => true,
96
+ execFileAsyncImpl: async (file, args, options) => {
97
+ calls.push({ file, args, options });
98
+ return {
99
+ stdout: 'OK_GEMINI_CLI\n',
100
+ stderr: 'Loaded cached credentials.',
101
+ };
102
+ },
103
+ });
104
+
105
+ assert.ok(service);
106
+ assert.equal(service.transport, 'cli');
107
+
108
+ const response = await service.generateText({
109
+ instructions: 'Responda curto.',
110
+ userPrompt: 'Diga OK.',
111
+ model: 'gemini-2.5-flash',
112
+ });
113
+
114
+ assert.equal(response.model, 'gemini-2.5-flash');
115
+ assert.equal(response.text, 'OK_GEMINI_CLI');
116
+ assert.equal(calls.length, 1);
117
+ assert.equal(calls[0].file, 'gemini');
118
+ assert.ok(calls[0].args.includes('-m'));
119
+ assert.ok(calls[0].args.includes('gemini-2.5-flash'));
120
+ assert.ok(calls[0].args.includes('-p'));
121
+ assert.ok(calls[0].args.includes('--output-format'));
122
+ });
123
+
124
+ test('createGeminiTextService propaga erro do Gemini CLI', async () => {
125
+ const service = createGeminiTextService({
126
+ authMode: 'cli',
127
+ cliCommand: 'gemini',
128
+ defaultModel: 'gemini-2.5-flash',
129
+ isCliAvailableImpl: () => true,
130
+ execFileAsyncImpl: async () => {
131
+ const error = new Error('Command failed');
132
+ error.stderr = 'ModelNotFoundError: Requested entity was not found.';
133
+ throw error;
134
+ },
135
+ });
136
+
137
+ await assert.rejects(
138
+ () =>
139
+ service.generateText({
140
+ userPrompt: 'teste',
141
+ }),
142
+ /ModelNotFoundError/,
143
+ );
144
+ });
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import OpenAI from 'openai';
5
5
  import { getAiHelpCachedResponse, upsertAiHelpCachedResponse } from './aiHelpResponseCacheRepository.js';
6
- import { createGeminiTextService, DEFAULT_GEMINI_MODEL } from './geminiService.js';
6
+ import { createGeminiTextService, DEFAULT_GEMINI_MODEL, isGeminiAuthReady } from './geminiService.js';
7
7
 
8
8
  const DEFAULT_FAQ_INTERVAL_MS = 6 * 60 * 60 * 1000;
9
9
  const DEFAULT_MAX_RESPONSE_CHARS = 3400;
@@ -169,6 +169,16 @@ const normalizeLlmProvider = (value, fallback = 'gemini') => {
169
169
  return fallback;
170
170
  };
171
171
 
172
+ const normalizeGeminiAuthMode = (value, fallback = 'cli') => {
173
+ const normalized = String(value || '')
174
+ .trim()
175
+ .toLowerCase();
176
+ if (normalized === 'api_key') return 'api_key';
177
+ if (normalized === 'cli') return 'cli';
178
+ if (normalized === 'auto') return 'auto';
179
+ return fallback;
180
+ };
181
+
172
182
  const looksLikeGeminiModel = (value) =>
173
183
  String(value || '')
174
184
  .trim()
@@ -241,7 +251,13 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
241
251
  const cachePathValue = String(faq.cache_file || '').trim();
242
252
  const cachePath = cachePathValue ? path.resolve(process.cwd(), cachePathValue) : path.join(process.cwd(), 'data', 'cache', `${moduleKey}-ai-faq-cache.json`);
243
253
 
244
- const provider = normalizeLlmProvider(envValue('PROVIDER') || llm.provider || process.env.AI_HELP_LLM_PROVIDER, process.env.GEMINI_API_KEY ? 'gemini' : 'openai');
254
+ const geminiAuthMode = normalizeGeminiAuthMode(envValue('GEMINI_AUTH_MODE') || llm.gemini_auth_mode || process.env.GEMINI_AUTH_MODE, 'cli');
255
+ const hasGeminiAuthHint = isGeminiAuthReady({
256
+ authMode: geminiAuthMode,
257
+ apiKey: process.env.GEMINI_API_KEY,
258
+ cliCommand: process.env.GEMINI_CLI_COMMAND || 'gemini',
259
+ });
260
+ const provider = normalizeLlmProvider(envValue('PROVIDER') || llm.provider || process.env.AI_HELP_LLM_PROVIDER, hasGeminiAuthHint ? 'gemini' : 'openai');
245
261
  const defaultModelByProvider = provider === 'gemini' ? process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL : process.env.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
246
262
  const rawModel = String(envValue('MODEL') || llm.model || defaultModelByProvider).trim() || defaultModelByProvider;
247
263
  const modelFromEnv = String(envValue('MODEL') || '').trim();
@@ -271,6 +287,7 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
271
287
  .trim()
272
288
  .toLowerCase() !== 'false',
273
289
  provider,
290
+ geminiAuthMode,
274
291
  model: resolvedModel,
275
292
  maxResponseChars: Math.max(400, toPositiveInt(envValue('MAX_RESPONSE_CHARS') || llm.max_response_chars, DEFAULT_MAX_RESPONSE_CHARS, 400)),
276
293
  maxAgentContextChars: Math.max(2_000, toPositiveInt(envValue('MAX_AGENT_CONTEXT_CHARS') || llm.max_agent_context_chars, DEFAULT_MAX_AGENT_CONTEXT_CHARS, 2_000)),
@@ -286,6 +303,7 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
286
303
  let faqGenerationPromise = null;
287
304
  let cachedOpenAIClient = null;
288
305
  let cachedGeminiService = null;
306
+ let cachedGeminiServiceKey = '';
289
307
 
290
308
  const createEmptyCache = () => ({
291
309
  version: FAQ_CACHE_VERSION,
@@ -441,7 +459,14 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
441
459
  .join('\n');
442
460
  };
443
461
 
444
- const isGeminiReady = () => Boolean(String(process.env.GEMINI_API_KEY || '').trim());
462
+ const isGeminiReady = () => {
463
+ const config = getAiHelpConfig();
464
+ return isGeminiAuthReady({
465
+ authMode: config.llm.geminiAuthMode,
466
+ apiKey: process.env.GEMINI_API_KEY,
467
+ cliCommand: process.env.GEMINI_CLI_COMMAND || 'gemini',
468
+ });
469
+ };
445
470
  const isOpenAIReady = () => Boolean(String(process.env.OPENAI_API_KEY || '').trim());
446
471
  const isProviderReady = (provider) => (provider === 'gemini' ? isGeminiReady() : isOpenAIReady());
447
472
 
@@ -453,12 +478,16 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
453
478
  const getGeminiService = () => {
454
479
  if (!isGeminiReady()) return null;
455
480
  const config = getAiHelpConfig();
456
- if (!cachedGeminiService) {
481
+ const currentServiceKey = `${config.llm.model}|${config.llm.timeoutMs}|${config.llm.geminiAuthMode}|${process.env.GEMINI_CLI_COMMAND || 'gemini'}|${Boolean(String(process.env.GEMINI_API_KEY || '').trim())}`;
482
+ if (!cachedGeminiService || cachedGeminiServiceKey !== currentServiceKey) {
457
483
  cachedGeminiService = createGeminiTextService({
458
484
  apiKey: process.env.GEMINI_API_KEY,
459
485
  defaultModel: config.llm.model || process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
460
486
  timeoutMs: config.llm.timeoutMs,
487
+ authMode: config.llm.geminiAuthMode,
488
+ cliCommand: process.env.GEMINI_CLI_COMMAND || 'gemini',
461
489
  });
490
+ cachedGeminiServiceKey = currentServiceKey;
462
491
  }
463
492
  return cachedGeminiService;
464
493
  };
@@ -2,7 +2,7 @@ import logger from '#logger';
2
2
  import { findById, upsert } from '../../../database/index.js';
3
3
  import { extractUserIdInfo, resolveUserIdCached, isLidUserId, isWhatsAppUserId } from '../../config/index.js';
4
4
 
5
- const GROUP_METADATA_FIELDS = ['id', 'subject', 'description', 'owner_jid', 'creation', 'participants'];
5
+ const GROUP_METADATA_FIELDS = ['id', 'subject', 'description', 'owner_jid', 'creation', 'participants', 'linked_parent_jid', 'is_community', 'is_community_announce', 'member_add_mode', 'join_approval_mode', 'addressing_mode'];
6
6
 
7
7
  const PARTICIPANT_ACTIONS = new Set(['add', 'remove', 'promote', 'demote', 'modify']);
8
8
 
@@ -285,6 +285,17 @@ export const buildGroupMetadataFromUpdate = (event, existing) => {
285
285
  const currentParticipants = parseParticipantsFromDb(existing?.participants);
286
286
  const participants = event.participants || currentParticipants;
287
287
  const normalizedParticipants = normalizeParticipantsList(participants);
288
+ const resolveExistingValue = (snakeKey, camelKey) => existing?.[snakeKey] ?? existing?.[camelKey];
289
+ const resolveBoolean = (eventValue, snakeKey, camelKey) => {
290
+ if (eventValue !== undefined) return Boolean(eventValue);
291
+ const existingValue = resolveExistingValue(snakeKey, camelKey);
292
+ return existingValue === undefined || existingValue === null ? null : Boolean(existingValue);
293
+ };
294
+
295
+ const linkedParentRaw = event.linkedParent ?? resolveExistingValue('linked_parent_jid', 'linkedParent');
296
+ const linkedParent = typeof linkedParentRaw === 'string' ? linkedParentRaw.trim() || null : linkedParentRaw || null;
297
+ const addressingModeRaw = event.addressingMode ?? resolveExistingValue('addressing_mode', 'addressingMode');
298
+ const addressingMode = typeof addressingModeRaw === 'string' ? addressingModeRaw.trim() || null : addressingModeRaw || null;
288
299
 
289
300
  return {
290
301
  id: event.id,
@@ -293,6 +304,12 @@ export const buildGroupMetadataFromUpdate = (event, existing) => {
293
304
  owner_jid: event.owner ?? existing?.owner_jid,
294
305
  creation: event.creation ?? existing?.creation,
295
306
  participants: normalizedParticipants,
307
+ linked_parent_jid: linkedParent,
308
+ is_community: resolveBoolean(event.isCommunity, 'is_community', 'isCommunity'),
309
+ is_community_announce: resolveBoolean(event.isCommunityAnnounce, 'is_community_announce', 'isCommunityAnnounce'),
310
+ member_add_mode: resolveBoolean(event.memberAddMode, 'member_add_mode', 'memberAddMode'),
311
+ join_approval_mode: resolveBoolean(event.joinApprovalMode, 'join_approval_mode', 'joinApprovalMode'),
312
+ addressing_mode: addressingMode,
296
313
  };
297
314
  };
298
315
 
@@ -308,4 +325,10 @@ export const buildGroupMetadataFromGroup = (group) => ({
308
325
  owner_jid: group.owner,
309
326
  creation: group.creation,
310
327
  participants: normalizeParticipantsList(group.participants || []),
328
+ linked_parent_jid: typeof group.linkedParent === 'string' ? group.linkedParent.trim() || null : group.linkedParent || null,
329
+ is_community: group.isCommunity === undefined ? null : Boolean(group.isCommunity),
330
+ is_community_announce: group.isCommunityAnnounce === undefined ? null : Boolean(group.isCommunityAnnounce),
331
+ member_add_mode: group.memberAddMode === undefined ? null : Boolean(group.memberAddMode),
332
+ join_approval_mode: group.joinApprovalMode === undefined ? null : Boolean(group.joinApprovalMode),
333
+ addressing_mode: typeof group.addressingMode === 'string' ? group.addressingMode.trim() || null : group.addressingMode || null,
311
334
  });
@@ -105,7 +105,7 @@ const INVALID_SURROGATE_REGEX = /surrogate pair/i;
105
105
  const messageQueue = [];
106
106
 
107
107
  /**
108
- * Conjunto de IDs de mensagens que estão enfileiradas, para evitar duplicação.
108
+ * Conjunto de IDs de mensagens já enfileiradas no formato `${session_id}:${message_id}`.
109
109
  * @type {Set<string>}
110
110
  */
111
111
  const messagePendingIds = new Set();
@@ -132,7 +132,7 @@ const chatCache = new Map();
132
132
 
133
133
  /**
134
134
  * Fila em memória com eventos do Baileys pendentes de persistência.
135
- * @type {Array<{event_name:string, socket_generation:(number|null), chat_id:(string|null), message_id:(string|null), participant_id:(string|null), payload_summary:(string|null), event_timestamp:Date}>}
135
+ * @type {Array<{session_id:string, event_name:string, socket_generation:(number|null), chat_id:(string|null), message_id:(string|null), participant_id:(string|null), payload_summary:(string|null), event_timestamp:Date}>}
136
136
  */
137
137
  const baileysEventQueue = [];
138
138
 
@@ -251,8 +251,8 @@ const isInvalidJsonPayloadError = (error) => {
251
251
  * - content: remove surrogate inválido
252
252
  * - raw_message: serializa JSON seguro para coluna JSON
253
253
  *
254
- * @param {{message_id:string, chat_id:string, sender_id:string, canonical_sender_id?:(string|null), content:(string|null), raw_message:(Object|string|null), timestamp:(number|string|Date)}} messageData
255
- * @returns {{message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}}
254
+ * @param {{session_id?:(string|null), message_id:string, chat_id:string, sender_id:string, canonical_sender_id?:(string|null), content:(string|null), raw_message:(Object|string|null), timestamp:(number|string|Date), allow_group_write?:(boolean)}} messageData
255
+ * @returns {{session_id:string, message_id:string, chat_id:(string|null), sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}}
256
256
  */
257
257
  const normalizeUserIdForColumn = (value, maxLength = 255) => {
258
258
  if (value === null || value === undefined) return null;
@@ -277,8 +277,13 @@ const resolveCanonicalSenderIdForMessage = (messageData) => {
277
277
 
278
278
  const normalizeMessageForQueue = (messageData) => {
279
279
  const senderId = normalizeUserIdForColumn(messageData?.sender_id, 255);
280
+ const messageId = normalizeTextForColumn(messageData?.message_id, 255);
281
+ const chatId = normalizeTextForColumn(messageData?.chat_id, 255);
280
282
  return {
281
283
  ...messageData,
284
+ session_id: normalizeSessionIdForColumn(messageData?.session_id || messageData?.sessionId),
285
+ message_id: messageId,
286
+ chat_id: chatId,
282
287
  sender_id: senderId,
283
288
  canonical_sender_id: resolveCanonicalSenderIdForMessage({
284
289
  ...messageData,
@@ -302,7 +307,25 @@ const normalizeTimestampForColumn = (value) => {
302
307
  return __timeNow();
303
308
  };
304
309
 
310
+ const normalizeSessionIdForColumn = (value) => {
311
+ const normalized = normalizeTextForColumn(value, 64);
312
+ return normalized || 'default';
313
+ };
314
+
315
+ const isGroupChatId = (chatId) => {
316
+ const normalized = String(chatId || '').trim();
317
+ return normalized.endsWith('@g.us');
318
+ };
319
+
320
+ const buildMessagePendingKey = (sessionId, messageId) => {
321
+ const safeSessionId = normalizeSessionIdForColumn(sessionId);
322
+ const safeMessageId = normalizeTextForColumn(messageId, 255);
323
+ if (!safeMessageId) return null;
324
+ return `${safeSessionId}:${safeMessageId}`;
325
+ };
326
+
305
327
  const normalizeBaileysEventForQueue = (eventData) => ({
328
+ session_id: normalizeSessionIdForColumn(eventData?.session_id || eventData?.sessionId),
306
329
  event_name: normalizeTextForColumn(eventData?.event_name, 64),
307
330
  socket_generation: Number.isFinite(Number(eventData?.socket_generation)) ? Math.max(0, Math.floor(Number(eventData.socket_generation))) : null,
308
331
  chat_id: normalizeTextForColumn(eventData?.chat_id, 255),
@@ -313,14 +336,14 @@ const normalizeBaileysEventForQueue = (eventData) => ({
313
336
  });
314
337
 
315
338
  const insertBaileysEventBatch = async (batch) => {
316
- const placeholders = buildPlaceholders(batch.length, 7);
339
+ const placeholders = buildPlaceholders(batch.length, 8);
317
340
  const params = [];
318
341
  for (const entry of batch) {
319
- params.push(entry.event_name, entry.socket_generation, entry.chat_id, entry.message_id, entry.participant_id, entry.payload_summary, entry.event_timestamp);
342
+ params.push(entry.session_id, entry.event_name, entry.socket_generation, entry.chat_id, entry.message_id, entry.participant_id, entry.payload_summary, entry.event_timestamp);
320
343
  }
321
344
 
322
345
  const sql = `INSERT INTO ${TABLES.BAILEYS_EVENT_JOURNAL}
323
- (event_name, socket_generation, chat_id, message_id, participant_id, payload_summary, event_timestamp)
346
+ (session_id, event_name, socket_generation, chat_id, message_id, participant_id, payload_summary, event_timestamp)
324
347
  VALUES ${placeholders}`;
325
348
 
326
349
  await executeQuery(sql, params);
@@ -329,18 +352,18 @@ const insertBaileysEventBatch = async (batch) => {
329
352
  /**
330
353
  * Executa INSERT IGNORE de um batch de mensagens.
331
354
  *
332
- * @param {Array<{message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
355
+ * @param {Array<{session_id:string, message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
333
356
  * @returns {Promise<void>}
334
357
  */
335
358
  const insertMessageBatch = async (batch) => {
336
- const placeholders = buildPlaceholders(batch.length, 7);
359
+ const placeholders = buildPlaceholders(batch.length, 8);
337
360
  const params = [];
338
361
  for (const message of batch) {
339
- params.push(message.message_id, message.chat_id, message.sender_id, message.canonical_sender_id, message.content, message.raw_message, message.timestamp);
362
+ params.push(message.session_id, message.message_id, message.chat_id, message.sender_id, message.canonical_sender_id, message.content, message.raw_message, message.timestamp);
340
363
  }
341
364
 
342
365
  const sql = `INSERT IGNORE INTO ${TABLES.MESSAGES}
343
- (message_id, chat_id, sender_id, canonical_sender_id, content, raw_message, timestamp)
366
+ (session_id, message_id, chat_id, sender_id, canonical_sender_id, content, raw_message, timestamp)
344
367
  VALUES ${placeholders}`;
345
368
 
346
369
  await executeQuery(sql, params);
@@ -424,12 +447,13 @@ const refreshMessageActivityDailyForBatch = async (batch) => {
424
447
  /**
425
448
  * Remove IDs de mensagens do set de pendentes.
426
449
  *
427
- * @param {Array<{message_id:string}>} batch
450
+ * @param {Array<{session_id:string, message_id:string}>} batch
428
451
  * @returns {void}
429
452
  */
430
453
  const clearPendingMessageIds = (batch) => {
431
454
  for (const message of batch) {
432
- messagePendingIds.delete(message.message_id);
455
+ const key = buildMessagePendingKey(message?.session_id, message?.message_id);
456
+ if (key) messagePendingIds.delete(key);
433
457
  }
434
458
  };
435
459
 
@@ -438,7 +462,7 @@ const clearPendingMessageIds = (batch) => {
438
462
  * - Mensagem inválida é descartada para não travar a fila inteira.
439
463
  * - Em erro transitório, re-enfileira o restante e interrompe.
440
464
  *
441
- * @param {Array<{message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
465
+ * @param {Array<{session_id:string, message_id:string, chat_id:string, sender_id:(string|null), canonical_sender_id:(string|null), content:(string|null), raw_message:(string|null), timestamp:(number|string|Date)}>} batch
442
466
  * @returns {Promise<void>}
443
467
  */
444
468
  const salvageJsonErrorBatch = async (batch) => {
@@ -679,26 +703,32 @@ const baileysEventFlushRunner = createFlushRunner({
679
703
 
680
704
  /**
681
705
  * Enfileira uma mensagem para INSERT no banco (INSERT IGNORE).
682
- * - Evita duplicar message_id usando um Set.
706
+ * - Evita duplicar por `${session_id}:${message_id}` usando um Set.
683
707
  * - Força flush se a fila estiver muito grande.
684
708
  * - Agenda flush quando atinge o tamanho de batch.
685
709
  *
686
- * @param {{message_id:string, chat_id:string, sender_id:string, canonical_sender_id?:(string|null), content:(string|null), raw_message:(Object|string|null), timestamp:(number|string)}} messageData
710
+ * @param {{session_id?:(string|null), message_id:string, chat_id:string, sender_id:string, canonical_sender_id?:(string|null), content:(string|null), raw_message:(Object|string|null), timestamp:(number|string), allow_group_write?:(boolean)}} messageData
687
711
  * Objeto com os campos necessários para persistência.
688
712
  * @returns {boolean} true se foi enfileirada; false se inválida/duplicada.
689
713
  */
690
714
  export function queueMessageInsert(messageData) {
691
- if (!messageData?.message_id) return false;
692
- if (messagePendingIds.has(messageData.message_id)) return false;
693
-
694
715
  const normalizedMessage = normalizeMessageForQueue(messageData);
716
+ if (!normalizedMessage?.message_id) return false;
717
+
718
+ if (isGroupChatId(normalizedMessage.chat_id) && normalizedMessage.allow_group_write === false) {
719
+ return false;
720
+ }
721
+
722
+ const pendingKey = buildMessagePendingKey(normalizedMessage.session_id, normalizedMessage.message_id);
723
+ if (!pendingKey) return false;
724
+ if (messagePendingIds.has(pendingKey)) return false;
695
725
 
696
726
  if (messageQueue.length >= MESSAGE_QUEUE_MAX) {
697
727
  logger.warn('Fila de mensagens cheia, forçando flush.', { size: messageQueue.length });
698
728
  scheduleFlush();
699
729
  }
700
730
 
701
- messagePendingIds.add(normalizedMessage.message_id);
731
+ messagePendingIds.add(pendingKey);
702
732
  messageQueue.push(normalizedMessage);
703
733
  updateQueueMetrics();
704
734
 
@@ -782,7 +812,7 @@ export function queueChatUpdate(chat, options = {}) {
782
812
  /**
783
813
  * Enfileira um evento resumido do Baileys para o journal de auditoria.
784
814
  *
785
- * @param {{event_name:string, socket_generation?:(number|null), chat_id?:(string|null), message_id?:(string|null), participant_id?:(string|null), payload_summary?:any, event_timestamp?:(string|number|Date)}} eventData
815
+ * @param {{session_id?:(string|null), event_name:string, socket_generation?:(number|null), chat_id?:(string|null), message_id?:(string|null), participant_id?:(string|null), payload_summary?:any, event_timestamp?:(string|number|Date)}} eventData
786
816
  * @returns {boolean}
787
817
  */
788
818
  export function queueBaileysEventInsert(eventData) {