@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,1148 @@
1
+ import logger from '#logger';
2
+ import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
3
+ import { getJidServer, isUserJid, normalizeJid } from '../../config/index.js';
4
+ import stickerPackService from './stickerPackServiceRuntime.js';
5
+ import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
6
+ import { captureIncomingStickerAsset, resolveStickerAssetForCommand } from './stickerStorageService.js';
7
+ import { buildStickerPackMessage, sendStickerPackWithFallback } from './stickerPackMessageService.js';
8
+ import { sanitizeText } from './stickerPackUtils.js';
9
+ import { executeQuery, TABLES } from '../../../database/index.js';
10
+ import { extractSenderInfoFromMessage, extractUserIdInfo, resolveUserId } from '../../config/index.js';
11
+ import { toWhatsAppPhoneDigits } from '../../services/auth/whatsappLoginLinkService.js';
12
+
13
+ /**
14
+ * Handlers de comando textual para gerenciamento de packs de figurinha.
15
+ */
16
+ const RATE_WINDOW_MS = Math.max(10_000, Number(process.env.STICKER_PACK_RATE_WINDOW_MS) || 60_000);
17
+ const RATE_MAX_ACTIONS = Math.max(1, Number(process.env.STICKER_PACK_RATE_MAX_ACTIONS) || 20);
18
+ const MAX_PACK_ITEMS = Math.max(1, Number(process.env.STICKER_PACK_MAX_ITEMS) || 30);
19
+ const MAX_PACK_NAME_LENGTH = 120;
20
+ const LID_SERVERS = new Set(['lid', 'hosted.lid']);
21
+
22
+ const rateMap = new Map();
23
+
24
+ /**
25
+ * Separa texto em subcomando e argumentos restantes.
26
+ *
27
+ * @param {string} text Texto bruto após `pack`.
28
+ * @returns {{ command: string, rest: string }} Partes do comando.
29
+ */
30
+ const extractCommandParts = (text) => {
31
+ const raw = String(text || '').trim();
32
+ if (!raw) return { command: '', rest: '' };
33
+
34
+ const firstSpace = raw.indexOf(' ');
35
+ if (firstSpace === -1) {
36
+ return { command: raw.toLowerCase(), rest: '' };
37
+ }
38
+
39
+ return {
40
+ command: raw.slice(0, firstSpace).toLowerCase(),
41
+ rest: raw.slice(firstSpace + 1).trim(),
42
+ };
43
+ };
44
+
45
+ /**
46
+ * Remove aspas simples/duplas de borda e trim.
47
+ *
48
+ * @param {unknown} value Valor textual.
49
+ * @returns {string} Texto sem aspas externas.
50
+ */
51
+ const unquote = (value) => {
52
+ const raw = String(value || '').trim();
53
+ if (!raw) return '';
54
+
55
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
56
+ return raw.slice(1, -1).trim();
57
+ }
58
+
59
+ return raw;
60
+ };
61
+
62
+ /**
63
+ * Lê o primeiro token (com suporte a aspas) e retorna o restante.
64
+ *
65
+ * @param {string} input Texto de entrada.
66
+ * @returns {{ token: string|null, rest: string }} Token e restante.
67
+ */
68
+ const readToken = (input) => {
69
+ const raw = String(input || '').trim();
70
+ if (!raw) return { token: null, rest: '' };
71
+
72
+ const match = raw.match(/^("([^"\\]|\\.)*"|'([^'\\]|\\.)*'|\S+)([\s\S]*)$/);
73
+ if (!match) return { token: null, rest: '' };
74
+
75
+ return {
76
+ token: unquote(match[1]),
77
+ rest: (match[4] || '').trim(),
78
+ };
79
+ };
80
+
81
+ /**
82
+ * Divide argumentos por `|`, removendo segmentos vazios.
83
+ *
84
+ * @param {unknown} value Texto com opções pipe.
85
+ * @returns {string[]} Segmentos limpos.
86
+ */
87
+ const splitPipeSegments = (value) =>
88
+ String(value || '')
89
+ .split('|')
90
+ .map((segment) => segment.trim())
91
+ .filter(Boolean);
92
+
93
+ /**
94
+ * Converte segmentos `chave=valor` em objeto de opções.
95
+ *
96
+ * @param {string[]} segments Segmentos já divididos por pipe.
97
+ * @returns {Record<string, string>} Mapa de opções.
98
+ */
99
+ const parsePipeOptions = (segments) => {
100
+ const options = {};
101
+
102
+ for (const segment of segments) {
103
+ const eqIndex = segment.indexOf('=');
104
+ if (eqIndex <= 0) continue;
105
+
106
+ const key = segment.slice(0, eqIndex).trim().toLowerCase();
107
+ const value = unquote(segment.slice(eqIndex + 1));
108
+
109
+ if (!key) continue;
110
+ options[key] = value;
111
+ }
112
+
113
+ return options;
114
+ };
115
+
116
+ /**
117
+ * Envia resposta textual no chat preservando contexto/ephemeral.
118
+ *
119
+ * @param {{ sock: object, remoteJid: string, messageInfo: object, expirationMessage: number|undefined, text: string }} params Contexto de envio.
120
+ * @returns {Promise<object>} Resultado de envio.
121
+ */
122
+ const sendReply = async ({ sock, remoteJid, messageInfo, expirationMessage, text }) =>
123
+ sendAndStore(
124
+ sock,
125
+ remoteJid,
126
+ { text },
127
+ {
128
+ quoted: messageInfo,
129
+ ephemeralExpiration: expirationMessage,
130
+ },
131
+ );
132
+
133
+ const PACK_VISUAL_DIVIDER = '━━━━━━━━━━━━━━━━━━━';
134
+ const PACK_VISUAL_HEADER = '📦 *PACKS DE FIGURINHAS — CENTRAL DE GERENCIAMENTO*';
135
+
136
+ /**
137
+ * Normaliza blocos de texto para linhas não vazias.
138
+ *
139
+ * @param {unknown} value Texto, lista de textos ou aninhamento.
140
+ * @returns {string[]} Linhas prontas para renderização.
141
+ */
142
+ const normalizeMessageLines = (value) => {
143
+ if (value === null || value === undefined) return [];
144
+ if (Array.isArray(value)) {
145
+ return value.flatMap((item) => normalizeMessageLines(item));
146
+ }
147
+
148
+ const line = String(value).trim();
149
+ return line ? [line] : [];
150
+ };
151
+
152
+ /**
153
+ * Formata rótulo visual de visibilidade do pack.
154
+ *
155
+ * @param {string} visibility Visibilidade interna.
156
+ * @returns {string} Rótulo amigável com ícone.
157
+ */
158
+ const formatVisibilityLabel = (visibility) => {
159
+ const normalized = String(visibility || '').toLowerCase();
160
+ if (normalized === 'public') return '🌍 Público';
161
+ if (normalized === 'unlisted') return '🔗 Não listado';
162
+ return '🔒 Privado';
163
+ };
164
+
165
+ /**
166
+ * Detecta packs automáticos de curadoria temática para ocultar em listagens padrão.
167
+ * Mantém visível o auto-pack coletor do usuário (ex.: "minhasfigurinhas1").
168
+ *
169
+ * @param {object|null|undefined} pack Pack retornado pelo serviço.
170
+ * @returns {boolean} Verdadeiro quando for auto-pack temático/curadoria.
171
+ */
172
+ const isThemeCurationPack = (pack) => {
173
+ if (!pack || typeof pack !== 'object') return false;
174
+
175
+ const name = String(pack.name || '').trim();
176
+ if (/^\[auto\]/i.test(name)) return true;
177
+
178
+ const description = String(pack.description || '').toLowerCase();
179
+ if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
180
+
181
+ const themeKey = String(pack.pack_theme_key || '').trim();
182
+ return Boolean(themeKey);
183
+ };
184
+
185
+ /**
186
+ * Monta mensagem visual padronizada para comandos de pack.
187
+ *
188
+ * @param {{ intro?: unknown[], sections?: Array<{title?: string, lines?: unknown[]}>, footer?: unknown[] }} params Blocos da mensagem.
189
+ * @returns {string} Texto final formatado.
190
+ */
191
+ const buildPackVisualMessage = ({ intro = [], sections = [], footer = [] }) => {
192
+ const lines = [PACK_VISUAL_HEADER];
193
+ const introLines = normalizeMessageLines(intro);
194
+ if (introLines.length) {
195
+ lines.push('', ...introLines);
196
+ }
197
+
198
+ for (const section of sections) {
199
+ if (!section) continue;
200
+ const title = section.title ? String(section.title).trim() : '';
201
+ const sectionLines = normalizeMessageLines(section.lines);
202
+ if (!title && !sectionLines.length) continue;
203
+
204
+ lines.push('', PACK_VISUAL_DIVIDER);
205
+ if (title) lines.push(title);
206
+ if (sectionLines.length) {
207
+ lines.push('', ...sectionLines);
208
+ }
209
+ }
210
+
211
+ const footerLines = normalizeMessageLines(footer);
212
+ if (footerLines.length) {
213
+ lines.push('', PACK_VISUAL_DIVIDER, ...footerLines);
214
+ }
215
+
216
+ return lines.join('\n');
217
+ };
218
+
219
+ /**
220
+ * Monta template visual para ações de sucesso/instrução.
221
+ *
222
+ * @param {{ title: string, explanation?: unknown[], details?: unknown[], nextSteps?: unknown[], footer?: unknown[] }} params Dados da mensagem.
223
+ * @returns {string} Texto final.
224
+ */
225
+ const buildActionMessage = ({ title, explanation = [], details = [], nextSteps = [], footer = [] }) =>
226
+ buildPackVisualMessage({
227
+ intro: [title, ...normalizeMessageLines(explanation)],
228
+ sections: [normalizeMessageLines(details).length ? { title: '📌 *DETALHES*', lines: details } : null, normalizeMessageLines(nextSteps).length ? { title: '➡️ *PRÓXIMAS AÇÕES*', lines: nextSteps } : null],
229
+ footer,
230
+ });
231
+
232
+ /**
233
+ * Renderiza listagem de packs em formato amigável para chat.
234
+ *
235
+ * @param {object[]} packs Lista de packs do usuário.
236
+ * @param {string} prefix Prefixo de comando.
237
+ * @returns {string} Mensagem formatada.
238
+ */
239
+ const formatPackList = (packs, prefix) => {
240
+ if (!packs.length) {
241
+ return buildPackVisualMessage({
242
+ intro: ['📭 *Nenhum pack extra encontrado.*', 'As figurinhas que você cria continuam sendo salvas automaticamente no seu *Pack Principal*.'],
243
+ sections: [
244
+ {
245
+ title: '🆕 *COMECE EM 3 PASSOS*',
246
+ lines: [`1) Crie um pack: \`${prefix}pack create meupack\``, `2) Responda uma figurinha e adicione: \`${prefix}pack add <pack>\``, `3) Veja o resumo: \`${prefix}pack info <pack>\``],
247
+ },
248
+ ],
249
+ footer: ['💡 Dica: crie packs por tema (memes, animes, reactions) para achar tudo mais rápido.'],
250
+ });
251
+ }
252
+
253
+ const lines = packs.map((pack, index) => {
254
+ const count = Number(pack.sticker_count || 0);
255
+ return [`${index + 1}. *${pack.name}*`, ` 🆔 ID: \`${pack.pack_key}\``, ` 🧩 Itens: ${count}/${MAX_PACK_ITEMS}`, ` 👁️ Visibilidade: ${formatVisibilityLabel(pack.visibility)}`].join('\n');
256
+ });
257
+
258
+ return buildPackVisualMessage({
259
+ intro: [`📋 *Packs encontrados: ${packs.length}*`, 'Você pode usar o *nome* ou o *ID* do pack para ver detalhes, editar ou enviar.'],
260
+ sections: [
261
+ { title: '📦 *SEUS PACKS*', lines },
262
+ {
263
+ title: '🛠 *ATALHOS*',
264
+ lines: [`ℹ️ Detalhes: \`${prefix}pack info <pack>\``, `📤 Enviar: \`${prefix}pack send <pack>\``, `🆕 Criar novo: \`${prefix}pack create meupack\``],
265
+ },
266
+ ],
267
+ footer: ['✅ Tudo pronto — escolha um pack e continue gerenciando.'],
268
+ });
269
+ };
270
+
271
+ /**
272
+ * Renderiza detalhes completos de um pack com preview de itens.
273
+ *
274
+ * @param {{ items: object[], cover_sticker_id?: string, name: string, pack_key: string, publisher: string, visibility: string, description?: string }} pack Pack completo.
275
+ * @param {string} prefix Prefixo de comando.
276
+ * @returns {string} Mensagem formatada.
277
+ */
278
+ const formatPackInfo = (pack, prefix) => {
279
+ const coverIndex = pack.items.findIndex((item) => item.sticker_id === pack.cover_sticker_id);
280
+ const coverLabel = coverIndex >= 0 ? `figurinha #${coverIndex + 1}` : 'não definida';
281
+ const itemLines = pack.items.slice(0, 12).map((item, index) => {
282
+ const emojis = Array.isArray(item.emojis) && item.emojis.length ? ` ${item.emojis.join(' ')}` : '';
283
+ const coverTag = item.sticker_id === pack.cover_sticker_id ? ' 🖼️ *Capa*' : '';
284
+ return `${index + 1}. \`${item.sticker_id.slice(0, 10)}\`${emojis}${coverTag}`;
285
+ });
286
+
287
+ if (pack.items.length > 12) {
288
+ itemLines.push(`… e mais ${pack.items.length - 12} figurinha(s).`);
289
+ }
290
+
291
+ return buildPackVisualMessage({
292
+ intro: [`ℹ️ *Informações do pack: "${pack.name}"*`, 'Aqui você vê identificação, visibilidade e uma prévia dos itens cadastrados.'],
293
+ sections: [
294
+ {
295
+ title: '📌 *DADOS DO PACK*',
296
+ lines: [`📛 Nome: *${pack.name}*`, `🆔 ID: \`${pack.pack_key}\``, `👤 Publisher: *${pack.publisher}*`, `👁️ Visibilidade: ${formatVisibilityLabel(pack.visibility)}`, `🧩 Itens: *${pack.items.length}/${MAX_PACK_ITEMS}*`, `🖼️ Capa: *${coverLabel}*`, `📝 Descrição: ${pack.description ? `"${pack.description}"` : 'não definida'}`],
297
+ },
298
+ {
299
+ title: '🖼️ *PRÉVIA (ATÉ 12 ITENS)*',
300
+ lines: itemLines.length ? itemLines : ['Nenhuma figurinha cadastrada neste pack ainda.'],
301
+ },
302
+ {
303
+ title: '⚙️ *AÇÕES DISPONÍVEIS*',
304
+ lines: [`➕ Adicionar: \`${prefix}pack add ${pack.pack_key}\``, `🖼 Definir capa: \`${prefix}pack setcover ${pack.pack_key}\``, `🔀 Reordenar: \`${prefix}pack reorder ${pack.pack_key} 1 2 3 ...\``, `📤 Enviar: \`${prefix}pack send ${pack.pack_key}\``],
305
+ },
306
+ ],
307
+ footer: ['💡 Se precisar, use o guia completo com `pack` para ver exemplos e comandos extras.'],
308
+ });
309
+ };
310
+
311
+ /**
312
+ * Retorna texto de ajuda principal dos comandos de pack.
313
+ *
314
+ * @param {string} prefix Prefixo de comando.
315
+ * @returns {string} Guia textual.
316
+ */
317
+ const buildPackHelp = (prefix) => ['📦 *PACKS DE FIGURINHAS — GUIA RÁPIDO*', '', 'Toda figurinha que você criar é salva automaticamente no seu *Pack Principal*.', 'Além disso, você pode criar packs extras para organizar por tema e enviar mais rápido.', '', PACK_VISUAL_DIVIDER, '🧭 *COMANDOS PRINCIPAIS*', '', '🆕 Criar um pack', `\`${prefix}pack create "Meus memes 😂" | publisher="Seu Nome" | desc="Descrição"\``, '_Nome livre: espaços e emojis são permitidos._', '', '📋 Listar packs', `\`${prefix}pack list\``, '', 'ℹ️ Ver detalhes do pack', `\`${prefix}pack info <pack>\``, '', '➕ Adicionar figurinha', `\`${prefix}pack add <pack>\``, '_Dica: responda uma figurinha (ou use a última enviada)._', '', '🖼 Definir capa', `\`${prefix}pack setcover <pack>\``, '', '📤 Enviar pack no chat', `\`${prefix}pack send "<nome do pack>"\``, `_Ou use o ID: \`${prefix}pack send <pack_id>\`_`, '', PACK_VISUAL_DIVIDER, '🧰 *COMANDOS EXTRAS*', '', '`rename` • `setpub` • `setdesc` • `remove` • `reorder` • `clone` • `publish` • `delete`', '', PACK_VISUAL_DIVIDER, '✅ *Pronto!* Se quiser, diga o que você quer fazer (criar, organizar, enviar) que eu te guio.'].join('\n');
318
+
319
+ /**
320
+ * Template visual de erro orientado a resolução.
321
+ *
322
+ * @param {{ title: string, explanation?: unknown[], steps?: unknown[], commandPrefix: string }} params Conteúdo do erro.
323
+ * @returns {string} Texto formatado.
324
+ */
325
+ const buildErrorMessage = ({ title, explanation = [], steps = [], commandPrefix }) =>
326
+ buildPackVisualMessage({
327
+ intro: [title, ...normalizeMessageLines(explanation)],
328
+ sections: [
329
+ normalizeMessageLines(steps).length
330
+ ? {
331
+ title: '🧭 *COMO RESOLVER*',
332
+ lines: steps,
333
+ }
334
+ : null,
335
+ ],
336
+ footer: [`💡 Guia completo: \`${commandPrefix}pack\``],
337
+ });
338
+
339
+ /**
340
+ * Converte erro interno em mensagem amigável para usuário.
341
+ *
342
+ * @param {unknown} error Erro recebido durante o comando.
343
+ * @param {string} commandPrefix Prefixo configurado.
344
+ * @returns {string} Mensagem final de erro.
345
+ */
346
+ const formatErrorMessage = (error, commandPrefix) => {
347
+ if (!(error instanceof StickerPackError)) {
348
+ return buildErrorMessage({
349
+ title: '❌ *Não consegui concluir sua solicitação.*',
350
+ explanation: ['Ocorreu um erro inesperado ao processar o comando.'],
351
+ steps: ['Aguarde alguns segundos e tente novamente.'],
352
+ commandPrefix,
353
+ });
354
+ }
355
+
356
+ switch (error.code) {
357
+ case STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND:
358
+ return buildErrorMessage({
359
+ title: '🔎 *Pack não encontrado.*',
360
+ explanation: ['Não localizei um pack com esse nome ou ID.'],
361
+ steps: [`Veja a lista com \`${commandPrefix}pack list\`.`, 'Copie o ID exatamente como aparece.', `Depois tente novamente (ex.: \`${commandPrefix}pack info <pack>\`).`],
362
+ commandPrefix,
363
+ });
364
+ case STICKER_PACK_ERROR_CODES.DUPLICATE_STICKER:
365
+ return buildErrorMessage({
366
+ title: '⚠️ *Essa figurinha já está no pack.*',
367
+ explanation: ['Para manter o pack organizado, não adiciono itens duplicados.'],
368
+ steps: [`Veja os itens com \`${commandPrefix}pack info <pack>\`.`, 'Se quiser reorganizar, use `reorder`.'],
369
+ commandPrefix,
370
+ });
371
+ case STICKER_PACK_ERROR_CODES.PACK_LIMIT_REACHED:
372
+ return buildErrorMessage({
373
+ title: '⚠️ *Limite de figurinhas atingido.*',
374
+ explanation: [error.message || 'Este pack já está no limite e não aceita novos itens no momento.'],
375
+ steps: [`Crie outro pack: \`${commandPrefix}pack create novopack\`.`, 'Depois continue adicionando as próximas figurinhas no novo pack.'],
376
+ commandPrefix,
377
+ });
378
+ case STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND:
379
+ return buildErrorMessage({
380
+ title: '🧩 *Não encontrei uma figurinha válida para usar.*',
381
+ explanation: ['Para esse comando, você precisa responder uma figurinha ou ter uma figurinha recente no contexto.'],
382
+ steps: ['Responda diretamente a figurinha que você quer usar.', 'Ou envie uma figurinha e execute o comando novamente.'],
383
+ commandPrefix,
384
+ });
385
+ case STICKER_PACK_ERROR_CODES.INVALID_INPUT:
386
+ return buildErrorMessage({
387
+ title: '⚠️ *Formato do comando inválido.*',
388
+ explanation: [error.message || 'Revise o formato do comando e tente novamente.'],
389
+ steps: [`Abra os exemplos: \`${commandPrefix}pack\`.`],
390
+ commandPrefix,
391
+ });
392
+ case STICKER_PACK_ERROR_CODES.STORAGE_ERROR:
393
+ return buildErrorMessage({
394
+ title: '💾 *Falha ao acessar os dados do pack.*',
395
+ explanation: [error.message || 'Os arquivos não ficaram disponíveis agora.'],
396
+ steps: ['Tente novamente em instantes. Se persistir, envie o comando usado para eu analisar.'],
397
+ commandPrefix,
398
+ });
399
+ default:
400
+ return buildErrorMessage({
401
+ title: '❌ *Erro ao gerenciar packs.*',
402
+ explanation: [error.message || 'Ocorreu um erro interno durante a operação.'],
403
+ steps: ['Tente novamente e, se continuar, compartilhe o comando usado para análise.'],
404
+ commandPrefix,
405
+ });
406
+ }
407
+ };
408
+
409
+ /**
410
+ * Aplica rate limit por usuário para comandos de pack.
411
+ *
412
+ * @param {string} ownerJid JID do dono do comando.
413
+ * @returns {{ limited: boolean, remainingMs: number }} Estado do limite.
414
+ */
415
+ const checkRateLimit = (ownerJid) => {
416
+ const now = Date.now();
417
+ const entry = rateMap.get(ownerJid);
418
+
419
+ if (!entry || entry.resetAt <= now) {
420
+ rateMap.set(ownerJid, {
421
+ count: 1,
422
+ resetAt: now + RATE_WINDOW_MS,
423
+ });
424
+ return { limited: false, remainingMs: 0 };
425
+ }
426
+
427
+ if (entry.count >= RATE_MAX_ACTIONS) {
428
+ return {
429
+ limited: true,
430
+ remainingMs: entry.resetAt - now,
431
+ };
432
+ }
433
+
434
+ entry.count += 1;
435
+ rateMap.set(ownerJid, entry);
436
+ return { limited: false, remainingMs: 0 };
437
+ };
438
+
439
+ /**
440
+ * Lê identificador inicial e restante textual do comando.
441
+ *
442
+ * @param {string} input Texto de entrada.
443
+ * @returns {{ identifier: string|null, value: string }} Partes parseadas.
444
+ */
445
+ const parseIdentifierAndValue = (input) => {
446
+ const { token: identifier, rest } = readToken(input);
447
+ return {
448
+ identifier,
449
+ value: unquote(rest),
450
+ };
451
+ };
452
+
453
+ const readSingleArgument = (input) => {
454
+ const value = unquote(input);
455
+ return value ? value : null;
456
+ };
457
+
458
+ const buildOwnerLookupJids = (value) => {
459
+ const normalized = normalizeJid(value) || '';
460
+ if (!normalized || !normalized.includes('@')) return [];
461
+ const lookup = new Set([normalized]);
462
+ const digits = toWhatsAppPhoneDigits(normalized);
463
+ if (!digits) return Array.from(lookup);
464
+ lookup.add(normalizeJid(`${digits}@s.whatsapp.net`) || '');
465
+ lookup.add(normalizeJid(`${digits}@c.us`) || '');
466
+ lookup.add(normalizeJid(`${digits}@hosted`) || '');
467
+ return Array.from(lookup).filter(Boolean);
468
+ };
469
+
470
+ const appendOwnerCandidate = (candidateSet, lookupSet, value) => {
471
+ const normalized = normalizeJid(value) || '';
472
+ if (!normalized || !normalized.includes('@')) return;
473
+ candidateSet.add(normalized);
474
+ for (const lookupJid of buildOwnerLookupJids(normalized)) {
475
+ lookupSet.add(lookupJid);
476
+ }
477
+ };
478
+
479
+ const dedupePacksById = (packs = []) => {
480
+ const dedup = new Map();
481
+ for (const pack of Array.isArray(packs) ? packs : []) {
482
+ if (!pack?.id) continue;
483
+ const existing = dedup.get(pack.id);
484
+ if (!existing) {
485
+ dedup.set(pack.id, pack);
486
+ continue;
487
+ }
488
+ const currentUpdatedAt = Date.parse(String(pack.updated_at || pack.created_at || ''));
489
+ const existingUpdatedAt = Date.parse(String(existing.updated_at || existing.created_at || ''));
490
+ if (Number.isFinite(currentUpdatedAt) && (!Number.isFinite(existingUpdatedAt) || currentUpdatedAt > existingUpdatedAt)) {
491
+ dedup.set(pack.id, pack);
492
+ }
493
+ }
494
+
495
+ return Array.from(dedup.values()).sort((a, b) => {
496
+ const aUpdatedAt = Date.parse(String(a?.updated_at || a?.created_at || ''));
497
+ const bUpdatedAt = Date.parse(String(b?.updated_at || b?.created_at || ''));
498
+ if (!Number.isFinite(aUpdatedAt) && !Number.isFinite(bUpdatedAt)) return 0;
499
+ if (!Number.isFinite(aUpdatedAt)) return 1;
500
+ if (!Number.isFinite(bUpdatedAt)) return -1;
501
+ return bUpdatedAt - aUpdatedAt;
502
+ });
503
+ };
504
+
505
+ const resolveOwnerCandidatesForPackCommand = async ({ senderJid, messageInfo }) => {
506
+ const candidates = new Set();
507
+ const lookupByJid = new Set();
508
+
509
+ const senderInfo = extractSenderInfoFromMessage(messageInfo);
510
+ appendOwnerCandidate(candidates, lookupByJid, senderJid);
511
+ appendOwnerCandidate(candidates, lookupByJid, senderInfo?.jid);
512
+ appendOwnerCandidate(candidates, lookupByJid, senderInfo?.participantAlt);
513
+ appendOwnerCandidate(candidates, lookupByJid, senderInfo?.lid);
514
+
515
+ const directResolved = await resolveUserId(extractUserIdInfo(senderJid)).catch(() => null);
516
+ if (directResolved) {
517
+ appendOwnerCandidate(candidates, lookupByJid, directResolved);
518
+ }
519
+
520
+ const senderResolved = await resolveUserId({
521
+ lid: senderInfo?.lid,
522
+ jid: senderInfo?.jid || senderJid || null,
523
+ participantAlt: senderInfo?.participantAlt || null,
524
+ }).catch(() => null);
525
+ if (senderResolved) {
526
+ appendOwnerCandidate(candidates, lookupByJid, senderResolved);
527
+ }
528
+
529
+ const lookupValues = Array.from(lookupByJid).filter(Boolean);
530
+ for (let offset = 0; offset < lookupValues.length; offset += 200) {
531
+ const chunk = lookupValues.slice(offset, offset + 200);
532
+ if (!chunk.length) continue;
533
+ const placeholders = chunk.map(() => '?').join(', ');
534
+ const lookupParams = [...chunk, ...chunk];
535
+ const rows = await executeQuery(
536
+ `SELECT lid, jid
537
+ FROM ${TABLES.LID_MAP}
538
+ WHERE jid IN (${placeholders})
539
+ OR lid IN (${placeholders})
540
+ ORDER BY last_seen DESC
541
+ LIMIT 500`,
542
+ lookupParams,
543
+ ).catch(() => []);
544
+
545
+ for (const row of Array.isArray(rows) ? rows : []) {
546
+ appendOwnerCandidate(candidates, lookupByJid, row?.jid || '');
547
+ appendOwnerCandidate(candidates, lookupByJid, row?.lid || '');
548
+ }
549
+ }
550
+
551
+ const lidCandidates = Array.from(candidates).filter((candidate) => LID_SERVERS.has(getJidServer(candidate)));
552
+ for (const lidValue of lidCandidates) {
553
+ const resolved = await resolveUserId(extractUserIdInfo(lidValue)).catch(() => null);
554
+ if (resolved) {
555
+ appendOwnerCandidate(candidates, lookupByJid, resolved);
556
+ }
557
+ }
558
+
559
+ return Array.from(candidates);
560
+ };
561
+
562
+ const pickPrimaryOwnerCandidate = (ownerCandidates, senderJid) => {
563
+ const preferred = (Array.isArray(ownerCandidates) ? ownerCandidates : []).find((candidate) => {
564
+ const server = getJidServer(candidate);
565
+ if (!server || LID_SERVERS.has(server)) return false;
566
+ return server !== 'google.oauth';
567
+ });
568
+ if (preferred) return preferred;
569
+
570
+ const normalizedSender = normalizeJid(senderJid) || '';
571
+ if (normalizedSender) return normalizedSender;
572
+ return Array.isArray(ownerCandidates) && ownerCandidates.length ? ownerCandidates[0] : senderJid;
573
+ };
574
+
575
+ const runWithOwnerFallback = async (ownerCandidates, action) => {
576
+ const owners = Array.isArray(ownerCandidates) && ownerCandidates.length ? ownerCandidates : [];
577
+ let notFoundError = null;
578
+ for (const candidateOwner of owners) {
579
+ try {
580
+ return await action(candidateOwner);
581
+ } catch (error) {
582
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
583
+ notFoundError = notFoundError || error;
584
+ continue;
585
+ }
586
+ throw error;
587
+ }
588
+ }
589
+
590
+ if (notFoundError) throw notFoundError;
591
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND, 'Pack não encontrado para este usuário.');
592
+ };
593
+
594
+ /**
595
+ * Normaliza e valida nome de pack (permite espaços e emojis).
596
+ *
597
+ * @param {string} value Nome informado.
598
+ * @param {{ label?: string }} [options] Label para mensagens de erro.
599
+ * @returns {string} Nome normalizado.
600
+ * @throws {StickerPackError} Quando o nome estiver vazio.
601
+ */
602
+ const normalizePackName = (value, { label = 'Nome do pack' } = {}) => {
603
+ const normalized = sanitizeText(unquote(value), MAX_PACK_NAME_LENGTH, { allowEmpty: false });
604
+
605
+ if (!normalized) {
606
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.INVALID_INPUT, `${label} é obrigatório.`);
607
+ }
608
+
609
+ return normalized;
610
+ };
611
+
612
+ /**
613
+ * Converte entrada de reordenação em lista de sticker IDs.
614
+ *
615
+ * @param {{ ownerJid: string, identifier: string, rawOrder: string }} params Contexto de reordenação.
616
+ * @returns {Promise<string[]>} IDs na ordem desejada.
617
+ */
618
+ const parseReorderInput = async ({ ownerJid, identifier, rawOrder }) => {
619
+ const tokens = String(rawOrder || '')
620
+ .split(/[\s,]+/)
621
+ .map((item) => item.trim())
622
+ .filter(Boolean);
623
+
624
+ if (!tokens.length) {
625
+ return [];
626
+ }
627
+
628
+ const onlyNumbers = tokens.every((token) => /^\d+$/.test(token));
629
+ if (!onlyNumbers) {
630
+ return tokens;
631
+ }
632
+
633
+ const pack = await stickerPackService.getPackInfo({ ownerJid, identifier });
634
+ const ids = [];
635
+
636
+ for (const token of tokens) {
637
+ const index = Number(token);
638
+ const item = pack.items[index - 1];
639
+ if (item?.sticker_id) ids.push(item.sticker_id);
640
+ }
641
+
642
+ return ids;
643
+ };
644
+
645
+ /**
646
+ * Resolve sticker a partir do contexto atual (quoted/último sticker).
647
+ *
648
+ * @param {{ messageInfo: object, ownerJid: string, includeQuoted?: boolean }} params Contexto da mensagem.
649
+ * @returns {Promise<object|null>} Asset resolvido.
650
+ */
651
+ const resolveStickerFromCommandContext = async ({ messageInfo, ownerJid, includeQuoted = true }) => {
652
+ return resolveStickerAssetForCommand({
653
+ messageInfo,
654
+ ownerJid,
655
+ includeQuoted,
656
+ fallbackToLast: true,
657
+ });
658
+ };
659
+
660
+ /**
661
+ * Handler principal do comando `pack` e seus subcomandos.
662
+ *
663
+ * @param {{
664
+ * sock: object,
665
+ * remoteJid: string,
666
+ * messageInfo: object,
667
+ * expirationMessage: number|undefined,
668
+ * senderJid: string,
669
+ * senderName: string,
670
+ * text: string,
671
+ * commandPrefix: string,
672
+ * }} params Contexto da requisição.
673
+ * @returns {Promise<void>}
674
+ */
675
+ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, senderName, text, commandPrefix }) {
676
+ const ownerCandidatesRaw = await resolveOwnerCandidatesForPackCommand({
677
+ senderJid,
678
+ messageInfo,
679
+ }).catch(() => []);
680
+ const ownerJid = pickPrimaryOwnerCandidate(ownerCandidatesRaw, senderJid);
681
+ const ownerCandidates = Array.from(new Set([ownerJid, ...ownerCandidatesRaw].filter(Boolean)));
682
+ const rate = checkRateLimit(ownerJid);
683
+
684
+ if (rate.limited) {
685
+ const waitSeconds = Math.ceil(rate.remainingMs / 1000);
686
+ await sendReply({
687
+ sock,
688
+ remoteJid,
689
+ messageInfo,
690
+ expirationMessage,
691
+ text: buildActionMessage({
692
+ title: '⏳ *Muitas ações em sequência.*',
693
+ explanation: ['Para manter o sistema estável, ativei uma pausa rápida antes do próximo comando.'],
694
+ details: [`⏱️ Você poderá tentar novamente em: *${waitSeconds}s*.`],
695
+ nextSteps: ['Aguarde o tempo acima e repita o comando de pack que deseja executar.'],
696
+ }),
697
+ });
698
+ return;
699
+ }
700
+
701
+ const { command, rest } = extractCommandParts(text);
702
+ const subcommand = command || 'help';
703
+
704
+ try {
705
+ switch (subcommand) {
706
+ case 'create': {
707
+ const segments = splitPipeSegments(rest);
708
+ const base = segments.shift() || '';
709
+ const options = parsePipeOptions(segments);
710
+
711
+ const name = normalizePackName(base);
712
+ const publisher = options.publisher || options.pub || options.autor || senderName || 'OmniZap';
713
+ const description = options.desc || options.description || '';
714
+ const visibility = options.visibility || options.vis || 'public';
715
+
716
+ const created = await stickerPackService.createPack({
717
+ ownerJid,
718
+ name,
719
+ publisher,
720
+ description,
721
+ visibility,
722
+ });
723
+
724
+ await sendReply({
725
+ sock,
726
+ remoteJid,
727
+ messageInfo,
728
+ expirationMessage,
729
+ text: buildActionMessage({
730
+ title: '✅ *Pack criado!*',
731
+ explanation: ['Seu pack já está disponível e pronto para receber figurinhas.'],
732
+ details: [`📛 Nome: *${created.name}*`, `🆔 ID: \`${created.pack_key}\``, `👤 Publisher: *${created.publisher}*`, `👁️ Visibilidade: ${formatVisibilityLabel(created.visibility)}`],
733
+ nextSteps: [`Responda uma figurinha e use: \`${commandPrefix}pack add ${created.pack_key}\`.`, `Para conferir: \`${commandPrefix}pack info ${created.pack_key}\`.`],
734
+ footer: ['💡 Dica: use packs por tema para organizar e enviar mais rápido.'],
735
+ }),
736
+ });
737
+ return;
738
+ }
739
+
740
+ case 'list': {
741
+ const packLists = await Promise.all(ownerCandidates.map((candidateOwner) => stickerPackService.listPacks({ ownerJid: candidateOwner, limit: 100 })));
742
+ const packs = dedupePacksById(packLists.flatMap((items) => (Array.isArray(items) ? items : [])));
743
+ const manualPacks = packs.filter((pack) => !isThemeCurationPack(pack));
744
+
745
+ await sendReply({
746
+ sock,
747
+ remoteJid,
748
+ messageInfo,
749
+ expirationMessage,
750
+ text: formatPackList(manualPacks, commandPrefix),
751
+ });
752
+ return;
753
+ }
754
+
755
+ case 'info': {
756
+ const identifier = readSingleArgument(rest);
757
+ const pack = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.getPackInfo({ ownerJid: candidateOwner, identifier }));
758
+
759
+ await sendReply({
760
+ sock,
761
+ remoteJid,
762
+ messageInfo,
763
+ expirationMessage,
764
+ text: formatPackInfo(pack, commandPrefix),
765
+ });
766
+ return;
767
+ }
768
+
769
+ case 'rename': {
770
+ const { identifier, value } = parseIdentifierAndValue(rest);
771
+ const normalizedName = normalizePackName(value, { label: 'Novo nome do pack' });
772
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
773
+ stickerPackService.renamePack({
774
+ ownerJid: candidateOwner,
775
+ identifier,
776
+ name: normalizedName,
777
+ }),
778
+ );
779
+
780
+ await sendReply({
781
+ sock,
782
+ remoteJid,
783
+ messageInfo,
784
+ expirationMessage,
785
+ text: buildActionMessage({
786
+ title: '✏️ *Nome atualizado!*',
787
+ explanation: ['Alteração salva com sucesso.'],
788
+ details: [`📛 Novo nome: *${updated.name}*`, `🆔 ID: \`${updated.pack_key}\``],
789
+ nextSteps: [`Ver detalhes: \`${commandPrefix}pack info ${updated.pack_key}\`.`],
790
+ }),
791
+ });
792
+ return;
793
+ }
794
+
795
+ case 'setpub': {
796
+ const { identifier, value } = parseIdentifierAndValue(rest);
797
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
798
+ stickerPackService.setPackPublisher({
799
+ ownerJid: candidateOwner,
800
+ identifier,
801
+ publisher: value,
802
+ }),
803
+ );
804
+
805
+ await sendReply({
806
+ sock,
807
+ remoteJid,
808
+ messageInfo,
809
+ expirationMessage,
810
+ text: buildActionMessage({
811
+ title: '👤 *Publisher atualizado!*',
812
+ explanation: ['O publisher deste pack foi ajustado e já aparece nas informações.'],
813
+ details: [`📦 Pack: *${updated.name}*`, `👤 Publisher: *${updated.publisher}*`, `🆔 ID: \`${updated.pack_key}\``],
814
+ nextSteps: [`Se quiser, ajuste a descrição: \`${commandPrefix}pack setdesc ${updated.pack_key} "Nova descrição"\`.`],
815
+ }),
816
+ });
817
+ return;
818
+ }
819
+
820
+ case 'setdesc': {
821
+ const { identifier, value } = parseIdentifierAndValue(rest);
822
+ const description = value === '-' || value.toLowerCase() === 'clear' ? '' : value;
823
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
824
+ stickerPackService.setPackDescription({
825
+ ownerJid: candidateOwner,
826
+ identifier,
827
+ description,
828
+ }),
829
+ );
830
+
831
+ await sendReply({
832
+ sock,
833
+ remoteJid,
834
+ messageInfo,
835
+ expirationMessage,
836
+ text: buildActionMessage({
837
+ title: '📝 *Descrição atualizada!*',
838
+ explanation: ['A descrição ajuda a identificar o tema do pack.'],
839
+ details: [`📦 Pack: *${updated.name}*`, description ? `📝 Descrição: "${updated.description}"` : '🧹 Descrição removida.'],
840
+ nextSteps: [`Ver como ficou: \`${commandPrefix}pack info ${updated.pack_key}\`.`],
841
+ }),
842
+ });
843
+ return;
844
+ }
845
+
846
+ case 'setcover': {
847
+ const identifier = readSingleArgument(rest);
848
+ const asset = await resolveStickerFromCommandContext({ messageInfo, ownerJid });
849
+
850
+ if (!asset) {
851
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND, 'Não encontrei uma figurinha para definir como capa.');
852
+ }
853
+
854
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
855
+ stickerPackService.setPackCover({
856
+ ownerJid: candidateOwner,
857
+ identifier,
858
+ stickerId: asset.id,
859
+ }),
860
+ );
861
+
862
+ await sendReply({
863
+ sock,
864
+ remoteJid,
865
+ messageInfo,
866
+ expirationMessage,
867
+ text: buildActionMessage({
868
+ title: '🖼️ *Capa definida!*',
869
+ explanation: ['A figurinha selecionada agora é a capa deste pack.'],
870
+ details: [`📦 Pack: *${updated.name}*`, `🆔 ID: \`${updated.pack_key}\``],
871
+ nextSteps: [`Para enviar: \`${commandPrefix}pack send ${updated.pack_key}\`.`],
872
+ }),
873
+ });
874
+ return;
875
+ }
876
+
877
+ case 'add': {
878
+ const segments = splitPipeSegments(rest);
879
+ const identifier = readSingleArgument(segments.shift() || '');
880
+ const options = parsePipeOptions(segments);
881
+
882
+ const asset = await resolveStickerFromCommandContext({ messageInfo, ownerJid });
883
+ if (!asset) {
884
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND, 'Não encontrei uma figurinha para adicionar.');
885
+ }
886
+
887
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
888
+ stickerPackService.addStickerToPack({
889
+ ownerJid: candidateOwner,
890
+ identifier,
891
+ asset,
892
+ emojis: options.emojis,
893
+ accessibilityLabel: options.label || options.accessibility || null,
894
+ }),
895
+ );
896
+
897
+ await sendReply({
898
+ sock,
899
+ remoteJid,
900
+ messageInfo,
901
+ expirationMessage,
902
+ text: buildActionMessage({
903
+ title: '➕ *Figurinha adicionada!*',
904
+ explanation: ['Item adicionado com sucesso ao pack selecionado.'],
905
+ details: [`📦 Pack: *${updated.name}*`, `🧩 Itens: *${updated.items.length}/${MAX_PACK_ITEMS}*`, `🆔 ID: \`${updated.pack_key}\``],
906
+ nextSteps: [`Definir como capa: responda a figurinha e use \`${commandPrefix}pack setcover ${updated.pack_key}\`.`, `Ver lista completa: \`${commandPrefix}pack info ${updated.pack_key}\`.`],
907
+ }),
908
+ });
909
+ return;
910
+ }
911
+
912
+ case 'remove': {
913
+ const { token: identifier, rest: selectorRest } = readToken(rest);
914
+ const { token: selector } = readToken(selectorRest);
915
+
916
+ const result = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
917
+ stickerPackService.removeStickerFromPack({
918
+ ownerJid: candidateOwner,
919
+ identifier,
920
+ selector,
921
+ }),
922
+ );
923
+
924
+ await sendReply({
925
+ sock,
926
+ remoteJid,
927
+ messageInfo,
928
+ expirationMessage,
929
+ text: buildActionMessage({
930
+ title: '🗑️ *Figurinha removida!*',
931
+ explanation: ['Remoção concluída e o pack foi reordenado automaticamente.'],
932
+ details: [`📦 Pack: *${result.pack.name}*`, `🔢 Item removido: figurinha #${result.removed.position}`, `🧩 Itens: *${result.pack.items.length}/${MAX_PACK_ITEMS}*`],
933
+ nextSteps: [`Conferir: \`${commandPrefix}pack info ${result.pack.pack_key}\`.`],
934
+ }),
935
+ });
936
+ return;
937
+ }
938
+
939
+ case 'reorder': {
940
+ const { token: identifier, rest: rawOrder } = readToken(rest);
941
+ const updated = await runWithOwnerFallback(ownerCandidates, async (candidateOwner) => {
942
+ const orderStickerIds = await parseReorderInput({
943
+ ownerJid: candidateOwner,
944
+ identifier,
945
+ rawOrder,
946
+ });
947
+
948
+ return stickerPackService.reorderPackItems({
949
+ ownerJid: candidateOwner,
950
+ identifier,
951
+ orderStickerIds,
952
+ });
953
+ });
954
+
955
+ await sendReply({
956
+ sock,
957
+ remoteJid,
958
+ messageInfo,
959
+ expirationMessage,
960
+ text: buildActionMessage({
961
+ title: '🔀 *Ordem atualizada!*',
962
+ explanation: ['A nova sequência foi aplicada ao pack.'],
963
+ details: [`📦 Pack: *${updated.name}*`, `🆔 ID: \`${updated.pack_key}\``],
964
+ nextSteps: [`Verificar sequência: \`${commandPrefix}pack info ${updated.pack_key}\`.`],
965
+ }),
966
+ });
967
+ return;
968
+ }
969
+
970
+ case 'clone': {
971
+ const { token: identifier, rest: cloneNameRaw } = readToken(rest);
972
+ const cloneName = normalizePackName(cloneNameRaw, { label: 'Novo nome do clone' });
973
+
974
+ const cloned = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
975
+ stickerPackService.clonePack({
976
+ ownerJid: candidateOwner,
977
+ identifier,
978
+ newName: cloneName,
979
+ }),
980
+ );
981
+
982
+ await sendReply({
983
+ sock,
984
+ remoteJid,
985
+ messageInfo,
986
+ expirationMessage,
987
+ text: buildActionMessage({
988
+ title: '🧬 *Clone criado!*',
989
+ explanation: ['O pack foi duplicado com as mesmas figurinhas e configurações.'],
990
+ details: [`📦 Novo pack: *${cloned.name}*`, `🆔 ID: \`${cloned.pack_key}\``],
991
+ nextSteps: [`Renomear: \`${commandPrefix}pack rename ${cloned.pack_key} novonome\`.`, `Enviar: \`${commandPrefix}pack send ${cloned.pack_key}\`.`],
992
+ }),
993
+ });
994
+ return;
995
+ }
996
+
997
+ case 'delete': {
998
+ const identifier = readSingleArgument(rest);
999
+ const deleted = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.deletePack({ ownerJid: candidateOwner, identifier }));
1000
+
1001
+ await sendReply({
1002
+ sock,
1003
+ remoteJid,
1004
+ messageInfo,
1005
+ expirationMessage,
1006
+ text: buildActionMessage({
1007
+ title: '🗑️ *Pack removido!*',
1008
+ explanation: ['O pack foi excluído e não aparecerá mais na sua lista.'],
1009
+ details: [`📦 Pack removido: *${deleted.name}*`],
1010
+ nextSteps: [`Criar outro: \`${commandPrefix}pack create meupack\`.`],
1011
+ }),
1012
+ });
1013
+ return;
1014
+ }
1015
+
1016
+ case 'publish': {
1017
+ const { token: identifier, rest: visibilityRaw } = readToken(rest);
1018
+ const visibility = unquote(visibilityRaw);
1019
+
1020
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
1021
+ stickerPackService.setPackVisibility({
1022
+ ownerJid: candidateOwner,
1023
+ identifier,
1024
+ visibility,
1025
+ }),
1026
+ );
1027
+
1028
+ await sendReply({
1029
+ sock,
1030
+ remoteJid,
1031
+ messageInfo,
1032
+ expirationMessage,
1033
+ text: buildActionMessage({
1034
+ title: '🌐 *Visibilidade atualizada!*',
1035
+ explanation: ['A configuração de privacidade foi aplicada ao pack.'],
1036
+ details: [`📦 Pack: *${updated.name}*`, `👁️ Visibilidade: ${formatVisibilityLabel(updated.visibility)}`, `🆔 ID: \`${updated.pack_key}\``],
1037
+ nextSteps: [`Compartilhar/enviar: \`${commandPrefix}pack send ${updated.pack_key}\`.`],
1038
+ }),
1039
+ });
1040
+ return;
1041
+ }
1042
+
1043
+ case 'send': {
1044
+ const identifier = readSingleArgument(rest);
1045
+ const packDetails = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.getPackInfoForSend({ ownerJid: candidateOwner, identifier }));
1046
+ const packBuild = await buildStickerPackMessage(packDetails);
1047
+ const sendResult = await sendStickerPackWithFallback({
1048
+ sock,
1049
+ jid: remoteJid,
1050
+ messageInfo,
1051
+ expirationMessage,
1052
+ packBuild,
1053
+ });
1054
+
1055
+ if (sendResult.mode === 'native') {
1056
+ await sendReply({
1057
+ sock,
1058
+ remoteJid,
1059
+ messageInfo,
1060
+ expirationMessage,
1061
+ text: buildActionMessage({
1062
+ title: '📤 *Aqui está seu pack!*',
1063
+ explanation: ['Se não carregar de imediato, aguarde um momento até os stickers carregarem.', 'Isso pode ser influenciado pela sua internet.'],
1064
+ }),
1065
+ });
1066
+ } else {
1067
+ await sendReply({
1068
+ sock,
1069
+ remoteJid,
1070
+ messageInfo,
1071
+ expirationMessage,
1072
+ text: buildActionMessage({
1073
+ title: 'ℹ️ *Pack enviado em modo compatível.*',
1074
+ explanation: [`O cliente não aceitou o formato nativo para *${packDetails.name}*.`, 'Enviei em modo compatível (prévia + figurinhas individuais).'],
1075
+ details: [`📦 Pack: *${packDetails.name}*`, `🧩 Progresso: *${sendResult.sentCount}/${sendResult.total}*`, sendResult.nativeError ? `🛠 Detalhe técnico: ${sendResult.nativeError}` : null],
1076
+ nextSteps: [`Você pode continuar gerenciando: \`${commandPrefix}pack info ${packDetails.pack_key}\`.`, `Para tentar novamente no formato nativo: \`${commandPrefix}pack send ${packDetails.pack_key}\` mais tarde.`],
1077
+ }),
1078
+ });
1079
+ }
1080
+ return;
1081
+ }
1082
+
1083
+ default: {
1084
+ await sendReply({
1085
+ sock,
1086
+ remoteJid,
1087
+ messageInfo,
1088
+ expirationMessage,
1089
+ text: buildPackHelp(commandPrefix),
1090
+ });
1091
+ }
1092
+ }
1093
+ } catch (error) {
1094
+ logger.error('Erro ao processar comando de sticker pack.', {
1095
+ action: 'pack_command_error',
1096
+ subcommand,
1097
+ owner_jid: ownerJid,
1098
+ error: error.message,
1099
+ code: error.code,
1100
+ });
1101
+
1102
+ await sendReply({
1103
+ sock,
1104
+ remoteJid,
1105
+ messageInfo,
1106
+ expirationMessage,
1107
+ text: formatErrorMessage(error, commandPrefix),
1108
+ });
1109
+ }
1110
+ }
1111
+
1112
+ /**
1113
+ * Captura stickers recebidos para manter cache/storage atualizado.
1114
+ *
1115
+ * @param {{ messageInfo: object, senderJid: string, isMessageFromBot: boolean }} params Contexto da mensagem recebida.
1116
+ * @returns {Promise<object|null>} Asset capturado ou `null`.
1117
+ */
1118
+ export async function maybeCaptureIncomingSticker({ messageInfo, senderJid, isMessageFromBot }) {
1119
+ if (isMessageFromBot) return null;
1120
+ if (!isUserJid(senderJid)) return null;
1121
+
1122
+ const senderInfo = extractSenderInfoFromMessage(messageInfo);
1123
+ let ownerJid = normalizeJid(senderJid) || senderJid;
1124
+ try {
1125
+ const resolvedOwner = await resolveUserId({
1126
+ lid: senderInfo?.lid,
1127
+ jid: senderInfo?.jid || senderJid || null,
1128
+ participantAlt: senderInfo?.participantAlt || null,
1129
+ });
1130
+ ownerJid = normalizeJid(resolvedOwner || ownerJid) || ownerJid;
1131
+ } catch {
1132
+ ownerJid = normalizeJid(senderJid) || senderJid;
1133
+ }
1134
+
1135
+ try {
1136
+ return await captureIncomingStickerAsset({
1137
+ messageInfo,
1138
+ ownerJid,
1139
+ });
1140
+ } catch (error) {
1141
+ logger.warn('Falha ao capturar figurinha recebida para storage.', {
1142
+ action: 'pack_capture_warning',
1143
+ owner_jid: ownerJid,
1144
+ error: error.message,
1145
+ });
1146
+ return null;
1147
+ }
1148
+ }