@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,400 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+ import { STICKER_DOMAIN_EVENTS } from './domainEvents.js';
3
+ import { publishStickerDomainEvent } from './stickerDomainEventBus.js';
4
+
5
+ /**
6
+ * Converte valores numéricos/booleanos vindos do banco para booleano.
7
+ *
8
+ * @param {unknown} value Valor cru retornado da query.
9
+ * @returns {boolean} Valor booleano normalizado.
10
+ */
11
+ const toBool = (value) => value === true || value === 1;
12
+
13
+ /**
14
+ * Normaliza uma linha da tabela de assets para o formato de domínio.
15
+ *
16
+ * @param {Record<string, unknown>|null|undefined} row Linha bruta da query.
17
+ * @returns {object|null} Asset normalizado ou `null`.
18
+ */
19
+ const normalizeStickerAssetRow = (row) => {
20
+ if (!row) return null;
21
+
22
+ return {
23
+ id: row.id,
24
+ owner_jid: row.owner_jid,
25
+ sha256: row.sha256,
26
+ mimetype: row.mimetype,
27
+ is_animated: toBool(row.is_animated),
28
+ width: row.width !== null && row.width !== undefined ? Number(row.width) : null,
29
+ height: row.height !== null && row.height !== undefined ? Number(row.height) : null,
30
+ size_bytes: row.size_bytes !== null && row.size_bytes !== undefined ? Number(row.size_bytes) : 0,
31
+ storage_path: row.storage_path,
32
+ created_at: row.created_at,
33
+ };
34
+ };
35
+
36
+ /**
37
+ * Busca um asset de figurinha pelo hash SHA-256.
38
+ *
39
+ * @param {string} sha256 Hash SHA-256 do arquivo.
40
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
41
+ * @returns {Promise<object|null>} Asset encontrado.
42
+ */
43
+ export async function findStickerAssetBySha256(sha256, connection = null) {
44
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_ASSET} WHERE sha256 = ? LIMIT 1`, [sha256], connection);
45
+ return normalizeStickerAssetRow(rows?.[0] || null);
46
+ }
47
+
48
+ /**
49
+ * Busca um asset de figurinha pelo ID.
50
+ *
51
+ * @param {string} id ID do asset.
52
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
53
+ * @returns {Promise<object|null>} Asset encontrado.
54
+ */
55
+ export async function findStickerAssetById(id, connection = null) {
56
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_ASSET} WHERE id = ? LIMIT 1`, [id], connection);
57
+ return normalizeStickerAssetRow(rows?.[0] || null);
58
+ }
59
+
60
+ /**
61
+ * Busca múltiplos assets por ID preservando a ordem solicitada.
62
+ *
63
+ * @param {string[]} ids Lista de IDs.
64
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
65
+ * @returns {Promise<object[]>} Lista de assets encontrados.
66
+ */
67
+ export async function findStickerAssetsByIds(ids, connection = null) {
68
+ if (!Array.isArray(ids) || ids.length === 0) return [];
69
+ const uniqueIds = Array.from(new Set(ids.filter(Boolean)));
70
+ if (!uniqueIds.length) return [];
71
+
72
+ const placeholders = uniqueIds.map(() => '?').join(', ');
73
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_ASSET} WHERE id IN (${placeholders})`, uniqueIds, connection);
74
+
75
+ const normalized = rows.map((row) => normalizeStickerAssetRow(row));
76
+ const byId = new Map(normalized.map((row) => [row.id, row]));
77
+ return uniqueIds.map((id) => byId.get(id)).filter(Boolean);
78
+ }
79
+
80
+ /**
81
+ * Retorna o último asset salvo por um usuário.
82
+ *
83
+ * @param {string} ownerJid JID dono do asset.
84
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
85
+ * @returns {Promise<object|null>} Último asset encontrado.
86
+ */
87
+ export async function findLatestStickerAssetByOwner(ownerJid, connection = null) {
88
+ const rows = await executeQuery(
89
+ `SELECT * FROM ${TABLES.STICKER_ASSET}
90
+ WHERE owner_jid = ?
91
+ ORDER BY created_at DESC
92
+ LIMIT 1`,
93
+ [ownerJid],
94
+ connection,
95
+ );
96
+
97
+ return normalizeStickerAssetRow(rows?.[0] || null);
98
+ }
99
+
100
+ /**
101
+ * Lista assets que ainda não possuem classificação associada.
102
+ *
103
+ * @param {{ limit?: number, connection?: import('mysql2/promise').PoolConnection|null }} [options]
104
+ * @returns {Promise<object[]>} Assets pendentes de classificação.
105
+ */
106
+ export async function listStickerAssetsPendingClassification({ limit = 50, connection = null } = {}) {
107
+ const safeLimit = Math.max(1, Math.min(300, Number(limit) || 50));
108
+
109
+ const rows = await executeQuery(
110
+ `SELECT a.*
111
+ FROM ${TABLES.STICKER_ASSET} a
112
+ LEFT JOIN ${TABLES.STICKER_ASSET_CLASSIFICATION} c ON c.asset_id = a.id
113
+ WHERE c.asset_id IS NULL
114
+ ORDER BY a.created_at ASC
115
+ LIMIT ${safeLimit}`,
116
+ [],
117
+ connection,
118
+ );
119
+
120
+ return rows.map((row) => normalizeStickerAssetRow(row));
121
+ }
122
+
123
+ /**
124
+ * Lista assets que ainda não pertencem a nenhum pack.
125
+ *
126
+ * @param {{
127
+ * search?: string,
128
+ * limit?: number,
129
+ * offset?: number,
130
+ * connection?: import('mysql2/promise').PoolConnection|null,
131
+ * }} [options] Filtros de listagem.
132
+ * @returns {Promise<{ assets: object[], hasMore: boolean, total: number }>} Resultado paginado.
133
+ */
134
+ export async function listStickerAssetsWithoutPack({ search = '', limit = 120, offset = 0, connection = null } = {}) {
135
+ const safeLimit = Math.max(1, Math.min(500, Number(limit) || 120));
136
+ const safeOffset = Math.max(0, Number(offset) || 0);
137
+ const safeLimitWithSentinel = safeLimit + 1;
138
+ const normalizedSearch = String(search || '')
139
+ .trim()
140
+ .toLowerCase()
141
+ .slice(0, 140);
142
+
143
+ const whereClauses = ['i.sticker_id IS NULL'];
144
+ const params = [];
145
+
146
+ if (normalizedSearch) {
147
+ const like = `%${normalizedSearch}%`;
148
+ whereClauses.push('(LOWER(a.sha256) LIKE ? OR LOWER(a.owner_jid) LIKE ? OR LOWER(a.storage_path) LIKE ?)');
149
+ params.push(like, like, like);
150
+ }
151
+
152
+ const rows = await executeQuery(
153
+ `SELECT a.*
154
+ FROM ${TABLES.STICKER_ASSET} a
155
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i ON i.sticker_id = a.id
156
+ WHERE ${whereClauses.join(' AND ')}
157
+ ORDER BY a.created_at DESC
158
+ LIMIT ${safeLimitWithSentinel} OFFSET ${safeOffset}`,
159
+ params,
160
+ connection,
161
+ );
162
+
163
+ const countRows = await executeQuery(
164
+ `SELECT COUNT(*) AS total
165
+ FROM ${TABLES.STICKER_ASSET} a
166
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i ON i.sticker_id = a.id
167
+ WHERE ${whereClauses.join(' AND ')}`,
168
+ params,
169
+ connection,
170
+ );
171
+
172
+ const hasMore = rows.length > safeLimit;
173
+ const total = Number(countRows?.[0]?.total || 0);
174
+ return {
175
+ assets: rows.slice(0, safeLimit).map((row) => normalizeStickerAssetRow(row)),
176
+ hasMore,
177
+ total,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Lista assets sem pack que já possuem classificação.
183
+ *
184
+ * @param {{
185
+ * search?: string,
186
+ * limit?: number,
187
+ * offset?: number,
188
+ * connection?: import('mysql2/promise').PoolConnection|null,
189
+ * }} [options] Filtros de listagem.
190
+ * @returns {Promise<{ assets: object[], hasMore: boolean, total: number }>} Resultado paginado.
191
+ */
192
+ export async function listClassifiedStickerAssetsWithoutPack({ search = '', limit = 120, offset = 0, connection = null } = {}) {
193
+ const safeLimit = Math.max(1, Math.min(500, Number(limit) || 120));
194
+ const safeOffset = Math.max(0, Number(offset) || 0);
195
+ const safeLimitWithSentinel = safeLimit + 1;
196
+ const normalizedSearch = String(search || '')
197
+ .trim()
198
+ .toLowerCase()
199
+ .slice(0, 140);
200
+
201
+ const whereClauses = ['i.sticker_id IS NULL'];
202
+ const params = [];
203
+
204
+ if (normalizedSearch) {
205
+ const like = `%${normalizedSearch}%`;
206
+ whereClauses.push('(LOWER(a.sha256) LIKE ? OR LOWER(a.owner_jid) LIKE ? OR LOWER(a.storage_path) LIKE ?)');
207
+ params.push(like, like, like);
208
+ }
209
+
210
+ const rows = await executeQuery(
211
+ `SELECT a.*
212
+ FROM ${TABLES.STICKER_ASSET} a
213
+ INNER JOIN ${TABLES.STICKER_ASSET_CLASSIFICATION} c ON c.asset_id = a.id
214
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i ON i.sticker_id = a.id
215
+ WHERE ${whereClauses.join(' AND ')}
216
+ ORDER BY a.created_at DESC
217
+ LIMIT ${safeLimitWithSentinel} OFFSET ${safeOffset}`,
218
+ params,
219
+ connection,
220
+ );
221
+
222
+ const countRows = await executeQuery(
223
+ `SELECT COUNT(*) AS total
224
+ FROM ${TABLES.STICKER_ASSET} a
225
+ INNER JOIN ${TABLES.STICKER_ASSET_CLASSIFICATION} c ON c.asset_id = a.id
226
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i ON i.sticker_id = a.id
227
+ WHERE ${whereClauses.join(' AND ')}`,
228
+ params,
229
+ connection,
230
+ );
231
+
232
+ const hasMore = rows.length > safeLimit;
233
+ const total = Number(countRows?.[0]?.total || 0);
234
+
235
+ return {
236
+ assets: rows.slice(0, safeLimit).map((row) => normalizeStickerAssetRow(row)),
237
+ hasMore,
238
+ total,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Conta quantos assets classificados ainda não pertencem a nenhum pack.
244
+ *
245
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
246
+ * @returns {Promise<number>} Quantidade de assets classificados sem pack.
247
+ */
248
+ export async function countClassifiedStickerAssetsWithoutPack(connection = null) {
249
+ const rows = await executeQuery(
250
+ `SELECT COUNT(*) AS total
251
+ FROM ${TABLES.STICKER_ASSET} a
252
+ INNER JOIN ${TABLES.STICKER_ASSET_CLASSIFICATION} c ON c.asset_id = a.id
253
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i ON i.sticker_id = a.id
254
+ WHERE i.sticker_id IS NULL`,
255
+ [],
256
+ connection,
257
+ );
258
+
259
+ return Number(rows?.[0]?.total || 0);
260
+ }
261
+
262
+ /**
263
+ * Lista assets classificados para curadoria (inclui com/sem pack) com paginação.
264
+ *
265
+ * @param {{
266
+ * limit?: number,
267
+ * offset?: number,
268
+ * includePacked?: boolean,
269
+ * includeUnpacked?: boolean,
270
+ * onlyVersionMismatch?: string|null,
271
+ * connection?: import('mysql2/promise').PoolConnection|null,
272
+ * }} [options]
273
+ * @returns {Promise<{ assets: object[], hasMore: boolean, total: number }>}
274
+ */
275
+ export async function listClassifiedStickerAssetsForCuration({ limit = 200, offset = 0, includePacked = true, includeUnpacked = true, onlyVersionMismatch = null, connection = null } = {}) {
276
+ const safeLimit = Math.max(1, Math.min(1000, Number(limit) || 200));
277
+ const safeOffset = Math.max(0, Number(offset) || 0);
278
+ const safeLimitWithSentinel = safeLimit + 1;
279
+
280
+ const whereClauses = [];
281
+ const params = [];
282
+
283
+ if (!includePacked && includeUnpacked) {
284
+ whereClauses.push('i_any.sticker_id IS NULL');
285
+ } else if (includePacked && !includeUnpacked) {
286
+ whereClauses.push('i_any.sticker_id IS NOT NULL');
287
+ }
288
+
289
+ const normalizedVersion = String(onlyVersionMismatch || '').trim();
290
+ if (normalizedVersion) {
291
+ whereClauses.push('c.classification_version <> ?');
292
+ params.push(normalizedVersion);
293
+ }
294
+
295
+ const whereSql = whereClauses.length ? `WHERE ${whereClauses.join(' AND ')}` : '';
296
+
297
+ const rows = await executeQuery(
298
+ `SELECT DISTINCT a.*
299
+ FROM ${TABLES.STICKER_ASSET} a
300
+ INNER JOIN ${TABLES.STICKER_ASSET_CLASSIFICATION} c ON c.asset_id = a.id
301
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i_any ON i_any.sticker_id = a.id
302
+ ${whereSql}
303
+ ORDER BY a.created_at DESC, a.id DESC
304
+ LIMIT ${safeLimitWithSentinel} OFFSET ${safeOffset}`,
305
+ params,
306
+ connection,
307
+ );
308
+
309
+ const countRows = await executeQuery(
310
+ `SELECT COUNT(DISTINCT a.id) AS total
311
+ FROM ${TABLES.STICKER_ASSET} a
312
+ INNER JOIN ${TABLES.STICKER_ASSET_CLASSIFICATION} c ON c.asset_id = a.id
313
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i_any ON i_any.sticker_id = a.id
314
+ ${whereSql}`,
315
+ params,
316
+ connection,
317
+ );
318
+
319
+ const hasMore = rows.length > safeLimit;
320
+ const total = Number(countRows?.[0]?.total || 0);
321
+ return {
322
+ assets: rows.slice(0, safeLimit).map((row) => normalizeStickerAssetRow(row)),
323
+ hasMore,
324
+ total,
325
+ };
326
+ }
327
+
328
+ /**
329
+ * Cria um novo asset de figurinha.
330
+ *
331
+ * @param {object} asset Payload de criação do asset.
332
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
333
+ * @returns {Promise<object|null>} Asset criado.
334
+ */
335
+ export async function createStickerAsset(asset, connection = null) {
336
+ await executeQuery(
337
+ `INSERT INTO ${TABLES.STICKER_ASSET}
338
+ (id, owner_jid, sha256, mimetype, is_animated, width, height, size_bytes, storage_path)
339
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
340
+ [asset.id, asset.owner_jid, asset.sha256, asset.mimetype, asset.is_animated ? 1 : 0, asset.width ?? null, asset.height ?? null, asset.size_bytes, asset.storage_path],
341
+ connection,
342
+ );
343
+
344
+ await publishStickerDomainEvent(
345
+ {
346
+ eventType: STICKER_DOMAIN_EVENTS.STICKER_ASSET_CREATED,
347
+ aggregateType: 'sticker_asset',
348
+ aggregateId: asset.id,
349
+ payload: {
350
+ asset_id: asset.id,
351
+ owner_jid: asset.owner_jid,
352
+ sha256: asset.sha256,
353
+ mimetype: asset.mimetype,
354
+ },
355
+ priority: 85,
356
+ idempotencyKey: `sticker_asset_created:${asset.id}`,
357
+ },
358
+ { connection },
359
+ );
360
+
361
+ return findStickerAssetById(asset.id, connection);
362
+ }
363
+
364
+ /**
365
+ * Atualiza o caminho físico (storage_path) de um asset.
366
+ *
367
+ * @param {string} id ID do asset.
368
+ * @param {string} storagePath Caminho em disco do arquivo.
369
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
370
+ * @returns {Promise<object|null>} Asset atualizado.
371
+ */
372
+ export async function updateStickerAssetStoragePath(id, storagePath, connection = null) {
373
+ await executeQuery(
374
+ `UPDATE ${TABLES.STICKER_ASSET}
375
+ SET storage_path = ?
376
+ WHERE id = ?`,
377
+ [storagePath, id],
378
+ connection,
379
+ );
380
+
381
+ return findStickerAssetById(id, connection);
382
+ }
383
+
384
+ /**
385
+ * Remove um asset pelo ID.
386
+ *
387
+ * @param {string} id ID do asset.
388
+ * @param {import('mysql2/promise').PoolConnection|null} [connection=null] Conexão transacional opcional.
389
+ * @returns {Promise<number>} Quantidade de linhas removidas.
390
+ */
391
+ export async function deleteStickerAssetById(id, connection = null) {
392
+ const result = await executeQuery(
393
+ `DELETE FROM ${TABLES.STICKER_ASSET}
394
+ WHERE id = ?`,
395
+ [id],
396
+ connection,
397
+ );
398
+
399
+ return Number(result?.affectedRows || 0);
400
+ }
@@ -0,0 +1,175 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { executeQuery, TABLES } from '../../../database/index.js';
4
+
5
+ const normalizeReason = (value) => {
6
+ const normalized = String(value || '')
7
+ .trim()
8
+ .toUpperCase();
9
+ if (['MODEL_UPGRADE', 'LOW_CONFIDENCE', 'TREND_SHIFT', 'NSFW_REVIEW'].includes(normalized)) return normalized;
10
+ return null;
11
+ };
12
+
13
+ const normalizeStatus = (value) => {
14
+ const normalized = String(value || '')
15
+ .trim()
16
+ .toLowerCase();
17
+ if (['pending', 'processing', 'completed', 'failed'].includes(normalized)) return normalized;
18
+ return null;
19
+ };
20
+
21
+ const clampInt = (value, fallback, min, max) => {
22
+ const numeric = Number(value);
23
+ if (!Number.isFinite(numeric)) return fallback;
24
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
25
+ };
26
+
27
+ const normalizeRow = (row) => {
28
+ if (!row) return null;
29
+ return {
30
+ id: Number(row.id),
31
+ asset_id: row.asset_id,
32
+ reason: row.reason,
33
+ priority: Number(row.priority || 0),
34
+ scheduled_at: row.scheduled_at || null,
35
+ status: row.status,
36
+ attempts: Number(row.attempts || 0),
37
+ max_attempts: Number(row.max_attempts || 0),
38
+ worker_token: row.worker_token || null,
39
+ last_error: row.last_error || null,
40
+ locked_at: row.locked_at || null,
41
+ processed_at: row.processed_at || null,
42
+ created_at: row.created_at || null,
43
+ updated_at: row.updated_at || null,
44
+ };
45
+ };
46
+
47
+ export async function enqueueStickerAssetReprocess({ assetId, reason, priority = 50, scheduledAt = null, maxAttempts = 5 }, connection = null) {
48
+ const normalizedReason = normalizeReason(reason);
49
+ if (!assetId || !normalizedReason) return false;
50
+
51
+ const safePriority = clampInt(priority, 50, 1, 100);
52
+ const safeMaxAttempts = clampInt(maxAttempts, 5, 1, 20);
53
+ const safeScheduledAt = scheduledAt ? new Date(scheduledAt) : null;
54
+ const scheduledValue = safeScheduledAt && Number.isFinite(safeScheduledAt.valueOf()) ? safeScheduledAt : null;
55
+
56
+ const existing = await executeQuery(
57
+ `SELECT id
58
+ FROM ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
59
+ WHERE asset_id = ?
60
+ AND reason = ?
61
+ AND status IN ('pending', 'processing')
62
+ LIMIT 1`,
63
+ [assetId, normalizedReason],
64
+ connection,
65
+ );
66
+ if (existing.length > 0) return false;
67
+
68
+ await executeQuery(
69
+ `INSERT INTO ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
70
+ (asset_id, reason, priority, scheduled_at, status, attempts, max_attempts)
71
+ VALUES (?, ?, ?, COALESCE(?, UTC_TIMESTAMP()), 'pending', 0, ?)`,
72
+ [assetId, normalizedReason, safePriority, scheduledValue, safeMaxAttempts],
73
+ connection,
74
+ );
75
+
76
+ return true;
77
+ }
78
+
79
+ export async function claimStickerAssetReprocessTask({ reasons = [], allowRetryFailed = true } = {}, connection = null) {
80
+ const workerToken = randomUUID();
81
+ const normalizedReasons = Array.from(new Set((Array.isArray(reasons) ? reasons : []).map(normalizeReason).filter(Boolean)));
82
+ const reasonFilter = normalizedReasons.length ? `AND reason IN (${normalizedReasons.map(() => '?').join(', ')})` : '';
83
+
84
+ const statusClause = allowRetryFailed ? "(status = 'pending' OR (status = 'failed' AND attempts < max_attempts))" : "status = 'pending'";
85
+
86
+ await executeQuery(
87
+ `UPDATE ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
88
+ SET status = 'processing',
89
+ worker_token = ?,
90
+ locked_at = UTC_TIMESTAMP(),
91
+ attempts = attempts + 1,
92
+ updated_at = UTC_TIMESTAMP()
93
+ WHERE id = (
94
+ SELECT id FROM (
95
+ SELECT id
96
+ FROM ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
97
+ WHERE ${statusClause}
98
+ AND scheduled_at <= UTC_TIMESTAMP()
99
+ ${reasonFilter}
100
+ ORDER BY priority DESC, scheduled_at ASC, id ASC
101
+ LIMIT 1
102
+ ) pick
103
+ )`,
104
+ [workerToken, ...normalizedReasons],
105
+ connection,
106
+ );
107
+
108
+ const rows = await executeQuery(
109
+ `SELECT *
110
+ FROM ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
111
+ WHERE worker_token = ?
112
+ AND status = 'processing'
113
+ ORDER BY id DESC
114
+ LIMIT 1`,
115
+ [workerToken],
116
+ connection,
117
+ );
118
+
119
+ return normalizeRow(rows?.[0] || null);
120
+ }
121
+
122
+ export async function completeStickerAssetReprocessTask(taskId, connection = null) {
123
+ if (!taskId) return false;
124
+ await executeQuery(
125
+ `UPDATE ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
126
+ SET status = 'completed',
127
+ processed_at = UTC_TIMESTAMP(),
128
+ worker_token = NULL,
129
+ locked_at = NULL,
130
+ last_error = NULL,
131
+ updated_at = UTC_TIMESTAMP()
132
+ WHERE id = ?`,
133
+ [taskId],
134
+ connection,
135
+ );
136
+ return true;
137
+ }
138
+
139
+ export async function failStickerAssetReprocessTask(taskId, { error = null, retryDelaySeconds = 0 } = {}, connection = null) {
140
+ if (!taskId) return false;
141
+ const safeDelay = clampInt(retryDelaySeconds, 0, 0, 86400 * 7);
142
+ const message =
143
+ String(error || '')
144
+ .trim()
145
+ .slice(0, 255) || null;
146
+
147
+ await executeQuery(
148
+ `UPDATE ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
149
+ SET status = IF(attempts >= max_attempts, 'failed', 'pending'),
150
+ worker_token = NULL,
151
+ locked_at = NULL,
152
+ last_error = ?,
153
+ scheduled_at = IF(attempts >= max_attempts, scheduled_at, UTC_TIMESTAMP() + INTERVAL ${safeDelay} SECOND),
154
+ updated_at = UTC_TIMESTAMP(),
155
+ processed_at = IF(attempts >= max_attempts, UTC_TIMESTAMP(), processed_at)
156
+ WHERE id = ?`,
157
+ [message, taskId],
158
+ connection,
159
+ );
160
+ return true;
161
+ }
162
+
163
+ export async function countStickerAssetReprocessQueueByStatus(status = 'pending', connection = null) {
164
+ const normalizedStatus = normalizeStatus(status);
165
+ if (!normalizedStatus) return 0;
166
+
167
+ const rows = await executeQuery(
168
+ `SELECT COUNT(*) AS total
169
+ FROM ${TABLES.STICKER_ASSET_REPROCESS_QUEUE}
170
+ WHERE status = ?`,
171
+ [normalizedStatus],
172
+ connection,
173
+ );
174
+ return Number(rows?.[0]?.total || 0);
175
+ }