@omnizap-system/omnizap 2.5.12

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 (425) hide show
  1. package/.clusterfuzzlite/Dockerfile +10 -0
  2. package/.env.example +907 -0
  3. package/.github/codeql/codeql-config.yml +10 -0
  4. package/.github/dependabot.yml +35 -0
  5. package/.github/workflows/ci.yml +73 -0
  6. package/.github/workflows/codeql.yml +106 -0
  7. package/.github/workflows/db-migration-check.yml +98 -0
  8. package/.github/workflows/dependency-review.yml +22 -0
  9. package/.github/workflows/deploy.yml +95 -0
  10. package/.github/workflows/release.yml +106 -0
  11. package/.github/workflows/security-attest-provenance.yml +51 -0
  12. package/.github/workflows/security-gitleaks.yml +34 -0
  13. package/.github/workflows/security-runner-hardening.yml +31 -0
  14. package/.github/workflows/security-scorecard.yml +44 -0
  15. package/.github/workflows/security-zap-baseline.yml +44 -0
  16. package/.github/workflows/security-zap-full-scan.yml +43 -0
  17. package/.github/workflows/security-zizmor.yml +36 -0
  18. package/.github/workflows/wiki-sync.yml +44 -0
  19. package/.gitleaks.toml +15 -0
  20. package/.prettierrc +34 -0
  21. package/CODE_OF_CONDUCT.md +114 -0
  22. package/LICENSE +56 -0
  23. package/README.md +110 -0
  24. package/SECURITY.md +110 -0
  25. package/app/config/index.js +4 -0
  26. package/app/configParts/adminIdentity.js +92 -0
  27. package/app/configParts/baileysConfig.js +1818 -0
  28. package/app/configParts/groupUtils.js +692 -0
  29. package/app/configParts/loggerConfig.js +394 -0
  30. package/app/configParts/messagePersistenceService.js +305 -0
  31. package/app/connection/baileysCompatibility.test.js +40 -0
  32. package/app/connection/baileysDbAuthState.js +344 -0
  33. package/app/connection/socketController.js +2243 -0
  34. package/app/controllers/messageController.js +7 -0
  35. package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
  36. package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
  37. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
  38. package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
  39. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
  40. package/app/controllers/messageProcessingPipeline.js +699 -0
  41. package/app/modules/adminModule/AGENT.md +4056 -0
  42. package/app/modules/adminModule/adminAiHelpService.js +56 -0
  43. package/app/modules/adminModule/adminConfigRuntime.js +177 -0
  44. package/app/modules/adminModule/commandConfig.json +7122 -0
  45. package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
  46. package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
  47. package/app/modules/adminModule/groupEventHandlers.js +399 -0
  48. package/app/modules/aiModule/AGENT.md +547 -0
  49. package/app/modules/aiModule/aiAiHelpService.js +14 -0
  50. package/app/modules/aiModule/aiConfigRuntime.js +135 -0
  51. package/app/modules/aiModule/catCommand.js +967 -0
  52. package/app/modules/aiModule/commandConfig.json +981 -0
  53. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  54. package/app/modules/gameModule/AGENT.md +196 -0
  55. package/app/modules/gameModule/commandConfig.json +366 -0
  56. package/app/modules/gameModule/diceCommand.js +42 -0
  57. package/app/modules/gameModule/gameAiHelpService.js +14 -0
  58. package/app/modules/gameModule/gameConfigRuntime.js +68 -0
  59. package/app/modules/menuModule/AGENT.md +205 -0
  60. package/app/modules/menuModule/commandConfig.json +366 -0
  61. package/app/modules/menuModule/common.js +316 -0
  62. package/app/modules/menuModule/menuAiHelpService.js +14 -0
  63. package/app/modules/menuModule/menuConfigRuntime.js +68 -0
  64. package/app/modules/menuModule/menus.js +66 -0
  65. package/app/modules/playModule/AGENT.md +321 -0
  66. package/app/modules/playModule/commandConfig.json +584 -0
  67. package/app/modules/playModule/playAiHelpService.js +14 -0
  68. package/app/modules/playModule/playCommand.js +1417 -0
  69. package/app/modules/playModule/playConfigRuntime.js +68 -0
  70. package/app/modules/quoteModule/AGENT.md +199 -0
  71. package/app/modules/quoteModule/commandConfig.json +366 -0
  72. package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
  73. package/app/modules/quoteModule/quoteCommand.js +842 -0
  74. package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
  75. package/app/modules/rpgPokemonModule/AGENT.md +229 -0
  76. package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
  77. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
  78. package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
  79. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
  80. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  81. package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
  82. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
  83. package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
  84. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  85. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  86. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  87. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  88. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
  89. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
  90. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  91. package/app/modules/statsModule/AGENT.md +320 -0
  92. package/app/modules/statsModule/commandConfig.json +540 -0
  93. package/app/modules/statsModule/globalRankingCommand.js +64 -0
  94. package/app/modules/statsModule/rankingCommand.js +41 -0
  95. package/app/modules/statsModule/rankingCommon.js +1305 -0
  96. package/app/modules/statsModule/statsAiHelpService.js +14 -0
  97. package/app/modules/statsModule/statsConfigRuntime.js +68 -0
  98. package/app/modules/stickerModule/AGENT.md +692 -0
  99. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  100. package/app/modules/stickerModule/commandConfig.json +1216 -0
  101. package/app/modules/stickerModule/convertToWebp.js +367 -0
  102. package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
  103. package/app/modules/stickerModule/stickerCommand.js +446 -0
  104. package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
  105. package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
  106. package/app/modules/stickerModule/stickerTextCommand.js +653 -0
  107. package/app/modules/stickerPackModule/AGENT.md +215 -0
  108. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  109. package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
  110. package/app/modules/stickerPackModule/commandConfig.json +387 -0
  111. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
  112. package/app/modules/stickerPackModule/domainEvents.js +52 -0
  113. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
  114. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
  115. package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
  116. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
  117. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  118. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
  119. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
  120. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
  121. package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
  122. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
  123. package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
  124. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
  125. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
  126. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  127. package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
  128. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
  129. package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
  130. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
  131. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  132. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
  133. package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
  134. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
  135. package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
  136. package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
  137. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
  138. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
  139. package/app/modules/stickerPackModule/stickerPackService.js +733 -0
  140. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
  141. package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
  142. package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
  143. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
  144. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
  145. package/app/modules/systemMetricsModule/AGENT.md +193 -0
  146. package/app/modules/systemMetricsModule/commandConfig.json +344 -0
  147. package/app/modules/systemMetricsModule/pingCommand.js +399 -0
  148. package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
  149. package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
  150. package/app/modules/tiktokModule/AGENT.md +196 -0
  151. package/app/modules/tiktokModule/commandConfig.json +366 -0
  152. package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
  153. package/app/modules/tiktokModule/tiktokCommand.js +716 -0
  154. package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
  155. package/app/modules/userModule/AGENT.md +200 -0
  156. package/app/modules/userModule/commandConfig.json +386 -0
  157. package/app/modules/userModule/userAiHelpService.js +14 -0
  158. package/app/modules/userModule/userCommand.js +1155 -0
  159. package/app/modules/userModule/userConfigRuntime.js +68 -0
  160. package/app/modules/waifuPicsModule/AGENT.md +431 -0
  161. package/app/modules/waifuPicsModule/commandConfig.json +780 -0
  162. package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
  163. package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
  164. package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
  165. package/app/observability/metrics.js +766 -0
  166. package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
  167. package/app/services/ai/aiLearningRepository.js +400 -0
  168. package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
  169. package/app/services/ai/commandConfigEnrichmentService.js +452 -0
  170. package/app/services/ai/commandConfigValidationService.js +443 -0
  171. package/app/services/ai/commandToolBuilderService.js +192 -0
  172. package/app/services/ai/conversationRouterService.js +516 -0
  173. package/app/services/ai/geminiService.js +115 -0
  174. package/app/services/ai/geminiService.test.js +87 -0
  175. package/app/services/ai/globalModuleAiHelpService.js +1412 -0
  176. package/app/services/ai/globalToolCallingService.js +203 -0
  177. package/app/services/ai/messageCommandExecutionService.js +391 -0
  178. package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
  179. package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
  180. package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
  181. package/app/services/ai/moduleToolExecutorService.js +464 -0
  182. package/app/services/ai/moduleToolRegistryService.js +178 -0
  183. package/app/services/ai/toolCandidateSelectorService.js +781 -0
  184. package/app/services/auth/googleWebLinkService.js +80 -0
  185. package/app/services/auth/whatsappLoginLinkService.js +230 -0
  186. package/app/services/external/pokeApiService.js +398 -0
  187. package/app/services/group/groupMetadataService.js +311 -0
  188. package/app/services/infra/dbWriteQueue.js +874 -0
  189. package/app/services/infra/featureFlagService.js +131 -0
  190. package/app/services/infra/queueUtils.js +55 -0
  191. package/app/services/messaging/captchaService.js +491 -0
  192. package/app/services/messaging/messagePersistenceService.js +1 -0
  193. package/app/services/messaging/newsBroadcastService.js +347 -0
  194. package/app/services/sticker/stickerFocusService.js +347 -0
  195. package/app/services/sticker/stickerFocusService.test.js +43 -0
  196. package/app/store/aiPromptStore.js +38 -0
  197. package/app/store/conversationSessionStore.js +131 -0
  198. package/app/store/groupConfigStore.js +58 -0
  199. package/app/store/premiumUserStore.js +54 -0
  200. package/app/utils/antiLink/antiLinkModule.js +700 -0
  201. package/app/utils/http/getImageBufferModule.js +18 -0
  202. package/app/utils/json/jsonSanitizer.js +113 -0
  203. package/app/utils/json/jsonSanitizer.test.js +40 -0
  204. package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
  205. package/app/workers/aiLearningWorker.js +605 -0
  206. package/app/workers/commandConfigEnrichmentWorker.js +242 -0
  207. package/database/index.js +2075 -0
  208. package/database/init.js +151 -0
  209. package/database/migrations/.gitkeep +0 -0
  210. package/database/migrations/20260307_d0_hardening_down.sql +64 -0
  211. package/database/migrations/20260307_d0_hardening_up.sql +79 -0
  212. package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
  213. package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
  214. package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
  215. package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
  216. package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
  217. package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
  218. package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
  219. package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
  220. package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
  221. package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
  222. package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
  223. package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
  224. package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
  225. package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
  226. package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
  227. package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
  228. package/database/schema.sql +1186 -0
  229. package/docker-compose.yml +104 -0
  230. package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
  231. package/docs/audits/stickerCatalogController-symbols.md +58 -0
  232. package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
  233. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
  234. package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
  235. package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
  236. package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
  237. package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
  238. package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
  239. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
  240. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
  241. package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
  242. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
  243. package/docs/seo/satellite-page-template.md +116 -0
  244. package/docs/seo/satellite-pages-phase1.json +364 -0
  245. package/docs/wiki/Home.md +120 -0
  246. package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
  247. package/docs/wiki/recent-changes-2026-03-08.md +47 -0
  248. package/ecosystem.prod.config.cjs +135 -0
  249. package/eslint.config.js +89 -0
  250. package/index.js +488 -0
  251. package/ml/clip_classifier/Dockerfile +18 -0
  252. package/ml/clip_classifier/README.md +118 -0
  253. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  254. package/ml/clip_classifier/classifier.py +654 -0
  255. package/ml/clip_classifier/embedding_store.py +481 -0
  256. package/ml/clip_classifier/env_loader.py +15 -0
  257. package/ml/clip_classifier/llm_label_expander.py +144 -0
  258. package/ml/clip_classifier/main.py +213 -0
  259. package/ml/clip_classifier/requirements.txt +10 -0
  260. package/ml/clip_classifier/similarity_engine.py +74 -0
  261. package/new-logo.png +0 -0
  262. package/observability/alert-rules.yml +60 -0
  263. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  264. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  265. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  266. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  267. package/observability/loki-config.yml +38 -0
  268. package/observability/mysql-setup.sql +46 -0
  269. package/observability/prometheus.yml +35 -0
  270. package/observability/promtail-config.yml +84 -0
  271. package/observability/sticker-catalog-slo.md +83 -0
  272. package/observability/sticker-scale-hardening-rollout.md +128 -0
  273. package/package.json +144 -0
  274. package/public/apple-touch-icon.png +0 -0
  275. package/public/assets/css/commands-react.input.css +71 -0
  276. package/public/assets/css/create-pack-react.input.css +31 -0
  277. package/public/assets/css/home-react.input.css +106 -0
  278. package/public/assets/css/login-react.input.css +58 -0
  279. package/public/assets/css/stickers-react.input.css +18 -0
  280. package/public/assets/css/terms-react.input.css +115 -0
  281. package/public/assets/css/user-react.input.css +57 -0
  282. package/public/assets/images/brand-icon-192.png +0 -0
  283. package/public/assets/images/brand-logo-128.webp +0 -0
  284. package/public/assets/images/hero-banner-1280.jpg +0 -0
  285. package/public/comandos/commands-catalog.json +4517 -0
  286. package/public/css/api-docs.css +161 -0
  287. package/public/css/stickers-admin.css +1288 -0
  288. package/public/css/styles.css +679 -0
  289. package/public/css/systemadm/admin.css +474 -0
  290. package/public/css/systemadm/base.css +73 -0
  291. package/public/css/systemadm/components.css +662 -0
  292. package/public/css/systemadm/layout.css +229 -0
  293. package/public/css/systemadm/tokens.css +56 -0
  294. package/public/favicon-16x16.png +0 -0
  295. package/public/favicon-32x32.png +0 -0
  296. package/public/favicon.ico +0 -0
  297. package/public/js/apps/apiDocsApp.js +235 -0
  298. package/public/js/apps/commandsReactApp.js +528 -0
  299. package/public/js/apps/createPackApp.js +1646 -0
  300. package/public/js/apps/homeReactApp.js +942 -0
  301. package/public/js/apps/loginReactApp.js +496 -0
  302. package/public/js/apps/stickersAdminApp.js +1753 -0
  303. package/public/js/apps/stickersApp.js +3797 -0
  304. package/public/js/apps/termsReactApp.js +528 -0
  305. package/public/js/apps/userApp.js +2540 -0
  306. package/public/js/apps/userProfile/actions.js +66 -0
  307. package/public/js/apps/userReactApp.js +547 -0
  308. package/public/js/catalog.js +950 -0
  309. package/public/pages/api-docs.html +40 -0
  310. package/public/pages/aup.html +158 -0
  311. package/public/pages/comandos.html +41 -0
  312. package/public/pages/dpa.html +227 -0
  313. package/public/pages/home.html +45 -0
  314. package/public/pages/licenca.html +182 -0
  315. package/public/pages/login.html +40 -0
  316. package/public/pages/notice-and-takedown.html +234 -0
  317. package/public/pages/politica-de-privacidade.html +251 -0
  318. package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
  319. package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
  320. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
  321. package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
  322. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
  323. package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
  324. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
  325. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
  326. package/public/pages/stickers-admin.html +31 -0
  327. package/public/pages/stickers-create.html +41 -0
  328. package/public/pages/stickers.html +45 -0
  329. package/public/pages/suboperadores.html +237 -0
  330. package/public/pages/termos-de-uso-texto-integral.html +241 -0
  331. package/public/pages/termos-de-uso.html +41 -0
  332. package/public/pages/user-password-reset.html +32 -0
  333. package/public/pages/user-systemadm.html +508 -0
  334. package/public/pages/user.html +39 -0
  335. package/public/robots.txt +9 -0
  336. package/public/site.webmanifest +24 -0
  337. package/public/sitemap.xml +98 -0
  338. package/schemas/command-config.schema.json +582 -0
  339. package/scripts/baileys-compat-smoke.mjs +12 -0
  340. package/scripts/cache-bust.mjs +142 -0
  341. package/scripts/deploy.sh +916 -0
  342. package/scripts/email-broadcast-terms-update.mjs +170 -0
  343. package/scripts/enrich-command-discovery-fields.mjs +286 -0
  344. package/scripts/generate-command-config-schema.mjs +273 -0
  345. package/scripts/generate-commands-catalog.mjs +308 -0
  346. package/scripts/generate-module-agents.mjs +631 -0
  347. package/scripts/generate-seo-satellite-pages.mjs +400 -0
  348. package/scripts/github-deploy-notify.mjs +174 -0
  349. package/scripts/github-release-notify.mjs +219 -0
  350. package/scripts/release.sh +599 -0
  351. package/scripts/run-codeql-local.sh +116 -0
  352. package/scripts/run-prettier-all.mjs +25 -0
  353. package/scripts/security-smoketest.mjs +581 -0
  354. package/scripts/sticker-catalog-loadtest.mjs +210 -0
  355. package/scripts/sticker-worker-task.mjs +119 -0
  356. package/scripts/sync-readme-snapshot.mjs +133 -0
  357. package/scripts/validate-command-config-schema.mjs +130 -0
  358. package/scripts/validate-command-configs.mjs +15 -0
  359. package/scripts/wiki-sync.sh +191 -0
  360. package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
  361. package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
  362. package/server/auth/jwt/webJwtService.js +147 -0
  363. package/server/auth/stickerCatalogAuthContext.js +165 -0
  364. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
  365. package/server/auth/userPassword/index.js +14 -0
  366. package/server/auth/userPassword/userPasswordAuthService.js +422 -0
  367. package/server/auth/userPassword/userPasswordCrypto.js +199 -0
  368. package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
  369. package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
  370. package/server/auth/validation/authSchemas.js +236 -0
  371. package/server/auth/webAccount/webAccountHandlers.js +1434 -0
  372. package/server/controllers/admin/adminBanService.js +138 -0
  373. package/server/controllers/admin/adminPanelHandlers.js +2083 -0
  374. package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
  375. package/server/controllers/admin/systemAdminController.js +201 -0
  376. package/server/controllers/email/emailAutomationController.js +239 -0
  377. package/server/controllers/metricsController.js +21 -0
  378. package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
  379. package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
  380. package/server/controllers/sticker/stickerCatalogController.js +4700 -0
  381. package/server/controllers/system/contactController.js +115 -0
  382. package/server/controllers/system/githubController.js +137 -0
  383. package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
  384. package/server/controllers/system/storageController.js +154 -0
  385. package/server/controllers/system/systemController.js +135 -0
  386. package/server/controllers/system/systemMetricsController.js +156 -0
  387. package/server/controllers/system/visitController.js +90 -0
  388. package/server/controllers/userController.js +145 -0
  389. package/server/email/emailAutomationRuntime.js +225 -0
  390. package/server/email/emailAutomationService.js +125 -0
  391. package/server/email/emailOutboxRepository.js +282 -0
  392. package/server/email/emailTemplateService.js +480 -0
  393. package/server/email/emailTransportService.js +156 -0
  394. package/server/http/clientIp.js +95 -0
  395. package/server/http/httpRequestUtils.js +262 -0
  396. package/server/http/httpRequestUtils.test.js +80 -0
  397. package/server/http/httpServer.js +180 -0
  398. package/server/http/requestContext.js +20 -0
  399. package/server/http/siteRoutingUtils.js +87 -0
  400. package/server/index.js +1 -0
  401. package/server/middleware/cachePolicy.js +26 -0
  402. package/server/middleware/cachePolicyHelpers.js +1 -0
  403. package/server/middleware/endpointRateLimit.js +181 -0
  404. package/server/middleware/rateLimit.js +70 -0
  405. package/server/middleware/requireAdminAuth.js +48 -0
  406. package/server/middleware/securityHeaders.js +97 -0
  407. package/server/routes/admin/systemAdminRouter.js +64 -0
  408. package/server/routes/email/emailAutomationRouter.js +46 -0
  409. package/server/routes/health/healthRouter.js +41 -0
  410. package/server/routes/indexRouter.js +234 -0
  411. package/server/routes/metrics/metricsRouter.js +58 -0
  412. package/server/routes/static/staticPageRouter.js +134 -0
  413. package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
  414. package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
  415. package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
  416. package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
  417. package/server/routes/sticker/catalogRouter.js +77 -0
  418. package/server/routes/sticker/stickerApiRouter.js +84 -0
  419. package/server/routes/sticker/stickerDataRouter.js +145 -0
  420. package/server/routes/sticker/stickerSiteRouter.js +43 -0
  421. package/server/routes/user/userApiPaths.js +66 -0
  422. package/server/routes/user/userRouter.js +65 -0
  423. package/server/utils/safePath.js +26 -0
  424. package/utils/logger/loggerModule.js +35 -0
  425. package/vite.config.mjs +38 -0
