@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.
- package/.clusterfuzzlite/Dockerfile +10 -0
- package/.env.example +907 -0
- package/.github/codeql/codeql-config.yml +10 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/codeql.yml +106 -0
- package/.github/workflows/db-migration-check.yml +98 -0
- package/.github/workflows/dependency-review.yml +22 -0
- package/.github/workflows/deploy.yml +95 -0
- package/.github/workflows/release.yml +106 -0
- package/.github/workflows/security-attest-provenance.yml +51 -0
- package/.github/workflows/security-gitleaks.yml +34 -0
- package/.github/workflows/security-runner-hardening.yml +31 -0
- package/.github/workflows/security-scorecard.yml +44 -0
- package/.github/workflows/security-zap-baseline.yml +44 -0
- package/.github/workflows/security-zap-full-scan.yml +43 -0
- package/.github/workflows/security-zizmor.yml +36 -0
- package/.github/workflows/wiki-sync.yml +44 -0
- package/.gitleaks.toml +15 -0
- package/.prettierrc +34 -0
- package/CODE_OF_CONDUCT.md +114 -0
- package/LICENSE +56 -0
- package/README.md +110 -0
- package/SECURITY.md +110 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +92 -0
- package/app/configParts/baileysConfig.js +1818 -0
- package/app/configParts/groupUtils.js +692 -0
- package/app/configParts/loggerConfig.js +394 -0
- package/app/configParts/messagePersistenceService.js +305 -0
- package/app/connection/baileysCompatibility.test.js +40 -0
- package/app/connection/baileysDbAuthState.js +344 -0
- package/app/connection/socketController.js +2243 -0
- package/app/controllers/messageController.js +7 -0
- package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
- package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
- package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
- package/app/controllers/messageProcessingPipeline.js +699 -0
- package/app/modules/adminModule/AGENT.md +4056 -0
- package/app/modules/adminModule/adminAiHelpService.js +56 -0
- package/app/modules/adminModule/adminConfigRuntime.js +177 -0
- package/app/modules/adminModule/commandConfig.json +7122 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
- package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
- package/app/modules/adminModule/groupEventHandlers.js +399 -0
- package/app/modules/aiModule/AGENT.md +547 -0
- package/app/modules/aiModule/aiAiHelpService.js +14 -0
- package/app/modules/aiModule/aiConfigRuntime.js +135 -0
- package/app/modules/aiModule/catCommand.js +967 -0
- package/app/modules/aiModule/commandConfig.json +981 -0
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/gameModule/AGENT.md +196 -0
- package/app/modules/gameModule/commandConfig.json +366 -0
- package/app/modules/gameModule/diceCommand.js +42 -0
- package/app/modules/gameModule/gameAiHelpService.js +14 -0
- package/app/modules/gameModule/gameConfigRuntime.js +68 -0
- package/app/modules/menuModule/AGENT.md +205 -0
- package/app/modules/menuModule/commandConfig.json +366 -0
- package/app/modules/menuModule/common.js +316 -0
- package/app/modules/menuModule/menuAiHelpService.js +14 -0
- package/app/modules/menuModule/menuConfigRuntime.js +68 -0
- package/app/modules/menuModule/menus.js +66 -0
- package/app/modules/playModule/AGENT.md +321 -0
- package/app/modules/playModule/commandConfig.json +584 -0
- package/app/modules/playModule/playAiHelpService.js +14 -0
- package/app/modules/playModule/playCommand.js +1417 -0
- package/app/modules/playModule/playConfigRuntime.js +68 -0
- package/app/modules/quoteModule/AGENT.md +199 -0
- package/app/modules/quoteModule/commandConfig.json +366 -0
- package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
- package/app/modules/quoteModule/quoteCommand.js +842 -0
- package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/AGENT.md +229 -0
- package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
- package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/AGENT.md +320 -0
- package/app/modules/statsModule/commandConfig.json +540 -0
- package/app/modules/statsModule/globalRankingCommand.js +64 -0
- package/app/modules/statsModule/rankingCommand.js +41 -0
- package/app/modules/statsModule/rankingCommon.js +1305 -0
- package/app/modules/statsModule/statsAiHelpService.js +14 -0
- package/app/modules/statsModule/statsConfigRuntime.js +68 -0
- package/app/modules/stickerModule/AGENT.md +692 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/commandConfig.json +1216 -0
- package/app/modules/stickerModule/convertToWebp.js +367 -0
- package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
- package/app/modules/stickerModule/stickerCommand.js +446 -0
- package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
- package/app/modules/stickerModule/stickerTextCommand.js +653 -0
- package/app/modules/stickerPackModule/AGENT.md +215 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
- package/app/modules/stickerPackModule/commandConfig.json +387 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
- package/app/modules/stickerPackModule/domainEvents.js +52 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
- package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
- package/app/modules/stickerPackModule/stickerPackService.js +733 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
- package/app/modules/systemMetricsModule/AGENT.md +193 -0
- package/app/modules/systemMetricsModule/commandConfig.json +344 -0
- package/app/modules/systemMetricsModule/pingCommand.js +399 -0
- package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
- package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
- package/app/modules/tiktokModule/AGENT.md +196 -0
- package/app/modules/tiktokModule/commandConfig.json +366 -0
- package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
- package/app/modules/tiktokModule/tiktokCommand.js +716 -0
- package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
- package/app/modules/userModule/AGENT.md +200 -0
- package/app/modules/userModule/commandConfig.json +386 -0
- package/app/modules/userModule/userAiHelpService.js +14 -0
- package/app/modules/userModule/userCommand.js +1155 -0
- package/app/modules/userModule/userConfigRuntime.js +68 -0
- package/app/modules/waifuPicsModule/AGENT.md +431 -0
- package/app/modules/waifuPicsModule/commandConfig.json +780 -0
- package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
- package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
- package/app/observability/metrics.js +766 -0
- package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
- package/app/services/ai/aiLearningRepository.js +400 -0
- package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
- package/app/services/ai/commandConfigEnrichmentService.js +452 -0
- package/app/services/ai/commandConfigValidationService.js +443 -0
- package/app/services/ai/commandToolBuilderService.js +192 -0
- package/app/services/ai/conversationRouterService.js +516 -0
- package/app/services/ai/geminiService.js +115 -0
- package/app/services/ai/geminiService.test.js +87 -0
- package/app/services/ai/globalModuleAiHelpService.js +1412 -0
- package/app/services/ai/globalToolCallingService.js +203 -0
- package/app/services/ai/messageCommandExecutionService.js +391 -0
- package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
- package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
- package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
- package/app/services/ai/moduleToolExecutorService.js +464 -0
- package/app/services/ai/moduleToolRegistryService.js +178 -0
- package/app/services/ai/toolCandidateSelectorService.js +781 -0
- package/app/services/auth/googleWebLinkService.js +80 -0
- package/app/services/auth/whatsappLoginLinkService.js +230 -0
- package/app/services/external/pokeApiService.js +398 -0
- package/app/services/group/groupMetadataService.js +311 -0
- package/app/services/infra/dbWriteQueue.js +874 -0
- package/app/services/infra/featureFlagService.js +131 -0
- package/app/services/infra/queueUtils.js +55 -0
- package/app/services/messaging/captchaService.js +491 -0
- package/app/services/messaging/messagePersistenceService.js +1 -0
- package/app/services/messaging/newsBroadcastService.js +347 -0
- package/app/services/sticker/stickerFocusService.js +347 -0
- package/app/services/sticker/stickerFocusService.test.js +43 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/conversationSessionStore.js +131 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +54 -0
- package/app/utils/antiLink/antiLinkModule.js +700 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
- package/app/workers/aiLearningWorker.js +605 -0
- package/app/workers/commandConfigEnrichmentWorker.js +242 -0
- package/database/index.js +2075 -0
- package/database/init.js +151 -0
- package/database/migrations/.gitkeep +0 -0
- package/database/migrations/20260307_d0_hardening_down.sql +64 -0
- package/database/migrations/20260307_d0_hardening_up.sql +79 -0
- package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
- package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
- package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
- package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
- package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
- package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
- package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
- package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
- package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
- package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
- package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
- package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
- package/database/schema.sql +1186 -0
- package/docker-compose.yml +104 -0
- package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
- package/docs/audits/stickerCatalogController-symbols.md +58 -0
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
- package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
- package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
- package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
- package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
- package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
- package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
- package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
- package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
- package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
- package/docs/seo/satellite-page-template.md +116 -0
- package/docs/seo/satellite-pages-phase1.json +364 -0
- package/docs/wiki/Home.md +120 -0
- package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
- package/docs/wiki/recent-changes-2026-03-08.md +47 -0
- package/ecosystem.prod.config.cjs +135 -0
- package/eslint.config.js +89 -0
- package/index.js +488 -0
- package/ml/clip_classifier/Dockerfile +18 -0
- package/ml/clip_classifier/README.md +118 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/new-logo.png +0 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +35 -0
- package/observability/promtail-config.yml +84 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +144 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/css/commands-react.input.css +71 -0
- package/public/assets/css/create-pack-react.input.css +31 -0
- package/public/assets/css/home-react.input.css +106 -0
- package/public/assets/css/login-react.input.css +58 -0
- package/public/assets/css/stickers-react.input.css +18 -0
- package/public/assets/css/terms-react.input.css +115 -0
- package/public/assets/css/user-react.input.css +57 -0
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/comandos/commands-catalog.json +4517 -0
- package/public/css/api-docs.css +161 -0
- package/public/css/stickers-admin.css +1288 -0
- package/public/css/styles.css +679 -0
- package/public/css/systemadm/admin.css +474 -0
- package/public/css/systemadm/base.css +73 -0
- package/public/css/systemadm/components.css +662 -0
- package/public/css/systemadm/layout.css +229 -0
- package/public/css/systemadm/tokens.css +56 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/js/apps/apiDocsApp.js +235 -0
- package/public/js/apps/commandsReactApp.js +528 -0
- package/public/js/apps/createPackApp.js +1646 -0
- package/public/js/apps/homeReactApp.js +942 -0
- package/public/js/apps/loginReactApp.js +496 -0
- package/public/js/apps/stickersAdminApp.js +1753 -0
- package/public/js/apps/stickersApp.js +3797 -0
- package/public/js/apps/termsReactApp.js +528 -0
- package/public/js/apps/userApp.js +2540 -0
- package/public/js/apps/userProfile/actions.js +66 -0
- package/public/js/apps/userReactApp.js +547 -0
- package/public/js/catalog.js +950 -0
- package/public/pages/api-docs.html +40 -0
- package/public/pages/aup.html +158 -0
- package/public/pages/comandos.html +41 -0
- package/public/pages/dpa.html +227 -0
- package/public/pages/home.html +45 -0
- package/public/pages/licenca.html +182 -0
- package/public/pages/login.html +40 -0
- package/public/pages/notice-and-takedown.html +234 -0
- package/public/pages/politica-de-privacidade.html +251 -0
- package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
- package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
- package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
- package/public/pages/stickers-admin.html +31 -0
- package/public/pages/stickers-create.html +41 -0
- package/public/pages/stickers.html +45 -0
- package/public/pages/suboperadores.html +237 -0
- package/public/pages/termos-de-uso-texto-integral.html +241 -0
- package/public/pages/termos-de-uso.html +41 -0
- package/public/pages/user-password-reset.html +32 -0
- package/public/pages/user-systemadm.html +508 -0
- package/public/pages/user.html +39 -0
- package/public/robots.txt +9 -0
- package/public/site.webmanifest +24 -0
- package/public/sitemap.xml +98 -0
- package/schemas/command-config.schema.json +582 -0
- package/scripts/baileys-compat-smoke.mjs +12 -0
- package/scripts/cache-bust.mjs +142 -0
- package/scripts/deploy.sh +916 -0
- package/scripts/email-broadcast-terms-update.mjs +170 -0
- package/scripts/enrich-command-discovery-fields.mjs +286 -0
- package/scripts/generate-command-config-schema.mjs +273 -0
- package/scripts/generate-commands-catalog.mjs +308 -0
- package/scripts/generate-module-agents.mjs +631 -0
- package/scripts/generate-seo-satellite-pages.mjs +400 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/github-release-notify.mjs +219 -0
- package/scripts/release.sh +599 -0
- package/scripts/run-codeql-local.sh +116 -0
- package/scripts/run-prettier-all.mjs +25 -0
- package/scripts/security-smoketest.mjs +581 -0
- package/scripts/sticker-catalog-loadtest.mjs +210 -0
- package/scripts/sticker-worker-task.mjs +119 -0
- package/scripts/sync-readme-snapshot.mjs +133 -0
- package/scripts/validate-command-config-schema.mjs +130 -0
- package/scripts/validate-command-configs.mjs +15 -0
- package/scripts/wiki-sync.sh +191 -0
- package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
- package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
- package/server/auth/jwt/webJwtService.js +147 -0
- package/server/auth/stickerCatalogAuthContext.js +165 -0
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
- package/server/auth/userPassword/index.js +14 -0
- package/server/auth/userPassword/userPasswordAuthService.js +422 -0
- package/server/auth/userPassword/userPasswordCrypto.js +199 -0
- package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
- package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
- package/server/auth/validation/authSchemas.js +236 -0
- package/server/auth/webAccount/webAccountHandlers.js +1434 -0
- package/server/controllers/admin/adminBanService.js +138 -0
- package/server/controllers/admin/adminPanelHandlers.js +2083 -0
- package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
- package/server/controllers/admin/systemAdminController.js +201 -0
- package/server/controllers/email/emailAutomationController.js +239 -0
- package/server/controllers/metricsController.js +21 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
- package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
- package/server/controllers/sticker/stickerCatalogController.js +4700 -0
- package/server/controllers/system/contactController.js +115 -0
- package/server/controllers/system/githubController.js +137 -0
- package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
- package/server/controllers/system/storageController.js +154 -0
- package/server/controllers/system/systemController.js +135 -0
- package/server/controllers/system/systemMetricsController.js +156 -0
- package/server/controllers/system/visitController.js +90 -0
- package/server/controllers/userController.js +145 -0
- package/server/email/emailAutomationRuntime.js +225 -0
- package/server/email/emailAutomationService.js +125 -0
- package/server/email/emailOutboxRepository.js +282 -0
- package/server/email/emailTemplateService.js +480 -0
- package/server/email/emailTransportService.js +156 -0
- package/server/http/clientIp.js +95 -0
- package/server/http/httpRequestUtils.js +262 -0
- package/server/http/httpRequestUtils.test.js +80 -0
- package/server/http/httpServer.js +180 -0
- package/server/http/requestContext.js +20 -0
- package/server/http/siteRoutingUtils.js +87 -0
- package/server/index.js +1 -0
- package/server/middleware/cachePolicy.js +26 -0
- package/server/middleware/cachePolicyHelpers.js +1 -0
- package/server/middleware/endpointRateLimit.js +181 -0
- package/server/middleware/rateLimit.js +70 -0
- package/server/middleware/requireAdminAuth.js +48 -0
- package/server/middleware/securityHeaders.js +97 -0
- package/server/routes/admin/systemAdminRouter.js +64 -0
- package/server/routes/email/emailAutomationRouter.js +46 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +234 -0
- package/server/routes/metrics/metricsRouter.js +58 -0
- package/server/routes/static/staticPageRouter.js +134 -0
- package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
- package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
- package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
- package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
- package/server/routes/sticker/catalogRouter.js +77 -0
- package/server/routes/sticker/stickerApiRouter.js +84 -0
- package/server/routes/sticker/stickerDataRouter.js +145 -0
- package/server/routes/sticker/stickerSiteRouter.js +43 -0
- package/server/routes/user/userApiPaths.js +66 -0
- package/server/routes/user/userRouter.js +65 -0
- package/server/utils/safePath.js +26 -0
- package/utils/logger/loggerModule.js +35 -0
- package/vite.config.mjs +38 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { URL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import logger from '#logger';
|
|
10
|
+
import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
|
|
11
|
+
import { getTikTokUsageText } from './tiktokConfigRuntime.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
|
|
14
|
+
const TIKTOK_EXTRACT_BASE_URL = (process.env.TIKTOK_EXTRACT_BASE_URL || 'http://127.0.0.1:8000').replace(/\/$/, '');
|
|
15
|
+
const TIKTOK_EXTRACT_PATH = process.env.TIKTOK_EXTRACT_PATH || '/extract';
|
|
16
|
+
const TIKTOK_EXTRACT_TIMEOUT_SECONDS = Number.parseInt(process.env.TIKTOK_EXTRACT_TIMEOUT_SECONDS || '60', 10);
|
|
17
|
+
const TIKTOK_HTTP_TIMEOUT_MS = Number.parseInt(process.env.TIKTOK_HTTP_TIMEOUT_MS || '120000', 10);
|
|
18
|
+
const TIKTOK_MAX_MB = Number.parseInt(process.env.TIKTOK_MAX_MB || '80', 10);
|
|
19
|
+
const TIKTOK_MAX_BYTES = Number.isFinite(TIKTOK_MAX_MB) ? TIKTOK_MAX_MB * 1024 * 1024 : 80 * 1024 * 1024;
|
|
20
|
+
const TIKTOK_MAX_URLS_PER_COMMAND = Number.parseInt(process.env.TIKTOK_MAX_URLS_PER_COMMAND || '5', 10);
|
|
21
|
+
const TIKTOK_MAX_IMAGES_PER_POST = Number.parseInt(process.env.TIKTOK_MAX_IMAGES_PER_POST || '10', 10);
|
|
22
|
+
const CAPTION_MAX_CHARS = 950;
|
|
23
|
+
|
|
24
|
+
const TEMP_DIR = path.join(os.tmpdir(), 'omnizap-tiktok');
|
|
25
|
+
const URL_REGEX = /https?:\/\/[^\s<>"']+/gi;
|
|
26
|
+
const IMAGE_PATH_HINTS = ['image', 'images', 'img', 'photo', 'photos', 'pic', 'pics', 'slide', 'slideshow', 'gallery', 'carousel', 'album'];
|
|
27
|
+
const ALBUM_KIND_HINTS = ['slide', 'album', 'image', 'images', 'photo', 'carousel'];
|
|
28
|
+
const TIKTOK_HOSTNAME = 'tiktok.com';
|
|
29
|
+
|
|
30
|
+
const HEADERS = {
|
|
31
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
32
|
+
Accept: '*/*',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const toNumberOrNull = (value) => {
|
|
36
|
+
if (value === null || value === undefined || value === '') return null;
|
|
37
|
+
const number = Number(value);
|
|
38
|
+
return Number.isFinite(number) ? number : null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const truncate = (value, maxChars) => {
|
|
42
|
+
const text = `${value || ''}`.trim();
|
|
43
|
+
if (!text) return '';
|
|
44
|
+
if (text.length <= maxChars) return text;
|
|
45
|
+
return `${text.slice(0, maxChars - 1).trimEnd()}…`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const toPositiveInt = (value, fallback) => {
|
|
49
|
+
const number = Number.parseInt(value, 10);
|
|
50
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sanitizeUrlToken = (value) => `${value || ''}`.trim().replace(/[),.;!?]+$/g, '');
|
|
54
|
+
|
|
55
|
+
const isHttpUrl = (value) => {
|
|
56
|
+
if (!value || typeof value !== 'string') return false;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(value);
|
|
59
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isTikTokUrl = (value) => {
|
|
66
|
+
if (!isHttpUrl(value)) return false;
|
|
67
|
+
try {
|
|
68
|
+
const host = new URL(value).hostname.toLowerCase();
|
|
69
|
+
return host === TIKTOK_HOSTNAME || host.endsWith(`.${TIKTOK_HOSTNAME}`);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const extractUrlsFromText = (text) => {
|
|
76
|
+
if (!text || typeof text !== 'string') return [];
|
|
77
|
+
const matches = text.match(URL_REGEX) || [];
|
|
78
|
+
return matches.map(sanitizeUrlToken).filter(Boolean);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const resolveUrl = (value, baseUrl) => {
|
|
82
|
+
if (!value || typeof value !== 'string') return null;
|
|
83
|
+
try {
|
|
84
|
+
return new URL(value, baseUrl).toString();
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const formatStat = (value) => {
|
|
91
|
+
const numeric = toNumberOrNull(value);
|
|
92
|
+
if (numeric === null) return '0';
|
|
93
|
+
return numeric.toLocaleString('pt-BR');
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const pickFirst = (...candidates) => {
|
|
97
|
+
for (const candidate of candidates) {
|
|
98
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
99
|
+
return candidate.trim();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const buildCaption = ({ requestedUrl, video, tiktok, mediaType = 'video' }) => {
|
|
106
|
+
const author = pickFirst(video?.author_name, tiktok?.username) || 'Desconhecido';
|
|
107
|
+
const username = pickFirst(tiktok?.username, video?.author_name);
|
|
108
|
+
const description = truncate(video?.description || 'Sem descrição.', 480);
|
|
109
|
+
const likes = formatStat(video?.stats?.likes);
|
|
110
|
+
const comments = formatStat(video?.stats?.comments);
|
|
111
|
+
const shares = formatStat(video?.stats?.shares);
|
|
112
|
+
const title = mediaType === 'images' ? "┏━〔 🖼️ TikTok Imagens Sem Marca d'Água 〕━⬣" : "┏━〔 🎬 TikTok Sem Marca d'Água 〕━⬣";
|
|
113
|
+
|
|
114
|
+
const lines = [title, `┃ 👤 Autor: *${author}*`, username ? `┃ 🆔 Perfil: *@${username.replace(/^@+/, '')}*` : null, `┃ ❤️ Curtidas: *${likes}*`, `┃ 💬 Comentários: *${comments}*`, `┃ 🔁 Compart.: *${shares}*`, '┗━━━━━━━━━━━━━━━⬣', '', '📝 *Descrição*', description, '', `🔗 ${requestedUrl}`].filter(Boolean);
|
|
115
|
+
|
|
116
|
+
return truncate(lines.join('\n'), CAPTION_MAX_CHARS);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const ensureTempDir = async () => {
|
|
120
|
+
await fsp.mkdir(TEMP_DIR, { recursive: true });
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const safeUnlink = async (filePath) => {
|
|
124
|
+
if (!filePath) return;
|
|
125
|
+
try {
|
|
126
|
+
await fsp.unlink(filePath);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
logger.warn('tiktok: falha ao remover arquivo temporário.', {
|
|
129
|
+
filePath,
|
|
130
|
+
error: error?.message || 'erro desconhecido',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const buildTempFilePath = () => path.join(TEMP_DIR, `tiktok-${Date.now()}-${randomUUID()}.mp4`);
|
|
136
|
+
|
|
137
|
+
const requestExtract = async (videoUrl) => {
|
|
138
|
+
const endpoint = `${TIKTOK_EXTRACT_BASE_URL}${TIKTOK_EXTRACT_PATH.startsWith('/') ? TIKTOK_EXTRACT_PATH : `/${TIKTOK_EXTRACT_PATH}`}`;
|
|
139
|
+
const { data } = await axios.get(endpoint, {
|
|
140
|
+
params: {
|
|
141
|
+
url: videoUrl,
|
|
142
|
+
timeout_seconds: TIKTOK_EXTRACT_TIMEOUT_SECONDS,
|
|
143
|
+
},
|
|
144
|
+
timeout: TIKTOK_HTTP_TIMEOUT_MS,
|
|
145
|
+
headers: HEADERS,
|
|
146
|
+
});
|
|
147
|
+
return data;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const collectInputUrls = ({ text, messageInfo }) => {
|
|
151
|
+
const urls = new Set();
|
|
152
|
+
const sources = [];
|
|
153
|
+
|
|
154
|
+
if (typeof text === 'string' && text.trim()) sources.push(text);
|
|
155
|
+
|
|
156
|
+
const rawMessage = messageInfo?.message;
|
|
157
|
+
if (rawMessage) {
|
|
158
|
+
try {
|
|
159
|
+
const serialized = JSON.stringify(rawMessage);
|
|
160
|
+
if (serialized) sources.push(serialized);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logger.warn('tiktok: não foi possível serializar mensagem para extração de URLs.', {
|
|
163
|
+
error: error?.message || 'erro desconhecido',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const source of sources) {
|
|
169
|
+
for (const candidate of extractUrlsFromText(source)) {
|
|
170
|
+
if (!isTikTokUrl(candidate)) continue;
|
|
171
|
+
urls.add(candidate);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [...urls];
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const collectDownloadCandidates = (payload) => {
|
|
179
|
+
const pageUrl = payload?.page?.url || TIKTOK_EXTRACT_BASE_URL;
|
|
180
|
+
const hd = payload?.download_buttons?.without_watermark_hd;
|
|
181
|
+
const noWm = payload?.download_buttons?.without_watermark;
|
|
182
|
+
const preferred = payload?.preferred_download;
|
|
183
|
+
|
|
184
|
+
const candidates = [
|
|
185
|
+
{ kind: 'without_watermark', url: resolveUrl(noWm?.url, pageUrl), isHd: false },
|
|
186
|
+
{
|
|
187
|
+
kind: preferred?.kind || 'preferred_download',
|
|
188
|
+
url: resolveUrl(preferred?.url, pageUrl),
|
|
189
|
+
isHd: `${preferred?.kind || ''}`.toLowerCase().includes('hd'),
|
|
190
|
+
},
|
|
191
|
+
{ kind: 'without_watermark_hd', url: resolveUrl(hd?.url, pageUrl), isHd: true },
|
|
192
|
+
{ kind: 'without_watermark_hd', url: resolveUrl(hd?.data_directurl, pageUrl), isHd: true },
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const dedupe = new Set();
|
|
196
|
+
return candidates
|
|
197
|
+
.filter((candidate) => candidate.url && isHttpUrl(candidate.url))
|
|
198
|
+
.filter((candidate) => {
|
|
199
|
+
if (dedupe.has(candidate.url)) return false;
|
|
200
|
+
dedupe.add(candidate.url);
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const isAlbumKind = (value) => {
|
|
206
|
+
const normalized = `${value || ''}`.trim().toLowerCase();
|
|
207
|
+
if (!normalized) return false;
|
|
208
|
+
return ALBUM_KIND_HINTS.some((hint) => normalized.includes(hint));
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const normalizeUrlList = (value) => {
|
|
212
|
+
if (!value) return [];
|
|
213
|
+
if (Array.isArray(value)) return value.filter((entry) => typeof entry === 'string');
|
|
214
|
+
if (typeof value === 'string') return [value];
|
|
215
|
+
return [];
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const collectAlbumImageCandidates = (payload) => {
|
|
219
|
+
const pageUrl = payload?.page?.url || TIKTOK_EXTRACT_BASE_URL;
|
|
220
|
+
const preferredKind = payload?.preferred_download?.kind;
|
|
221
|
+
const postType = payload?.tiktok?.post_type;
|
|
222
|
+
const dedupe = new Set();
|
|
223
|
+
const collected = [];
|
|
224
|
+
|
|
225
|
+
const append = (rawList) => {
|
|
226
|
+
for (const rawUrl of normalizeUrlList(rawList)) {
|
|
227
|
+
const resolved = resolveUrl(rawUrl, pageUrl);
|
|
228
|
+
if (!resolved || !isHttpUrl(resolved)) continue;
|
|
229
|
+
if (dedupe.has(resolved)) continue;
|
|
230
|
+
dedupe.add(resolved);
|
|
231
|
+
collected.push(resolved);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
append(payload?.album?.slide_download_urls);
|
|
236
|
+
append(payload?.album?.slide_image_urls);
|
|
237
|
+
append(payload?.album?.images);
|
|
238
|
+
|
|
239
|
+
if (isAlbumKind(preferredKind) || isAlbumKind(postType)) {
|
|
240
|
+
append(payload?.preferred_download?.url);
|
|
241
|
+
append(payload?.download_buttons?.slide?.url);
|
|
242
|
+
append(payload?.download_buttons?.slide?.data_directurl);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return collected;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const isAlbumPayload = (payload) => {
|
|
249
|
+
const albumCount = toPositiveInt(payload?.album?.count, 0);
|
|
250
|
+
const slideImageCount = normalizeUrlList(payload?.album?.slide_image_urls).length;
|
|
251
|
+
const slideDownloadCount = normalizeUrlList(payload?.album?.slide_download_urls).length;
|
|
252
|
+
const preferredKind = payload?.preferred_download?.kind;
|
|
253
|
+
const postType = payload?.tiktok?.post_type;
|
|
254
|
+
const slideButtonAvailable = Boolean(payload?.download_buttons?.slide?.available);
|
|
255
|
+
|
|
256
|
+
return albumCount > 0 || slideImageCount > 0 || slideDownloadCount > 0 || isAlbumKind(preferredKind) || isAlbumKind(postType) || slideButtonAvailable;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const hasImagePathHint = (pathSegments) => {
|
|
260
|
+
if (!Array.isArray(pathSegments) || !pathSegments.length) return false;
|
|
261
|
+
return pathSegments.some((segment) => IMAGE_PATH_HINTS.some((hint) => segment.includes(hint)));
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const isLikelyImageUrl = (value) => {
|
|
265
|
+
if (!value || typeof value !== 'string') return false;
|
|
266
|
+
return /(\.jpe?g|\.png|\.webp|\.gif|\.bmp|\.avif|\.heic)(\?|$)/i.test(value) || /[?&]format=(jpg|jpeg|png|webp|gif)\b/i.test(value);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const collectImageCandidates = (payload) => {
|
|
270
|
+
const pageUrl = payload?.page?.url || TIKTOK_EXTRACT_BASE_URL;
|
|
271
|
+
const urls = new Set();
|
|
272
|
+
const visited = new WeakSet();
|
|
273
|
+
|
|
274
|
+
const walk = (value, pathSegments = []) => {
|
|
275
|
+
if (value === null || value === undefined) return;
|
|
276
|
+
|
|
277
|
+
if (typeof value === 'string') {
|
|
278
|
+
const resolved = resolveUrl(value, pageUrl);
|
|
279
|
+
const pathHint = hasImagePathHint(pathSegments);
|
|
280
|
+
const looksLikeImage = isLikelyImageUrl(resolved);
|
|
281
|
+
const joinedPath = pathSegments.join('.');
|
|
282
|
+
const isAvatarPath = /(avatar|thumbnail|thumb|profile)/i.test(joinedPath);
|
|
283
|
+
if (resolved && !isAvatarPath && (pathHint || looksLikeImage)) {
|
|
284
|
+
urls.add(resolved);
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (typeof value !== 'object') return;
|
|
290
|
+
if (visited.has(value)) return;
|
|
291
|
+
visited.add(value);
|
|
292
|
+
|
|
293
|
+
if (Array.isArray(value)) {
|
|
294
|
+
value.forEach((entry, index) => walk(entry, [...pathSegments, String(index)]));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
299
|
+
walk(entry, [...pathSegments, key.toLowerCase()]);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
walk(payload, []);
|
|
304
|
+
|
|
305
|
+
const filtered = [...urls].filter((url) => {
|
|
306
|
+
const normalized = `${url}`.toLowerCase();
|
|
307
|
+
if (normalized.includes('/ssstik/m/')) return false;
|
|
308
|
+
if (normalized.includes('.mp3')) return false;
|
|
309
|
+
return true;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return filtered;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const sendImageCollection = async ({ sock, remoteJid, messageInfo, expirationMessage, imageUrls, caption }) => {
|
|
316
|
+
const maxImages = toPositiveInt(TIKTOK_MAX_IMAGES_PER_POST, 10);
|
|
317
|
+
const selected = imageUrls.slice(0, maxImages);
|
|
318
|
+
const skipped = Math.max(0, imageUrls.length - selected.length);
|
|
319
|
+
let sent = 0;
|
|
320
|
+
|
|
321
|
+
for (let index = 0; index < selected.length; index += 1) {
|
|
322
|
+
const imageUrl = selected[index];
|
|
323
|
+
const indexLabel = `🖼️ Imagem ${index + 1}/${selected.length}`;
|
|
324
|
+
const firstCaption = skipped > 0 ? `${caption}\n\n${indexLabel}\n⚠️ Mostrando ${selected.length}/${imageUrls.length} imagens.` : `${caption}\n\n${indexLabel}`;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
await sendAndStore(
|
|
328
|
+
sock,
|
|
329
|
+
remoteJid,
|
|
330
|
+
{
|
|
331
|
+
image: { url: imageUrl },
|
|
332
|
+
caption: index === 0 ? firstCaption : indexLabel,
|
|
333
|
+
},
|
|
334
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
335
|
+
);
|
|
336
|
+
sent += 1;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
logger.warn('tiktok: falha ao enviar imagem do carrossel.', {
|
|
339
|
+
remoteJid,
|
|
340
|
+
index: index + 1,
|
|
341
|
+
total: selected.length,
|
|
342
|
+
error: truncate(error?.message || 'erro desconhecido', 240),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (sent <= 0) {
|
|
348
|
+
throw new Error('Falha ao enviar imagens do TikTok.');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { sent, skipped, totalDetected: imageUrls.length };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const downloadToTempFile = async (url) => {
|
|
355
|
+
await ensureTempDir();
|
|
356
|
+
const filePath = buildTempFilePath();
|
|
357
|
+
const writer = fs.createWriteStream(filePath);
|
|
358
|
+
let totalBytes = 0;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const response = await axios.get(url, {
|
|
362
|
+
responseType: 'stream',
|
|
363
|
+
timeout: TIKTOK_HTTP_TIMEOUT_MS,
|
|
364
|
+
maxRedirects: 5,
|
|
365
|
+
headers: HEADERS,
|
|
366
|
+
validateStatus: (status) => status >= 200 && status < 400,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const contentType = `${response.headers?.['content-type'] || ''}`.toLowerCase();
|
|
370
|
+
const looksLikeMedia = !contentType || contentType.includes('video') || contentType.includes('octet-stream') || contentType.includes('mp4');
|
|
371
|
+
if (!looksLikeMedia) {
|
|
372
|
+
throw new Error(`Content-Type inválido para vídeo: ${contentType || 'desconhecido'}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const contentLength = toNumberOrNull(response.headers?.['content-length']);
|
|
376
|
+
if (contentLength !== null && contentLength > TIKTOK_MAX_BYTES) {
|
|
377
|
+
throw new Error(`Arquivo excede o limite de ${TIKTOK_MAX_MB}MB.`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await new Promise((resolve, reject) => {
|
|
381
|
+
response.data.on('data', (chunk) => {
|
|
382
|
+
totalBytes += chunk.length;
|
|
383
|
+
if (totalBytes > TIKTOK_MAX_BYTES) {
|
|
384
|
+
const error = new Error(`Arquivo excede o limite de ${TIKTOK_MAX_MB}MB.`);
|
|
385
|
+
response.data.destroy(error);
|
|
386
|
+
reject(error);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
response.data.on('error', reject);
|
|
391
|
+
writer.on('error', reject);
|
|
392
|
+
writer.on('finish', resolve);
|
|
393
|
+
response.data.pipe(writer);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (totalBytes <= 0) {
|
|
397
|
+
throw new Error('Download vazio.');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
filePath,
|
|
402
|
+
bytes: totalBytes,
|
|
403
|
+
contentType: response.headers?.['content-type'] || 'video/mp4',
|
|
404
|
+
};
|
|
405
|
+
} catch (error) {
|
|
406
|
+
await safeUnlink(filePath);
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const tryDownloadCandidates = async (candidates) => {
|
|
412
|
+
const failures = [];
|
|
413
|
+
|
|
414
|
+
for (const candidate of candidates) {
|
|
415
|
+
try {
|
|
416
|
+
const result = await downloadToTempFile(candidate.url);
|
|
417
|
+
return {
|
|
418
|
+
...result,
|
|
419
|
+
selectedUrl: candidate.url,
|
|
420
|
+
selectedKind: candidate.kind,
|
|
421
|
+
isHd: candidate.isHd,
|
|
422
|
+
};
|
|
423
|
+
} catch (error) {
|
|
424
|
+
failures.push({
|
|
425
|
+
kind: candidate.kind,
|
|
426
|
+
url: candidate.url,
|
|
427
|
+
error: error?.message || 'falha desconhecida',
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const detailed = failures.map((failure) => `[${failure.kind}] ${failure.error}`).join('; ');
|
|
433
|
+
throw new Error(detailed || 'Nenhum link de download disponível.');
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const trySendDirectCandidates = async ({ sock, remoteJid, messageInfo, expirationMessage, caption, candidates }) => {
|
|
437
|
+
const failures = [];
|
|
438
|
+
|
|
439
|
+
for (const candidate of candidates) {
|
|
440
|
+
try {
|
|
441
|
+
await sendAndStore(
|
|
442
|
+
sock,
|
|
443
|
+
remoteJid,
|
|
444
|
+
{
|
|
445
|
+
video: { url: candidate.url },
|
|
446
|
+
mimetype: 'video/mp4',
|
|
447
|
+
caption,
|
|
448
|
+
},
|
|
449
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
delivery: 'direct',
|
|
454
|
+
selectedUrl: candidate.url,
|
|
455
|
+
selectedKind: candidate.kind,
|
|
456
|
+
isHd: candidate.isHd,
|
|
457
|
+
};
|
|
458
|
+
} catch (error) {
|
|
459
|
+
failures.push({
|
|
460
|
+
kind: candidate.kind,
|
|
461
|
+
url: candidate.url,
|
|
462
|
+
error: error?.message || 'falha desconhecida',
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const detailed = failures.map((failure) => `[${failure.kind}] ${failure.error}`).join('; ');
|
|
468
|
+
throw new Error(detailed || 'Falha no envio direto por URL.');
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const sendUsage = async ({ sock, remoteJid, messageInfo, expirationMessage, commandPrefix }) =>
|
|
472
|
+
sendAndStore(
|
|
473
|
+
sock,
|
|
474
|
+
remoteJid,
|
|
475
|
+
{
|
|
476
|
+
text: [getTikTokUsageText('tiktok', { commandPrefix }) || '', '', '✅ Suporta múltiplos links e posts de imagem (carrossel).'].filter(Boolean).join('\n'),
|
|
477
|
+
},
|
|
478
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const processTikTokUrl = async ({ sock, remoteJid, messageInfo, expirationMessage, inputUrl }) => {
|
|
482
|
+
let tempFilePath = null;
|
|
483
|
+
try {
|
|
484
|
+
const payload = await requestExtract(inputUrl);
|
|
485
|
+
|
|
486
|
+
if (!payload?.ok) {
|
|
487
|
+
const reason = truncate(payload?.error || payload?.message || 'A API não retornou um download válido.', 220);
|
|
488
|
+
throw new Error(reason);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const requestedUrl = payload?.requested_url || inputUrl;
|
|
492
|
+
const albumPost = isAlbumPayload(payload);
|
|
493
|
+
if (albumPost) {
|
|
494
|
+
const albumCandidates = collectAlbumImageCandidates(payload);
|
|
495
|
+
const imageCandidates = albumCandidates.length > 0 ? albumCandidates : collectImageCandidates(payload);
|
|
496
|
+
if (!imageCandidates.length) {
|
|
497
|
+
throw new Error('A API indicou álbum de imagens, mas não retornou URLs válidas.');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const imageCaption = buildCaption({
|
|
501
|
+
requestedUrl,
|
|
502
|
+
video: payload?.video || {},
|
|
503
|
+
tiktok: payload?.tiktok || {},
|
|
504
|
+
mediaType: 'images',
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const imageResult = await sendImageCollection({
|
|
508
|
+
sock,
|
|
509
|
+
remoteJid,
|
|
510
|
+
messageInfo,
|
|
511
|
+
expirationMessage,
|
|
512
|
+
imageUrls: imageCandidates,
|
|
513
|
+
caption: imageCaption,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
mediaType: 'images',
|
|
518
|
+
requestedUrl,
|
|
519
|
+
imageCount: imageResult.sent,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const candidates = collectDownloadCandidates(payload);
|
|
524
|
+
if (!candidates.length) {
|
|
525
|
+
const imageCandidates = collectImageCandidates(payload);
|
|
526
|
+
if (imageCandidates.length > 0) {
|
|
527
|
+
const imageCaption = buildCaption({
|
|
528
|
+
requestedUrl,
|
|
529
|
+
video: payload?.video || {},
|
|
530
|
+
tiktok: payload?.tiktok || {},
|
|
531
|
+
mediaType: 'images',
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const imageResult = await sendImageCollection({
|
|
535
|
+
sock,
|
|
536
|
+
remoteJid,
|
|
537
|
+
messageInfo,
|
|
538
|
+
expirationMessage,
|
|
539
|
+
imageUrls: imageCandidates,
|
|
540
|
+
caption: imageCaption,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
mediaType: 'images',
|
|
545
|
+
requestedUrl,
|
|
546
|
+
imageCount: imageResult.sent,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
throw new Error('Não encontrei links de download no retorno da API.');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const caption = buildCaption({
|
|
554
|
+
requestedUrl,
|
|
555
|
+
video: payload?.video || {},
|
|
556
|
+
tiktok: payload?.tiktok || {},
|
|
557
|
+
mediaType: 'video',
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
let deliveryResult = null;
|
|
561
|
+
try {
|
|
562
|
+
deliveryResult = await trySendDirectCandidates({
|
|
563
|
+
sock,
|
|
564
|
+
remoteJid,
|
|
565
|
+
messageInfo,
|
|
566
|
+
expirationMessage,
|
|
567
|
+
caption,
|
|
568
|
+
candidates,
|
|
569
|
+
});
|
|
570
|
+
} catch (directError) {
|
|
571
|
+
logger.warn('tiktok: envio direto falhou, aplicando fallback com download local.', {
|
|
572
|
+
remoteJid,
|
|
573
|
+
error: truncate(directError?.message || 'erro desconhecido', 300),
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const download = await tryDownloadCandidates(candidates);
|
|
577
|
+
tempFilePath = download.filePath;
|
|
578
|
+
|
|
579
|
+
await sendAndStore(
|
|
580
|
+
sock,
|
|
581
|
+
remoteJid,
|
|
582
|
+
{
|
|
583
|
+
video: { url: tempFilePath },
|
|
584
|
+
mimetype: download.contentType || 'video/mp4',
|
|
585
|
+
caption,
|
|
586
|
+
},
|
|
587
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
deliveryResult = {
|
|
591
|
+
delivery: 'local',
|
|
592
|
+
selectedUrl: download.selectedUrl,
|
|
593
|
+
selectedKind: download.selectedKind,
|
|
594
|
+
isHd: download.isHd,
|
|
595
|
+
bytes: download.bytes,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
logger.info('tiktok: vídeo enviado com sucesso.', {
|
|
600
|
+
remoteJid,
|
|
601
|
+
delivery: deliveryResult?.delivery || 'unknown',
|
|
602
|
+
isHd: Boolean(deliveryResult?.isHd),
|
|
603
|
+
selectedKind: deliveryResult?.selectedKind || null,
|
|
604
|
+
bytes: deliveryResult?.bytes || null,
|
|
605
|
+
source: payload?.source || null,
|
|
606
|
+
requestedUrl,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
return {
|
|
610
|
+
mediaType: 'video',
|
|
611
|
+
requestedUrl,
|
|
612
|
+
delivery: deliveryResult?.delivery || 'unknown',
|
|
613
|
+
};
|
|
614
|
+
} finally {
|
|
615
|
+
await safeUnlink(tempFilePath);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
export async function handleTikTokCommand({ sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
|
|
620
|
+
const inputUrls = collectInputUrls({ text, messageInfo });
|
|
621
|
+
if (!inputUrls.length) {
|
|
622
|
+
await sendUsage({ sock, remoteJid, messageInfo, expirationMessage, commandPrefix });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const maxUrls = toPositiveInt(TIKTOK_MAX_URLS_PER_COMMAND, 5);
|
|
627
|
+
const urls = inputUrls.slice(0, maxUrls);
|
|
628
|
+
const ignoredCount = Math.max(0, inputUrls.length - urls.length);
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const startText = urls.length === 1 ? "⏳ Baixando TikTok sem marca d'água, aguarde..." : `⏳ Processando ${urls.length} links do TikTok...`;
|
|
632
|
+
|
|
633
|
+
await sendAndStore(
|
|
634
|
+
sock,
|
|
635
|
+
remoteJid,
|
|
636
|
+
{
|
|
637
|
+
text: ignoredCount > 0 ? `${startText}\n⚠️ Limite por comando: ${maxUrls}. Ignorados: ${ignoredCount}.` : startText,
|
|
638
|
+
},
|
|
639
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
const failures = [];
|
|
643
|
+
let deliveredCount = 0;
|
|
644
|
+
let videosSent = 0;
|
|
645
|
+
let imagesSent = 0;
|
|
646
|
+
|
|
647
|
+
for (let index = 0; index < urls.length; index += 1) {
|
|
648
|
+
const currentUrl = urls[index];
|
|
649
|
+
try {
|
|
650
|
+
const result = await processTikTokUrl({
|
|
651
|
+
sock,
|
|
652
|
+
remoteJid,
|
|
653
|
+
messageInfo,
|
|
654
|
+
expirationMessage,
|
|
655
|
+
inputUrl: currentUrl,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
deliveredCount += 1;
|
|
659
|
+
if (result?.mediaType === 'images') {
|
|
660
|
+
imagesSent += 1;
|
|
661
|
+
} else {
|
|
662
|
+
videosSent += 1;
|
|
663
|
+
}
|
|
664
|
+
} catch (error) {
|
|
665
|
+
failures.push({
|
|
666
|
+
url: currentUrl,
|
|
667
|
+
error: truncate(error?.message || 'erro desconhecido', 200),
|
|
668
|
+
});
|
|
669
|
+
logger.warn('tiktok: falha em item da fila.', {
|
|
670
|
+
remoteJid,
|
|
671
|
+
index: index + 1,
|
|
672
|
+
total: urls.length,
|
|
673
|
+
url: currentUrl,
|
|
674
|
+
error: error?.message || 'erro desconhecido',
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (urls.length === 1 && deliveredCount <= 0) {
|
|
680
|
+
throw new Error(failures[0]?.error || 'Nenhum link foi processado com sucesso.');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (urls.length > 1) {
|
|
684
|
+
if (deliveredCount <= 0) {
|
|
685
|
+
const failureDetails = failures
|
|
686
|
+
.slice(0, 3)
|
|
687
|
+
.map((item, index) => `${index + 1}. ${item.error}`)
|
|
688
|
+
.join('\n');
|
|
689
|
+
throw new Error(failureDetails || 'Nenhum link foi processado com sucesso.');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const summaryLines = ['✅ Processamento do TikTok concluído.', `• Itens enviados: ${deliveredCount}/${urls.length}`, `• Vídeos: ${videosSent}`, `• Posts de imagem: ${imagesSent}`];
|
|
693
|
+
|
|
694
|
+
if (failures.length > 0) {
|
|
695
|
+
summaryLines.push(`• Falhas: ${failures.length}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
await sendAndStore(sock, remoteJid, { text: summaryLines.join('\n') }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
699
|
+
}
|
|
700
|
+
} catch (error) {
|
|
701
|
+
logger.error('tiktok: falha ao processar comando.', {
|
|
702
|
+
remoteJid,
|
|
703
|
+
error: error?.message || 'erro desconhecido',
|
|
704
|
+
requestedText: truncate(text, 400),
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
await sendAndStore(
|
|
708
|
+
sock,
|
|
709
|
+
remoteJid,
|
|
710
|
+
{
|
|
711
|
+
text: ['❌ Não consegui baixar esse TikTok agora.', '', `Motivo: ${truncate(error?.message || 'falha desconhecida', 240)}`].join('\n'),
|
|
712
|
+
},
|
|
713
|
+
{ quoted: messageInfo, ephemeralExpiration: expirationMessage },
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|