@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,967 @@
1
+ import OpenAI from 'openai';
2
+ import NodeCache from 'node-cache';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ import logger from '#logger';
7
+ import premiumUserStore from '../../store/premiumUserStore.js';
8
+ import aiPromptStore from '../../store/aiPromptStore.js';
9
+ import { downloadMediaMessage, extractAllMediaDetails, getJidUser, isSameJidUser, normalizeJid } from '../../config/index.js';
10
+ import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
11
+ import { getAdminJid, resolveAdminJid } from '../../config/index.js';
12
+ import { extractUserIdInfo, resolveUserId, resolveUserIdCached } from '../../config/index.js';
13
+ import { getAiCommandOperationalLimits, getAiCommandOptionConfig, getAiCommandSystemMessages, getAiUsageText, isAiCommandPremiumOnly } from './aiConfigRuntime.js';
14
+
15
+ const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-5-nano';
16
+ const OPENAI_IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || OPENAI_MODEL;
17
+ const OPENAI_TTS_MODEL = process.env.OPENAI_TTS_MODEL || 'gpt-4o-mini-tts';
18
+ const OPENAI_TTS_VOICE = process.env.OPENAI_TTS_VOICE || 'alloy';
19
+ const OPENAI_TTS_FORMAT_RAW = (process.env.OPENAI_TTS_FORMAT || 'mp3').toLowerCase();
20
+ const OPENAI_TTS_PTT = process.env.OPENAI_TTS_PTT === 'true';
21
+ const OPENAI_TTS_MAX_CHARS = Number.parseInt(process.env.OPENAI_TTS_MAX_CHARS || '4096', 10);
22
+ const OPENAI_MAX_IMAGE_MB = Number.parseFloat(process.env.OPENAI_MAX_IMAGE_MB || '50');
23
+ const OPENAI_TIMEOUT_MS = Number.parseInt(process.env.OPENAI_TIMEOUT_MS || '30000', 10);
24
+ const OPENAI_IMAGE_TIMEOUT_MS = Number.parseInt(process.env.OPENAI_IMAGE_TIMEOUT_MS || '120000', 10);
25
+ const OPENAI_MAX_RETRIES = Number.parseInt(process.env.OPENAI_MAX_RETRIES || '2', 10);
26
+ const OPENAI_RETRY_BASE_MS = Number.parseInt(process.env.OPENAI_RETRY_BASE_MS || '500', 10);
27
+ const OPENAI_RETRY_MAX_MS = Number.parseInt(process.env.OPENAI_RETRY_MAX_MS || '4000', 10);
28
+ const DEFAULT_SYSTEM_PROMPT = `Responda em PT-BR:`.trim();
29
+ const DEFAULT_IMAGE_PROMPT = 'Responda em PT-BR:';
30
+ const TEMP_DIR = path.join(process.cwd(), 'temp', 'ai');
31
+
32
+ const BASE_SYSTEM_PROMPT = process.env.OPENAI_SYSTEM_PROMPT?.trim() || DEFAULT_SYSTEM_PROMPT;
33
+ const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
34
+ const OWNER_JID = getAdminJid();
35
+
36
+ const SESSION_TTL_SECONDS = Number.parseInt(process.env.OPENAI_SESSION_TTL_SECONDS || '21600', 10);
37
+ const sessionCache = new NodeCache({
38
+ stdTTL: SESSION_TTL_SECONDS,
39
+ checkperiod: Math.max(60, Math.floor(SESSION_TTL_SECONDS / 4)),
40
+ });
41
+ let cachedClient = null;
42
+
43
+ const AUDIO_FLAG_ALIASES = new Set(['--audio', '--voz', '--voice', '--tts', '-a']);
44
+ const TEXT_FLAG_ALIASES = new Set(['--texto', '--text', '--txt']);
45
+ const IMAGE_DETAIL_ALIASES = new Map([
46
+ ['low', 'low'],
47
+ ['high', 'high'],
48
+ ['auto', 'auto'],
49
+ ['baixo', 'low'],
50
+ ['baixa', 'low'],
51
+ ['alto', 'high'],
52
+ ['alta', 'high'],
53
+ ['automatico', 'auto'],
54
+ ['automático', 'auto'],
55
+ ]);
56
+ const IMAGE_GEN_SIZE_OPTIONS = new Set(['auto', '1024x1024', '1024x1536', '1536x1024']);
57
+ const IMAGE_GEN_SIZE_ALIASES = new Map([
58
+ ['1024', '1024x1024'],
59
+ ['square', '1024x1024'],
60
+ ['quadrado', '1024x1024'],
61
+ ['portrait', '1024x1536'],
62
+ ['retrato', '1024x1536'],
63
+ ['landscape', '1536x1024'],
64
+ ['paisagem', '1536x1024'],
65
+ ['auto', 'auto'],
66
+ ]);
67
+ const IMAGE_GEN_QUALITY_OPTIONS = new Set(['auto', 'low', 'medium', 'high']);
68
+ const IMAGE_GEN_QUALITY_ALIASES = new Map([
69
+ ['baixa', 'low'],
70
+ ['baixo', 'low'],
71
+ ['media', 'medium'],
72
+ ['média', 'medium'],
73
+ ['medio', 'medium'],
74
+ ['médio', 'medium'],
75
+ ['alta', 'high'],
76
+ ['alto', 'high'],
77
+ ['auto', 'auto'],
78
+ ]);
79
+ const IMAGE_GEN_FORMAT_OPTIONS = new Set(['png', 'jpeg', 'webp']);
80
+ const IMAGE_GEN_FORMAT_ALIASES = new Map([
81
+ ['jpg', 'jpeg'],
82
+ ['jpeg', 'jpeg'],
83
+ ['png', 'png'],
84
+ ['webp', 'webp'],
85
+ ]);
86
+ const IMAGE_GEN_BACKGROUND_OPTIONS = new Set(['auto', 'transparent', 'opaque']);
87
+ const IMAGE_GEN_BACKGROUND_ALIASES = new Map([
88
+ ['auto', 'auto'],
89
+ ['transparent', 'transparent'],
90
+ ['transparente', 'transparent'],
91
+ ['opaque', 'opaque'],
92
+ ['opaco', 'opaque'],
93
+ ['opaca', 'opaque'],
94
+ ]);
95
+ const IMAGE_GEN_FLAG_ALIASES = {
96
+ size: new Set(['--size', '--tamanho']),
97
+ quality: new Set(['--quality', '--qualidade']),
98
+ format: new Set(['--format', '--formato']),
99
+ background: new Set(['--background', '--fundo']),
100
+ compression: new Set(['--compression', '--compressao', '--compressão']),
101
+ };
102
+ const AUDIO_MIME_BY_FORMAT = {
103
+ mp3: 'audio/mpeg',
104
+ wav: 'audio/wav',
105
+ opus: 'audio/ogg; codecs=opus',
106
+ aac: 'audio/aac',
107
+ flac: 'audio/flac',
108
+ pcm: 'audio/pcm',
109
+ };
110
+ const SAFE_TTS_FORMAT = AUDIO_MIME_BY_FORMAT[OPENAI_TTS_FORMAT_RAW] ? OPENAI_TTS_FORMAT_RAW : 'mp3';
111
+ const TTS_OUTPUT_FORMAT = OPENAI_TTS_PTT ? 'opus' : SAFE_TTS_FORMAT;
112
+ const TTS_MIME_TYPE = AUDIO_MIME_BY_FORMAT[TTS_OUTPUT_FORMAT] || 'audio/mpeg';
113
+ const TTS_MAX_CHARS = Number.isFinite(OPENAI_TTS_MAX_CHARS) && OPENAI_TTS_MAX_CHARS > 0 ? OPENAI_TTS_MAX_CHARS : 4096;
114
+ const OPENAI_TIMEOUT = Number.isFinite(OPENAI_TIMEOUT_MS) && OPENAI_TIMEOUT_MS > 0 ? OPENAI_TIMEOUT_MS : 30000;
115
+ const OPENAI_IMAGE_TIMEOUT = Number.isFinite(OPENAI_IMAGE_TIMEOUT_MS) && OPENAI_IMAGE_TIMEOUT_MS > 0 ? OPENAI_IMAGE_TIMEOUT_MS : 120000;
116
+ const OPENAI_CLIENT_TIMEOUT = Math.max(OPENAI_TIMEOUT, OPENAI_IMAGE_TIMEOUT);
117
+ const OPENAI_RETRIES = Number.isFinite(OPENAI_MAX_RETRIES) && OPENAI_MAX_RETRIES >= 0 ? OPENAI_MAX_RETRIES : 2;
118
+ const OPENAI_RETRY_BASE = Number.isFinite(OPENAI_RETRY_BASE_MS) && OPENAI_RETRY_BASE_MS > 0 ? OPENAI_RETRY_BASE_MS : 500;
119
+ const OPENAI_RETRY_MAX = Number.isFinite(OPENAI_RETRY_MAX_MS) && OPENAI_RETRY_MAX_MS > 0 ? OPENAI_RETRY_MAX_MS : 4000;
120
+ const MAX_IMAGE_BYTES = Number.isFinite(OPENAI_MAX_IMAGE_MB) && OPENAI_MAX_IMAGE_MB > 0 ? OPENAI_MAX_IMAGE_MB * 1024 * 1024 : 50 * 1024 * 1024;
121
+
122
+ const normalizeText = (value) =>
123
+ String(value || '')
124
+ .trim()
125
+ .toLowerCase();
126
+
127
+ const toSet = (values = [], fallbackValues = []) => {
128
+ const source = Array.isArray(values) && values.length ? values : fallbackValues;
129
+ return new Set(source.map((value) => normalizeText(value)).filter(Boolean));
130
+ };
131
+
132
+ const toMap = (value = {}, fallbackMap = new Map()) => {
133
+ const output = new Map();
134
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
135
+ for (const [key, mapValue] of Object.entries(value)) {
136
+ const normalizedKey = normalizeText(key);
137
+ const normalizedValue = normalizeText(mapValue);
138
+ if (!normalizedKey || !normalizedValue) continue;
139
+ output.set(normalizedKey, normalizedValue);
140
+ }
141
+ }
142
+
143
+ if (output.size > 0) {
144
+ return output;
145
+ }
146
+ return new Map(fallbackMap);
147
+ };
148
+
149
+ const resolveAiMessages = (commandName) => {
150
+ const commandMessages = getAiCommandSystemMessages(commandName);
151
+ const mergeMessage = (key, fallback) => String(commandMessages?.[key] || '').trim() || String(fallback || '').trim();
152
+
153
+ return {
154
+ premiumOnly: mergeMessage('premium_only', ['⭐ *Comando Premium*', '', 'Este comando é exclusivo para usuários premium.', 'Fale com o administrador para liberar o acesso.'].join('\n')),
155
+ openAiNotConfigured: mergeMessage('openai_nao_configurada', ['⚠️ *OpenAI não configurada*', '', `Defina a variável *OPENAI_API_KEY* no \`.env\` para usar o comando *${commandName}*.`].join('\n')),
156
+ imageTooLarge: mergeMessage('imagem_muito_grande', '⚠️ A imagem enviada ultrapassa o limite de {{limite_mb}} MB. Envie uma imagem menor.'),
157
+ imageDownloadFailed: mergeMessage('imagem_download_falhou', '⚠️ Não consegui baixar a imagem. Tente reenviar.'),
158
+ invalidOptions: mergeMessage('opcoes_invalidas', '⚠️ Opções inválidas no comando.\nDetalhes: {{detalhes}}\n\nUse *{{prefix}}catimg* sem opções para ver o formato correto.'),
159
+ emptyResponse: mergeMessage('resposta_vazia', '⚠️ Não consegui gerar uma resposta agora. Tente novamente.'),
160
+ longAudioFallback: mergeMessage('audio_muito_longo', '⚠️ A resposta ficou longa demais para áudio. Enviando em texto.'),
161
+ audioFailedFallback: mergeMessage('audio_falhou', '⚠️ Não consegui gerar o áudio agora. Enviando texto.'),
162
+ genericAiError: mergeMessage('erro_openai', ['❌ *Erro ao falar com a IA*', 'Tente novamente em alguns instantes.'].join('\n')),
163
+ promptTooLong: mergeMessage('prompt_muito_longo', '⚠️ Prompt muito longo. Limite: {{max_chars}} caracteres.'),
164
+ promptResetSuccess: mergeMessage('prompt_reset_sucesso', '✅ Prompt da IA restaurado para o padrão.'),
165
+ promptUpdateSuccess: mergeMessage('prompt_update_sucesso', '✅ Prompt da IA atualizado para você.'),
166
+ };
167
+ };
168
+
169
+ const applyTemplate = (template, vars = {}) => {
170
+ let output = String(template || '');
171
+ for (const [key, value] of Object.entries(vars || {})) {
172
+ output = output.replaceAll(`{{${key}}}`, String(value ?? ''));
173
+ }
174
+ return output;
175
+ };
176
+
177
+ const resolveCatParseOptions = () => {
178
+ const config = getAiCommandOptionConfig('cat');
179
+ const parse = config?.parse || {};
180
+
181
+ return {
182
+ audioFlags: toSet(parse.audio_flags, [...AUDIO_FLAG_ALIASES]),
183
+ textFlags: toSet(parse.text_flags, [...TEXT_FLAG_ALIASES]),
184
+ imageDetailAliases: toMap(parse.image_detail_aliases, IMAGE_DETAIL_ALIASES),
185
+ };
186
+ };
187
+
188
+ const resolveCatImageGenerationOptions = () => {
189
+ const config = getAiCommandOptionConfig('catimg');
190
+ const image = config?.geracao_imagem || {};
191
+ const fallbackFlagAliases = {
192
+ size: [...IMAGE_GEN_FLAG_ALIASES.size],
193
+ quality: [...IMAGE_GEN_FLAG_ALIASES.quality],
194
+ format: [...IMAGE_GEN_FLAG_ALIASES.format],
195
+ background: [...IMAGE_GEN_FLAG_ALIASES.background],
196
+ compression: [...IMAGE_GEN_FLAG_ALIASES.compression],
197
+ };
198
+
199
+ const normalizeFlagAliasSet = (key) => toSet(image?.flag_aliases?.[key], fallbackFlagAliases[key] || []);
200
+
201
+ const compressionMin = Number(image?.compression?.min);
202
+ const compressionMax = Number(image?.compression?.max);
203
+
204
+ return {
205
+ sizeOptions: toSet(image?.size_options, [...IMAGE_GEN_SIZE_OPTIONS]),
206
+ sizeAliases: toMap(image?.size_aliases, IMAGE_GEN_SIZE_ALIASES),
207
+ qualityOptions: toSet(image?.quality_options, [...IMAGE_GEN_QUALITY_OPTIONS]),
208
+ qualityAliases: toMap(image?.quality_aliases, IMAGE_GEN_QUALITY_ALIASES),
209
+ formatOptions: toSet(image?.format_options, [...IMAGE_GEN_FORMAT_OPTIONS]),
210
+ formatAliases: toMap(image?.format_aliases, IMAGE_GEN_FORMAT_ALIASES),
211
+ backgroundOptions: toSet(image?.background_options, [...IMAGE_GEN_BACKGROUND_OPTIONS]),
212
+ backgroundAliases: toMap(image?.background_aliases, IMAGE_GEN_BACKGROUND_ALIASES),
213
+ flagAliases: {
214
+ size: normalizeFlagAliasSet('size'),
215
+ quality: normalizeFlagAliasSet('quality'),
216
+ format: normalizeFlagAliasSet('format'),
217
+ background: normalizeFlagAliasSet('background'),
218
+ compression: normalizeFlagAliasSet('compression'),
219
+ },
220
+ compression: {
221
+ min: Number.isFinite(compressionMin) ? compressionMin : 0,
222
+ max: Number.isFinite(compressionMax) ? compressionMax : 100,
223
+ },
224
+ };
225
+ };
226
+
227
+ const resolveCatPromptMaxChars = () => {
228
+ const limits = getAiCommandOperationalLimits('catprompt');
229
+ const value = Number.parseInt(String(limits?.prompt_max_chars ?? ''), 10);
230
+ if (!Number.isFinite(value) || value <= 0) return 2000;
231
+ return value;
232
+ };
233
+
234
+ const getClient = () => {
235
+ if (cachedClient) return cachedClient;
236
+ cachedClient = new OpenAI({
237
+ apiKey: process.env.OPENAI_API_KEY,
238
+ timeout: OPENAI_CLIENT_TIMEOUT,
239
+ maxRetries: 0,
240
+ });
241
+ return cachedClient;
242
+ };
243
+
244
+ const buildSessionKey = (remoteJid, senderJid, scope) => {
245
+ const base = `${remoteJid}:${senderJid}`;
246
+ return scope ? `${base}:${scope}` : base;
247
+ };
248
+
249
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
250
+
251
+ const isRetryableOpenAIError = (error) => {
252
+ const status = error?.status || error?.statusCode || error?.response?.status;
253
+ if ([408, 409, 429, 500, 502, 503, 504].includes(status)) return true;
254
+ const code = error?.code || error?.cause?.code;
255
+ if (['ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'ECONNREFUSED', 'EPIPE'].includes(code)) {
256
+ return true;
257
+ }
258
+ if (error?.name === 'AbortError') return true;
259
+ if (typeof error?.message === 'string' && /timeout/i.test(error.message)) return true;
260
+ return false;
261
+ };
262
+
263
+ const runWithTimeout = async (operation, label, timeoutMs = OPENAI_TIMEOUT) => {
264
+ if (!timeoutMs || timeoutMs <= 0) {
265
+ return operation;
266
+ }
267
+ let timeoutId;
268
+ let didTimeout = false;
269
+ const timeoutPromise = new Promise((_, reject) => {
270
+ timeoutId = setTimeout(() => {
271
+ didTimeout = true;
272
+ const timeoutError = new Error(`OpenAI ${label} excedeu ${timeoutMs}ms`);
273
+ timeoutError.code = 'OPENAI_TIMEOUT';
274
+ reject(timeoutError);
275
+ }, timeoutMs);
276
+ });
277
+ try {
278
+ return await Promise.race([operation, timeoutPromise]);
279
+ } catch (error) {
280
+ if (didTimeout && operation?.catch) {
281
+ operation.catch(() => {});
282
+ }
283
+ throw error;
284
+ } finally {
285
+ clearTimeout(timeoutId);
286
+ }
287
+ };
288
+
289
+ const callOpenAI = async (operationFactory, label, timeoutMs) => {
290
+ let attempt = 0;
291
+ while (true) {
292
+ try {
293
+ const operation = operationFactory();
294
+ return await runWithTimeout(operation, label, timeoutMs);
295
+ } catch (error) {
296
+ attempt += 1;
297
+ if (attempt > OPENAI_RETRIES || !isRetryableOpenAIError(error)) {
298
+ throw error;
299
+ }
300
+ const backoff = Math.min(OPENAI_RETRY_MAX, OPENAI_RETRY_BASE * 2 ** (attempt - 1));
301
+ const jitter = Math.round(backoff * (0.8 + Math.random() * 0.4));
302
+ logger.warn(`OpenAI ${label} falhou. Retry ${attempt}/${OPENAI_RETRIES} em ${jitter}ms.`, {
303
+ error: error.message,
304
+ status: error?.status || error?.statusCode || error?.response?.status || null,
305
+ });
306
+ await sleep(jitter);
307
+ }
308
+ }
309
+ };
310
+
311
+ const sendUsage = async (sock, remoteJid, messageInfo, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX) => {
312
+ const usageText =
313
+ getAiUsageText('cat', {
314
+ commandPrefix,
315
+ header: '🤖 *Comando CAT*',
316
+ variant: 'default',
317
+ }) || ['🤖 *Comando CAT*', '', 'Use assim:', `*${commandPrefix}cat* [--audio] sua pergunta`, `*${commandPrefix}cat* (responda ou envie uma imagem com legenda)`, '', 'Opções:', '--audio | --texto', '--detail low | high | auto', '', 'Exemplo:', `*${commandPrefix}cat* Explique como funciona a fotossíntese.`, `*${commandPrefix}cat* --audio Resuma a imagem.`].join('\n');
318
+
319
+ await sendAndStore(sock, remoteJid, { text: usageText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
320
+ };
321
+
322
+ const reactToMessage = async (sock, remoteJid, messageInfo) => {
323
+ try {
324
+ if (!messageInfo?.key) return;
325
+ await sendAndStore(sock, remoteJid, {
326
+ react: {
327
+ text: '🐈‍⬛',
328
+ key: messageInfo.key,
329
+ },
330
+ });
331
+ } catch (error) {
332
+ logger.warn('handleCatCommand: falha ao reagir à mensagem.', error);
333
+ }
334
+ };
335
+
336
+ const isPremiumAllowed = async (senderJid) => {
337
+ const senderInfo = extractUserIdInfo({
338
+ jid: senderJid,
339
+ lid: senderJid,
340
+ participantAlt: senderJid,
341
+ });
342
+ const cachedSender = resolveUserIdCached(senderInfo);
343
+ const resolvedSender = await resolveUserId(senderInfo).catch(() => null);
344
+ const senderCandidates = Array.from(new Set([senderJid, senderInfo.jid, senderInfo.lid, senderInfo.participantAlt, senderInfo.raw, cachedSender, resolvedSender].map((value) => normalizeJid(value || '')).filter(Boolean)));
345
+
346
+ if (!senderCandidates.length) return false;
347
+
348
+ const adminJid = (await resolveAdminJid()) || OWNER_JID;
349
+ const normalizedAdmin = normalizeJid(adminJid || '');
350
+ if (!normalizedAdmin) return true;
351
+ if (senderCandidates.some((sender) => sender === normalizedAdmin || isSameJidUser(sender, normalizedAdmin))) return true;
352
+
353
+ const premiumUsers = await premiumUserStore.getPremiumUsers();
354
+ if (!Array.isArray(premiumUsers) || premiumUsers.length === 0) return false;
355
+ const premiumCandidates = Array.from(new Set(premiumUsers.map((jid) => normalizeJid(jid || '')).filter(Boolean)));
356
+ if (!premiumCandidates.length) return false;
357
+
358
+ return senderCandidates.some((sender) => premiumCandidates.some((premium) => premium === sender || isSameJidUser(premium, sender)));
359
+ };
360
+
361
+ const sendPremiumOnly = async (sock, remoteJid, messageInfo, expirationMessage, { commandName = 'cat' } = {}) => {
362
+ const messages = resolveAiMessages(commandName);
363
+ await sendAndStore(sock, remoteJid, { text: messages.premiumOnly }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
364
+ };
365
+
366
+ const sendPromptUsage = async (sock, remoteJid, messageInfo, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX) => {
367
+ const usageText =
368
+ getAiUsageText('catprompt', {
369
+ commandPrefix,
370
+ header: '🧠 *Prompt da IA*',
371
+ variant: 'default',
372
+ }) || ['🧠 *Prompt da IA*', '', 'Use assim:', `*${commandPrefix}catprompt* seu novo prompt`, '', 'Para voltar ao padrão:', `*${commandPrefix}catprompt reset*`].join('\n');
373
+
374
+ await sendAndStore(sock, remoteJid, { text: usageText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
375
+ };
376
+
377
+ const sendImageUsage = async (sock, remoteJid, messageInfo, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX) => {
378
+ const usageText =
379
+ getAiUsageText('catimg', {
380
+ commandPrefix,
381
+ header: '🖼️ *Imagem IA*',
382
+ variant: 'default',
383
+ }) || ['🖼️ *Imagem IA*', '', 'Use assim:', `*${commandPrefix}catimg* seu prompt`, `*${commandPrefix}catimg* (responda uma imagem com legenda para editar)`, '', 'Opções:', '--size 1024x1024 | 1024x1536 | 1536x1024 | auto', '--quality low | medium | high | auto', '--format png | jpeg | webp', '--background transparent | opaque | auto', '--compression 0-100', '', 'Exemplo:', `*${commandPrefix}catimg* --size 1536x1024 Um gato astronauta em aquarela.`].join('\n');
384
+
385
+ await sendAndStore(sock, remoteJid, { text: usageText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
386
+ };
387
+
388
+ const normalizeImageDetail = (value, detailAliases = IMAGE_DETAIL_ALIASES) => {
389
+ if (!value) return null;
390
+ const normalized = detailAliases.get(normalizeText(value));
391
+ return normalized || null;
392
+ };
393
+
394
+ const normalizeImageGenSize = (value, sizeOptions = IMAGE_GEN_SIZE_OPTIONS, sizeAliases = IMAGE_GEN_SIZE_ALIASES) => {
395
+ if (!value) return null;
396
+ const raw = normalizeText(value);
397
+ if (sizeOptions.has(raw)) return raw;
398
+ const alias = sizeAliases.get(raw);
399
+ if (alias && sizeOptions.has(alias)) return alias;
400
+ return null;
401
+ };
402
+
403
+ const normalizeImageGenQuality = (value, qualityOptions = IMAGE_GEN_QUALITY_OPTIONS, qualityAliases = IMAGE_GEN_QUALITY_ALIASES) => {
404
+ if (!value) return null;
405
+ const raw = normalizeText(value);
406
+ if (qualityOptions.has(raw)) return raw;
407
+ const alias = qualityAliases.get(raw);
408
+ if (alias && qualityOptions.has(alias)) return alias;
409
+ return null;
410
+ };
411
+
412
+ const normalizeImageGenFormat = (value, formatOptions = IMAGE_GEN_FORMAT_OPTIONS, formatAliases = IMAGE_GEN_FORMAT_ALIASES) => {
413
+ if (!value) return null;
414
+ const raw = normalizeText(value);
415
+ const alias = formatAliases.get(raw);
416
+ if (alias && formatOptions.has(alias)) return alias;
417
+ if (formatOptions.has(raw)) return raw;
418
+ return null;
419
+ };
420
+
421
+ const normalizeImageGenBackground = (value, backgroundOptions = IMAGE_GEN_BACKGROUND_OPTIONS, backgroundAliases = IMAGE_GEN_BACKGROUND_ALIASES) => {
422
+ if (!value) return null;
423
+ const raw = normalizeText(value);
424
+ if (backgroundOptions.has(raw)) return raw;
425
+ const alias = backgroundAliases.get(raw);
426
+ if (alias && backgroundOptions.has(alias)) return alias;
427
+ return null;
428
+ };
429
+
430
+ const normalizeImageGenCompression = (value, { min = 0, max = 100 } = {}) => {
431
+ if (value === null || value === undefined || value === '') return null;
432
+ const numeric = Number.parseInt(value, 10);
433
+ if (!Number.isFinite(numeric)) return null;
434
+ if (numeric < min || numeric > max) return null;
435
+ return numeric;
436
+ };
437
+
438
+ const parseCatOptions = (rawText = '', optionConfig = {}) => {
439
+ const audioFlags = optionConfig?.audioFlags || AUDIO_FLAG_ALIASES;
440
+ const textFlags = optionConfig?.textFlags || TEXT_FLAG_ALIASES;
441
+ const imageDetailAliases = optionConfig?.imageDetailAliases || IMAGE_DETAIL_ALIASES;
442
+
443
+ const tokens = rawText.trim().split(/\s+/).filter(Boolean);
444
+ let wantsAudio = false;
445
+ let imageDetail = null;
446
+ const filtered = [];
447
+
448
+ for (let i = 0; i < tokens.length; i += 1) {
449
+ const token = tokens[i];
450
+ const lower = normalizeText(token);
451
+
452
+ if (audioFlags.has(lower)) {
453
+ wantsAudio = true;
454
+ continue;
455
+ }
456
+ if (textFlags.has(lower)) {
457
+ wantsAudio = false;
458
+ continue;
459
+ }
460
+
461
+ if (lower.startsWith('--detail=') || lower.startsWith('--detalhe=')) {
462
+ const value = token.split('=')[1];
463
+ const detail = normalizeImageDetail(value, imageDetailAliases);
464
+ if (detail) {
465
+ imageDetail = detail;
466
+ continue;
467
+ }
468
+ }
469
+
470
+ if (lower === '--detail' || lower === '--detalhe') {
471
+ const value = tokens[i + 1];
472
+ const detail = normalizeImageDetail(value, imageDetailAliases);
473
+ if (detail) {
474
+ imageDetail = detail;
475
+ i += 1;
476
+ continue;
477
+ }
478
+ }
479
+
480
+ filtered.push(token);
481
+ }
482
+
483
+ return {
484
+ prompt: filtered.join(' ').trim(),
485
+ wantsAudio,
486
+ imageDetail,
487
+ };
488
+ };
489
+
490
+ const parseImageGenOptions = (rawText = '', optionConfig = {}) => {
491
+ const sizeOptions = optionConfig?.sizeOptions || IMAGE_GEN_SIZE_OPTIONS;
492
+ const sizeAliases = optionConfig?.sizeAliases || IMAGE_GEN_SIZE_ALIASES;
493
+ const qualityOptions = optionConfig?.qualityOptions || IMAGE_GEN_QUALITY_OPTIONS;
494
+ const qualityAliases = optionConfig?.qualityAliases || IMAGE_GEN_QUALITY_ALIASES;
495
+ const formatOptions = optionConfig?.formatOptions || IMAGE_GEN_FORMAT_OPTIONS;
496
+ const formatAliases = optionConfig?.formatAliases || IMAGE_GEN_FORMAT_ALIASES;
497
+ const backgroundOptions = optionConfig?.backgroundOptions || IMAGE_GEN_BACKGROUND_OPTIONS;
498
+ const backgroundAliases = optionConfig?.backgroundAliases || IMAGE_GEN_BACKGROUND_ALIASES;
499
+ const flagAliases = optionConfig?.flagAliases || IMAGE_GEN_FLAG_ALIASES;
500
+ const compression = optionConfig?.compression || { min: 0, max: 100 };
501
+
502
+ const tokens = rawText.trim().split(/\s+/).filter(Boolean);
503
+ const promptParts = [];
504
+ const toolOptions = {};
505
+ const errors = [];
506
+
507
+ const setOption = (key, rawValue, normalizedValue) => {
508
+ if (!normalizedValue) {
509
+ errors.push(`${key}=${rawValue}`);
510
+ return;
511
+ }
512
+ toolOptions[key] = normalizedValue;
513
+ };
514
+
515
+ for (let i = 0; i < tokens.length; i += 1) {
516
+ const token = tokens[i];
517
+ const lower = normalizeText(token);
518
+
519
+ if (lower.startsWith('--size=')) {
520
+ const value = token.split('=')[1];
521
+ setOption('size', value, normalizeImageGenSize(value, sizeOptions, sizeAliases));
522
+ continue;
523
+ }
524
+ if (flagAliases.size?.has(lower)) {
525
+ const value = tokens[i + 1];
526
+ if (value) {
527
+ setOption('size', value, normalizeImageGenSize(value, sizeOptions, sizeAliases));
528
+ i += 1;
529
+ continue;
530
+ }
531
+ }
532
+
533
+ if (lower.startsWith('--quality=')) {
534
+ const value = token.split('=')[1];
535
+ setOption('quality', value, normalizeImageGenQuality(value, qualityOptions, qualityAliases));
536
+ continue;
537
+ }
538
+ if (flagAliases.quality?.has(lower)) {
539
+ const value = tokens[i + 1];
540
+ if (value) {
541
+ setOption('quality', value, normalizeImageGenQuality(value, qualityOptions, qualityAliases));
542
+ i += 1;
543
+ continue;
544
+ }
545
+ }
546
+
547
+ if (lower.startsWith('--format=')) {
548
+ const value = token.split('=')[1];
549
+ setOption('output_format', value, normalizeImageGenFormat(value, formatOptions, formatAliases));
550
+ continue;
551
+ }
552
+ if (flagAliases.format?.has(lower)) {
553
+ const value = tokens[i + 1];
554
+ if (value) {
555
+ setOption('output_format', value, normalizeImageGenFormat(value, formatOptions, formatAliases));
556
+ i += 1;
557
+ continue;
558
+ }
559
+ }
560
+
561
+ if (lower.startsWith('--background=')) {
562
+ const value = token.split('=')[1];
563
+ setOption('background', value, normalizeImageGenBackground(value, backgroundOptions, backgroundAliases));
564
+ continue;
565
+ }
566
+ if (flagAliases.background?.has(lower)) {
567
+ const value = tokens[i + 1];
568
+ if (value) {
569
+ setOption('background', value, normalizeImageGenBackground(value, backgroundOptions, backgroundAliases));
570
+ i += 1;
571
+ continue;
572
+ }
573
+ }
574
+
575
+ if (lower.startsWith('--compression=')) {
576
+ const value = token.split('=')[1];
577
+ setOption('output_compression', value, normalizeImageGenCompression(value, compression));
578
+ continue;
579
+ }
580
+ if (flagAliases.compression?.has(lower)) {
581
+ const value = tokens[i + 1];
582
+ if (value) {
583
+ setOption('output_compression', value, normalizeImageGenCompression(value, compression));
584
+ i += 1;
585
+ continue;
586
+ }
587
+ }
588
+
589
+ if (lower === '--transparent' || lower === '--transparente') {
590
+ toolOptions.background = 'transparent';
591
+ continue;
592
+ }
593
+ if (lower === '--opaque' || lower === '--opaco' || lower === '--opaca') {
594
+ toolOptions.background = 'opaque';
595
+ continue;
596
+ }
597
+
598
+ promptParts.push(token);
599
+ }
600
+
601
+ if (toolOptions.output_compression !== undefined) {
602
+ const format = toolOptions.output_format;
603
+ if (!format || !['jpeg', 'webp'].includes(format)) {
604
+ errors.push('output_compression');
605
+ delete toolOptions.output_compression;
606
+ }
607
+ }
608
+
609
+ return {
610
+ prompt: promptParts.join(' ').trim(),
611
+ toolOptions,
612
+ errors,
613
+ };
614
+ };
615
+
616
+ const buildUserTempDir = (senderJid) => {
617
+ const userId = getJidUser(senderJid) || senderJid || 'anon';
618
+ const sanitizedUserId = String(userId).replace(/[^a-zA-Z0-9.-]/g, '_');
619
+ return path.join(TEMP_DIR, sanitizedUserId);
620
+ };
621
+
622
+ const findImageMedia = (messageInfo) => {
623
+ const mediaEntries = extractAllMediaDetails(messageInfo, {
624
+ includeAllTypes: true,
625
+ includeQuoted: true,
626
+ includeUnknown: false,
627
+ });
628
+ return mediaEntries.find((entry) => entry.mediaType === 'image') || null;
629
+ };
630
+
631
+ const buildImageDataUrl = async (imageMedia, senderJid) => {
632
+ if (!imageMedia) {
633
+ return { dataUrl: null };
634
+ }
635
+
636
+ const fileLength = imageMedia.fileLength || imageMedia.mediaKey?.fileLength || 0;
637
+ if (fileLength && fileLength > MAX_IMAGE_BYTES) {
638
+ return { error: 'too_large', fileLength };
639
+ }
640
+
641
+ const userDir = buildUserTempDir(senderJid);
642
+ await fs.mkdir(userDir, { recursive: true });
643
+
644
+ let downloadedPath = null;
645
+ try {
646
+ downloadedPath = await downloadMediaMessage(imageMedia.mediaKey, 'image', userDir);
647
+ if (!downloadedPath) {
648
+ return { error: 'download_failed' };
649
+ }
650
+
651
+ const buffer = await fs.readFile(downloadedPath);
652
+ const base64 = buffer.toString('base64');
653
+ const mimeType = imageMedia.mimetype || imageMedia.mediaKey?.mimetype || 'image/jpeg';
654
+ return { dataUrl: `data:${mimeType};base64,${base64}` };
655
+ } finally {
656
+ if (downloadedPath) {
657
+ fs.unlink(downloadedPath).catch(() => {});
658
+ }
659
+ }
660
+ };
661
+
662
+ export async function handleCatCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
663
+ const commandName = 'cat';
664
+ const commandMessages = resolveAiMessages(commandName);
665
+ const { prompt: rawPrompt, wantsAudio, imageDetail } = parseCatOptions(text || '', resolveCatParseOptions());
666
+
667
+ if (!process.env.OPENAI_API_KEY) {
668
+ logger.warn('handleCatCommand: OPENAI_API_KEY não configurada.');
669
+ await sendAndStore(sock, remoteJid, { text: commandMessages.openAiNotConfigured }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
670
+ return;
671
+ }
672
+
673
+ await reactToMessage(sock, remoteJid, messageInfo);
674
+
675
+ if (isAiCommandPremiumOnly(commandName)) {
676
+ if (!(await isPremiumAllowed(senderJid))) {
677
+ await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage, { commandName });
678
+ return;
679
+ }
680
+ }
681
+
682
+ const imageMedia = findImageMedia(messageInfo);
683
+ const imageResult = await buildImageDataUrl(imageMedia, senderJid);
684
+ if (imageResult.error === 'too_large') {
685
+ const limitMb = Math.round((MAX_IMAGE_BYTES / (1024 * 1024)) * 10) / 10;
686
+ await sendAndStore(sock, remoteJid, { text: applyTemplate(commandMessages.imageTooLarge, { limite_mb: limitMb }) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
687
+ return;
688
+ }
689
+
690
+ if (imageResult.error === 'download_failed') {
691
+ await sendAndStore(sock, remoteJid, { text: commandMessages.imageDownloadFailed }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
692
+ return;
693
+ }
694
+
695
+ const sessionKey = buildSessionKey(remoteJid, senderJid);
696
+ const session = sessionCache.get(sessionKey);
697
+ const userPrompt = await aiPromptStore.getPrompt(senderJid);
698
+ const userPreference = typeof userPrompt === 'string' ? userPrompt.trim() : '';
699
+ const effectiveSystemPrompt = userPreference || BASE_SYSTEM_PROMPT;
700
+ const effectiveImagePrompt = userPreference || DEFAULT_IMAGE_PROMPT;
701
+
702
+ const effectivePrompt = rawPrompt || (imageResult.dataUrl ? effectiveImagePrompt : '');
703
+ if (!effectivePrompt && !imageResult.dataUrl) {
704
+ await sendUsage(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
705
+ return;
706
+ }
707
+
708
+ const content = [];
709
+ if (effectivePrompt) {
710
+ content.push({ type: 'input_text', text: effectivePrompt });
711
+ }
712
+ if (imageResult.dataUrl) {
713
+ const imagePayload = { type: 'input_image', image_url: imageResult.dataUrl };
714
+ if (imageDetail) {
715
+ imagePayload.detail = imageDetail;
716
+ }
717
+ content.push(imagePayload);
718
+ }
719
+
720
+ const payload = {
721
+ model: OPENAI_MODEL,
722
+ input: [
723
+ {
724
+ role: 'user',
725
+ content,
726
+ },
727
+ ],
728
+ };
729
+
730
+ if (effectiveSystemPrompt) {
731
+ payload.instructions = effectiveSystemPrompt;
732
+ }
733
+
734
+ if (session?.previousResponseId) {
735
+ payload.previous_response_id = session.previousResponseId;
736
+ }
737
+
738
+ try {
739
+ const client = getClient();
740
+ const response = await callOpenAI(() => client.responses.create(payload), 'responses.create', OPENAI_TIMEOUT);
741
+ const outputText = response.output_text?.trim();
742
+
743
+ sessionCache.set(sessionKey, {
744
+ previousResponseId: response.id,
745
+ updatedAt: Date.now(),
746
+ });
747
+
748
+ if (!outputText) {
749
+ await sendAndStore(sock, remoteJid, { text: commandMessages.emptyResponse }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
750
+ return;
751
+ }
752
+
753
+ if (wantsAudio) {
754
+ if (outputText.length > TTS_MAX_CHARS) {
755
+ await sendAndStore(sock, remoteJid, { text: commandMessages.longAudioFallback }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
756
+ } else {
757
+ try {
758
+ const audioResponse = await callOpenAI(
759
+ () =>
760
+ client.audio.speech.create({
761
+ model: OPENAI_TTS_MODEL,
762
+ voice: OPENAI_TTS_VOICE,
763
+ input: outputText,
764
+ response_format: TTS_OUTPUT_FORMAT,
765
+ }),
766
+ 'audio.speech.create',
767
+ OPENAI_TIMEOUT,
768
+ );
769
+ const audioBuffer = Buffer.from(await audioResponse.arrayBuffer());
770
+ await sendAndStore(
771
+ sock,
772
+ remoteJid,
773
+ {
774
+ audio: audioBuffer,
775
+ mimetype: TTS_MIME_TYPE,
776
+ ptt: OPENAI_TTS_PTT,
777
+ },
778
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
779
+ );
780
+ return;
781
+ } catch (audioError) {
782
+ logger.error('handleCatCommand: erro ao gerar audio.', audioError);
783
+ await sendAndStore(sock, remoteJid, { text: commandMessages.audioFailedFallback }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
784
+ }
785
+ }
786
+ }
787
+
788
+ await sendAndStore(sock, remoteJid, { text: `🐈‍⬛ ${outputText}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
789
+ } catch (error) {
790
+ logger.error('handleCatCommand: erro ao chamar OpenAI.', error);
791
+ await sendAndStore(
792
+ sock,
793
+ remoteJid,
794
+ {
795
+ text: commandMessages.genericAiError,
796
+ },
797
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
798
+ );
799
+ }
800
+ }
801
+
802
+ export async function handleCatImageCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
803
+ const commandName = 'catimg';
804
+ const commandMessages = resolveAiMessages(commandName);
805
+ const { prompt, toolOptions, errors } = parseImageGenOptions(text || '', resolveCatImageGenerationOptions());
806
+
807
+ if (!process.env.OPENAI_API_KEY) {
808
+ logger.warn('handleCatImageCommand: OPENAI_API_KEY não configurada.');
809
+ await sendAndStore(sock, remoteJid, { text: commandMessages.openAiNotConfigured }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
810
+ return;
811
+ }
812
+
813
+ await reactToMessage(sock, remoteJid, messageInfo);
814
+
815
+ if (isAiCommandPremiumOnly(commandName)) {
816
+ if (!(await isPremiumAllowed(senderJid))) {
817
+ await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage, { commandName });
818
+ return;
819
+ }
820
+ }
821
+
822
+ const imageMedia = findImageMedia(messageInfo);
823
+ const imageResult = await buildImageDataUrl(imageMedia, senderJid);
824
+ if (imageResult.error === 'too_large') {
825
+ const limitMb = Math.round((MAX_IMAGE_BYTES / (1024 * 1024)) * 10) / 10;
826
+ await sendAndStore(sock, remoteJid, { text: applyTemplate(commandMessages.imageTooLarge, { limite_mb: limitMb }) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
827
+ return;
828
+ }
829
+
830
+ if (imageResult.error === 'download_failed') {
831
+ await sendAndStore(sock, remoteJid, { text: commandMessages.imageDownloadFailed }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
832
+ return;
833
+ }
834
+
835
+ if (!prompt) {
836
+ await sendImageUsage(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
837
+ return;
838
+ }
839
+
840
+ if (errors.length) {
841
+ await sendAndStore(
842
+ sock,
843
+ remoteJid,
844
+ {
845
+ text: applyTemplate(commandMessages.invalidOptions, {
846
+ detalhes: errors.join(', '),
847
+ prefix: commandPrefix,
848
+ }),
849
+ },
850
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
851
+ );
852
+ return;
853
+ }
854
+
855
+ const userPrompt = await aiPromptStore.getPrompt(senderJid);
856
+ const userPreference = typeof userPrompt === 'string' ? userPrompt.trim() : '';
857
+ const effectiveSystemPrompt = userPreference || BASE_SYSTEM_PROMPT;
858
+
859
+ const content = [];
860
+ content.push({ type: 'input_text', text: prompt });
861
+ if (imageResult.dataUrl) {
862
+ content.push({ type: 'input_image', image_url: imageResult.dataUrl });
863
+ }
864
+
865
+ const imageTool = { type: 'image_generation', ...toolOptions };
866
+
867
+ const payload = {
868
+ model: OPENAI_IMAGE_MODEL,
869
+ input: [
870
+ {
871
+ role: 'user',
872
+ content,
873
+ },
874
+ ],
875
+ tools: [imageTool],
876
+ tool_choice: { type: 'image_generation' },
877
+ };
878
+
879
+ if (effectiveSystemPrompt) {
880
+ payload.instructions = effectiveSystemPrompt;
881
+ }
882
+
883
+ const sessionKey = buildSessionKey(remoteJid, senderJid, 'image');
884
+ const session = sessionCache.get(sessionKey);
885
+ if (session?.previousResponseId) {
886
+ payload.previous_response_id = session.previousResponseId;
887
+ }
888
+
889
+ try {
890
+ const client = getClient();
891
+ const response = await callOpenAI(() => client.responses.create(payload), 'responses.create.image', OPENAI_IMAGE_TIMEOUT);
892
+ const outputText = response.output_text?.trim();
893
+
894
+ sessionCache.set(sessionKey, {
895
+ previousResponseId: response.id,
896
+ updatedAt: Date.now(),
897
+ });
898
+
899
+ const imageOutputs = Array.isArray(response.output) ? response.output.filter((output) => output.type === 'image_generation_call' && output.result) : [];
900
+ const imageBase64 = imageOutputs[0]?.result;
901
+
902
+ if (!imageBase64) {
903
+ if (outputText) {
904
+ await sendAndStore(sock, remoteJid, { text: `🖼️ ${outputText}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
905
+ return;
906
+ }
907
+
908
+ await sendAndStore(sock, remoteJid, { text: commandMessages.emptyResponse }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
909
+ return;
910
+ }
911
+
912
+ const outputFormat = toolOptions.output_format || 'png';
913
+ const mimeByFormat = {
914
+ png: 'image/png',
915
+ jpeg: 'image/jpeg',
916
+ webp: 'image/webp',
917
+ };
918
+ const mimetype = mimeByFormat[outputFormat] || 'image/png';
919
+ const imageBuffer = Buffer.from(imageBase64, 'base64');
920
+ const caption = outputText ? `🖼️ ${outputText}` : '🖼️ Imagem gerada.';
921
+
922
+ await sendAndStore(sock, remoteJid, { image: imageBuffer, caption, mimetype }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
923
+ } catch (error) {
924
+ logger.error('handleCatImageCommand: erro ao chamar OpenAI.', error);
925
+ await sendAndStore(
926
+ sock,
927
+ remoteJid,
928
+ {
929
+ text: commandMessages.genericAiError,
930
+ },
931
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
932
+ );
933
+ }
934
+ }
935
+
936
+ export async function handleCatPromptCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
937
+ const commandName = 'catprompt';
938
+ const commandMessages = resolveAiMessages(commandName);
939
+ const promptMaxChars = resolveCatPromptMaxChars();
940
+ const promptText = text?.trim();
941
+ if (!promptText) {
942
+ await sendPromptUsage(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
943
+ return;
944
+ }
945
+
946
+ if (isAiCommandPremiumOnly(commandName)) {
947
+ if (!(await isPremiumAllowed(senderJid))) {
948
+ await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage, { commandName });
949
+ return;
950
+ }
951
+ }
952
+
953
+ const lower = promptText.toLowerCase();
954
+ if (lower === 'reset' || lower === 'default' || lower === 'padrao' || lower === 'padrão') {
955
+ await aiPromptStore.clearPrompt(senderJid);
956
+ await sendAndStore(sock, remoteJid, { text: commandMessages.promptResetSuccess }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
957
+ return;
958
+ }
959
+
960
+ if (promptText.length > promptMaxChars) {
961
+ await sendAndStore(sock, remoteJid, { text: applyTemplate(commandMessages.promptTooLong, { max_chars: promptMaxChars }) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
962
+ return;
963
+ }
964
+
965
+ await aiPromptStore.setPrompt(senderJid, promptText);
966
+ await sendAndStore(sock, remoteJid, { text: commandMessages.promptUpdateSuccess }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
967
+ }