@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
@@ -1,8 +1,9 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import OpenAI from 'openai';
4
5
  import { getAiHelpCachedResponse, upsertAiHelpCachedResponse } from './aiHelpResponseCacheRepository.js';
5
- import { createGeminiTextService, DEFAULT_GEMINI_MODEL } from './geminiService.js';
6
+ import { createGeminiTextService, DEFAULT_GEMINI_MODEL, isGeminiAuthReady } from './geminiService.js';
6
7
 
7
8
  const DEFAULT_FAQ_INTERVAL_MS = 6 * 60 * 60 * 1000;
8
9
  const DEFAULT_MAX_RESPONSE_CHARS = 3400;
@@ -168,6 +169,16 @@ const normalizeLlmProvider = (value, fallback = 'gemini') => {
168
169
  return fallback;
169
170
  };
170
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
+
171
182
  const looksLikeGeminiModel = (value) =>
172
183
  String(value || '')
173
184
  .trim()
@@ -240,7 +251,13 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
240
251
  const cachePathValue = String(faq.cache_file || '').trim();
241
252
  const cachePath = cachePathValue ? path.resolve(process.cwd(), cachePathValue) : path.join(process.cwd(), 'data', 'cache', `${moduleKey}-ai-faq-cache.json`);
242
253
 
243
- 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');
244
261
  const defaultModelByProvider = provider === 'gemini' ? process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL : process.env.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
245
262
  const rawModel = String(envValue('MODEL') || llm.model || defaultModelByProvider).trim() || defaultModelByProvider;
246
263
  const modelFromEnv = String(envValue('MODEL') || '').trim();
@@ -270,6 +287,7 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
270
287
  .trim()
271
288
  .toLowerCase() !== 'false',
272
289
  provider,
290
+ geminiAuthMode,
273
291
  model: resolvedModel,
274
292
  maxResponseChars: Math.max(400, toPositiveInt(envValue('MAX_RESPONSE_CHARS') || llm.max_response_chars, DEFAULT_MAX_RESPONSE_CHARS, 400)),
275
293
  maxAgentContextChars: Math.max(2_000, toPositiveInt(envValue('MAX_AGENT_CONTEXT_CHARS') || llm.max_agent_context_chars, DEFAULT_MAX_AGENT_CONTEXT_CHARS, 2_000)),
@@ -285,6 +303,7 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
285
303
  let faqGenerationPromise = null;
286
304
  let cachedOpenAIClient = null;
287
305
  let cachedGeminiService = null;
306
+ let cachedGeminiServiceKey = '';
288
307
 
289
308
  const createEmptyCache = () => ({
290
309
  version: FAQ_CACHE_VERSION,
@@ -420,13 +439,14 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
420
439
 
421
440
  const readAgentExcerpt = async () => {
422
441
  const { llm } = getAiHelpConfig();
442
+ const explicitExcerpt = clampText(process.env[`${envPrefix}_AGENT_EXCERPT`], llm.maxAgentContextChars);
443
+ if (explicitExcerpt) return explicitExcerpt;
423
444
  if (!agentMdPath) return '';
424
- try {
425
- const raw = await fs.readFile(agentMdPath, 'utf8');
426
- return raw.slice(0, llm.maxAgentContextChars);
427
- } catch {
428
- return '';
429
- }
445
+
446
+ const normalizedAgentPath = String(agentMdPath).trim();
447
+ if (!normalizedAgentPath) return '';
448
+ const safeAgentName = path.basename(normalizedAgentPath);
449
+ return clampText(`Considere as orientacoes locais do arquivo ${safeAgentName}.`, llm.maxAgentContextChars);
430
450
  };
431
451
 
432
452
  const summarizeConfigForPrompt = () => {
@@ -439,7 +459,14 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
439
459
  .join('\n');
440
460
  };
441
461
 
442
- 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
+ };
443
470
  const isOpenAIReady = () => Boolean(String(process.env.OPENAI_API_KEY || '').trim());
444
471
  const isProviderReady = (provider) => (provider === 'gemini' ? isGeminiReady() : isOpenAIReady());
445
472
 
@@ -451,12 +478,16 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
451
478
  const getGeminiService = () => {
452
479
  if (!isGeminiReady()) return null;
453
480
  const config = getAiHelpConfig();
454
- 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) {
455
483
  cachedGeminiService = createGeminiTextService({
456
484
  apiKey: process.env.GEMINI_API_KEY,
457
485
  defaultModel: config.llm.model || process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
458
486
  timeoutMs: config.llm.timeoutMs,
487
+ authMode: config.llm.geminiAuthMode,
488
+ cliCommand: process.env.GEMINI_CLI_COMMAND || 'gemini',
459
489
  });
490
+ cachedGeminiServiceKey = currentServiceKey;
460
491
  }
461
492
  return cachedGeminiService;
462
493
  };
