@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,4700 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
4
+ import { spawn } from 'node:child_process';
5
+ import { URL, URLSearchParams } from 'node:url';
6
+
7
+ import { executeQuery, pool, TABLES } from '../../../database/index.js';
8
+ import { normalizeJid, resolveBotJid, getActiveSocket, profilePictureUrlFromActiveSocket, extractUserIdInfo, resolveUserId } from '../../../app/config/index.js';
9
+ import { resolveWhatsAppOwnerJidFromLoginPayload, toWhatsAppOwnerJid, toWhatsAppPhoneDigits } from '../../../app/services/auth/whatsappLoginLinkService.js';
10
+ import logger from '#logger';
11
+ import { listStickerPacksForCatalog, findStickerPackByPackKey, listStickerPacksByOwner, bumpStickerPackVersion, findStickerPackByOwnerAndIdentifier, softDeleteStickerPack, updateStickerPackFields } from '../../../app/modules/stickerPackModule/stickerPackRepository.js';
12
+ import { listStickerPackItems, countStickerPackItemRefsByStickerId, createStickerPackItem, getStickerPackItemByStickerId, removeStickerPackItemByStickerId, removeStickerPackItemsByPackId } from '../../../app/modules/stickerPackModule/stickerPackItemRepository.js';
13
+ import { listClassifiedStickerAssetsWithoutPack, listStickerAssetsWithoutPack, deleteStickerAssetById, findStickerAssetsByIds } from '../../../app/modules/stickerPackModule/stickerAssetRepository.js';
14
+ import { deleteStickerAssetClassificationByAssetId, findStickerClassificationByAssetId, listStickerClassificationsByAssetIds } from '../../../app/modules/stickerPackModule/stickerAssetClassificationRepository.js';
15
+ import { decoratePackClassificationSummary, decorateStickerClassification, getPackClassificationSummaryByAssetIds } from '../../../app/modules/stickerPackModule/stickerClassificationService.js';
16
+ import { getEmptyStickerPackEngagement, getStickerPackEngagementByPackId, incrementStickerPackDislike, incrementStickerPackLike, incrementStickerPackOpen, listStickerPackEngagementByPackIds } from '../../../app/modules/stickerPackModule/stickerPackEngagementRepository.js';
17
+ import { createStickerPackInteractionEvent, listStickerPackInteractionStatsByPackIds, listViewerRecentPackIds } from '../../../app/modules/stickerPackModule/stickerPackInteractionEventRepository.js';
18
+ import { buildCreatorRanking, buildIntentCollections, buildPersonalizedRecommendations, buildViewerTagAffinity, computePackSignals } from '../../../app/modules/stickerPackModule/stickerPackMarketplaceService.js';
19
+ import { listStickerPackScoreSnapshotsByPackIds } from '../../../app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js';
20
+ import { createCatalogApiRouter } from '../../routes/sticker/catalogRouter.js';
21
+ import { normalizeGoogleSubject } from '../../auth/googleWebAuth/googleWebAuthRuntime.js';
22
+ import { createStickerCatalogAuthContext } from '../../auth/stickerCatalogAuthContext.js';
23
+ import { appendSetCookie, buildCookieString, getCookieValuesFromRequest, normalizeBasePath, normalizeCatalogVisibility, normalizeVisitPath, parseCookies, readJsonBody, resolveRequestRemoteIp, sendJson, sendText, toIsoOrNull, withTimeout } from '../../http/httpRequestUtils.js';
24
+ import { getSiteRoutingConfig, maybeRedirectToCanonicalHost, toSiteAbsoluteUrl } from '../../http/siteRoutingUtils.js';
25
+ import { handlePublicDataAssetRequest, listDataImageFiles, toPublicDataUrlFromStoragePath } from '../system/storageController.js';
26
+ import { buildSupportInfo, resolveCatalogBotPhone } from '../system/contactController.js';
27
+ import { trackWebVisitMetric } from '../system/visitController.js';
28
+ import { systemContext, systemHandlers } from '../system/systemController.js';
29
+
30
+ const { handleSystemSummaryRequest, handleReadmeSummaryRequest, handleReadmeMarkdownRequest, handleGlobalRankingSummaryRequest, handleMarketplaceGlobalStatsRequest, handleGitHubProjectSummaryRequest, handleSupportInfoRequest, handleBotContactInfoRequest, handleHomeBootstrapRequest } = systemHandlers;
31
+ const { getSystemSummaryCached, getMarketplaceGlobalStatsCached } = systemContext;
32
+
33
+ import { queueAutomatedEmail, queueWelcomeEmail } from '../../email/emailAutomationService.js';
34
+ import { createStickerCatalogAdminBanContext, createStickerCatalogAdminHandlersContext } from '../admin/stickerCatalogAdminContext.js';
35
+ import { createStickerCatalogSeoContext } from '../seo/stickerCatalogSeoContext.js';
36
+ import { getMarketplaceDriftSnapshot } from '../../../app/modules/stickerPackModule/stickerMarketplaceDriftService.js';
37
+ import { getStickerAssetExternalUrl, getStickerStorageConfig, readStickerAssetBuffer, saveStickerAssetFromBuffer } from '../../../app/modules/stickerPackModule/stickerStorageService.js';
38
+ import { convertToWebp } from '../../../app/modules/stickerModule/convertToWebp.js';
39
+ import { sanitizeText } from '../../../app/modules/stickerPackModule/stickerPackUtils.js';
40
+ import stickerPackService from '../../../app/modules/stickerPackModule/stickerPackServiceRuntime.js';
41
+ import { STICKER_PACK_ERROR_CODES, StickerPackError } from '../../../app/modules/stickerPackModule/stickerPackErrors.js';
42
+ import { getFeatureFlagsSnapshot, isFeatureEnabled, refreshFeatureFlags } from '../../../app/services/infra/featureFlagService.js';
43
+
44
+ const parseEnvBool = (value, fallback = false) => {
45
+ if (value === undefined || value === null || value === '') return fallback;
46
+ const normalized = String(value).trim().toLowerCase();
47
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
48
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
49
+ return fallback;
50
+ };
51
+
52
+ const normalizeCatalogSortParam = (value) => {
53
+ const normalized = String(value || '')
54
+ .trim()
55
+ .toLowerCase();
56
+ if (normalized === 'new') return 'recent';
57
+ if (normalized === 'liked') return 'likes';
58
+ if (normalized === 'popular') return 'trending';
59
+ if (['recent', 'likes', 'downloads', 'trending', 'comments'].includes(normalized)) return normalized;
60
+ return 'popular';
61
+ };
62
+
63
+ export const stripWebpExtension = (value) =>
64
+ String(value || '')
65
+ .trim()
66
+ .replace(/\.webp$/i, '');
67
+
68
+ const clampInt = (value, fallback, min, max) => {
69
+ const parsed = Number(value);
70
+ if (!Number.isFinite(parsed)) return fallback;
71
+ return Math.max(min, Math.min(max, Math.floor(parsed)));
72
+ };
73
+ const parseMaxPacksPerOwnerLimit = (value, fallback = 50) => {
74
+ if (value === undefined || value === null || value === '') {
75
+ return Math.max(1, Number(fallback) || 50);
76
+ }
77
+ const normalized = String(value).trim().toLowerCase();
78
+ if (['0', '-1', 'inf', 'infinity', 'unlimited', 'sem-limite'].includes(normalized)) {
79
+ return Number.POSITIVE_INFINITY;
80
+ }
81
+ const parsed = Number(normalized);
82
+ if (Number.isFinite(parsed) && parsed > 0) {
83
+ return Math.max(1, Math.floor(parsed));
84
+ }
85
+ return Math.max(1, Number(fallback) || 50);
86
+ };
87
+ const serializePackOwnerLimit = (value) => (Number.isFinite(value) ? Math.max(1, Number(value) || 1) : null);
88
+
89
+ const STICKER_CATALOG_ENABLED = Boolean(process.env.STICKER_CATALOG_ENABLED !== 'false');
90
+ const STICKER_WEB_PATH = normalizeBasePath(process.env.STICKER_WEB_PATH, '/stickers');
91
+ const STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PATH, '/api/sticker-packs');
92
+ const USER_API_BASE_PATH = normalizeBasePath(process.env.USER_API_BASE_PATH || process.env.AUTH_API_BASE_PATH, '/api');
93
+ const STICKER_ORPHAN_API_PATH = `${STICKER_API_BASE_PATH}/orphan-stickers`;
94
+ const STICKER_CREATE_WEB_PATH = `${STICKER_WEB_PATH}/create`;
95
+ const STICKER_LOGIN_WEB_PATH = normalizeBasePath(process.env.STICKER_LOGIN_WEB_PATH, '/login');
96
+ const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PATH, '/user');
97
+ const USER_PASSWORD_RESET_WEB_PATH = normalizeBasePath(process.env.USER_PASSWORD_RESET_WEB_PATH, '/user/password-reset');
98
+ const PASSWORD_RECOVERY_SESSION_AUTH_METHOD = 'password_recovery_session';
99
+ const PASSWORD_RECOVERY_SESSION_TTL_SECONDS = clampInt(process.env.WEB_PASSWORD_RECOVERY_SESSION_TTL_SECONDS, 15 * 60, 60, 24 * 60 * 60);
100
+ const STICKER_DATA_PUBLIC_PATH = normalizeBasePath(process.env.STICKER_DATA_PUBLIC_PATH, '/data');
101
+ const STICKER_DATA_PUBLIC_DIR = path.resolve(process.env.STICKER_DATA_PUBLIC_DIR || path.join(process.cwd(), 'data'));
102
+ const STICKER_WEB_ASSET_VERSION =
103
+ sanitizeText(process.env.STICKER_WEB_ASSET_VERSION || process.env.OMNIZAP_BUILD_ID || '', 64, {
104
+ allowEmpty: true,
105
+ }) || '';
106
+ const CATALOG_PUBLIC_DIR = path.resolve(process.cwd(), 'public');
107
+ const CATALOG_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'pages', 'stickers.html');
108
+ const CREATE_PACK_TEMPLATE_PATH = path.join(CATALOG_PUBLIC_DIR, 'pages', 'stickers-create.html');
109
+ const CATALOG_STYLES_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'css', 'styles.css');
110
+ const CATALOG_SCRIPT_FILE_PATH = path.join(CATALOG_PUBLIC_DIR, 'js', 'catalog.js');
111
+ const DEFAULT_LIST_LIMIT = clampInt(process.env.STICKER_WEB_LIST_LIMIT, 16, 1, 60);
112
+ const MAX_LIST_LIMIT = clampInt(process.env.STICKER_WEB_LIST_MAX_LIMIT, 60, 1, 100);
113
+ const DEFAULT_ORPHAN_LIST_LIMIT = clampInt(process.env.STICKER_ORPHAN_LIST_LIMIT, 120, 1, 300);
114
+ const MAX_ORPHAN_LIST_LIMIT = clampInt(process.env.STICKER_ORPHAN_LIST_MAX_LIMIT, 300, 1, 500);
115
+ const DEFAULT_DATA_LIST_LIMIT = clampInt(process.env.STICKER_DATA_LIST_LIMIT, 50, 1, 200);
116
+ const MAX_DATA_LIST_LIMIT = clampInt(process.env.STICKER_DATA_LIST_MAX_LIMIT, 200, 1, 500);
117
+ const MAX_DATA_SCAN_FILES = clampInt(process.env.STICKER_DATA_SCAN_MAX_FILES, 10000, 100, 50000);
118
+ const ASSET_CACHE_SECONDS = clampInt(process.env.STICKER_WEB_ASSET_CACHE_SECONDS, 60 * 60 * 24 * 30, 60 * 60, 60 * 60 * 24 * 365);
119
+ const STATIC_TEXT_CACHE_SECONDS = clampInt(process.env.STICKER_WEB_STATIC_TEXT_CACHE_SECONDS, 60 * 60, 60, 60 * 60 * 24 * 30);
120
+ const IMMUTABLE_ASSET_CACHE_SECONDS = clampInt(process.env.STICKER_WEB_IMMUTABLE_ASSET_CACHE_SECONDS, 60 * 60 * 24 * 365, 60 * 60, 60 * 60 * 24 * 365);
121
+ const STICKER_WEB_WHATSAPP_MESSAGE_TEMPLATE = String(process.env.STICKER_WEB_WHATSAPP_MESSAGE_TEMPLATE || '/pack send {{pack_key}}').trim() || '/pack send {{pack_key}}';
122
+ const PACK_COMMAND_PREFIX = String(process.env.COMMAND_PREFIX || '/').trim() || '/';
123
+ const PACK_CREATE_NAME_REGEX = '^[\\s\\S]+$';
124
+ const PACK_CREATE_MAX_NAME_LENGTH = 120;
125
+ const PACK_CREATE_MAX_PUBLISHER_LENGTH = 120;
126
+ const PACK_CREATE_MAX_DESCRIPTION_LENGTH = 1024;
127
+ const PACK_CREATE_MAX_ITEMS = Math.max(1, Number(process.env.STICKER_PACK_MAX_ITEMS) || 30);
128
+ const PACK_CREATE_MAX_PACKS_PER_OWNER = parseMaxPacksPerOwnerLimit(process.env.STICKER_PACK_MAX_PACKS_PER_OWNER, 50);
129
+ const PACK_WEB_EDIT_TOKEN_TTL_MS = Math.max(60_000, Number(process.env.STICKER_WEB_EDIT_TOKEN_TTL_MS) || 6 * 60 * 60 * 1000);
130
+ const STICKER_WEB_GOOGLE_CLIENT_ID = String(process.env.STICKER_WEB_GOOGLE_CLIENT_ID || '').trim();
131
+ const STICKER_WEB_GOOGLE_AUTH_REQUIRED = Boolean(process.env.STICKER_WEB_GOOGLE_AUTH_REQUIRED !== 'false' && (process.env.STICKER_WEB_GOOGLE_AUTH_REQUIRED === 'true' || STICKER_WEB_GOOGLE_CLIENT_ID));
132
+ const STICKER_WEB_GOOGLE_SESSION_TTL_MS = Math.max(5 * 60 * 1000, Number(process.env.STICKER_WEB_GOOGLE_SESSION_TTL_MS) || 7 * 24 * 60 * 60 * 1000);
133
+ const STICKER_CATALOG_ONLY_CLASSIFIED = Boolean(process.env.STICKER_CATALOG_ONLY_CLASSIFIED !== 'false');
134
+ const USER_INTERNAL_API_TOKEN = String(process.env.USER_INTERNAL_API_TOKEN || process.env.ADMIN_TOKEN || process.env.ADMIN_API_TOKEN || '').trim();
135
+ const USER_INTERNAL_READ_REQUIRE_AUTH = Boolean(process.env.USER_INTERNAL_READ_REQUIRE_AUTH !== 'false');
136
+ const USER_CONTACT_ENDPOINT_REQUIRE_AUTH = Boolean(process.env.USER_CONTACT_ENDPOINT_REQUIRE_AUTH !== 'false');
137
+ const HOME_BOOTSTRAP_EXPOSE_CONTACT = Boolean(process.env.HOME_BOOTSTRAP_EXPOSE_CONTACT !== 'false');
138
+ const ADMIN_PANEL_EMAIL =
139
+ String(process.env.ADM_EMAIL || '')
140
+ .trim()
141
+ .toLowerCase() || '';
142
+ const GLOBAL_RANK_REFRESH_SECONDS = clampInt(process.env.GLOBAL_RANK_REFRESH_SECONDS, 600, 60, 3600);
143
+ const CATALOG_LIST_CACHE_SECONDS = clampInt(process.env.STICKER_CATALOG_LIST_CACHE_SECONDS, 90, 15, 900);
144
+ const CATALOG_CREATOR_RANKING_CACHE_SECONDS = clampInt(process.env.STICKER_CATALOG_CREATOR_RANKING_CACHE_SECONDS, 120, 15, 900);
145
+ const CATALOG_PACK_PAYLOAD_CACHE_SECONDS = clampInt(process.env.STICKER_CATALOG_PACK_PAYLOAD_CACHE_SECONDS, 300, 30, 1800);
146
+ const MARKETPLACE_GLOBAL_STATS_API_PATH = '/api/marketplace/stats';
147
+ const MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS = clampInt(process.env.MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS, 45, 30, 60);
148
+ const HOME_MARKETPLACE_STATS_CACHE_SECONDS = clampInt(process.env.HOME_MARKETPLACE_STATS_CACHE_SECONDS, 45, 10, 300);
149
+ const SYSTEM_SUMMARY_CACHE_SECONDS = clampInt(process.env.SYSTEM_SUMMARY_CACHE_SECONDS, 20, 5, 120);
150
+ const README_SUMMARY_CACHE_SECONDS = clampInt(process.env.README_SUMMARY_CACHE_SECONDS, 60 * 30, 60, 60 * 60 * 6);
151
+ const README_MESSAGE_TYPE_SAMPLE_LIMIT = clampInt(process.env.README_MESSAGE_TYPE_SAMPLE_LIMIT, 25000, 500, 250000);
152
+ const README_COMMAND_PREFIX = String(process.env.README_COMMAND_PREFIX || PACK_COMMAND_PREFIX).trim() || PACK_COMMAND_PREFIX;
153
+ const SITE_ORIGIN = getSiteRoutingConfig().origin;
154
+ const SITEMAP_MAX_PACKS = clampInt(process.env.STICKER_SITEMAP_MAX_PACKS, 45000, 100, 50000);
155
+ const SITEMAP_CACHE_SECONDS = clampInt(process.env.STICKER_SITEMAP_CACHE_SECONDS, 180, 30, 3600);
156
+ const SEO_DISCOVERY_LINK_LIMIT = clampInt(process.env.STICKER_SEO_DISCOVERY_LINK_LIMIT, 60, 10, 200);
157
+ const SEO_DISCOVERY_CACHE_SECONDS = clampInt(process.env.STICKER_SEO_DISCOVERY_CACHE_SECONDS, 180, 30, 3600);
158
+ const PACK_PAGE_ROUTE_EXCLUSIONS = new Set(['profile', 'perfil', 'creators', 'criadores']);
159
+ const NSFW_STICKER_PLACEHOLDER_URL = String(process.env.NSFW_STICKER_PLACEHOLDER_URL || 'https://iili.io/qfhwS6u.jpg').trim();
160
+ const { maxStickerBytes: MAX_STICKER_UPLOAD_BYTES } = getStickerStorageConfig();
161
+ const MAX_STICKER_SOURCE_UPLOAD_BYTES = Math.max(MAX_STICKER_UPLOAD_BYTES, Number(process.env.STICKER_WEB_UPLOAD_SOURCE_MAX_BYTES) || 20 * 1024 * 1024);
162
+ const ALLOWED_WEB_UPLOAD_VIDEO_MIMETYPES = new Set(['video/mp4', 'video/webm', 'video/quicktime', 'video/x-m4v']);
163
+ const webPackEditTokenMap = new Map();
164
+ const WEB_SESSION_COOKIE_NAME = 'omnizap_sid';
165
+ const PACK_WEB_STATUS_VALUES = new Set(['draft', 'uploading', 'processing', 'published', 'failed']);
166
+ const PACK_WEB_UPLOAD_STATUS_VALUES = new Set(['pending', 'processing', 'done', 'failed']);
167
+ const WEB_UPLOAD_ERROR_MESSAGE_MAX = 255;
168
+ const WEB_UPLOAD_MAX_CONCURRENCY = Math.max(1, Math.min(6, Number(process.env.STICKER_WEB_UPLOAD_CONCURRENCY) || 3));
169
+ const WEB_DRAFT_CLEANUP_TTL_MS = Math.max(60 * 60 * 1000, Number(process.env.STICKER_WEB_DRAFT_CLEANUP_TTL_MS) || 24 * 60 * 60 * 1000);
170
+ const WEB_DRAFT_CLEANUP_RUN_INTERVAL_MS = Math.max(60 * 1000, Number(process.env.STICKER_WEB_DRAFT_CLEANUP_RUN_INTERVAL_MS) || 15 * 60 * 1000);
171
+ const WEB_UPLOAD_ID_MAX_LENGTH = 120;
172
+ const WEB_VISITOR_COOKIE_TTL_SECONDS = clampInt(process.env.WEB_VISITOR_COOKIE_TTL_SECONDS, 60 * 60 * 24 * 365, 60 * 60, 60 * 60 * 24 * 3650);
173
+ const WEB_SESSION_COOKIE_TTL_SECONDS = clampInt(process.env.WEB_SESSION_COOKIE_TTL_SECONDS, 60 * 60 * 24 * 30, 30 * 60, 60 * 60 * 24 * 365);
174
+ const STICKER_PREVIEW_SIDE_PX = clampInt(process.env.STICKER_PREVIEW_SIDE_PX, 112, 96, 512);
175
+ const STICKER_PREVIEW_QUALITY = clampInt(process.env.STICKER_PREVIEW_QUALITY, 20, 10, 80);
176
+ const STICKER_PREVIEW_TIMEOUT_MS = clampInt(process.env.STICKER_PREVIEW_TIMEOUT_MS, 2500, 500, 12000);
177
+ const STICKER_PREVIEW_CACHE_TTL_MS = clampInt(process.env.STICKER_PREVIEW_CACHE_TTL_MS, 6 * 60 * 60 * 1000, 60 * 1000, 7 * 24 * 60 * 60 * 1000);
178
+ const STICKER_PREVIEW_CACHE_MAX_ITEMS = clampInt(process.env.STICKER_PREVIEW_CACHE_MAX_ITEMS, 2000, 100, 20000);
179
+ const STICKER_PREVIEW_TEMP_DIR = path.join(process.cwd(), 'temp', 'stickers', 'web-preview');
180
+ const staleDraftCleanupState = {
181
+ running: false,
182
+ lastRunAt: 0,
183
+ };
184
+
185
+ const hasPathPrefix = (pathname, prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`);
186
+ const isPackPubliclyVisible = (pack) => {
187
+ const visibility = String(pack?.visibility || '').toLowerCase();
188
+ if (visibility !== 'public' && visibility !== 'unlisted') return false;
189
+ if (String(pack?.status || 'published') !== 'published') return false;
190
+
191
+ const packStatus = String(pack?.pack_status || 'ready').toLowerCase();
192
+ if (packStatus === 'ready') return true;
193
+ return packStatus === 'building' && Number(pack?.is_auto_pack || 0) === 1;
194
+ };
195
+ const HOME_MARKETPLACE_STATS_CACHE = new Map();
196
+ const CATALOG_LIST_CACHE = new Map();
197
+ const CATALOG_CREATOR_RANKING_CACHE = new Map();
198
+ const CATALOG_PACK_PAYLOAD_CACHE = new Map();
199
+ const STICKER_PREVIEW_CACHE = new Map();
200
+ const GLOBAL_RANK_CACHE = { expiresAt: 0, value: null, pending: null };
201
+ const SYSTEM_SUMMARY_CACHE = { expiresAt: 0, value: null, pending: null };
202
+ const README_SUMMARY_CACHE = { expiresAt: 0, value: null, pending: null };
203
+
204
+ const buildCacheKey = (parts) => JSON.stringify(parts);
205
+
206
+ const getCacheBucket = (cacheMap, key) => {
207
+ let bucket = cacheMap.get(key);
208
+ if (!bucket) {
209
+ bucket = {
210
+ expiresAt: 0,
211
+ value: null,
212
+ pending: null,
213
+ };
214
+ cacheMap.set(key, bucket);
215
+ }
216
+ return bucket;
217
+ };
218
+
219
+ const getCachedSnapshot = async ({ cacheMap, key, ttlSeconds, staleWhileRefresh = true, staleOnError = true, load }) => {
220
+ const bucket = getCacheBucket(cacheMap, key);
221
+ const now = Date.now();
222
+ const hasValue = bucket.value !== null;
223
+ const hasFreshValue = hasValue && now < bucket.expiresAt;
224
+
225
+ if (hasFreshValue) {
226
+ return bucket.value;
227
+ }
228
+
229
+ if (!bucket.pending) {
230
+ bucket.pending = Promise.resolve()
231
+ .then(load)
232
+ .then((value) => {
233
+ bucket.value = value;
234
+ bucket.expiresAt = Date.now() + ttlSeconds * 1000;
235
+ return value;
236
+ })
237
+ .finally(() => {
238
+ bucket.pending = null;
239
+ });
240
+ }
241
+
242
+ if (hasValue && staleWhileRefresh) {
243
+ return bucket.value;
244
+ }
245
+
246
+ try {
247
+ return await bucket.pending;
248
+ } catch (error) {
249
+ if (hasValue && staleOnError) return bucket.value;
250
+ throw error;
251
+ }
252
+ };
253
+
254
+ const canUseRankingSnapshotRead = async (subjectKey = 'catalog') =>
255
+ isFeatureEnabled('enable_ranking_snapshot_read', {
256
+ fallback: true,
257
+ subjectKey,
258
+ });
259
+
260
+ const logPackWebFlow = (level, phase, payload = {}) => {
261
+ const method = typeof logger?.[level] === 'function' ? logger[level].bind(logger) : logger.info.bind(logger);
262
+ method(`Fluxo web de criação/publicação de pack: ${phase}`, {
263
+ action: `sticker_pack_web_${phase}`,
264
+ phase,
265
+ ...payload,
266
+ });
267
+ };
268
+
269
+ const normalizePackWebStatus = (value, fallback = 'draft') => {
270
+ const normalized = String(value || '')
271
+ .trim()
272
+ .toLowerCase();
273
+ return PACK_WEB_STATUS_VALUES.has(normalized) ? normalized : fallback;
274
+ };
275
+
276
+ const normalizePackWebUploadStatus = (value, fallback = 'pending') => {
277
+ const normalized = String(value || '')
278
+ .trim()
279
+ .toLowerCase();
280
+ return PACK_WEB_UPLOAD_STATUS_VALUES.has(normalized) ? normalized : fallback;
281
+ };
282
+
283
+ const sha256Hex = (buffer) => createHash('sha256').update(buffer).digest('hex');
284
+ const normalizeEmail = (value) =>
285
+ String(value || '')
286
+ .trim()
287
+ .toLowerCase()
288
+ .slice(0, 255);
289
+ const constantTimeStringEqual = (left, right) => {
290
+ const leftBuffer = Buffer.from(String(left || ''), 'utf8');
291
+ const rightBuffer = Buffer.from(String(right || ''), 'utf8');
292
+ if (!leftBuffer.length || leftBuffer.length !== rightBuffer.length) return false;
293
+ try {
294
+ return timingSafeEqual(leftBuffer, rightBuffer);
295
+ } catch {
296
+ return false;
297
+ }
298
+ };
299
+
300
+ const extractBearerTokenFromRequest = (req) => {
301
+ const authHeader = String(req?.headers?.authorization || '').trim();
302
+ if (!authHeader.toLowerCase().startsWith('bearer ')) return '';
303
+ return authHeader.slice(7).trim();
304
+ };
305
+
306
+ const resolveInternalApiTokenFromRequest = (req) => {
307
+ const explicit = String(req?.headers?.['x-internal-api-token'] || '').trim();
308
+ if (explicit) return explicit;
309
+ const adminHeader = String(req?.headers?.['x-admin-token'] || '').trim();
310
+ if (adminHeader) return adminHeader;
311
+ return extractBearerTokenFromRequest(req);
312
+ };
313
+
314
+ const hasValidInternalApiToken = (req) => {
315
+ if (!USER_INTERNAL_API_TOKEN) return false;
316
+ const requestToken = resolveInternalApiTokenFromRequest(req);
317
+ if (!requestToken) return false;
318
+ return constantTimeStringEqual(requestToken, USER_INTERNAL_API_TOKEN);
319
+ };
320
+
321
+ const isAuthenticatedGoogleSession = (session) => Boolean(session?.sub && (session?.ownerJid || session?.ownerPhone || session?.email));
322
+
323
+ const resolveSupportAdminPhone = async () => {
324
+ try {
325
+ const support = await buildSupportInfo();
326
+ return toWhatsAppPhoneDigits(support?.phone || '') || '';
327
+ } catch {
328
+ return '';
329
+ }
330
+ };
331
+
332
+ const isAdminGoogleSession = async (session) => {
333
+ if (!isAuthenticatedGoogleSession(session)) return false;
334
+
335
+ const sessionEmail = normalizeEmail(session?.email || '');
336
+ if (ADMIN_PANEL_EMAIL && sessionEmail && sessionEmail === ADMIN_PANEL_EMAIL) {
337
+ return true;
338
+ }
339
+
340
+ const adminPhone = await resolveSupportAdminPhone().catch(() => '');
341
+ if (!adminPhone) return false;
342
+ return toWhatsAppPhoneDigits(session?.ownerPhone || session?.ownerJid || '') === adminPhone || toWhatsAppPhoneDigits(session?.ownerJid || '') === adminPhone;
343
+ };
344
+
345
+ const requireInternalUserApiReadAccess = async (req, res) => {
346
+ if (!USER_INTERNAL_READ_REQUIRE_AUTH) return true;
347
+
348
+ if (hasValidInternalApiToken(req)) return true;
349
+
350
+ const session = await resolveGoogleWebSessionFromRequest(req).catch(() => null);
351
+ if (await isAdminGoogleSession(session)) return true;
352
+
353
+ sendJson(req, res, 401, { error: 'Nao autorizado.' });
354
+ return false;
355
+ };
356
+
357
+ const requireContactUserApiReadAccess = async (req, res, session = null) => {
358
+ if (!USER_CONTACT_ENDPOINT_REQUIRE_AUTH) return true;
359
+ if (hasValidInternalApiToken(req)) return true;
360
+ if (isAuthenticatedGoogleSession(session)) return true;
361
+ sendJson(req, res, 401, { error: 'Sessao obrigatoria para consultar contato.' });
362
+ return false;
363
+ };
364
+
365
+ const clampUploadErrorMessage = (message) =>
366
+ String(message || '')
367
+ .trim()
368
+ .slice(0, WEB_UPLOAD_ERROR_MESSAGE_MAX) || null;
369
+
370
+ const runSqlTransaction = async (handler) => {
371
+ const connection = await pool.getConnection();
372
+ try {
373
+ await connection.beginTransaction();
374
+ const result = await handler(connection);
375
+ await connection.commit();
376
+ return result;
377
+ } catch (error) {
378
+ await connection.rollback();
379
+ throw error;
380
+ } finally {
381
+ connection.release();
382
+ }
383
+ };
384
+
385
+ const normalizeWebUploadId = (value) =>
386
+ String(value || '')
387
+ .trim()
388
+ .replace(/[^a-zA-Z0-9._:-]/g, '')
389
+ .slice(0, WEB_UPLOAD_ID_MAX_LENGTH);
390
+
391
+ const normalizeStickerHashHex = (value) => {
392
+ const normalized = String(value || '')
393
+ .trim()
394
+ .toLowerCase()
395
+ .replace(/[^a-f0-9]/g, '');
396
+ return normalized.length === 64 ? normalized : '';
397
+ };
398
+
399
+ const normalizePackWebUploadRow = (row) => {
400
+ if (!row) return null;
401
+ return {
402
+ id: row.id,
403
+ pack_id: row.pack_id,
404
+ upload_id: row.upload_id,
405
+ sticker_hash: row.sticker_hash,
406
+ source_mimetype: row.source_mimetype || null,
407
+ upload_status: normalizePackWebUploadStatus(row.upload_status, 'pending'),
408
+ sticker_id: row.sticker_id || null,
409
+ error_code: row.error_code || null,
410
+ error_message: row.error_message || null,
411
+ attempt_count: Number(row.attempt_count || 0),
412
+ last_attempt_at: row.last_attempt_at || null,
413
+ created_at: row.created_at || null,
414
+ updated_at: row.updated_at || null,
415
+ };
416
+ };
417
+
418
+ const listPackWebUploads = async (packId, connection = null) => {
419
+ const rows = await executeQuery(
420
+ `SELECT *
421
+ FROM ${TABLES.STICKER_PACK_WEB_UPLOAD}
422
+ WHERE pack_id = ?
423
+ ORDER BY updated_at ASC, created_at ASC`,
424
+ [packId],
425
+ connection,
426
+ );
427
+ return rows.map((row) => normalizePackWebUploadRow(row));
428
+ };
429
+
430
+ const findPackWebUploadByUploadId = async (packId, uploadId, connection = null) => {
431
+ if (!packId || !uploadId) return null;
432
+ const rows = await executeQuery(
433
+ `SELECT *
434
+ FROM ${TABLES.STICKER_PACK_WEB_UPLOAD}
435
+ WHERE pack_id = ? AND upload_id = ?
436
+ LIMIT 1`,
437
+ [packId, uploadId],
438
+ connection,
439
+ );
440
+ return normalizePackWebUploadRow(rows?.[0] || null);
441
+ };
442
+
443
+ const findPackWebUploadByStickerHash = async (packId, stickerHash, connection = null) => {
444
+ if (!packId || !stickerHash) return null;
445
+ const rows = await executeQuery(
446
+ `SELECT *
447
+ FROM ${TABLES.STICKER_PACK_WEB_UPLOAD}
448
+ WHERE pack_id = ? AND sticker_hash = ?
449
+ LIMIT 1`,
450
+ [packId, stickerHash],
451
+ connection,
452
+ );
453
+ return normalizePackWebUploadRow(rows?.[0] || null);
454
+ };
455
+
456
+ const createPackWebUpload = async (entry, connection = null) => {
457
+ await executeQuery(
458
+ `INSERT INTO ${TABLES.STICKER_PACK_WEB_UPLOAD}
459
+ (id, pack_id, upload_id, sticker_hash, source_mimetype, upload_status, sticker_id, error_code, error_message, attempt_count, last_attempt_at)
460
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
461
+ [entry.id, entry.pack_id, entry.upload_id, entry.sticker_hash, entry.source_mimetype ?? null, normalizePackWebUploadStatus(entry.upload_status, 'pending'), entry.sticker_id ?? null, entry.error_code ?? null, clampUploadErrorMessage(entry.error_message), Math.max(0, Number(entry.attempt_count || 0)), entry.last_attempt_at ?? null],
462
+ connection,
463
+ );
464
+ return findPackWebUploadByUploadId(entry.pack_id, entry.upload_id, connection);
465
+ };
466
+
467
+ const updatePackWebUpload = async (uploadIdPk, fields, connection = null) => {
468
+ const clauses = [];
469
+ const params = [];
470
+ const mappings = {
471
+ upload_status: (value) => normalizePackWebUploadStatus(value, 'pending'),
472
+ sticker_id: (value) => value ?? null,
473
+ error_code: (value) => (value ? String(value).trim().slice(0, 64) : null),
474
+ error_message: (value) => clampUploadErrorMessage(value),
475
+ source_mimetype: (value) => (value ? String(value).trim().slice(0, 64) : null),
476
+ attempt_count: (value) => Math.max(0, Number(value || 0)),
477
+ last_attempt_at: (value) => value ?? null,
478
+ };
479
+
480
+ for (const [field, mapValue] of Object.entries(mappings)) {
481
+ if (!(field in fields)) continue;
482
+ clauses.push(`${field} = ?`);
483
+ params.push(mapValue(fields[field]));
484
+ }
485
+
486
+ if (!clauses.length) return null;
487
+ clauses.push('updated_at = CURRENT_TIMESTAMP');
488
+
489
+ await executeQuery(
490
+ `UPDATE ${TABLES.STICKER_PACK_WEB_UPLOAD}
491
+ SET ${clauses.join(', ')}
492
+ WHERE id = ?`,
493
+ [...params, uploadIdPk],
494
+ connection,
495
+ );
496
+
497
+ const rows = await executeQuery(`SELECT * FROM ${TABLES.STICKER_PACK_WEB_UPLOAD} WHERE id = ? LIMIT 1`, [uploadIdPk], connection);
498
+ return normalizePackWebUploadRow(rows?.[0] || null);
499
+ };
500
+
501
+ const setStickerPackStatus = async (packId, status, connection = null) => {
502
+ const normalizedStatus = normalizePackWebStatus(status, 'draft');
503
+ await executeQuery(
504
+ `UPDATE ${TABLES.STICKER_PACK}
505
+ SET status = ?,
506
+ version = version + 1,
507
+ updated_at = CURRENT_TIMESTAMP
508
+ WHERE id = ? AND deleted_at IS NULL`,
509
+ [normalizedStatus, packId],
510
+ connection,
511
+ );
512
+ return normalizedStatus;
513
+ };
514
+
515
+ const lockStickerPackByPackKey = async (packKey, connection) => {
516
+ const rows = await executeQuery(
517
+ `SELECT *
518
+ FROM ${TABLES.STICKER_PACK}
519
+ WHERE pack_key = ? AND deleted_at IS NULL
520
+ LIMIT 1
521
+ FOR UPDATE`,
522
+ [packKey],
523
+ connection,
524
+ );
525
+ return rows?.[0] || null;
526
+ };
527
+
528
+ const getPackConsistencySnapshot = async (packId, coverStickerId = null, connection = null) => {
529
+ const [itemsRow] = await executeQuery(
530
+ `SELECT
531
+ COUNT(*) AS sticker_count,
532
+ SUM(CASE WHEN sticker_id = ? THEN 1 ELSE 0 END) AS cover_matches
533
+ FROM ${TABLES.STICKER_PACK_ITEM}
534
+ WHERE pack_id = ?`,
535
+ [coverStickerId || '', packId],
536
+ connection,
537
+ );
538
+
539
+ const [uploadRow] = await executeQuery(
540
+ `SELECT
541
+ COUNT(*) AS total_uploads,
542
+ SUM(CASE WHEN upload_status = 'done' THEN 1 ELSE 0 END) AS done_uploads,
543
+ SUM(CASE WHEN upload_status = 'failed' THEN 1 ELSE 0 END) AS failed_uploads,
544
+ SUM(CASE WHEN upload_status = 'processing' THEN 1 ELSE 0 END) AS processing_uploads,
545
+ SUM(CASE WHEN upload_status = 'pending' THEN 1 ELSE 0 END) AS pending_uploads
546
+ FROM ${TABLES.STICKER_PACK_WEB_UPLOAD}
547
+ WHERE pack_id = ?`,
548
+ [packId],
549
+ connection,
550
+ );
551
+
552
+ return {
553
+ sticker_count: Number(itemsRow?.sticker_count || 0),
554
+ cover_set: Boolean(coverStickerId),
555
+ cover_valid: Boolean(coverStickerId) && Number(itemsRow?.cover_matches || 0) > 0,
556
+ total_uploads: Number(uploadRow?.total_uploads || 0),
557
+ done_uploads: Number(uploadRow?.done_uploads || 0),
558
+ failed_uploads: Number(uploadRow?.failed_uploads || 0),
559
+ processing_uploads: Number(uploadRow?.processing_uploads || 0),
560
+ pending_uploads: Number(uploadRow?.pending_uploads || 0),
561
+ };
562
+ };
563
+
564
+ const buildPackPublishStateData = async (pack, { includeUploads = true, connection = null } = {}) => {
565
+ const snapshot = await getPackConsistencySnapshot(pack.id, pack.cover_sticker_id, connection);
566
+ const uploads = includeUploads ? await listPackWebUploads(pack.id, connection) : [];
567
+
568
+ return {
569
+ pack_key: pack.pack_key,
570
+ status: normalizePackWebStatus(pack.status, 'draft'),
571
+ visibility: pack.visibility,
572
+ cover_sticker_id: pack.cover_sticker_id || null,
573
+ consistency: {
574
+ sticker_count: snapshot.sticker_count,
575
+ cover_set: snapshot.cover_set,
576
+ cover_valid: snapshot.cover_valid,
577
+ total_uploads: snapshot.total_uploads,
578
+ done_uploads: snapshot.done_uploads,
579
+ failed_uploads: snapshot.failed_uploads,
580
+ processing_uploads: snapshot.processing_uploads,
581
+ pending_uploads: snapshot.pending_uploads,
582
+ can_publish: snapshot.sticker_count >= 1 && snapshot.failed_uploads === 0 && snapshot.processing_uploads === 0 && snapshot.pending_uploads === 0 && snapshot.cover_valid,
583
+ },
584
+ uploads: uploads.map((entry) => ({
585
+ upload_id: entry.upload_id,
586
+ sticker_hash: entry.sticker_hash,
587
+ status: entry.upload_status,
588
+ sticker_id: entry.sticker_id || null,
589
+ error_code: entry.error_code || null,
590
+ error_message: entry.error_message || null,
591
+ attempt_count: Number(entry.attempt_count || 0),
592
+ updated_at: toIsoOrNull(entry.updated_at),
593
+ })),
594
+ updated_at: toIsoOrNull(pack.updated_at),
595
+ published: normalizePackWebStatus(pack.status, 'draft') === 'published',
596
+ };
597
+ };
598
+
599
+ const maybeCleanupStaleDraftPacks = async () => {
600
+ if (staleDraftCleanupState.running) return;
601
+ if (Date.now() - staleDraftCleanupState.lastRunAt < WEB_DRAFT_CLEANUP_RUN_INTERVAL_MS) return;
602
+
603
+ staleDraftCleanupState.running = true;
604
+ staleDraftCleanupState.lastRunAt = Date.now();
605
+
606
+ try {
607
+ const rows = await executeQuery(
608
+ `SELECT p.id, p.pack_key
609
+ FROM ${TABLES.STICKER_PACK} p
610
+ LEFT JOIN ${TABLES.STICKER_PACK_ITEM} i ON i.pack_id = p.id
611
+ WHERE p.deleted_at IS NULL
612
+ AND p.status = 'draft'
613
+ AND p.updated_at < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? SECOND)
614
+ GROUP BY p.id, p.pack_key
615
+ HAVING COUNT(i.id) = 0
616
+ LIMIT 200`,
617
+ [Math.floor(WEB_DRAFT_CLEANUP_TTL_MS / 1000)],
618
+ );
619
+
620
+ if (!rows.length) return;
621
+
622
+ await executeQuery(
623
+ `UPDATE ${TABLES.STICKER_PACK}
624
+ SET deleted_at = CURRENT_TIMESTAMP,
625
+ updated_at = CURRENT_TIMESTAMP,
626
+ version = version + 1
627
+ WHERE id IN (${rows.map(() => '?').join(', ')})`,
628
+ rows.map((row) => row.id),
629
+ );
630
+
631
+ logPackWebFlow('info', 'cleanup_draft_deleted', {
632
+ deleted_count: rows.length,
633
+ ttl_ms: WEB_DRAFT_CLEANUP_TTL_MS,
634
+ });
635
+ } catch (error) {
636
+ logPackWebFlow('warn', 'cleanup_draft_failed', {
637
+ error: error?.message,
638
+ });
639
+ } finally {
640
+ staleDraftCleanupState.running = false;
641
+ }
642
+ };
643
+
644
+ const triggerStaleDraftCleanup = () => {
645
+ maybeCleanupStaleDraftPacks().catch(() => {});
646
+ };
647
+
648
+ let revokeGoogleWebSessionsByIdentityBridge = async () => 0;
649
+
650
+ const adminBanContext = createStickerCatalogAdminBanContext({
651
+ executeQuery,
652
+ tables: TABLES,
653
+ sanitizeText,
654
+ normalizeGoogleSubject,
655
+ normalizeEmail,
656
+ normalizeJid,
657
+ toIsoOrNull,
658
+ revokeGoogleWebSessionsByIdentity: (payload) => revokeGoogleWebSessionsByIdentityBridge(payload),
659
+ });
660
+
661
+ const { listAdminBans, createAdminBanRecord, revokeAdminBanRecord, assertGoogleIdentityNotBanned } = adminBanContext;
662
+
663
+ const sendAsset = (req, res, buffer, mimetype = 'image/webp', cacheControlOverride = '') => {
664
+ const maxAgeSeconds = Math.max(60 * 60 * 24, ASSET_CACHE_SECONDS);
665
+ const staleWhileRevalidateSeconds = Math.min(60 * 60 * 24 * 7, Math.max(300, maxAgeSeconds));
666
+ res.statusCode = 200;
667
+ res.setHeader('Content-Type', mimetype);
668
+ res.setHeader('Content-Length', String(buffer.length));
669
+ res.setHeader('Cache-Control', cacheControlOverride || `public, max-age=${maxAgeSeconds}, stale-while-revalidate=${staleWhileRevalidateSeconds}`);
670
+ if (req.method === 'HEAD') {
671
+ res.end();
672
+ return;
673
+ }
674
+ res.end(buffer);
675
+ };
676
+
677
+ const CATALOG_STYLES_WEB_PATH = `${STICKER_WEB_PATH}/assets/styles.css`;
678
+ const CATALOG_SCRIPT_WEB_PATH = `${STICKER_WEB_PATH}/assets/catalog.js`;
679
+ const resolveActiveSocketBotJid = (activeSocket) => {
680
+ if (!activeSocket) return '';
681
+ const candidates = [activeSocket?.user?.id, activeSocket?.authState?.creds?.me?.id, activeSocket?.authState?.creds?.me?.lid];
682
+ for (const candidate of candidates) {
683
+ const resolved = resolveBotJid(candidate) || '';
684
+ if (resolved) return resolved;
685
+ }
686
+ return '';
687
+ };
688
+
689
+ const buildPackWhatsAppText = (pack) => STICKER_WEB_WHATSAPP_MESSAGE_TEMPLATE.replaceAll('{{pack_key}}', String(pack?.pack_key || '')).replaceAll('{{pack_name}}', String(pack?.name || ''));
690
+
691
+ const buildPackWhatsAppInfo = (pack) => {
692
+ const phone = resolveCatalogBotPhone();
693
+ if (!phone) return null;
694
+
695
+ const text = buildPackWhatsAppText(pack);
696
+ const url = `https://wa.me/${phone}?text=${encodeURIComponent(text)}`;
697
+
698
+ return {
699
+ phone,
700
+ text,
701
+ url,
702
+ };
703
+ };
704
+
705
+ const saveWebPackEditToken = ({ packId, ownerJid }) => {
706
+ if (!packId || !ownerJid) return null;
707
+ const token = randomUUID();
708
+ webPackEditTokenMap.set(token, {
709
+ packId,
710
+ ownerJid,
711
+ expiresAt: Date.now() + PACK_WEB_EDIT_TOKEN_TTL_MS,
712
+ });
713
+ return token;
714
+ };
715
+
716
+ const resolveWebPackEditToken = (token) => {
717
+ const normalized = String(token || '').trim();
718
+ if (!normalized) return null;
719
+ const entry = webPackEditTokenMap.get(normalized);
720
+ if (!entry) return null;
721
+ if (entry.expiresAt <= Date.now()) {
722
+ webPackEditTokenMap.delete(normalized);
723
+ return null;
724
+ }
725
+ return entry;
726
+ };
727
+
728
+ const decodeStickerBase64Payload = (value) => {
729
+ const raw = String(value || '').trim();
730
+ if (!raw) return null;
731
+
732
+ const dataUrlMatch = raw.match(/^data:([^;]+);base64,([\s\S]+)$/i);
733
+ const base64Value = dataUrlMatch ? dataUrlMatch[2] : raw;
734
+ const mimetype = dataUrlMatch
735
+ ? String(dataUrlMatch[1] || '')
736
+ .trim()
737
+ .toLowerCase()
738
+ : 'image/webp';
739
+ const cleaned = base64Value.replace(/\s+/g, '');
740
+ if (!cleaned) return null;
741
+
742
+ const buffer = Buffer.from(cleaned, 'base64');
743
+ if (!buffer.length) return null;
744
+ return {
745
+ buffer,
746
+ mimetype,
747
+ };
748
+ };
749
+
750
+ const isLikelyWebpBuffer = (buffer) => Buffer.isBuffer(buffer) && buffer.length >= 16 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP';
751
+
752
+ const resolveExtensionFromMimetype = (mimetype) => {
753
+ const normalized = String(mimetype || '')
754
+ .trim()
755
+ .toLowerCase();
756
+ if (normalized === 'image/jpeg') return 'jpg';
757
+ if (normalized === 'image/jpg') return 'jpg';
758
+ if (normalized === 'image/png') return 'png';
759
+ if (normalized === 'image/gif') return 'gif';
760
+ if (normalized === 'image/avif') return 'avif';
761
+ if (normalized === 'image/heic') return 'heic';
762
+ if (normalized === 'image/heif') return 'heif';
763
+ if (normalized === 'image/bmp') return 'bmp';
764
+ if (normalized === 'image/tiff') return 'tiff';
765
+ if (normalized === 'image/x-icon') return 'ico';
766
+ if (normalized === 'video/webm') return 'webm';
767
+ if (normalized === 'video/quicktime') return 'mov';
768
+ if (normalized === 'video/x-m4v') return 'm4v';
769
+ if (normalized === 'video/mp4') return 'mp4';
770
+ if (normalized === 'image/webp') return 'webp';
771
+ return 'bin';
772
+ };
773
+
774
+ const isPreviewVariantRequested = (url) => {
775
+ const variant = String(url?.searchParams?.get('variant') || url?.searchParams?.get('mode') || '')
776
+ .trim()
777
+ .toLowerCase();
778
+ if (['preview', 'thumb', 'thumbnail', 'small'].includes(variant)) return true;
779
+
780
+ const previewFlag = String(url?.searchParams?.get('preview') || '')
781
+ .trim()
782
+ .toLowerCase();
783
+ return ['1', 'true', 'yes', 'y', 'on'].includes(previewFlag);
784
+ };
785
+
786
+ const getStickerPreviewFromCache = (cacheKey) => {
787
+ const entry = STICKER_PREVIEW_CACHE.get(cacheKey);
788
+ if (!entry) return null;
789
+ if (entry.expiresAt <= Date.now()) {
790
+ STICKER_PREVIEW_CACHE.delete(cacheKey);
791
+ return null;
792
+ }
793
+ return entry.buffer;
794
+ };
795
+
796
+ const saveStickerPreviewToCache = (cacheKey, buffer) => {
797
+ if (!cacheKey || !Buffer.isBuffer(buffer) || !buffer.length) return;
798
+ if (STICKER_PREVIEW_CACHE.size >= STICKER_PREVIEW_CACHE_MAX_ITEMS) {
799
+ const overflow = STICKER_PREVIEW_CACHE.size - STICKER_PREVIEW_CACHE_MAX_ITEMS + 1;
800
+ const keys = STICKER_PREVIEW_CACHE.keys();
801
+ for (let index = 0; index < overflow; index += 1) {
802
+ const next = keys.next();
803
+ if (next.done) break;
804
+ STICKER_PREVIEW_CACHE.delete(next.value);
805
+ }
806
+ }
807
+ STICKER_PREVIEW_CACHE.set(cacheKey, {
808
+ buffer,
809
+ expiresAt: Date.now() + STICKER_PREVIEW_CACHE_TTL_MS,
810
+ });
811
+ };
812
+
813
+ const runPreviewFfmpeg = (args, timeoutMs = STICKER_PREVIEW_TIMEOUT_MS) =>
814
+ new Promise((resolve, reject) => {
815
+ const child = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] });
816
+ let stderr = '';
817
+ let timedOut = false;
818
+
819
+ const timer = setTimeout(
820
+ () => {
821
+ timedOut = true;
822
+ try {
823
+ child.kill('SIGTERM');
824
+ } catch {
825
+ // Processo já encerrado.
826
+ }
827
+ setTimeout(() => {
828
+ try {
829
+ child.kill('SIGKILL');
830
+ } catch {
831
+ // Processo já encerrado.
832
+ }
833
+ }, 1200);
834
+ },
835
+ Math.max(400, Number(timeoutMs || STICKER_PREVIEW_TIMEOUT_MS)),
836
+ );
837
+
838
+ child.stderr.on('data', (chunk) => {
839
+ stderr = `${stderr}${String(chunk || '')}`.slice(-16 * 1024);
840
+ });
841
+
842
+ child.on('error', (error) => {
843
+ clearTimeout(timer);
844
+ reject(error);
845
+ });
846
+
847
+ child.on('close', (code) => {
848
+ clearTimeout(timer);
849
+ if (timedOut) {
850
+ const timeoutError = new Error('preview_ffmpeg_timeout');
851
+ timeoutError.code = 'ETIMEDOUT';
852
+ timeoutError.stderr = stderr;
853
+ reject(timeoutError);
854
+ return;
855
+ }
856
+ if (code !== 0) {
857
+ const processError = new Error(`preview_ffmpeg_failed_code_${code}`);
858
+ processError.code = code;
859
+ processError.stderr = stderr;
860
+ reject(processError);
861
+ return;
862
+ }
863
+ resolve();
864
+ });
865
+ });
866
+
867
+ const generateStickerPreviewBuffer = async ({ sourceBuffer, mimetype = 'image/webp', cacheKey = '' } = {}) => {
868
+ if (!Buffer.isBuffer(sourceBuffer) || !sourceBuffer.length) return null;
869
+ if (cacheKey) {
870
+ const cached = getStickerPreviewFromCache(cacheKey);
871
+ if (cached) return cached;
872
+ }
873
+
874
+ const uniqueId = randomUUID();
875
+ const extension = resolveExtensionFromMimetype(mimetype || 'image/webp');
876
+ const inputPath = path.join(STICKER_PREVIEW_TEMP_DIR, `${uniqueId}.in.${extension}`);
877
+ const outputPath = path.join(STICKER_PREVIEW_TEMP_DIR, `${uniqueId}.preview.webp`);
878
+
879
+ try {
880
+ await fs.mkdir(STICKER_PREVIEW_TEMP_DIR, { recursive: true });
881
+ await fs.writeFile(inputPath, sourceBuffer);
882
+ const side = Math.max(96, Math.min(512, Number(STICKER_PREVIEW_SIDE_PX || 112)));
883
+ const quality = Math.max(10, Math.min(80, Number(STICKER_PREVIEW_QUALITY || 20)));
884
+ const filter = `scale=if(gte(iw,ih),${side},-1):if(gte(iw,ih),-1,${side}):flags=lanczos`;
885
+ await runPreviewFfmpeg(['-y', '-i', inputPath, '-vf', filter, '-frames:v', '1', '-vcodec', 'libwebp', '-lossless', '0', '-q:v', String(quality), '-compression_level', '6', '-preset', 'picture', '-an', outputPath], STICKER_PREVIEW_TIMEOUT_MS);
886
+ const previewBuffer = await fs.readFile(outputPath);
887
+ if (cacheKey && previewBuffer.length) {
888
+ saveStickerPreviewToCache(cacheKey, previewBuffer);
889
+ }
890
+ return previewBuffer.length ? previewBuffer : null;
891
+ } finally {
892
+ await fs.unlink(inputPath).catch(() => {});
893
+ await fs.unlink(outputPath).catch(() => {});
894
+ }
895
+ };
896
+
897
+ const convertUploadMediaToWebp = async ({ ownerJid, buffer, mimetype }) => {
898
+ const normalizedMimetype =
899
+ String(mimetype || '')
900
+ .trim()
901
+ .toLowerCase() || 'image/webp';
902
+ const isVideo = normalizedMimetype.startsWith('video/');
903
+ const isImage = normalizedMimetype.startsWith('image/');
904
+
905
+ if (!isVideo && !isImage) {
906
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Formato não suportado. Envie imagem ou vídeo.');
907
+ }
908
+
909
+ if (isLikelyWebpBuffer(buffer) && buffer.length <= MAX_STICKER_UPLOAD_BYTES) {
910
+ return { buffer, mimetype: 'image/webp' };
911
+ }
912
+
913
+ if (isVideo && !ALLOWED_WEB_UPLOAD_VIDEO_MIMETYPES.has(normalizedMimetype) && normalizedMimetype !== 'video/mp4') {
914
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'Formato de vídeo não suportado. Use mp4/webm/mov/m4v.');
915
+ }
916
+
917
+ const uniqueId = randomUUID();
918
+ const inputPath = path.join(process.cwd(), 'temp', 'stickers', 'web-create', `${uniqueId}.${resolveExtensionFromMimetype(normalizedMimetype)}`);
919
+
920
+ await fs.mkdir(path.dirname(inputPath), { recursive: true });
921
+ await fs.writeFile(inputPath, buffer);
922
+
923
+ const conversionProfiles = isVideo
924
+ ? [
925
+ { videoMaxDurationSeconds: 8, videoFps: 10, videoQuality: 55, videoCompressionLevel: 6 },
926
+ { videoMaxDurationSeconds: 6, videoFps: 9, videoQuality: 50, videoCompressionLevel: 6 },
927
+ { videoMaxDurationSeconds: 4, videoFps: 8, videoQuality: 44, videoCompressionLevel: 6 },
928
+ { videoMaxDurationSeconds: 3, videoFps: 8, videoQuality: 38, videoCompressionLevel: 6 },
929
+ { videoMaxDurationSeconds: 2, videoFps: 7, videoQuality: 34, videoCompressionLevel: 6 },
930
+ { videoMaxDurationSeconds: 1, videoFps: 6, videoQuality: 30, videoCompressionLevel: 6 },
931
+ ]
932
+ : [{ stretch: true }, { stretch: false }];
933
+
934
+ let lastError = null;
935
+ try {
936
+ for (const profile of conversionProfiles) {
937
+ let outputPath = null;
938
+ try {
939
+ outputPath = await convertToWebp(inputPath, isVideo ? 'video' : 'image', ownerJid, randomUUID(), {
940
+ ...profile,
941
+ maxOutputSizeBytes: MAX_STICKER_UPLOAD_BYTES,
942
+ });
943
+ const converted = await fs.readFile(outputPath);
944
+ if (!isLikelyWebpBuffer(converted) || converted.length > MAX_STICKER_UPLOAD_BYTES) {
945
+ throw new Error('WEBP convertido excedeu o limite final.');
946
+ }
947
+ return { buffer: converted, mimetype: 'image/webp' };
948
+ } catch (error) {
949
+ lastError = error;
950
+ } finally {
951
+ if (outputPath) {
952
+ await fs.unlink(outputPath).catch(() => {});
953
+ }
954
+ }
955
+ }
956
+ } finally {
957
+ await fs.unlink(inputPath).catch(() => {});
958
+ }
959
+
960
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.INVALID_INPUT, `Não foi possível converter a mídia para sticker no limite de ${Math.round(MAX_STICKER_UPLOAD_BYTES / 1024)}KB.`, lastError);
961
+ };
962
+
963
+ const PACK_TAG_MARKER_REGEX = /\[pack-tags:([^\]]+)\]/i;
964
+ const AUTO_PACK_MARKER_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/gi;
965
+ const AUTO_PACK_MARKER_TEST_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/i;
966
+ const AUTO_PACK_COLLECTOR_MARKER = '[auto-pack:collector]';
967
+ const AUTO_PACK_COLLECTOR_LEGACY_TEXT = 'coleção automática de figurinhas criadas pelo usuário.';
968
+ const AUTO_PACK_DESCRIPTION_PREFIX_REGEX = /^curadoria automática por tema\.\s*tema:\s*[^.]+\.?\s*(?:score\s*=\s*-?\d+(?:\.\d+)?\.?\s*)?/i;
969
+ const AUTO_PACK_SCORE_FRAGMENT_REGEX = /\bscore\s*=\s*-?\d+(?:\.\d+)?\.?/gi;
970
+ const normalizePackTag = (value) =>
971
+ String(value || '')
972
+ .trim()
973
+ .toLowerCase()
974
+ .normalize('NFD')
975
+ .replace(/[\u0300-\u036f]/g, '')
976
+ .replace(/[^a-z0-9]+/g, '-')
977
+ .replace(/^-+|-+$/g, '')
978
+ .slice(0, 40);
979
+
980
+ const mergeUniqueTags = (...groups) => {
981
+ const merged = [];
982
+ const seen = new Set();
983
+ for (const group of groups) {
984
+ for (const entry of Array.isArray(group) ? group : []) {
985
+ const normalized = normalizePackTag(entry);
986
+ if (!normalized || seen.has(normalized)) continue;
987
+ seen.add(normalized);
988
+ merged.push(normalized);
989
+ }
990
+ }
991
+ return merged;
992
+ };
993
+
994
+ const parsePackDescriptionMetadata = (description) => {
995
+ const raw = String(description || '').trim();
996
+ if (!raw) return { cleanDescription: null, tags: [] };
997
+
998
+ const marker = raw.match(PACK_TAG_MARKER_REGEX);
999
+ const markerTags = marker?.[1]
1000
+ ? marker[1]
1001
+ .split(',')
1002
+ .map((entry) => normalizePackTag(entry))
1003
+ .filter(Boolean)
1004
+ : [];
1005
+ let cleanDescription = raw.replace(PACK_TAG_MARKER_REGEX, '').trim() || null;
1006
+ const hasAutoPackMarker = AUTO_PACK_MARKER_TEST_REGEX.test(cleanDescription || '');
1007
+ if (cleanDescription) {
1008
+ cleanDescription = cleanDescription.replace(AUTO_PACK_MARKER_REGEX, '').trim() || null;
1009
+ }
1010
+ if (cleanDescription && hasAutoPackMarker) {
1011
+ cleanDescription =
1012
+ cleanDescription
1013
+ .replace(AUTO_PACK_DESCRIPTION_PREFIX_REGEX, '')
1014
+ .replace(AUTO_PACK_SCORE_FRAGMENT_REGEX, '')
1015
+ .replace(/\s{2,}/g, ' ')
1016
+ .replace(/^[\s.:-]+/, '')
1017
+ .trim() || null;
1018
+ }
1019
+
1020
+ return {
1021
+ cleanDescription,
1022
+ tags: mergeUniqueTags(markerTags).slice(0, 8),
1023
+ };
1024
+ };
1025
+
1026
+ const isCollectorAutoPack = (pack) => {
1027
+ if (!pack || typeof pack !== 'object') return false;
1028
+ const description = String(pack.description || '').toLowerCase();
1029
+ return description.includes(AUTO_PACK_COLLECTOR_MARKER) || description.includes(AUTO_PACK_COLLECTOR_LEGACY_TEXT);
1030
+ };
1031
+
1032
+ const isThemeCurationAutoPack = (pack) => {
1033
+ if (!pack || typeof pack !== 'object') return false;
1034
+ const name = String(pack.name || '').trim();
1035
+ if (/^\[auto\]/i.test(name)) return true;
1036
+
1037
+ const description = String(pack.description || '').toLowerCase();
1038
+ if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
1039
+
1040
+ return Boolean(String(pack.pack_theme_key || '').trim());
1041
+ };
1042
+
1043
+ const shouldHidePackFromMyProfileDefault = (pack, { includeAutoPacks = false } = {}) => {
1044
+ if (!pack || typeof pack !== 'object') return false;
1045
+ if (includeAutoPacks) return false;
1046
+ if (isCollectorAutoPack(pack)) return false;
1047
+ if (isThemeCurationAutoPack(pack)) return true;
1048
+ return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
1049
+ };
1050
+
1051
+ const buildPackDescriptionWithTags = (description, tags = []) => {
1052
+ const cleanDescription = sanitizeText(description || '', PACK_CREATE_MAX_DESCRIPTION_LENGTH, { allowEmpty: true }) || '';
1053
+ const normalizedTags = mergeUniqueTags(tags).slice(0, 8);
1054
+ const marker = normalizedTags.length ? `[pack-tags:${normalizedTags.join(',')}]` : '';
1055
+ const combined = `${marker}${marker && cleanDescription ? ' ' : ''}${cleanDescription}`.trim();
1056
+ return combined || null;
1057
+ };
1058
+
1059
+ const buildPackApiUrl = (packKey) => `${STICKER_API_BASE_PATH}/${encodeURIComponent(packKey)}`;
1060
+ const buildPackWebUrl = (packKey) => `${STICKER_WEB_PATH}/${encodeURIComponent(packKey)}`;
1061
+ const buildStickerAssetUrl = (packKey, stickerId) => `${STICKER_API_BASE_PATH}/${encodeURIComponent(packKey)}/stickers/${encodeURIComponent(stickerId)}.webp`;
1062
+ const buildStickerAssetPreviewUrl = (packKey, stickerId, versionToken = '') => {
1063
+ const params = new URLSearchParams();
1064
+ params.set('variant', 'preview');
1065
+ params.set('sz', String(STICKER_PREVIEW_SIDE_PX));
1066
+ params.set('q', String(STICKER_PREVIEW_QUALITY));
1067
+ const normalizedVersion = String(versionToken || '').trim();
1068
+ if (normalizedVersion) params.set('v', normalizedVersion);
1069
+ return `${buildStickerAssetUrl(packKey, stickerId)}?${params.toString()}`;
1070
+ };
1071
+ const buildDataAssetApiBaseUrl = () => `${STICKER_API_BASE_PATH}/data-files`;
1072
+
1073
+ const mapPackSummary = (pack, engagement = null, signals = null) => {
1074
+ const safeEngagement = engagement || getEmptyStickerPackEngagement();
1075
+ const metadata = parsePackDescriptionMetadata(pack.description);
1076
+ const stickerCount = Number(pack.sticker_count || 0);
1077
+ const coverVersionToken = toIsoOrNull(pack.updated_at) || toIsoOrNull(pack.created_at) || '';
1078
+ return {
1079
+ id: pack.id,
1080
+ pack_key: pack.pack_key,
1081
+ name: pack.name,
1082
+ publisher: pack.publisher,
1083
+ description: metadata.cleanDescription,
1084
+ visibility: pack.visibility,
1085
+ status: normalizePackWebStatus(pack.status, 'published'),
1086
+ sticker_count: stickerCount,
1087
+ is_complete: stickerCount >= PACK_CREATE_MAX_ITEMS,
1088
+ cover_sticker_id: pack.cover_sticker_id || null,
1089
+ cover_url: pack.cover_sticker_id ? buildStickerAssetUrl(pack.pack_key, pack.cover_sticker_id) : null,
1090
+ cover_preview_url: pack.cover_sticker_id ? buildStickerAssetPreviewUrl(pack.pack_key, pack.cover_sticker_id, coverVersionToken) : null,
1091
+ api_url: buildPackApiUrl(pack.pack_key),
1092
+ web_url: buildPackWebUrl(pack.pack_key),
1093
+ whatsapp: buildPackWhatsAppInfo(pack),
1094
+ created_at: toIsoOrNull(pack.created_at),
1095
+ updated_at: toIsoOrNull(pack.updated_at),
1096
+ engagement: {
1097
+ open_count: Number(safeEngagement.open_count || 0),
1098
+ like_count: Number(safeEngagement.like_count || 0),
1099
+ dislike_count: Number(safeEngagement.dislike_count || 0),
1100
+ score: Number(safeEngagement.score || 0) || Number(safeEngagement.like_count || 0) - Number(safeEngagement.dislike_count || 0),
1101
+ updated_at: toIsoOrNull(safeEngagement.updated_at),
1102
+ },
1103
+ signals: signals || null,
1104
+ manual_tags: metadata.tags,
1105
+ };
1106
+ };
1107
+
1108
+ const NSFW_HINT_TOKENS = ['nsfw', 'adult', 'explicit', 'suggestive', 'sexual', 'porn', 'nud', 'gore', '18', 'bikini', 'lingerie', 'underwear', 'swimsuit'];
1109
+ const normalizeNsfwToken = (value) =>
1110
+ String(value || '')
1111
+ .trim()
1112
+ .toLowerCase()
1113
+ .normalize('NFD')
1114
+ .replace(/[\u0300-\u036f]/g, '')
1115
+ .replace(/[^a-z0-9]+/g, '-');
1116
+
1117
+ const hasNsfwHint = (value) => {
1118
+ const normalized = normalizeNsfwToken(value);
1119
+ if (!normalized) return false;
1120
+ return NSFW_HINT_TOKENS.some((token) => normalized.includes(token));
1121
+ };
1122
+
1123
+ const hasNsfwHintsInList = (values) => {
1124
+ const list = Array.isArray(values) ? values : [];
1125
+ return list.some((entry) => hasNsfwHint(entry));
1126
+ };
1127
+
1128
+ const isClassificationMarkedNsfw = (classification) => {
1129
+ if (!classification || typeof classification !== 'object') return false;
1130
+ if (classification.is_nsfw === true) return true;
1131
+
1132
+ const nsfw = classification.nsfw || {};
1133
+ if (Number(nsfw.flagged_items || 0) > 0) return true;
1134
+ if (Number(nsfw.max_score || 0) >= 0.12) return true;
1135
+ if (Number(nsfw.avg_score || 0) >= 0.08) return true;
1136
+
1137
+ if (hasNsfwHint(classification.category || classification.majority_category || '')) return true;
1138
+ if (hasNsfwHintsInList(classification.tags)) return true;
1139
+ return false;
1140
+ };
1141
+
1142
+ const isSignalMarkedNsfw = (signals) => {
1143
+ const level = String(signals?.nsfw_level || '')
1144
+ .trim()
1145
+ .toLowerCase();
1146
+ if (signals?.sensitive_content === true) return true;
1147
+ if (!level) return false;
1148
+ return level !== 'safe';
1149
+ };
1150
+
1151
+ const isPackSummaryMarkedNsfw = (packSummary) => {
1152
+ if (!packSummary || typeof packSummary !== 'object') return false;
1153
+ if (packSummary.is_nsfw === true) return true;
1154
+ if (isSignalMarkedNsfw(packSummary.signals)) return true;
1155
+ if (isClassificationMarkedNsfw(packSummary.classification)) return true;
1156
+ if (hasNsfwHint(packSummary.name || '')) return true;
1157
+ if (hasNsfwHint(packSummary.description || '')) return true;
1158
+ if (hasNsfwHintsInList(packSummary.tags) || hasNsfwHintsInList(packSummary.manual_tags)) return true;
1159
+ return false;
1160
+ };
1161
+
1162
+ const mapPackDetails = (pack, items, { byAssetClassification = new Map(), packClassification = null, engagement = null, signals = null, hideSensitiveAssets = false } = {}) => {
1163
+ const coverStickerId = pack.cover_sticker_id || items[0]?.sticker_id || null;
1164
+ const metadata = parsePackDescriptionMetadata(pack.description);
1165
+ const decoratedClassification = decoratePackClassificationSummary(packClassification);
1166
+ const mergedTags = mergeUniqueTags(decoratedClassification?.tags || [], metadata.tags);
1167
+ const summary = mapPackSummary(
1168
+ {
1169
+ ...pack,
1170
+ description: metadata.cleanDescription,
1171
+ cover_sticker_id: coverStickerId,
1172
+ sticker_count: items.length,
1173
+ },
1174
+ engagement,
1175
+ signals,
1176
+ );
1177
+ const packPreview = {
1178
+ ...summary,
1179
+ classification: {
1180
+ ...(decoratedClassification || {}),
1181
+ tags: mergedTags,
1182
+ },
1183
+ tags: mergedTags,
1184
+ };
1185
+ const packIsNsfw = isPackSummaryMarkedNsfw(packPreview);
1186
+ const safeSummary = hideSensitiveAssets && packIsNsfw ? { ...summary, cover_url: null, cover_preview_url: null } : summary;
1187
+
1188
+ return {
1189
+ ...safeSummary,
1190
+ is_nsfw: packIsNsfw,
1191
+ items: items.map((item) => {
1192
+ const decoratedItemClassification = decorateStickerClassification(byAssetClassification.get(item.sticker_id) || null);
1193
+ const itemIsNsfw = isClassificationMarkedNsfw(decoratedItemClassification);
1194
+ const hideAsset = hideSensitiveAssets && (packIsNsfw || itemIsNsfw);
1195
+ const previewVersionToken = String(item?.asset?.id || item?.asset?.size_bytes || item?.created_at || pack?.updated_at || '').trim();
1196
+ return {
1197
+ // `tags` facilita renderização direta no front sem precisar reprocessar score.
1198
+ id: item.id,
1199
+ sticker_id: item.sticker_id,
1200
+ position: Number(item.position || 0),
1201
+ emojis: Array.isArray(item.emojis) ? item.emojis : [],
1202
+ accessibility_label: item.accessibility_label || null,
1203
+ created_at: toIsoOrNull(item.created_at),
1204
+ asset_url: hideAsset ? null : buildStickerAssetUrl(pack.pack_key, item.sticker_id),
1205
+ asset_preview_url: hideAsset ? null : buildStickerAssetPreviewUrl(pack.pack_key, item.sticker_id, previewVersionToken),
1206
+ tags: decoratedItemClassification?.tags || [],
1207
+ is_nsfw: itemIsNsfw,
1208
+ asset: item.asset
1209
+ ? {
1210
+ id: item.asset.id,
1211
+ mimetype: item.asset.mimetype || 'image/webp',
1212
+ is_animated: Boolean(item.asset.is_animated),
1213
+ width: item.asset.width !== null && item.asset.width !== undefined ? Number(item.asset.width) : null,
1214
+ height: item.asset.height !== null && item.asset.height !== undefined ? Number(item.asset.height) : null,
1215
+ size_bytes: item.asset.size_bytes !== null && item.asset.size_bytes !== undefined ? Number(item.asset.size_bytes) : 0,
1216
+ classification: decoratedItemClassification,
1217
+ }
1218
+ : null,
1219
+ };
1220
+ }),
1221
+ classification: {
1222
+ ...(decoratedClassification || {}),
1223
+ tags: mergedTags,
1224
+ },
1225
+ tags: mergedTags,
1226
+ };
1227
+ };
1228
+
1229
+ const mapOrphanStickerAsset = (asset, classification = null, { hideSensitiveAssets = false } = {}) => {
1230
+ const decoratedClassification = decorateStickerClassification(classification || null);
1231
+ const isNsfw = isClassificationMarkedNsfw(decoratedClassification);
1232
+ return {
1233
+ id: asset.id,
1234
+ owner_jid: asset.owner_jid,
1235
+ sha256: asset.sha256,
1236
+ mimetype: asset.mimetype || 'image/webp',
1237
+ is_animated: Boolean(asset.is_animated),
1238
+ width: asset.width !== null && asset.width !== undefined ? Number(asset.width) : null,
1239
+ height: asset.height !== null && asset.height !== undefined ? Number(asset.height) : null,
1240
+ size_bytes: asset.size_bytes !== null && asset.size_bytes !== undefined ? Number(asset.size_bytes) : 0,
1241
+ created_at: toIsoOrNull(asset.created_at),
1242
+ url: hideSensitiveAssets && isNsfw ? null : toPublicDataUrlFromStoragePath(asset.storage_path),
1243
+ classification: decoratedClassification,
1244
+ tags: decoratedClassification?.tags || [],
1245
+ is_nsfw: isNsfw,
1246
+ };
1247
+ };
1248
+
1249
+ const toSummaryEntry = (entry, { hideSensitiveCover = false } = {}) => {
1250
+ const summary = {
1251
+ ...mapPackSummary(entry.pack, entry.engagement, entry.signals),
1252
+ classification: entry.packClassification,
1253
+ tags: mergeUniqueTags(entry.packClassification?.tags || [], parsePackDescriptionMetadata(entry.pack?.description).tags),
1254
+ };
1255
+ const isNsfw = isPackSummaryMarkedNsfw(summary);
1256
+ const safeSummary = hideSensitiveCover && isNsfw ? { ...summary, cover_url: null, cover_preview_url: null } : summary;
1257
+ return {
1258
+ ...safeSummary,
1259
+ is_nsfw: isNsfw,
1260
+ };
1261
+ };
1262
+
1263
+ const classifyPackIntent = (entry) => {
1264
+ if (entry?.signals?.trending_now) return 'crescendo_agora';
1265
+ if (entry?.signals?.pack_score >= 0.65) return 'em_alta';
1266
+ if (Number(entry?.engagement?.like_count || 0) >= 12) return 'mais_curtidos';
1267
+ return 'novos';
1268
+ };
1269
+
1270
+ const normalizeViewerKey = (raw) =>
1271
+ String(raw || '')
1272
+ .trim()
1273
+ .replace(/[^a-zA-Z0-9._:@-]+/g, '')
1274
+ .slice(0, 120);
1275
+
1276
+ const resolveActorKeysFromRequest = (req, url) => {
1277
+ const searchParams = url?.searchParams || new URLSearchParams();
1278
+ const cookies = parseCookies(req);
1279
+ const normalizeHeaderValue = (value) => (Array.isArray(value) ? value[0] : value);
1280
+ const firstKey = (...values) => {
1281
+ for (const value of values) {
1282
+ const normalized = normalizeViewerKey(value);
1283
+ if (normalized) return normalized;
1284
+ }
1285
+ return '';
1286
+ };
1287
+
1288
+ const sessionKey = firstKey(searchParams.get('session_key'), searchParams.get('sid'), cookies.omnizap_sid, cookies.session_key, normalizeHeaderValue(req.headers['x-session-key']), normalizeHeaderValue(req.headers['x-client-session']));
1289
+
1290
+ let actorKey = firstKey(searchParams.get('viewer_key'), searchParams.get('actor_key'), cookies.omnizap_vid, cookies.viewer_key, cookies.visitor_key, normalizeHeaderValue(req.headers['x-viewer-key']), normalizeHeaderValue(req.headers['x-visitor-key']), sessionKey);
1291
+
1292
+ if (!actorKey) {
1293
+ const remoteIp = String(resolveRequestRemoteIp(req) || '').trim();
1294
+ const userAgent = String(req.headers['user-agent'] || '')
1295
+ .trim()
1296
+ .slice(0, 300);
1297
+ if (remoteIp || userAgent) {
1298
+ actorKey = createHash('sha256').update(`${remoteIp}|${userAgent}`).digest('hex').slice(0, 64);
1299
+ }
1300
+ }
1301
+
1302
+ return {
1303
+ actorKey: firstKey(actorKey),
1304
+ sessionKey: firstKey(sessionKey),
1305
+ source: firstKey(searchParams.get('source'), searchParams.get('client_source'), normalizeHeaderValue(req.headers['x-client-source']), normalizeHeaderValue(req.headers['x-source'])) || 'web',
1306
+ };
1307
+ };
1308
+
1309
+ const hydrateMarketplaceEntries = async (packs, { includeItems = true, driftSnapshot = null } = {}) => {
1310
+ const packIds = packs.map((pack) => pack.id);
1311
+ const engagementByPackId = await listStickerPackEngagementByPackIds(packIds);
1312
+ const interactionStatsByPackId = await listStickerPackInteractionStatsByPackIds(packIds);
1313
+ const useSnapshot = await canUseRankingSnapshotRead(`hydrate:${packIds.length}:${includeItems ? 1 : 0}`);
1314
+ const snapshotByPackId = useSnapshot ? await listStickerPackScoreSnapshotsByPackIds(packIds).catch(() => new Map()) : new Map();
1315
+
1316
+ const entries = [];
1317
+ const packClassificationById = new Map();
1318
+
1319
+ for (const pack of packs) {
1320
+ const items = includeItems ? await listStickerPackItems(pack.id) : [];
1321
+ const stickerIds = items.map((item) => item.sticker_id);
1322
+ const [packClassification, itemClassifications] = await Promise.all([getPackClassificationSummaryByAssetIds(stickerIds), stickerIds.length ? listStickerClassificationsByAssetIds(stickerIds) : Promise.resolve([])]);
1323
+ const byAssetClassification = new Map(itemClassifications.map((classification) => [classification.asset_id, classification]));
1324
+ const orderedClassifications = stickerIds.map((stickerId) => byAssetClassification.get(stickerId)).filter(Boolean);
1325
+ const engagement = engagementByPackId.get(pack.id) || getEmptyStickerPackEngagement();
1326
+ const interactionStats = interactionStatsByPackId.get(pack.id) || null;
1327
+ const packMetadata = parsePackDescriptionMetadata(pack.description);
1328
+ const decoratedClassification = decoratePackClassificationSummary(packClassification);
1329
+ const mergedPackTags = mergeUniqueTags(decoratedClassification?.tags || [], packMetadata.tags);
1330
+ const snapshot = snapshotByPackId.get(pack.id);
1331
+ const signals = snapshot?.signals
1332
+ ? {
1333
+ ...snapshot.signals,
1334
+ ranking_score: Number(snapshot?.signals?.ranking_score || 0),
1335
+ pack_score: Number(snapshot?.signals?.pack_score || 0),
1336
+ trend_score: Number(snapshot?.signals?.trend_score || 0),
1337
+ nsfw_level: String(snapshot?.signals?.nsfw_level || 'safe'),
1338
+ sensitive_content: Boolean(snapshot?.signals?.sensitive_content),
1339
+ }
1340
+ : computePackSignals({
1341
+ pack: { ...pack, items },
1342
+ engagement,
1343
+ packClassification,
1344
+ itemClassifications: orderedClassifications,
1345
+ interactionStats,
1346
+ scoringWeights: driftSnapshot?.weights || null,
1347
+ });
1348
+
1349
+ const entry = {
1350
+ pack,
1351
+ items,
1352
+ engagement,
1353
+ packClassification: {
1354
+ ...(decoratedClassification || {}),
1355
+ tags: mergedPackTags,
1356
+ },
1357
+ signals,
1358
+ interactionStats,
1359
+ };
1360
+ entries.push(entry);
1361
+ packClassificationById.set(pack.id, entry.packClassification);
1362
+ }
1363
+
1364
+ return { entries, packClassificationById };
1365
+ };
1366
+
1367
+ const isStickerClassified = (classification) => {
1368
+ if (!classification || typeof classification !== 'object') return false;
1369
+ if (classification.category) return true;
1370
+ if (classification.is_nsfw) return true;
1371
+ if (classification.all_scores && Object.keys(classification.all_scores).length > 0) return true;
1372
+ return false;
1373
+ };
1374
+
1375
+ const isPackClassified = (classificationSummary) => Boolean(classificationSummary && Number(classificationSummary.classified_items || 0) > 0);
1376
+
1377
+ const normalizeCategoryToken = (value) =>
1378
+ String(value || '')
1379
+ .trim()
1380
+ .toLowerCase()
1381
+ .normalize('NFD')
1382
+ .replace(/[\u0300-\u036f]/g, '')
1383
+ .replace(/[^a-z0-9]+/g, '-')
1384
+ .replace(/^-+|-+$/g, '')
1385
+ .slice(0, 40);
1386
+
1387
+ const parseCategoryFilters = (rawValue) => {
1388
+ const raw = String(rawValue || '').trim();
1389
+ if (!raw) return [];
1390
+
1391
+ const parts = raw
1392
+ .split(',')
1393
+ .map((part) => normalizeCategoryToken(part))
1394
+ .filter(Boolean);
1395
+
1396
+ return Array.from(new Set(parts)).slice(0, 20);
1397
+ };
1398
+
1399
+ const hasAnyCategory = (tags, categories) => {
1400
+ if (!Array.isArray(categories) || !categories.length) return true;
1401
+ const normalized = new Set((Array.isArray(tags) ? tags : []).map((entry) => normalizeCategoryToken(entry)));
1402
+ return categories.some((category) => normalized.has(category));
1403
+ };
1404
+
1405
+ const getEntryStickerCount = (entry) => Math.max(0, Number(entry?.pack?.sticker_count || 0));
1406
+ const compareEntriesByPackCompleteness = (left, right) => {
1407
+ const leftCount = getEntryStickerCount(left);
1408
+ const rightCount = getEntryStickerCount(right);
1409
+ const leftIsComplete = leftCount >= PACK_CREATE_MAX_ITEMS ? 1 : 0;
1410
+ const rightIsComplete = rightCount >= PACK_CREATE_MAX_ITEMS ? 1 : 0;
1411
+ if (rightIsComplete !== leftIsComplete) return rightIsComplete - leftIsComplete;
1412
+ if (rightCount !== leftCount) return rightCount - leftCount;
1413
+ const leftHasCover = left?.pack?.cover_sticker_id ? 1 : 0;
1414
+ const rightHasCover = right?.pack?.cover_sticker_id ? 1 : 0;
1415
+ return rightHasCover - leftHasCover;
1416
+ };
1417
+
1418
+ const resolveClassificationTags = (classification) => decorateStickerClassification(classification || null)?.tags || [];
1419
+
1420
+ const listClassifiedOrphanAssetsByCategories = async ({ search = '', categories = [], limit = 120, offset = 0 }) => {
1421
+ const safeLimit = Math.max(1, Math.min(MAX_ORPHAN_LIST_LIMIT, Number(limit) || DEFAULT_ORPHAN_LIST_LIMIT));
1422
+ const safeOffset = Math.max(0, Number(offset) || 0);
1423
+ const normalizedCategories = Array.isArray(categories) ? categories.filter(Boolean) : [];
1424
+ const scanBatchSize = Math.max(safeLimit, 180);
1425
+
1426
+ let cursorOffset = 0;
1427
+ let matchedCount = 0;
1428
+ const pageAssets = [];
1429
+
1430
+ while (true) {
1431
+ const { assets, hasMore } = await listClassifiedStickerAssetsWithoutPack({
1432
+ search,
1433
+ limit: scanBatchSize,
1434
+ offset: cursorOffset,
1435
+ });
1436
+
1437
+ if (!assets.length) break;
1438
+
1439
+ const classifications = await listStickerClassificationsByAssetIds(assets.map((asset) => asset.id));
1440
+ const byAssetId = new Map(classifications.map((entry) => [entry.asset_id, entry]));
1441
+
1442
+ for (const asset of assets) {
1443
+ const tags = resolveClassificationTags(byAssetId.get(asset.id));
1444
+ if (!hasAnyCategory(tags, normalizedCategories)) continue;
1445
+
1446
+ const currentIndex = matchedCount;
1447
+ matchedCount += 1;
1448
+
1449
+ if (currentIndex >= safeOffset && pageAssets.length < safeLimit) {
1450
+ pageAssets.push(asset);
1451
+ }
1452
+ }
1453
+
1454
+ cursorOffset += assets.length;
1455
+ if (!hasMore) break;
1456
+ }
1457
+
1458
+ return {
1459
+ assets: pageAssets,
1460
+ total: matchedCount,
1461
+ hasMore: safeOffset + safeLimit < matchedCount,
1462
+ };
1463
+ };
1464
+
1465
+ export const extractPackKeyFromWebPath = (pathname) => {
1466
+ if (!hasPathPrefix(pathname, STICKER_WEB_PATH)) return null;
1467
+
1468
+ const suffix = pathname.slice(STICKER_WEB_PATH.length);
1469
+ if (!suffix || suffix === '/') return null;
1470
+
1471
+ const [firstSegment] = suffix.split('/').filter(Boolean);
1472
+ if (!firstSegment) return null;
1473
+
1474
+ try {
1475
+ return decodeURIComponent(firstSegment);
1476
+ } catch {
1477
+ return null;
1478
+ }
1479
+ };
1480
+
1481
+ const seoContext = createStickerCatalogSeoContext({
1482
+ executeQuery,
1483
+ tables: TABLES,
1484
+ listStickerPacksForCatalog,
1485
+ logger,
1486
+ sendJson,
1487
+ toSiteAbsoluteUrl,
1488
+ isPackPubliclyVisible,
1489
+ buildPackWebUrl,
1490
+ config: {
1491
+ stickerWebPath: STICKER_WEB_PATH,
1492
+ stickerApiBasePath: STICKER_API_BASE_PATH,
1493
+ stickerOrphanApiPath: STICKER_ORPHAN_API_PATH,
1494
+ stickerLoginWebPath: STICKER_LOGIN_WEB_PATH,
1495
+ stickerCreateWebPath: STICKER_CREATE_WEB_PATH,
1496
+ stickerDataPublicPath: STICKER_DATA_PUBLIC_PATH,
1497
+ defaultListLimit: DEFAULT_LIST_LIMIT,
1498
+ defaultOrphanListLimit: DEFAULT_ORPHAN_LIST_LIMIT,
1499
+ catalogTemplatePath: CATALOG_TEMPLATE_PATH,
1500
+ createPackTemplatePath: CREATE_PACK_TEMPLATE_PATH,
1501
+ catalogStylesFilePath: CATALOG_STYLES_FILE_PATH,
1502
+ catalogScriptFilePath: CATALOG_SCRIPT_FILE_PATH,
1503
+ stickerWebAssetVersion: STICKER_WEB_ASSET_VERSION,
1504
+ catalogStylesWebPath: CATALOG_STYLES_WEB_PATH,
1505
+ catalogScriptWebPath: CATALOG_SCRIPT_WEB_PATH,
1506
+ nsfwStickerPlaceholderUrl: NSFW_STICKER_PLACEHOLDER_URL,
1507
+ packCommandPrefix: PACK_COMMAND_PREFIX,
1508
+ staticTextCacheSeconds: STATIC_TEXT_CACHE_SECONDS,
1509
+ immutableAssetCacheSeconds: IMMUTABLE_ASSET_CACHE_SECONDS,
1510
+ sitemapMaxPacks: SITEMAP_MAX_PACKS,
1511
+ sitemapCacheSeconds: SITEMAP_CACHE_SECONDS,
1512
+ seoDiscoveryLinkLimit: SEO_DISCOVERY_LINK_LIMIT,
1513
+ seoDiscoveryCacheSeconds: SEO_DISCOVERY_CACHE_SECONDS,
1514
+ },
1515
+ });
1516
+
1517
+ const { handleCatalogStaticAssetRequest, renderCatalogHtml, renderPackSeoHtml, renderPackNotFoundHtml, renderCreatePackHtml, handleSitemapRequest } = seoContext;
1518
+ const handleListRequest = async (req, res, url) => {
1519
+ const q = sanitizeText(url.searchParams.get('q') || '', 120, { allowEmpty: true }) || '';
1520
+ const visibility = normalizeCatalogVisibility(url.searchParams.get('visibility'));
1521
+ const sort = normalizeCatalogSortParam(url.searchParams.get('sort'));
1522
+ const categories = parseCategoryFilters(url.searchParams.get('categories'));
1523
+ const intent = sanitizeText(url.searchParams.get('intent') || '', 32, { allowEmpty: true }) || '';
1524
+ const includeSensitive = parseEnvBool(url.searchParams.get('include_sensitive'), true);
1525
+ const limit = clampInt(url.searchParams.get('limit'), DEFAULT_LIST_LIMIT, 1, MAX_LIST_LIMIT);
1526
+ const offset = clampInt(url.searchParams.get('offset'), 0, 0, 100000);
1527
+ const normalizedIntent = normalizeCategoryToken(intent).replace(/-/g, '_');
1528
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
1529
+ const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
1530
+ const cacheKey = buildCacheKey(['list', q, visibility, sort, categories.join(','), normalizedIntent, includeSensitive ? 1 : 0, limit, offset, hasNsfwAccess ? 1 : 0]);
1531
+ const payload = await getCachedSnapshot({
1532
+ cacheMap: CATALOG_LIST_CACHE,
1533
+ key: cacheKey,
1534
+ ttlSeconds: CATALOG_LIST_CACHE_SECONDS,
1535
+ staleWhileRefresh: true,
1536
+ staleOnError: true,
1537
+ load: async () => {
1538
+ const batchLimit = Math.max(limit, Math.min(MAX_LIST_LIMIT, 24));
1539
+ const maxPagesToScan = 8;
1540
+ const seenPackIds = new Set();
1541
+ const collectedEntries = [];
1542
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
1543
+ let sourceHasMore = true;
1544
+ let cursorOffset = offset;
1545
+ let pagesScanned = 0;
1546
+
1547
+ while (collectedEntries.length < limit && sourceHasMore && pagesScanned < maxPagesToScan) {
1548
+ pagesScanned += 1;
1549
+ const { packs, hasMore } = await listStickerPacksForCatalog({
1550
+ visibility,
1551
+ search: q,
1552
+ limit: batchLimit,
1553
+ offset: cursorOffset,
1554
+ });
1555
+ sourceHasMore = hasMore;
1556
+ cursorOffset += batchLimit;
1557
+ if (!packs.length) break;
1558
+
1559
+ const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
1560
+ const entriesClassified = STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries;
1561
+ const entriesByCategory = categories.length ? entriesClassified.filter((entry) => hasAnyCategory(entry.packClassification?.tags || [], categories)) : entriesClassified;
1562
+ const entriesBySensitivity = includeSensitive ? entriesByCategory : entriesByCategory.filter((entry) => entry.signals?.nsfw_level === 'safe');
1563
+ const entriesByIntent = intent ? entriesBySensitivity.filter((entry) => classifyPackIntent(entry) === normalizedIntent) : entriesBySensitivity;
1564
+ const sortedEntries = [...entriesByIntent].sort((left, right) => {
1565
+ const completenessDelta = compareEntriesByPackCompleteness(left, right);
1566
+ if (completenessDelta !== 0) return completenessDelta;
1567
+ if (sort === 'recent') {
1568
+ return Date.parse(right?.pack?.created_at || right?.pack?.updated_at || 0) - Date.parse(left?.pack?.created_at || left?.pack?.updated_at || 0);
1569
+ }
1570
+ if (sort === 'likes') {
1571
+ return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
1572
+ }
1573
+ if (sort === 'downloads') {
1574
+ return Number(right?.engagement?.open_count || 0) - Number(left?.engagement?.open_count || 0);
1575
+ }
1576
+ if (sort === 'comments') {
1577
+ const commentDelta = Number(right?.engagement?.comment_count || 0) - Number(left?.engagement?.comment_count || 0);
1578
+ if (commentDelta !== 0) return commentDelta;
1579
+ return Number(right?.engagement?.like_count || 0) - Number(left?.engagement?.like_count || 0);
1580
+ }
1581
+ if (sort === 'trending') {
1582
+ const trendDelta = Number(right?.signals?.trend_score || 0) - Number(left?.signals?.trend_score || 0);
1583
+ if (trendDelta !== 0) return trendDelta;
1584
+ }
1585
+ const leftScore = Number(left?.signals?.ranking_score || 0);
1586
+ const rightScore = Number(right?.signals?.ranking_score || 0);
1587
+ if (rightScore !== leftScore) return rightScore - leftScore;
1588
+ return Date.parse(right?.pack?.updated_at || 0) - Date.parse(left?.pack?.updated_at || 0);
1589
+ });
1590
+
1591
+ for (const entry of sortedEntries) {
1592
+ if (!entry?.pack?.id) continue;
1593
+ if (seenPackIds.has(entry.pack.id)) continue;
1594
+ seenPackIds.add(entry.pack.id);
1595
+ collectedEntries.push(entry);
1596
+ if (collectedEntries.length >= limit) break;
1597
+ }
1598
+ }
1599
+
1600
+ return {
1601
+ data: collectedEntries.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
1602
+ pagination: {
1603
+ limit,
1604
+ offset,
1605
+ has_more: sourceHasMore,
1606
+ next_offset: sourceHasMore ? cursorOffset : null,
1607
+ },
1608
+ filters: {
1609
+ q,
1610
+ visibility,
1611
+ sort,
1612
+ categories,
1613
+ intent: intent || null,
1614
+ include_sensitive: includeSensitive,
1615
+ },
1616
+ };
1617
+ },
1618
+ });
1619
+
1620
+ sendJson(req, res, 200, payload);
1621
+ };
1622
+
1623
+ const handleIntentCollectionsRequest = async (req, res, url) => {
1624
+ const visibility = normalizeCatalogVisibility(url.searchParams.get('visibility'));
1625
+ const q = sanitizeText(url.searchParams.get('q') || '', 120, { allowEmpty: true }) || '';
1626
+ const categories = parseCategoryFilters(url.searchParams.get('categories'));
1627
+ const limit = clampInt(url.searchParams.get('limit'), 18, 4, 50);
1628
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
1629
+ const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
1630
+
1631
+ const { packs } = await listStickerPacksForCatalog({
1632
+ visibility,
1633
+ search: q,
1634
+ limit: Math.max(limit * 3, 40),
1635
+ offset: 0,
1636
+ });
1637
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
1638
+ const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
1639
+ const entriesClassified = STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries;
1640
+ const entriesByCategory = categories.length ? entriesClassified.filter((entry) => hasAnyCategory(entry.packClassification?.tags || [], categories)) : entriesClassified;
1641
+ const intents = buildIntentCollections(entriesByCategory, { limit });
1642
+
1643
+ sendJson(req, res, 200, {
1644
+ data: {
1645
+ em_alta: intents.em_alta.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
1646
+ novos: intents.novos.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
1647
+ crescendo_agora: intents.crescendo_agora.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
1648
+ mais_curtidos: intents.mais_curtidos.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
1649
+ melhor_avaliados: intents.melhor_avaliados.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
1650
+ },
1651
+ filters: {
1652
+ visibility,
1653
+ q,
1654
+ categories,
1655
+ limit,
1656
+ },
1657
+ });
1658
+ };
1659
+
1660
+ const resolveMarketplaceVisibilityValues = (visibility) => (visibility === 'all' ? ['public', 'unlisted'] : visibility === 'unlisted' ? ['unlisted'] : ['public']);
1661
+
1662
+ const getHomeMarketplaceStatsCacheBucket = (visibility) => {
1663
+ const key = normalizeCatalogVisibility(visibility);
1664
+ let bucket = HOME_MARKETPLACE_STATS_CACHE.get(key);
1665
+ if (!bucket) {
1666
+ bucket = {
1667
+ expiresAt: 0,
1668
+ value: null,
1669
+ pending: null,
1670
+ };
1671
+ HOME_MARKETPLACE_STATS_CACHE.set(key, bucket);
1672
+ }
1673
+ return bucket;
1674
+ };
1675
+
1676
+ const buildMarketplaceStatsSnapshot = async (visibility) => {
1677
+ const normalizedVisibility = normalizeCatalogVisibility(visibility);
1678
+ const visibilityValues = resolveMarketplaceVisibilityValues(normalizedVisibility);
1679
+ const placeholders = visibilityValues.map(() => '?').join(', ');
1680
+
1681
+ const [packStatsRow] = await executeQuery(
1682
+ `SELECT
1683
+ COUNT(DISTINCT p.id) AS packs_total,
1684
+ COUNT(i.sticker_id) AS stickers_total,
1685
+ COUNT(DISTINCT p.publisher) AS creators_total
1686
+ FROM sticker_pack p
1687
+ LEFT JOIN sticker_pack_item i ON i.pack_id = p.id
1688
+ WHERE p.deleted_at IS NULL
1689
+ AND p.status = 'published'
1690
+ AND COALESCE(p.pack_status, 'ready') = 'ready'
1691
+ AND p.visibility IN (${placeholders})`,
1692
+ visibilityValues,
1693
+ );
1694
+
1695
+ const [downloadsRow] = await executeQuery(
1696
+ `SELECT COALESCE(SUM(e.open_count), 0) AS downloads_total
1697
+ FROM sticker_pack_engagement e
1698
+ INNER JOIN sticker_pack p ON p.id = e.pack_id
1699
+ WHERE p.deleted_at IS NULL
1700
+ AND p.status = 'published'
1701
+ AND COALESCE(p.pack_status, 'ready') = 'ready'
1702
+ AND p.visibility IN (${placeholders})`,
1703
+ visibilityValues,
1704
+ );
1705
+
1706
+ return {
1707
+ data: {
1708
+ packs_total: Number(packStatsRow?.packs_total || 0),
1709
+ stickers_total: Number(packStatsRow?.stickers_total || 0),
1710
+ creators_total: Number(packStatsRow?.creators_total || 0),
1711
+ downloads_total: Number(downloadsRow?.downloads_total || 0),
1712
+ },
1713
+ filters: {
1714
+ visibility: normalizedVisibility,
1715
+ },
1716
+ };
1717
+ };
1718
+
1719
+ const getMarketplaceStatsCached = async (visibility) => {
1720
+ const normalizedVisibility = normalizeCatalogVisibility(visibility);
1721
+ const bucket = getHomeMarketplaceStatsCacheBucket(normalizedVisibility);
1722
+ const now = Date.now();
1723
+ const hasValue = Boolean(bucket.value);
1724
+
1725
+ if (hasValue && now < bucket.expiresAt) {
1726
+ return bucket.value;
1727
+ }
1728
+
1729
+ if (!bucket.pending) {
1730
+ bucket.pending = withTimeout(buildMarketplaceStatsSnapshot(normalizedVisibility), 5000)
1731
+ .then((data) => {
1732
+ bucket.value = data;
1733
+ bucket.expiresAt = Date.now() + HOME_MARKETPLACE_STATS_CACHE_SECONDS * 1000;
1734
+ return data;
1735
+ })
1736
+ .finally(() => {
1737
+ bucket.pending = null;
1738
+ });
1739
+ }
1740
+
1741
+ if (hasValue) return bucket.value;
1742
+ return bucket.pending;
1743
+ };
1744
+
1745
+ const handleMarketplaceStatsRequest = async (req, res, url) => {
1746
+ const visibility = normalizeCatalogVisibility(url.searchParams.get('visibility'));
1747
+ const cacheBucket = getHomeMarketplaceStatsCacheBucket(visibility);
1748
+ try {
1749
+ const payload = await getMarketplaceStatsCached(visibility);
1750
+ sendJson(req, res, 200, {
1751
+ ...payload,
1752
+ meta: {
1753
+ cache_seconds: HOME_MARKETPLACE_STATS_CACHE_SECONDS,
1754
+ },
1755
+ });
1756
+ } catch (error) {
1757
+ logger.warn('Falha ao montar stats da home do marketplace.', {
1758
+ action: 'home_marketplace_stats_error',
1759
+ error: error?.message,
1760
+ visibility,
1761
+ });
1762
+ if (cacheBucket.value) {
1763
+ sendJson(req, res, 200, {
1764
+ ...cacheBucket.value,
1765
+ meta: {
1766
+ cache_seconds: HOME_MARKETPLACE_STATS_CACHE_SECONDS,
1767
+ stale: true,
1768
+ error: error?.message || 'fallback_cache',
1769
+ },
1770
+ });
1771
+ return;
1772
+ }
1773
+ sendJson(req, res, 503, { error: 'Stats do marketplace indisponíveis no momento.' });
1774
+ }
1775
+ };
1776
+
1777
+ const handleCreatePackConfigRequest = async (req, res) => {
1778
+ triggerStaleDraftCleanup();
1779
+ sendJson(req, res, 200, {
1780
+ data: {
1781
+ command_prefix: PACK_COMMAND_PREFIX,
1782
+ limits: {
1783
+ pack_name_max_length: PACK_CREATE_MAX_NAME_LENGTH,
1784
+ publisher_max_length: PACK_CREATE_MAX_PUBLISHER_LENGTH,
1785
+ description_max_length: PACK_CREATE_MAX_DESCRIPTION_LENGTH,
1786
+ stickers_per_pack: PACK_CREATE_MAX_ITEMS,
1787
+ packs_per_owner: serializePackOwnerLimit(PACK_CREATE_MAX_PACKS_PER_OWNER),
1788
+ packs_per_owner_unlimited: !Number.isFinite(PACK_CREATE_MAX_PACKS_PER_OWNER),
1789
+ sticker_upload_max_bytes: MAX_STICKER_UPLOAD_BYTES,
1790
+ sticker_upload_source_max_bytes: MAX_STICKER_SOURCE_UPLOAD_BYTES,
1791
+ },
1792
+ rules: {
1793
+ pack_name_regex: PACK_CREATE_NAME_REGEX,
1794
+ pack_name_hint: 'Nome livre (espaços e emojis são permitidos).',
1795
+ visibility_values: ['public', 'unlisted', 'private'],
1796
+ owner_phone_required: !STICKER_WEB_GOOGLE_AUTH_REQUIRED,
1797
+ owner_phone_hint: STICKER_WEB_GOOGLE_AUTH_REQUIRED ? 'Login Google obrigatório para criar packs nesta página.' : 'Informe o número de celular com DDD para vincular o pack ao criador.',
1798
+ suggested_tags: ['anime', 'meme', 'game', 'texto', 'nsfw', 'dark', 'cartoon', 'foto-real', 'cyberpunk'],
1799
+ },
1800
+ auth: {
1801
+ google: {
1802
+ enabled: Boolean(STICKER_WEB_GOOGLE_CLIENT_ID),
1803
+ required: Boolean(STICKER_WEB_GOOGLE_AUTH_REQUIRED),
1804
+ client_id: STICKER_WEB_GOOGLE_CLIENT_ID || null,
1805
+ session_ttl_ms: STICKER_WEB_GOOGLE_SESSION_TTL_MS,
1806
+ },
1807
+ },
1808
+ examples: {
1809
+ create: `${PACK_COMMAND_PREFIX}pack create meupack | publisher="Seu Nome" | desc="Descrição"`,
1810
+ add_sticker: `${PACK_COMMAND_PREFIX}pack add <pack>`,
1811
+ set_description: `${PACK_COMMAND_PREFIX}pack setdesc <pack> "Nova descrição"`,
1812
+ },
1813
+ links: {
1814
+ stickers: `${STICKER_WEB_PATH}/`,
1815
+ create: `${STICKER_CREATE_WEB_PATH}/`,
1816
+ api_base: STICKER_API_BASE_PATH,
1817
+ create_api: `${STICKER_API_BASE_PATH}/create`,
1818
+ google_auth_session_api: `${STICKER_API_BASE_PATH}/auth/google/session`,
1819
+ upload_api_template: `${STICKER_API_BASE_PATH}/:pack_key/stickers-upload`,
1820
+ finalize_api_template: `${STICKER_API_BASE_PATH}/:pack_key/finalize`,
1821
+ publish_state_api_template: `${STICKER_API_BASE_PATH}/:pack_key/publish-state`,
1822
+ },
1823
+ publish_flow: {
1824
+ statuses: ['draft', 'uploading', 'processing', 'published', 'failed'],
1825
+ upload_queue_concurrency: WEB_UPLOAD_MAX_CONCURRENCY,
1826
+ finalize_required: true,
1827
+ },
1828
+ },
1829
+ });
1830
+ };
1831
+
1832
+ const buildOwnerLookupJids = (value) => {
1833
+ const normalized = normalizeJid(value) || '';
1834
+ if (!normalized || !normalized.includes('@')) return [];
1835
+ const lookup = new Set([normalized]);
1836
+ const phoneDigits = toWhatsAppPhoneDigits(normalized);
1837
+ if (!phoneDigits) return Array.from(lookup);
1838
+ lookup.add(normalizeJid(`${phoneDigits}@s.whatsapp.net`) || '');
1839
+ lookup.add(normalizeJid(`${phoneDigits}@c.us`) || '');
1840
+ lookup.add(normalizeJid(`${phoneDigits}@hosted`) || '');
1841
+ return Array.from(lookup).filter(Boolean);
1842
+ };
1843
+
1844
+ const appendMyProfileOwnerCandidate = (candidateSet, lookupSet, value) => {
1845
+ const normalized = normalizeJid(value) || '';
1846
+ if (!normalized || !normalized.includes('@')) return;
1847
+
1848
+ candidateSet.add(normalized);
1849
+ for (const lookupJid of buildOwnerLookupJids(normalized)) {
1850
+ lookupSet.add(lookupJid);
1851
+ }
1852
+
1853
+ const phoneOwner = toWhatsAppOwnerJid(value);
1854
+ if (phoneOwner) {
1855
+ candidateSet.add(phoneOwner);
1856
+ for (const lookupJid of buildOwnerLookupJids(phoneOwner)) {
1857
+ lookupSet.add(lookupJid);
1858
+ }
1859
+ }
1860
+ };
1861
+
1862
+ const buildPhoneSet = (...values) => {
1863
+ const set = new Set();
1864
+ for (const value of values) {
1865
+ const digits = toWhatsAppPhoneDigits(value);
1866
+ if (digits) set.add(digits);
1867
+ }
1868
+ return set;
1869
+ };
1870
+
1871
+ const resolveMyProfileOwnerCandidates = async (session) => {
1872
+ const candidates = new Set();
1873
+ const lookupByJid = new Set();
1874
+ const lidCandidates = new Set();
1875
+ const appendCandidate = (value) => appendMyProfileOwnerCandidate(candidates, lookupByJid, value);
1876
+ const trustedPhones = new Set();
1877
+ const blockedJids = new Set();
1878
+ const blockedPhones = new Set();
1879
+ const normalizedSub = normalizeGoogleSubject(session?.sub);
1880
+ const normalizedEmail = normalizeEmail(session?.email);
1881
+ const normalizedSessionOwnerJid = normalizeJid(session?.ownerJid || '') || '';
1882
+ const normalizedSessionOwnerPhone = toWhatsAppPhoneDigits(session?.ownerPhone || session?.ownerJid) || '';
1883
+
1884
+ appendCandidate(session?.ownerJid);
1885
+ appendCandidate(toWhatsAppOwnerJid(session?.ownerPhone || session?.ownerJid));
1886
+ for (const phone of buildPhoneSet(session?.ownerPhone, session?.ownerJid)) {
1887
+ trustedPhones.add(phone);
1888
+ }
1889
+
1890
+ const activeSocket = getActiveSocket();
1891
+ const botJid = normalizeJid(resolveActiveSocketBotJid(activeSocket) || '');
1892
+ if (botJid) {
1893
+ blockedJids.add(botJid);
1894
+ for (const phone of buildPhoneSet(botJid)) {
1895
+ blockedPhones.add(phone);
1896
+ }
1897
+ }
1898
+
1899
+ const legacyGoogleOwner = buildGoogleOwnerJid(session?.sub);
1900
+ if (legacyGoogleOwner) appendCandidate(legacyGoogleOwner);
1901
+
1902
+ const sessionResolved = await resolveUserId(extractUserIdInfo(session?.ownerJid || session?.ownerPhone || null)).catch(() => null);
1903
+ if (sessionResolved) {
1904
+ appendCandidate(sessionResolved);
1905
+ for (const phone of buildPhoneSet(sessionResolved)) {
1906
+ trustedPhones.add(phone);
1907
+ }
1908
+ }
1909
+
1910
+ const identityClauses = [];
1911
+ const identityParams = [];
1912
+ if (normalizedSub) {
1913
+ identityClauses.push('google_sub = ?');
1914
+ identityParams.push(normalizedSub);
1915
+ }
1916
+ if (normalizedEmail) {
1917
+ identityClauses.push('email = ?');
1918
+ identityParams.push(normalizedEmail);
1919
+ }
1920
+ if (normalizedSessionOwnerJid) {
1921
+ identityClauses.push('owner_jid = ?');
1922
+ identityParams.push(normalizedSessionOwnerJid);
1923
+ }
1924
+ if (normalizedSessionOwnerPhone) {
1925
+ identityClauses.push('owner_phone = ?');
1926
+ identityParams.push(normalizedSessionOwnerPhone);
1927
+ }
1928
+
1929
+ if (identityClauses.length) {
1930
+ try {
1931
+ const userRows = await executeQuery(
1932
+ `SELECT owner_jid, owner_phone
1933
+ FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
1934
+ WHERE ${identityClauses.join(' OR ')}
1935
+ ORDER BY COALESCE(last_seen_at, last_login_at, updated_at, created_at) DESC
1936
+ LIMIT 10`,
1937
+ identityParams,
1938
+ );
1939
+
1940
+ for (const row of Array.isArray(userRows) ? userRows : []) {
1941
+ appendCandidate(row?.owner_jid || '');
1942
+ appendCandidate(row?.owner_phone || '');
1943
+ for (const phone of buildPhoneSet(row?.owner_jid, row?.owner_phone)) {
1944
+ trustedPhones.add(phone);
1945
+ }
1946
+ const mappedResolved = await resolveUserId(extractUserIdInfo(row?.owner_jid || row?.owner_phone || null)).catch(() => null);
1947
+ if (mappedResolved) appendCandidate(mappedResolved);
1948
+ }
1949
+
1950
+ const sessionRows = await executeQuery(
1951
+ `SELECT owner_jid, owner_phone
1952
+ FROM ${TABLES.STICKER_WEB_GOOGLE_SESSION}
1953
+ WHERE revoked_at IS NULL
1954
+ AND expires_at > UTC_TIMESTAMP()
1955
+ AND (${identityClauses.join(' OR ')})
1956
+ ORDER BY COALESCE(last_seen_at, created_at) DESC
1957
+ LIMIT 20`,
1958
+ identityParams,
1959
+ ).catch(() => []);
1960
+
1961
+ for (const row of Array.isArray(sessionRows) ? sessionRows : []) {
1962
+ appendCandidate(row?.owner_jid || '');
1963
+ appendCandidate(row?.owner_phone || '');
1964
+ for (const phone of buildPhoneSet(row?.owner_jid, row?.owner_phone)) {
1965
+ trustedPhones.add(phone);
1966
+ }
1967
+ }
1968
+ } catch (error) {
1969
+ logger.warn('Falha ao resolver owners para perfil web.', {
1970
+ action: 'sticker_pack_my_profile_owner_candidates_failed',
1971
+ google_sub: normalizedSub,
1972
+ email: normalizedEmail,
1973
+ error: error?.message,
1974
+ });
1975
+ }
1976
+ }
1977
+
1978
+ for (const ownerJid of Array.from(candidates)) {
1979
+ const identity = extractUserIdInfo(ownerJid);
1980
+ if (identity?.lid) lidCandidates.add(identity.lid);
1981
+ if (!identity?.lid && !identity?.jid) continue;
1982
+ const resolved = await resolveUserId(identity).catch(() => null);
1983
+ if (!resolved) continue;
1984
+ appendCandidate(resolved);
1985
+ const resolvedIdentity = extractUserIdInfo(resolved);
1986
+ if (resolvedIdentity?.lid) lidCandidates.add(resolvedIdentity.lid);
1987
+ }
1988
+
1989
+ const lookupValues = Array.from(lookupByJid).filter(Boolean);
1990
+ for (let offset = 0; offset < lookupValues.length; offset += 200) {
1991
+ const chunk = lookupValues.slice(offset, offset + 200);
1992
+ if (!chunk.length) continue;
1993
+ const placeholders = chunk.map(() => '?').join(', ');
1994
+ const lookupParams = [...chunk, ...chunk];
1995
+ const rows = await executeQuery(
1996
+ `SELECT lid, jid
1997
+ FROM ${TABLES.LID_MAP}
1998
+ WHERE jid IN (${placeholders})
1999
+ OR lid IN (${placeholders})
2000
+ ORDER BY last_seen DESC
2001
+ LIMIT 500`,
2002
+ lookupParams,
2003
+ ).catch(() => []);
2004
+
2005
+ for (const row of Array.isArray(rows) ? rows : []) {
2006
+ appendCandidate(row?.jid || '');
2007
+ const resolvedLid = normalizeJid(row?.lid || '');
2008
+ if (resolvedLid) lidCandidates.add(resolvedLid);
2009
+ }
2010
+
2011
+ const packOwnerRows = await executeQuery(
2012
+ `SELECT DISTINCT p.owner_jid
2013
+ FROM ${TABLES.STICKER_PACK} p
2014
+ INNER JOIN ${TABLES.LID_MAP} lm
2015
+ ON lm.lid = p.owner_jid
2016
+ WHERE p.deleted_at IS NULL
2017
+ AND (
2018
+ lm.jid IN (${placeholders})
2019
+ OR lm.lid IN (${placeholders})
2020
+ )
2021
+ LIMIT 500`,
2022
+ lookupParams,
2023
+ ).catch(() => []);
2024
+
2025
+ for (const row of Array.isArray(packOwnerRows) ? packOwnerRows : []) {
2026
+ const packOwnerLid = normalizeJid(row?.owner_jid || '');
2027
+ if (!packOwnerLid) continue;
2028
+ appendCandidate(packOwnerLid);
2029
+ lidCandidates.add(packOwnerLid);
2030
+ }
2031
+ }
2032
+
2033
+ for (const lid of lidCandidates) {
2034
+ const resolved = await resolveUserId(extractUserIdInfo(lid)).catch(() => null);
2035
+ if (resolved) {
2036
+ appendCandidate(resolved);
2037
+ appendCandidate(lid);
2038
+ }
2039
+ }
2040
+
2041
+ const filtered = [];
2042
+ for (const candidate of Array.from(candidates)) {
2043
+ const normalized = normalizeJid(candidate) || '';
2044
+ if (!normalized || !normalized.includes('@')) continue;
2045
+ if (blockedJids.has(normalized)) continue;
2046
+
2047
+ const directPhone = toWhatsAppPhoneDigits(normalized);
2048
+ if (directPhone && blockedPhones.has(directPhone)) continue;
2049
+
2050
+ const isGoogleOwner = normalized.endsWith('@google.oauth');
2051
+ if (trustedPhones.size === 0) {
2052
+ filtered.push(normalized);
2053
+ continue;
2054
+ }
2055
+
2056
+ if (directPhone) {
2057
+ if (!trustedPhones.has(directPhone)) continue;
2058
+ filtered.push(normalized);
2059
+ continue;
2060
+ }
2061
+
2062
+ const resolved = await resolveUserId(extractUserIdInfo(normalized)).catch(() => null);
2063
+ const resolvedPhone = toWhatsAppPhoneDigits(resolved || '');
2064
+ if (resolvedPhone) {
2065
+ if (!trustedPhones.has(resolvedPhone) || blockedPhones.has(resolvedPhone)) continue;
2066
+ filtered.push(normalized);
2067
+ continue;
2068
+ }
2069
+
2070
+ if (isGoogleOwner) {
2071
+ filtered.push(normalized);
2072
+ }
2073
+ }
2074
+
2075
+ return Array.from(new Set(filtered));
2076
+ };
2077
+
2078
+ const authContext = createStickerCatalogAuthContext({
2079
+ executeQuery,
2080
+ runSqlTransaction,
2081
+ tables: TABLES,
2082
+ logger,
2083
+ sendJson,
2084
+ readJsonBody,
2085
+ parseCookies,
2086
+ getCookieValuesFromRequest,
2087
+ appendSetCookie,
2088
+ buildCookieString,
2089
+ normalizeEmail,
2090
+ normalizeJid,
2091
+ sanitizeText,
2092
+ toIsoOrNull,
2093
+ toWhatsAppPhoneDigits,
2094
+ resolveWhatsAppOwnerJidFromLoginPayload,
2095
+ assertGoogleIdentityNotBanned,
2096
+ queueAutomatedEmail,
2097
+ queueWelcomeEmail,
2098
+ resolveRequestRemoteIp,
2099
+ toSiteAbsoluteUrl,
2100
+ listStickerPacksByOwner,
2101
+ listStickerPackEngagementByPackIds,
2102
+ mapPackSummary,
2103
+ isPackPubliclyVisible,
2104
+ resolveMyProfileOwnerCandidates,
2105
+ shouldHidePackFromMyProfileDefault,
2106
+ parseEnvBool,
2107
+ clampInt,
2108
+ userPasswordResetWebPath: USER_PASSWORD_RESET_WEB_PATH,
2109
+ userProfileWebPath: USER_PROFILE_WEB_PATH,
2110
+ passwordRecoverySessionAuthMethod: PASSWORD_RECOVERY_SESSION_AUTH_METHOD,
2111
+ passwordRecoverySessionTtlSeconds: PASSWORD_RECOVERY_SESSION_TTL_SECONDS,
2112
+ webSessionCookieName: WEB_SESSION_COOKIE_NAME,
2113
+ notAllowedErrorCode: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
2114
+ stickerWebGoogleClientId: STICKER_WEB_GOOGLE_CLIENT_ID,
2115
+ stickerWebGoogleAuthRequired: STICKER_WEB_GOOGLE_AUTH_REQUIRED,
2116
+ stickerWebGoogleSessionTtlMs: STICKER_WEB_GOOGLE_SESSION_TTL_MS,
2117
+ stickerApiBasePath: STICKER_API_BASE_PATH,
2118
+ stickerWebPath: STICKER_WEB_PATH,
2119
+ stickerLoginWebPath: STICKER_LOGIN_WEB_PATH,
2120
+ siteOrigin: SITE_ORIGIN,
2121
+ });
2122
+
2123
+ const { upsertGoogleWebUserRecord, resolveGoogleWebSessionFromRequest, mapGoogleSessionResponseData, handleGoogleAuthSessionRequest, revokeGoogleWebSessionsByIdentity, buildGoogleOwnerJid, handleTermsAcceptanceRequest, handlePasswordAuthRequest, handlePasswordRecoveryRequest, handlePasswordRecoveryVerifyRequest, handlePasswordRecoverySessionCreateRequest, handlePasswordRecoverySessionStatusRequest, handlePasswordRecoverySessionRequest, handlePasswordRecoverySessionVerifyRequest, handlePasswordLoginRequest, handleMyProfileRequest } = authContext;
2124
+
2125
+ // Configuração de pontes para o systemController resolver dependências circulares
2126
+ globalThis.getMarketplaceStatsCachedBridge = (visibility) => getMarketplaceStatsCached(visibility);
2127
+ globalThis.resolveGoogleWebSessionFromRequestBridge = (req) => resolveGoogleWebSessionFromRequest(req);
2128
+ globalThis.mapGoogleSessionResponseDataBridge = (sess, opts) => mapGoogleSessionResponseData(sess, opts);
2129
+ globalThis.getActiveSocketBridge = () => getActiveSocket();
2130
+ globalThis.profilePictureUrlFromActiveSocketBridge = (jid, type, timeout) => profilePictureUrlFromActiveSocket(jid, type, timeout);
2131
+
2132
+ revokeGoogleWebSessionsByIdentityBridge = revokeGoogleWebSessionsByIdentity;
2133
+
2134
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
2135
+
2136
+ const invalidateStickerCatalogDerivedCaches = () => {
2137
+ GLOBAL_RANK_CACHE.expiresAt = 0;
2138
+ GLOBAL_RANK_CACHE.value = null;
2139
+ GLOBAL_RANK_CACHE.pending = null;
2140
+ HOME_MARKETPLACE_STATS_CACHE.clear();
2141
+ CATALOG_LIST_CACHE.clear();
2142
+ CATALOG_CREATOR_RANKING_CACHE.clear();
2143
+ CATALOG_PACK_PAYLOAD_CACHE.clear();
2144
+ SYSTEM_SUMMARY_CACHE.expiresAt = 0;
2145
+ SYSTEM_SUMMARY_CACHE.value = null;
2146
+ SYSTEM_SUMMARY_CACHE.pending = null;
2147
+ README_SUMMARY_CACHE.expiresAt = 0;
2148
+ README_SUMMARY_CACHE.value = null;
2149
+ README_SUMMARY_CACHE.pending = null;
2150
+ };
2151
+
2152
+ const sendManagedMutationStatus = (req, res, status, extra = {}, statusCode = 200) => {
2153
+ sendJson(req, res, statusCode, {
2154
+ data: {
2155
+ success: true,
2156
+ status,
2157
+ ...extra,
2158
+ },
2159
+ });
2160
+ };
2161
+
2162
+ const sendManagedPackMutationStatus = async (req, res, status, pack, extra = {}, statusCode = 200) => {
2163
+ if (!pack) {
2164
+ sendManagedMutationStatus(req, res, status, extra, statusCode);
2165
+ return;
2166
+ }
2167
+ const managed = await buildManagedPackResponseData(pack);
2168
+ sendJson(req, res, statusCode, {
2169
+ data: {
2170
+ success: true,
2171
+ status,
2172
+ ...extra,
2173
+ ...managed,
2174
+ },
2175
+ });
2176
+ };
2177
+
2178
+ const cleanupOrphanStickerAssets = async (assetIds, { reason = 'manage_mutation' } = {}) => {
2179
+ const normalizedIds = Array.from(new Set((Array.isArray(assetIds) ? assetIds : []).map((id) => String(id || '').trim()).filter(Boolean)));
2180
+ if (!normalizedIds.length) return { checked: 0, deleted: 0, skipped: 0, errors: 0 };
2181
+
2182
+ const assets = await findStickerAssetsByIds(normalizedIds).catch(() => []);
2183
+ const byId = new Map((Array.isArray(assets) ? assets : []).map((asset) => [asset.id, asset]));
2184
+ const summary = { checked: 0, deleted: 0, skipped: 0, errors: 0 };
2185
+
2186
+ for (const assetId of normalizedIds) {
2187
+ summary.checked += 1;
2188
+ try {
2189
+ const result = await runSqlTransaction(async (connection) => {
2190
+ const refs = await countStickerPackItemRefsByStickerId(assetId, connection);
2191
+ if (refs > 0) return { deleted: false, refs, alreadyGone: false };
2192
+ await deleteStickerAssetClassificationByAssetId(assetId, connection);
2193
+ const deletedRows = await deleteStickerAssetById(assetId, connection);
2194
+ return { deleted: deletedRows > 0, refs, alreadyGone: deletedRows === 0 };
2195
+ });
2196
+
2197
+ if (!result.deleted) {
2198
+ summary.skipped += 1;
2199
+ continue;
2200
+ }
2201
+
2202
+ summary.deleted += 1;
2203
+ const asset = byId.get(assetId);
2204
+ if (asset?.storage_path) {
2205
+ await fs.unlink(asset.storage_path).catch((error) => {
2206
+ if (error?.code === 'ENOENT') return;
2207
+ logger.warn('Falha ao remover arquivo físico de sticker órfão.', {
2208
+ action: 'sticker_orphan_asset_file_delete_failed',
2209
+ asset_id: assetId,
2210
+ storage_path: asset.storage_path,
2211
+ reason,
2212
+ error: error?.message,
2213
+ });
2214
+ });
2215
+ }
2216
+ } catch (error) {
2217
+ summary.errors += 1;
2218
+ logger.warn('Falha ao limpar asset órfão após mutação de pack.', {
2219
+ action: 'sticker_orphan_asset_cleanup_failed',
2220
+ asset_id: assetId,
2221
+ reason,
2222
+ error: error?.message,
2223
+ });
2224
+ }
2225
+ }
2226
+
2227
+ return summary;
2228
+ };
2229
+
2230
+ const deleteManagedPackWithCleanup = async ({ ownerJid, identifier, fallbackPack = null }) => {
2231
+ const transactionResult = await runSqlTransaction(async (connection) => {
2232
+ const pack =
2233
+ (await findStickerPackByOwnerAndIdentifier(ownerJid, fallbackPack?.id || identifier, {
2234
+ connection,
2235
+ })) || (fallbackPack?.pack_key && fallbackPack?.pack_key !== identifier ? await findStickerPackByOwnerAndIdentifier(ownerJid, identifier, { connection }) : null);
2236
+
2237
+ if (!pack) {
2238
+ return {
2239
+ missing: true,
2240
+ deletedPack: null,
2241
+ removedStickerIds: [],
2242
+ removedCount: 0,
2243
+ };
2244
+ }
2245
+
2246
+ const items = await listStickerPackItems(pack.id, connection);
2247
+ const removedStickerIds = items.map((item) => item?.sticker_id).filter(Boolean);
2248
+ await removeStickerPackItemsByPackId(pack.id, connection);
2249
+ const deletedPack = await softDeleteStickerPack(pack.id, connection);
2250
+
2251
+ return {
2252
+ missing: false,
2253
+ deletedPack,
2254
+ removedStickerIds,
2255
+ removedCount: items.length,
2256
+ };
2257
+ });
2258
+
2259
+ if (!transactionResult.missing && transactionResult.removedStickerIds.length) {
2260
+ await cleanupOrphanStickerAssets(transactionResult.removedStickerIds, {
2261
+ reason: 'delete_pack',
2262
+ });
2263
+ }
2264
+
2265
+ invalidateStickerCatalogDerivedCaches();
2266
+ return transactionResult;
2267
+ };
2268
+
2269
+ const mapStickerPackWebManageError = (error) => {
2270
+ if (!(error instanceof StickerPackError)) {
2271
+ return {
2272
+ statusCode: 500,
2273
+ code: STICKER_PACK_ERROR_CODES.INTERNAL_ERROR,
2274
+ message: error?.message || 'Falha interna ao gerenciar pack.',
2275
+ };
2276
+ }
2277
+
2278
+ if (error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
2279
+ return { statusCode: 404, code: error.code, message: error.message || 'Pack nao encontrado.' };
2280
+ }
2281
+ if (error.code === STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND) {
2282
+ return {
2283
+ statusCode: 404,
2284
+ code: error.code,
2285
+ message: error.message || 'Sticker nao encontrado.',
2286
+ };
2287
+ }
2288
+ if (error.code === STICKER_PACK_ERROR_CODES.NOT_ALLOWED) {
2289
+ return {
2290
+ statusCode: 403,
2291
+ code: error.code,
2292
+ message: error.message || 'Operacao nao permitida.',
2293
+ };
2294
+ }
2295
+ if (error.code === STICKER_PACK_ERROR_CODES.PACK_LIMIT_REACHED) {
2296
+ return {
2297
+ statusCode: 429,
2298
+ code: error.code,
2299
+ message: error.message || 'Limite de packs atingido.',
2300
+ };
2301
+ }
2302
+ if (error.code === STICKER_PACK_ERROR_CODES.INVALID_INPUT) {
2303
+ return { statusCode: 400, code: error.code, message: error.message || 'Dados invalidos.' };
2304
+ }
2305
+ return {
2306
+ statusCode: 400,
2307
+ code: error.code,
2308
+ message: error.message || 'Falha ao gerenciar pack.',
2309
+ };
2310
+ };
2311
+
2312
+ const requireGoogleWebSessionForManagement = async (req, res) => {
2313
+ const session = await resolveGoogleWebSessionFromRequest(req);
2314
+ if (!session?.ownerJid) {
2315
+ sendJson(req, res, 401, {
2316
+ error: 'Login Google obrigatorio para gerenciar packs.',
2317
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
2318
+ });
2319
+ return null;
2320
+ }
2321
+ return session;
2322
+ };
2323
+
2324
+ const loadOwnedPackForWebManagement = async (req, res, packKey, { allowMissing = false } = {}) => {
2325
+ const session = await requireGoogleWebSessionForManagement(req, res);
2326
+ if (!session) return null;
2327
+
2328
+ const normalizedPackKey = sanitizeText(packKey, 160, { allowEmpty: false });
2329
+ if (!normalizedPackKey) {
2330
+ sendJson(req, res, 400, {
2331
+ error: 'pack_key invalido.',
2332
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
2333
+ });
2334
+ return null;
2335
+ }
2336
+
2337
+ const ownerCandidatesRaw = await resolveMyProfileOwnerCandidates(session).catch(() => []);
2338
+ const ownerCandidates = Array.from(new Set([normalizeJid(session.ownerJid) || '', ...ownerCandidatesRaw].filter(Boolean)));
2339
+ const fallbackOwnerJid = ownerCandidates[0] || normalizeJid(session.ownerJid) || '';
2340
+
2341
+ for (const ownerJid of ownerCandidates) {
2342
+ try {
2343
+ const pack = await stickerPackService.getPackInfo({
2344
+ ownerJid,
2345
+ identifier: normalizedPackKey,
2346
+ });
2347
+ return { session, ownerJid, ownerCandidates, packKey: normalizedPackKey, pack };
2348
+ } catch (error) {
2349
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
2350
+ continue;
2351
+ }
2352
+ const mapped = mapStickerPackWebManageError(error);
2353
+ sendJson(req, res, mapped.statusCode, {
2354
+ error: mapped.message,
2355
+ code: mapped.code,
2356
+ });
2357
+ return null;
2358
+ }
2359
+ }
2360
+
2361
+ if (allowMissing) {
2362
+ return {
2363
+ session,
2364
+ ownerJid: fallbackOwnerJid,
2365
+ ownerCandidates,
2366
+ packKey: normalizedPackKey,
2367
+ pack: null,
2368
+ missing: true,
2369
+ };
2370
+ }
2371
+
2372
+ sendJson(req, res, 404, {
2373
+ error: 'Pack nao encontrado para este usuario.',
2374
+ code: STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND,
2375
+ });
2376
+ return null;
2377
+ };
2378
+
2379
+ const buildManagedPackAnalytics = async (pack) => {
2380
+ const engagement = await getStickerPackEngagementByPackId(pack.id);
2381
+ const interactionStatsByPack = await listStickerPackInteractionStatsByPackIds([pack.id]);
2382
+ const interaction = interactionStatsByPack.get(pack.id) || {
2383
+ open_horizon: 0,
2384
+ open_baseline: 0,
2385
+ like_horizon: 0,
2386
+ like_baseline: 0,
2387
+ dislike_horizon: 0,
2388
+ dislike_baseline: 0,
2389
+ };
2390
+ return {
2391
+ downloads: Number(engagement?.open_count || 0),
2392
+ likes: Number(engagement?.like_count || 0),
2393
+ dislikes: Number(engagement?.dislike_count || 0),
2394
+ score: Number(engagement?.score || 0) || Number(engagement?.like_count || 0) - Number(engagement?.dislike_count || 0),
2395
+ engagement: {
2396
+ open_count: Number(engagement?.open_count || 0),
2397
+ like_count: Number(engagement?.like_count || 0),
2398
+ dislike_count: Number(engagement?.dislike_count || 0),
2399
+ updated_at: toIsoOrNull(engagement?.updated_at || null),
2400
+ },
2401
+ interaction_window: {
2402
+ open_horizon: Number(interaction.open_horizon || 0),
2403
+ open_baseline: Number(interaction.open_baseline || 0),
2404
+ like_horizon: Number(interaction.like_horizon || 0),
2405
+ like_baseline: Number(interaction.like_baseline || 0),
2406
+ dislike_horizon: Number(interaction.dislike_horizon || 0),
2407
+ dislike_baseline: Number(interaction.dislike_baseline || 0),
2408
+ },
2409
+ };
2410
+ };
2411
+
2412
+ const buildManagedPackResponseData = async (pack) => {
2413
+ const items = Array.isArray(pack?.items) ? pack.items : [];
2414
+ const stickerIds = items.map((item) => item.sticker_id).filter(Boolean);
2415
+
2416
+ const [classifications, packClassification, analytics, publishState] = await Promise.all([stickerIds.length ? listStickerClassificationsByAssetIds(stickerIds) : Promise.resolve([]), pack?.classification || (stickerIds.length ? getPackClassificationSummaryByAssetIds(stickerIds).catch(() => null) : null), buildManagedPackAnalytics(pack), buildPackPublishStateData(pack, { includeUploads: true })]);
2417
+
2418
+ const byAssetClassification = new Map((Array.isArray(classifications) ? classifications : []).map((entry) => [entry.asset_id, entry]));
2419
+
2420
+ return {
2421
+ pack: mapPackDetails(pack, items, {
2422
+ byAssetClassification,
2423
+ packClassification: packClassification || null,
2424
+ engagement: analytics.engagement,
2425
+ signals: null,
2426
+ }),
2427
+ publish_state: publishState,
2428
+ analytics,
2429
+ };
2430
+ };
2431
+
2432
+ const sendManagedPackResponse = async (req, res, pack) => {
2433
+ const data = await buildManagedPackResponseData(pack);
2434
+ sendJson(req, res, 200, { data });
2435
+ };
2436
+
2437
+ const parseTagListInput = (value) => {
2438
+ if (Array.isArray(value)) return value;
2439
+ if (typeof value === 'string') return value.split(',');
2440
+ return [];
2441
+ };
2442
+
2443
+ const handleManagedPackRequest = async (req, res, packKey) => {
2444
+ if (!['GET', 'HEAD', 'PATCH', 'DELETE'].includes(req.method || '')) {
2445
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
2446
+ return;
2447
+ }
2448
+
2449
+ const isMutableMethod = req.method === 'PATCH' || req.method === 'DELETE';
2450
+ const context = await loadOwnedPackForWebManagement(req, res, packKey, {
2451
+ allowMissing: isMutableMethod,
2452
+ });
2453
+ if (!context) return;
2454
+ const { packKey: normalizedPackKey } = context;
2455
+
2456
+ if (req.method === 'GET' || req.method === 'HEAD') {
2457
+ await sendManagedPackResponse(req, res, context.pack);
2458
+ return;
2459
+ }
2460
+
2461
+ if (req.method === 'DELETE') {
2462
+ if (context.missing || !context.pack) {
2463
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2464
+ deleted: false,
2465
+ pack_key: normalizedPackKey,
2466
+ });
2467
+ return;
2468
+ }
2469
+
2470
+ try {
2471
+ const result = await deleteManagedPackWithCleanup({
2472
+ ownerJid: context.ownerJid,
2473
+ identifier: normalizedPackKey,
2474
+ fallbackPack: context.pack,
2475
+ });
2476
+ if (result?.missing) {
2477
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2478
+ deleted: false,
2479
+ pack_key: normalizedPackKey,
2480
+ });
2481
+ return;
2482
+ }
2483
+
2484
+ sendManagedMutationStatus(req, res, 'deleted', {
2485
+ deleted: true,
2486
+ pack_key: result?.deletedPack?.pack_key || normalizedPackKey,
2487
+ id: result?.deletedPack?.id || context.pack?.id || null,
2488
+ deleted_at: toIsoOrNull(result?.deletedPack?.deleted_at || new Date()),
2489
+ removed_sticker_count: Number(result?.removedCount || 0),
2490
+ });
2491
+ } catch (error) {
2492
+ const mapped = mapStickerPackWebManageError(error);
2493
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
2494
+ }
2495
+ return;
2496
+ }
2497
+
2498
+ let payload = {};
2499
+ try {
2500
+ payload = await readJsonBody(req);
2501
+ } catch (error) {
2502
+ sendJson(req, res, Number(error?.statusCode || 400), {
2503
+ error: error?.message || 'Body inválido.',
2504
+ });
2505
+ return;
2506
+ }
2507
+
2508
+ try {
2509
+ if (context.missing || !context.pack) {
2510
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2511
+ updated: false,
2512
+ pack_key: normalizedPackKey,
2513
+ });
2514
+ return;
2515
+ }
2516
+
2517
+ let updatedPack = context.pack;
2518
+ let changed = false;
2519
+
2520
+ if (hasOwn(payload, 'name')) {
2521
+ const nextName = sanitizeText(payload?.name, PACK_CREATE_MAX_NAME_LENGTH, {
2522
+ allowEmpty: false,
2523
+ });
2524
+ const currentName = sanitizeText(updatedPack?.name, PACK_CREATE_MAX_NAME_LENGTH, {
2525
+ allowEmpty: false,
2526
+ });
2527
+ if (!nextName) {
2528
+ updatedPack = await stickerPackService.renamePack({
2529
+ ownerJid: context.ownerJid,
2530
+ identifier: normalizedPackKey,
2531
+ name: payload.name,
2532
+ });
2533
+ } else if (nextName !== currentName) {
2534
+ updatedPack = await stickerPackService.renamePack({
2535
+ ownerJid: context.ownerJid,
2536
+ identifier: normalizedPackKey,
2537
+ name: payload.name,
2538
+ });
2539
+ changed = true;
2540
+ }
2541
+ }
2542
+
2543
+ if (hasOwn(payload, 'publisher')) {
2544
+ const nextPublisher = sanitizeText(payload?.publisher, PACK_CREATE_MAX_PUBLISHER_LENGTH, {
2545
+ allowEmpty: false,
2546
+ });
2547
+ const currentPublisher = sanitizeText(updatedPack?.publisher, PACK_CREATE_MAX_PUBLISHER_LENGTH, { allowEmpty: false });
2548
+ if (!nextPublisher) {
2549
+ updatedPack = await stickerPackService.setPackPublisher({
2550
+ ownerJid: context.ownerJid,
2551
+ identifier: normalizedPackKey,
2552
+ publisher: payload.publisher,
2553
+ });
2554
+ } else if (nextPublisher !== currentPublisher) {
2555
+ updatedPack = await stickerPackService.setPackPublisher({
2556
+ ownerJid: context.ownerJid,
2557
+ identifier: normalizedPackKey,
2558
+ publisher: payload.publisher,
2559
+ });
2560
+ changed = true;
2561
+ }
2562
+ }
2563
+
2564
+ if (hasOwn(payload, 'visibility')) {
2565
+ const nextVisibility = String(payload?.visibility || '')
2566
+ .trim()
2567
+ .toLowerCase();
2568
+ const currentVisibility = String(updatedPack?.visibility || '')
2569
+ .trim()
2570
+ .toLowerCase();
2571
+ if (!nextVisibility) {
2572
+ updatedPack = await stickerPackService.setPackVisibility({
2573
+ ownerJid: context.ownerJid,
2574
+ identifier: normalizedPackKey,
2575
+ visibility: payload.visibility,
2576
+ });
2577
+ } else if (nextVisibility !== currentVisibility) {
2578
+ updatedPack = await stickerPackService.setPackVisibility({
2579
+ ownerJid: context.ownerJid,
2580
+ identifier: normalizedPackKey,
2581
+ visibility: payload.visibility,
2582
+ });
2583
+ changed = true;
2584
+ }
2585
+ }
2586
+
2587
+ if (hasOwn(payload, 'description') || hasOwn(payload, 'tags')) {
2588
+ const currentMeta = parsePackDescriptionMetadata(updatedPack?.description);
2589
+ const nextDescription = hasOwn(payload, 'description') ? String(payload?.description || '') : String(currentMeta.cleanDescription || '');
2590
+ const nextTags = hasOwn(payload, 'tags') ? parseTagListInput(payload?.tags) : currentMeta.tags;
2591
+ const descriptionWithTags = buildPackDescriptionWithTags(nextDescription, nextTags);
2592
+ const currentDescriptionWithTags = buildPackDescriptionWithTags(currentMeta.cleanDescription || '', currentMeta.tags);
2593
+ if (String(descriptionWithTags || '') !== String(currentDescriptionWithTags || '')) {
2594
+ updatedPack = await stickerPackService.setPackDescription({
2595
+ ownerJid: context.ownerJid,
2596
+ identifier: normalizedPackKey,
2597
+ description: descriptionWithTags || '',
2598
+ });
2599
+ changed = true;
2600
+ }
2601
+ }
2602
+
2603
+ if (changed) invalidateStickerCatalogDerivedCaches();
2604
+ await sendManagedPackMutationStatus(req, res, changed ? 'updated' : 'unchanged', updatedPack, {
2605
+ updated: changed,
2606
+ pack_key: normalizedPackKey,
2607
+ });
2608
+ } catch (error) {
2609
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
2610
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2611
+ updated: false,
2612
+ pack_key: normalizedPackKey,
2613
+ });
2614
+ return;
2615
+ }
2616
+ const mapped = mapStickerPackWebManageError(error);
2617
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
2618
+ }
2619
+ };
2620
+
2621
+ const handleManagedPackCloneRequest = async (req, res, packKey) => {
2622
+ if (req.method !== 'POST') {
2623
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
2624
+ return;
2625
+ }
2626
+ const context = await loadOwnedPackForWebManagement(req, res, packKey);
2627
+ if (!context) return;
2628
+
2629
+ let payload = {};
2630
+ try {
2631
+ payload = await readJsonBody(req);
2632
+ } catch (error) {
2633
+ sendJson(req, res, Number(error?.statusCode || 400), {
2634
+ error: error?.message || 'Body inválido.',
2635
+ });
2636
+ return;
2637
+ }
2638
+
2639
+ const baseName =
2640
+ sanitizeText(context.pack?.name || 'Pack', PACK_CREATE_MAX_NAME_LENGTH, {
2641
+ allowEmpty: false,
2642
+ }) || 'Pack';
2643
+ const requestedName = sanitizeText(payload?.new_name || '', PACK_CREATE_MAX_NAME_LENGTH, {
2644
+ allowEmpty: true,
2645
+ });
2646
+ const newName = requestedName || `${baseName} (copia)`;
2647
+
2648
+ try {
2649
+ const cloned = await stickerPackService.clonePack({
2650
+ ownerJid: context.ownerJid,
2651
+ identifier: context.packKey,
2652
+ newName,
2653
+ });
2654
+ invalidateStickerCatalogDerivedCaches();
2655
+ sendJson(req, res, 201, {
2656
+ data: {
2657
+ pack: mapPackSummary(cloned),
2658
+ },
2659
+ });
2660
+ } catch (error) {
2661
+ const mapped = mapStickerPackWebManageError(error);
2662
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
2663
+ }
2664
+ };
2665
+
2666
+ const handleManagedPackCoverRequest = async (req, res, packKey) => {
2667
+ if (req.method !== 'POST') {
2668
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
2669
+ return;
2670
+ }
2671
+ const context = await loadOwnedPackForWebManagement(req, res, packKey, { allowMissing: true });
2672
+ if (!context) return;
2673
+ if (context.missing || !context.pack) {
2674
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2675
+ pack_key: context.packKey || String(packKey || ''),
2676
+ });
2677
+ return;
2678
+ }
2679
+
2680
+ let payload = {};
2681
+ try {
2682
+ payload = await readJsonBody(req);
2683
+ } catch (error) {
2684
+ sendJson(req, res, Number(error?.statusCode || 400), {
2685
+ error: error?.message || 'Body inválido.',
2686
+ });
2687
+ return;
2688
+ }
2689
+
2690
+ try {
2691
+ const updated = await stickerPackService.setPackCover({
2692
+ ownerJid: context.ownerJid,
2693
+ identifier: context.packKey,
2694
+ stickerId: payload?.sticker_id,
2695
+ });
2696
+ invalidateStickerCatalogDerivedCaches();
2697
+ await sendManagedPackMutationStatus(req, res, 'updated', updated, {
2698
+ pack_key: context.packKey,
2699
+ });
2700
+ } catch (error) {
2701
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
2702
+ sendManagedMutationStatus(req, res, 'already_deleted', { pack_key: context.packKey });
2703
+ return;
2704
+ }
2705
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND) {
2706
+ const fresh = await stickerPackService.getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey }).catch(() => context.pack);
2707
+ await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
2708
+ pack_key: context.packKey,
2709
+ sticker_id: sanitizeText(payload?.sticker_id, 36, { allowEmpty: true }) || null,
2710
+ });
2711
+ return;
2712
+ }
2713
+ const mapped = mapStickerPackWebManageError(error);
2714
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
2715
+ }
2716
+ };
2717
+
2718
+ const handleManagedPackReorderRequest = async (req, res, packKey) => {
2719
+ if (req.method !== 'POST') {
2720
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
2721
+ return;
2722
+ }
2723
+ const context = await loadOwnedPackForWebManagement(req, res, packKey, { allowMissing: true });
2724
+ if (!context) return;
2725
+ if (context.missing || !context.pack) {
2726
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2727
+ pack_key: context.packKey || String(packKey || ''),
2728
+ });
2729
+ return;
2730
+ }
2731
+
2732
+ let payload = {};
2733
+ try {
2734
+ payload = await readJsonBody(req);
2735
+ } catch (error) {
2736
+ sendJson(req, res, Number(error?.statusCode || 400), {
2737
+ error: error?.message || 'Body inválido.',
2738
+ });
2739
+ return;
2740
+ }
2741
+
2742
+ const requestedOrderIds = Array.isArray(payload?.order_sticker_ids) ? payload.order_sticker_ids : [];
2743
+ const currentItems = Array.isArray(context.pack?.items) ? context.pack.items : [];
2744
+ if (currentItems.length < 2) {
2745
+ await sendManagedPackMutationStatus(req, res, 'noop', context.pack, {
2746
+ pack_key: context.packKey,
2747
+ reason: 'pack_has_less_than_two_stickers',
2748
+ });
2749
+ return;
2750
+ }
2751
+ if (!requestedOrderIds.length) {
2752
+ await sendManagedPackMutationStatus(req, res, 'noop', context.pack, {
2753
+ pack_key: context.packKey,
2754
+ reason: 'empty_order_payload',
2755
+ });
2756
+ return;
2757
+ }
2758
+
2759
+ try {
2760
+ const updated = await stickerPackService.reorderPackItems({
2761
+ ownerJid: context.ownerJid,
2762
+ identifier: context.packKey,
2763
+ orderStickerIds: requestedOrderIds,
2764
+ });
2765
+ invalidateStickerCatalogDerivedCaches();
2766
+ await sendManagedPackMutationStatus(req, res, 'updated', updated, {
2767
+ pack_key: context.packKey,
2768
+ });
2769
+ } catch (error) {
2770
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
2771
+ sendManagedMutationStatus(req, res, 'already_deleted', { pack_key: context.packKey });
2772
+ return;
2773
+ }
2774
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.INVALID_INPUT) {
2775
+ const fresh = await stickerPackService.getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey }).catch(() => context.pack);
2776
+ await sendManagedPackMutationStatus(req, res, 'noop', fresh, {
2777
+ pack_key: context.packKey,
2778
+ reason: 'invalid_or_stale_order',
2779
+ });
2780
+ return;
2781
+ }
2782
+ const mapped = mapStickerPackWebManageError(error);
2783
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
2784
+ }
2785
+ };
2786
+
2787
+ const handleManagedPackStickerDeleteRequest = async (req, res, packKey, stickerId) => {
2788
+ if (req.method !== 'DELETE') {
2789
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
2790
+ return;
2791
+ }
2792
+ const context = await loadOwnedPackForWebManagement(req, res, packKey, { allowMissing: true });
2793
+ if (!context) return;
2794
+ if (context.missing || !context.pack) {
2795
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2796
+ pack_key: context.packKey || String(packKey || ''),
2797
+ sticker_id: sanitizeText(stickerId, 36, { allowEmpty: true }) || null,
2798
+ });
2799
+ return;
2800
+ }
2801
+
2802
+ try {
2803
+ const result = await stickerPackService.removeStickerFromPack({
2804
+ ownerJid: context.ownerJid,
2805
+ identifier: context.packKey,
2806
+ selector: stickerId,
2807
+ });
2808
+ invalidateStickerCatalogDerivedCaches();
2809
+ const removedStickerId = result?.removed?.sticker_id || sanitizeText(stickerId, 36, { allowEmpty: true }) || null;
2810
+ if (removedStickerId) {
2811
+ await cleanupOrphanStickerAssets([removedStickerId], { reason: 'remove_sticker' });
2812
+ }
2813
+ await sendManagedPackMutationStatus(req, res, 'updated', result?.pack || context.pack, {
2814
+ pack_key: context.packKey,
2815
+ removed_sticker_id: removedStickerId,
2816
+ });
2817
+ } catch (error) {
2818
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
2819
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2820
+ pack_key: context.packKey,
2821
+ sticker_id: sanitizeText(stickerId, 36, { allowEmpty: true }) || null,
2822
+ });
2823
+ return;
2824
+ }
2825
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND) {
2826
+ const fresh = await stickerPackService.getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey }).catch(() => context.pack);
2827
+ await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
2828
+ pack_key: context.packKey,
2829
+ sticker_id: sanitizeText(stickerId, 36, { allowEmpty: true }) || null,
2830
+ });
2831
+ return;
2832
+ }
2833
+ const mapped = mapStickerPackWebManageError(error);
2834
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
2835
+ }
2836
+ };
2837
+
2838
+ const handleManagedPackStickerCreateRequest = async (req, res, packKey) => {
2839
+ if (req.method !== 'POST') {
2840
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
2841
+ return;
2842
+ }
2843
+ const context = await loadOwnedPackForWebManagement(req, res, packKey, { allowMissing: true });
2844
+ if (!context) return;
2845
+ if (context.missing || !context.pack) {
2846
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2847
+ pack_key: context.packKey || String(packKey || ''),
2848
+ });
2849
+ return;
2850
+ }
2851
+
2852
+ let payload = {};
2853
+ try {
2854
+ payload = await readJsonBody(req, {
2855
+ maxBytes: Math.max(256 * 1024, Math.round(MAX_STICKER_SOURCE_UPLOAD_BYTES * 1.6)),
2856
+ });
2857
+ } catch (error) {
2858
+ sendJson(req, res, Number(error?.statusCode || 400), {
2859
+ error: error?.message || 'Body inválido.',
2860
+ });
2861
+ return;
2862
+ }
2863
+
2864
+ const decoded = decodeStickerBase64Payload(payload?.sticker_base64 || payload?.sticker_data_url || '');
2865
+ if (!decoded?.buffer) {
2866
+ sendJson(req, res, 400, {
2867
+ error: 'Envie sticker_base64 ou sticker_data_url.',
2868
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
2869
+ });
2870
+ return;
2871
+ }
2872
+ if (decoded.buffer.length > MAX_STICKER_SOURCE_UPLOAD_BYTES) {
2873
+ sendJson(req, res, 400, {
2874
+ error: `Arquivo excede limite de ${Math.round(MAX_STICKER_SOURCE_UPLOAD_BYTES / (1024 * 1024))}MB.`,
2875
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
2876
+ });
2877
+ return;
2878
+ }
2879
+
2880
+ let uploadedAssetId = '';
2881
+ try {
2882
+ const normalizedUpload = await convertUploadMediaToWebp({
2883
+ ownerJid: context.ownerJid,
2884
+ buffer: decoded.buffer,
2885
+ mimetype: decoded.mimetype || 'image/webp',
2886
+ });
2887
+ const asset = await saveStickerAssetFromBuffer({
2888
+ ownerJid: context.ownerJid,
2889
+ buffer: normalizedUpload.buffer,
2890
+ mimetype: normalizedUpload.mimetype || 'image/webp',
2891
+ });
2892
+ uploadedAssetId = String(asset?.id || '').trim();
2893
+
2894
+ let updatedPack = await stickerPackService.addStickerToPack({
2895
+ ownerJid: context.ownerJid,
2896
+ identifier: context.packKey,
2897
+ asset: { id: uploadedAssetId },
2898
+ emojis: [],
2899
+ accessibilityLabel: null,
2900
+ });
2901
+
2902
+ if (payload?.set_cover === true) {
2903
+ updatedPack = await stickerPackService.setPackCover({
2904
+ ownerJid: context.ownerJid,
2905
+ identifier: context.packKey,
2906
+ stickerId: uploadedAssetId,
2907
+ });
2908
+ }
2909
+
2910
+ invalidateStickerCatalogDerivedCaches();
2911
+
2912
+ sendJson(req, res, 201, {
2913
+ data: {
2914
+ success: true,
2915
+ status: 'updated',
2916
+ added_sticker_id: uploadedAssetId,
2917
+ ...(await buildManagedPackResponseData(updatedPack)),
2918
+ },
2919
+ });
2920
+ } catch (error) {
2921
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
2922
+ sendManagedMutationStatus(req, res, 'already_deleted', { pack_key: context.packKey });
2923
+ return;
2924
+ }
2925
+ if (uploadedAssetId) {
2926
+ await cleanupOrphanStickerAssets([uploadedAssetId], {
2927
+ reason: 'add_sticker_error_recovery',
2928
+ }).catch(() => {});
2929
+ }
2930
+ const mapped = mapStickerPackWebManageError(error);
2931
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
2932
+ }
2933
+ };
2934
+
2935
+ const handleManagedPackStickerReplaceRequest = async (req, res, packKey, stickerId) => {
2936
+ if (req.method !== 'POST') {
2937
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
2938
+ return;
2939
+ }
2940
+ const context = await loadOwnedPackForWebManagement(req, res, packKey, { allowMissing: true });
2941
+ if (!context) return;
2942
+ if (context.missing || !context.pack) {
2943
+ sendManagedMutationStatus(req, res, 'already_deleted', {
2944
+ pack_key: context.packKey || String(packKey || ''),
2945
+ sticker_id: sanitizeText(stickerId, 36, { allowEmpty: true }) || null,
2946
+ });
2947
+ return;
2948
+ }
2949
+
2950
+ let payload = {};
2951
+ try {
2952
+ payload = await readJsonBody(req, {
2953
+ maxBytes: Math.max(256 * 1024, Math.round(MAX_STICKER_SOURCE_UPLOAD_BYTES * 1.6)),
2954
+ });
2955
+ } catch (error) {
2956
+ sendJson(req, res, Number(error?.statusCode || 400), {
2957
+ error: error?.message || 'Body inválido.',
2958
+ });
2959
+ return;
2960
+ }
2961
+
2962
+ const decoded = decodeStickerBase64Payload(payload?.sticker_base64 || payload?.sticker_data_url || '');
2963
+ if (!decoded?.buffer) {
2964
+ sendJson(req, res, 400, {
2965
+ error: 'Envie sticker_base64 ou sticker_data_url.',
2966
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
2967
+ });
2968
+ return;
2969
+ }
2970
+ if (decoded.buffer.length > MAX_STICKER_SOURCE_UPLOAD_BYTES) {
2971
+ sendJson(req, res, 400, {
2972
+ error: `Arquivo excede limite de ${Math.round(MAX_STICKER_SOURCE_UPLOAD_BYTES / (1024 * 1024))}MB.`,
2973
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
2974
+ });
2975
+ return;
2976
+ }
2977
+
2978
+ const normalizedStickerId = sanitizeText(stickerId, 36, { allowEmpty: false });
2979
+ if (!normalizedStickerId) {
2980
+ sendJson(req, res, 400, {
2981
+ error: 'sticker_id invalido.',
2982
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
2983
+ });
2984
+ return;
2985
+ }
2986
+
2987
+ let uploadedAssetId = '';
2988
+ try {
2989
+ const originalPack = await stickerPackService.getPackInfo({
2990
+ ownerJid: context.ownerJid,
2991
+ identifier: context.packKey,
2992
+ });
2993
+ const originalItems = Array.isArray(originalPack?.items) ? originalPack.items : [];
2994
+ const oldItem = originalItems.find((item) => item?.sticker_id === normalizedStickerId);
2995
+ if (!oldItem) {
2996
+ await sendManagedPackMutationStatus(req, res, 'already_deleted', originalPack, {
2997
+ pack_key: context.packKey,
2998
+ sticker_id: normalizedStickerId,
2999
+ });
3000
+ return;
3001
+ }
3002
+
3003
+ const normalizedUpload = await convertUploadMediaToWebp({
3004
+ ownerJid: context.ownerJid,
3005
+ buffer: decoded.buffer,
3006
+ mimetype: decoded.mimetype || 'image/webp',
3007
+ });
3008
+ const asset = await saveStickerAssetFromBuffer({
3009
+ ownerJid: context.ownerJid,
3010
+ buffer: normalizedUpload.buffer,
3011
+ mimetype: normalizedUpload.mimetype || 'image/webp',
3012
+ });
3013
+ uploadedAssetId = String(asset?.id || '').trim();
3014
+
3015
+ if (uploadedAssetId && uploadedAssetId === normalizedStickerId) {
3016
+ await sendManagedPackMutationStatus(req, res, 'unchanged', originalPack, {
3017
+ pack_key: context.packKey,
3018
+ replaced_sticker_id: normalizedStickerId,
3019
+ new_sticker_id: uploadedAssetId,
3020
+ });
3021
+ return;
3022
+ }
3023
+
3024
+ const swapResult = await runSqlTransaction(async (connection) => {
3025
+ const packRow = await findStickerPackByOwnerAndIdentifier(context.ownerJid, context.packKey, {
3026
+ connection,
3027
+ });
3028
+ if (!packRow) return { status: 'pack_missing' };
3029
+
3030
+ const liveOldItem = await getStickerPackItemByStickerId(packRow.id, normalizedStickerId, connection);
3031
+ if (!liveOldItem) return { status: 'old_sticker_missing' };
3032
+
3033
+ const duplicateTarget = uploadedAssetId ? await getStickerPackItemByStickerId(packRow.id, uploadedAssetId, connection) : null;
3034
+ if (duplicateTarget) {
3035
+ return { status: 'duplicate_target' };
3036
+ }
3037
+
3038
+ const removed = await removeStickerPackItemByStickerId(packRow.id, normalizedStickerId, connection);
3039
+ if (!removed) return { status: 'old_sticker_missing' };
3040
+
3041
+ await createStickerPackItem(
3042
+ {
3043
+ id: randomUUID(),
3044
+ pack_id: packRow.id,
3045
+ sticker_id: uploadedAssetId,
3046
+ position: Number(removed.position || liveOldItem.position || oldItem.position || 1),
3047
+ emojis: Array.isArray(liveOldItem.emojis) ? liveOldItem.emojis : Array.isArray(oldItem.emojis) ? oldItem.emojis : [],
3048
+ accessibility_label: liveOldItem.accessibility_label ?? oldItem.accessibility_label ?? null,
3049
+ },
3050
+ connection,
3051
+ );
3052
+
3053
+ if (String(packRow.cover_sticker_id || '') === normalizedStickerId) {
3054
+ await updateStickerPackFields(
3055
+ packRow.id,
3056
+ {
3057
+ cover_sticker_id: uploadedAssetId,
3058
+ },
3059
+ connection,
3060
+ );
3061
+ } else {
3062
+ await bumpStickerPackVersion(packRow.id, connection);
3063
+ }
3064
+
3065
+ return { status: 'updated', pack_id: packRow.id };
3066
+ });
3067
+
3068
+ if (swapResult?.status === 'pack_missing') {
3069
+ await cleanupOrphanStickerAssets(uploadedAssetId ? [uploadedAssetId] : [], {
3070
+ reason: 'replace_sticker_pack_missing',
3071
+ });
3072
+ sendManagedMutationStatus(req, res, 'already_deleted', {
3073
+ pack_key: context.packKey,
3074
+ sticker_id: normalizedStickerId,
3075
+ });
3076
+ return;
3077
+ }
3078
+
3079
+ if (swapResult?.status === 'old_sticker_missing') {
3080
+ const fresh = await stickerPackService.getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey }).catch(() => originalPack);
3081
+ await cleanupOrphanStickerAssets(uploadedAssetId ? [uploadedAssetId] : [], {
3082
+ reason: 'replace_sticker_old_missing',
3083
+ });
3084
+ await sendManagedPackMutationStatus(req, res, 'already_deleted', fresh, {
3085
+ pack_key: context.packKey,
3086
+ sticker_id: normalizedStickerId,
3087
+ });
3088
+ return;
3089
+ }
3090
+
3091
+ if (swapResult?.status === 'duplicate_target') {
3092
+ const fresh = await stickerPackService.getPackInfo({ ownerJid: context.ownerJid, identifier: context.packKey }).catch(() => originalPack);
3093
+ await sendManagedPackMutationStatus(req, res, 'noop', fresh, {
3094
+ pack_key: context.packKey,
3095
+ reason: 'duplicate_target_sticker',
3096
+ sticker_id: normalizedStickerId,
3097
+ new_sticker_id: uploadedAssetId || null,
3098
+ });
3099
+ return;
3100
+ }
3101
+
3102
+ invalidateStickerCatalogDerivedCaches();
3103
+ const finalPack = await stickerPackService.getPackInfo({
3104
+ ownerJid: context.ownerJid,
3105
+ identifier: context.packKey,
3106
+ });
3107
+ await cleanupOrphanStickerAssets([normalizedStickerId], {
3108
+ reason: 'replace_sticker_old_cleanup',
3109
+ });
3110
+ sendJson(req, res, 200, {
3111
+ data: {
3112
+ success: true,
3113
+ status: 'updated',
3114
+ replaced_sticker_id: normalizedStickerId,
3115
+ new_sticker_id: uploadedAssetId || null,
3116
+ ...(await buildManagedPackResponseData(finalPack)),
3117
+ },
3118
+ });
3119
+ } catch (error) {
3120
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
3121
+ sendManagedMutationStatus(req, res, 'already_deleted', {
3122
+ pack_key: context.packKey,
3123
+ sticker_id: normalizedStickerId,
3124
+ });
3125
+ return;
3126
+ }
3127
+ if (uploadedAssetId) {
3128
+ await cleanupOrphanStickerAssets([uploadedAssetId], {
3129
+ reason: 'replace_sticker_error_recovery',
3130
+ }).catch(() => {});
3131
+ }
3132
+ const mapped = mapStickerPackWebManageError(error);
3133
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
3134
+ }
3135
+ };
3136
+
3137
+ const handleManagedPackAnalyticsRequest = async (req, res, packKey) => {
3138
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
3139
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
3140
+ return;
3141
+ }
3142
+ const context = await loadOwnedPackForWebManagement(req, res, packKey);
3143
+ if (!context) return;
3144
+
3145
+ try {
3146
+ const analytics = await buildManagedPackAnalytics(context.pack);
3147
+ const publishState = await buildPackPublishStateData(context.pack, { includeUploads: true });
3148
+ sendJson(req, res, 200, {
3149
+ data: {
3150
+ pack_key: context.packKey,
3151
+ analytics,
3152
+ publish_state: publishState,
3153
+ },
3154
+ });
3155
+ } catch (error) {
3156
+ const mapped = mapStickerPackWebManageError(error);
3157
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
3158
+ }
3159
+ };
3160
+
3161
+ const normalizeCreatePackName = (value) => sanitizeText(value, PACK_CREATE_MAX_NAME_LENGTH, { allowEmpty: true }) || '';
3162
+
3163
+ const mapStickerPackCreateError = (error) => {
3164
+ if (!(error instanceof StickerPackError)) {
3165
+ return {
3166
+ statusCode: 500,
3167
+ code: STICKER_PACK_ERROR_CODES.INTERNAL_ERROR,
3168
+ message: 'Falha interna ao criar pack.',
3169
+ };
3170
+ }
3171
+
3172
+ if (error.code === STICKER_PACK_ERROR_CODES.INVALID_INPUT) {
3173
+ return {
3174
+ statusCode: 400,
3175
+ code: error.code,
3176
+ message: error.message || 'Dados de entrada inválidos para criar pack.',
3177
+ };
3178
+ }
3179
+
3180
+ if (error.code === STICKER_PACK_ERROR_CODES.PACK_LIMIT_REACHED) {
3181
+ return {
3182
+ statusCode: 429,
3183
+ code: error.code,
3184
+ message: error.message || 'Limite de packs atingido para este usuário.',
3185
+ };
3186
+ }
3187
+
3188
+ return {
3189
+ statusCode: 400,
3190
+ code: error.code,
3191
+ message: error.message || 'Não foi possível criar o pack.',
3192
+ };
3193
+ };
3194
+
3195
+ const handleCreatePackRequest = async (req, res) => {
3196
+ triggerStaleDraftCleanup();
3197
+ let payload = {};
3198
+ try {
3199
+ payload = await readJsonBody(req);
3200
+ } catch (error) {
3201
+ const statusCode = Number(error?.statusCode || 400);
3202
+ sendJson(req, res, statusCode, { error: error?.message || 'Body inválido.' });
3203
+ return;
3204
+ }
3205
+
3206
+ const name = normalizeCreatePackName(payload?.name);
3207
+ if (!name) {
3208
+ sendJson(req, res, 400, {
3209
+ error: 'Nome inválido. Informe um nome de pack (espaços e emojis são permitidos).',
3210
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
3211
+ });
3212
+ return;
3213
+ }
3214
+
3215
+ const publisher = sanitizeText(payload?.publisher || 'OmniZap Creator', PACK_CREATE_MAX_PUBLISHER_LENGTH, { allowEmpty: false });
3216
+ const description = sanitizeText(payload?.description || '', PACK_CREATE_MAX_DESCRIPTION_LENGTH, {
3217
+ allowEmpty: true,
3218
+ });
3219
+ const manualTags = mergeUniqueTags(Array.isArray(payload?.tags) ? payload.tags : []).slice(0, 8);
3220
+ const persistedDescription = buildPackDescriptionWithTags(description, manualTags);
3221
+ const visibility = String(payload?.visibility || 'public')
3222
+ .trim()
3223
+ .toLowerCase();
3224
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
3225
+ if (!googleSession?.ownerJid || !googleSession?.sub) {
3226
+ sendJson(req, res, 401, {
3227
+ error: 'Sessão expirada ou ausente. Faça login novamente para criar packs.',
3228
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
3229
+ });
3230
+ return;
3231
+ }
3232
+
3233
+ try {
3234
+ await assertGoogleIdentityNotBanned({
3235
+ sub: googleSession.sub,
3236
+ email: googleSession.email,
3237
+ ownerJid: googleSession.ownerJid,
3238
+ });
3239
+ } catch (error) {
3240
+ sendJson(req, res, Number(error?.statusCode || 403), {
3241
+ error: error?.message || 'Conta sem permissão para criar packs.',
3242
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
3243
+ });
3244
+ return;
3245
+ }
3246
+
3247
+ const googleCreator = {
3248
+ ownerJid: googleSession.ownerJid,
3249
+ sub: googleSession.sub,
3250
+ email: googleSession.email,
3251
+ name: googleSession.name,
3252
+ picture: googleSession.picture,
3253
+ };
3254
+
3255
+ if (googleCreator?.sub && googleCreator?.ownerJid) {
3256
+ await upsertGoogleWebUserRecord({
3257
+ sub: googleCreator.sub,
3258
+ ownerJid: googleCreator.ownerJid,
3259
+ email: googleCreator.email,
3260
+ name: googleCreator.name,
3261
+ picture: googleCreator.picture,
3262
+ }).catch((error) => {
3263
+ logger.warn('Falha ao persistir usuário Google web durante criação de pack.', {
3264
+ action: 'sticker_pack_google_web_user_upsert_create_pack_failed',
3265
+ error: error?.message,
3266
+ });
3267
+ });
3268
+ }
3269
+
3270
+ const ownerJid = googleCreator.ownerJid;
3271
+
3272
+ try {
3273
+ logPackWebFlow('info', 'create_pack_start', {
3274
+ owner_jid: ownerJid,
3275
+ google_sub: googleCreator?.sub || null,
3276
+ requested_visibility: visibility,
3277
+ name,
3278
+ });
3279
+ const created = await stickerPackService.createPack({
3280
+ ownerJid,
3281
+ name,
3282
+ publisher,
3283
+ description: persistedDescription,
3284
+ visibility,
3285
+ status: 'draft',
3286
+ });
3287
+ const editToken = saveWebPackEditToken({ packId: created.id, ownerJid });
3288
+ logPackWebFlow('info', 'create_pack_success', {
3289
+ owner_jid: ownerJid,
3290
+ pack_id: created.id,
3291
+ pack_key: created.pack_key,
3292
+ status: created.status || 'draft',
3293
+ visibility: created.visibility,
3294
+ });
3295
+
3296
+ sendJson(req, res, 201, {
3297
+ data: mapPackSummary(created),
3298
+ meta: {
3299
+ owner_jid: ownerJid,
3300
+ edit_token: editToken,
3301
+ edit_token_expires_in_ms: PACK_WEB_EDIT_TOKEN_TTL_MS,
3302
+ google_auth: googleCreator
3303
+ ? {
3304
+ provider: 'google',
3305
+ email: googleCreator.email,
3306
+ name: googleCreator.name,
3307
+ sub: googleCreator.sub,
3308
+ }
3309
+ : null,
3310
+ limits: {
3311
+ stickers_per_pack: PACK_CREATE_MAX_ITEMS,
3312
+ packs_per_owner: serializePackOwnerLimit(PACK_CREATE_MAX_PACKS_PER_OWNER),
3313
+ packs_per_owner_unlimited: !Number.isFinite(PACK_CREATE_MAX_PACKS_PER_OWNER),
3314
+ },
3315
+ },
3316
+ });
3317
+ } catch (error) {
3318
+ const mapped = mapStickerPackCreateError(error);
3319
+ logPackWebFlow('warn', 'create_pack_failed', {
3320
+ owner_jid: ownerJid,
3321
+ google_sub: googleCreator?.sub || null,
3322
+ name,
3323
+ visibility,
3324
+ error: error?.message,
3325
+ error_code: error?.code,
3326
+ });
3327
+ logger.warn('Falha ao criar pack via API web.', {
3328
+ action: 'sticker_catalog_create_pack_failed',
3329
+ owner_jid: ownerJid,
3330
+ google_sub: googleCreator?.sub || null,
3331
+ name,
3332
+ visibility,
3333
+ error: error?.message,
3334
+ error_code: error?.code,
3335
+ });
3336
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
3337
+ }
3338
+ };
3339
+
3340
+ const handleUploadStickerToPackRequest = async (req, res, packKey) => {
3341
+ triggerStaleDraftCleanup();
3342
+ const pack = await findStickerPackByPackKey(packKey);
3343
+ if (!pack) {
3344
+ sendJson(req, res, 404, {
3345
+ error: 'Pack nao encontrado.',
3346
+ code: STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND,
3347
+ });
3348
+ return;
3349
+ }
3350
+
3351
+ let payload = {};
3352
+ try {
3353
+ payload = await readJsonBody(req, {
3354
+ maxBytes: Math.max(256 * 1024, Math.round(MAX_STICKER_SOURCE_UPLOAD_BYTES * 1.6)),
3355
+ });
3356
+ } catch (error) {
3357
+ sendJson(req, res, Number(error?.statusCode || 400), {
3358
+ error: error?.message || 'Body inválido.',
3359
+ });
3360
+ return;
3361
+ }
3362
+
3363
+ const editToken = resolveWebPackEditToken(payload?.edit_token);
3364
+ if (!editToken || editToken.packId !== pack.id || editToken.ownerJid !== pack.owner_jid) {
3365
+ sendJson(req, res, 403, {
3366
+ error: 'Token de edição inválido para este pack.',
3367
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
3368
+ });
3369
+ return;
3370
+ }
3371
+
3372
+ const decoded = decodeStickerBase64Payload(payload?.sticker_base64 || payload?.sticker_data_url || '');
3373
+ if (!decoded?.buffer) {
3374
+ sendJson(req, res, 400, {
3375
+ error: 'Envie sticker_base64 ou sticker_data_url no payload.',
3376
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
3377
+ });
3378
+ return;
3379
+ }
3380
+
3381
+ if (decoded.buffer.length > MAX_STICKER_SOURCE_UPLOAD_BYTES) {
3382
+ sendJson(req, res, 400, {
3383
+ error: `Arquivo excede limite de ${Math.round(MAX_STICKER_SOURCE_UPLOAD_BYTES / (1024 * 1024))}MB.`,
3384
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
3385
+ });
3386
+ return;
3387
+ }
3388
+
3389
+ const computedStickerHash = sha256Hex(decoded.buffer);
3390
+ const payloadStickerHash = normalizeStickerHashHex(payload?.sticker_hash);
3391
+ if (payloadStickerHash && payloadStickerHash !== computedStickerHash) {
3392
+ sendJson(req, res, 400, {
3393
+ error: 'sticker_hash nao corresponde ao arquivo enviado.',
3394
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
3395
+ });
3396
+ return;
3397
+ }
3398
+
3399
+ const uploadId = normalizeWebUploadId(payload?.upload_id) || `h-${computedStickerHash.slice(0, 24)}`;
3400
+ const stickerHash = payloadStickerHash || computedStickerHash;
3401
+
3402
+ logPackWebFlow('info', 'upload_start', {
3403
+ pack_key: pack.pack_key,
3404
+ pack_id: pack.id,
3405
+ owner_jid: pack.owner_jid,
3406
+ upload_id: uploadId,
3407
+ sticker_hash: stickerHash,
3408
+ source_bytes: decoded.buffer.length,
3409
+ source_mimetype: decoded.mimetype || 'image/webp',
3410
+ });
3411
+
3412
+ let reservedUpload = null;
3413
+ let idempotentDoneResponse = null;
3414
+ let packStatusForResponse = normalizePackWebStatus(pack.status, 'draft');
3415
+
3416
+ try {
3417
+ await runSqlTransaction(async (connection) => {
3418
+ const lockedPackRow = await lockStickerPackByPackKey(pack.pack_key, connection);
3419
+ if (!lockedPackRow) {
3420
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND, 'Pack nao encontrado.');
3421
+ }
3422
+ if (String(lockedPackRow.id) !== String(pack.id) || String(lockedPackRow.owner_jid) !== String(pack.owner_jid)) {
3423
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.NOT_ALLOWED, 'Pack inválido para edição.');
3424
+ }
3425
+
3426
+ let existingUpload = await findPackWebUploadByUploadId(pack.id, uploadId, connection);
3427
+ if (existingUpload && existingUpload.sticker_hash !== stickerHash) {
3428
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.INVALID_INPUT, 'upload_id já foi usado para outro arquivo neste pack.');
3429
+ }
3430
+
3431
+ if (!existingUpload) {
3432
+ existingUpload = await findPackWebUploadByStickerHash(pack.id, stickerHash, connection);
3433
+ }
3434
+
3435
+ const currentPackStatus = normalizePackWebStatus(lockedPackRow.status, 'draft');
3436
+ if (existingUpload?.upload_status === 'done' && existingUpload.sticker_id) {
3437
+ const snapshot = await getPackConsistencySnapshot(pack.id, lockedPackRow.cover_sticker_id, connection);
3438
+ idempotentDoneResponse = {
3439
+ data: {
3440
+ pack_key: pack.pack_key,
3441
+ sticker_id: existingUpload.sticker_id,
3442
+ sticker_count: snapshot.sticker_count,
3443
+ asset_url: buildStickerAssetUrl(pack.pack_key, existingUpload.sticker_id),
3444
+ idempotent: true,
3445
+ upload_id: existingUpload.upload_id,
3446
+ sticker_hash: existingUpload.sticker_hash,
3447
+ pack_status: currentPackStatus,
3448
+ },
3449
+ };
3450
+ return;
3451
+ }
3452
+
3453
+ if (currentPackStatus === 'published') {
3454
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.NOT_ALLOWED, 'Pack já foi publicado. Crie um novo pack para enviar novos stickers.');
3455
+ }
3456
+
3457
+ if (currentPackStatus === 'processing') {
3458
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.NOT_ALLOWED, 'Pack está em finalização. Aguarde e tente novamente.');
3459
+ }
3460
+
3461
+ if (!existingUpload) {
3462
+ reservedUpload = await createPackWebUpload(
3463
+ {
3464
+ id: randomUUID(),
3465
+ pack_id: pack.id,
3466
+ upload_id: uploadId,
3467
+ sticker_hash: stickerHash,
3468
+ source_mimetype: decoded.mimetype || 'image/webp',
3469
+ upload_status: 'processing',
3470
+ attempt_count: 1,
3471
+ last_attempt_at: new Date(),
3472
+ },
3473
+ connection,
3474
+ );
3475
+ } else {
3476
+ reservedUpload = await updatePackWebUpload(
3477
+ existingUpload.id,
3478
+ {
3479
+ upload_status: 'processing',
3480
+ source_mimetype: decoded.mimetype || existingUpload.source_mimetype || 'image/webp',
3481
+ error_code: null,
3482
+ error_message: null,
3483
+ attempt_count: Math.max(1, Number(existingUpload.attempt_count || 0) + 1),
3484
+ last_attempt_at: new Date(),
3485
+ },
3486
+ connection,
3487
+ );
3488
+ }
3489
+
3490
+ packStatusForResponse = currentPackStatus === 'failed' ? 'uploading' : 'uploading';
3491
+ if (currentPackStatus !== 'uploading') {
3492
+ await setStickerPackStatus(pack.id, 'uploading', connection);
3493
+ packStatusForResponse = 'uploading';
3494
+ }
3495
+ });
3496
+
3497
+ if (idempotentDoneResponse) {
3498
+ logPackWebFlow('info', 'upload_success', {
3499
+ pack_key: pack.pack_key,
3500
+ pack_id: pack.id,
3501
+ owner_jid: pack.owner_jid,
3502
+ upload_id: uploadId,
3503
+ sticker_hash: stickerHash,
3504
+ idempotent: true,
3505
+ });
3506
+ sendJson(req, res, 200, idempotentDoneResponse);
3507
+ return;
3508
+ }
3509
+
3510
+ const normalizedUpload = await convertUploadMediaToWebp({
3511
+ ownerJid: pack.owner_jid,
3512
+ buffer: decoded.buffer,
3513
+ mimetype: decoded.mimetype || 'image/webp',
3514
+ });
3515
+ const asset = await saveStickerAssetFromBuffer({
3516
+ ownerJid: pack.owner_jid,
3517
+ buffer: normalizedUpload.buffer,
3518
+ mimetype: normalizedUpload.mimetype || 'image/webp',
3519
+ });
3520
+
3521
+ let updatedPack;
3522
+ try {
3523
+ updatedPack = await stickerPackService.addStickerToPack({
3524
+ ownerJid: pack.owner_jid,
3525
+ identifier: pack.pack_key,
3526
+ asset: { id: asset.id },
3527
+ emojis: [],
3528
+ accessibilityLabel: null,
3529
+ });
3530
+ } catch (error) {
3531
+ if (error?.code !== STICKER_PACK_ERROR_CODES.DUPLICATE_STICKER) {
3532
+ throw error;
3533
+ }
3534
+ updatedPack = await stickerPackService.getPackInfo({
3535
+ ownerJid: pack.owner_jid,
3536
+ identifier: pack.pack_key,
3537
+ });
3538
+ }
3539
+
3540
+ if (payload?.set_cover === true) {
3541
+ updatedPack = await stickerPackService.setPackCover({
3542
+ ownerJid: pack.owner_jid,
3543
+ identifier: pack.pack_key,
3544
+ stickerId: asset.id,
3545
+ });
3546
+ }
3547
+
3548
+ let responseStickerCount = Number(updatedPack?.sticker_count || 0);
3549
+ await runSqlTransaction(async (connection) => {
3550
+ const lockedPackRow = await lockStickerPackByPackKey(pack.pack_key, connection);
3551
+ if (!lockedPackRow) {
3552
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND, 'Pack nao encontrado.');
3553
+ }
3554
+
3555
+ const uploadRow = (reservedUpload?.id && normalizePackWebUploadRow((await executeQuery(`SELECT * FROM ${TABLES.STICKER_PACK_WEB_UPLOAD} WHERE id = ? LIMIT 1`, [reservedUpload.id], connection))?.[0])) || (await findPackWebUploadByUploadId(pack.id, uploadId, connection)) || (await findPackWebUploadByStickerHash(pack.id, stickerHash, connection));
3556
+
3557
+ if (!uploadRow) {
3558
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.INTERNAL_ERROR, 'Registro de upload não encontrado para finalizar.');
3559
+ }
3560
+
3561
+ await updatePackWebUpload(
3562
+ uploadRow.id,
3563
+ {
3564
+ upload_status: 'done',
3565
+ sticker_id: asset.id,
3566
+ error_code: null,
3567
+ error_message: null,
3568
+ source_mimetype: decoded.mimetype || 'image/webp',
3569
+ },
3570
+ connection,
3571
+ );
3572
+
3573
+ if (normalizePackWebStatus(lockedPackRow.status, 'draft') !== 'published') {
3574
+ await setStickerPackStatus(pack.id, 'uploading', connection);
3575
+ packStatusForResponse = 'uploading';
3576
+ } else {
3577
+ packStatusForResponse = 'published';
3578
+ }
3579
+
3580
+ const snapshot = await getPackConsistencySnapshot(pack.id, payload?.set_cover === true ? asset.id : lockedPackRow.cover_sticker_id, connection);
3581
+ responseStickerCount = snapshot.sticker_count;
3582
+ });
3583
+
3584
+ logPackWebFlow('info', 'upload_success', {
3585
+ pack_key: pack.pack_key,
3586
+ pack_id: pack.id,
3587
+ owner_jid: pack.owner_jid,
3588
+ upload_id: uploadId,
3589
+ sticker_hash: stickerHash,
3590
+ sticker_id: asset.id,
3591
+ pack_status: packStatusForResponse,
3592
+ });
3593
+
3594
+ sendJson(req, res, 201, {
3595
+ data: {
3596
+ pack_key: pack.pack_key,
3597
+ sticker_id: asset.id,
3598
+ sticker_count: responseStickerCount,
3599
+ asset_url: buildStickerAssetUrl(pack.pack_key, asset.id),
3600
+ upload_id: uploadId,
3601
+ sticker_hash: stickerHash,
3602
+ idempotent: false,
3603
+ pack_status: packStatusForResponse,
3604
+ },
3605
+ });
3606
+ } catch (error) {
3607
+ if (reservedUpload?.id) {
3608
+ await runSqlTransaction(async (connection) => {
3609
+ const currentUpload = (await findPackWebUploadByUploadId(pack.id, uploadId, connection)) || (await findPackWebUploadByStickerHash(pack.id, stickerHash, connection));
3610
+ if (currentUpload) {
3611
+ await updatePackWebUpload(
3612
+ currentUpload.id,
3613
+ {
3614
+ upload_status: 'failed',
3615
+ error_code: String(error?.code || 'UPLOAD_FAILED').slice(0, 64),
3616
+ error_message: error?.message || 'Falha no upload do sticker.',
3617
+ source_mimetype: decoded?.mimetype || currentUpload.source_mimetype || 'image/webp',
3618
+ },
3619
+ connection,
3620
+ );
3621
+ }
3622
+ await setStickerPackStatus(pack.id, 'draft', connection);
3623
+ }).catch((updateError) => {
3624
+ logPackWebFlow('warn', 'upload_failed_mark_failed', {
3625
+ pack_key: pack.pack_key,
3626
+ pack_id: pack.id,
3627
+ upload_id: uploadId,
3628
+ sticker_hash: stickerHash,
3629
+ original_error: error?.message,
3630
+ update_error: updateError?.message,
3631
+ });
3632
+ });
3633
+ }
3634
+
3635
+ logPackWebFlow('warn', 'upload_failed', {
3636
+ pack_key: pack.pack_key,
3637
+ pack_id: pack.id,
3638
+ owner_jid: pack.owner_jid,
3639
+ upload_id: uploadId,
3640
+ sticker_hash: stickerHash,
3641
+ error: error?.message,
3642
+ error_code: error?.code,
3643
+ });
3644
+ const mapped = mapStickerPackCreateError(error);
3645
+ sendJson(req, res, mapped.statusCode, {
3646
+ error: mapped.message,
3647
+ code: mapped.code,
3648
+ });
3649
+ }
3650
+ };
3651
+
3652
+ const handlePackPublishStateRequest = async (req, res, packKey, url = null) => {
3653
+ const pack = await findStickerPackByPackKey(packKey);
3654
+ if (!pack) {
3655
+ sendJson(req, res, 404, {
3656
+ error: 'Pack nao encontrado.',
3657
+ code: STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND,
3658
+ });
3659
+ return;
3660
+ }
3661
+
3662
+ let payload = {};
3663
+ if (req.method === 'POST') {
3664
+ try {
3665
+ payload = await readJsonBody(req);
3666
+ } catch (error) {
3667
+ sendJson(req, res, Number(error?.statusCode || 400), {
3668
+ error: error?.message || 'Body inválido.',
3669
+ });
3670
+ return;
3671
+ }
3672
+ }
3673
+
3674
+ const editTokenValue = (req.method === 'GET' || req.method === 'HEAD' ? String(url?.searchParams?.get('edit_token') || '') : '') || String(payload?.edit_token || '');
3675
+ const editToken = resolveWebPackEditToken(editTokenValue);
3676
+ if (!editToken || editToken.packId !== pack.id || editToken.ownerJid !== pack.owner_jid) {
3677
+ sendJson(req, res, 403, {
3678
+ error: 'Token de edição inválido para este pack.',
3679
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
3680
+ });
3681
+ return;
3682
+ }
3683
+
3684
+ const packState = await buildPackPublishStateData(pack, { includeUploads: true });
3685
+ sendJson(req, res, 200, {
3686
+ data: packState,
3687
+ pack: mapPackSummary({ ...pack, sticker_count: packState.consistency.sticker_count }),
3688
+ });
3689
+ };
3690
+
3691
+ const handleFinalizePackRequest = async (req, res, packKey) => {
3692
+ triggerStaleDraftCleanup();
3693
+
3694
+ let payload = {};
3695
+ try {
3696
+ payload = await readJsonBody(req);
3697
+ } catch (error) {
3698
+ sendJson(req, res, Number(error?.statusCode || 400), {
3699
+ error: error?.message || 'Body inválido.',
3700
+ });
3701
+ return;
3702
+ }
3703
+
3704
+ const pack = await findStickerPackByPackKey(packKey);
3705
+ if (!pack) {
3706
+ sendJson(req, res, 404, {
3707
+ error: 'Pack nao encontrado.',
3708
+ code: STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND,
3709
+ });
3710
+ return;
3711
+ }
3712
+
3713
+ const editToken = resolveWebPackEditToken(payload?.edit_token);
3714
+ if (!editToken || editToken.packId !== pack.id || editToken.ownerJid !== pack.owner_jid) {
3715
+ sendJson(req, res, 403, {
3716
+ error: 'Token de edição inválido para este pack.',
3717
+ code: STICKER_PACK_ERROR_CODES.NOT_ALLOWED,
3718
+ });
3719
+ return;
3720
+ }
3721
+
3722
+ logPackWebFlow('info', 'finalize_start', {
3723
+ pack_key: pack.pack_key,
3724
+ pack_id: pack.id,
3725
+ owner_jid: pack.owner_jid,
3726
+ });
3727
+
3728
+ let finalizeResult = {
3729
+ canPublish: false,
3730
+ packStatus: normalizePackWebStatus(pack.status, 'draft'),
3731
+ reason: 'unknown',
3732
+ };
3733
+
3734
+ try {
3735
+ await runSqlTransaction(async (connection) => {
3736
+ const lockedPackRow = await lockStickerPackByPackKey(pack.pack_key, connection);
3737
+ if (!lockedPackRow) {
3738
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND, 'Pack nao encontrado.');
3739
+ }
3740
+
3741
+ const currentStatus = normalizePackWebStatus(lockedPackRow.status, 'draft');
3742
+ if (currentStatus === 'published') {
3743
+ finalizeResult = {
3744
+ canPublish: true,
3745
+ packStatus: 'published',
3746
+ reason: 'already_published',
3747
+ };
3748
+ return;
3749
+ }
3750
+
3751
+ await setStickerPackStatus(pack.id, 'processing', connection);
3752
+
3753
+ const snapshot = await getPackConsistencySnapshot(pack.id, lockedPackRow.cover_sticker_id, connection);
3754
+ const canPublish = snapshot.sticker_count >= 1 && snapshot.failed_uploads === 0 && snapshot.processing_uploads === 0 && snapshot.pending_uploads === 0 && snapshot.cover_valid;
3755
+
3756
+ if (canPublish) {
3757
+ await setStickerPackStatus(pack.id, 'published', connection);
3758
+ finalizeResult = {
3759
+ canPublish: true,
3760
+ packStatus: 'published',
3761
+ reason: 'published',
3762
+ };
3763
+ return;
3764
+ }
3765
+
3766
+ await setStickerPackStatus(pack.id, 'draft', connection);
3767
+ finalizeResult = {
3768
+ canPublish: false,
3769
+ packStatus: 'draft',
3770
+ reason: snapshot.failed_uploads > 0 ? 'failed_uploads' : snapshot.processing_uploads > 0 ? 'uploads_processing' : snapshot.pending_uploads > 0 ? 'uploads_pending' : !snapshot.cover_valid ? 'cover_missing' : snapshot.sticker_count < 1 ? 'not_enough_stickers' : 'inconsistent',
3771
+ };
3772
+ });
3773
+ } catch (error) {
3774
+ await runSqlTransaction(async (connection) => {
3775
+ const lockedPackRow = await lockStickerPackByPackKey(pack.pack_key, connection).catch(() => null);
3776
+ if (lockedPackRow) {
3777
+ await setStickerPackStatus(pack.id, 'failed', connection);
3778
+ }
3779
+ }).catch(() => null);
3780
+
3781
+ logPackWebFlow('error', 'finalize_failed', {
3782
+ pack_key: pack.pack_key,
3783
+ pack_id: pack.id,
3784
+ owner_jid: pack.owner_jid,
3785
+ error: error?.message,
3786
+ error_code: error?.code,
3787
+ });
3788
+
3789
+ const mapped = mapStickerPackCreateError(error);
3790
+ sendJson(req, res, mapped.statusCode, { error: mapped.message, code: mapped.code });
3791
+ return;
3792
+ }
3793
+
3794
+ const freshPack = (await findStickerPackByPackKey(pack.pack_key)) || pack;
3795
+ const packState = await buildPackPublishStateData(freshPack, { includeUploads: true });
3796
+
3797
+ if (finalizeResult.canPublish) {
3798
+ logPackWebFlow('info', 'finalize_success', {
3799
+ pack_key: pack.pack_key,
3800
+ pack_id: pack.id,
3801
+ owner_jid: pack.owner_jid,
3802
+ pack_status: 'published',
3803
+ sticker_count: packState?.consistency?.sticker_count || 0,
3804
+ });
3805
+
3806
+ sendJson(req, res, 200, {
3807
+ data: {
3808
+ pack: mapPackSummary({ ...freshPack, sticker_count: packState.consistency.sticker_count }),
3809
+ publish_state: packState,
3810
+ },
3811
+ });
3812
+ return;
3813
+ }
3814
+
3815
+ logPackWebFlow('warn', 'finalize_failed', {
3816
+ pack_key: pack.pack_key,
3817
+ pack_id: pack.id,
3818
+ owner_jid: pack.owner_jid,
3819
+ reason: finalizeResult.reason,
3820
+ pack_status: finalizeResult.packStatus,
3821
+ consistency: packState.consistency,
3822
+ });
3823
+
3824
+ sendJson(req, res, 409, {
3825
+ error: 'Pack ainda não está consistente para publicação.',
3826
+ code: STICKER_PACK_ERROR_CODES.INVALID_INPUT,
3827
+ data: {
3828
+ pack: mapPackSummary({ ...freshPack, sticker_count: packState.consistency.sticker_count }),
3829
+ publish_state: packState,
3830
+ reason: finalizeResult.reason,
3831
+ },
3832
+ });
3833
+ };
3834
+
3835
+ const handleCreatorRankingRequest = async (req, res, url) => {
3836
+ const visibility = normalizeCatalogVisibility(url.searchParams.get('visibility'));
3837
+ const q = sanitizeText(url.searchParams.get('q') || '', 120, { allowEmpty: true }) || '';
3838
+ const limit = clampInt(url.searchParams.get('limit'), 50, 5, 200);
3839
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
3840
+ const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
3841
+ const cacheKey = buildCacheKey(['creator_ranking', visibility, q, limit, hasNsfwAccess ? 1 : 0]);
3842
+ const payload = await getCachedSnapshot({
3843
+ cacheMap: CATALOG_CREATOR_RANKING_CACHE,
3844
+ key: cacheKey,
3845
+ ttlSeconds: CATALOG_CREATOR_RANKING_CACHE_SECONDS,
3846
+ staleWhileRefresh: true,
3847
+ staleOnError: true,
3848
+ load: async () => {
3849
+ const { packs } = await listStickerPacksForCatalog({
3850
+ visibility,
3851
+ search: q,
3852
+ limit: 120,
3853
+ offset: 0,
3854
+ });
3855
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
3856
+ const { entries } = await hydrateMarketplaceEntries(packs, { driftSnapshot });
3857
+ const ranking = buildCreatorRanking(STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries, { limit });
3858
+
3859
+ return {
3860
+ data: ranking.map((creator) => ({
3861
+ creator_score: Number((Number(creator.avg_pack_score || 0) * 0.45 + Number(creator.total_likes || 0) * 0.0008 + Number(creator.total_opens || 0) * 0.00015).toFixed(6)),
3862
+ publisher: creator.publisher,
3863
+ verified: Boolean(creator.verified),
3864
+ badges: creator.verified ? ['verified_creator'] : [],
3865
+ stats: {
3866
+ packs_count: Number(creator.packs_count || 0),
3867
+ total_likes: Number(creator.total_likes || 0),
3868
+ total_opens: Number(creator.total_opens || 0),
3869
+ avg_pack_score: Number(creator.avg_pack_score || 0),
3870
+ },
3871
+ top_pack: creator.top_pack ? toSummaryEntry(creator.top_pack, { hideSensitiveCover: !hasNsfwAccess }) : null,
3872
+ })),
3873
+ filters: {
3874
+ visibility,
3875
+ q,
3876
+ limit,
3877
+ },
3878
+ };
3879
+ },
3880
+ });
3881
+
3882
+ sendJson(req, res, 200, payload);
3883
+ };
3884
+
3885
+ const handleRecommendationsRequest = async (req, res, url) => {
3886
+ const visibility = normalizeCatalogVisibility(url.searchParams.get('visibility'));
3887
+ const q = sanitizeText(url.searchParams.get('q') || '', 120, { allowEmpty: true }) || '';
3888
+ const categories = parseCategoryFilters(url.searchParams.get('categories'));
3889
+ const limit = clampInt(url.searchParams.get('limit'), 18, 4, 50);
3890
+ const viewerKey = normalizeViewerKey(url.searchParams.get('viewer_key'));
3891
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
3892
+ const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
3893
+
3894
+ const { packs } = await listStickerPacksForCatalog({
3895
+ visibility,
3896
+ search: q,
3897
+ limit: Math.max(limit * 4, 80),
3898
+ offset: 0,
3899
+ });
3900
+ const driftSnapshot = await getMarketplaceDriftSnapshot();
3901
+ const { entries, packClassificationById } = await hydrateMarketplaceEntries(packs, {
3902
+ driftSnapshot,
3903
+ });
3904
+ const entriesClassified = STICKER_CATALOG_ONLY_CLASSIFIED ? entries.filter((entry) => isPackClassified(entry.packClassification)) : entries;
3905
+ const entriesByCategory = categories.length ? entriesClassified.filter((entry) => hasAnyCategory(entry.packClassification?.tags || [], categories)) : entriesClassified;
3906
+
3907
+ const viewerRecentPackIds = viewerKey ? await listViewerRecentPackIds(viewerKey, { days: 45, limit: 160 }) : [];
3908
+ const viewerAffinity = buildViewerTagAffinity({
3909
+ viewerEntries: viewerRecentPackIds,
3910
+ packClassificationById,
3911
+ });
3912
+ const personalized = buildPersonalizedRecommendations({
3913
+ entries: entriesByCategory,
3914
+ viewerAffinity,
3915
+ excludePackIds: new Set(viewerRecentPackIds.map((entry) => entry.pack_id)),
3916
+ limit,
3917
+ });
3918
+ const fallback = buildIntentCollections(entriesByCategory, { limit }).em_alta;
3919
+ const result = personalized.length ? personalized : fallback;
3920
+
3921
+ sendJson(req, res, 200, {
3922
+ data: result.map((entry) => toSummaryEntry(entry, { hideSensitiveCover: !hasNsfwAccess })),
3923
+ meta: {
3924
+ personalized: Boolean(personalized.length),
3925
+ viewer_key_present: Boolean(viewerKey),
3926
+ inferred_affinity_tags: Array.from(viewerAffinity.entries())
3927
+ .sort((left, right) => Number(right[1]) - Number(left[1]))
3928
+ .slice(0, 8)
3929
+ .map(([tag]) => tag),
3930
+ },
3931
+ filters: {
3932
+ visibility,
3933
+ q,
3934
+ categories,
3935
+ limit,
3936
+ },
3937
+ });
3938
+ };
3939
+
3940
+ const handleOrphanStickerListRequest = async (req, res, url) => {
3941
+ const q = sanitizeText(url.searchParams.get('q') || '', 140, { allowEmpty: true }) || '';
3942
+ const categories = parseCategoryFilters(url.searchParams.get('categories'));
3943
+ const limit = clampInt(url.searchParams.get('limit'), DEFAULT_ORPHAN_LIST_LIMIT, 1, MAX_ORPHAN_LIST_LIMIT);
3944
+ const offset = clampInt(url.searchParams.get('offset'), 0, 0, 1_000_000);
3945
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
3946
+ const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
3947
+
3948
+ const { assets, hasMore, total } = categories.length
3949
+ ? await listClassifiedOrphanAssetsByCategories({ search: q, categories, limit, offset })
3950
+ : await (STICKER_CATALOG_ONLY_CLASSIFIED ? listClassifiedStickerAssetsWithoutPack : listStickerAssetsWithoutPack)({
3951
+ search: q,
3952
+ limit,
3953
+ offset,
3954
+ });
3955
+ const classifications = await listStickerClassificationsByAssetIds(assets.map((asset) => asset.id));
3956
+ const byAssetId = new Map(classifications.map((entry) => [entry.asset_id, entry]));
3957
+ const filteredAssets = STICKER_CATALOG_ONLY_CLASSIFIED ? assets.filter((asset) => isStickerClassified(byAssetId.get(asset.id))) : assets;
3958
+ const filteredByCategories = categories.length ? filteredAssets.filter((asset) => hasAnyCategory(resolveClassificationTags(byAssetId.get(asset.id)), categories)) : filteredAssets;
3959
+ const currentPage = Math.floor(offset / limit) + 1;
3960
+ const totalPages = Math.max(1, Math.ceil(total / limit));
3961
+
3962
+ sendJson(req, res, 200, {
3963
+ data: filteredByCategories.map((asset) =>
3964
+ mapOrphanStickerAsset(asset, byAssetId.get(asset.id) || null, {
3965
+ hideSensitiveAssets: !hasNsfwAccess,
3966
+ }),
3967
+ ),
3968
+ pagination: {
3969
+ limit,
3970
+ offset,
3971
+ page: currentPage,
3972
+ total,
3973
+ total_pages: totalPages,
3974
+ has_more: hasMore,
3975
+ next_offset: hasMore ? offset + limit : null,
3976
+ },
3977
+ filters: {
3978
+ q,
3979
+ categories,
3980
+ },
3981
+ });
3982
+ };
3983
+
3984
+ const handleDataFileListRequest = async (req, res, url) => {
3985
+ const q = sanitizeText(url.searchParams.get('q') || '', 140, { allowEmpty: true }) || '';
3986
+ const limit = clampInt(url.searchParams.get('limit'), DEFAULT_DATA_LIST_LIMIT, 1, MAX_DATA_LIST_LIMIT);
3987
+ const offset = clampInt(url.searchParams.get('offset'), 0, 0, 1_000_000);
3988
+ const normalizedQuery = q.toLowerCase();
3989
+
3990
+ const allFiles = await listDataImageFiles();
3991
+ const filteredFiles = normalizedQuery ? allFiles.filter((item) => item.name.toLowerCase().includes(normalizedQuery) || item.relative_path.toLowerCase().includes(normalizedQuery)) : allFiles;
3992
+
3993
+ const page = filteredFiles.slice(offset, offset + limit);
3994
+ const hasMore = offset + limit < filteredFiles.length;
3995
+
3996
+ sendJson(req, res, 200, {
3997
+ data: page,
3998
+ pagination: {
3999
+ limit,
4000
+ offset,
4001
+ has_more: hasMore,
4002
+ next_offset: hasMore ? offset + limit : null,
4003
+ total: filteredFiles.length,
4004
+ },
4005
+ filters: {
4006
+ q,
4007
+ },
4008
+ meta: {
4009
+ root: STICKER_DATA_PUBLIC_DIR,
4010
+ public_path: STICKER_DATA_PUBLIC_PATH,
4011
+ api_base: buildDataAssetApiBaseUrl(),
4012
+ },
4013
+ });
4014
+ };
4015
+
4016
+ const fetchPublicPackPayload = async (normalizedPackKey) => {
4017
+ const safePackKey = sanitizeText(normalizedPackKey, 160, { allowEmpty: false });
4018
+ if (!safePackKey) return null;
4019
+ const cacheKey = buildCacheKey(['public_pack_payload', safePackKey]);
4020
+
4021
+ return getCachedSnapshot({
4022
+ cacheMap: CATALOG_PACK_PAYLOAD_CACHE,
4023
+ key: cacheKey,
4024
+ ttlSeconds: CATALOG_PACK_PAYLOAD_CACHE_SECONDS,
4025
+ staleWhileRefresh: true,
4026
+ staleOnError: true,
4027
+ load: async () => {
4028
+ const pack = await findStickerPackByPackKey(normalizedPackKey);
4029
+ if (!pack || !isPackPubliclyVisible(pack)) return null;
4030
+
4031
+ const items = await listStickerPackItems(pack.id);
4032
+ const stickerIds = items.map((item) => item.sticker_id);
4033
+ const [classifications, packClassification, engagement] = await Promise.all([listStickerClassificationsByAssetIds(stickerIds), getPackClassificationSummaryByAssetIds(stickerIds), getStickerPackEngagementByPackId(pack.id)]);
4034
+
4035
+ if (STICKER_CATALOG_ONLY_CLASSIFIED && !isPackClassified(packClassification)) {
4036
+ return null;
4037
+ }
4038
+
4039
+ const [interactionStatsByPack, driftSnapshot, snapshotByPackId] = await Promise.all([
4040
+ listStickerPackInteractionStatsByPackIds([pack.id]),
4041
+ getMarketplaceDriftSnapshot(),
4042
+ canUseRankingSnapshotRead(`pack_payload:${pack.id}`)
4043
+ .then((enabled) => (enabled ? listStickerPackScoreSnapshotsByPackIds([pack.id]) : new Map()))
4044
+ .catch(() => new Map()),
4045
+ ]);
4046
+ const byAssetClassification = new Map(classifications.map((entry) => [entry.asset_id, entry]));
4047
+ const orderedClassifications = stickerIds.map((stickerId) => byAssetClassification.get(stickerId)).filter(Boolean);
4048
+ const snapshot = snapshotByPackId.get(pack.id);
4049
+ const signals = snapshot?.signals
4050
+ ? snapshot.signals
4051
+ : computePackSignals({
4052
+ pack: { ...pack, items },
4053
+ engagement,
4054
+ packClassification,
4055
+ itemClassifications: orderedClassifications,
4056
+ interactionStats: interactionStatsByPack.get(pack.id) || null,
4057
+ scoringWeights: driftSnapshot.weights,
4058
+ });
4059
+
4060
+ return {
4061
+ pack,
4062
+ items,
4063
+ byAssetClassification,
4064
+ packClassification,
4065
+ engagement,
4066
+ signals,
4067
+ };
4068
+ },
4069
+ });
4070
+ };
4071
+
4072
+ const handleDetailsRequest = async (req, res, packKey, url) => {
4073
+ const normalizedPackKey = sanitizeText(packKey, 160, { allowEmpty: false });
4074
+ const categories = parseCategoryFilters(url.searchParams.get('categories'));
4075
+ if (!normalizedPackKey) {
4076
+ sendJson(req, res, 400, { error: 'pack_key invalido.' });
4077
+ return;
4078
+ }
4079
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
4080
+ const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
4081
+
4082
+ const packPayload = await fetchPublicPackPayload(normalizedPackKey);
4083
+ if (!packPayload) {
4084
+ sendJson(req, res, 404, { error: 'Pack nao encontrado.' });
4085
+ return;
4086
+ }
4087
+
4088
+ const { pack, items, byAssetClassification, packClassification, engagement, signals } = packPayload;
4089
+ const visibleItems = STICKER_CATALOG_ONLY_CLASSIFIED ? items.filter((item) => isStickerClassified(byAssetClassification.get(item.sticker_id))) : items;
4090
+ const visibleItemsByCategories = categories.length ? visibleItems.filter((item) => hasAnyCategory(resolveClassificationTags(byAssetClassification.get(item.sticker_id)), categories)) : visibleItems;
4091
+
4092
+ sendJson(req, res, 200, {
4093
+ data: mapPackDetails(pack, visibleItemsByCategories, {
4094
+ byAssetClassification,
4095
+ packClassification,
4096
+ engagement,
4097
+ signals,
4098
+ hideSensitiveAssets: !hasNsfwAccess,
4099
+ }),
4100
+ });
4101
+ };
4102
+
4103
+ const handleAssetRequest = async (req, res, packKey, stickerToken, url) => {
4104
+ const normalizedPackKey = sanitizeText(packKey, 160, { allowEmpty: false });
4105
+ const normalizedStickerId = sanitizeText(stripWebpExtension(stickerToken), 36, {
4106
+ allowEmpty: false,
4107
+ });
4108
+ const previewVariant = isPreviewVariantRequested(url);
4109
+
4110
+ if (!normalizedPackKey || !normalizedStickerId) {
4111
+ sendJson(req, res, 400, { error: 'Parametros invalidos.' });
4112
+ return;
4113
+ }
4114
+
4115
+ const pack = await findStickerPackByPackKey(normalizedPackKey);
4116
+ if (!pack || !isPackPubliclyVisible(pack)) {
4117
+ sendJson(req, res, 404, { error: 'Pack nao encontrado.' });
4118
+ return;
4119
+ }
4120
+
4121
+ const items = await listStickerPackItems(pack.id);
4122
+ const item = items.find((entry) => entry.sticker_id === normalizedStickerId);
4123
+ const stickerIds = items.map((entry) => entry.sticker_id).filter(Boolean);
4124
+
4125
+ if (!item?.asset) {
4126
+ sendJson(req, res, 404, { error: 'Sticker nao encontrado.' });
4127
+ return;
4128
+ }
4129
+
4130
+ try {
4131
+ const buffer = await readStickerAssetBuffer(item.asset);
4132
+ const classification = await findStickerClassificationByAssetId(normalizedStickerId).catch(() => null);
4133
+ const packClassification = stickerIds.length ? await getPackClassificationSummaryByAssetIds(stickerIds).catch(() => null) : null;
4134
+ if (STICKER_CATALOG_ONLY_CLASSIFIED && !isStickerClassified(classification)) {
4135
+ sendJson(req, res, 404, { error: 'Sticker nao encontrado.' });
4136
+ return;
4137
+ }
4138
+ const decorated = classification ? decorateStickerClassification(classification) : null;
4139
+ const packMetadata = parsePackDescriptionMetadata(pack.description);
4140
+ const decoratedPackClassification = decoratePackClassificationSummary(packClassification);
4141
+ const packMarkedNsfw = isPackSummaryMarkedNsfw({
4142
+ name: pack.name || '',
4143
+ description: packMetadata.cleanDescription || '',
4144
+ classification: decoratedPackClassification,
4145
+ tags: mergeUniqueTags(decoratedPackClassification?.tags || [], packMetadata.tags),
4146
+ manual_tags: packMetadata.tags,
4147
+ });
4148
+ const stickerMarkedNsfw = isClassificationMarkedNsfw(decorated);
4149
+ if (packMarkedNsfw || stickerMarkedNsfw) {
4150
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
4151
+ const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
4152
+ if (!hasNsfwAccess) {
4153
+ sendJson(req, res, 403, {
4154
+ error: 'Conteudo sensivel disponivel apenas para usuarios logados.',
4155
+ });
4156
+ return;
4157
+ }
4158
+ }
4159
+
4160
+ const externalAssetUrl = previewVariant
4161
+ ? null
4162
+ : await getStickerAssetExternalUrl(item.asset, {
4163
+ secure: true,
4164
+ expiresInSeconds: Math.max(60, Math.min(3600, Number(process.env.STICKER_OBJECT_STORAGE_SIGNED_URL_TTL_SECONDS) || 300)),
4165
+ }).catch(() => null);
4166
+ if (!previewVariant && externalAssetUrl) {
4167
+ res.statusCode = 302;
4168
+ res.setHeader('Location', externalAssetUrl);
4169
+ res.setHeader('Cache-Control', 'private, max-age=45');
4170
+ if (req.method === 'HEAD') {
4171
+ res.end();
4172
+ return;
4173
+ }
4174
+ res.end();
4175
+ return;
4176
+ }
4177
+
4178
+ if (decorated) {
4179
+ res.setHeader('X-Sticker-Category', String(decorated?.category || 'unknown'));
4180
+ res.setHeader('X-Sticker-NSFW', decorated?.is_nsfw ? '1' : '0');
4181
+ if (Array.isArray(decorated?.tags) && decorated.tags.length) {
4182
+ res.setHeader('X-Sticker-Tags', decorated.tags.join(','));
4183
+ }
4184
+ }
4185
+ if (previewVariant) {
4186
+ const previewCacheKey = [normalizedPackKey, normalizedStickerId, Number(item.asset?.size_bytes || 0), STICKER_PREVIEW_SIDE_PX, STICKER_PREVIEW_QUALITY].join(':');
4187
+ const previewBuffer = await generateStickerPreviewBuffer({
4188
+ sourceBuffer: buffer,
4189
+ mimetype: item.asset?.mimetype || 'image/webp',
4190
+ cacheKey: previewCacheKey,
4191
+ }).catch((previewError) => {
4192
+ logger.warn('Falha ao gerar preview de sticker para catálogo.', {
4193
+ action: 'sticker_catalog_preview_generate_failed',
4194
+ pack_key: normalizedPackKey,
4195
+ sticker_id: normalizedStickerId,
4196
+ error: previewError?.message,
4197
+ });
4198
+ return null;
4199
+ });
4200
+ if (previewBuffer && previewBuffer.length) {
4201
+ res.setHeader('X-Sticker-Preview', '1');
4202
+ sendAsset(req, res, previewBuffer, 'image/webp', `public, max-age=${IMMUTABLE_ASSET_CACHE_SECONDS}, immutable`);
4203
+ return;
4204
+ }
4205
+ }
4206
+ sendAsset(req, res, buffer, item.asset.mimetype || 'image/webp');
4207
+ } catch (error) {
4208
+ logger.warn('Falha ao ler asset de sticker para rota web.', {
4209
+ action: 'sticker_catalog_asset_read_failed',
4210
+ pack_key: normalizedPackKey,
4211
+ sticker_id: normalizedStickerId,
4212
+ error: error?.message,
4213
+ });
4214
+ sendJson(req, res, 404, { error: 'Arquivo de sticker indisponivel.' });
4215
+ }
4216
+ };
4217
+
4218
+ const findPublicPackByKey = async (rawPackKey) => {
4219
+ const normalizedPackKey = sanitizeText(rawPackKey, 160, { allowEmpty: false });
4220
+ if (!normalizedPackKey) return null;
4221
+ const pack = await findStickerPackByPackKey(normalizedPackKey);
4222
+ if (!pack || !isPackPubliclyVisible(pack)) return null;
4223
+ return pack;
4224
+ };
4225
+
4226
+ const handlePackInteractionRequest = async (req, res, packKey, interaction, url) => {
4227
+ const pack = await findPublicPackByKey(packKey);
4228
+ if (!pack) {
4229
+ sendJson(req, res, 404, { error: 'Pack nao encontrado.' });
4230
+ return;
4231
+ }
4232
+
4233
+ let engagement;
4234
+ if (interaction === 'open') {
4235
+ engagement = await incrementStickerPackOpen(pack.id);
4236
+ } else if (interaction === 'like') {
4237
+ engagement = await incrementStickerPackLike(pack.id);
4238
+ } else if (interaction === 'dislike') {
4239
+ engagement = await incrementStickerPackDislike(pack.id);
4240
+ } else {
4241
+ sendJson(req, res, 400, { error: 'Interacao invalida.' });
4242
+ return;
4243
+ }
4244
+
4245
+ const actor = resolveActorKeysFromRequest(req, url);
4246
+ await createStickerPackInteractionEvent({
4247
+ packId: pack.id,
4248
+ interaction,
4249
+ actorKey: actor.actorKey,
4250
+ sessionKey: actor.sessionKey,
4251
+ source: actor.source,
4252
+ }).catch(() => null);
4253
+
4254
+ sendJson(req, res, 200, {
4255
+ data: {
4256
+ pack_key: pack.pack_key,
4257
+ interaction,
4258
+ engagement: {
4259
+ open_count: Number(engagement.open_count || 0),
4260
+ like_count: Number(engagement.like_count || 0),
4261
+ dislike_count: Number(engagement.dislike_count || 0),
4262
+ score: Number(engagement.score || 0),
4263
+ updated_at: toIsoOrNull(engagement.updated_at),
4264
+ },
4265
+ },
4266
+ });
4267
+ };
4268
+
4269
+ const { handleAdminPanelSessionRequest, handleAdminOverviewRequest, handleAdminUsersRequest, handleAdminForceLogoutRequest, handleAdminFeatureFlagsRequest, handleAdminOpsActionRequest, handleAdminSearchRequest, handleAdminExportRequest, handleAdminModeratorsRequest, handleAdminModeratorDeleteRequest, handleAdminPacksRequest, handleAdminPackDetailsRequest, handleAdminPackDeleteRequest, handleAdminPackStickerDeleteRequest, handleAdminGlobalStickerDeleteRequest, handleAdminBansRequest, handleAdminBanRevokeRequest } = createStickerCatalogAdminHandlersContext({
4270
+ executeQuery,
4271
+ tables: TABLES,
4272
+ logger,
4273
+ sendJson,
4274
+ readJsonBody,
4275
+ parseCookies,
4276
+ getCookieValuesFromRequest,
4277
+ appendSetCookie,
4278
+ buildCookieString,
4279
+ sanitizeText,
4280
+ normalizeGoogleSubject,
4281
+ normalizeEmail,
4282
+ normalizeJid,
4283
+ toIsoOrNull,
4284
+ toWhatsAppPhoneDigits,
4285
+ mapGoogleSessionResponseData,
4286
+ resolveGoogleWebSessionFromRequest,
4287
+ revokeGoogleWebSessionsByIdentity,
4288
+ getMarketplaceGlobalStatsCached,
4289
+ getSystemSummaryCached,
4290
+ getFeatureFlagsSnapshot,
4291
+ refreshFeatureFlags,
4292
+ listAdminBans,
4293
+ createAdminBanRecord,
4294
+ revokeAdminBanRecord,
4295
+ normalizeVisitPath,
4296
+ stickerWebPath: STICKER_WEB_PATH,
4297
+ findStickerPackByPackKey,
4298
+ stickerPackService,
4299
+ buildManagedPackResponseData,
4300
+ sendManagedMutationStatus,
4301
+ sendManagedPackMutationStatus,
4302
+ deleteManagedPackWithCleanup,
4303
+ mapStickerPackWebManageError,
4304
+ cleanupOrphanStickerAssets,
4305
+ invalidateStickerCatalogDerivedCaches,
4306
+ });
4307
+
4308
+ const catalogApiRouter = createCatalogApiRouter({
4309
+ apiBasePath: STICKER_API_BASE_PATH,
4310
+ orphanApiPath: STICKER_ORPHAN_API_PATH,
4311
+ sendJson,
4312
+ handlers: {
4313
+ handleCreatePackRequest,
4314
+ handleGoogleAuthSessionRequest,
4315
+ handleTermsAcceptanceRequest,
4316
+ handlePasswordLoginRequest,
4317
+ handlePasswordAuthRequest,
4318
+ handlePasswordRecoveryRequest,
4319
+ handlePasswordRecoveryVerifyRequest,
4320
+ handlePasswordRecoverySessionCreateRequest,
4321
+ handlePasswordRecoverySessionStatusRequest,
4322
+ handlePasswordRecoverySessionRequest,
4323
+ handlePasswordRecoverySessionVerifyRequest,
4324
+ handleMyProfileRequest,
4325
+ handleAdminPanelSessionRequest,
4326
+ handleListRequest,
4327
+ handleIntentCollectionsRequest,
4328
+ handleCreatorRankingRequest,
4329
+ handleRecommendationsRequest,
4330
+ handleMarketplaceStatsRequest,
4331
+ handleHomeBootstrapRequest,
4332
+ handleCreatePackConfigRequest,
4333
+ handleOrphanStickerListRequest,
4334
+ handleDataFileListRequest,
4335
+ handleSystemSummaryRequest,
4336
+ handleGitHubProjectSummaryRequest,
4337
+ handleGlobalRankingSummaryRequest,
4338
+ handleReadmeSummaryRequest,
4339
+ handleReadmeMarkdownRequest,
4340
+ handleSupportInfoRequest,
4341
+ handleBotContactInfoRequest,
4342
+ handleAdminOverviewRequest,
4343
+ handleAdminUsersRequest,
4344
+ handleAdminForceLogoutRequest,
4345
+ handleAdminFeatureFlagsRequest,
4346
+ handleAdminOpsActionRequest,
4347
+ handleAdminSearchRequest,
4348
+ handleAdminExportRequest,
4349
+ handleAdminModeratorsRequest,
4350
+ handleAdminModeratorDeleteRequest,
4351
+ handleAdminPacksRequest,
4352
+ handleAdminPackDetailsRequest,
4353
+ handleAdminPackDeleteRequest,
4354
+ handleAdminPackStickerDeleteRequest,
4355
+ handleAdminGlobalStickerDeleteRequest,
4356
+ handleAdminBansRequest,
4357
+ handleAdminBanRevokeRequest,
4358
+ handleDetailsRequest,
4359
+ handlePackInteractionRequest,
4360
+ handleManagedPackRequest,
4361
+ handleManagedPackCloneRequest,
4362
+ handleManagedPackCoverRequest,
4363
+ handleManagedPackReorderRequest,
4364
+ handleManagedPackAnalyticsRequest,
4365
+ handleManagedPackStickerCreateRequest,
4366
+ handleManagedPackStickerDeleteRequest,
4367
+ handleManagedPackStickerReplaceRequest,
4368
+ handlePackPublishStateRequest,
4369
+ handleFinalizePackRequest,
4370
+ handleUploadStickerToPackRequest,
4371
+ handleAssetRequest,
4372
+ },
4373
+ });
4374
+
4375
+ const handleCatalogApiRequest = async (req, res, pathname, url) => catalogApiRouter({ req, res, pathname, url });
4376
+
4377
+ const handleCatalogPageRequest = async (req, res, pathname) => {
4378
+ const normalizedPath = pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
4379
+ void trackWebVisitMetric(req, res, {
4380
+ pagePath: normalizedPath || STICKER_WEB_PATH,
4381
+ source: 'catalog_page',
4382
+ }).catch((error) => {
4383
+ logger.warn('Falha ao registrar visita de página do catálogo.', {
4384
+ action: 'web_visit_track_catalog_page_failed',
4385
+ error: error?.message,
4386
+ page_path: normalizedPath || STICKER_WEB_PATH,
4387
+ });
4388
+ });
4389
+
4390
+ if (normalizedPath === STICKER_CREATE_WEB_PATH) {
4391
+ try {
4392
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
4393
+ if (!googleSession?.ownerJid) {
4394
+ const requestUrl = new URL(req.url || `${STICKER_CREATE_WEB_PATH}/`, SITE_ORIGIN);
4395
+ const nextPath = `${requestUrl.pathname}${requestUrl.search}`;
4396
+ const loginRedirectUrl = new URL(`${STICKER_LOGIN_WEB_PATH}/`, SITE_ORIGIN);
4397
+ loginRedirectUrl.searchParams.set('next', nextPath);
4398
+
4399
+ res.statusCode = 302;
4400
+ res.setHeader('Location', `${loginRedirectUrl.pathname}${loginRedirectUrl.search}`);
4401
+ res.setHeader('Cache-Control', 'no-store');
4402
+ res.end();
4403
+ return;
4404
+ }
4405
+
4406
+ const html = await renderCreatePackHtml();
4407
+ sendText(req, res, 200, html, 'text/html; charset=utf-8');
4408
+ return;
4409
+ } catch (error) {
4410
+ if (error?.code === 'ENOENT') {
4411
+ sendJson(req, res, 404, { error: 'Template da criacao de packs nao encontrado.' });
4412
+ return;
4413
+ }
4414
+ logger.error('Falha ao renderizar pagina de criacao de packs.', {
4415
+ action: 'sticker_catalog_create_page_render_failed',
4416
+ path: pathname,
4417
+ error: error?.message,
4418
+ });
4419
+ sendJson(req, res, 500, { error: 'Falha interna ao renderizar criacao de packs.' });
4420
+ return;
4421
+ }
4422
+ }
4423
+
4424
+ const initialPackKey = extractPackKeyFromWebPath(pathname);
4425
+ const initialPackKeyNormalized = String(initialPackKey || '')
4426
+ .trim()
4427
+ .toLowerCase();
4428
+ const shouldRenderPackSeoPage = Boolean(initialPackKey && !PACK_PAGE_ROUTE_EXCLUSIONS.has(initialPackKeyNormalized));
4429
+
4430
+ if (shouldRenderPackSeoPage) {
4431
+ const safePackKey = sanitizeText(initialPackKey, 160, { allowEmpty: false });
4432
+ if (!safePackKey) {
4433
+ const notFoundHtml = renderPackNotFoundHtml(initialPackKey);
4434
+ sendText(req, res, 404, notFoundHtml, 'text/html; charset=utf-8');
4435
+ return;
4436
+ }
4437
+
4438
+ try {
4439
+ const packPayload = await fetchPublicPackPayload(safePackKey);
4440
+ if (!packPayload) {
4441
+ const notFoundHtml = renderPackNotFoundHtml(safePackKey);
4442
+ sendText(req, res, 404, notFoundHtml, 'text/html; charset=utf-8');
4443
+ return;
4444
+ }
4445
+
4446
+ const googleSession = await resolveGoogleWebSessionFromRequest(req);
4447
+ const hasNsfwAccess = Boolean(googleSession?.sub && googleSession?.ownerJid);
4448
+ const packSummary = toSummaryEntry(
4449
+ {
4450
+ pack: packPayload.pack,
4451
+ engagement: packPayload.engagement,
4452
+ signals: packPayload.signals,
4453
+ packClassification: packPayload.packClassification,
4454
+ },
4455
+ { hideSensitiveCover: !hasNsfwAccess },
4456
+ );
4457
+ const html = renderPackSeoHtml({ packSummary });
4458
+ sendText(req, res, 200, html, 'text/html; charset=utf-8');
4459
+ return;
4460
+ } catch (error) {
4461
+ logger.error('Falha ao renderizar pagina SEO de pack.', {
4462
+ action: 'sticker_catalog_pack_seo_page_render_failed',
4463
+ path: pathname,
4464
+ pack_key: safePackKey,
4465
+ error: error?.message,
4466
+ });
4467
+ const notFoundHtml = renderPackNotFoundHtml(safePackKey);
4468
+ sendText(req, res, 404, notFoundHtml, 'text/html; charset=utf-8');
4469
+ return;
4470
+ }
4471
+ }
4472
+
4473
+ try {
4474
+ const html = await renderCatalogHtml({ initialPackKey: '' });
4475
+ sendText(req, res, 200, html, 'text/html; charset=utf-8');
4476
+ } catch (error) {
4477
+ if (error?.code === 'ENOENT') {
4478
+ sendJson(req, res, 404, { error: 'Template do catalogo nao encontrado.' });
4479
+ return;
4480
+ }
4481
+
4482
+ logger.error('Falha ao renderizar pagina do catalogo de sticker packs.', {
4483
+ action: 'sticker_catalog_page_render_failed',
4484
+ path: pathname,
4485
+ error: error?.message,
4486
+ stack: error?.stack,
4487
+ });
4488
+ sendJson(req, res, 500, { error: `Falha interna ao renderizar catalogo: ${error?.message}` });
4489
+ }
4490
+ };
4491
+
4492
+ export const isStickerCatalogEnabled = () => STICKER_CATALOG_ENABLED;
4493
+ export const getStickerCatalogConfig = () => ({
4494
+ stickerCatalogEnabled: STICKER_CATALOG_ENABLED,
4495
+ stickerWebPath: STICKER_WEB_PATH,
4496
+ stickerApiBasePath: STICKER_API_BASE_PATH,
4497
+ stickerOrphanApiPath: STICKER_ORPHAN_API_PATH,
4498
+ stickerCreateWebPath: STICKER_CREATE_WEB_PATH,
4499
+ stickerLoginWebPath: STICKER_LOGIN_WEB_PATH,
4500
+ userProfileWebPath: USER_PROFILE_WEB_PATH,
4501
+ userPasswordResetWebPath: USER_PASSWORD_RESET_WEB_PATH,
4502
+ stickerDataPublicPath: STICKER_DATA_PUBLIC_PATH,
4503
+ stickerDataPublicDir: STICKER_DATA_PUBLIC_DIR,
4504
+ stickerWebAssetVersion: STICKER_WEB_ASSET_VERSION,
4505
+ catalogTemplatePath: CATALOG_TEMPLATE_PATH,
4506
+ createPackTemplatePath: CREATE_PACK_TEMPLATE_PATH,
4507
+ catalogStylesFilePath: CATALOG_STYLES_FILE_PATH,
4508
+ catalogScriptFilePath: CATALOG_SCRIPT_FILE_PATH,
4509
+ defaultListLimit: DEFAULT_LIST_LIMIT,
4510
+ maxListLimit: MAX_LIST_LIMIT,
4511
+ defaultOrphanListLimit: DEFAULT_ORPHAN_LIST_LIMIT,
4512
+ maxOrphanListLimit: MAX_ORPHAN_LIST_LIMIT,
4513
+ defaultDataListLimit: DEFAULT_DATA_LIST_LIMIT,
4514
+ maxDataListLimit: MAX_DATA_LIST_LIMIT,
4515
+ maxDataScanFiles: MAX_DATA_SCAN_FILES,
4516
+ assetCacheSeconds: ASSET_CACHE_SECONDS,
4517
+ staticTextCacheSeconds: STATIC_TEXT_CACHE_SECONDS,
4518
+ immutableAssetCacheSeconds: IMMUTABLE_ASSET_CACHE_SECONDS,
4519
+ stickerWebWhatsappMessageTemplate: STICKER_WEB_WHATSAPP_MESSAGE_TEMPLATE,
4520
+ packCommandPrefix: PACK_COMMAND_PREFIX,
4521
+ packCreateNameRegex: PACK_CREATE_NAME_REGEX,
4522
+ packCreateMaxNameLength: PACK_CREATE_MAX_NAME_LENGTH,
4523
+ packCreateMaxPublisherLength: PACK_CREATE_MAX_PUBLISHER_LENGTH,
4524
+ packCreateMaxDescriptionLength: PACK_CREATE_MAX_DESCRIPTION_LENGTH,
4525
+ packCreateMaxItems: PACK_CREATE_MAX_ITEMS,
4526
+ packCreateMaxPacksPerOwner: PACK_CREATE_MAX_PACKS_PER_OWNER,
4527
+ packWebEditTokenTtlMs: PACK_WEB_EDIT_TOKEN_TTL_MS,
4528
+ stickerWebGoogleClientId: STICKER_WEB_GOOGLE_CLIENT_ID,
4529
+ stickerWebGoogleAuthRequired: STICKER_WEB_GOOGLE_AUTH_REQUIRED,
4530
+ stickerWebGoogleSessionTtlMs: STICKER_WEB_GOOGLE_SESSION_TTL_MS,
4531
+ stickerCatalogOnlyClassified: STICKER_CATALOG_ONLY_CLASSIFIED,
4532
+ metricsEndpoint: process.env.METRICS_ENDPOINT,
4533
+ metricsToken: process.env.METRICS_TOKEN,
4534
+ metricsSummaryTimeoutMs: process.env.STICKER_SYSTEM_METRICS_TIMEOUT_MS,
4535
+ githubRepository: process.env.GITHUB_REPOSITORY,
4536
+ githubToken: process.env.GITHUB_TOKEN,
4537
+ githubProjectCacheSeconds: Number(process.env.GITHUB_PROJECT_CACHE_SECONDS || 300),
4538
+ userInternalApiToken: USER_INTERNAL_API_TOKEN,
4539
+ userInternalReadRequireAuth: USER_INTERNAL_READ_REQUIRE_AUTH,
4540
+ userContactEndpointRequireAuth: USER_CONTACT_ENDPOINT_REQUIRE_AUTH,
4541
+ homeBootstrapExposeContact: HOME_BOOTSTRAP_EXPOSE_CONTACT,
4542
+ adminPanelEmail: ADMIN_PANEL_EMAIL,
4543
+ globalRankRefreshSeconds: GLOBAL_RANK_REFRESH_SECONDS,
4544
+ catalogListCacheSeconds: CATALOG_LIST_CACHE_SECONDS,
4545
+ catalogCreatorRankingCacheSeconds: CATALOG_CREATOR_RANKING_CACHE_SECONDS,
4546
+ catalogPackPayloadCacheSeconds: CATALOG_PACK_PAYLOAD_CACHE_SECONDS,
4547
+ marketplaceGlobalStatsCacheSeconds: MARKETPLACE_GLOBAL_STATS_CACHE_SECONDS,
4548
+ homeMarketplaceStatsCacheSeconds: HOME_MARKETPLACE_STATS_CACHE_SECONDS,
4549
+ systemSummaryCacheSeconds: SYSTEM_SUMMARY_CACHE_SECONDS,
4550
+ readmeSummaryCacheSeconds: README_SUMMARY_CACHE_SECONDS,
4551
+ readmeMessageTypeSampleLimit: README_MESSAGE_TYPE_SAMPLE_LIMIT,
4552
+ readmeCommandPrefix: README_COMMAND_PREFIX,
4553
+ siteOrigin: SITE_ORIGIN,
4554
+ sitemapMaxPacks: SITEMAP_MAX_PACKS,
4555
+ sitemapCacheSeconds: SITEMAP_CACHE_SECONDS,
4556
+ seoDiscoveryLinkLimit: SEO_DISCOVERY_LINK_LIMIT,
4557
+ seoDiscoveryCacheSeconds: SEO_DISCOVERY_CACHE_SECONDS,
4558
+ nsfwStickerPlaceholderUrl: NSFW_STICKER_PLACEHOLDER_URL,
4559
+ maxStickerUploadBytes: MAX_STICKER_UPLOAD_BYTES,
4560
+ maxStickerSourceUploadBytes: MAX_STICKER_SOURCE_UPLOAD_BYTES,
4561
+ webVisitorCookieTtlSeconds: WEB_VISITOR_COOKIE_TTL_SECONDS,
4562
+ webSessionCookieTtlSeconds: WEB_SESSION_COOKIE_TTL_SECONDS,
4563
+ stickerPreviewSidePx: STICKER_PREVIEW_SIDE_PX,
4564
+ stickerPreviewQuality: STICKER_PREVIEW_QUALITY,
4565
+ stickerPreviewTimeoutMs: STICKER_PREVIEW_TIMEOUT_MS,
4566
+ stickerPreviewCacheTtlMs: STICKER_PREVIEW_CACHE_TTL_MS,
4567
+ stickerPreviewCacheMaxItems: STICKER_PREVIEW_CACHE_MAX_ITEMS,
4568
+ catalogStylesWebPath: CATALOG_STYLES_WEB_PATH,
4569
+ catalogScriptWebPath: CATALOG_SCRIPT_WEB_PATH,
4570
+ });
4571
+
4572
+ /**
4573
+ * Manipula rotas web/API de catalogo de sticker packs.
4574
+ *
4575
+ * @param {import('node:http').IncomingMessage} req Requisicao HTTP.
4576
+ * @param {import('node:http').ServerResponse} res Resposta HTTP.
4577
+ * @param {{ pathname: string, url: URL }} context Contexto parseado da URL.
4578
+ * @returns {Promise<boolean>} `true` quando a rota foi tratada.
4579
+ */
4580
+ export async function maybeHandleStickerCatalogRequest(req, res, { pathname, url }) {
4581
+ if (!STICKER_CATALOG_ENABLED) return false;
4582
+ if (!['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'].includes(req.method || '')) return false;
4583
+ if (maybeRedirectToCanonicalHost(req, res, url)) return true;
4584
+
4585
+ if (pathname === '/sitemap.xml') {
4586
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
4587
+ try {
4588
+ return await handleSitemapRequest(req, res);
4589
+ } catch (error) {
4590
+ logger.error('Falha ao gerar sitemap dinamico.', {
4591
+ action: 'sticker_catalog_sitemap_failed',
4592
+ error: error?.message,
4593
+ });
4594
+ sendJson(req, res, 500, { error: 'Falha ao gerar sitemap.' });
4595
+ return true;
4596
+ }
4597
+ }
4598
+
4599
+ if (hasPathPrefix(pathname, STICKER_DATA_PUBLIC_PATH)) {
4600
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
4601
+ return handlePublicDataAssetRequest(req, res, pathname);
4602
+ }
4603
+
4604
+ if (hasPathPrefix(pathname, STICKER_WEB_PATH)) {
4605
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
4606
+ const handledStaticAsset = await handleCatalogStaticAssetRequest(req, res, pathname);
4607
+ if (handledStaticAsset) return true;
4608
+
4609
+ await handleCatalogPageRequest(req, res, pathname);
4610
+ return true;
4611
+ }
4612
+
4613
+ if (pathname === MARKETPLACE_GLOBAL_STATS_API_PATH) {
4614
+ if (!['GET', 'HEAD'].includes(req.method || '')) return false;
4615
+ try {
4616
+ await handleMarketplaceGlobalStatsRequest(req, res);
4617
+ } catch (error) {
4618
+ logger.error('Erro ao processar API global do marketplace.', {
4619
+ action: 'marketplace_global_stats_api_error',
4620
+ path: pathname,
4621
+ error: error?.message,
4622
+ });
4623
+ sendJson(req, res, 500, { error: 'Falha interna ao processar a requisicao.' });
4624
+ }
4625
+ return true;
4626
+ }
4627
+
4628
+ const handleUserApiReadRoute = async (handler, action, { access = 'public' } = {}) => {
4629
+ if (!['GET', 'HEAD'].includes(req.method || '')) {
4630
+ sendJson(req, res, 405, { error: 'Metodo nao permitido.' });
4631
+ return true;
4632
+ }
4633
+
4634
+ let resolvedSession = null;
4635
+ if (access === 'internal') {
4636
+ const allowed = await requireInternalUserApiReadAccess(req, res);
4637
+ if (!allowed) return true;
4638
+ } else if (access === 'contact') {
4639
+ resolvedSession = await resolveGoogleWebSessionFromRequest(req).catch(() => null);
4640
+ const allowed = await requireContactUserApiReadAccess(req, res, resolvedSession);
4641
+ if (!allowed) return true;
4642
+ }
4643
+
4644
+ try {
4645
+ await handler({ session: resolvedSession });
4646
+ } catch (error) {
4647
+ logger.error('Erro ao processar rota utilitaria da API de usuario.', {
4648
+ action,
4649
+ path: pathname,
4650
+ error: error?.message,
4651
+ });
4652
+ sendJson(req, res, 500, { error: 'Falha interna ao processar a requisicao.' });
4653
+ }
4654
+ return true;
4655
+ };
4656
+
4657
+ if (pathname === `${USER_API_BASE_PATH}/home-bootstrap`) {
4658
+ return handleUserApiReadRoute(() => handleHomeBootstrapRequest(req, res, url), 'user_home_bootstrap_api_error');
4659
+ }
4660
+
4661
+ if (pathname === `${USER_API_BASE_PATH}/system-summary`) {
4662
+ return handleUserApiReadRoute(() => handleSystemSummaryRequest(req, res), 'user_system_summary_api_error', { access: 'internal' });
4663
+ }
4664
+
4665
+ if (pathname === `${USER_API_BASE_PATH}/project-summary`) {
4666
+ return handleUserApiReadRoute(() => handleGitHubProjectSummaryRequest(req, res), 'user_project_summary_api_error', { access: 'internal' });
4667
+ }
4668
+
4669
+ if (pathname === `${USER_API_BASE_PATH}/global-ranking-summary`) {
4670
+ return handleUserApiReadRoute(() => handleGlobalRankingSummaryRequest(req, res), 'user_global_ranking_summary_api_error');
4671
+ }
4672
+
4673
+ if (pathname === `${USER_API_BASE_PATH}/readme-markdown`) {
4674
+ return handleUserApiReadRoute(() => handleReadmeMarkdownRequest(req, res), 'user_readme_markdown_api_error', { access: 'internal' });
4675
+ }
4676
+
4677
+ if (pathname === `${USER_API_BASE_PATH}/support`) {
4678
+ return handleUserApiReadRoute(() => handleSupportInfoRequest(req, res), 'user_support_api_error', { access: 'contact' });
4679
+ }
4680
+
4681
+ if (pathname === `${USER_API_BASE_PATH}/bot-contact`) {
4682
+ return handleUserApiReadRoute(() => handleBotContactInfoRequest(req, res), 'user_bot_contact_api_error');
4683
+ }
4684
+
4685
+ if (hasPathPrefix(pathname, STICKER_API_BASE_PATH)) {
4686
+ try {
4687
+ return await handleCatalogApiRequest(req, res, pathname, url);
4688
+ } catch (error) {
4689
+ logger.error('Erro ao processar API de sticker packs.', {
4690
+ action: 'sticker_catalog_api_error',
4691
+ path: pathname,
4692
+ error: error?.message,
4693
+ });
4694
+ sendJson(req, res, 500, { error: 'Falha interna ao processar a requisicao.' });
4695
+ return true;
4696
+ }
4697
+ }
4698
+
4699
+ return false;
4700
+ }