@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,1155 @@
|
|
|
1
|
+
import { executeQuery, TABLES } from '../../../database/index.js';
|
|
2
|
+
import { getJidUser, getProfilePicBuffer, normalizeJid } from '../../config/index.js';
|
|
3
|
+
import { isUserAdmin } from '../../config/index.js';
|
|
4
|
+
import { extractUserIdInfo, isWhatsAppUserId, resolveUserId, resolveUserIdCached } from '../../config/index.js';
|
|
5
|
+
import { fetchBlocklistFromActiveSocket } from '../../config/index.js';
|
|
6
|
+
import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
|
|
7
|
+
import premiumUserStore from '../../store/premiumUserStore.js';
|
|
8
|
+
import logger from '#logger';
|
|
9
|
+
import { MESSAGE_TYPE_SQL, TIMESTAMP_TO_DATETIME_SQL } from '../statsModule/rankingCommon.js';
|
|
10
|
+
import { getAdminJid } from '../../config/index.js';
|
|
11
|
+
import { getUserUsageText } from './userConfigRuntime.js';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
|
|
14
|
+
const ACTIVE_DAYS_WINDOW = Number.parseInt(process.env.USER_PROFILE_ACTIVE_DAYS || '30', 10);
|
|
15
|
+
const OWNER_JID = getAdminJid();
|
|
16
|
+
const MIN_PHONE_DIGITS = 5;
|
|
17
|
+
const MAX_PHONE_DIGITS = 20;
|
|
18
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
const SOCIAL_RECENT_DAYS = Number.parseInt(process.env.USER_PROFILE_SOCIAL_DAYS || '45', 10);
|
|
20
|
+
const SOCIAL_DST_EXPR = `JSON_UNQUOTE(
|
|
21
|
+
COALESCE(
|
|
22
|
+
JSON_EXTRACT(m.raw_message, '$.message.extendedTextMessage.contextInfo.participant'),
|
|
23
|
+
JSON_EXTRACT(m.raw_message, '$.message.extendedTextMessage.contextInfo.mentionedJid[0]'),
|
|
24
|
+
JSON_EXTRACT(m.raw_message, '$.message.imageMessage.contextInfo.participant'),
|
|
25
|
+
JSON_EXTRACT(m.raw_message, '$.message.imageMessage.contextInfo.mentionedJid[0]'),
|
|
26
|
+
JSON_EXTRACT(m.raw_message, '$.message.videoMessage.contextInfo.participant'),
|
|
27
|
+
JSON_EXTRACT(m.raw_message, '$.message.videoMessage.contextInfo.mentionedJid[0]'),
|
|
28
|
+
JSON_EXTRACT(m.raw_message, '$.message.documentMessage.contextInfo.participant'),
|
|
29
|
+
JSON_EXTRACT(m.raw_message, '$.message.documentMessage.contextInfo.mentionedJid[0]')
|
|
30
|
+
)
|
|
31
|
+
)`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Monta o texto de ajuda com a forma correta de uso do comando.
|
|
35
|
+
* @param {string} [commandPrefix=DEFAULT_COMMAND_PREFIX] Prefixo configurado para comandos.
|
|
36
|
+
* @returns {string} Texto de instruções para o usuário.
|
|
37
|
+
*/
|
|
38
|
+
const buildUsageText = (commandPrefix = DEFAULT_COMMAND_PREFIX) => getUserUsageText('user', { commandPrefix }) || ['Formato de uso:', `${commandPrefix}user perfil <id|telefone>`, '', 'Dica:', '• Você pode mencionar alguém.', '• Ou responder a mensagem do usuário desejado.'].join('\n');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extrai o `contextInfo` da mensagem, incluindo estruturas aninhadas.
|
|
42
|
+
* @param {object} messageInfo Estrutura da mensagem recebida pelo bot.
|
|
43
|
+
* @returns {object|null} `contextInfo` encontrado ou `null` quando indisponível.
|
|
44
|
+
*/
|
|
45
|
+
const getContextInfo = (messageInfo) => {
|
|
46
|
+
const message = messageInfo?.message;
|
|
47
|
+
if (!message || typeof message !== 'object') return null;
|
|
48
|
+
|
|
49
|
+
for (const value of Object.values(message)) {
|
|
50
|
+
if (value?.contextInfo && typeof value.contextInfo === 'object') {
|
|
51
|
+
return value.contextInfo;
|
|
52
|
+
}
|
|
53
|
+
if (value?.message && typeof value.message === 'object') {
|
|
54
|
+
for (const nested of Object.values(value.message)) {
|
|
55
|
+
if (nested?.contextInfo && typeof nested.contextInfo === 'object') {
|
|
56
|
+
return nested.contextInfo;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normaliza e valida o alvo informado manualmente no comando.
|
|
67
|
+
* @param {string} rawValue Valor bruto digitado após o subcomando.
|
|
68
|
+
* @returns {{ jid: string | null, invalid: boolean }} JID normalizado ou sinalização de entrada inválida.
|
|
69
|
+
*/
|
|
70
|
+
const parseTargetArgument = (rawValue) => {
|
|
71
|
+
const value = typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
72
|
+
if (!value) return { jid: null, invalid: false };
|
|
73
|
+
|
|
74
|
+
const withoutAt = value.startsWith('@') ? value.slice(1).trim() : value;
|
|
75
|
+
if (!withoutAt) return { jid: null, invalid: true };
|
|
76
|
+
|
|
77
|
+
if (withoutAt.includes('@')) {
|
|
78
|
+
const normalized = normalizeJid(withoutAt);
|
|
79
|
+
return normalized ? { jid: normalized, invalid: false } : { jid: null, invalid: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const digits = withoutAt.replace(/\D/g, '');
|
|
83
|
+
const hasValidLength = digits.length >= MIN_PHONE_DIGITS && digits.length <= MAX_PHONE_DIGITS;
|
|
84
|
+
if (!digits || !hasValidLength) return { jid: null, invalid: true };
|
|
85
|
+
|
|
86
|
+
return { jid: `${digits}@s.whatsapp.net`, invalid: false };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Define qual usuário será usado como alvo (menção, argumento, reply ou remetente).
|
|
91
|
+
* @param {object} messageInfo Mensagem usada para inferir contexto.
|
|
92
|
+
* @param {string|null} senderJid JID do remetente do comando.
|
|
93
|
+
* @param {string} targetArg Argumento explícito passado no comando.
|
|
94
|
+
* @returns {{ source: string | object | null, invalidExplicitTarget: boolean }} Fonte escolhida e sinalizador de argumento inválido.
|
|
95
|
+
*/
|
|
96
|
+
const resolveCandidateTarget = (messageInfo, senderJid, targetArg) => {
|
|
97
|
+
const contextInfo = getContextInfo(messageInfo);
|
|
98
|
+
const mentioned = Array.isArray(contextInfo?.mentionedJid) ? contextInfo.mentionedJid.find(Boolean) || null : null;
|
|
99
|
+
const parsedTarget = parseTargetArgument(targetArg);
|
|
100
|
+
const repliedSource =
|
|
101
|
+
contextInfo?.participant || contextInfo?.participantAlt
|
|
102
|
+
? {
|
|
103
|
+
participant: contextInfo.participant || null,
|
|
104
|
+
participantAlt: contextInfo.participantAlt || null,
|
|
105
|
+
}
|
|
106
|
+
: null;
|
|
107
|
+
const hasContextTarget = Boolean(mentioned || repliedSource);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
source: mentioned || parsedTarget.jid || repliedSource || senderJid || null,
|
|
111
|
+
invalidExplicitTarget: parsedTarget.invalid && !hasContextTarget,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve o identificador canônico do usuário, considerando mapeamento JID/LID.
|
|
117
|
+
* @param {string|object|null} source Fonte de identificação do usuário.
|
|
118
|
+
* @returns {Promise<string|null>} ID canônico resolvido ou fallback quando possível.
|
|
119
|
+
*/
|
|
120
|
+
const resolveCanonicalTarget = async (source) => {
|
|
121
|
+
if (!source) return null;
|
|
122
|
+
const info = extractUserIdInfo(source);
|
|
123
|
+
const fallbackId = resolveUserIdCached(info) || info.raw || null;
|
|
124
|
+
try {
|
|
125
|
+
const resolved = await resolveUserId(info);
|
|
126
|
+
return normalizeJid(resolved) || resolved || fallbackId;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
logger.warn('Falha ao resolver alvo no comando user perfil.', {
|
|
129
|
+
error: error.message,
|
|
130
|
+
source: info.raw,
|
|
131
|
+
});
|
|
132
|
+
return fallbackId;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Carrega todos os IDs equivalentes ao alvo (JID e/ou LID) para consultas no banco.
|
|
138
|
+
* @param {string|null} canonicalTarget ID canônico do usuário.
|
|
139
|
+
* @returns {Promise<string[]>} Lista de IDs possíveis para o mesmo usuário.
|
|
140
|
+
*/
|
|
141
|
+
const resolveSenderIdsForTarget = async (canonicalTarget) => {
|
|
142
|
+
if (!canonicalTarget) return [];
|
|
143
|
+
const ids = new Set([canonicalTarget]);
|
|
144
|
+
|
|
145
|
+
if (isWhatsAppUserId(canonicalTarget)) {
|
|
146
|
+
const rows = await executeQuery(`SELECT lid FROM ${TABLES.LID_MAP} WHERE jid = ?`, [canonicalTarget]);
|
|
147
|
+
(rows || []).forEach((row) => {
|
|
148
|
+
if (row?.lid) ids.add(row.lid);
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
const rows = await executeQuery(`SELECT jid FROM ${TABLES.LID_MAP} WHERE lid = ?`, [canonicalTarget]);
|
|
152
|
+
(rows || []).forEach((row) => {
|
|
153
|
+
if (row?.jid) ids.add(normalizeJid(row.jid) || row.jid);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Array.from(ids);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Constrói placeholders SQL para cláusulas `IN`.
|
|
162
|
+
* @param {unknown[]} items Itens que serão bindados na query.
|
|
163
|
+
* @returns {string} String no formato `?, ?, ?`.
|
|
164
|
+
*/
|
|
165
|
+
const buildInClause = (items) => items.map(() => '?').join(', ');
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Busca contagem e período de atividade do usuário no histórico de mensagens.
|
|
169
|
+
* @param {{ canonicalId: string | null, senderIds?: string[] }} params Parâmetros de busca.
|
|
170
|
+
* @returns {Promise<{ totalMessages: number, firstMessage: string | Date | null, lastMessage: string | Date | null }>} Estatísticas básicas.
|
|
171
|
+
*/
|
|
172
|
+
const fetchUserStats = async ({ canonicalId, senderIds = [] }) => {
|
|
173
|
+
if (canonicalId) {
|
|
174
|
+
const [row] = await executeQuery(
|
|
175
|
+
`SELECT COUNT(*) AS total_messages,
|
|
176
|
+
MIN(m.timestamp) AS first_message,
|
|
177
|
+
MAX(m.timestamp) AS last_message
|
|
178
|
+
FROM ${TABLES.MESSAGES} m
|
|
179
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
180
|
+
ON lm.lid = m.sender_id
|
|
181
|
+
AND lm.jid IS NOT NULL
|
|
182
|
+
WHERE m.sender_id IS NOT NULL
|
|
183
|
+
AND COALESCE(lm.jid, m.sender_id) = ?`,
|
|
184
|
+
[canonicalId],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
totalMessages: Number(row?.total_messages || 0),
|
|
189
|
+
firstMessage: row?.first_message || null,
|
|
190
|
+
lastMessage: row?.last_message || null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!senderIds.length) return { totalMessages: 0, firstMessage: null, lastMessage: null };
|
|
195
|
+
|
|
196
|
+
const inClause = buildInClause(senderIds);
|
|
197
|
+
const [row] = await executeQuery(
|
|
198
|
+
`SELECT COUNT(*) AS total_messages,
|
|
199
|
+
MIN(timestamp) AS first_message,
|
|
200
|
+
MAX(timestamp) AS last_message
|
|
201
|
+
FROM ${TABLES.MESSAGES}
|
|
202
|
+
WHERE sender_id IN (${inClause})`,
|
|
203
|
+
senderIds,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
totalMessages: Number(row?.total_messages || 0),
|
|
208
|
+
firstMessage: row?.first_message || null,
|
|
209
|
+
lastMessage: row?.last_message || null,
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Converte timestamps numéricos ou datas textuais para milissegundos.
|
|
215
|
+
* @param {number|string|Date|null|undefined} value Valor de data/hora em formatos suportados.
|
|
216
|
+
* @returns {number|null} Timestamp em milissegundos ou `null` quando inválido.
|
|
217
|
+
*/
|
|
218
|
+
const toMillis = (value) => {
|
|
219
|
+
if (value === null || value === undefined) return null;
|
|
220
|
+
if (typeof value === 'number') {
|
|
221
|
+
if (value > 1e12) return value;
|
|
222
|
+
if (value > 1e9) return value * 1000;
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
const parsed = Date.parse(value);
|
|
226
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Formata uma proporção em percentual com duas casas decimais.
|
|
231
|
+
* @param {number} value Numerador.
|
|
232
|
+
* @param {number} total Denominador.
|
|
233
|
+
* @returns {string} Percentual no padrão `00.00%`.
|
|
234
|
+
*/
|
|
235
|
+
const formatPercent = (value, total) => {
|
|
236
|
+
const numericValue = Number(value || 0);
|
|
237
|
+
const numericTotal = Number(total || 0);
|
|
238
|
+
if (numericTotal <= 0) return '0.00%';
|
|
239
|
+
return `${((numericValue / numericTotal) * 100).toFixed(2)}%`;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Calcula a diferença inteira em dias entre dois timestamps.
|
|
244
|
+
* @param {number} fromMs Timestamp inicial em milissegundos.
|
|
245
|
+
* @param {number} [toMs=Date.now()] Timestamp final em milissegundos.
|
|
246
|
+
* @returns {number} Quantidade de dias inteiros.
|
|
247
|
+
*/
|
|
248
|
+
const toIntegerDays = (fromMs, toMs = Date.now()) => {
|
|
249
|
+
if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs < fromMs) return 0;
|
|
250
|
+
return Math.floor((toMs - fromMs) / DAY_MS);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Normaliza um valor de data para chave `YYYY-MM-DD`.
|
|
255
|
+
* @param {Date|string|number|null|undefined} value Valor retornado do banco.
|
|
256
|
+
* @returns {string|null} Chave normalizada ou `null` quando inválida.
|
|
257
|
+
*/
|
|
258
|
+
const normalizeDayKey = (value) => {
|
|
259
|
+
if (!value) return null;
|
|
260
|
+
if (value instanceof Date) {
|
|
261
|
+
if (Number.isNaN(value.getTime())) return null;
|
|
262
|
+
return value.toISOString().slice(0, 10);
|
|
263
|
+
}
|
|
264
|
+
const raw = String(value).trim();
|
|
265
|
+
if (!raw) return null;
|
|
266
|
+
const match = raw.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
267
|
+
if (match?.[1]) return match[1];
|
|
268
|
+
const parsed = new Date(raw);
|
|
269
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
270
|
+
return parsed.toISOString().slice(0, 10);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Calcula a maior sequência de dias consecutivos com atividade.
|
|
275
|
+
* @param {string[]} days Dias ativos ordenados no formato `YYYY-MM-DD`.
|
|
276
|
+
* @returns {number} Melhor sequência contínua em dias.
|
|
277
|
+
*/
|
|
278
|
+
const computeStreak = (days) => {
|
|
279
|
+
if (!days.length) return 0;
|
|
280
|
+
let best = 1;
|
|
281
|
+
let current = 1;
|
|
282
|
+
let prev = new Date(`${days[0]}T00:00:00Z`).getTime();
|
|
283
|
+
for (let i = 1; i < days.length; i += 1) {
|
|
284
|
+
const currentDay = new Date(`${days[i]}T00:00:00Z`).getTime();
|
|
285
|
+
const diff = currentDay - prev;
|
|
286
|
+
if (diff === DAY_MS) {
|
|
287
|
+
current += 1;
|
|
288
|
+
} else {
|
|
289
|
+
current = 1;
|
|
290
|
+
}
|
|
291
|
+
if (current > best) best = current;
|
|
292
|
+
prev = currentDay;
|
|
293
|
+
}
|
|
294
|
+
return best;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Consolida métricas globais de atividade do usuário para o perfil.
|
|
299
|
+
* @param {{ canonicalId: string | null, totalMessages?: number, firstMessage?: string | Date | null, lastMessage?: string | Date | null }} params Dados base do usuário.
|
|
300
|
+
* @returns {Promise<{ activeDays: number, avgPerDay: string, streakDays: number, favoriteType: string | null, favoriteCount: number }>} Indicadores de frequência e tipo favorito.
|
|
301
|
+
*/
|
|
302
|
+
const fetchUserGlobalRankingInsights = async ({ canonicalId, totalMessages = 0, firstMessage = null, lastMessage = null }) => {
|
|
303
|
+
if (!canonicalId) {
|
|
304
|
+
return {
|
|
305
|
+
activeDays: 0,
|
|
306
|
+
avgPerDay: '0.00',
|
|
307
|
+
streakDays: 0,
|
|
308
|
+
favoriteType: null,
|
|
309
|
+
favoriteCount: 0,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const daysRows = await executeQuery(
|
|
314
|
+
`SELECT DISTINCT DATE(ts) AS day
|
|
315
|
+
FROM (
|
|
316
|
+
SELECT ${TIMESTAMP_TO_DATETIME_SQL} AS ts
|
|
317
|
+
FROM ${TABLES.MESSAGES} m
|
|
318
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
319
|
+
ON lm.lid = m.sender_id
|
|
320
|
+
AND lm.jid IS NOT NULL
|
|
321
|
+
WHERE m.sender_id IS NOT NULL
|
|
322
|
+
AND COALESCE(lm.jid, m.sender_id) = ?
|
|
323
|
+
AND m.timestamp IS NOT NULL
|
|
324
|
+
) d
|
|
325
|
+
WHERE d.ts IS NOT NULL
|
|
326
|
+
ORDER BY day ASC`,
|
|
327
|
+
[canonicalId],
|
|
328
|
+
);
|
|
329
|
+
const days = Array.from(new Set((daysRows || []).map((item) => normalizeDayKey(item?.day)).filter(Boolean))).sort();
|
|
330
|
+
const activeDays = days.length;
|
|
331
|
+
const streakDays = computeStreak(days);
|
|
332
|
+
|
|
333
|
+
const firstMs = toMillis(firstMessage);
|
|
334
|
+
const lastMs = toMillis(lastMessage);
|
|
335
|
+
let avgPerDay = '0.00';
|
|
336
|
+
if (Number(totalMessages) > 0 && firstMs !== null && lastMs !== null) {
|
|
337
|
+
const rangeDays = Math.max(1, Math.ceil((lastMs - firstMs) / DAY_MS) + 1);
|
|
338
|
+
avgPerDay = (Number(totalMessages) / rangeDays).toFixed(2);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const [favRow] = await executeQuery(
|
|
342
|
+
`SELECT
|
|
343
|
+
${MESSAGE_TYPE_SQL} AS message_type,
|
|
344
|
+
COUNT(*) AS total
|
|
345
|
+
FROM ${TABLES.MESSAGES} m
|
|
346
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
347
|
+
ON lm.lid = m.sender_id
|
|
348
|
+
AND lm.jid IS NOT NULL
|
|
349
|
+
WHERE m.sender_id IS NOT NULL
|
|
350
|
+
AND COALESCE(lm.jid, m.sender_id) = ?
|
|
351
|
+
AND m.raw_message IS NOT NULL
|
|
352
|
+
GROUP BY message_type
|
|
353
|
+
ORDER BY total DESC
|
|
354
|
+
LIMIT 1`,
|
|
355
|
+
[canonicalId],
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
activeDays,
|
|
360
|
+
avgPerDay,
|
|
361
|
+
streakDays,
|
|
362
|
+
favoriteType: favRow?.message_type || null,
|
|
363
|
+
favoriteCount: Number(favRow?.total || 0),
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Compara volume de mensagens dos últimos 30 dias com os 30 dias anteriores.
|
|
369
|
+
* @param {string|null} canonicalId ID canônico do usuário.
|
|
370
|
+
* @returns {Promise<{ last30: number, prev30: number, delta: number, trendLabel: 'subiu'|'caiu'|'estável' }>} Resultado da tendência.
|
|
371
|
+
*/
|
|
372
|
+
const fetchUserTrendInsights = async (canonicalId) => {
|
|
373
|
+
if (!canonicalId) return { last30: 0, prev30: 0, delta: 0, trendLabel: 'estável' };
|
|
374
|
+
|
|
375
|
+
const [row] = await executeQuery(
|
|
376
|
+
`SELECT
|
|
377
|
+
SUM(CASE WHEN m.timestamp >= NOW() - INTERVAL 30 DAY THEN 1 ELSE 0 END) AS last30,
|
|
378
|
+
SUM(
|
|
379
|
+
CASE
|
|
380
|
+
WHEN m.timestamp < NOW() - INTERVAL 30 DAY
|
|
381
|
+
AND m.timestamp >= NOW() - INTERVAL 60 DAY
|
|
382
|
+
THEN 1
|
|
383
|
+
ELSE 0
|
|
384
|
+
END
|
|
385
|
+
) AS prev30
|
|
386
|
+
FROM ${TABLES.MESSAGES} m
|
|
387
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
388
|
+
ON lm.lid = m.sender_id
|
|
389
|
+
AND lm.jid IS NOT NULL
|
|
390
|
+
WHERE m.sender_id IS NOT NULL
|
|
391
|
+
AND m.timestamp IS NOT NULL
|
|
392
|
+
AND COALESCE(lm.jid, m.sender_id) = ?`,
|
|
393
|
+
[canonicalId],
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const last30 = Number(row?.last30 || 0);
|
|
397
|
+
const prev30 = Number(row?.prev30 || 0);
|
|
398
|
+
const delta = last30 - prev30;
|
|
399
|
+
const trendLabel = delta > 0 ? 'subiu' : delta < 0 ? 'caiu' : 'estável';
|
|
400
|
+
return { last30, prev30, delta, trendLabel };
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Traduz a hora do dia para uma faixa textual.
|
|
405
|
+
* @param {number|string|null} hour Hora em formato 0-23.
|
|
406
|
+
* @returns {string} Faixa horária (`madrugada`, `manhã`, `tarde`, `noite` ou `N/D`).
|
|
407
|
+
*/
|
|
408
|
+
const getHourBand = (hour) => {
|
|
409
|
+
const h = Number(hour);
|
|
410
|
+
if (!Number.isFinite(h) || h < 0 || h > 23) return 'N/D';
|
|
411
|
+
if (h < 6) return 'madrugada';
|
|
412
|
+
if (h < 12) return 'manhã';
|
|
413
|
+
if (h < 18) return 'tarde';
|
|
414
|
+
return 'noite';
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Obtém o horário de maior atividade do usuário.
|
|
419
|
+
* @param {string|null} canonicalId ID canônico do usuário.
|
|
420
|
+
* @returns {Promise<{ activeHour: number|null, hourBand: string, count: number }>} Hora mais ativa e total de mensagens na faixa.
|
|
421
|
+
*/
|
|
422
|
+
const fetchUserActiveHourInsights = async (canonicalId) => {
|
|
423
|
+
if (!canonicalId) return { activeHour: null, hourBand: 'N/D', count: 0 };
|
|
424
|
+
const [row] = await executeQuery(
|
|
425
|
+
`SELECT HOUR(m.timestamp) AS active_hour,
|
|
426
|
+
COUNT(*) AS total
|
|
427
|
+
FROM ${TABLES.MESSAGES} m
|
|
428
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
429
|
+
ON lm.lid = m.sender_id
|
|
430
|
+
AND lm.jid IS NOT NULL
|
|
431
|
+
WHERE m.sender_id IS NOT NULL
|
|
432
|
+
AND m.timestamp IS NOT NULL
|
|
433
|
+
AND COALESCE(lm.jid, m.sender_id) = ?
|
|
434
|
+
GROUP BY HOUR(m.timestamp)
|
|
435
|
+
ORDER BY total DESC
|
|
436
|
+
LIMIT 1`,
|
|
437
|
+
[canonicalId],
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const activeHour = row?.active_hour ?? null;
|
|
441
|
+
return {
|
|
442
|
+
activeHour,
|
|
443
|
+
hourBand: getHourBand(activeHour),
|
|
444
|
+
count: Number(row?.total || 0),
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Identifica o tipo de mensagem dominante no período atual e no período anterior.
|
|
450
|
+
* @param {string|null} canonicalId ID canônico do usuário.
|
|
451
|
+
* @returns {Promise<{ last30: { type: string|null, count: number }, prev30: { type: string|null, count: number } }>} Tipos dominantes por janela.
|
|
452
|
+
*/
|
|
453
|
+
const fetchDominantTypeByPeriod = async (canonicalId) => {
|
|
454
|
+
if (!canonicalId) {
|
|
455
|
+
return {
|
|
456
|
+
last30: { type: null, count: 0 },
|
|
457
|
+
prev30: { type: null, count: 0 },
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const rows = await executeQuery(
|
|
462
|
+
`SELECT period, message_type, total
|
|
463
|
+
FROM (
|
|
464
|
+
SELECT
|
|
465
|
+
CASE
|
|
466
|
+
WHEN m.timestamp >= NOW() - INTERVAL 30 DAY THEN 'last30'
|
|
467
|
+
ELSE 'prev30'
|
|
468
|
+
END AS period,
|
|
469
|
+
${MESSAGE_TYPE_SQL} AS message_type,
|
|
470
|
+
COUNT(*) AS total
|
|
471
|
+
FROM ${TABLES.MESSAGES} m
|
|
472
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
473
|
+
ON lm.lid = m.sender_id
|
|
474
|
+
AND lm.jid IS NOT NULL
|
|
475
|
+
WHERE m.sender_id IS NOT NULL
|
|
476
|
+
AND m.raw_message IS NOT NULL
|
|
477
|
+
AND m.timestamp IS NOT NULL
|
|
478
|
+
AND m.timestamp >= NOW() - INTERVAL 60 DAY
|
|
479
|
+
AND COALESCE(lm.jid, m.sender_id) = ?
|
|
480
|
+
GROUP BY period, message_type
|
|
481
|
+
) t
|
|
482
|
+
ORDER BY period, total DESC`,
|
|
483
|
+
[canonicalId],
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const result = {
|
|
487
|
+
last30: { type: null, count: 0 },
|
|
488
|
+
prev30: { type: null, count: 0 },
|
|
489
|
+
};
|
|
490
|
+
(rows || []).forEach((row) => {
|
|
491
|
+
const period = row?.period;
|
|
492
|
+
if (!period || !result[period]) return;
|
|
493
|
+
if (result[period].type) return;
|
|
494
|
+
result[period] = {
|
|
495
|
+
type: row?.message_type || null,
|
|
496
|
+
count: Number(row?.total || 0),
|
|
497
|
+
};
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
return result;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Calcula posição do usuário no ranking global por volume de mensagens.
|
|
505
|
+
* @param {string|null} canonicalId ID canônico do usuário.
|
|
506
|
+
* @returns {Promise<{ position: number|null, totalRankedUsers: number, totalMessages: number }>} Posição no ranking e totais associados.
|
|
507
|
+
*/
|
|
508
|
+
const fetchUserRanking = async (canonicalId) => {
|
|
509
|
+
if (!canonicalId) {
|
|
510
|
+
return { position: null, totalRankedUsers: 0, totalMessages: 0 };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const [totalRow] = await executeQuery(
|
|
514
|
+
`SELECT COUNT(*) AS total_messages
|
|
515
|
+
FROM ${TABLES.MESSAGES} m
|
|
516
|
+
LEFT JOIN ${TABLES.LID_MAP} lm ON lm.lid = m.sender_id
|
|
517
|
+
WHERE m.sender_id IS NOT NULL
|
|
518
|
+
AND COALESCE(lm.jid, m.sender_id) = ?`,
|
|
519
|
+
[canonicalId],
|
|
520
|
+
);
|
|
521
|
+
const totalMessages = Number(totalRow?.total_messages || 0);
|
|
522
|
+
|
|
523
|
+
const [rankedUsersRow] = await executeQuery(
|
|
524
|
+
`SELECT COUNT(*) AS total_ranked_users
|
|
525
|
+
FROM (
|
|
526
|
+
SELECT COALESCE(lm.jid, m.sender_id) AS canonical_id
|
|
527
|
+
FROM ${TABLES.MESSAGES} m
|
|
528
|
+
LEFT JOIN ${TABLES.LID_MAP} lm ON lm.lid = m.sender_id
|
|
529
|
+
WHERE m.sender_id IS NOT NULL
|
|
530
|
+
GROUP BY COALESCE(lm.jid, m.sender_id)
|
|
531
|
+
) ranked_users`,
|
|
532
|
+
);
|
|
533
|
+
const totalRankedUsers = Number(rankedUsersRow?.total_ranked_users || 0);
|
|
534
|
+
|
|
535
|
+
if (totalMessages <= 0) {
|
|
536
|
+
return { position: null, totalRankedUsers, totalMessages };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const [rankRow] = await executeQuery(
|
|
540
|
+
`SELECT COUNT(*) + 1 AS rank_position
|
|
541
|
+
FROM (
|
|
542
|
+
SELECT COALESCE(lm.jid, m.sender_id) AS canonical_id,
|
|
543
|
+
COUNT(*) AS total_messages
|
|
544
|
+
FROM ${TABLES.MESSAGES} m
|
|
545
|
+
LEFT JOIN ${TABLES.LID_MAP} lm ON lm.lid = m.sender_id
|
|
546
|
+
WHERE m.sender_id IS NOT NULL
|
|
547
|
+
GROUP BY COALESCE(lm.jid, m.sender_id)
|
|
548
|
+
) ranked
|
|
549
|
+
WHERE ranked.total_messages > ?`,
|
|
550
|
+
[totalMessages],
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
position: Number.isFinite(Number(rankRow?.rank_position)) ? Number(rankRow.rank_position) : null,
|
|
555
|
+
totalRankedUsers,
|
|
556
|
+
totalMessages,
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Busca o `pushName` mais recente entre um conjunto de IDs equivalentes.
|
|
562
|
+
* @param {string[]} senderIds IDs usados nas mensagens salvas.
|
|
563
|
+
* @returns {Promise<string|null>} Nome exibido mais recente, quando disponível.
|
|
564
|
+
*/
|
|
565
|
+
const fetchLatestPushName = async (senderIds) => {
|
|
566
|
+
if (!senderIds.length) return null;
|
|
567
|
+
const inClause = buildInClause(senderIds);
|
|
568
|
+
const [row] = await executeQuery(
|
|
569
|
+
`SELECT JSON_UNQUOTE(JSON_EXTRACT(raw_message, '$.pushName')) AS push_name
|
|
570
|
+
FROM ${TABLES.MESSAGES}
|
|
571
|
+
WHERE sender_id IN (${inClause})
|
|
572
|
+
AND raw_message IS NOT NULL
|
|
573
|
+
AND JSON_EXTRACT(raw_message, '$.pushName') IS NOT NULL
|
|
574
|
+
ORDER BY id DESC
|
|
575
|
+
LIMIT 1`,
|
|
576
|
+
senderIds,
|
|
577
|
+
);
|
|
578
|
+
return row?.push_name || null;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Tenta resolver o nome de exibição do contato a partir do cache de contatos do socket.
|
|
583
|
+
* @param {object} sock Instância do socket Baileys.
|
|
584
|
+
* @param {string[]} ids Lista de IDs candidatos.
|
|
585
|
+
* @returns {string|null} Nome encontrado ou `null`.
|
|
586
|
+
*/
|
|
587
|
+
const resolveNameFromContacts = (sock, ids) => {
|
|
588
|
+
for (const id of ids) {
|
|
589
|
+
const contact = sock?.contacts?.[id];
|
|
590
|
+
const name = contact?.notify || contact?.name || contact?.short || null;
|
|
591
|
+
if (name) return name;
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Busca o `pushName` mais recente para um ID canônico específico.
|
|
598
|
+
* @param {string|null} canonicalId ID canônico alvo.
|
|
599
|
+
* @returns {Promise<string|null>} Nome mais recente registrado nas mensagens.
|
|
600
|
+
*/
|
|
601
|
+
const fetchCanonicalPushName = async (canonicalId) => {
|
|
602
|
+
if (!canonicalId) return null;
|
|
603
|
+
const [row] = await executeQuery(
|
|
604
|
+
`SELECT JSON_UNQUOTE(JSON_EXTRACT(m.raw_message, '$.pushName')) AS push_name
|
|
605
|
+
FROM ${TABLES.MESSAGES} m
|
|
606
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
607
|
+
ON lm.lid = m.sender_id
|
|
608
|
+
AND lm.jid IS NOT NULL
|
|
609
|
+
WHERE m.sender_id IS NOT NULL
|
|
610
|
+
AND COALESCE(lm.jid, m.sender_id) = ?
|
|
611
|
+
AND m.raw_message IS NOT NULL
|
|
612
|
+
AND JSON_EXTRACT(m.raw_message, '$.pushName') IS NOT NULL
|
|
613
|
+
ORDER BY m.id DESC
|
|
614
|
+
LIMIT 1`,
|
|
615
|
+
[canonicalId],
|
|
616
|
+
);
|
|
617
|
+
return row?.push_name || null;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Monta a base SQL reutilizável para análises de interação social.
|
|
622
|
+
* @param {string} selectSql Trecho `SELECT ...` que será aplicado sobre a CTE `base`.
|
|
623
|
+
* @returns {string} Query SQL final.
|
|
624
|
+
*/
|
|
625
|
+
const buildSocialBaseQuery = (selectSql) => `
|
|
626
|
+
WITH base AS (
|
|
627
|
+
SELECT
|
|
628
|
+
COALESCE(src_map.jid, m.sender_id) AS src,
|
|
629
|
+
COALESCE(dst_map.jid, ${SOCIAL_DST_EXPR}) AS dst
|
|
630
|
+
FROM ${TABLES.MESSAGES} m
|
|
631
|
+
LEFT JOIN ${TABLES.LID_MAP} src_map
|
|
632
|
+
ON src_map.lid = m.sender_id
|
|
633
|
+
AND src_map.jid IS NOT NULL
|
|
634
|
+
LEFT JOIN ${TABLES.LID_MAP} dst_map
|
|
635
|
+
ON dst_map.lid = ${SOCIAL_DST_EXPR}
|
|
636
|
+
AND dst_map.jid IS NOT NULL
|
|
637
|
+
WHERE m.raw_message IS NOT NULL
|
|
638
|
+
AND m.sender_id IS NOT NULL
|
|
639
|
+
AND m.timestamp IS NOT NULL
|
|
640
|
+
AND m.timestamp >= NOW() - INTERVAL ${SOCIAL_RECENT_DAYS} DAY
|
|
641
|
+
AND ${SOCIAL_DST_EXPR} IS NOT NULL
|
|
642
|
+
AND ${SOCIAL_DST_EXPR} <> ''
|
|
643
|
+
AND COALESCE(src_map.jid, m.sender_id) <> COALESCE(dst_map.jid, ${SOCIAL_DST_EXPR})
|
|
644
|
+
)
|
|
645
|
+
${selectSql}
|
|
646
|
+
`;
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Calcula métricas sociais do usuário (envio/recebimento de respostas e parceiros).
|
|
650
|
+
* @param {{ canonicalId: string | null, sock: object }} params Parâmetros de consulta.
|
|
651
|
+
* @returns {Promise<{
|
|
652
|
+
* repliesSent: number,
|
|
653
|
+
* repliesReceived: number,
|
|
654
|
+
* socialScore: number,
|
|
655
|
+
* uniquePartners: number,
|
|
656
|
+
* topPartnerId: string|null,
|
|
657
|
+
* topPartnerCount: number,
|
|
658
|
+
* topPartnerLabel: string,
|
|
659
|
+
* responseRatePercent: string,
|
|
660
|
+
* responseRatio: string,
|
|
661
|
+
* topPartners: Array<{ id: string|null, count: number, label: string }>
|
|
662
|
+
* }>} Métricas sociais agregadas.
|
|
663
|
+
*/
|
|
664
|
+
const fetchUserSocialInsights = async ({ canonicalId, sock }) => {
|
|
665
|
+
if (!canonicalId) {
|
|
666
|
+
return {
|
|
667
|
+
repliesSent: 0,
|
|
668
|
+
repliesReceived: 0,
|
|
669
|
+
socialScore: 0,
|
|
670
|
+
uniquePartners: 0,
|
|
671
|
+
topPartnerId: null,
|
|
672
|
+
topPartnerCount: 0,
|
|
673
|
+
topPartnerLabel: 'N/D',
|
|
674
|
+
responseRatePercent: '0.00%',
|
|
675
|
+
responseRatio: '0/0',
|
|
676
|
+
topPartners: [],
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const [summaryRow] = await executeQuery(
|
|
681
|
+
buildSocialBaseQuery(
|
|
682
|
+
`SELECT
|
|
683
|
+
SUM(CASE WHEN src = ? THEN 1 ELSE 0 END) AS replies_sent,
|
|
684
|
+
SUM(CASE WHEN dst = ? THEN 1 ELSE 0 END) AS replies_received,
|
|
685
|
+
COUNT(DISTINCT CASE
|
|
686
|
+
WHEN src = ? THEN dst
|
|
687
|
+
WHEN dst = ? THEN src
|
|
688
|
+
ELSE NULL
|
|
689
|
+
END) AS unique_partners
|
|
690
|
+
FROM base
|
|
691
|
+
WHERE src = ? OR dst = ?`,
|
|
692
|
+
),
|
|
693
|
+
[canonicalId, canonicalId, canonicalId, canonicalId, canonicalId, canonicalId],
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
const topPartnerRows = await executeQuery(
|
|
697
|
+
buildSocialBaseQuery(
|
|
698
|
+
`SELECT
|
|
699
|
+
CASE WHEN src = ? THEN dst ELSE src END AS partner_id,
|
|
700
|
+
COUNT(*) AS total
|
|
701
|
+
FROM base
|
|
702
|
+
WHERE src = ? OR dst = ?
|
|
703
|
+
GROUP BY partner_id
|
|
704
|
+
ORDER BY total DESC
|
|
705
|
+
LIMIT 3`,
|
|
706
|
+
),
|
|
707
|
+
[canonicalId, canonicalId, canonicalId],
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const repliesSent = Number(summaryRow?.replies_sent || 0);
|
|
711
|
+
const repliesReceived = Number(summaryRow?.replies_received || 0);
|
|
712
|
+
const uniquePartners = Number(summaryRow?.unique_partners || 0);
|
|
713
|
+
const topPartners = await Promise.all(
|
|
714
|
+
(topPartnerRows || []).map(async (row) => {
|
|
715
|
+
const id = row?.partner_id || null;
|
|
716
|
+
const count = Number(row?.total || 0);
|
|
717
|
+
const mention = id && getJidUser(id) ? `@${getJidUser(id)}` : null;
|
|
718
|
+
const fromContacts = resolveNameFromContacts(sock, id ? [id] : []);
|
|
719
|
+
const pushName = id ? await fetchCanonicalPushName(id) : null;
|
|
720
|
+
const label = fromContacts || pushName || mention || id || 'N/D';
|
|
721
|
+
return { id, count, label };
|
|
722
|
+
}),
|
|
723
|
+
);
|
|
724
|
+
const topPartner = topPartners[0] || null;
|
|
725
|
+
const topPartnerId = topPartner?.id || null;
|
|
726
|
+
const topPartnerCount = Number(topPartner?.count || 0);
|
|
727
|
+
const topPartnerLabel = topPartner?.label || 'N/D';
|
|
728
|
+
const totalSocial = repliesSent + repliesReceived;
|
|
729
|
+
const responseRatePercent = totalSocial > 0 ? `${((repliesSent / totalSocial) * 100).toFixed(2)}%` : '0.00%';
|
|
730
|
+
const responseRatio = `${repliesSent}/${repliesReceived}`;
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
repliesSent,
|
|
734
|
+
repliesReceived,
|
|
735
|
+
socialScore: repliesSent + repliesReceived,
|
|
736
|
+
uniquePartners,
|
|
737
|
+
topPartnerId,
|
|
738
|
+
topPartnerCount,
|
|
739
|
+
topPartnerLabel,
|
|
740
|
+
responseRatePercent,
|
|
741
|
+
responseRatio,
|
|
742
|
+
topPartners,
|
|
743
|
+
};
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Retorna os grupos onde o usuário mais fala.
|
|
748
|
+
* @param {string|null} canonicalId ID canônico do usuário.
|
|
749
|
+
* @returns {Promise<Array<{ chatId: string|null, subject: string|null, total: number }>>} Top grupos por volume.
|
|
750
|
+
*/
|
|
751
|
+
const fetchTopGroupsInsights = async (canonicalId) => {
|
|
752
|
+
if (!canonicalId) return [];
|
|
753
|
+
const rows = await executeQuery(
|
|
754
|
+
`SELECT
|
|
755
|
+
m.chat_id,
|
|
756
|
+
COALESCE(gm.subject, '') AS group_subject,
|
|
757
|
+
COUNT(*) AS total
|
|
758
|
+
FROM ${TABLES.MESSAGES} m
|
|
759
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
760
|
+
ON lm.lid = m.sender_id
|
|
761
|
+
AND lm.jid IS NOT NULL
|
|
762
|
+
LEFT JOIN ${TABLES.GROUPS_METADATA} gm
|
|
763
|
+
ON gm.id = m.chat_id
|
|
764
|
+
WHERE m.sender_id IS NOT NULL
|
|
765
|
+
AND m.chat_id LIKE '%@g.us'
|
|
766
|
+
AND COALESCE(lm.jid, m.sender_id) = ?
|
|
767
|
+
GROUP BY m.chat_id, gm.subject
|
|
768
|
+
ORDER BY total DESC
|
|
769
|
+
LIMIT 3`,
|
|
770
|
+
[canonicalId],
|
|
771
|
+
);
|
|
772
|
+
return (rows || []).map((row) => ({
|
|
773
|
+
chatId: row?.chat_id || null,
|
|
774
|
+
subject: row?.group_subject ? String(row.group_subject).trim() : null,
|
|
775
|
+
total: Number(row?.total || 0),
|
|
776
|
+
}));
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Calcula participação proporcional do usuário no global e no grupo atual.
|
|
781
|
+
* @param {{ canonicalId: string | null, totalMessages: number, remoteJid: string, isGroupMessage: boolean }} params Contexto da conversa e totais.
|
|
782
|
+
* @returns {Promise<{ globalTotal: number, globalShare: string, groupTotal: number, groupUserTotal: number, groupShare: string }>} Métricas de participação.
|
|
783
|
+
*/
|
|
784
|
+
const fetchParticipationInsights = async ({ canonicalId, totalMessages, remoteJid, isGroupMessage }) => {
|
|
785
|
+
const [globalRow] = await executeQuery(
|
|
786
|
+
`SELECT COUNT(*) AS total
|
|
787
|
+
FROM ${TABLES.MESSAGES}
|
|
788
|
+
WHERE sender_id IS NOT NULL`,
|
|
789
|
+
);
|
|
790
|
+
const globalTotal = Number(globalRow?.total || 0);
|
|
791
|
+
|
|
792
|
+
const globalShare = formatPercent(totalMessages, globalTotal);
|
|
793
|
+
|
|
794
|
+
if (!isGroupMessage || !remoteJid || !canonicalId) {
|
|
795
|
+
return {
|
|
796
|
+
globalTotal,
|
|
797
|
+
globalShare,
|
|
798
|
+
groupTotal: 0,
|
|
799
|
+
groupUserTotal: 0,
|
|
800
|
+
groupShare: 'N/D',
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const [groupTotalsRow, groupUserRow] = await Promise.all([
|
|
805
|
+
executeQuery(
|
|
806
|
+
`SELECT COUNT(*) AS total
|
|
807
|
+
FROM ${TABLES.MESSAGES}
|
|
808
|
+
WHERE sender_id IS NOT NULL
|
|
809
|
+
AND chat_id = ?`,
|
|
810
|
+
[remoteJid],
|
|
811
|
+
),
|
|
812
|
+
executeQuery(
|
|
813
|
+
`SELECT COUNT(*) AS total
|
|
814
|
+
FROM ${TABLES.MESSAGES} m
|
|
815
|
+
LEFT JOIN ${TABLES.LID_MAP} lm
|
|
816
|
+
ON lm.lid = m.sender_id
|
|
817
|
+
AND lm.jid IS NOT NULL
|
|
818
|
+
WHERE m.sender_id IS NOT NULL
|
|
819
|
+
AND m.chat_id = ?
|
|
820
|
+
AND COALESCE(lm.jid, m.sender_id) = ?`,
|
|
821
|
+
[remoteJid, canonicalId],
|
|
822
|
+
),
|
|
823
|
+
]);
|
|
824
|
+
|
|
825
|
+
const groupTotal = Number(groupTotalsRow?.[0]?.total || 0);
|
|
826
|
+
const groupUserTotal = Number(groupUserRow?.[0]?.total || 0);
|
|
827
|
+
const groupShare = groupTotal > 0 ? formatPercent(groupUserTotal, groupTotal) : '0.00%';
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
globalTotal,
|
|
831
|
+
globalShare,
|
|
832
|
+
groupTotal,
|
|
833
|
+
groupUserTotal,
|
|
834
|
+
groupShare,
|
|
835
|
+
};
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Formata JID para telefone em padrão internacional simples.
|
|
840
|
+
* @param {string|null} jid JID do usuário.
|
|
841
|
+
* @returns {string} Telefone formatado ou `N/D`.
|
|
842
|
+
*/
|
|
843
|
+
const formatPhone = (jid) => {
|
|
844
|
+
const user = getJidUser(jid);
|
|
845
|
+
if (!user) return 'N/D';
|
|
846
|
+
const digits = user.replace(/\D/g, '');
|
|
847
|
+
return digits ? `+${digits}` : user;
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Formata data/hora no padrão pt-BR com timezone de São Paulo.
|
|
852
|
+
* @param {string|Date|null} value Valor de data para formatação.
|
|
853
|
+
* @returns {string} Data formatada ou texto padrão quando indisponível.
|
|
854
|
+
*/
|
|
855
|
+
const formatDateTime = (value) => {
|
|
856
|
+
if (!value) return 'Sem registros';
|
|
857
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
858
|
+
if (Number.isNaN(date.getTime())) return 'Sem registros';
|
|
859
|
+
return new Intl.DateTimeFormat('pt-BR', {
|
|
860
|
+
dateStyle: 'short',
|
|
861
|
+
timeStyle: 'medium',
|
|
862
|
+
timeZone: 'America/Sao_Paulo',
|
|
863
|
+
}).format(date);
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Verifica se houve interação dentro da janela de atividade configurada.
|
|
868
|
+
* @param {string|Date|null} lastMessage Última mensagem registrada.
|
|
869
|
+
* @returns {boolean} `true` quando a última interação está dentro da janela ativa.
|
|
870
|
+
*/
|
|
871
|
+
const hasRecentInteraction = (lastMessage) => {
|
|
872
|
+
if (!lastMessage) return false;
|
|
873
|
+
const parsed = lastMessage instanceof Date ? lastMessage.getTime() : new Date(lastMessage).getTime();
|
|
874
|
+
if (!Number.isFinite(parsed)) return false;
|
|
875
|
+
const maxAgeMs = ACTIVE_DAYS_WINDOW * 24 * 60 * 60 * 1000;
|
|
876
|
+
return Date.now() - parsed <= maxAgeMs;
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Consulta se algum dos IDs do usuário está bloqueado no WhatsApp.
|
|
881
|
+
* @param {string[]} targetIds IDs que representam o usuário alvo.
|
|
882
|
+
* @returns {Promise<boolean>} `true` quando o alvo consta na blocklist.
|
|
883
|
+
*/
|
|
884
|
+
const isTargetBlocked = async (targetIds) => {
|
|
885
|
+
try {
|
|
886
|
+
const blocklist = await fetchBlocklistFromActiveSocket();
|
|
887
|
+
if (!Array.isArray(blocklist) || blocklist.length === 0) return false;
|
|
888
|
+
const normalizedBlocked = new Set(blocklist.map((jid) => normalizeJid(jid) || jid).filter(Boolean));
|
|
889
|
+
return targetIds.some((id) => normalizedBlocked.has(normalizeJid(id) || id));
|
|
890
|
+
} catch (error) {
|
|
891
|
+
logger.warn('Falha ao consultar blocklist no comando user perfil.', { error: error.message });
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Converte a primeira mensagem em tempo de casa no bot (em dias).
|
|
898
|
+
* @param {string|Date|null} firstMessage Primeira mensagem registrada.
|
|
899
|
+
* @returns {string} Tempo de casa formatado.
|
|
900
|
+
*/
|
|
901
|
+
const formatTempoDeCasa = (firstMessage) => {
|
|
902
|
+
const firstMs = toMillis(firstMessage);
|
|
903
|
+
if (!Number.isFinite(firstMs)) return 'N/D';
|
|
904
|
+
const days = toIntegerDays(firstMs, Date.now());
|
|
905
|
+
return `${days} dia(s)`;
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Calcula quantos dias o usuário está sem enviar mensagens.
|
|
910
|
+
* @param {string|Date|null} lastMessage Última mensagem registrada.
|
|
911
|
+
* @returns {string} Quantidade de dias sem falar.
|
|
912
|
+
*/
|
|
913
|
+
const formatDaysSinceLastMessage = (lastMessage) => {
|
|
914
|
+
const lastMs = toMillis(lastMessage);
|
|
915
|
+
if (!Number.isFinite(lastMs)) return 'N/D';
|
|
916
|
+
return `${toIntegerDays(lastMs, Date.now())} dia(s)`;
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Formata o resumo da tendência de mensagens dos últimos períodos.
|
|
921
|
+
* @param {{ trendLabel: string, delta: number, last30: number, prev30: number }} trend Dados de tendência.
|
|
922
|
+
* @returns {string} Texto de tendência pronto para exibição.
|
|
923
|
+
*/
|
|
924
|
+
const formatTrendLabel = ({ trendLabel, delta, last30, prev30 }) => {
|
|
925
|
+
const sign = delta > 0 ? '+' : '';
|
|
926
|
+
return `${trendLabel} (${sign}${delta} | 30d: ${last30} vs ant.: ${prev30})`;
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Trunca labels longos preservando tamanho máximo com reticências.
|
|
931
|
+
* @param {string} value Texto original.
|
|
932
|
+
* @param {number} [max=30] Tamanho máximo permitido.
|
|
933
|
+
* @returns {string} Texto truncado quando necessário.
|
|
934
|
+
*/
|
|
935
|
+
const truncateLabel = (value, max = 30) => {
|
|
936
|
+
const input = String(value || '');
|
|
937
|
+
if (input.length <= max) return input;
|
|
938
|
+
return `${input.slice(0, Math.max(0, max - 1))}…`;
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Formata a saída do horário mais ativo do usuário.
|
|
943
|
+
* @param {{ hourBand: string, activeHour: number|null, count: number }} insights Dados de atividade por hora.
|
|
944
|
+
* @returns {string} Texto de horário mais ativo.
|
|
945
|
+
*/
|
|
946
|
+
const formatActiveHourLabel = ({ hourBand, activeHour, count }) => {
|
|
947
|
+
if (!Number.isFinite(Number(activeHour))) return 'N/D';
|
|
948
|
+
return `${hourBand} (${String(activeHour).padStart(2, '0')}h, ${count} msg)`;
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Formata os tipos de mensagem dominantes por janela temporal.
|
|
953
|
+
* @param {{ last30?: { type?: string|null, count?: number }, prev30?: { type?: string|null, count?: number } }} dominantByPeriod Resultado bruto da consulta.
|
|
954
|
+
* @returns {string} Texto com comparativo entre período atual e anterior.
|
|
955
|
+
*/
|
|
956
|
+
const formatDominantTypeByPeriod = (dominantByPeriod) => {
|
|
957
|
+
const last30Type = dominantByPeriod?.last30?.type || 'N/D';
|
|
958
|
+
const last30Count = Number(dominantByPeriod?.last30?.count || 0);
|
|
959
|
+
const prev30Type = dominantByPeriod?.prev30?.type || 'N/D';
|
|
960
|
+
const prev30Count = Number(dominantByPeriod?.prev30?.count || 0);
|
|
961
|
+
return `30d: ${last30Type} (${last30Count}) | ant.: ${prev30Type} (${prev30Count})`;
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Formata lista dos principais parceiros de interação em linhas.
|
|
966
|
+
* @param {Array<{ label: string, count: number }>} [topPartners=[]] Lista dos parceiros.
|
|
967
|
+
* @returns {string} Bloco multiline com ranking de parceiros.
|
|
968
|
+
*/
|
|
969
|
+
const formatTopPartnersLine = (topPartners = []) => {
|
|
970
|
+
if (!Array.isArray(topPartners) || topPartners.length === 0) return ' N/D';
|
|
971
|
+
return topPartners
|
|
972
|
+
.slice(0, 3)
|
|
973
|
+
.map((entry, index) => ` ${index + 1}) ${truncateLabel(entry.label, 26)} (${entry.count})`)
|
|
974
|
+
.join('\n');
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Formata lista dos grupos com maior volume de mensagens do usuário.
|
|
979
|
+
* @param {Array<{ subject?: string|null, chatId?: string|null, total: number }>} [topGroups=[]] Lista de grupos.
|
|
980
|
+
* @returns {string} Bloco multiline com ranking de grupos.
|
|
981
|
+
*/
|
|
982
|
+
const formatTopGroupsLine = (topGroups = []) => {
|
|
983
|
+
if (!Array.isArray(topGroups) || topGroups.length === 0) return ' N/D';
|
|
984
|
+
return topGroups
|
|
985
|
+
.slice(0, 3)
|
|
986
|
+
.map((entry, index) => ` ${index + 1}) ${truncateLabel((entry.subject && entry.subject.trim()) || entry.chatId || 'grupo', 24)} (${entry.total})`)
|
|
987
|
+
.join('\n');
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Insere linhas em branco entre itens para melhorar legibilidade.
|
|
992
|
+
* @param {string[]} [lines=[]] Linhas que serão espaçadas.
|
|
993
|
+
* @returns {string[]} Linhas com separação vertical.
|
|
994
|
+
*/
|
|
995
|
+
const withVerticalSpacing = (lines = []) => lines.flatMap((line, index) => (index === lines.length - 1 ? [line] : [line, '']));
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Constrói a mensagem final do perfil com seções e métricas organizadas.
|
|
999
|
+
* @param {object} data Dados agregados do usuário para renderização.
|
|
1000
|
+
* @returns {string} Texto completo enviado no comando de perfil.
|
|
1001
|
+
*/
|
|
1002
|
+
const buildProfileMessage = ({ mentionLabel, displayName, phone, canonicalTarget, status, firstMessage, tempoDeCasa, lastInteraction, diasSemFalar, totalMessages, rankingLabel, trendLabel, avgPerDay, activeDays, streakDays, activeHourLabel, favoriteTypeLabel, dominantTypeByPeriodLabel, socialScore, socialSent, socialReceived, responseRateLabel, socialPartners, topPartnerLabel, topPartnersLabel, topGroupsLabel, globalShareLabel, groupShareLabel, tags }) => ['👤 *PERFIL DO USUÁRIO*', '━━━━━━━━━━━━━━━━━━━━', '', '🧾 *Identificação*', ...withVerticalSpacing([`• Usuário: ${mentionLabel}`, `• Nome: ${displayName}`, `• Número: ${phone}`, `• ID: ${canonicalTarget || 'N/D'}`, `• Status: *${status}*`]), '', '📈 *Mensagens e Ranking*', ...withVerticalSpacing([`• Primeira mensagem: ${firstMessage}`, `• Tempo de casa no bot: ${tempoDeCasa}`, `• Última interação: ${lastInteraction}`, `• Dias sem falar: ${diasSemFalar}`, `• Mensagens gerais registradas: ${totalMessages}`, `• Participação global: ${globalShareLabel}`, `• Participação no grupo atual: ${groupShareLabel}`, `• Posição no ranking (mensagens): ${rankingLabel}`, `• Tendência de mensagens: ${trendLabel}`, `• Média/dia (global): ${avgPerDay}`, `• Dias ativos (global): ${activeDays}`, `• Streak (global): ${streakDays} dia(s)`, `• Horário mais ativo: ${activeHourLabel}`, `• Tipo favorito (global): ${favoriteTypeLabel}`, `• Tipo dominante por período: ${dominantTypeByPeriodLabel}`]), '', '🌐 *Interações Sociais*', ...withVerticalSpacing([`• Interações sociais (${SOCIAL_RECENT_DAYS}d): ${socialScore}`, `• Respostas enviadas (${SOCIAL_RECENT_DAYS}d): ${socialSent}`, `• Respostas recebidas (${SOCIAL_RECENT_DAYS}d): ${socialReceived}`, `• Taxa de resposta (${SOCIAL_RECENT_DAYS}d): ${responseRateLabel}`, `• Parceiros sociais (${SOCIAL_RECENT_DAYS}d): ${socialPartners}`, `• Parceiro principal (${SOCIAL_RECENT_DAYS}d): ${topPartnerLabel}`, `• Top 3 parceiros (${SOCIAL_RECENT_DAYS}d):\n${topPartnersLabel}`]), '', '🏘️ *Presença em Grupos*', ...withVerticalSpacing([`• Top grupos onde fala:\n${topGroupsLabel}`]), '', '🏷️ *Contexto*', ...withVerticalSpacing([`• Tags: ${tags.length ? tags.join(', ') : 'sem tags'}`])].join('\n');
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Seleciona o primeiro ID de usuário válido dentro de uma lista.
|
|
1006
|
+
* @param {string[]} [ids=[]] IDs candidatos.
|
|
1007
|
+
* @returns {string|null} Primeiro JID de usuário válido ou `null`.
|
|
1008
|
+
*/
|
|
1009
|
+
const resolveMentionJid = (ids = []) => ids.find((id) => isWhatsAppUserId(id)) || null;
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Processa o comando `user perfil`, resolve o alvo e envia o resumo com métricas.
|
|
1013
|
+
* @param {object} params Parâmetros operacionais do comando.
|
|
1014
|
+
* @param {object} params.sock Instância do socket Baileys.
|
|
1015
|
+
* @param {string} params.remoteJid JID da conversa atual.
|
|
1016
|
+
* @param {object} params.messageInfo Mensagem original usada como contexto.
|
|
1017
|
+
* @param {number|undefined} params.expirationMessage Configuração de expiração de mensagem.
|
|
1018
|
+
* @param {string} params.senderJid JID de quem executou o comando.
|
|
1019
|
+
* @param {string[]} [params.args=[]] Argumentos recebidos após o comando.
|
|
1020
|
+
* @param {boolean} params.isGroupMessage Indica se o contexto é grupo.
|
|
1021
|
+
* @param {string} [params.commandPrefix=DEFAULT_COMMAND_PREFIX] Prefixo de comandos.
|
|
1022
|
+
* @returns {Promise<void>} Finaliza após responder ao usuário.
|
|
1023
|
+
*/
|
|
1024
|
+
export async function handleUserCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, args = [], isGroupMessage, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
|
|
1025
|
+
const subcommand = args?.[0]?.toLowerCase() || '';
|
|
1026
|
+
if (subcommand !== 'perfil' && subcommand !== 'profile') {
|
|
1027
|
+
await sendAndStore(sock, remoteJid, { text: buildUsageText(commandPrefix) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const explicitTargetArg = args.slice(1).join(' ').trim();
|
|
1032
|
+
const { source, invalidExplicitTarget } = resolveCandidateTarget(messageInfo, senderJid, explicitTargetArg);
|
|
1033
|
+
if (invalidExplicitTarget) {
|
|
1034
|
+
await sendAndStore(sock, remoteJid, { text: `❌ ID ou telefone inválido.\n\n${buildUsageText(commandPrefix)}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (!source) {
|
|
1038
|
+
await sendAndStore(sock, remoteJid, { text: buildUsageText(commandPrefix) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
try {
|
|
1043
|
+
const canonicalTarget = await resolveCanonicalTarget(source);
|
|
1044
|
+
const senderIds = await resolveSenderIdsForTarget(canonicalTarget);
|
|
1045
|
+
const normalizedTargetIds = Array.from(new Set([canonicalTarget, ...senderIds].map((value) => normalizeJid(value) || value).filter(Boolean)));
|
|
1046
|
+
const mentionJid = resolveMentionJid(normalizedTargetIds);
|
|
1047
|
+
const senderCanonical = resolveUserIdCached({
|
|
1048
|
+
jid: senderJid,
|
|
1049
|
+
lid: senderJid,
|
|
1050
|
+
participantAlt: null,
|
|
1051
|
+
});
|
|
1052
|
+
const rankingTargetId = mentionJid || canonicalTarget;
|
|
1053
|
+
|
|
1054
|
+
const [stats, ranking, latestPushName, premiumUsers, blocked, groupAdmin] = await Promise.all([fetchUserStats({ canonicalId: rankingTargetId, senderIds: normalizedTargetIds }), fetchUserRanking(rankingTargetId), fetchLatestPushName(normalizedTargetIds), premiumUserStore.getPremiumUsers(), isTargetBlocked(normalizedTargetIds), isGroupMessage ? isUserAdmin(remoteJid, mentionJid || canonicalTarget) : Promise.resolve(false)]);
|
|
1055
|
+
const [globalInsights, socialInsights, trendInsights, activeHourInsights, dominantTypeByPeriod, topGroups, participationInsights] = await Promise.all([
|
|
1056
|
+
fetchUserGlobalRankingInsights({
|
|
1057
|
+
canonicalId: rankingTargetId,
|
|
1058
|
+
totalMessages: stats.totalMessages,
|
|
1059
|
+
firstMessage: stats.firstMessage,
|
|
1060
|
+
lastMessage: stats.lastMessage,
|
|
1061
|
+
}),
|
|
1062
|
+
fetchUserSocialInsights({
|
|
1063
|
+
canonicalId: rankingTargetId,
|
|
1064
|
+
sock,
|
|
1065
|
+
}),
|
|
1066
|
+
fetchUserTrendInsights(rankingTargetId),
|
|
1067
|
+
fetchUserActiveHourInsights(rankingTargetId),
|
|
1068
|
+
fetchDominantTypeByPeriod(rankingTargetId),
|
|
1069
|
+
fetchTopGroupsInsights(rankingTargetId),
|
|
1070
|
+
fetchParticipationInsights({
|
|
1071
|
+
canonicalId: rankingTargetId,
|
|
1072
|
+
totalMessages: stats.totalMessages,
|
|
1073
|
+
remoteJid,
|
|
1074
|
+
isGroupMessage,
|
|
1075
|
+
}),
|
|
1076
|
+
]);
|
|
1077
|
+
|
|
1078
|
+
const premiumSet = new Set((premiumUsers || []).map((jid) => normalizeJid(jid) || jid));
|
|
1079
|
+
const isPremium = normalizedTargetIds.some((id) => premiumSet.has(id));
|
|
1080
|
+
const isOwner = OWNER_JID ? normalizedTargetIds.some((id) => id === OWNER_JID) : false;
|
|
1081
|
+
const recentInteraction = hasRecentInteraction(stats.lastMessage);
|
|
1082
|
+
const status = blocked ? 'bloqueado' : 'ativo';
|
|
1083
|
+
const mentionUser = getJidUser(mentionJid || canonicalTarget);
|
|
1084
|
+
const mentionLabel = mentionUser ? `@${mentionUser}` : canonicalTarget || 'Desconhecido';
|
|
1085
|
+
const nameFromContacts = resolveNameFromContacts(sock, normalizedTargetIds);
|
|
1086
|
+
const displayName = nameFromContacts || latestPushName || mentionLabel;
|
|
1087
|
+
|
|
1088
|
+
const tags = [];
|
|
1089
|
+
if (senderCanonical && canonicalTarget && senderCanonical === canonicalTarget) tags.push('você');
|
|
1090
|
+
if (isPremium) tags.push('premium');
|
|
1091
|
+
if (groupAdmin) tags.push('admin do grupo');
|
|
1092
|
+
if (isOwner) tags.push('owner');
|
|
1093
|
+
if (!recentInteraction && stats.totalMessages > 0) tags.push('inativo');
|
|
1094
|
+
if (stats.totalMessages === 0) tags.push('sem histórico');
|
|
1095
|
+
const rankingLabel = ranking.position && ranking.totalRankedUsers > 0 ? `#${ranking.position} de ${ranking.totalRankedUsers}` : 'fora do ranking (sem mensagens)';
|
|
1096
|
+
const favoriteTypeLabel = globalInsights.favoriteType ? `${globalInsights.favoriteType} (${globalInsights.favoriteCount})` : 'N/D';
|
|
1097
|
+
const topPartnerLabel = socialInsights.topPartnerCount > 0 ? `${socialInsights.topPartnerLabel} (${socialInsights.topPartnerCount})` : 'N/D';
|
|
1098
|
+
const trendLabel = formatTrendLabel(trendInsights);
|
|
1099
|
+
const activeHourLabel = formatActiveHourLabel(activeHourInsights);
|
|
1100
|
+
const dominantTypeByPeriodLabel = formatDominantTypeByPeriod(dominantTypeByPeriod);
|
|
1101
|
+
const responseRateLabel = `${socialInsights.responseRatePercent} (${socialInsights.responseRatio})`;
|
|
1102
|
+
const topPartnersLabel = formatTopPartnersLine(socialInsights.topPartners);
|
|
1103
|
+
const topGroupsLabel = formatTopGroupsLine(topGroups);
|
|
1104
|
+
const groupShareLabel = isGroupMessage ? `${participationInsights.groupShare} (${participationInsights.groupUserTotal}/${participationInsights.groupTotal})` : 'N/D';
|
|
1105
|
+
const globalShareLabel = `${participationInsights.globalShare} (${stats.totalMessages}/${participationInsights.globalTotal})`;
|
|
1106
|
+
|
|
1107
|
+
const text = buildProfileMessage({
|
|
1108
|
+
mentionLabel,
|
|
1109
|
+
displayName,
|
|
1110
|
+
phone: formatPhone(canonicalTarget),
|
|
1111
|
+
canonicalTarget,
|
|
1112
|
+
status,
|
|
1113
|
+
firstMessage: formatDateTime(stats.firstMessage),
|
|
1114
|
+
tempoDeCasa: formatTempoDeCasa(stats.firstMessage),
|
|
1115
|
+
lastInteraction: formatDateTime(stats.lastMessage),
|
|
1116
|
+
diasSemFalar: formatDaysSinceLastMessage(stats.lastMessage),
|
|
1117
|
+
totalMessages: stats.totalMessages,
|
|
1118
|
+
globalShareLabel,
|
|
1119
|
+
groupShareLabel,
|
|
1120
|
+
rankingLabel,
|
|
1121
|
+
trendLabel,
|
|
1122
|
+
avgPerDay: globalInsights.avgPerDay,
|
|
1123
|
+
activeDays: globalInsights.activeDays,
|
|
1124
|
+
streakDays: globalInsights.streakDays,
|
|
1125
|
+
activeHourLabel,
|
|
1126
|
+
favoriteTypeLabel,
|
|
1127
|
+
dominantTypeByPeriodLabel,
|
|
1128
|
+
socialScore: socialInsights.socialScore,
|
|
1129
|
+
socialSent: socialInsights.repliesSent,
|
|
1130
|
+
socialReceived: socialInsights.repliesReceived,
|
|
1131
|
+
responseRateLabel,
|
|
1132
|
+
socialPartners: socialInsights.uniquePartners,
|
|
1133
|
+
topPartnerLabel,
|
|
1134
|
+
topPartnersLabel,
|
|
1135
|
+
topGroupsLabel,
|
|
1136
|
+
tags,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
const mentions = mentionJid ? [mentionJid] : [];
|
|
1140
|
+
const avatarJid = mentionJid;
|
|
1141
|
+
const profilePicBuffer = avatarJid
|
|
1142
|
+
? await getProfilePicBuffer(sock, {
|
|
1143
|
+
key: {
|
|
1144
|
+
participant: avatarJid,
|
|
1145
|
+
remoteJid,
|
|
1146
|
+
},
|
|
1147
|
+
})
|
|
1148
|
+
: null;
|
|
1149
|
+
|
|
1150
|
+
await sendAndStore(sock, remoteJid, profilePicBuffer ? (mentions.length ? { image: profilePicBuffer, caption: text, mentions } : { image: profilePicBuffer, caption: text }) : mentions.length ? { text, mentions } : { text }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
logger.error('Erro ao processar comando user perfil.', { error: error.message });
|
|
1153
|
+
await sendAndStore(sock, remoteJid, { text: '❌ Não foi possível carregar o perfil do usuário agora.' }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
|
|
1154
|
+
}
|
|
1155
|
+
}
|