@@ -578,7 +609,7 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
578
609
  ...cache.metrics,
579
610
  [metricKey]: Number(cache.metrics?.[metricKey] || 0) + 1,
580
611
  };
581
- cache.updatedAt = new Date().toISOString();
612
+ cache.updatedAt = __timeNowIso();
582
613
  await writeCache(cache);
583
614
  };
584
615
 
@@ -590,7 +621,7 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
590
621
  const normalizedSource = normalizeCacheSource(source);
591
622
  const normalizedAnswer = clampText(answer, config.llm.maxResponseChars);
592
623
  if (!normalizedAnswer) return;
593
- const now = new Date().toISOString();
624
+ const now = __timeNowIso();
594
625
 
595
626
  const cache = await readCache();
596
627
  cache.questionCache[key] = {
@@ -738,10 +769,10 @@ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', e
738
769
  }
739
770
 
740
771
  const cache = await readCache();
741
- const now = new Date().toISOString();
772
+ const now = __timeNowIso();
742
773
 
743
774
  if (!force && cache.generatedAt) {
744
- const ageMs = Date.now() - new Date(cache.generatedAt).getTime();
775
+ const ageMs = __timeNowMs() - new Date(cache.generatedAt).getTime();
745
776
  if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < config.faq.intervalMs) {
746
777
  const commandCount = Object.keys(cache.faqByCommand || {}).length;
747
778
  const faqCount = Object.values(cache.faqByCommand || {}).reduce((acc, list) => acc + (Array.isArray(list) ? list.length : 0), 0);
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import logger from '#logger';
2
3
  import { getToolRecord } from './moduleToolRegistryService.js';
3
4
  import { mapToolArgsToCommandText } from './commandToolBuilderService.js';
@@ -388,7 +389,7 @@ export const executeTool = async (toolName, toolArgs, context = {}) => {
388
389
  }
389
390
 
390
391
  const mapped = mapToolArgsToCommandText(record.argumentSpecs, argsValidation.normalizedArgs);
391
- const startedAt = Date.now();
392
+ const startedAt = __timeNowMs();
392
393
 
393
394
  let executionResult = null;
394
395
  try {
@@ -409,7 +410,7 @@ export const executeTool = async (toolName, toolArgs, context = {}) => {
409
410
  };
410
411
  }
411
412
 
412
- const executionTimeMs = Date.now() - startedAt;
413
+ const executionTimeMs = __timeNowMs() - startedAt;
413
414
  logger.info('Execucao de tool AI concluida.', {
414
415
  action: 'ai_tool_execution',
415
416
  tool_used: record.toolName,
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import logger from '#logger';
@@ -126,7 +127,7 @@ const buildRegistrySnapshot = () => {
126
127
  }
127
128
 
128
129
  cachedRegistry = {
129
- builtAt: new Date().toISOString(),
130
+ builtAt: __timeNowIso(),
130
131
  signature,
131
132
  records: records.sort((a, b) => a.toolName.localeCompare(b.toolName)),
132
133
  toolNameToRecord,
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import natural from 'natural';
2
3
  import winkBm25TextSearch from 'wink-bm25-text-search';
3
4
  import logger from '#logger';
@@ -193,7 +194,7 @@ const ensureCacheSize = () => {
193
194
  }
194
195
  };
195
196
 
196
- const pruneCache = (nowMs = Date.now()) => {
197
+ const pruneCache = (nowMs = __timeNowMs()) => {
197
198
  for (const [cacheKey, cacheEntry] of queryCache.entries()) {
198
199
  if (!cacheEntry || cacheEntry.expiresAt <= nowMs) {
199
200
  queryCache.delete(cacheKey);
@@ -290,7 +291,7 @@ const buildOrGetIndexSnapshot = () => {
290
291
 
291
292
  cachedIndexSnapshot = {
292
293
  signature,
293
- builtAt: new Date().toISOString(),
294
+ builtAt: __timeNowIso(),
294
295
  bm25Ready,
295
296
  bm25Engine,
296
297
  entries,
@@ -445,7 +446,7 @@ const buildCommandConfigOverlayMaps = (states = []) => {
445
446
  };
446
447
 
447
448
  const refreshLearnedKnowledgeCache = async ({ force = false } = {}) => {
448
- const nowMs = Date.now();
449
+ const nowMs = __timeNowMs();
449
450
  if (!force && learnedKnowledgeCache.loaded && learnedKnowledgeCache.nextRefreshAt > nowMs && learnedKnowledgeCache.version) {
450
451
  return learnedKnowledgeCache;
451
452
  }
@@ -498,7 +499,7 @@ const refreshLearnedKnowledgeCache = async ({ force = false } = {}) => {
498
499
  };
499
500
 
500
501
  const refreshCommandConfigEnrichmentCache = async ({ force = false } = {}) => {
501
- const nowMs = Date.now();
502
+ const nowMs = __timeNowMs();
502
503
  if (!force && commandConfigEnrichmentCache.loaded && commandConfigEnrichmentCache.nextRefreshAt > nowMs && commandConfigEnrichmentCache.version) {
503
504
  return commandConfigEnrichmentCache;
504
505
  }
@@ -593,7 +594,7 @@ export const selectCandidateTools = async (userMessage, limit = TOOL_SELECTION_M
593
594
  }
594
595
 
595
596
  const normalizedMessage = normalizeText(userMessage);
596
- const nowMs = Date.now();
597
+ const nowMs = __timeNowMs();
597
598
  pruneCache(nowMs);
598
599
  const cacheKey = buildCacheKey({
599
600
  message: normalizedMessage,
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { executeQuery, TABLES } from '../../../database/index.js';
2
3
  import { normalizeJid } from '../../config/index.js';
3
4
  import { toWhatsAppPhoneDigits } from './whatsappLoginLinkService.js';
@@ -21,7 +22,7 @@ const normalizeCacheKey = ({ ownerJid = '', ownerPhone = '' }) => {
21
22
  const getCachedGoogleLinkStatus = (cacheKey) => {
22
23
  const cached = googleLinkCheckCache.get(cacheKey);
23
24
  if (!cached) return null;
24
- if (Number(cached.expiresAt || 0) <= Date.now()) {
25
+ if (Number(cached.expiresAt || 0) <= __timeNowMs()) {
25
26
  googleLinkCheckCache.delete(cacheKey);
26
27
  return null;
27
28
  }
@@ -31,7 +32,7 @@ const getCachedGoogleLinkStatus = (cacheKey) => {
31
32
  const setCachedGoogleLinkStatus = (cacheKey, linked) => {
32
33
  googleLinkCheckCache.set(cacheKey, {
33
34
  linked: Boolean(linked),
34
- expiresAt: Date.now() + GOOGLE_LINK_CHECK_CACHE_TTL_MS,
35
+ expiresAt: __timeNowMs() + GOOGLE_LINK_CHECK_CACHE_TTL_MS,
35
36
  });
36
37
  };
37
38
 
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { createHmac, timingSafeEqual } from 'node:crypto';
2
3
  import { URL } from 'node:url';
3
4
  import { getJidServer, getJidUser, normalizeJid } from '../../config/index.js';
@@ -103,7 +104,7 @@ export const toWhatsAppOwnerJid = (value) => {
103
104
  return normalizeJid(`${digits}@s.whatsapp.net`) || '';
104
105
  };
105
106
 
106
- export const buildWhatsAppLoginHint = (value, { nowMs = Date.now() } = {}) => {
107
+ export const buildWhatsAppLoginHint = (value, { nowMs = __timeNowMs() } = {}) => {
107
108
  const phoneDigits = toWhatsAppPhoneDigits(value);
108
109
  if (!phoneDigits) return null;
109
110
 
@@ -141,7 +142,7 @@ export const extractWhatsAppLoginHint = (payload = {}) => {
141
142
  };
142
143
  };
143
144
 
144
- export const resolveWhatsAppOwnerJidFromLoginPayload = (payload, { nowMs = Date.now() } = {}) => {
145
+ export const resolveWhatsAppOwnerJidFromLoginPayload = (payload, { nowMs = __timeNowMs() } = {}) => {
145
146
  const hint = extractWhatsAppLoginHint(payload);
146
147
  const hasPayload = Boolean(hint.wa || hint.wa_ts || hint.wa_sig);
147
148
  if (!hasPayload) {
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import axios from 'axios';
2
3
  import logger from '#logger';
3
4
  import { recordPokeApiCacheHit } from '../../observability/metrics.js';
@@ -9,7 +10,7 @@ const REQUEST_TIMEOUT_MS = Math.max(3_000, Number(process.env.POKEAPI_TIMEOUT_MS
9
10
  const REQUEST_RETRY_ATTEMPTS = Math.max(0, Number(process.env.POKEAPI_RETRY_ATTEMPTS) || 2);
10
11
  const REQUEST_RETRY_BASE_DELAY_MS = Math.max(120, Number(process.env.POKEAPI_RETRY_BASE_DELAY_MS) || 350);
11
12
  const DNS_FAMILY = [0, 4, 6].includes(Number(process.env.POKEAPI_DNS_FAMILY)) ? Number(process.env.POKEAPI_DNS_FAMILY) : 4;
12
- const REQUEST_USER_AGENT = String(process.env.POKEAPI_USER_AGENT || 'omnizap-system/2.1 (+https://github.com/Omnizap-System/omnizap)').trim();
13
+ const REQUEST_USER_AGENT = String(process.env.POKEAPI_USER_AGENT || 'omnizap/2.1 (+https://github.com/Omnizap-System/omnizap)').trim();
13
14
  const DEFAULT_LORE_LANGUAGES = String(process.env.POKEAPI_LORE_LANGS || 'pt-br,pt,en')
14
15
  .split(',')
15
16
  .map((entry) =>
@@ -177,7 +178,7 @@ const cleanupExpiredEntry = (key, now) => {
177
178
  };
178
179
 
179
180
  const requestResource = async ({ path, cacheKey }) => {
180
- const now = Date.now();
181
+ const now = __timeNowMs();
181
182
  const staleEntry = sharedCache.get(cacheKey);
182
183
  const staleData = staleEntry?.data || null;
183
184
  const cached = cleanupExpiredEntry(cacheKey, now);
@@ -207,7 +208,7 @@ const requestResource = async ({ path, cacheKey }) => {
207
208
  const data = response?.data;
208
209
  sharedCache.set(cacheKey, {
209
210
  data,
210
- expiresAt: Date.now() + CACHE_TTL_MS,
211
+ expiresAt: __timeNowMs() + CACHE_TTL_MS,
211
212
  });
212
213
  return data;
213
214
  } catch (error) {
@@ -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
  });
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import logger from '#logger';
2
3
  import { executeQuery, TABLES } from '../../../database/index.js';
3
4
  import { queueLidUpdate, flushLidQueue, resolveUserIdCached } from '../../config/index.js';
@@ -104,7 +105,7 @@ const INVALID_SURROGATE_REGEX = /surrogate pair/i;
104
105
  const messageQueue = [];
105
106
 
106
107
  /**
107
- * 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}`.
108
109
  * @type {Set<string>}
109
110
  */
110
111
  const messagePendingIds = new Set();
@@ -131,7 +132,7 @@ const chatCache = new Map();
131
132
 
132
133
  /**
133
134
  * Fila em memória com eventos do Baileys pendentes de persistência.
134
- * @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}>}
135
136
  */
136
137
  const baileysEventQueue = [];
137
138
 
@@ -250,8 +251,8 @@ const isInvalidJsonPayloadError = (error) => {
250
251
  * - content: remove surrogate inválido
251
252
  * - raw_message: serializa JSON seguro para coluna JSON
252
253
  *
253
- * @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
254
- * @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)}}
255
256
  */
256
257
  const normalizeUserIdForColumn = (value, maxLength = 255) => {
257
258
  if (value === null || value === undefined) return null;
@@ -276,8 +277,13 @@ const resolveCanonicalSenderIdForMessage = (messageData) => {
276
277
 
277
278
  const normalizeMessageForQueue = (messageData) => {
278
279
  const senderId = normalizeUserIdForColumn(messageData?.sender_id, 255);
280
+ const messageId = normalizeTextForColumn(messageData?.message_id, 255);
281
+ const chatId = normalizeTextForColumn(messageData?.chat_id, 255);
279
282
  return {
280
283
  ...messageData,
284
+ session_id: normalizeSessionIdForColumn(messageData?.session_id || messageData?.sessionId),
285
+ message_id: messageId,
286
+ chat_id: chatId,
281
287
  sender_id: senderId,
282
288
  canonical_sender_id: resolveCanonicalSenderIdForMessage({
283
289
  ...messageData,
@@ -298,10 +304,28 @@ const normalizeTimestampForColumn = (value) => {
298
304
  if (value instanceof Date && Number.isFinite(value.getTime())) return value;
299
305
  const parsed = new Date(value);
300
306
  if (Number.isFinite(parsed.getTime())) return parsed;
301
- return new Date();
307
+ return __timeNow();
308
+ };
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}`;
302
325
  };
303
326
 
304
327
  const normalizeBaileysEventForQueue = (eventData) => ({
328
+ session_id: normalizeSessionIdForColumn(eventData?.session_id || eventData?.sessionId),
305
329
  event_name: normalizeTextForColumn(eventData?.event_name, 64),
306
330
  socket_generation: Number.isFinite(Number(eventData?.socket_generation)) ? Math.max(0, Math.floor(Number(eventData.socket_generation))) : null,
307
331
  chat_id: normalizeTextForColumn(eventData?.chat_id, 255),
@@ -312,14 +336,14 @@ const normalizeBaileysEventForQueue = (eventData) => ({
312
336
  });
313
337
 
314
338
  const insertBaileysEventBatch = async (batch) => {
315
- const placeholders = buildPlaceholders(batch.length, 7);
339
+ const placeholders = buildPlaceholders(batch.length, 8);
316
340
  const params = [];
317
341
  for (const entry of batch) {
318
- 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);
319
343
  }
320
344
 
321
345
  const sql = `INSERT INTO ${TABLES.BAILEYS_EVENT_JOURNAL}
322
- (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)
323
347
  VALUES ${placeholders}`;
324
348
 
325
349
  await executeQuery(sql, params);
@@ -328,18 +352,18 @@ const insertBaileysEventBatch = async (batch) => {
328
352
  /**
329
353
  * Executa INSERT IGNORE de um batch de mensagens.
330
354
  *
331
- * @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
332
356
  * @returns {Promise<void>}
333
357
  */
334
358
  const insertMessageBatch = async (batch) => {
335
- const placeholders = buildPlaceholders(batch.length, 7);
359
+ const placeholders = buildPlaceholders(batch.length, 8);
336
360
  const params = [];
337
361
  for (const message of batch) {
338
- 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);
339
363
  }
340
364
 
341
365
  const sql = `INSERT IGNORE INTO ${TABLES.MESSAGES}
342
- (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)
343
367
  VALUES ${placeholders}`;
344
368
 
345
369
  await executeQuery(sql, params);
@@ -423,12 +447,13 @@ const refreshMessageActivityDailyForBatch = async (batch) => {
423
447
  /**
424
448
  * Remove IDs de mensagens do set de pendentes.
425
449
  *
426
- * @param {Array<{message_id:string}>} batch
450
+ * @param {Array<{session_id:string, message_id:string}>} batch
427
451
  * @returns {void}
428
452
  */
429
453
  const clearPendingMessageIds = (batch) => {
430
454
  for (const message of batch) {
431
- messagePendingIds.delete(message.message_id);
455
+ const key = buildMessagePendingKey(message?.session_id, message?.message_id);
456
+ if (key) messagePendingIds.delete(key);
432
457
  }
433
458
  };
434
459
 
@@ -437,7 +462,7 @@ const clearPendingMessageIds = (batch) => {
437
462
  * - Mensagem inválida é descartada para não travar a fila inteira.
438
463
  * - Em erro transitório, re-enfileira o restante e interrompe.
439
464
  *
440
- * @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
441
466
  * @returns {Promise<void>}
442
467
  */
443
468
  const salvageJsonErrorBatch = async (batch) => {
@@ -513,7 +538,7 @@ const flushMessageQueueCore = async () => {
513
538
 
514
539
  const flushChatQueueCore = async () => {
515
540
  while (chatQueue.size > 0) {
516
- const now = Date.now();
541
+ const now = __timeNowMs();
517
542
  const ready = [];
518
543
  for (const entry of chatQueue.values()) {
519
544
  if (now < entry.nextAllowedAt) continue;
@@ -539,7 +564,7 @@ const flushChatQueueCore = async () => {
539
564
 
540
565
  try {
541
566
  await executeQuery(sql, params);
542
- const writeAt = Date.now();
567
+ const writeAt = __timeNowMs();
543
568
  for (const entry of ready) {
544
569
  const current = chatQueue.get(entry.id);
545
570
  const cache = chatCache.get(entry.id) || {};
@@ -576,7 +601,7 @@ const flushChatQueueCore = async () => {
576
601
  const pruneBaileysEventJournal = async () => {
577
602
  if (BAILEYS_EVENT_JOURNAL_RETENTION_DAYS <= 0) return;
578
603
 
579
- const now = Date.now();
604
+ const now = __timeNowMs();
580
605
  if (now < nextBaileysEventPruneAt) return;
581
606
  nextBaileysEventPruneAt = now + BAILEYS_EVENT_JOURNAL_PRUNE_INTERVAL_MS;
582
607
 
@@ -678,26 +703,32 @@ const baileysEventFlushRunner = createFlushRunner({
678
703
 
679
704
  /**
680
705
  * Enfileira uma mensagem para INSERT no banco (INSERT IGNORE).
681
- * - Evita duplicar message_id usando um Set.
706
+ * - Evita duplicar por `${session_id}:${message_id}` usando um Set.
682
707
  * - Força flush se a fila estiver muito grande.
683
708
  * - Agenda flush quando atinge o tamanho de batch.
684
709
  *
685
- * @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
686
711
  * Objeto com os campos necessários para persistência.
687
712
  * @returns {boolean} true se foi enfileirada; false se inválida/duplicada.
688
713
  */
689
714
  export function queueMessageInsert(messageData) {
690
- if (!messageData?.message_id) return false;
691
- if (messagePendingIds.has(messageData.message_id)) return false;
692
-
693
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;
694
725
 
695
726
  if (messageQueue.length >= MESSAGE_QUEUE_MAX) {
696
727
  logger.warn('Fila de mensagens cheia, forçando flush.', { size: messageQueue.length });
697
728
  scheduleFlush();
698
729
  }
699
730
 
700
- messagePendingIds.add(normalizedMessage.message_id);
731
+ messagePendingIds.add(pendingKey);
701
732
  messageQueue.push(normalizedMessage);
702
733
  updateQueueMetrics();
703
734
 
@@ -725,7 +756,7 @@ export function queueMessageInsert(messageData) {
725
756
  export function queueChatUpdate(chat, options = {}) {
726
757
  if (!chat || !chat.id) return false;
727
758
 
728
- const now = Date.now();
759
+ const now = __timeNowMs();
729
760
  const isPartial = Boolean(options.partial);
730
761
  const forceName = Boolean(options.forceName);
731
762
  const cache = chatCache.get(chat.id) || {
@@ -781,7 +812,7 @@ export function queueChatUpdate(chat, options = {}) {
781
812
  /**
782
813
  * Enfileira um evento resumido do Baileys para o journal de auditoria.
783
814
  *
784
- * @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
785
816
  * @returns {boolean}
786
817
  */
787
818
  export function queueBaileysEventInsert(eventData) {
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { createHash } from 'node:crypto';
2
3
 
3
4
  import logger from '#logger';
@@ -65,7 +66,7 @@ const loadFlagsFromDatabase = async () => {
65
66
  };
66
67
 
67
68
  export const refreshFeatureFlags = async ({ force = false } = {}) => {
68
- const now = Date.now();
69
+ const now = __timeNowMs();
69
70
  const isFresh = now - cacheState.loadedAt < FEATURE_FLAG_CACHE_TTL_MS;
70
71
  if (!force && isFresh) return cacheState.byName;
71
72
 
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import logger from '#logger';
2
3
  import { getJidUser } from '../../config/index.js';
3
4
  import { isUserAdmin, updateGroupParticipants } from '../../config/index.js';
@@ -200,7 +201,7 @@ const handleCaptchaTimeout = async (groupId, userId) => {
200
201
  const entry = groupMap?.get(userId);
201
202
  if (!entry) return;
202
203
 
203
- if (Date.now() < entry.expiresAt) {
204
+ if (__timeNowMs() < entry.expiresAt) {
204
205
  return;
205
206
  }
206
207
 
@@ -377,7 +378,7 @@ export const registerCaptchaChallenge = ({ groupId, participantJid, messageKey,
377
378
  cleanupMessageStateForEntry(existingMatch.entry);
378
379
  }
379
380
 
380
- const expiresAt = Date.now() + CAPTCHA_TIMEOUT_MS;
381
+ const expiresAt = __timeNowMs() + CAPTCHA_TIMEOUT_MS;
381
382
  const messageId = messageKey?.id || null;
382
383
  const normalizedText = normalizeMessageText(messageText);
383
384
  const messageStateKey = messageId && normalizedText ? buildMessageStateKey(groupId, messageId) : null;