@@ -0,0 +1,1818 @@
1
+ /* eslint-disable no-unused-vars */
2
+ /* eslint-disable no-useless-escape */
3
+ import { fetchLatestBaileysVersion, downloadContentFromMessage, jidNormalizedUser, jidEncode, jidDecode, areJidsSameUser, normalizeMessageContent, isJidMetaAI, isPnUser, isLidUser, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidNewsletter, isHostedPnUser, isHostedLidUser, isJidBot, SERVER_JID, PSA_WID, STORIES_JID, META_AI_JID, delay } from '@whiskeysockets/baileys';
4
+
5
+ import { baileysConfigLogger as logger } from './loggerConfig.js';
6
+ import { executeQuery, TABLES } from '../../database/index.js';
7
+ import { buildRowPlaceholders, createFlushRunner } from '../services/infra/queueUtils.js';
8
+ import { recordError, setQueueDepth } from '../observability/metrics.js';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { createWriteStream } from 'node:fs';
11
+ import path from 'node:path';
12
+ import { pipeline } from 'node:stream/promises';
13
+ import { Readable } from 'node:stream';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const DEFAULT_BAILEYS_VERSION = [7, 0, 0];
17
+
18
+ let activeSocket = null;
19
+
20
+ export const setActiveSocket = (socket) => {
21
+ activeSocket = socket;
22
+ };
23
+
24
+ export const getActiveSocket = () => activeSocket;
25
+
26
+ /**
27
+ * Indica se uma instância de socket está aberta para operações.
28
+ * @param {object|null|undefined} socket Instância de socket.
29
+ * @returns {boolean}
30
+ */
31
+ export const isSocketOpen = (socket) => {
32
+ if (!socket?.ws) return false;
33
+ if (typeof socket.ws.isOpen === 'boolean') return socket.ws.isOpen;
34
+ return socket.ws.readyState === 1;
35
+ };
36
+
37
+ /**
38
+ * Indica se o socket ativo está aberto para operações.
39
+ * @returns {boolean}
40
+ */
41
+ export const isActiveSocketOpen = () => isSocketOpen(activeSocket);
42
+
43
+ /**
44
+ * Executa um método em uma instância de socket validando disponibilidade.
45
+ * @param {object|null|undefined} socket Instância de socket.
46
+ * @param {string} methodName Nome do método no socket.
47
+ * @param {...any} args Argumentos do método.
48
+ * @returns {Promise<any>}
49
+ */
50
+ export const runSocketMethod = async (socket, methodName, ...args) => {
51
+ if (!isSocketOpen(socket)) {
52
+ throw new Error(`Socket do WhatsApp indisponível para "${methodName}".`);
53
+ }
54
+
55
+ const method = socket?.[methodName];
56
+ if (typeof method !== 'function') {
57
+ throw new Error(`Método "${methodName}" não disponível no socket informado.`);
58
+ }
59
+
60
+ return method.apply(socket, args);
61
+ };
62
+
63
+ /**
64
+ * Executa um método do socket ativo após validar disponibilidade.
65
+ * @param {string} methodName Nome do método no socket.
66
+ * @param {...any} args Argumentos do método.
67
+ * @returns {Promise<any>}
68
+ */
69
+ export const runActiveSocketMethod = async (methodName, ...args) => runSocketMethod(activeSocket, methodName, ...args);
70
+
71
+ /**
72
+ * Recupera a blocklist da conta conectada.
73
+ * @returns {Promise<(string|undefined)[]>}
74
+ */
75
+ export const fetchBlocklistFromActiveSocket = async () => runActiveSocketMethod('fetchBlocklist');
76
+
77
+ /**
78
+ * Recupera URL da foto de perfil via socket ativo.
79
+ * @param {string} jid JID alvo.
80
+ * @param {'preview'|'image'} [type='image'] Resolução da imagem.
81
+ * @param {number} [timeoutMs] Timeout opcional da query.
82
+ * @returns {Promise<string|null>}
83
+ */
84
+ export const profilePictureUrlFromActiveSocket = async (jid, type = 'image', timeoutMs) => {
85
+ const url = await runActiveSocketMethod('profilePictureUrl', jid, type, timeoutMs);
86
+ return typeof url === 'string' && url.trim() ? url : null;
87
+ };
88
+
89
+ /**
90
+ * Constantes de JID expostas pelo Baileys para facilitar comparações.
91
+ * @type {{SERVER_JID: string, PSA_WID: string, STORIES_JID: string, META_AI_JID: string}}
92
+ */
93
+ export const JID_CONSTANTS = {
94
+ SERVER_JID,
95
+ PSA_WID,
96
+ STORIES_JID,
97
+ META_AI_JID,
98
+ };
99
+
100
+ /**
101
+ * Servidores válidos para IDs de usuário WhatsApp (PN).
102
+ * Exportado para evitar duplicação de regras em outros serviços.
103
+ * @type {ReadonlySet<string>}
104
+ */
105
+ export const WHATSAPP_USER_JID_SERVERS = new Set(['s.whatsapp.net', 'c.us', 'hosted']);
106
+
107
+ /**
108
+ * Servidores válidos para IDs do tipo LID.
109
+ * Exportado para evitar duplicação de regras em outros serviços.
110
+ * @type {ReadonlySet<string>}
111
+ */
112
+ export const LID_USER_JID_SERVERS = new Set(['lid', 'hosted.lid']);
113
+
114
+ const decodeJidParts = (() => {
115
+ let lastJid = null;
116
+ let lastDecoded = null;
117
+
118
+ return (jid) => {
119
+ if (!jid) return null;
120
+ if (jid === lastJid) return lastDecoded;
121
+ const decoded = jidDecode(jid) || null;
122
+ lastJid = jid;
123
+ lastDecoded = decoded;
124
+ return decoded;
125
+ };
126
+ })();
127
+
128
+ /**
129
+ * Tipos de mensagem conhecidos do Baileys
130
+ * Mapeamento de chaves do proto.Message para tipos normalizados
131
+ */
132
+ export const MEDIA_TYPE_MAPPING = {
133
+ conversation: 'text',
134
+ extendedTextMessage: 'text',
135
+ imageMessage: 'image',
136
+ videoMessage: 'video',
137
+ audioMessage: 'audio',
138
+ documentMessage: 'document',
139
+ documentWithCaptionMessage: 'document',
140
+ stickerMessage: 'sticker',
141
+ contactMessage: 'contact',
142
+ contactsArrayMessage: 'contacts',
143
+ locationMessage: 'location',
144
+ liveLocationMessage: 'liveLocation',
145
+ buttonsMessage: 'buttons',
146
+ buttonsResponseMessage: 'buttonsResponse',
147
+ templateMessage: 'template',
148
+ templateButtonReplyMessage: 'buttonReply',
149
+ listMessage: 'list',
150
+ listResponseMessage: 'listResponse',
151
+ ephemeralMessage: 'ephemeral',
152
+ reactionMessage: 'reaction',
153
+ pollCreationMessage: 'poll',
154
+ pollUpdateMessage: 'pollUpdate',
155
+ pollResultSnapshotMessage: 'pollResult',
156
+ invoiceMessage: 'invoice',
157
+ sendPaymentMessage: 'payment',
158
+ requestPaymentMessage: 'paymentRequest',
159
+ cancelPaymentRequestMessage: 'paymentCancel',
160
+ declinePaymentRequestMessage: 'paymentDecline',
161
+ groupInviteMessage: 'groupInvite',
162
+ productMessage: 'product',
163
+ orderMessage: 'order',
164
+ viewOnceMessage: 'viewOnce',
165
+ viewOnceMessageV2: 'viewOnceV2',
166
+ interactiveMessage: 'interactive',
167
+ interactiveResponseMessage: 'interactiveResponse',
168
+ newsletterAdminInviteMessage: 'newsletterInvite',
169
+ eventMessage: 'event',
170
+ requestPhoneNumberMessage: 'requestPhoneNumber',
171
+ call: 'call',
172
+ messageHistoryBundle: 'messageHistoryBundle',
173
+ messageHistoryNotice: 'messageHistoryNotice',
174
+ albumMessage: 'album',
175
+ stickerPackMessage: 'stickerPack',
176
+ highlyStructuredMessage: 'structured',
177
+ fastRatchetKeySenderKeyDistributionMessage: 'keyDistribution',
178
+ deviceSentMessage: 'deviceSent',
179
+ messageContextInfo: 'contextInfo',
180
+ botInvokeMessage: 'botInvoke',
181
+ };
182
+
183
+ /**
184
+ * Tipos de midia que contem conteudo binario/arquivo
185
+ */
186
+ export const BINARY_MEDIA_TYPES = new Set(['image', 'video', 'videoNote', 'audio', 'voice', 'document', 'sticker']);
187
+
188
+ const normalizeMessage = (message) => normalizeMessageContent(message) || message;
189
+
190
+ const MESSAGE_CONTENT_WRAPPER_KEYS = ['ephemeralMessage', 'viewOnceMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension', 'deviceSentMessage', 'documentWithCaptionMessage', 'botInvokeMessage', 'editedMessage', 'keepInChatMessage'];
191
+
192
+ const resolveSingleWrapperMessage = (node) => {
193
+ if (!node || typeof node !== 'object') return null;
194
+
195
+ const keys = Object.keys(node);
196
+ if (keys.length !== 1) return null;
197
+
198
+ const wrapperValue = node[keys[0]];
199
+ if (wrapperValue && typeof wrapperValue === 'object' && wrapperValue.message && typeof wrapperValue.message === 'object') {
200
+ return wrapperValue.message;
201
+ }
202
+
203
+ return null;
204
+ };
205
+
206
+ const unwrapMessageContent = (message, maxDepth = 8) => {
207
+ let current = normalizeMessage(message);
208
+ const visited = new Set();
209
+
210
+ for (let depth = 0; depth < maxDepth; depth += 1) {
211
+ if (!current || typeof current !== 'object') break;
212
+ if (visited.has(current)) break;
213
+ visited.add(current);
214
+
215
+ let next = null;
216
+ for (const wrapperKey of MESSAGE_CONTENT_WRAPPER_KEYS) {
217
+ const wrapperMessage = current?.[wrapperKey]?.message;
218
+ if (wrapperMessage && typeof wrapperMessage === 'object') {
219
+ next = wrapperMessage;
220
+ break;
221
+ }
222
+ }
223
+
224
+ if (!next && current.message && typeof current.message === 'object') {
225
+ next = current.message;
226
+ }
227
+
228
+ if (!next) {
229
+ next = resolveSingleWrapperMessage(current);
230
+ }
231
+
232
+ if (!next || next === current) break;
233
+ current = normalizeMessage(next);
234
+ }
235
+
236
+ return current || message;
237
+ };
238
+
239
+ const hasNonEmptyMediaKey = (mediaKey) => {
240
+ if (!mediaKey) return false;
241
+
242
+ if (typeof mediaKey === 'string') {
243
+ return mediaKey.trim().length > 0;
244
+ }
245
+
246
+ if (Buffer.isBuffer(mediaKey) || mediaKey instanceof Uint8Array) {
247
+ return mediaKey.length > 0;
248
+ }
249
+
250
+ if (Array.isArray(mediaKey)) {
251
+ return mediaKey.length > 0;
252
+ }
253
+
254
+ if (typeof mediaKey === 'object') {
255
+ if (typeof mediaKey.byteLength === 'number') {
256
+ return mediaKey.byteLength > 0;
257
+ }
258
+ return Object.keys(mediaKey).length > 0;
259
+ }
260
+
261
+ return Boolean(mediaKey);
262
+ };
263
+
264
+ const buildMediaEntry = (mediaType, messageKey, value, isQuoted, overrides = {}) => ({
265
+ mediaType,
266
+ mediaKey: value,
267
+ messageKey,
268
+ isQuoted,
269
+ isBinary: BINARY_MEDIA_TYPES.has(mediaType),
270
+ hasUrl: !!value.url,
271
+ hasDirectPath: !!value.directPath,
272
+ hasMediaKey: !!value.mediaKey,
273
+ hasFileEncSha256: !!value.fileEncSha256,
274
+ mimetype: value.mimetype || null,
275
+ fileLength: value.fileLength || null,
276
+ fileName: value.fileName || null,
277
+ caption: value.caption || null,
278
+ ...overrides,
279
+ });
280
+
281
+ const collectMediaFromMessage = (message, { includeQuoted = true } = {}) => {
282
+ if (!message || !message.message) {
283
+ return [];
284
+ }
285
+
286
+ const messageContent = message.message;
287
+ let allMedia = detectAllMediaTypes(messageContent, false);
288
+
289
+ if (includeQuoted) {
290
+ const quotedMessage = messageContent?.extendedTextMessage?.contextInfo?.quotedMessage;
291
+ if (quotedMessage) {
292
+ allMedia = allMedia.concat(detectAllMediaTypes(quotedMessage, true));
293
+ }
294
+ }
295
+
296
+ return allMedia;
297
+ };
298
+
299
+ const filterMedia = (media, { includeAllTypes = false, includeUnknown = false } = {}) => {
300
+ let filtered = media;
301
+
302
+ if (!includeAllTypes) {
303
+ filtered = filtered.filter((item) => item.isBinary);
304
+ }
305
+
306
+ if (!includeUnknown) {
307
+ filtered = filtered.filter((item) => !item.isUnknownType);
308
+ }
309
+
310
+ return filtered;
311
+ };
312
+
313
+ const findExpiration = (root) => {
314
+ if (!root || typeof root !== 'object') return null;
315
+
316
+ const stack = [root];
317
+ const visited = new WeakSet();
318
+
319
+ while (stack.length > 0) {
320
+ const current = stack.pop();
321
+ if (!current || typeof current !== 'object') continue;
322
+ if (visited.has(current)) continue;
323
+ visited.add(current);
324
+
325
+ const expiration = current.contextInfo?.expiration;
326
+ if (typeof expiration === 'number') return expiration;
327
+
328
+ for (const value of Object.values(current)) {
329
+ if (value && typeof value === 'object') {
330
+ stack.push(value);
331
+ }
332
+ }
333
+ }
334
+
335
+ return null;
336
+ };
337
+
338
+ const getMediaExtension = (type) => {
339
+ if (type === 'image') return 'jpeg';
340
+ if (type === 'video') return 'mp4';
341
+ if (type === 'audio') return 'mp3';
342
+ return 'bin';
343
+ };
344
+
345
+ const isBadDecryptError = (error) => {
346
+ if (!error || typeof error !== 'object') return false;
347
+ if (error.code === 'ERR_OSSL_BAD_DECRYPT') return true;
348
+
349
+ const message = String(error.message || '').toLowerCase();
350
+ const reason = String(error.reason || '').toLowerCase();
351
+ const opensslStack = Array.isArray(error.opensslErrorStack) ? error.opensslErrorStack.join(' ').toLowerCase() : '';
352
+
353
+ return message.includes('bad decrypt') || reason.includes('bad decrypt') || opensslStack.includes('bad decrypt');
354
+ };
355
+
356
+ /**
357
+ * Converte a versão do Baileys (string ou array) para o formato `[major, minor, patch]`.
358
+ * @param {string|number[]|null|undefined} rawVersion - Valor bruto informado na variável de ambiente.
359
+ * @returns {number[]|null} Retorna a versão normalizada ou `null` se inválida.
360
+ */
361
+ function parseBaileysVersion(rawVersion) {
362
+ if (!rawVersion) {
363
+ return null;
364
+ }
365
+
366
+ const cleaned = String(rawVersion).replace(/[\[\]\s]/g, '');
367
+ const parts = cleaned
368
+ .split(/[.,]/)
369
+ .filter(Boolean)
370
+ .map((value) => Number(value));
371
+
372
+ if (parts.length < 3 || parts.some((value) => Number.isNaN(value))) {
373
+ return null;
374
+ }
375
+
376
+ return parts.slice(0, 3);
377
+ }
378
+
379
+ /**
380
+ * Codifica um usuário no formato JID aceito pelo WhatsApp.
381
+ * @param {string|number|null|undefined} user - Identificador do usuário.
382
+ * @param {string} [server='c.us'] - Domínio do servidor JID.
383
+ * @param {number} [device] - ID do dispositivo quando aplicável.
384
+ * @returns {string|null} JID codificado ou `null` para entrada inválida.
385
+ */
386
+ export function encodeJid(user, server = 'c.us', device) {
387
+ if (user === null || user === undefined) return null;
388
+ return jidEncode(user, server, device);
389
+ }
390
+
391
+ /**
392
+ * Decodifica um JID em partes (`user`, `server`, `device`) usando cache simples.
393
+ * @param {string} jid - JID completo.
394
+ * @returns {{user?: string, server?: string, domainType?: number, device?: number}|null} Partes do JID ou `null`.
395
+ */
396
+ export function decodeJid(jid) {
397
+ if (!jid) return null;
398
+ return decodeJidParts(jid);
399
+ }
400
+
401
+ /**
402
+ * Converte strings de env para boolean de forma tolerante.
403
+ * Aceita: 1/0, true/false, yes/no, y/n, on/off.
404
+ * @param {unknown} value Valor bruto vindo do process.env.
405
+ * @param {boolean} fallback Valor padrão quando não for possível interpretar.
406
+ * @returns {boolean}
407
+ */
408
+ export const parseEnvBool = (value, fallback) => {
409
+ if (value === undefined || value === null || value === '') return fallback;
410
+ const normalized = String(value).trim().toLowerCase();
411
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
412
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
413
+ return fallback;
414
+ };
415
+
416
+ /**
417
+ * Converte strings de env para inteiro com fallback e clamp.
418
+ * @param {unknown} value Valor bruto.
419
+ * @param {number} fallback Valor padrão.
420
+ * @param {number} min Valor mínimo.
421
+ * @param {number} max Valor máximo.
422
+ * @returns {number}
423
+ */
424
+ export const parseEnvInt = (value, fallback, min, max) => {
425
+ const parsed = Number(value);
426
+ if (!Number.isFinite(parsed)) return fallback;
427
+ return Math.max(min, Math.min(max, Math.floor(parsed)));
428
+ };
429
+
430
+ /**
431
+ * Converte strings CSV em array de strings.
432
+ * @param {unknown} value Valor bruto.
433
+ * @param {string[]} fallback Lista padrão.
434
+ * @returns {string[]}
435
+ */
436
+ export const parseEnvCsv = (value, fallback) => {
437
+ if (value === undefined || value === null || value === '') return [...fallback];
438
+ const parsed = String(value)
439
+ .split(',')
440
+ .map((entry) => String(entry || '').trim())
441
+ .filter(Boolean);
442
+ return parsed.length > 0 ? parsed : [...fallback];
443
+ };
444
+
445
+ /**
446
+ * Estados de presença suportados pelo Baileys (WAPresence).
447
+ * Referência: Types/Chat.d.ts.
448
+ * @type {ReadonlyArray<import('@whiskeysockets/baileys').WAPresence>}
449
+ */
450
+ export const WA_PRESENCE_VALUES = Object.freeze(['unavailable', 'available', 'composing', 'recording', 'paused']);
451
+ const WA_PRESENCE_SET = new Set(WA_PRESENCE_VALUES);
452
+
453
+ /**
454
+ * Verifica se o valor informado é um estado de presença válido.
455
+ * @param {unknown} value
456
+ * @returns {value is import('@whiskeysockets/baileys').WAPresence}
457
+ */
458
+ export const isWAPresence = (value) => {
459
+ if (typeof value !== 'string') return false;
460
+ return WA_PRESENCE_SET.has(value.trim().toLowerCase());
461
+ };
462
+
463
+ /**
464
+ * Normaliza um estado de presença para o formato aceito pelo Baileys.
465
+ * @param {unknown} value
466
+ * @param {import('@whiskeysockets/baileys').WAPresence} [fallback='available']
467
+ * @returns {import('@whiskeysockets/baileys').WAPresence}
468
+ */
469
+ export const normalizeWAPresence = (value, fallback = 'available') => {
470
+ const normalizedFallback = isWAPresence(fallback) ? String(fallback).trim().toLowerCase() : 'available';
471
+ if (!isWAPresence(value)) return /** @type {import('@whiskeysockets/baileys').WAPresence} */ (normalizedFallback);
472
+ return /** @type {import('@whiskeysockets/baileys').WAPresence} */ (String(value).trim().toLowerCase());
473
+ };
474
+
475
+ /**
476
+ * Normaliza um JID para o formato canônico.
477
+ * @param {string} jid - JID de entrada.
478
+ * @returns {string} JID normalizado ou string vazia quando ausente.
479
+ */
480
+ export function normalizeJid(jid) {
481
+ if (!jid) return '';
482
+ return jidNormalizedUser(jid);
483
+ }
484
+
485
+ /**
486
+ * Extrai o identificador do usuário de um JID.
487
+ * @param {string} jid - JID completo.
488
+ * @returns {string|null} Usuário extraído ou `null`.
489
+ */
490
+ export function getJidUser(jid) {
491
+ return decodeJidParts(jid)?.user || null;
492
+ }
493
+
494
+ /**
495
+ * Extrai o servidor de um JID.
496
+ * @param {string} jid - JID completo.
497
+ * @returns {string|null} Servidor extraído ou `null`.
498
+ */
499
+ export function getJidServer(jid) {
500
+ return decodeJidParts(jid)?.server || null;
501
+ }
502
+
503
+ /**
504
+ * Verifica se o JID pertence ao namespace LID.
505
+ * @param {string} jid - JID a validar.
506
+ * @returns {boolean} `true` quando for LID.
507
+ */
508
+ export function isLidJid(jid) {
509
+ const server = getJidServer(jid);
510
+ return Boolean(server && LID_USER_JID_SERVERS.has(server));
511
+ }
512
+
513
+ /**
514
+ * Verifica se o JID pertence ao namespace PN do WhatsApp.
515
+ * @param {string} jid - JID a validar.
516
+ * @returns {boolean} `true` quando for usuário WhatsApp.
517
+ */
518
+ export function isWhatsAppJid(jid) {
519
+ const server = getJidServer(jid);
520
+ return Boolean(server && WHATSAPP_USER_JID_SERVERS.has(server));
521
+ }
522
+
523
+ export const ADDRESSING_MODE_LID = 'lid';
524
+ export const ADDRESSING_MODE_PN = 'pn';
525
+
526
+ /**
527
+ * Normaliza um modo de endereçamento (lid/pn).
528
+ * @param {unknown} value
529
+ * @returns {'lid'|'pn'|undefined}
530
+ */
531
+ export const normalizeAddressingMode = (value) => {
532
+ if (value === undefined || value === null) return undefined;
533
+ const normalized = String(value).trim().toLowerCase();
534
+ if (normalized === ADDRESSING_MODE_LID) return ADDRESSING_MODE_LID;
535
+ if (normalized === ADDRESSING_MODE_PN) return ADDRESSING_MODE_PN;
536
+ return undefined;
537
+ };
538
+
539
+ /**
540
+ * Resolve modo de endereçamento a partir da chave da mensagem.
541
+ * @param {object} [key={}]
542
+ * @param {object} [senderInfo={}]
543
+ * @returns {'lid'|'pn'|undefined}
544
+ */
545
+ export const resolveAddressingModeFromMessageKey = (key = {}, senderInfo = {}) => {
546
+ const explicit = normalizeAddressingMode(key?.addressingMode);
547
+ if (explicit) return explicit;
548
+
549
+ const candidates = [senderInfo?.lid, key?.participant, key?.participantAlt, key?.remoteJid, key?.remoteJidAlt];
550
+ for (const candidate of candidates) {
551
+ const normalized = normalizeJid(String(candidate || '').trim());
552
+ if (!normalized) continue;
553
+ if (isLidJid(normalized)) return ADDRESSING_MODE_LID;
554
+ if (isWhatsAppJid(normalized)) return ADDRESSING_MODE_PN;
555
+ }
556
+
557
+ return undefined;
558
+ };
559
+
560
+ /**
561
+ * Resolve JID canônico de usuário WhatsApp a partir de candidatos.
562
+ * @param {...string} candidates
563
+ * @returns {string}
564
+ */
565
+ export const resolveCanonicalWhatsAppJid = (...candidates) => {
566
+ for (const candidate of candidates) {
567
+ const normalized = normalizeJid(String(candidate || '').trim());
568
+ if (!normalized) continue;
569
+ if (!isWhatsAppJid(normalized)) continue;
570
+ const user = String(getJidUser(normalized) || '')
571
+ .split(':')[0]
572
+ .replace(/\D+/g, '');
573
+ if (!user) continue;
574
+ if (user.length < 10 || user.length > 15) continue;
575
+ return normalizeJid(`${user}@s.whatsapp.net`) || normalized;
576
+ }
577
+ return '';
578
+ };
579
+
580
+ /**
581
+ * Normaliza PN para JID de WhatsApp quando o payload vier sem domínio.
582
+ * @param {string|null|undefined} pn
583
+ * @returns {string|null}
584
+ */
585
+ export const normalizePnToJid = (pn) => {
586
+ if (!pn || typeof pn !== 'string') return null;
587
+ const normalized = pn.trim();
588
+ if (!normalized) return null;
589
+ if (isWhatsAppJid(normalized)) return normalized;
590
+ if (/^\d+(?::\d+)?$/.test(normalized)) return `${normalized}@s.whatsapp.net`;
591
+ return null;
592
+ };
593
+
594
+ /**
595
+ * Verifica se dois JIDs pertencem ao mesmo usuário.
596
+ * @param {string} jid1 - Primeiro JID.
597
+ * @param {string} jid2 - Segundo JID.
598
+ * @returns {boolean} `true` quando representam o mesmo usuário.
599
+ */
600
+ export function isSameJidUser(jid1, jid2) {
601
+ return areJidsSameUser(jid1, jid2);
602
+ }
603
+
604
+ /**
605
+ * Verifica se o JID representa um usuário (PN/LID, hospedado ou não).
606
+ * @param {string} jid - JID a validar.
607
+ * @returns {boolean} `true` quando for JID de usuário.
608
+ */
609
+ export function isUserJid(jid) {
610
+ return Boolean(jid && (isPnUser(jid) || isHostedPnUser(jid) || isLidUser(jid) || isHostedLidUser(jid)));
611
+ }
612
+
613
+ /**
614
+ * Verifica se o JID é de grupo.
615
+ * @param {string} jid - JID a validar.
616
+ * @returns {boolean} `true` quando for grupo.
617
+ */
618
+ export function isGroupJid(jid) {
619
+ return Boolean(jid && isJidGroup(jid));
620
+ }
621
+
622
+ /**
623
+ * Verifica se o JID é de broadcast.
624
+ * @param {string} jid - JID a validar.
625
+ * @returns {boolean} `true` quando for broadcast.
626
+ */
627
+ export function isBroadcastJid(jid) {
628
+ return Boolean(jid && isJidBroadcast(jid));
629
+ }
630
+
631
+ /**
632
+ * Verifica se o JID é do status broadcast.
633
+ * @param {string} jid - JID a validar.
634
+ * @returns {boolean} `true` quando for status.
635
+ */
636
+ export function isStatusJid(jid) {
637
+ return Boolean(jid && isJidStatusBroadcast(jid));
638
+ }
639
+
640
+ /**
641
+ * Verifica se o JID é de newsletter/canal.
642
+ * @param {string} jid - JID a validar.
643
+ * @returns {boolean} `true` quando for newsletter.
644
+ */
645
+ export function isNewsletterJid(jid) {
646
+ return Boolean(jid && isJidNewsletter(jid));
647
+ }
648
+
649
+ /**
650
+ * Verifica se o JID pertence à Meta AI.
651
+ * @param {string} jid - JID a validar.
652
+ * @returns {boolean} `true` quando for Meta AI.
653
+ */
654
+ export function isMetaAiJid(jid) {
655
+ return Boolean(jid && isJidMetaAI(jid));
656
+ }
657
+
658
+ /**
659
+ * Verifica se o JID pertence a um bot.
660
+ * @param {string} jid - JID a validar.
661
+ * @returns {boolean} `true` quando for bot.
662
+ */
663
+ export function isBotJid(jid) {
664
+ return Boolean(jid && isJidBot(jid));
665
+ }
666
+
667
+ /**
668
+ * Resolve o JID do bot a partir do `sock.user.id`.
669
+ * @param {string} sockUserId - ID bruto retornado pelo socket.
670
+ * @returns {string|null} JID normalizado do bot ou `null`.
671
+ */
672
+ export function resolveBotJid(sockUserId) {
673
+ const normalized = normalizeJid(sockUserId);
674
+ if (normalized) return normalized;
675
+ if (!sockUserId || typeof sockUserId !== 'string') return null;
676
+ const rawUser = sockUserId.split(':')[0];
677
+ return encodeJid(rawUser, 's.whatsapp.net');
678
+ }
679
+
680
+ /**
681
+ * Resolve a versão do Baileys com prioridade para `BAILEYS_VERSION`.
682
+ * Se a variável não for válida, tenta buscar a recomendada e aplica fallback local.
683
+ * @returns {Promise<number[]>} Versão no formato `[major, minor, patch]`.
684
+ */
685
+ export async function resolveBaileysVersion() {
686
+ const envVersion = parseBaileysVersion(process.env.BAILEYS_VERSION);
687
+ if (envVersion) {
688
+ return envVersion;
689
+ }
690
+
691
+ if (process.env.BAILEYS_VERSION) {
692
+ logger.warn('Valor invalido em BAILEYS_VERSION; usando fallback local.', {
693
+ provided: process.env.BAILEYS_VERSION,
694
+ });
695
+ }
696
+
697
+ const shouldFetchLatest = parseEnvBool(process.env.BAILEYS_FETCH_LATEST_VERSION, false);
698
+ if (!shouldFetchLatest) {
699
+ return DEFAULT_BAILEYS_VERSION;
700
+ }
701
+
702
+ try {
703
+ const { version } = await fetchLatestBaileysVersion();
704
+ if (Array.isArray(version) && version.length >= 3) {
705
+ return version;
706
+ }
707
+ } catch (error) {
708
+ logger.warn('Falha ao buscar a versao recomendada do Baileys; usando fallback.', {
709
+ error: error.message,
710
+ });
711
+ }
712
+
713
+ return DEFAULT_BAILEYS_VERSION;
714
+ }
715
+
716
+ /**
717
+ * Baixa a foto de perfil associada à mensagem recebida.
718
+ * @param {import('@whiskeysockets/baileys').WASocket} sock - Instância conectada do socket.
719
+ * @param {import('@whiskeysockets/baileys').proto.IWebMessageInfo} msg - Mensagem usada para resolver o JID.
720
+ * @returns {Promise<Buffer|null>} Buffer da imagem ou `null` se indisponível.
721
+ */
722
+ export async function getProfilePicBuffer(sock, msg) {
723
+ const rawJid = msg?.key?.participant || msg?.key?.remoteJid;
724
+ const jid = jidNormalizedUser(rawJid);
725
+
726
+ try {
727
+ const url = await runSocketMethod(sock, 'profilePictureUrl', jid, 'image');
728
+ if (!url) return null;
729
+
730
+ const response = await fetch(url);
731
+ if (!response.ok) return null;
732
+
733
+ const data = await response.arrayBuffer();
734
+ return Buffer.from(data);
735
+ } catch (error) {
736
+ return null;
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Extrai o valor de expiração de uma mensagem do WhatsApp, ou retorna 24 horas (em segundos) por padrão.
742
+ * @param {{message?: object}|null|undefined} sock - Estrutura contendo a propriedade `message`.
743
+ * @returns {number} Tempo de expiração em segundos.
744
+ */
745
+ export function getExpiration(sock) {
746
+ const DEFAULT_EXPIRATION_SECONDS = 24 * 60 * 60;
747
+
748
+ if (!sock || typeof sock !== 'object' || !sock.message) {
749
+ return DEFAULT_EXPIRATION_SECONDS;
750
+ }
751
+
752
+ const normalizedMessage = normalizeMessage(sock.message);
753
+ const expiration = findExpiration(normalizedMessage);
754
+
755
+ return typeof expiration === 'number' ? expiration : DEFAULT_EXPIRATION_SECONDS;
756
+ }
757
+
758
+ /**
759
+ * Extrai o conteúdo de texto de uma mensagem do WhatsApp.
760
+ * @param {{message?: object}} messageInfo - Objeto que contém o payload da mensagem.
761
+ * @returns {string} Conteúdo textual extraído ou descrição do tipo de mensagem.
762
+ */
763
+ export const extractMessageContent = ({ message }) => {
764
+ if (!message) return 'Mensagem vazia';
765
+
766
+ const normalizedMessage = unwrapMessageContent(message);
767
+ if (!normalizedMessage) return 'Mensagem vazia';
768
+
769
+ const text = normalizedMessage.conversation?.trim() || normalizedMessage.extendedTextMessage?.text;
770
+
771
+ if (text) return text;
772
+
773
+ const handlers = [
774
+ [normalizedMessage.imageMessage, (m) => m.caption || '[Imagem]'],
775
+ [normalizedMessage.videoMessage, (m) => m.caption || '[Vídeo]'],
776
+ [normalizedMessage.documentMessage, (m) => m.fileName || '[Documento]'],
777
+ [normalizedMessage.audioMessage, (m) => (m.ptt ? '[Áudio] (voz)' : '[Áudio]')],
778
+ [normalizedMessage.stickerMessage, () => '[Figurinha]'],
779
+ [normalizedMessage.locationMessage, (m) => `[Localização] Lat: ${m.degreesLatitude}, Long: ${m.degreesLongitude}`],
780
+ [normalizedMessage.contactMessage, (m) => `[Contato] ${m.displayName}`],
781
+ [normalizedMessage.contactsArrayMessage, (m) => `[Contatos] ${m.contacts.map((c) => c.displayName).join(', ')}`],
782
+ [normalizedMessage.listMessage, (m) => m.description || '[Mensagem de Lista]'],
783
+ [normalizedMessage.listResponseMessage, (m) => `[Lista] ${m.singleSelectReply?.selectedRowId || m.title || ''}`.trim()],
784
+ [normalizedMessage.buttonsMessage, (m) => m.contentText || '[Mensagem de Botões]'],
785
+ [normalizedMessage.buttonsResponseMessage, (m) => `[Botão] ${m.selectedDisplayText || m.selectedButtonId || ''}`.trim()],
786
+ [normalizedMessage.templateButtonReplyMessage, (m) => `[Resposta de Botão] ${m.selectedDisplayText || ''}`.trim()],
787
+ [normalizedMessage.interactiveResponseMessage, (m) => `[Interativo] ${m.body?.text || m.nativeFlowResponseMessage?.name || ''}`.trim()],
788
+ [normalizedMessage.productMessage, (m) => m.product?.title || '[Mensagem de Produto]'],
789
+ [normalizedMessage.reactionMessage, (m) => `[Reação] ${m.text || ''}`.trim()],
790
+ [normalizedMessage.pollCreationMessage, (m) => `[Enquete] ${m.name}`],
791
+ [normalizedMessage.pollResultSnapshotMessage, (m) => `[Resultado de Enquete] ${m.name || ''}`.trim()],
792
+ [normalizedMessage.requestPhoneNumberMessage, () => '[Solicitação de telefone]'],
793
+ [normalizedMessage.groupInviteMessage, (m) => `[Convite de grupo] ${m.groupName || ''}`.trim()],
794
+ [normalizedMessage.eventMessage, (m) => `[Evento] ${m.name || ''}`.trim()],
795
+ [normalizedMessage.newsletterAdminInviteMessage, () => '[Convite de newsletter]'],
796
+ [normalizedMessage.albumMessage, () => '[Álbum]'],
797
+ [normalizedMessage.stickerPackMessage, () => '[Pacote de figurinhas]'],
798
+ [normalizedMessage.messageHistoryBundle, () => '[Histórico de mensagens]'],
799
+ [normalizedMessage.messageHistoryNotice, () => '[Aviso de histórico de mensagens]'],
800
+ [normalizedMessage.call, () => '[Chamada]'],
801
+ ];
802
+
803
+ for (const [msg, fn] of handlers) {
804
+ if (msg) return fn(msg);
805
+ }
806
+
807
+ return 'Tipo de mensagem não suportado ou sem conteúdo.';
808
+ };
809
+
810
+ /**
811
+ * Faz o download de mídia a partir de uma mensagem do Baileys.
812
+ * @param {import('@whiskeysockets/baileys').WAProto.IMessage} message - Objeto da mídia a ser baixada.
813
+ * @param {string} type - Tipo de mídia (ex.: `image`, `video`, `audio`, `document`).
814
+ * @param {string} outputPath - Diretório onde o arquivo será salvo.
815
+ * @returns {Promise<string|null>} Caminho do arquivo salvo ou `null` em caso de falha.
816
+ */
817
+ export const downloadMediaMessage = async (message, type, outputPath) => {
818
+ if (!message || typeof message !== 'object') {
819
+ logger.warn('Skipping media download: invalid message payload.', { type });
820
+ return null;
821
+ }
822
+
823
+ if (!hasNonEmptyMediaKey(message.mediaKey)) {
824
+ logger.warn('Skipping media download: missing or empty media key.', {
825
+ type,
826
+ hasUrl: Boolean(message.url),
827
+ hasDirectPath: Boolean(message.directPath),
828
+ });
829
+ return null;
830
+ }
831
+
832
+ if (!message.url && !message.directPath) {
833
+ logger.warn('Skipping media download: media URL/directPath not found.', { type });
834
+ return null;
835
+ }
836
+
837
+ try {
838
+ const stream = await downloadContentFromMessage(message, type);
839
+
840
+ const fileId = message.key?.id || Date.now();
841
+ const extension = getMediaExtension(type);
842
+ const fileName = `${Date.now()}-${fileId}.${extension}`;
843
+ const filePath = path.join(outputPath, fileName);
844
+
845
+ await pipeline(Readable.from(stream), createWriteStream(filePath));
846
+ logger.info(`Media downloaded successfully to ${filePath}`);
847
+ return filePath;
848
+ } catch (error) {
849
+ if (error?.message?.includes('Cannot derive from empty media key')) {
850
+ logger.warn('Skipping media download: invalid media key received from source.', { type });
851
+ return null;
852
+ }
853
+
854
+ if (isBadDecryptError(error)) {
855
+ logger.warn('Skipping media download: failed to decrypt media payload from source.', {
856
+ type,
857
+ code: error.code,
858
+ reason: error.reason || null,
859
+ });
860
+ return null;
861
+ }
862
+
863
+ logger.error(`Error downloading media: ${error.message}`, error);
864
+ return null;
865
+ }
866
+ };
867
+
868
+ /**
869
+ * Detecta dinamicamente todos os tipos de midia em um objeto de mensagem
870
+ * @param {object} messageContent - Conteudo da mensagem
871
+ * @param {boolean} isQuoted - Se e de uma mensagem citada
872
+ * @returns {Array} Array de objetos com detalhes da midia encontrada
873
+ */
874
+ export function detectAllMediaTypes(messageContent, isQuoted = false) {
875
+ if (!messageContent || typeof messageContent !== 'object') {
876
+ return [];
877
+ }
878
+
879
+ const normalizedMessage = unwrapMessageContent(messageContent);
880
+ if (!normalizedMessage || typeof normalizedMessage !== 'object') {
881
+ return [];
882
+ }
883
+
884
+ const mediaFound = [];
885
+
886
+ for (const [key, value] of Object.entries(normalizedMessage)) {
887
+ if (!value || typeof value !== 'object') continue;
888
+
889
+ let mediaType = MEDIA_TYPE_MAPPING[key];
890
+ if (key === 'audioMessage' && value.ptt) {
891
+ mediaType = 'voice';
892
+ } else if (key === 'videoMessage' && value.ptv) {
893
+ mediaType = 'videoNote';
894
+ }
895
+
896
+ if (mediaType) {
897
+ mediaFound.push(buildMediaEntry(mediaType, key, value, isQuoted));
898
+ continue;
899
+ }
900
+
901
+ if (key.toLowerCase().includes('message')) {
902
+ const inferredType = key.replace(/Message$/, '').toLowerCase();
903
+ mediaFound.push(
904
+ buildMediaEntry(inferredType, key, value, isQuoted, {
905
+ isBinary: false,
906
+ isUnknownType: true,
907
+ }),
908
+ );
909
+ }
910
+ }
911
+
912
+ return mediaFound;
913
+ }
914
+
915
+ /**
916
+ * Extrai detalhes da midia da mensagem de forma dinamica
917
+ * @param {object} message - O objeto da mensagem
918
+ * @param {object} options - Opcoes de configuracao
919
+ * @param {boolean} options.includeAllTypes - Se deve incluir todos os tipos, nao apenas binarios
920
+ * @param {boolean} options.includeQuoted - Se deve incluir midia de mensagens citadas
921
+ * @param {boolean} options.includeUnknown - Se deve incluir tipos desconhecidos
922
+ * @returns {{mediaType: string, mediaKey: object, details: object}|null} - Detalhes da midia ou null se nao encontrada
923
+ */
924
+ export function extractMediaDetails(message, options = {}) {
925
+ const { includeAllTypes = false, includeQuoted = true, includeUnknown = false } = options;
926
+
927
+ const allMedia = collectMediaFromMessage(message, { includeQuoted });
928
+ const filteredMedia = filterMedia(allMedia, { includeAllTypes, includeUnknown });
929
+
930
+ if (filteredMedia.length > 0) {
931
+ const primaryMedia = filteredMedia[0];
932
+ return {
933
+ mediaType: primaryMedia.mediaType,
934
+ mediaKey: primaryMedia.mediaKey,
935
+ isQuoted: primaryMedia.isQuoted,
936
+ details: {
937
+ messageKey: primaryMedia.messageKey,
938
+ isBinary: primaryMedia.isBinary,
939
+ isUnknownType: primaryMedia.isUnknownType,
940
+ hasUrl: primaryMedia.hasUrl,
941
+ hasDirectPath: primaryMedia.hasDirectPath,
942
+ hasMediaKey: primaryMedia.hasMediaKey,
943
+ hasFileEncSha256: primaryMedia.hasFileEncSha256,
944
+ mimetype: primaryMedia.mimetype,
945
+ fileLength: primaryMedia.fileLength,
946
+ fileName: primaryMedia.fileName,
947
+ caption: primaryMedia.caption,
948
+ allMediaFound: allMedia.length > 1 ? allMedia : null,
949
+ },
950
+ };
951
+ }
952
+
953
+ return null;
954
+ }
955
+
956
+ /**
957
+ * Extrai todos os tipos de midia de uma mensagem
958
+ * @param {object} message - O objeto da mensagem
959
+ * @param {object} options - Opcoes de configuracao
960
+ * @returns {Array} Array com todos os tipos de midia encontrados
961
+ */
962
+ export function extractAllMediaDetails(message, options = {}) {
963
+ const { includeAllTypes = true, includeQuoted = true, includeUnknown = true } = options;
964
+
965
+ const allMedia = collectMediaFromMessage(message, { includeQuoted });
966
+ return filterMedia(allMedia, { includeAllTypes, includeUnknown });
967
+ }
968
+
969
+ /**
970
+ * Verifica se uma mensagem contem midia
971
+ * @param {object} message - O objeto da mensagem
972
+ * @param {string} specificType - Tipo especifico para verificar (opcional)
973
+ * @returns {boolean} True se contem midia
974
+ */
975
+ export function hasMedia(message, specificType = null) {
976
+ const allMedia = collectMediaFromMessage(message, { includeQuoted: true });
977
+ const filtered = filterMedia(allMedia, { includeAllTypes: true, includeUnknown: true });
978
+
979
+ if (!filtered.length) {
980
+ return false;
981
+ }
982
+
983
+ if (specificType) {
984
+ return filtered.some((media) => media.mediaType === specificType);
985
+ }
986
+
987
+ return true;
988
+ }
989
+
990
+ /**
991
+ * Obtem informacoes sobre os tipos de midia suportados
992
+ * @returns {object} Informacoes sobre tipos de midia
993
+ */
994
+ export function getMediaTypeInfo() {
995
+ return {
996
+ knownTypes: Object.values(MEDIA_TYPE_MAPPING),
997
+ binaryTypes: Array.from(BINARY_MEDIA_TYPES),
998
+ typeMapping: { ...MEDIA_TYPE_MAPPING },
999
+ totalKnownTypes: Object.keys(MEDIA_TYPE_MAPPING).length,
1000
+ };
1001
+ }
1002
+
1003
+ /**
1004
+ * ===============================
1005
+ * LID Map Utilities
1006
+ * ===============================
1007
+ */
1008
+ const CACHE_TTL_MS = 20 * 60 * 1000;
1009
+ const NEGATIVE_TTL_MS = 5 * 60 * 1000;
1010
+ const STORE_COOLDOWN_MS = 10 * 60 * 1000;
1011
+ const BATCH_LIMIT = 800;
1012
+ const BACKFILL_DEFAULT_BATCH = 50000;
1013
+ const BACKFILL_SOURCE = 'backfill';
1014
+ const __filename = fileURLToPath(import.meta.url);
1015
+ const __dirname = path.dirname(__filename);
1016
+ const BAILEYS_AUTH_DIR = path.resolve(__dirname, '../connection/auth');
1017
+ const BAILEYS_AUTH_SESSION_ID = (() => {
1018
+ const raw = String(process.env.BAILEYS_AUTH_SESSION_ID || '').trim();
1019
+ return raw || 'default';
1020
+ })();
1021
+
1022
+ const lidCache = new Map();
1023
+ const lidWriteBuffer = new Map();
1024
+ const authReverseLidCache = new Map();
1025
+
1026
+ let backfillPromise = null;
1027
+
1028
+ const updateLidQueueMetric = () => {
1029
+ setQueueDepth('lid_map', lidWriteBuffer.size);
1030
+ };
1031
+
1032
+ /**
1033
+ * Retorna timestamp atual em ms.
1034
+ * @returns {number}
1035
+ */
1036
+ const now = () => Date.now();
1037
+
1038
+ const normalizeLid = (lid) => {
1039
+ if (!lid || !isLidJid(lid)) return null;
1040
+ const normalized = normalizeJid(lid);
1041
+ return normalized || null;
1042
+ };
1043
+
1044
+ const normalizeWhatsAppJid = (jid) => {
1045
+ if (!jid || !isWhatsAppJid(jid)) return null;
1046
+ const normalized = normalizeJid(jid);
1047
+ return normalized || null;
1048
+ };
1049
+
1050
+ const toDigits = (value) => String(value || '').replace(/\D+/g, '');
1051
+
1052
+ const parseReverseMappingPhoneDigits = (content) => {
1053
+ const raw = String(content || '').trim();
1054
+ if (!raw) return '';
1055
+
1056
+ let parsed;
1057
+ try {
1058
+ parsed = JSON.parse(raw);
1059
+ } catch {
1060
+ parsed = raw;
1061
+ }
1062
+
1063
+ const digits = toDigits(parsed);
1064
+ return digits.length >= 10 && digits.length <= 15 ? digits : '';
1065
+ };
1066
+
1067
+ const resolveAuthStoreJidByLid = async (lid) => {
1068
+ const normalizedLid = normalizeLid(lid);
1069
+ if (!normalizedLid) return null;
1070
+
1071
+ const [rawUser] = normalizedLid.split('@');
1072
+ const rootUser = rawUser ? rawUser.split(':')[0] : '';
1073
+ if (!rootUser || !/^\d+$/.test(rootUser)) return null;
1074
+
1075
+ if (authReverseLidCache.has(rootUser)) {
1076
+ return authReverseLidCache.get(rootUser);
1077
+ }
1078
+
1079
+ try {
1080
+ const authStateTable = TABLES.BAILEYS_AUTH_STATE;
1081
+ if (authStateTable) {
1082
+ const rows = await executeQuery(`SELECT payload FROM \`${authStateTable}\` WHERE session_id = ? AND category = ? AND item_id = ? LIMIT 1`, [BAILEYS_AUTH_SESSION_ID, 'lid-mapping', `${rootUser}_reverse`]);
1083
+ const payload = rows?.[0]?.payload;
1084
+ if (payload !== undefined && payload !== null) {
1085
+ const phoneDigits = parseReverseMappingPhoneDigits(payload);
1086
+ const resolvedJid = phoneDigits ? normalizeWhatsAppJid(`${phoneDigits}@s.whatsapp.net`) : null;
1087
+ authReverseLidCache.set(rootUser, resolvedJid);
1088
+ return resolvedJid;
1089
+ }
1090
+ }
1091
+ } catch (error) {
1092
+ logger.warn('Falha ao resolver LID via auth state MySQL.', {
1093
+ lid: normalizedLid,
1094
+ error: error?.message,
1095
+ });
1096
+ }
1097
+
1098
+ const reverseFilePath = path.join(BAILEYS_AUTH_DIR, `lid-mapping-${rootUser}_reverse.json`);
1099
+ try {
1100
+ const content = await readFile(reverseFilePath, 'utf8');
1101
+ const phoneDigits = parseReverseMappingPhoneDigits(content);
1102
+ const resolvedJid = phoneDigits ? normalizeWhatsAppJid(`${phoneDigits}@s.whatsapp.net`) : null;
1103
+ authReverseLidCache.set(rootUser, resolvedJid);
1104
+ return resolvedJid;
1105
+ } catch (error) {
1106
+ if (error?.code !== 'ENOENT') {
1107
+ logger.warn('Falha ao resolver LID via auth store local.', {
1108
+ lid: normalizedLid,
1109
+ error: error?.message,
1110
+ });
1111
+ }
1112
+ authReverseLidCache.set(rootUser, null);
1113
+ return null;
1114
+ }
1115
+ };
1116
+
1117
+ /**
1118
+ * Mascara um JID para logs.
1119
+ * @param {string|null|undefined} jid
1120
+ * @returns {string|null}
1121
+ */
1122
+ const maskJid = (jid) => {
1123
+ if (!jid || typeof jid !== 'string') return null;
1124
+ const [user, server] = jid.split('@');
1125
+ if (!user || !server) return jid;
1126
+ const head = user.slice(0, 3);
1127
+ const tail = user.slice(-2);
1128
+ return `${head}***${tail}@${server}`;
1129
+ };
1130
+
1131
+ /**
1132
+ * Busca entrada do cache (com expiração).
1133
+ * @param {string|null|undefined} lid
1134
+ * @returns {{jid: string|null, expiresAt: number, lastStoredAt: number|null}|null}
1135
+ */
1136
+ const getCacheEntry = (lid) => {
1137
+ if (!lid) return null;
1138
+ const nowTs = now();
1139
+ const entry = lidCache.get(lid);
1140
+ if (entry) {
1141
+ if (entry.expiresAt && entry.expiresAt < nowTs) {
1142
+ lidCache.delete(lid);
1143
+ } else {
1144
+ return entry;
1145
+ }
1146
+ }
1147
+
1148
+ const baseLid = normalizeLid(lid);
1149
+ if (!baseLid || baseLid === lid) return null;
1150
+ const baseEntry = lidCache.get(baseLid);
1151
+ if (!baseEntry) return null;
1152
+ if (baseEntry.expiresAt && baseEntry.expiresAt < nowTs) {
1153
+ lidCache.delete(baseLid);
1154
+ return null;
1155
+ }
1156
+ return baseEntry;
1157
+ };
1158
+
1159
+ /**
1160
+ * Atualiza cache local do LID.
1161
+ * @param {string} lid
1162
+ * @param {string|null} jid
1163
+ * @param {number} ttlMs
1164
+ * @param {number|null} lastStoredAt
1165
+ * @returns {void}
1166
+ */
1167
+ const setCacheEntry = (lid, jid, ttlMs, lastStoredAt) => {
1168
+ if (!lid) return;
1169
+ const normalizedJid = normalizeWhatsAppJid(jid);
1170
+ const baseLid = normalizeLid(lid);
1171
+ const previousEntry = lidCache.get(lid) || (baseLid ? lidCache.get(baseLid) : null);
1172
+ const entry = {
1173
+ jid: normalizedJid ?? null,
1174
+ expiresAt: now() + (ttlMs || CACHE_TTL_MS),
1175
+ lastStoredAt: lastStoredAt ?? previousEntry?.lastStoredAt ?? null,
1176
+ };
1177
+ lidCache.set(lid, entry);
1178
+ if (baseLid && baseLid !== lid) {
1179
+ lidCache.set(baseLid, entry);
1180
+ }
1181
+ };
1182
+
1183
+ /**
1184
+ * Retorna JID do cache para um LID.
1185
+ * @param {string|null|undefined} lid
1186
+ * @returns {string|null|undefined} undefined quando nao cacheado.
1187
+ */
1188
+ export const getCachedJidForLid = (lid) => {
1189
+ const entry = getCacheEntry(lid);
1190
+ if (!entry) return undefined;
1191
+ return entry.jid ?? null;
1192
+ };
1193
+
1194
+ /**
1195
+ * Divide lista em batches.
1196
+ * @param {Array<any>} items
1197
+ * @param {number} [limit=BATCH_LIMIT]
1198
+ * @returns {Array<Array<any>>}
1199
+ */
1200
+ const buildChunks = (items, limit = BATCH_LIMIT) => {
1201
+ const chunks = [];
1202
+ for (let i = 0; i < items.length; i += limit) {
1203
+ chunks.push(items.slice(i, i + limit));
1204
+ }
1205
+ return chunks;
1206
+ };
1207
+
1208
+ /**
1209
+ * Pré-carrega cache a partir do banco.
1210
+ * @param {Array<string>} [lids=[]]
1211
+ * @returns {Promise<Map<string, string|null>>}
1212
+ */
1213
+ export const primeLidCache = async (lids = []) => {
1214
+ const uniqueLids = Array.from(new Set((lids || []).filter(Boolean)));
1215
+ if (!uniqueLids.length) return new Map();
1216
+
1217
+ const pending = uniqueLids.filter((lid) => isLidJid(lid) && getCachedJidForLid(lid) === undefined);
1218
+ if (!pending.length) {
1219
+ const map = new Map();
1220
+ uniqueLids.forEach((lid) => map.set(lid, getCachedJidForLid(lid) ?? null));
1221
+ return map;
1222
+ }
1223
+
1224
+ const results = new Map();
1225
+ const baseByLid = new Map();
1226
+ const lookupSet = new Set();
1227
+ pending.forEach((lid) => {
1228
+ const base = normalizeLid(lid);
1229
+ baseByLid.set(lid, base);
1230
+ lookupSet.add(lid);
1231
+ if (base && base !== lid) lookupSet.add(base);
1232
+ });
1233
+
1234
+ const lookupList = Array.from(lookupSet);
1235
+ const chunks = buildChunks(lookupList);
1236
+ const rowMap = new Map();
1237
+
1238
+ for (const chunk of chunks) {
1239
+ const placeholders = chunk.map(() => '?').join(', ');
1240
+ const rows = await executeQuery(`SELECT lid, jid FROM ${TABLES.LID_MAP} WHERE lid IN (${placeholders})`, chunk);
1241
+
1242
+ (rows || []).forEach((row) => {
1243
+ if (!row?.lid) return;
1244
+ const jid = row.jid && isWhatsAppJid(row.jid) ? normalizeJid(row.jid) : null;
1245
+ rowMap.set(row.lid, jid);
1246
+ });
1247
+ }
1248
+
1249
+ for (const lid of pending) {
1250
+ const base = baseByLid.get(lid);
1251
+ const direct = rowMap.has(lid) ? rowMap.get(lid) : undefined;
1252
+ const baseValue = base && base !== lid && rowMap.has(base) ? rowMap.get(base) : undefined;
1253
+ let resolved = direct ?? baseValue ?? null;
1254
+
1255
+ if (!resolved) {
1256
+ const authStoreResolved = await resolveAuthStoreJidByLid(lid);
1257
+ if (authStoreResolved) {
1258
+ resolved = authStoreResolved;
1259
+ }
1260
+ }
1261
+
1262
+ const directHasJid = typeof direct === 'string' && direct.length > 0;
1263
+ const shouldSeed = Boolean(resolved && (!directHasJid || direct !== resolved));
1264
+ setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS, shouldSeed ? 0 : undefined);
1265
+ if (shouldSeed) {
1266
+ queueLidUpdate(lid, resolved, 'prime');
1267
+ }
1268
+ results.set(lid, resolved);
1269
+ }
1270
+
1271
+ uniqueLids.forEach((lid) => {
1272
+ if (!results.has(lid)) {
1273
+ results.set(lid, getCachedJidForLid(lid) ?? null);
1274
+ }
1275
+ });
1276
+
1277
+ return results;
1278
+ };
1279
+
1280
+ /**
1281
+ * Retorna o primeiro JID valido do WhatsApp.
1282
+ * @param {...string} candidates
1283
+ * @returns {string|null}
1284
+ */
1285
+ const pickWhatsAppJid = (...candidates) => {
1286
+ for (const candidate of candidates) {
1287
+ if (!candidate || typeof candidate !== 'string') continue;
1288
+ const normalized = normalizeWhatsAppJid(candidate);
1289
+ if (normalized) return normalized;
1290
+ }
1291
+ return null;
1292
+ };
1293
+
1294
+ /**
1295
+ * Retorna o primeiro LID valido.
1296
+ * @param {...string} candidates
1297
+ * @returns {string|null}
1298
+ */
1299
+ const pickLid = (...candidates) => {
1300
+ for (const candidate of candidates) {
1301
+ if (!candidate || typeof candidate !== 'string') continue;
1302
+ const normalized = normalizeLid(candidate);
1303
+ if (normalized) return normalized;
1304
+ }
1305
+ return null;
1306
+ };
1307
+
1308
+ /**
1309
+ * Monta filtro SQL por sufixo de servidor JID.
1310
+ * @param {string} column
1311
+ * @param {Iterable<string>} servers
1312
+ * @returns {{clause: string, params: Array<string>}}
1313
+ */
1314
+ const buildServerLikeFilter = (column, servers) => {
1315
+ const values = Array.from(new Set(Array.from(servers || []).filter(Boolean)));
1316
+ if (!values.length) {
1317
+ return { clause: '1 = 0', params: [] };
1318
+ }
1319
+
1320
+ const clause = values.map(() => `${column} LIKE ?`).join(' OR ');
1321
+ const params = values.map((server) => `%@${server}`);
1322
+ return { clause: `(${clause})`, params };
1323
+ };
1324
+
1325
+ /**
1326
+ * Resolve candidatos principais de identidade de usuário.
1327
+ * Centraliza regra usada por `resolveUserIdCached` e `resolveUserId`.
1328
+ * @param {{lid?: string|null, jid?: string|null, participantAlt?: string|null}} [params]
1329
+ * @returns {{directJid: string|null, lidValue: string|null, fallback: string|null}}
1330
+ */
1331
+ const resolveIdentityCandidates = ({ lid, jid, participantAlt } = {}) => {
1332
+ const directJid = pickWhatsAppJid(jid, participantAlt, lid);
1333
+ if (directJid) {
1334
+ return {
1335
+ directJid,
1336
+ lidValue: null,
1337
+ fallback: directJid,
1338
+ };
1339
+ }
1340
+
1341
+ const lidValue = pickLid(lid, jid, participantAlt);
1342
+ if (lidValue) {
1343
+ return {
1344
+ directJid: null,
1345
+ lidValue,
1346
+ fallback: lidValue,
1347
+ };
1348
+ }
1349
+
1350
+ return {
1351
+ directJid: null,
1352
+ lidValue: null,
1353
+ fallback: jid || participantAlt || lid || null,
1354
+ };
1355
+ };
1356
+
1357
+ const buildLidUpsertSql = (rows) => {
1358
+ const placeholders = buildRowPlaceholders(rows, '(?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?)');
1359
+ return `
1360
+ INSERT INTO ${TABLES.LID_MAP} (lid, jid, first_seen, last_seen, source)
1361
+ VALUES ${placeholders}
1362
+ ON DUPLICATE KEY UPDATE
1363
+ jid = COALESCE(VALUES(jid), jid),
1364
+ last_seen = VALUES(last_seen),
1365
+ source = VALUES(source)
1366
+ `;
1367
+ };
1368
+
1369
+ /**
1370
+ * Enfileira atualizacao do lid_map (com cooldown e dedupe).
1371
+ * @param {string} lid
1372
+ * @param {string|null} jid
1373
+ * @param {string} [source='message']
1374
+ * @returns {{queued: boolean, reconciled: boolean}}
1375
+ */
1376
+ export const queueLidUpdate = (lid, jid, source = 'message') => {
1377
+ let resolvedLid = lid;
1378
+ let resolvedJid = jid;
1379
+
1380
+ if (!isLidJid(resolvedLid) && isLidJid(resolvedJid) && isWhatsAppJid(resolvedLid)) {
1381
+ resolvedLid = jid;
1382
+ resolvedJid = lid;
1383
+ }
1384
+
1385
+ if (!resolvedLid || !isLidJid(resolvedLid)) {
1386
+ return { queued: false, reconciled: false };
1387
+ }
1388
+
1389
+ const normalizedJid = resolvedJid && isWhatsAppJid(resolvedJid) ? normalizeJid(resolvedJid) : null;
1390
+ const lidsToUpdate = new Set([resolvedLid]);
1391
+ const baseLid = normalizeLid(resolvedLid);
1392
+ if (baseLid && baseLid !== resolvedLid) lidsToUpdate.add(baseLid);
1393
+
1394
+ let queued = false;
1395
+ let reconciled = false;
1396
+
1397
+ for (const targetLid of lidsToUpdate) {
1398
+ const cacheEntry = getCacheEntry(targetLid);
1399
+ const cachedJid = cacheEntry?.jid ?? null;
1400
+ const lastStoredAt = cacheEntry?.lastStoredAt || 0;
1401
+ const nowTs = now();
1402
+
1403
+ const mappingChanged = Boolean(normalizedJid && normalizedJid !== cachedJid);
1404
+ const mappingSame = normalizedJid === cachedJid;
1405
+
1406
+ if (mappingSame && nowTs - lastStoredAt < STORE_COOLDOWN_MS) {
1407
+ continue;
1408
+ }
1409
+
1410
+ const buffered = lidWriteBuffer.get(targetLid);
1411
+ const effectiveJid = normalizedJid ?? buffered?.jid ?? cachedJid ?? null;
1412
+ const entry = {
1413
+ lid: targetLid,
1414
+ jid: effectiveJid,
1415
+ source,
1416
+ queuedAt: nowTs,
1417
+ reconcileJid: mappingChanged ? normalizedJid : null,
1418
+ };
1419
+
1420
+ lidWriteBuffer.set(targetLid, entry);
1421
+ setCacheEntry(targetLid, effectiveJid, CACHE_TTL_MS, nowTs);
1422
+ queued = true;
1423
+ reconciled = reconciled || Boolean(entry.reconcileJid);
1424
+ }
1425
+
1426
+ if (queued) updateLidQueueMetric();
1427
+
1428
+ return { queued, reconciled };
1429
+ };
1430
+
1431
+ /**
1432
+ * Resolve ID canônico usando apenas cache.
1433
+ * @param {{lid?: string|null, jid?: string|null, participantAlt?: string|null}} [params]
1434
+ * @returns {string|null}
1435
+ */
1436
+ export const resolveUserIdCached = ({ lid, jid, participantAlt } = {}) => {
1437
+ const { directJid, lidValue, fallback } = resolveIdentityCandidates({ lid, jid, participantAlt });
1438
+ if (directJid) return directJid;
1439
+ if (!lidValue) return fallback;
1440
+
1441
+ const mapped = getCachedJidForLid(lidValue);
1442
+ if (mapped !== undefined) return mapped || lidValue;
1443
+ return lidValue;
1444
+ };
1445
+
1446
+ /**
1447
+ * Extrai informacoes do remetente a partir de uma mensagem do Baileys.
1448
+ * @param {import('@whiskeysockets/baileys').WAMessage} msg
1449
+ * @returns {{lid: string|null, jid: string|null, participantAlt: string|null, remoteJid: string|null, remoteJidAlt: string|null, groupMessage: boolean}}
1450
+ */
1451
+ export const extractSenderInfoFromMessage = (msg) => {
1452
+ const remoteJid = normalizeJid(msg?.key?.remoteJid || '') || null;
1453
+ const remoteJidAlt = normalizeJid(msg?.key?.remoteJidAlt || '') || null;
1454
+ const participant = normalizeJid(msg?.key?.participant || '') || null;
1455
+ const participantAlt = normalizeJid(msg?.key?.participantAlt || '') || null;
1456
+ const groupMessage = isGroupJid(remoteJid);
1457
+
1458
+ let lid = null;
1459
+ let jid = null;
1460
+
1461
+ if (groupMessage) {
1462
+ if (isWhatsAppJid(participant)) jid = participant;
1463
+ if (isLidJid(participant)) lid = participant;
1464
+ if (isWhatsAppJid(participantAlt)) jid = participantAlt;
1465
+ if (!lid && isLidJid(participantAlt)) lid = participantAlt;
1466
+ } else {
1467
+ if (isWhatsAppJid(remoteJid)) jid = remoteJid;
1468
+ if (!jid && isWhatsAppJid(remoteJidAlt)) jid = remoteJidAlt;
1469
+ if (!jid && isWhatsAppJid(participant)) jid = participant;
1470
+ if (!jid && isWhatsAppJid(participantAlt)) jid = participantAlt;
1471
+ if (isLidJid(participant)) lid = participant;
1472
+ if (!lid && isLidJid(remoteJid)) lid = remoteJid;
1473
+ if (!lid && isLidJid(participantAlt)) lid = participantAlt;
1474
+ }
1475
+
1476
+ return { lid, jid, participantAlt, remoteJid, remoteJidAlt, groupMessage };
1477
+ };
1478
+
1479
+ /**
1480
+ * Busca JID para um LID no banco e atualiza cache.
1481
+ * @param {string} lid
1482
+ * @returns {Promise<string|null>}
1483
+ */
1484
+ const fetchJidByLid = async (lid) => {
1485
+ const cached = getCachedJidForLid(lid);
1486
+ if (cached !== undefined) return cached || null;
1487
+
1488
+ const candidates = [lid];
1489
+ const base = normalizeLid(lid);
1490
+ if (base && base !== lid) candidates.push(base);
1491
+
1492
+ const placeholders = candidates.map(() => '?').join(', ');
1493
+ const rows = await executeQuery(`SELECT lid, jid FROM ${TABLES.LID_MAP} WHERE lid IN (${placeholders})`, candidates);
1494
+
1495
+ const rowMap = new Map();
1496
+ (rows || []).forEach((row) => {
1497
+ if (!row?.lid) return;
1498
+ const jid = row.jid && isWhatsAppJid(row.jid) ? normalizeJid(row.jid) : null;
1499
+ rowMap.set(row.lid, jid);
1500
+ });
1501
+
1502
+ const direct = rowMap.has(lid) ? rowMap.get(lid) : undefined;
1503
+ const baseValue = base && base !== lid && rowMap.has(base) ? rowMap.get(base) : undefined;
1504
+ let resolved = direct ?? baseValue ?? null;
1505
+ let resolveSource = 'db';
1506
+
1507
+ if (!resolved) {
1508
+ const authStoreResolved = await resolveAuthStoreJidByLid(lid);
1509
+ if (authStoreResolved) {
1510
+ resolved = authStoreResolved;
1511
+ resolveSource = 'auth-store';
1512
+ }
1513
+ }
1514
+
1515
+ if (!resolved) {
1516
+ const normalized = base || lid;
1517
+ const [rawUser, rawServer] = String(normalized).split('@');
1518
+ const rootUser = rawUser ? rawUser.split(':')[0] : '';
1519
+ const server = rawServer || '';
1520
+
1521
+ if (rootUser && server) {
1522
+ const derivedRows = await executeQuery(
1523
+ `SELECT jid
1524
+ FROM ${TABLES.LID_MAP}
1525
+ WHERE jid IS NOT NULL
1526
+ AND (
1527
+ lid = ?
1528
+ OR lid = ?
1529
+ OR lid = ?
1530
+ OR lid LIKE ?
1531
+ )
1532
+ ORDER BY last_seen DESC
1533
+ LIMIT 1`,
1534
+ [lid, base || lid, `${rootUser}@${server}`, `${rootUser}:%@${server}`],
1535
+ );
1536
+ const derivedJid = derivedRows?.[0]?.jid;
1537
+ if (derivedJid && isWhatsAppJid(derivedJid)) {
1538
+ resolved = normalizeJid(derivedJid);
1539
+ resolveSource = 'derived';
1540
+ }
1541
+ }
1542
+ }
1543
+
1544
+ const directHasJid = typeof direct === 'string' && direct.length > 0;
1545
+ const shouldSeedDerived = Boolean(resolved && (!directHasJid || direct !== resolved));
1546
+
1547
+ setCacheEntry(lid, resolved, resolved ? CACHE_TTL_MS : NEGATIVE_TTL_MS, shouldSeedDerived ? 0 : undefined);
1548
+
1549
+ if (shouldSeedDerived) {
1550
+ queueLidUpdate(lid, resolved, resolveSource === 'auth-store' ? 'auth-store' : 'derived');
1551
+ }
1552
+
1553
+ return resolved;
1554
+ };
1555
+
1556
+ /**
1557
+ * Resolve ID canônico consultando banco se necessário.
1558
+ * @param {{lid?: string|null, jid?: string|null, participantAlt?: string|null}} [params]
1559
+ * @returns {Promise<string|null>}
1560
+ */
1561
+ export const resolveUserId = async ({ lid, jid, participantAlt } = {}) => {
1562
+ const { directJid, lidValue, fallback } = resolveIdentityCandidates({ lid, jid, participantAlt });
1563
+ if (directJid) return directJid;
1564
+ if (!lidValue) return fallback;
1565
+
1566
+ const mapped = await fetchJidByLid(lidValue);
1567
+ return mapped || lidValue;
1568
+ };
1569
+
1570
+ /**
1571
+ * Reconcilia mensagens antigas do LID para o JID real.
1572
+ * @param {{lid?: string|null, jid?: string|null, source?: string}} [params]
1573
+ * @returns {Promise<{updated: number}>}
1574
+ */
1575
+ export const reconcileLidToJid = async ({ lid, jid, source = 'map' } = {}) => {
1576
+ if (!lid || !jid) return { updated: 0 };
1577
+ let result;
1578
+ try {
1579
+ result = await executeQuery(
1580
+ `UPDATE ${TABLES.MESSAGES}
1581
+ SET sender_id = ?,
1582
+ canonical_sender_id = ?
1583
+ WHERE sender_id = ?
1584
+ OR canonical_sender_id = ?`,
1585
+ [jid, jid, lid, lid],
1586
+ );
1587
+ } catch (error) {
1588
+ // Compatibilidade para ambientes legados sem a coluna canonical_sender_id.
1589
+ const badField = String(error?.code || '').toUpperCase() === 'ER_BAD_FIELD_ERROR' || Number(error?.errno || 0) === 1054;
1590
+ if (!badField) throw error;
1591
+ result = await executeQuery(`UPDATE ${TABLES.MESSAGES} SET sender_id = ? WHERE sender_id = ?`, [jid, lid]);
1592
+ }
1593
+
1594
+ const updated = Number(result?.affectedRows || 0);
1595
+ if (updated > 0) {
1596
+ logger.info('Reconciliação lid->jid aplicada.', {
1597
+ lid: maskJid(lid),
1598
+ jid: maskJid(jid),
1599
+ updated,
1600
+ source,
1601
+ });
1602
+ }
1603
+ return { updated };
1604
+ };
1605
+
1606
+ const flushLidQueueCore = async () => {
1607
+ if (lidWriteBuffer.size === 0) return;
1608
+ const entries = Array.from(lidWriteBuffer.values());
1609
+ for (let i = 0; i < entries.length; i += BATCH_LIMIT) {
1610
+ const batch = entries.slice(i, i + BATCH_LIMIT);
1611
+ if (!batch.length) continue;
1612
+
1613
+ const sql = buildLidUpsertSql(batch.length);
1614
+ const params = [];
1615
+ for (const entry of batch) {
1616
+ params.push(entry.lid, entry.jid, entry.source);
1617
+ }
1618
+
1619
+ try {
1620
+ await executeQuery(sql, params);
1621
+ } catch (error) {
1622
+ logger.error('Falha ao persistir batch do lid_map.', { error: error.message });
1623
+ recordError('lid_map');
1624
+ break;
1625
+ }
1626
+
1627
+ const reconcileTargets = [];
1628
+ for (const entry of batch) {
1629
+ const current = lidWriteBuffer.get(entry.lid);
1630
+ if (!current || current.queuedAt === entry.queuedAt) {
1631
+ lidWriteBuffer.delete(entry.lid);
1632
+ }
1633
+ if (entry.reconcileJid) {
1634
+ reconcileTargets.push({ lid: entry.lid, jid: entry.reconcileJid, source: entry.source });
1635
+ }
1636
+ }
1637
+
1638
+ updateLidQueueMetric();
1639
+ if (reconcileTargets.length > 0) {
1640
+ setImmediate(() => {
1641
+ for (const target of reconcileTargets) {
1642
+ reconcileLidToJid(target).catch((error) => {
1643
+ logger.warn('Falha ao reconciliar lid->jid.', { error: error.message });
1644
+ recordError('lid_map_reconcile');
1645
+ });
1646
+ }
1647
+ });
1648
+ }
1649
+ }
1650
+ };
1651
+
1652
+ const lidFlushRunner = createFlushRunner({
1653
+ onFlush: flushLidQueueCore,
1654
+ onError: (error) => {
1655
+ logger.error('Falha ao executar flush do lid_map.', { error: error.message });
1656
+ recordError('lid_map');
1657
+ },
1658
+ onFinally: () => {
1659
+ updateLidQueueMetric();
1660
+ },
1661
+ });
1662
+
1663
+ /**
1664
+ * Executa o flush do buffer lid_map em batch.
1665
+ * @returns {Promise<void>}
1666
+ */
1667
+ export const flushLidQueue = async () => {
1668
+ await lidFlushRunner.run();
1669
+ };
1670
+
1671
+ export const maybeStoreLidMap = async (lid, jid, source = 'message') => {
1672
+ const result = queueLidUpdate(lid, jid, source);
1673
+ return { stored: result.queued, reconciled: result.reconciled };
1674
+ };
1675
+
1676
+ /**
1677
+ * Extrai lid/jid/participantAlt de um objeto ou string.
1678
+ * @param {object|string|null|undefined} value
1679
+ * @returns {{lid: string|null, jid: string|null, participantAlt: string|null, raw: string|null}}
1680
+ */
1681
+ export const extractUserIdInfo = (value) => {
1682
+ if (!value) return { lid: null, jid: null, participantAlt: null, raw: null };
1683
+ if (typeof value === 'string') {
1684
+ return {
1685
+ lid: normalizeLid(value),
1686
+ jid: normalizeWhatsAppJid(value),
1687
+ participantAlt: null,
1688
+ raw: value,
1689
+ };
1690
+ }
1691
+
1692
+ const readJid = (entry) => (typeof entry === 'string' ? normalizeJid(entry) || null : null);
1693
+
1694
+ const participantAlt = readJid(value.participantAlt);
1695
+ const remoteJidAlt = readJid(value.remoteJidAlt);
1696
+ const alternateJid = participantAlt || remoteJidAlt;
1697
+ const participant = readJid(value.participant);
1698
+ const remoteJid = readJid(value.remoteJid);
1699
+ const jidValue = readJid(value.jid);
1700
+ const lidValue = readJid(value.lid);
1701
+ const idValue = typeof value.id === 'string' ? value.id : null;
1702
+ const jidCandidate = jidValue || idValue || alternateJid || participant || remoteJid || null;
1703
+ const lidCandidate = lidValue || participant || remoteJid || alternateJid || idValue || jidValue || null;
1704
+
1705
+ return {
1706
+ lid: pickLid(lidCandidate, alternateJid, participant),
1707
+ jid: pickWhatsAppJid(jidCandidate, alternateJid, participant),
1708
+ participantAlt: alternateJid,
1709
+ raw: jidCandidate || lidCandidate,
1710
+ };
1711
+ };
1712
+
1713
+ /**
1714
+ * Alias: verifica se valor e LID.
1715
+ * @param {string|null|undefined} value
1716
+ * @returns {boolean}
1717
+ */
1718
+ export const isLidUserId = (value) => isLidJid(value);
1719
+
1720
+ /**
1721
+ * Alias: verifica se valor e JID do WhatsApp.
1722
+ * @param {string|null|undefined} value
1723
+ * @returns {boolean}
1724
+ */
1725
+ export const isWhatsAppUserId = (value) => isWhatsAppJid(value);
1726
+
1727
+ /**
1728
+ * Retorna o range de IDs da tabela messages.
1729
+ * @returns {Promise<{minId: number, maxId: number}>}
1730
+ */
1731
+ const getMessageIdRange = async () => {
1732
+ const rows = await executeQuery(`SELECT MIN(id) AS min_id, MAX(id) AS max_id FROM ${TABLES.MESSAGES}`);
1733
+ const minId = Number(rows?.[0]?.min_id || 0);
1734
+ const maxId = Number(rows?.[0]?.max_id || 0);
1735
+ return { minId, maxId };
1736
+ };
1737
+
1738
+ /**
1739
+ * Executa um batch do backfill lid_map.
1740
+ * @param {number} fromId
1741
+ * @param {number} toId
1742
+ * @returns {Promise<any>}
1743
+ */
1744
+ const runBackfillBatch = async (fromId, toId) => {
1745
+ const lidFilter = buildServerLikeFilter('s.lid', LID_USER_JID_SERVERS);
1746
+ const jidFilter = buildServerLikeFilter('s.jid', WHATSAPP_USER_JID_SERVERS);
1747
+
1748
+ const sql = `
1749
+ INSERT INTO ${TABLES.LID_MAP} (lid, jid, first_seen, last_seen, source)
1750
+ SELECT
1751
+ s.lid,
1752
+ s.jid,
1753
+ MIN(s.ts) AS first_seen,
1754
+ MAX(s.ts) AS last_seen,
1755
+ ?
1756
+ FROM (
1757
+ SELECT
1758
+ JSON_UNQUOTE(JSON_EXTRACT(m.raw_message, '$.key.participant')) AS lid,
1759
+ JSON_UNQUOTE(JSON_EXTRACT(m.raw_message, '$.key.participantAlt')) AS jid,
1760
+ m.timestamp AS ts
1761
+ FROM ${TABLES.MESSAGES} m
1762
+ WHERE m.id BETWEEN ? AND ?
1763
+ AND m.raw_message IS NOT NULL
1764
+ AND m.timestamp IS NOT NULL
1765
+ ) s
1766
+ WHERE ${lidFilter.clause}
1767
+ AND ${jidFilter.clause}
1768
+ GROUP BY s.lid, s.jid
1769
+ ON DUPLICATE KEY UPDATE
1770
+ jid = COALESCE(VALUES(jid), ${TABLES.LID_MAP}.jid),
1771
+ last_seen = GREATEST(${TABLES.LID_MAP}.last_seen, VALUES(last_seen)),
1772
+ source = VALUES(source)
1773
+ `;
1774
+
1775
+ return executeQuery(sql, [BACKFILL_SOURCE, fromId, toId, ...lidFilter.params, ...jidFilter.params]);
1776
+ };
1777
+
1778
+ /**
1779
+ * Backfill do lid_map a partir de messages.raw_message.
1780
+ * @param {{batchSize?: number, sleepMs?: number, maxBatches?: number|null}} [options]
1781
+ * @returns {Promise<{batches: number, minId?: number, maxId?: number}>}
1782
+ */
1783
+ export const backfillLidMapFromMessages = async ({ batchSize = BACKFILL_DEFAULT_BATCH, sleepMs = 50, maxBatches = null } = {}) => {
1784
+ const { minId, maxId } = await getMessageIdRange();
1785
+ if (!minId || !maxId || maxId < minId) {
1786
+ logger.info('Backfill lid_map ignorado: tabela messages vazia.');
1787
+ return { batches: 0 };
1788
+ }
1789
+
1790
+ let batches = 0;
1791
+ for (let start = minId; start <= maxId; start += batchSize) {
1792
+ const end = Math.min(start + batchSize - 1, maxId);
1793
+ await runBackfillBatch(start, end);
1794
+ batches += 1;
1795
+ if (maxBatches && batches >= maxBatches) break;
1796
+ if (sleepMs > 0) await delay(sleepMs);
1797
+ }
1798
+
1799
+ logger.info('Backfill lid_map finalizado.', { batches, minId, maxId });
1800
+ return { batches, minId, maxId };
1801
+ };
1802
+
1803
+ /**
1804
+ * Garante que o backfill rode apenas uma vez por processo.
1805
+ * @param {{batchSize?: number, sleepMs?: number, maxBatches?: number|null}} [options]
1806
+ * @returns {Promise<{batches: number, minId?: number, maxId?: number}>}
1807
+ */
1808
+ export const backfillLidMapFromMessagesOnce = async (options = {}) => {
1809
+ if (!backfillPromise) {
1810
+ backfillPromise = backfillLidMapFromMessages(options).catch((error) => {
1811
+ logger.warn('Falha no backfill lid_map.', { error: error.message });
1812
+ throw error;
1813
+ });
1814
+ }
1815
+ return backfillPromise;
1816
+ };
1817
+
1818
+ updateLidQueueMetric();