@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,795 @@
1
+ import axios from 'axios';
2
+ import { createCanvas, loadImage } from 'canvas';
3
+ import logger from '#logger';
4
+
5
+ const CANVAS_SIZE = 1024;
6
+ const PANEL_RADIUS = 24;
7
+ const IMAGE_CACHE_TTL_MS = Math.max(2 * 60 * 1000, Number(process.env.RPG_BATTLE_CANVAS_CACHE_TTL_MS) || 10 * 60 * 1000);
8
+ const IMAGE_CACHE_LIMIT = Math.max(20, Number(process.env.RPG_BATTLE_CANVAS_CACHE_LIMIT) || 120);
9
+ const IMAGE_TIMEOUT_MS = Math.max(2_000, Number(process.env.RPG_BATTLE_CANVAS_TIMEOUT_MS) || 7_000);
10
+
11
+ const imageCache = globalThis.__omnizapBattleCanvasImageCache instanceof Map ? globalThis.__omnizapBattleCanvasImageCache : new Map();
12
+ globalThis.__omnizapBattleCanvasImageCache = imageCache;
13
+
14
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
15
+ const toInt = (value, fallback = 0) => {
16
+ const parsed = Number(value);
17
+ return Number.isFinite(parsed) ? Math.trunc(parsed) : fallback;
18
+ };
19
+ const normalizeText = (value) =>
20
+ String(value || '')
21
+ .toLowerCase()
22
+ .normalize('NFD')
23
+ .replace(/[\u0300-\u036f]/g, '');
24
+
25
+ const hexToRgb = (hex) => {
26
+ const raw = String(hex || '')
27
+ .trim()
28
+ .replace('#', '');
29
+ if (!/^[a-f0-9]{6}$/i.test(raw)) return null;
30
+ return {
31
+ r: Number.parseInt(raw.slice(0, 2), 16),
32
+ g: Number.parseInt(raw.slice(2, 4), 16),
33
+ b: Number.parseInt(raw.slice(4, 6), 16),
34
+ };
35
+ };
36
+
37
+ const toRgba = (hex, alpha = 1) => {
38
+ const rgb = hexToRgb(hex);
39
+ if (!rgb) return `rgba(255,255,255,${clamp(alpha, 0, 1)})`;
40
+ return `rgba(${rgb.r},${rgb.g},${rgb.b},${clamp(alpha, 0, 1)})`;
41
+ };
42
+
43
+ const isLightHex = (hex) => {
44
+ const rgb = hexToRgb(hex);
45
+ if (!rgb) return false;
46
+ const luminance = rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114;
47
+ return luminance >= 160;
48
+ };
49
+
50
+ const toRatio = (currentHp, maxHp) => {
51
+ const max = Math.max(1, toInt(maxHp, 1));
52
+ const current = clamp(toInt(currentHp, 0), 0, max);
53
+ return current / max;
54
+ };
55
+
56
+ const hpColorByRatio = (ratio) => {
57
+ if (ratio <= 0.25) return '#ef4444';
58
+ if (ratio <= 0.55) return '#f59e0b';
59
+ return '#22c55e';
60
+ };
61
+
62
+ const TYPE_COLORS = new Map([
63
+ ['normal', '#a8a77a'],
64
+ ['fire', '#ee8130'],
65
+ ['water', '#6390f0'],
66
+ ['electric', '#f7d02c'],
67
+ ['grass', '#7ac74c'],
68
+ ['ice', '#96d9d6'],
69
+ ['fighting', '#c22e28'],
70
+ ['poison', '#a33ea1'],
71
+ ['ground', '#e2bf65'],
72
+ ['flying', '#a98ff3'],
73
+ ['psychic', '#f95587'],
74
+ ['bug', '#a6b91a'],
75
+ ['rock', '#b6a136'],
76
+ ['ghost', '#735797'],
77
+ ['dragon', '#6f35fc'],
78
+ ['dark', '#705746'],
79
+ ['steel', '#b7b7ce'],
80
+ ['fairy', '#d685ad'],
81
+ ]);
82
+
83
+ const STATUS_MAP = new Map([
84
+ ['burn', { icon: '🔥', label: 'BRN', color: '#f97316' }],
85
+ ['brn', { icon: '🔥', label: 'BRN', color: '#f97316' }],
86
+ ['poison', { icon: '☠', label: 'PSN', color: '#a855f7' }],
87
+ ['psn', { icon: '☠', label: 'PSN', color: '#a855f7' }],
88
+ ['toxic', { icon: '☠', label: 'TOX', color: '#7c3aed' }],
89
+ ['bad-poison', { icon: '☠', label: 'TOX', color: '#7c3aed' }],
90
+ ['paralyze', { icon: '⚡', label: 'PAR', color: '#facc15' }],
91
+ ['paralysis', { icon: '⚡', label: 'PAR', color: '#facc15' }],
92
+ ['par', { icon: '⚡', label: 'PAR', color: '#facc15' }],
93
+ ['sleep', { icon: '💤', label: 'SLP', color: '#60a5fa' }],
94
+ ['slp', { icon: '💤', label: 'SLP', color: '#60a5fa' }],
95
+ ['freeze', { icon: '❄️', label: 'FRZ', color: '#67e8f9' }],
96
+ ['frz', { icon: '❄️', label: 'FRZ', color: '#67e8f9' }],
97
+ ['confusion', { icon: '🌀', label: 'CNF', color: '#fbbf24' }],
98
+ ['conf', { icon: '🌀', label: 'CNF', color: '#fbbf24' }],
99
+ ]);
100
+
101
+ const TYPE_ICONS = new Map([
102
+ ['normal', '⚪'],
103
+ ['fire', '🔥'],
104
+ ['water', '💧'],
105
+ ['electric', '⚡'],
106
+ ['grass', '🍃'],
107
+ ['ice', '❄️'],
108
+ ['fighting', '🥊'],
109
+ ['poison', '☠️'],
110
+ ['ground', '🟤'],
111
+ ['flying', '🪽'],
112
+ ['psychic', '🔮'],
113
+ ['bug', '🐞'],
114
+ ['rock', '🪨'],
115
+ ['ghost', '👻'],
116
+ ['dragon', '🐉'],
117
+ ['dark', '🌑'],
118
+ ['steel', '⚙️'],
119
+ ['fairy', '✨'],
120
+ ]);
121
+
122
+ const ROLE_THEMES = {
123
+ player: {
124
+ icon: '👤',
125
+ label: 'JOGADOR',
126
+ accent: '#38bdf8',
127
+ panelBg: 'rgba(8,47,73,0.45)',
128
+ },
129
+ enemy: {
130
+ icon: '⚠️',
131
+ label: 'INIMIGO',
132
+ accent: '#fb7185',
133
+ panelBg: 'rgba(76,5,25,0.42)',
134
+ },
135
+ };
136
+
137
+ const BIOME_THEMES = [
138
+ {
139
+ match: ['forest', 'floresta', 'grass', 'jungle', 'leaf'],
140
+ colors: ['#134e4a', '#166534', '#0f766e'],
141
+ },
142
+ { match: ['volcano', 'fire', 'magma', 'lava'], colors: ['#7f1d1d', '#b91c1c', '#f97316'] },
143
+ { match: ['water', 'ocean', 'sea', 'lake', 'river'], colors: ['#082f49', '#0c4a6e', '#0369a1'] },
144
+ { match: ['mountain', 'rock', 'cave'], colors: ['#292524', '#44403c', '#57534e'] },
145
+ { match: ['desert', 'sand', 'ground'], colors: ['#78350f', '#92400e', '#b45309'] },
146
+ { match: ['ice', 'snow', 'tundra'], colors: ['#0f172a', '#1d4ed8', '#22d3ee'] },
147
+ ];
148
+
149
+ const trimText = (value, max = 120) => {
150
+ const raw = String(value || '')
151
+ .replace(/\s+/g, ' ')
152
+ .trim();
153
+ if (!raw) return '';
154
+ if (raw.length <= max) return raw;
155
+ return `${raw.slice(0, Math.max(24, max - 1)).trimEnd()}…`;
156
+ };
157
+
158
+ const fitText = (ctx, text, maxWidth, baseSize, weight = 700, family = 'Sans') => {
159
+ let size = baseSize;
160
+ while (size > 14) {
161
+ ctx.font = `${weight} ${size}px ${family}`;
162
+ if (ctx.measureText(text).width <= maxWidth) return;
163
+ size -= 1;
164
+ }
165
+ };
166
+
167
+ const drawRoundRect = (ctx, x, y, width, height, radius, fillStyle) => {
168
+ ctx.beginPath();
169
+ ctx.moveTo(x + radius, y);
170
+ ctx.lineTo(x + width - radius, y);
171
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
172
+ ctx.lineTo(x + width, y + height - radius);
173
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
174
+ ctx.lineTo(x + radius, y + height);
175
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
176
+ ctx.lineTo(x, y + radius);
177
+ ctx.quadraticCurveTo(x, y, x + radius, y);
178
+ ctx.closePath();
179
+ if (fillStyle) {
180
+ ctx.fillStyle = fillStyle;
181
+ ctx.fill();
182
+ }
183
+ };
184
+
185
+ const cleanupCache = () => {
186
+ const now = Date.now();
187
+ for (const [key, entry] of imageCache.entries()) {
188
+ if (!entry || entry.expiresAt <= now) {
189
+ imageCache.delete(key);
190
+ }
191
+ }
192
+
193
+ if (imageCache.size <= IMAGE_CACHE_LIMIT) return;
194
+ const overflow = imageCache.size - IMAGE_CACHE_LIMIT;
195
+ const keys = [...imageCache.keys()].slice(0, overflow);
196
+ keys.forEach((key) => imageCache.delete(key));
197
+ };
198
+
199
+ const resolveImage = async (imageUrl) => {
200
+ const url = String(imageUrl || '').trim();
201
+ if (!url || !/^https?:\/\//i.test(url)) return null;
202
+
203
+ cleanupCache();
204
+ const cached = imageCache.get(url);
205
+ if (cached && cached.expiresAt > Date.now()) return cached.image;
206
+
207
+ try {
208
+ const response = await axios.get(url, {
209
+ responseType: 'arraybuffer',
210
+ timeout: IMAGE_TIMEOUT_MS,
211
+ headers: { Accept: 'image/*' },
212
+ });
213
+ const image = await loadImage(Buffer.from(response.data));
214
+ imageCache.set(url, { image, expiresAt: Date.now() + IMAGE_CACHE_TTL_MS });
215
+ return image;
216
+ } catch (error) {
217
+ imageCache.set(url, { image: null, expiresAt: Date.now() + 90_000 });
218
+ logger.debug('Falha ao carregar sprite para frame de batalha.', {
219
+ imageUrl: url,
220
+ error: error.message,
221
+ });
222
+ return null;
223
+ }
224
+ };
225
+
226
+ const resolveBiomeTheme = (biomeLabel) => {
227
+ const normalized = String(biomeLabel || '')
228
+ .trim()
229
+ .toLowerCase();
230
+ if (!normalized) return ['#0f172a', '#1f2937', '#334155'];
231
+ for (const theme of BIOME_THEMES) {
232
+ if (theme.match.some((entry) => normalized.includes(entry))) return theme.colors;
233
+ }
234
+ return ['#1d4ed8', '#1e3a8a', '#334155'];
235
+ };
236
+
237
+ const normalizeTypeList = (types) => {
238
+ if (!Array.isArray(types)) return [];
239
+ return types
240
+ .map((entry) =>
241
+ String(entry || '')
242
+ .trim()
243
+ .toLowerCase(),
244
+ )
245
+ .filter(Boolean)
246
+ .slice(0, 3);
247
+ };
248
+
249
+ const normalizeStatuses = (pokemon = {}) => {
250
+ const candidates = [pokemon?.status, pokemon?.nonVolatileStatus, pokemon?.condition, pokemon?.statusCondition, ...(Array.isArray(pokemon?.statusEffects) ? pokemon.statusEffects : []), ...(Array.isArray(pokemon?.conditions) ? pokemon.conditions : []), ...(Array.isArray(pokemon?.statuses) ? pokemon.statuses : [])];
251
+ const found = [];
252
+ for (const candidate of candidates) {
253
+ const key = String(candidate || '')
254
+ .trim()
255
+ .toLowerCase();
256
+ if (!key) continue;
257
+ const normalized = STATUS_MAP.get(key);
258
+ if (!normalized) continue;
259
+ if (!found.some((entry) => entry.label === normalized.label)) {
260
+ found.push(normalized);
261
+ }
262
+ }
263
+ return found.slice(0, 3);
264
+ };
265
+
266
+ const drawBackground = (ctx, biomeLabel, enemyTypes = []) => {
267
+ const [c1, c2, c3] = resolveBiomeTheme(biomeLabel);
268
+ const gradient = ctx.createLinearGradient(0, 0, CANVAS_SIZE, CANVAS_SIZE);
269
+ gradient.addColorStop(0, c1);
270
+ gradient.addColorStop(0.52, c2);
271
+ gradient.addColorStop(1, c3);
272
+ ctx.fillStyle = gradient;
273
+ ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
274
+
275
+ const enemyType = normalizeTypeList(enemyTypes)[0];
276
+ const enemyTypeColor = TYPE_COLORS.get(enemyType);
277
+ if (enemyTypeColor) {
278
+ const typeAura = ctx.createRadialGradient(CANVAS_SIZE * 0.74, CANVAS_SIZE * 0.3, 50, CANVAS_SIZE * 0.74, CANVAS_SIZE * 0.3, 360);
279
+ typeAura.addColorStop(0, toRgba(enemyTypeColor, 0.35));
280
+ typeAura.addColorStop(1, 'rgba(255,255,255,0)');
281
+ ctx.fillStyle = typeAura;
282
+ ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
283
+ }
284
+
285
+ ctx.globalAlpha = 0.09;
286
+ ctx.strokeStyle = '#ffffff';
287
+ for (let i = 0; i <= 14; i += 1) {
288
+ const y = 160 + i * 42;
289
+ ctx.beginPath();
290
+ ctx.moveTo(40, y);
291
+ ctx.lineTo(CANVAS_SIZE - 40, y + (i % 2 === 0 ? 10 : -10));
292
+ ctx.stroke();
293
+ }
294
+ ctx.globalAlpha = 1;
295
+
296
+ const vignette = ctx.createRadialGradient(CANVAS_SIZE / 2, CANVAS_SIZE / 2, CANVAS_SIZE * 0.2, CANVAS_SIZE / 2, CANVAS_SIZE / 2, CANVAS_SIZE * 0.65);
297
+ vignette.addColorStop(0, 'rgba(255,255,255,0)');
298
+ vignette.addColorStop(1, 'rgba(0,0,0,0.45)');
299
+ ctx.fillStyle = vignette;
300
+ ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
301
+ };
302
+
303
+ const drawArena = (ctx) => {
304
+ ctx.globalAlpha = 0.22;
305
+ drawRoundRect(ctx, 80, 610, 360, 120, 90, '#cbd5e1');
306
+ drawRoundRect(ctx, CANVAS_SIZE - 440, 270, 360, 120, 90, '#cbd5e1');
307
+ ctx.globalAlpha = 1;
308
+ };
309
+
310
+ const drawShinyAura = (ctx, x, y, width, height) => {
311
+ const gradient = ctx.createRadialGradient(x, y, 28, x, y, Math.max(width, height) * 0.65);
312
+ gradient.addColorStop(0, 'rgba(252,211,77,0.66)');
313
+ gradient.addColorStop(0.4, 'rgba(59,130,246,0.26)');
314
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
315
+ ctx.fillStyle = gradient;
316
+ ctx.beginPath();
317
+ ctx.ellipse(x, y, width * 0.72, height * 0.6, 0, 0, Math.PI * 2);
318
+ ctx.fill();
319
+ };
320
+
321
+ const drawCombatAura = (ctx, { centerX, centerY, width, height, accent = '#ffffff', alpha = 0.2 }) => {
322
+ const gradient = ctx.createRadialGradient(centerX, centerY + 18, 24, centerX, centerY + 18, Math.max(width, height) * 0.65);
323
+ gradient.addColorStop(0, `${accent}55`);
324
+ gradient.addColorStop(0.5, `${accent}20`);
325
+ gradient.addColorStop(1, 'rgba(255,255,255,0)');
326
+ ctx.globalAlpha = clamp(alpha, 0.08, 0.35);
327
+ ctx.fillStyle = gradient;
328
+ ctx.beginPath();
329
+ ctx.ellipse(centerX, centerY + 18, width * 0.62, height * 0.5, 0, 0, Math.PI * 2);
330
+ ctx.fill();
331
+ ctx.globalAlpha = 1;
332
+ };
333
+
334
+ const drawPokemon = async (ctx, pokemon = {}, opts = {}) => {
335
+ const { centerX = 0, centerY = 0, maxWidth = 260, maxHeight = 260, facing = 'right', isPrimary = false, role = 'player', isActive = false, offsetX = 0, offsetY = 0, turn = 1 } = opts;
336
+ const finalCenterX = centerX + toInt(offsetX, 0);
337
+ const finalCenterY = centerY + toInt(offsetY, 0);
338
+ const image = await resolveImage(pokemon?.imageUrl || pokemon?.sprite);
339
+ const scaleBonus = isPrimary ? 1.08 : 0.96;
340
+ const targetMaxW = maxWidth * scaleBonus;
341
+ const targetMaxH = maxHeight * scaleBonus;
342
+ const isShiny = Boolean(pokemon?.isShiny);
343
+ const roleTheme = ROLE_THEMES[role] || ROLE_THEMES.player;
344
+ const activePulse = isActive ? (toInt(turn, 1) % 2 === 0 ? 0.32 : 0.2) : 0;
345
+
346
+ if (image) {
347
+ const ratio = Math.min(targetMaxW / image.width, targetMaxH / image.height);
348
+ const width = Math.max(40, Math.round(image.width * ratio));
349
+ const height = Math.max(40, Math.round(image.height * ratio));
350
+ const drawX = finalCenterX - width / 2;
351
+ const drawY = finalCenterY - height / 2;
352
+
353
+ drawCombatAura(ctx, {
354
+ centerX: finalCenterX,
355
+ centerY: finalCenterY + (isPrimary ? 12 : -8),
356
+ width,
357
+ height,
358
+ accent: roleTheme.accent,
359
+ alpha: isActive ? 0.3 : isPrimary ? 0.23 : 0.17,
360
+ });
361
+
362
+ if (isShiny) drawShinyAura(ctx, finalCenterX, finalCenterY, width, height);
363
+ if (isActive) {
364
+ ctx.globalAlpha = activePulse;
365
+ ctx.strokeStyle = roleTheme.accent;
366
+ ctx.lineWidth = 5;
367
+ ctx.beginPath();
368
+ ctx.ellipse(finalCenterX, finalCenterY + 14, width * 0.54, height * 0.44, 0, 0, Math.PI * 2);
369
+ ctx.stroke();
370
+ ctx.globalAlpha = 1;
371
+ }
372
+
373
+ ctx.save();
374
+ if (facing === 'left') {
375
+ ctx.translate(finalCenterX, finalCenterY);
376
+ ctx.scale(-1, 1);
377
+ ctx.drawImage(image, -width / 2, -height / 2, width, height);
378
+ } else {
379
+ ctx.drawImage(image, drawX, drawY, width, height);
380
+ }
381
+ ctx.restore();
382
+ return;
383
+ }
384
+
385
+ const fallbackW = Math.round(targetMaxW * 0.75);
386
+ const fallbackH = Math.round(targetMaxH * 0.75);
387
+ drawCombatAura(ctx, {
388
+ centerX: finalCenterX,
389
+ centerY: finalCenterY,
390
+ width: fallbackW,
391
+ height: fallbackH,
392
+ accent: roleTheme.accent,
393
+ alpha: isActive ? 0.28 : isPrimary ? 0.2 : 0.16,
394
+ });
395
+ if (isShiny) drawShinyAura(ctx, finalCenterX, finalCenterY, fallbackW, fallbackH);
396
+ if (isActive) {
397
+ ctx.globalAlpha = activePulse;
398
+ ctx.strokeStyle = roleTheme.accent;
399
+ ctx.lineWidth = 4;
400
+ ctx.beginPath();
401
+ ctx.ellipse(finalCenterX, finalCenterY + 8, fallbackW * 0.48, fallbackH * 0.4, 0, 0, Math.PI * 2);
402
+ ctx.stroke();
403
+ ctx.globalAlpha = 1;
404
+ }
405
+ ctx.fillStyle = 'rgba(255,255,255,0.14)';
406
+ drawRoundRect(ctx, finalCenterX - fallbackW / 2, finalCenterY - fallbackH / 2, fallbackW, fallbackH, 28, 'rgba(255,255,255,0.14)');
407
+ ctx.fillStyle = '#ffffff';
408
+ fitText(ctx, trimText(pokemon?.displayName || pokemon?.name || 'Pokemon', 18), fallbackW - 28, 30, 700);
409
+ ctx.textAlign = 'center';
410
+ ctx.textBaseline = 'middle';
411
+ ctx.fillText(trimText(pokemon?.displayName || pokemon?.name || 'Pokemon', 18), finalCenterX, finalCenterY);
412
+ };
413
+
414
+ const drawTypeBadges = (ctx, types = [], x, y) => {
415
+ let cursor = x;
416
+ types.forEach((type) => {
417
+ const label = String(type || '')
418
+ .slice(0, 3)
419
+ .toUpperCase();
420
+ const icon = TYPE_ICONS.get(type) || '◼';
421
+ const width = 76;
422
+ const color = TYPE_COLORS.get(type) || '#475569';
423
+ const textColor = isLightHex(color) ? '#0b1220' : '#f8fafc';
424
+ const gradient = ctx.createLinearGradient(cursor, y, cursor + width, y + 24);
425
+ gradient.addColorStop(0, toRgba(color, 0.92));
426
+ gradient.addColorStop(1, toRgba(color, 0.72));
427
+ drawRoundRect(ctx, cursor, y, width, 24, 12, gradient);
428
+ ctx.strokeStyle = toRgba(color, 1);
429
+ ctx.lineWidth = 1.5;
430
+ drawRoundRect(ctx, cursor, y, width, 24, 12);
431
+ ctx.stroke();
432
+ ctx.fillStyle = textColor;
433
+ ctx.font = '700 13px Sans';
434
+ ctx.textAlign = 'center';
435
+ ctx.textBaseline = 'middle';
436
+ ctx.fillText(`${icon} ${label}`, cursor + width / 2, y + 12);
437
+ cursor += width + 8;
438
+ });
439
+ };
440
+
441
+ const drawStatusBadges = (ctx, statuses = [], x, y) => {
442
+ let cursor = x;
443
+ statuses.forEach((entry) => {
444
+ const width = 70;
445
+ drawRoundRect(ctx, cursor, y, width, 24, 12, entry.color);
446
+ ctx.fillStyle = '#111827';
447
+ ctx.font = '700 14px Sans';
448
+ ctx.textAlign = 'center';
449
+ ctx.textBaseline = 'middle';
450
+ ctx.fillText(`${entry.icon} ${entry.label}`, cursor + width / 2, y + 12);
451
+ cursor += width + 8;
452
+ });
453
+ };
454
+
455
+ const drawStatusPanel = (ctx, pokemon = {}, opts = {}) => {
456
+ const { x = 0, y = 0, width = 360, height = 150, align = 'left', role = 'player', turn = 1, isActive = false } = opts;
457
+ const roleTheme = ROLE_THEMES[role] || ROLE_THEMES.player;
458
+
459
+ drawRoundRect(ctx, x, y, width, height, PANEL_RADIUS, roleTheme.panelBg);
460
+ ctx.strokeStyle = `${roleTheme.accent}b3`;
461
+ ctx.lineWidth = 2.5;
462
+ drawRoundRect(ctx, x, y, width, height, PANEL_RADIUS);
463
+ ctx.stroke();
464
+ if (isActive) {
465
+ ctx.globalAlpha = toInt(turn, 1) % 2 === 0 ? 0.45 : 0.28;
466
+ ctx.strokeStyle = roleTheme.accent;
467
+ ctx.lineWidth = 4;
468
+ drawRoundRect(ctx, x - 2, y - 2, width + 4, height + 4, PANEL_RADIUS + 2);
469
+ ctx.stroke();
470
+ ctx.globalAlpha = 1;
471
+ }
472
+ drawRoundRect(ctx, x + 1, y + 1, width - 2, height - 2, PANEL_RADIUS, 'rgba(0,0,0,0)');
473
+ ctx.strokeStyle = 'rgba(255,255,255,0.14)';
474
+ ctx.lineWidth = 1;
475
+ ctx.stroke();
476
+
477
+ const name = trimText(pokemon?.displayName || pokemon?.name || 'Pokemon', 24);
478
+ const level = Math.max(1, toInt(pokemon?.level, 1));
479
+ const hpCurrent = Math.max(0, toInt(pokemon?.currentHp, 0));
480
+ const hpMax = Math.max(1, toInt(pokemon?.maxHp, 1));
481
+ const hpRatio = toRatio(hpCurrent, hpMax);
482
+ const hpColor = hpColorByRatio(hpRatio);
483
+ const types = normalizeTypeList(pokemon?.types);
484
+ const statuses = normalizeStatuses(pokemon);
485
+ const padding = 16;
486
+ const textX = align === 'left' ? x + padding : x + width - padding;
487
+ const roleBadgeW = 128;
488
+ const roleBadgeX = align === 'left' ? x + 12 : x + width - roleBadgeW - 12;
489
+ drawRoundRect(ctx, roleBadgeX, y + 10, roleBadgeW, 24, 12, `${roleTheme.accent}cc`);
490
+ ctx.fillStyle = '#0b1220';
491
+ ctx.font = '700 13px Sans';
492
+ ctx.textAlign = 'center';
493
+ ctx.textBaseline = 'middle';
494
+ ctx.fillText(`${roleTheme.icon} ${roleTheme.label}`, roleBadgeX + roleBadgeW / 2, y + 22);
495
+
496
+ ctx.fillStyle = '#e2e8f0';
497
+ fitText(ctx, name, width - 120, 28, 800);
498
+ ctx.textAlign = align;
499
+ ctx.textBaseline = 'alphabetic';
500
+ ctx.fillText(name, textX, y + 56);
501
+
502
+ ctx.fillStyle = '#93c5fd';
503
+ ctx.font = '700 16px Sans';
504
+ ctx.fillText(`Lv.${level}`, textX, y + 78);
505
+
506
+ const barX = x + padding;
507
+ const barY = y + 90;
508
+ const barW = width - padding * 2;
509
+ const barH = 18;
510
+ drawRoundRect(ctx, barX, barY, barW, barH, 9, 'rgba(15,23,42,0.8)');
511
+ const hpFillW = Math.max(18, Math.round(barW * hpRatio));
512
+ const hpGradient = ctx.createLinearGradient(barX, barY, barX + hpFillW, barY);
513
+ hpGradient.addColorStop(0, '#e2e8f0');
514
+ hpGradient.addColorStop(0.22, hpColorByRatio(clamp(hpRatio + 0.2, 0, 1)));
515
+ hpGradient.addColorStop(1, hpColor);
516
+ drawRoundRect(ctx, barX, barY, hpFillW, barH, 9, hpGradient);
517
+ if (hpRatio <= 0.3) {
518
+ const pulse = Math.max(0.14, toInt(turn, 1) % 2 === 0 ? 0.32 : 0.2);
519
+ ctx.globalAlpha = pulse;
520
+ ctx.strokeStyle = '#ef4444';
521
+ ctx.lineWidth = 3;
522
+ drawRoundRect(ctx, barX - 2, barY - 2, barW + 4, barH + 4, 11);
523
+ ctx.stroke();
524
+ ctx.globalAlpha = 1;
525
+ }
526
+ ctx.fillStyle = 'rgba(255,255,255,0.35)';
527
+ drawRoundRect(ctx, barX + 2, barY + 2, Math.max(10, hpFillW - 4), 4, 4, 'rgba(255,255,255,0.35)');
528
+ ctx.fillStyle = '#f8fafc';
529
+ ctx.font = '600 12px Sans';
530
+ ctx.textAlign = 'center';
531
+ ctx.fillText(`${hpCurrent}/${hpMax}`, barX + barW / 2, barY + 31);
532
+
533
+ const badgeX = x + padding;
534
+ drawTypeBadges(ctx, types, badgeX, y + 122);
535
+ if (statuses.length) {
536
+ drawStatusBadges(ctx, statuses, badgeX, y + 150);
537
+ }
538
+ };
539
+
540
+ const resolveActionTone = (actionText) => {
541
+ const raw = String(actionText || '').trim();
542
+ const normalized = normalizeText(raw);
543
+ if (normalized.includes('vitoria') || normalized.includes('venceu') || normalized.includes('desmaiou') || normalized.includes('derrot')) {
544
+ return {
545
+ icon: '🏆',
546
+ color: '#fde68a',
547
+ subline: 'Vitória garantida!',
548
+ badge: 'FINAL',
549
+ weight: 'high',
550
+ };
551
+ }
552
+ if (normalized.includes('captur') || normalized.includes('poke bola') || normalized.includes('pokebola')) {
553
+ return { icon: '🎯', color: '#fbcfe8', subline: null, badge: 'CAPTURA', weight: 'medium' };
554
+ }
555
+ if (normalized.includes('dano') || normalized.includes('causou') || normalized.includes('atac')) {
556
+ return { icon: '💥', color: '#fecaca', subline: null, badge: 'IMPACTO', weight: 'medium' };
557
+ }
558
+ if (normalized.includes('curou') || normalized.includes('recuper')) {
559
+ return { icon: '✨', color: '#bbf7d0', subline: null, badge: 'SUPORTE', weight: 'medium' };
560
+ }
561
+ if (normalized.includes('apareceu') || normalized.includes('inici')) {
562
+ return { icon: '🌀', color: '#bfdbfe', subline: null, badge: 'INICIO', weight: 'low' };
563
+ }
564
+ return { icon: '⚔️', color: '#bfdbfe', subline: null, badge: 'ACAO', weight: 'low' };
565
+ };
566
+
567
+ const resolveSecondaryAction = ({ logLines = [], primaryAction = '' }) => {
568
+ const primary = normalizeText(primaryAction);
569
+ const lines = Array.isArray(logLines) ? logLines : [];
570
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
571
+ const line = trimText(lines[index], 88);
572
+ const normalized = normalizeText(line);
573
+ if (!line || !normalized) continue;
574
+ if (primary && (normalized === primary || primary.includes(normalized))) continue;
575
+ if (normalized.includes('turno') || normalized.includes('use /rpg') || normalized.includes('hp:')) continue;
576
+ return line;
577
+ }
578
+ return null;
579
+ };
580
+
581
+ const inferActiveRole = ({ actionText = '', turn = 1, leftPokemon = {}, rightPokemon = {} }) => {
582
+ const normalized = normalizeText(actionText);
583
+ const leftName = normalizeText(leftPokemon?.displayName || leftPokemon?.name || '');
584
+ const rightName = normalizeText(rightPokemon?.displayName || rightPokemon?.name || '');
585
+ const actionHint = /(usou|atac|causou|acertou|curou|recuper)/.test(normalized);
586
+
587
+ if (leftName && normalized.includes(leftName) && actionHint) return 'player';
588
+ if (rightName && normalized.includes(rightName) && actionHint) return 'enemy';
589
+ if (normalized.includes('seu ') || normalized.includes('jogador') || normalized.includes('voce')) return 'player';
590
+ if (normalized.includes('inimigo') || normalized.includes('adversario') || normalized.includes('oponente')) return 'enemy';
591
+
592
+ return Math.max(1, toInt(turn, 1)) % 2 === 1 ? 'player' : 'enemy';
593
+ };
594
+
595
+ const resolveImpactOffsets = ({ activeRole, actionText, turn = 1 }) => {
596
+ const normalized = normalizeText(actionText);
597
+ const isImpact = normalized.includes('dano') || normalized.includes('causou') || normalized.includes('atac');
598
+ if (!isImpact) return { player: { x: 0, y: 0 }, enemy: { x: 0, y: 0 } };
599
+
600
+ const shake = Math.max(2, toInt(turn, 1) % 2 === 0 ? 8 : 6);
601
+ if (activeRole === 'player') {
602
+ return { player: { x: 0, y: 0 }, enemy: { x: -shake, y: 2 } };
603
+ }
604
+ return { player: { x: shake, y: 2 }, enemy: { x: 0, y: 0 } };
605
+ };
606
+
607
+ const drawActiveTurnIndicator = (ctx, { activeRole = 'player', leftPokemon = {}, rightPokemon = {}, turn = 1 }) => {
608
+ const role = activeRole === 'enemy' ? 'enemy' : 'player';
609
+ const roleTheme = ROLE_THEMES[role] || ROLE_THEMES.player;
610
+ const isPlayer = role === 'player';
611
+ const anchor = isPlayer ? { x: 300, y: 352, labelY: 252 } : { x: 726, y: 176, labelY: 74 };
612
+ const actorName = trimText(isPlayer ? leftPokemon?.displayName || leftPokemon?.name || 'Seu Pokémon' : rightPokemon?.displayName || rightPokemon?.name || 'Inimigo', 18);
613
+ const label = `👉 ${actorName} age agora`;
614
+ ctx.font = '700 17px Sans';
615
+ const labelWidth = clamp(Math.round(ctx.measureText(label).width) + 28, 180, 320);
616
+ const chipX = clamp(anchor.x - labelWidth / 2, 24, CANVAS_SIZE - labelWidth - 24);
617
+ const chipY = anchor.labelY;
618
+ drawRoundRect(ctx, chipX, chipY, labelWidth, 32, 16, toRgba(roleTheme.accent, 0.9));
619
+ ctx.fillStyle = '#0f172a';
620
+ ctx.textAlign = 'center';
621
+ ctx.textBaseline = 'middle';
622
+ ctx.fillText(label, chipX + labelWidth / 2, chipY + 16);
623
+
624
+ ctx.globalAlpha = toInt(turn, 1) % 2 === 0 ? 0.88 : 0.72;
625
+ ctx.fillStyle = toRgba(roleTheme.accent, 0.95);
626
+ ctx.beginPath();
627
+ ctx.moveTo(anchor.x, chipY + 42);
628
+ ctx.lineTo(anchor.x - 14, chipY + 18);
629
+ ctx.lineTo(anchor.x + 14, chipY + 18);
630
+ ctx.closePath();
631
+ ctx.fill();
632
+ ctx.globalAlpha = 1;
633
+ };
634
+
635
+ const drawOverlay = (ctx, { turn = 1, modeLabel = 'Batalha', actionText = '', effectTag = null, logLines = [] }) => {
636
+ const panelX = 88;
637
+ const panelY = 770;
638
+ const panelW = CANVAS_SIZE - 176;
639
+ const panelH = 188;
640
+ drawRoundRect(ctx, panelX, panelY, panelW, panelH, 26, 'rgba(2,6,23,0.68)');
641
+ ctx.strokeStyle = 'rgba(255,255,255,0.18)';
642
+ ctx.lineWidth = 2;
643
+ drawRoundRect(ctx, panelX, panelY, panelW, panelH, 26);
644
+ ctx.stroke();
645
+
646
+ ctx.fillStyle = '#f8fafc';
647
+ ctx.font = '700 30px Sans';
648
+ ctx.textAlign = 'left';
649
+ ctx.fillText(`${modeLabel} • Turno ${Math.max(1, toInt(turn, 1))}`, panelX + 22, panelY + 42);
650
+
651
+ const action = trimText(actionText || 'Aguardando ação do jogador.', 96);
652
+ const tone = resolveActionTone(action);
653
+ const actionStartsWithIcon = /^([^\w\s]|\p{Extended_Pictographic})/u.test(action);
654
+ const decoratedAction = actionStartsWithIcon ? action : `${tone.icon} ${action}`;
655
+ drawRoundRect(ctx, panelX + 12, panelY + 16, 8, panelH - 32, 4, toRgba(String(tone.color || '#bfdbfe'), 0.95));
656
+ const eventBadgeW = 116;
657
+ drawRoundRect(ctx, panelX + panelW - eventBadgeW - 20, panelY + 16, eventBadgeW, 30, 15, toRgba(String(tone.color || '#bfdbfe'), 0.84));
658
+ ctx.fillStyle = '#0f172a';
659
+ ctx.font = '700 14px Sans';
660
+ ctx.textAlign = 'center';
661
+ ctx.textBaseline = 'middle';
662
+ ctx.fillText(String(tone.badge || 'ACAO'), panelX + panelW - eventBadgeW / 2 - 20, panelY + 31);
663
+ ctx.fillStyle = tone.color;
664
+ ctx.font = tone.weight === 'high' ? '700 25px Sans' : tone.weight === 'medium' ? '700 24px Sans' : '600 23px Sans';
665
+ ctx.textAlign = 'left';
666
+ ctx.textBaseline = 'alphabetic';
667
+ ctx.fillText(decoratedAction, panelX + 22, panelY + 92);
668
+ const secondaryAction = resolveSecondaryAction({ logLines, primaryAction: action });
669
+ if (secondaryAction) {
670
+ ctx.fillStyle = 'rgba(226,232,240,0.95)';
671
+ ctx.font = '600 18px Sans';
672
+ ctx.fillText(`• ${trimText(secondaryAction, 72)}`, panelX + 22, panelY + 122);
673
+ }
674
+ if (tone.subline) {
675
+ ctx.fillStyle = 'rgba(255,255,255,0.9)';
676
+ ctx.font = '600 18px Sans';
677
+ ctx.fillText(tone.subline, panelX + 22, panelY + (secondaryAction ? 150 : 124));
678
+ }
679
+
680
+ if (!effectTag) return;
681
+ const palette = effectTag === 'super' ? { label: 'SUPER EFETIVO', color: '#ef4444' } : effectTag === 'weak' ? { label: 'POUCO EFETIVO', color: '#f59e0b' } : effectTag === 'none' ? { label: 'SEM EFEITO', color: '#64748b' } : null;
682
+ if (!palette) return;
683
+ const badgeW = 220;
684
+ drawRoundRect(ctx, CANVAS_SIZE / 2 - badgeW / 2, 38, badgeW, 44, 22, palette.color);
685
+ ctx.fillStyle = '#0f172a';
686
+ ctx.font = '800 18px Sans';
687
+ ctx.textAlign = 'center';
688
+ ctx.textBaseline = 'middle';
689
+ ctx.fillText(palette.label, CANVAS_SIZE / 2, 60);
690
+ };
691
+
692
+ const drawTurnImpact = (ctx, { effectTag = null, turn = 1 }) => {
693
+ const intensity = effectTag === 'super' ? 0.28 : effectTag === 'weak' ? 0.16 : 0.1;
694
+ const pulse = (Math.max(1, toInt(turn, 1)) % 2 === 0 ? 1 : 0.8) * intensity;
695
+ ctx.globalAlpha = pulse;
696
+ const flash = ctx.createRadialGradient(CANVAS_SIZE / 2, CANVAS_SIZE / 2, 40, CANVAS_SIZE / 2, CANVAS_SIZE / 2, 360);
697
+ flash.addColorStop(0, effectTag === 'super' ? '#fca5a5' : '#f8fafc');
698
+ flash.addColorStop(1, 'rgba(255,255,255,0)');
699
+ ctx.fillStyle = flash;
700
+ ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
701
+ ctx.globalAlpha = 1;
702
+ };
703
+
704
+ export const inferEffectTagFromLogs = (logs = []) => {
705
+ const text = (Array.isArray(logs) ? logs : []).join(' ').toLowerCase();
706
+ if (text.includes('super efetivo')) return 'super';
707
+ if (text.includes('pouco efetivo')) return 'weak';
708
+ if (text.includes('não teve efeito') || text.includes('nao teve efeito')) return 'none';
709
+ return null;
710
+ };
711
+
712
+ export const renderBattleFrameCanvas = async ({ leftPokemon = {}, rightPokemon = {}, turn = 1, biomeLabel = '', modeLabel = 'Batalha Pokemon', actionText = '', effectTag = null, activeRole = null, logLines = [] }) => {
713
+ const resolvedActiveRole =
714
+ activeRole === 'player' || activeRole === 'enemy'
715
+ ? activeRole
716
+ : inferActiveRole({
717
+ actionText,
718
+ turn,
719
+ leftPokemon,
720
+ rightPokemon,
721
+ });
722
+ const impactOffsets = resolveImpactOffsets({
723
+ activeRole: resolvedActiveRole,
724
+ actionText,
725
+ turn,
726
+ });
727
+ const canvas = createCanvas(CANVAS_SIZE, CANVAS_SIZE);
728
+ const ctx = canvas.getContext('2d');
729
+ ctx.imageSmoothingEnabled = true;
730
+ ctx.imageSmoothingQuality = 'high';
731
+
732
+ drawBackground(ctx, biomeLabel, rightPokemon?.types);
733
+ drawArena(ctx);
734
+
735
+ await Promise.all([
736
+ drawPokemon(ctx, leftPokemon, {
737
+ centerX: 300,
738
+ centerY: 556,
739
+ maxWidth: 328,
740
+ maxHeight: 328,
741
+ facing: 'right',
742
+ isPrimary: true,
743
+ role: 'player',
744
+ isActive: resolvedActiveRole === 'player',
745
+ offsetX: impactOffsets.player.x,
746
+ offsetY: impactOffsets.player.y,
747
+ turn,
748
+ }),
749
+ drawPokemon(ctx, rightPokemon, {
750
+ centerX: 726,
751
+ centerY: 346,
752
+ maxWidth: 288,
753
+ maxHeight: 288,
754
+ facing: 'left',
755
+ isPrimary: false,
756
+ role: 'enemy',
757
+ isActive: resolvedActiveRole === 'enemy',
758
+ offsetX: impactOffsets.enemy.x,
759
+ offsetY: impactOffsets.enemy.y,
760
+ turn,
761
+ }),
762
+ ]);
763
+ drawActiveTurnIndicator(ctx, {
764
+ activeRole: resolvedActiveRole,
765
+ leftPokemon,
766
+ rightPokemon,
767
+ turn,
768
+ });
769
+
770
+ drawStatusPanel(ctx, leftPokemon, {
771
+ x: 44,
772
+ y: 458,
773
+ width: 420,
774
+ height: 168,
775
+ align: 'left',
776
+ role: 'player',
777
+ turn,
778
+ isActive: resolvedActiveRole === 'player',
779
+ });
780
+ drawStatusPanel(ctx, rightPokemon, {
781
+ x: CANVAS_SIZE - 464,
782
+ y: 110,
783
+ width: 420,
784
+ height: 168,
785
+ align: 'right',
786
+ role: 'enemy',
787
+ turn,
788
+ isActive: resolvedActiveRole === 'enemy',
789
+ });
790
+
791
+ drawTurnImpact(ctx, { effectTag, turn });
792
+ drawOverlay(ctx, { turn, modeLabel, actionText, effectTag, logLines });
793
+
794
+ return canvas.toBuffer('image/png', { compressionLevel: 4 });
795
+ };