@omnizap-system/omnizap 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.env.example +58 -13
  2. package/.github/workflows/ci.yml +5 -5
  3. package/.github/workflows/codeql.yml +1 -1
  4. package/.github/workflows/db-migration-check.yml +2 -2
  5. package/.github/workflows/dependency-review.yml +1 -1
  6. package/.github/workflows/deploy.yml +2 -2
  7. package/.github/workflows/release.yml +2 -2
  8. package/.github/workflows/security-attest-provenance.yml +2 -2
  9. package/.github/workflows/security-gitleaks.yml +13 -4
  10. package/.github/workflows/security-runner-hardening.yml +2 -2
  11. package/.github/workflows/security-scorecard.yml +1 -1
  12. package/.github/workflows/security-zap-baseline.yml +1 -1
  13. package/.github/workflows/security-zap-full-scan.yml +2 -1
  14. package/.github/workflows/security-zizmor.yml +1 -1
  15. package/.github/workflows/wiki-sync.yml +1 -1
  16. package/.gitleaksignore +9 -0
  17. package/CODE_OF_CONDUCT.md +2 -2
  18. package/GEMINI.md +64 -0
  19. package/README.md +52 -82
  20. package/SECURITY.md +1 -1
  21. package/app/config/index.js +2 -0
  22. package/app/configParts/adminIdentity.js +5 -5
  23. package/app/configParts/baileysConfig.js +230 -58
  24. package/app/configParts/groupUtils.js +5 -0
  25. package/app/configParts/messagePersistenceService.js +145 -4
  26. package/app/configParts/sessionConfig.js +157 -0
  27. package/app/connection/baileysCompatibility.test.js +1 -1
  28. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  29. package/app/connection/socketController.js +660 -158
  30. package/app/connection/socketController.multiSession.test.js +108 -0
  31. package/app/controllers/messageController.js +1 -1
  32. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  33. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  34. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  35. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  36. package/app/controllers/messageProcessingPipeline.js +93 -13
  37. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  38. package/app/modules/adminModule/AGENT.md +1 -1
  39. package/app/modules/adminModule/commandConfig.json +3318 -1347
  40. package/app/modules/adminModule/groupCommandHandlers.js +858 -15
  41. package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
  42. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  43. package/app/modules/aiModule/AGENT.md +47 -30
  44. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  45. package/app/modules/aiModule/catCommand.js +135 -27
  46. package/app/modules/aiModule/commandConfig.json +114 -28
  47. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  48. package/app/modules/gameModule/AGENT.md +1 -1
  49. package/app/modules/gameModule/commandConfig.json +29 -0
  50. package/app/modules/menuModule/AGENT.md +1 -1
  51. package/app/modules/menuModule/commandConfig.json +45 -10
  52. package/app/modules/menuModule/menuCatalogService.js +190 -0
  53. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  54. package/app/modules/menuModule/menuDynamicService.js +511 -0
  55. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  56. package/app/modules/menuModule/menus.js +36 -5
  57. package/app/modules/playModule/AGENT.md +10 -5
  58. package/app/modules/playModule/commandConfig.json +140 -12
  59. package/app/modules/playModule/playCommand.js +1 -1417
  60. package/app/modules/playModule/playCommandConstants.js +80 -0
  61. package/app/modules/playModule/playCommandCore.js +361 -0
  62. package/app/modules/playModule/playCommandHandlers.js +41 -0
  63. package/app/modules/playModule/playCommandMediaClient.js +1872 -0
  64. package/app/modules/playModule/playConfigRuntime.js +245 -4
  65. package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
  66. package/app/modules/quoteModule/AGENT.md +1 -1
  67. package/app/modules/quoteModule/commandConfig.json +29 -0
  68. package/app/modules/quoteModule/quoteCommand.js +3 -2
  69. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  70. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  71. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
  72. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
  73. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
  74. package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
  75. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
  76. package/app/modules/statsModule/AGENT.md +1 -1
  77. package/app/modules/statsModule/commandConfig.json +58 -0
  78. package/app/modules/statsModule/rankingCommon.js +5 -4
  79. package/app/modules/stickerModule/AGENT.md +1 -1
  80. package/app/modules/stickerModule/addStickerMetadata.js +4 -3
  81. package/app/modules/stickerModule/commandConfig.json +145 -0
  82. package/app/modules/stickerModule/stickerCommand.js +1 -1
  83. package/app/modules/stickerPackModule/AGENT.md +1 -1
  84. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  85. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  86. package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
  87. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
  88. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
  89. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
  90. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
  91. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
  92. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
  93. package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
  94. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
  95. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  96. package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
  97. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
  98. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  99. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  100. package/app/modules/systemMetricsModule/pingCommand.js +6 -5
  101. package/app/modules/tiktokModule/AGENT.md +1 -1
  102. package/app/modules/tiktokModule/commandConfig.json +29 -0
  103. package/app/modules/tiktokModule/tiktokCommand.js +2 -1
  104. package/app/modules/userModule/AGENT.md +1 -1
  105. package/app/modules/userModule/commandConfig.json +29 -0
  106. package/app/modules/userModule/userCommand.js +72 -23
  107. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  108. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  109. package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
  110. package/app/observability/metrics.js +136 -0
  111. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  112. package/app/services/ai/conversationRouterService.js +4 -3
  113. package/app/services/ai/geminiService.js +132 -7
  114. package/app/services/ai/geminiService.test.js +59 -2
  115. package/app/services/ai/globalModuleAiHelpService.js +3 -2
  116. package/app/services/ai/messageCommandExecutionService.js +2 -1
  117. package/app/services/ai/moduleAiHelpCoreService.js +45 -14
  118. package/app/services/ai/moduleToolExecutorService.js +3 -2
  119. package/app/services/ai/moduleToolRegistryService.js +2 -1
  120. package/app/services/ai/toolCandidateSelectorService.js +6 -5
  121. package/app/services/auth/googleWebLinkService.js +3 -2
  122. package/app/services/auth/whatsappLoginLinkService.js +3 -2
  123. package/app/services/external/pokeApiService.js +4 -3
  124. package/app/services/group/groupMetadataService.js +24 -1
  125. package/app/services/infra/dbWriteQueue.js +57 -26
  126. package/app/services/infra/featureFlagService.js +2 -1
  127. package/app/services/messaging/captchaService.js +3 -2
  128. package/app/services/messaging/newsBroadcastService.js +846 -29
  129. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  130. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  131. package/app/services/multiSession/groupOwnershipService.js +890 -0
  132. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  133. package/app/services/multiSession/sessionRegistryService.js +293 -0
  134. package/app/services/sticker/stickerFocusService.js +11 -10
  135. package/app/store/aiPromptStore.js +36 -19
  136. package/app/store/conversationSessionStore.js +7 -6
  137. package/app/store/groupConfigStore.js +41 -5
  138. package/app/store/premiumUserStore.js +21 -7
  139. package/app/utils/antiLink/antiLinkModule.js +352 -16
  140. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  141. package/app/workers/aiLearningWorker.js +6 -5
  142. package/app/workers/commandConfigEnrichmentWorker.js +4 -3
  143. package/database/index.js +14 -8
  144. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  145. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  146. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  147. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  148. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  149. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  150. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  151. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  152. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  153. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  154. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  155. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  156. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  157. package/database/schema.sql +102 -1
  158. package/docker-compose.yml +4 -1
  159. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  160. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
  161. package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
  162. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  163. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
  164. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  165. package/docs/security/omnizap-static-security-headers.conf +25 -0
  166. package/docs/wiki/Home.md +1 -1
  167. package/ecosystem.prod.config.cjs +32 -12
  168. package/index.js +57 -23
  169. package/observability/alert-rules.yml +20 -0
  170. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  171. package/observability/mysql-setup.sql +4 -4
  172. package/observability/system-admin-observability.md +26 -0
  173. package/package.json +20 -6
  174. package/public/apple-touch-icon.png +0 -0
  175. package/public/comandos/commands-catalog.json +2853 -3326
  176. package/public/favicon-16x16.png +0 -0
  177. package/public/favicon-32x32.png +0 -0
  178. package/public/favicon.ico +0 -0
  179. package/public/js/apps/apiDocsApp.js +3 -2
  180. package/public/js/apps/commandsReactApp.js +280 -99
  181. package/public/js/apps/createPackApp.js +11 -10
  182. package/public/js/apps/homeReactApp.js +181 -130
  183. package/public/js/apps/loginReactApp.js +1 -1
  184. package/public/js/apps/stickersApp.js +263 -110
  185. package/public/js/apps/termsReactApp.js +73 -24
  186. package/public/js/apps/userApp.js +4 -3
  187. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  188. package/public/js/apps/userReactApp.js +355 -280
  189. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  190. package/public/pages/api-docs.html +1 -1
  191. package/public/pages/aup.html +2 -2
  192. package/public/pages/dpa.html +3 -3
  193. package/public/pages/licenca.html +4 -4
  194. package/public/pages/login.html +1 -1
  195. package/public/pages/notice-and-takedown.html +2 -2
  196. package/public/pages/politica-de-privacidade.html +6 -6
  197. package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
  198. package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
  199. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
  200. package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
  201. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
  202. package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
  203. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
  204. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
  205. package/public/pages/stickers-admin.html +1 -1
  206. package/public/pages/stickers-create.html +1 -1
  207. package/public/pages/stickers.html +6 -6
  208. package/public/pages/suboperadores.html +2 -2
  209. package/public/pages/termos-de-uso-texto-integral.html +6 -6
  210. package/public/pages/termos-de-uso.html +3 -3
  211. package/public/pages/user-password-reset.html +4 -5
  212. package/public/pages/user-systemadm.html +9 -463
  213. package/public/pages/user.html +2 -2
  214. package/scripts/clear-whatsapp-session.sh +123 -0
  215. package/scripts/core-ai-mode.mjs +163 -0
  216. package/scripts/deploy.sh +11 -1
  217. package/scripts/email-broadcast-terms-update.mjs +2 -1
  218. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  219. package/scripts/generate-commands-catalog.mjs +166 -2
  220. package/scripts/generate-module-agents.mjs +2 -1
  221. package/scripts/generate-seo-satellite-pages.mjs +5 -4
  222. package/scripts/github-deploy-notify.mjs +2 -1
  223. package/scripts/github-release-notify.mjs +25 -10
  224. package/scripts/new-whatsapp-session.sh +317 -0
  225. package/scripts/release.sh +2 -19
  226. package/scripts/security-smoketest.mjs +6 -5
  227. package/scripts/security-web-surface-check.mjs +218 -0
  228. package/scripts/sticker-catalog-loadtest.mjs +5 -4
  229. package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
  230. package/server/auth/jwt/webJwtService.js +1 -1
  231. package/server/auth/stickerCatalogAuthContext.js +2 -1
  232. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
  233. package/server/auth/userPassword/userPasswordAuthService.js +2 -1
  234. package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
  235. package/server/auth/webAccount/webAccountHandlers.js +9 -10
  236. package/server/controllers/admin/adminPanelHandlers.js +267 -16
  237. package/server/controllers/admin/systemAdminController.js +267 -0
  238. package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
  239. package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
  240. package/server/controllers/sticker/stickerCatalogController.js +23 -36
  241. package/server/controllers/system/contactController.js +9 -17
  242. package/server/controllers/system/githubController.js +3 -2
  243. package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
  244. package/server/controllers/system/systemController.js +254 -1
  245. package/server/controllers/system/systemMetricsController.js +2 -1
  246. package/server/controllers/userController.js +6 -0
  247. package/server/email/emailTemplateService.js +5 -3
  248. package/server/http/httpServer.js +11 -6
  249. package/server/middleware/rateLimit.js +2 -1
  250. package/server/middleware/securityHeaders.js +20 -1
  251. package/server/routes/admin/systemAdminRouter.js +6 -0
  252. package/server/routes/indexRouter.js +30 -6
  253. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  254. package/server/routes/static/staticPageRouter.js +27 -1
  255. package/server/utils/publicContact.js +31 -0
  256. package/utils/time/timeModule.js +135 -0
  257. package/utils/time/timeModule.test.js +65 -0
  258. package/utils/whatsapp/contactEnv.js +39 -0
  259. package/vite.config.mjs +7 -1
  260. package/public/assets/images/brand-icon-192.png +0 -0
  261. package/scripts/sync-readme-snapshot.mjs +0 -133
@@ -0,0 +1,80 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
6
+
7
+ export const DEFAULT_TIMEOUT_MS = Number.parseInt(process.env.PLAY_TIMEOUT_MS || '900000', 10);
8
+ export const DOWNLOAD_TIMEOUT_MS = Number.parseInt(process.env.PLAY_DOWNLOAD_TIMEOUT_MS || '1800000', 10);
9
+ export const MEDIA_INFO_TIMEOUT_MS = Number.parseInt(process.env.PLAY_MEDIA_INFO_TIMEOUT_MS || '120000', 10);
10
+ export const PLAY_YTMP3_ENABLED = String(process.env.PLAY_YTMP3_ENABLED || 'true').toLowerCase() !== 'false';
11
+ export const PLAY_YTMP3_API_BASE_URL = (process.env.PLAY_YTMP3_API_BASE_URL || 'https://hub.ytconvert.org').trim();
12
+ export const PLAY_YTMP3_API_DOWNLOAD_PATH = (process.env.PLAY_YTMP3_API_DOWNLOAD_PATH || '/api/download').trim() || '/api/download';
13
+ export const PLAY_YTMP3_POLL_INTERVAL_MS = Math.max(500, Number.parseInt(process.env.PLAY_YTMP3_POLL_INTERVAL_MS || '2000', 10) || 2000);
14
+ export const PLAY_YTMP3_SEARCH_BASE_URL = (process.env.PLAY_YTMP3_SEARCH_BASE_URL || 'https://yt-meta.ytconvert.org').trim();
15
+ export const PLAY_YTMP3_SEARCH_PATH = (process.env.PLAY_YTMP3_SEARCH_PATH || '/search').trim() || '/search';
16
+ export const PLAY_YTMP3_VIDEO_DEFAULT_QUALITY = (process.env.PLAY_YTMP3_VIDEO_DEFAULT_QUALITY || '720').trim();
17
+
18
+ const PLAY_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
19
+ export const PLAY_LOCAL_DIR = path.join(PLAY_MODULE_DIR, 'local');
20
+ export const PLAY_DOWNLOADS_DIR = path.join(PLAY_LOCAL_DIR, 'downloads');
21
+ export const MAX_SEARCH_RESULTS = Math.min(10, Math.max(1, Number.parseInt(process.env.PLAY_SEARCH_RESULTS || '5', 10)));
22
+
23
+ const MAX_MEDIA_MB = Number.parseInt(process.env.PLAY_MAX_MB || '100', 10);
24
+ export const MAX_MEDIA_BYTES = Number.isFinite(MAX_MEDIA_MB) ? MAX_MEDIA_MB * 1024 * 1024 : 100 * 1024 * 1024;
25
+ export const MAX_MEDIA_MB_LABEL = Number.isFinite(MAX_MEDIA_MB) ? MAX_MEDIA_MB : 100;
26
+
27
+ export const THUMBNAIL_TIMEOUT_MS = 15000;
28
+ export const MAX_THUMB_BYTES = 5 * 1024 * 1024;
29
+ export const VIDEO_PROCESS_TIMEOUT_MS = Number.parseInt(process.env.PLAY_VIDEO_PROCESS_TIMEOUT_MS || '420000', 10);
30
+ export const VIDEO_FORCE_TRANSCODE = String(process.env.PLAY_VIDEO_FORCE_TRANSCODE || 'true').toLowerCase() !== 'false';
31
+ export const FFMPEG_BIN = (process.env.FFMPEG_PATH || 'ffmpeg').trim();
32
+ export const FFPROBE_BIN = (process.env.FFPROBE_PATH || 'ffprobe').trim();
33
+
34
+ export const SEARCH_CACHE_TTL_MS = 60 * 1000;
35
+ export const MAX_SEARCH_CACHE_ENTRIES = 500;
36
+ export const MAX_REDIRECTS = 2;
37
+ export const MAX_ERROR_BODY_BYTES = 64 * 1024;
38
+ export const MAX_META_BODY_CHARS = 512;
39
+
40
+ export const TRANSIENT_HTTP_STATUSES = new Set([502, 503, 504]);
41
+ export const TRANSIENT_NETWORK_CODES = new Set(['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN']);
42
+
43
+ export const YTDLS_ENDPOINTS = {
44
+ search: 'local:search',
45
+ queueStatus: 'local:queue-status',
46
+ download: 'local:download',
47
+ thumbnail: 'thumbnail',
48
+ ytmp3Create: 'ytmp3:create',
49
+ ytmp3Poll: 'ytmp3:poll',
50
+ ytmp3Download: 'ytmp3:download',
51
+ ytmp3Search: 'ytmp3:search',
52
+ ytmp3Metadata: 'ytmp3:metadata',
53
+ };
54
+
55
+ export const ERROR_CODES = {
56
+ INVALID_INPUT: 'EINVALID_INPUT',
57
+ API: 'EAPI',
58
+ TIMEOUT: 'ETIMEOUT',
59
+ TOO_BIG: 'ETOOBIG',
60
+ NOT_FOUND: 'ENOTFOUND',
61
+ };
62
+
63
+ export const KNOWN_ERROR_CODES = new Set(Object.values(ERROR_CODES));
64
+
65
+ export const TYPE_CONFIG = {
66
+ audio: {
67
+ waitText: '⏳ Processando sua mídia...',
68
+ queueWaitText: '⏳ Processando...',
69
+ readyTitle: '🎵 Áudio pronto!',
70
+ mimeFallback: 'audio/mpeg',
71
+ },
72
+ video: {
73
+ waitText: '⏳ Processando sua mídia...',
74
+ queueWaitText: '⏳ Processando...',
75
+ readyTitle: '🎬 Vídeo pronto!',
76
+ mimeFallback: 'video/mp4',
77
+ },
78
+ };
79
+
80
+ export const FILE_ACCESS_MODE = os.platform() === 'win32' ? 'F_OK' : 'X_OK';
@@ -0,0 +1,361 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
+ import crypto from 'node:crypto';
3
+ import logger from '#logger';
4
+ import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
5
+ import { getAdminJid } from '../../config/index.js';
6
+ import { getPlayOperationalLimits, getPlayText, getPlayUsageFallbackText, getPlayUsageText, getPlayWaitText } from './playConfigRuntime.js';
7
+ import { DEFAULT_COMMAND_PREFIX, ERROR_CODES, KNOWN_ERROR_CODES, TYPE_CONFIG, YTDLS_ENDPOINTS } from './playCommandConstants.js';
8
+ import { createError, withErrorMeta, normalizePlayError, truncateText, playMediaClient, formatters, fileUtils, isYouTubeBotCheckCause, buildYouTubeBotCheckUserMessage } from './playCommandMediaClient.js';
9
+
10
+ const adminJid = getAdminJid();
11
+ const adminAlertDedupCache = new Map();
12
+
13
+ export { DEFAULT_COMMAND_PREFIX };
14
+
15
+ const buildRequestId = () => {
16
+ if (typeof crypto.randomUUID === 'function') {
17
+ return crypto.randomUUID();
18
+ }
19
+ return `${__timeNowMs()}-${Math.random().toString(16).slice(2)}`;
20
+ };
21
+
22
+ const isTechnicalError = (error) => Boolean(error?.meta?.technical);
23
+
24
+ const getUserErrorMessage = (error) => {
25
+ const genericError = getPlayText('generic_error', 'Erro inesperado ao processar sua solicitação.');
26
+ if (isTechnicalError(error)) {
27
+ if (error?.code === ERROR_CODES.TIMEOUT) {
28
+ return getPlayText('user_error_timeout', 'A operação demorou mais que o esperado. Tente novamente.');
29
+ }
30
+ return getPlayText('user_error_technical_generic', 'Não foi possível processar sua solicitação agora. Tente novamente em instantes.');
31
+ }
32
+ if (!error) return genericError;
33
+ if (KNOWN_ERROR_CODES.has(error?.code) && error?.message) return error.message;
34
+ return genericError;
35
+ };
36
+
37
+ const buildAdminFailureText = (error, context = {}) => {
38
+ const adminTitle = getPlayText('admin_error_title', 'Erro no módulo play (diagnóstico).');
39
+ const cause = truncateText(error?.meta?.cause || error?.stack || error?.message || '', 1200);
40
+ const lines = [adminTitle, `Chat: ${context?.remoteJid || 'n/a'}`, `Request: ${context?.requestId || error?.meta?.requestId || 'n/a'}`, `Tipo: ${context?.type || error?.meta?.type || 'n/a'}`, `Code: ${error?.code || 'n/a'}`, `Endpoint: ${error?.meta?.endpoint || 'n/a'}`, `Status: ${error?.meta?.status || 'n/a'}`, `RawCode: ${error?.meta?.rawCode || 'n/a'}`, `ExitCode: ${error?.meta?.exitCode ?? 'n/a'}`, `Signal: ${error?.meta?.signal || 'n/a'}`, `Input: ${truncateText(error?.meta?.input || '', 300) || 'n/a'}`, `FilePath: ${error?.meta?.filePath || 'n/a'}`, `Mensagem usuário: ${getUserErrorMessage(error)}`, `Causa técnica: ${cause || 'n/a'}`];
41
+ return lines.join('\n');
42
+ };
43
+
44
+ const buildAdminAlertDedupKey = (error, context = {}) => {
45
+ const causeKey = truncateText(error?.meta?.cause || error?.message || '', 160);
46
+ return [context?.type || error?.meta?.type || 'n/a', error?.code || 'n/a', error?.meta?.endpoint || 'n/a', error?.meta?.status || 'n/a', error?.meta?.rawCode || 'n/a', causeKey || 'n/a'].join('|');
47
+ };
48
+
49
+ const pruneAdminAlertDedupCache = (nowMs, dedupeWindowMs) => {
50
+ const maxAge = Math.max(60_000, dedupeWindowMs * 2);
51
+ for (const [key, timestamp] of adminAlertDedupCache.entries()) {
52
+ if (!Number.isFinite(timestamp) || nowMs - timestamp > maxAge) {
53
+ adminAlertDedupCache.delete(key);
54
+ }
55
+ }
56
+ };
57
+
58
+ const shouldNotifyAdminAlert = (error, context = {}) => {
59
+ if (!adminJid) return false;
60
+ if (!isTechnicalError(error)) return false;
61
+
62
+ const limits = getPlayOperationalLimits();
63
+ const dedupeWindowMs = Number(limits?.admin_alert_dedupe_window_ms ?? 120000);
64
+ if (!Number.isFinite(dedupeWindowMs) || dedupeWindowMs <= 0) {
65
+ return true;
66
+ }
67
+
68
+ const nowMs = __timeNowMs();
69
+ const dedupeKey = buildAdminAlertDedupKey(error, context);
70
+ const lastSentAt = adminAlertDedupCache.get(dedupeKey);
71
+ if (Number.isFinite(lastSentAt) && nowMs - lastSentAt < dedupeWindowMs) {
72
+ return false;
73
+ }
74
+
75
+ adminAlertDedupCache.set(dedupeKey, nowMs);
76
+ pruneAdminAlertDedupCache(nowMs, dedupeWindowMs);
77
+ return true;
78
+ };
79
+
80
+ const resetAdminAlertDedupCacheForTests = () => {
81
+ adminAlertDedupCache.clear();
82
+ };
83
+
84
+ const notifyFailure = async (sock, remoteJid, messageInfo, expirationMessage, error, context) => {
85
+ const errorMessage = getUserErrorMessage(error);
86
+ const errorPrefix = getPlayText('error_prefix', '❌ Erro: ');
87
+
88
+ await sendAndStore(sock, remoteJid, { text: `${errorPrefix}${errorMessage}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
89
+
90
+ if (shouldNotifyAdminAlert(error, context)) {
91
+ await sendAndStore(sock, adminJid, {
92
+ text: buildAdminFailureText(error, { ...(context || {}), remoteJid }),
93
+ });
94
+ }
95
+ };
96
+
97
+ const processPlayRequest = async ({ sock, remoteJid, messageInfo, expirationMessage, text, type }) => {
98
+ const startTime = __timeNowMs();
99
+ const requestId = buildRequestId();
100
+ const config = TYPE_CONFIG[type];
101
+
102
+ if (!config) {
103
+ throw createError(ERROR_CODES.INVALID_INPUT, getPlayText('invalid_media_type', 'Tipo de mídia inválido.'), { technical: false });
104
+ }
105
+
106
+ logger.info('Play request iniciado.', {
107
+ requestId,
108
+ remoteJid,
109
+ type,
110
+ elapsedMs: 0,
111
+ });
112
+
113
+ let filePath = null;
114
+
115
+ try {
116
+ const candidateLinks = await playMediaClient.resolveYoutubeCandidates(text);
117
+ await sendAndStore(sock, remoteJid, { text: getPlayWaitText(type) || config.waitText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
118
+
119
+ let downloadResult = null;
120
+ let videoInfo = null;
121
+ let selectedLink = null;
122
+ let lastDownloadError = null;
123
+
124
+ for (let index = 0; index < candidateLinks.length; index += 1) {
125
+ const candidateLink = candidateLinks[index];
126
+ selectedLink = candidateLink;
127
+ try {
128
+ [downloadResult, videoInfo] = await Promise.all([playMediaClient.requestDownloadToFile(candidateLink, type, requestId), playMediaClient.fetchVideoInfo(candidateLink, text)]);
129
+ lastDownloadError = null;
130
+ break;
131
+ } catch (error) {
132
+ lastDownloadError = error;
133
+ if (isYouTubeBotCheckCause(error)) {
134
+ logger.warn('Play download: bloqueio anti-bot detectado; abortando novas tentativas de candidato.', {
135
+ requestId,
136
+ remoteJid,
137
+ type,
138
+ endpoint: error?.meta?.endpoint || YTDLS_ENDPOINTS.download,
139
+ attempt: index + 1,
140
+ candidateLink,
141
+ cause: truncateText(error?.meta?.cause || error?.message || ''),
142
+ });
143
+ throw withErrorMeta(createError(ERROR_CODES.API, buildYouTubeBotCheckUserMessage()), {
144
+ endpoint: error?.meta?.endpoint || YTDLS_ENDPOINTS.download,
145
+ cause: error?.meta?.cause || error?.message || '',
146
+ technical: false,
147
+ });
148
+ }
149
+
150
+ const hasNextCandidate = index < candidateLinks.length - 1;
151
+ if (!hasNextCandidate) {
152
+ throw error;
153
+ }
154
+ }
155
+ }
156
+
157
+ if (!downloadResult) {
158
+ throw (
159
+ lastDownloadError ||
160
+ createError(ERROR_CODES.API, getPlayText('download_failed', 'Falha ao baixar o arquivo localmente.'), {
161
+ endpoint: YTDLS_ENDPOINTS.download,
162
+ requestId,
163
+ technical: true,
164
+ })
165
+ );
166
+ }
167
+
168
+ filePath = downloadResult.filePath;
169
+ const deliveredType = downloadResult.mediaType || type;
170
+ const deliveredConfig = TYPE_CONFIG[deliveredType] || config;
171
+ const fallbackToAudio = type === 'video' && deliveredType === 'audio';
172
+
173
+ logger.info('Play download concluído.', {
174
+ requestId,
175
+ remoteJid,
176
+ type,
177
+ deliveredType,
178
+ fallbackToAudio,
179
+ endpoint: YTDLS_ENDPOINTS.download,
180
+ selectedLink: selectedLink || null,
181
+ elapsedMs: __timeNowMs() - startTime,
182
+ bytes: downloadResult.bytes || 0,
183
+ });
184
+
185
+ if (fallbackToAudio) {
186
+ await sendAndStore(sock, remoteJid, { text: getPlayText('video_fallback_to_audio', '⚠️ Este link retornou somente áudio. Enviando no formato de áudio.') }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
187
+ }
188
+
189
+ if (deliveredType === 'audio') {
190
+ const infoText = formatters.formatVideoInfo(videoInfo);
191
+ const caption = formatters.buildReadyCaption(deliveredType, infoText);
192
+ const thumbUrl = formatters.getThumbnailUrl(videoInfo);
193
+ let thumbBuffer = null;
194
+ let previewDelivered = false;
195
+
196
+ if (thumbUrl) {
197
+ try {
198
+ thumbBuffer = await playMediaClient.fetchThumbnailBuffer(thumbUrl);
199
+ } catch (error) {
200
+ logger.warn('Falha ao baixar thumbnail.', {
201
+ requestId,
202
+ remoteJid,
203
+ type: deliveredType,
204
+ requestedType: type,
205
+ endpoint: error?.meta?.endpoint || YTDLS_ENDPOINTS.thumbnail,
206
+ status: error?.meta?.status || null,
207
+ code: error?.code,
208
+ error: truncateText(error?.message || ''),
209
+ elapsedMs: __timeNowMs() - startTime,
210
+ });
211
+ }
212
+ }
213
+
214
+ if (thumbBuffer) {
215
+ try {
216
+ await sendAndStore(sock, remoteJid, { image: thumbBuffer, caption }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
217
+ previewDelivered = true;
218
+ } catch (error) {
219
+ logger.warn('Falha ao enviar thumbnail de áudio.', {
220
+ requestId,
221
+ remoteJid,
222
+ type: deliveredType,
223
+ requestedType: type,
224
+ code: error?.code || null,
225
+ error: truncateText(error?.message || ''),
226
+ elapsedMs: __timeNowMs() - startTime,
227
+ });
228
+ }
229
+ }
230
+
231
+ if (!previewDelivered && caption) {
232
+ try {
233
+ await sendAndStore(sock, remoteJid, { text: caption }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
234
+ } catch (error) {
235
+ logger.warn('Falha ao enviar preview textual do áudio.', {
236
+ requestId,
237
+ remoteJid,
238
+ type: deliveredType,
239
+ requestedType: type,
240
+ code: error?.code || null,
241
+ error: truncateText(error?.message || ''),
242
+ elapsedMs: __timeNowMs() - startTime,
243
+ });
244
+ }
245
+ }
246
+
247
+ await sendAndStore(
248
+ sock,
249
+ remoteJid,
250
+ {
251
+ audio: { url: filePath },
252
+ mimetype: downloadResult.contentType || deliveredConfig.mimeFallback,
253
+ ptt: false,
254
+ },
255
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
256
+ );
257
+
258
+ logger.info('Play áudio enviado.', {
259
+ requestId,
260
+ remoteJid,
261
+ type: deliveredType,
262
+ requestedType: type,
263
+ fallbackToAudio,
264
+ bytes: downloadResult.bytes || 0,
265
+ elapsedMs: __timeNowMs() - startTime,
266
+ });
267
+
268
+ return;
269
+ }
270
+
271
+ const infoText = formatters.formatVideoInfo(videoInfo);
272
+ const caption = formatters.buildReadyCaption(deliveredType, infoText);
273
+
274
+ await sendAndStore(
275
+ sock,
276
+ remoteJid,
277
+ {
278
+ video: { url: filePath },
279
+ mimetype: downloadResult.contentType || deliveredConfig.mimeFallback,
280
+ caption,
281
+ },
282
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
283
+ );
284
+
285
+ logger.info('Play vídeo enviado.', {
286
+ requestId,
287
+ remoteJid,
288
+ type: deliveredType,
289
+ requestedType: type,
290
+ bytes: downloadResult.bytes || 0,
291
+ elapsedMs: __timeNowMs() - startTime,
292
+ });
293
+ } catch (error) {
294
+ if (!filePath && error?.meta?.filePath) {
295
+ filePath = error.meta.filePath;
296
+ }
297
+
298
+ const normalizedError = withErrorMeta(normalizePlayError(error), {
299
+ requestId,
300
+ remoteJid,
301
+ type,
302
+ });
303
+
304
+ logger.error('Play falhou.', {
305
+ requestId,
306
+ remoteJid,
307
+ type,
308
+ endpoint: normalizedError?.meta?.endpoint || null,
309
+ status: normalizedError?.meta?.status || null,
310
+ elapsedMs: __timeNowMs() - startTime,
311
+ error: truncateText(normalizedError.message || ''),
312
+ cause: truncateText(normalizedError?.meta?.cause || ''),
313
+ code: normalizedError.code,
314
+ });
315
+
316
+ throw normalizedError;
317
+ } finally {
318
+ await fileUtils.safeUnlink(filePath);
319
+ }
320
+ };
321
+
322
+ const playService = {
323
+ processPlayRequest,
324
+ };
325
+
326
+ const resolveCommandNameByType = (type) => (type === 'audio' ? 'play' : 'playvid');
327
+
328
+ export const handleTypedPlayCommand = async ({ sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix, type }) => {
329
+ try {
330
+ if (!text?.trim()) {
331
+ const commandName = resolveCommandNameByType(type);
332
+ const usageText = getPlayUsageText(commandName, { commandPrefix }) || getPlayUsageFallbackText(type, commandPrefix);
333
+
334
+ await sendAndStore(sock, remoteJid, { text: usageText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
335
+ return;
336
+ }
337
+
338
+ await playService.processPlayRequest({
339
+ sock,
340
+ remoteJid,
341
+ messageInfo,
342
+ expirationMessage,
343
+ text,
344
+ type,
345
+ });
346
+ } catch (error) {
347
+ await notifyFailure(sock, remoteJid, messageInfo, expirationMessage, error, {
348
+ type,
349
+ requestId: error?.meta?.requestId,
350
+ });
351
+ }
352
+ };
353
+
354
+ export const __playCommandCoreTestUtils = {
355
+ isTechnicalError,
356
+ getUserErrorMessage,
357
+ buildAdminFailureText,
358
+ shouldNotifyAdminAlert,
359
+ resetAdminAlertDedupCacheForTests,
360
+ notifyFailure,
361
+ };
@@ -0,0 +1,41 @@
1
+ import { DEFAULT_COMMAND_PREFIX, handleTypedPlayCommand } from './playCommandCore.js';
2
+
3
+ /**
4
+ * Handler do comando play (audio).
5
+ * @param {object} sock
6
+ * @param {string} remoteJid
7
+ * @param {object} messageInfo
8
+ * @param {number} expirationMessage
9
+ * @param {string} text
10
+ * @returns {Promise<void>}
11
+ */
12
+ export const handlePlayCommand = async (sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix = DEFAULT_COMMAND_PREFIX) =>
13
+ handleTypedPlayCommand({
14
+ sock,
15
+ remoteJid,
16
+ messageInfo,
17
+ expirationMessage,
18
+ text,
19
+ commandPrefix,
20
+ type: 'audio',
21
+ });
22
+
23
+ /**
24
+ * Handler do comando playvid (video).
25
+ * @param {object} sock
26
+ * @param {string} remoteJid
27
+ * @param {object} messageInfo
28
+ * @param {number} expirationMessage
29
+ * @param {string} text
30
+ * @returns {Promise<void>}
31
+ */
32
+ export const handlePlayVidCommand = async (sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix = DEFAULT_COMMAND_PREFIX) =>
33
+ handleTypedPlayCommand({
34
+ sock,
35
+ remoteJid,
36
+ messageInfo,
37
+ expirationMessage,
38
+ text,
39
+ commandPrefix,
40
+ type: 'video',
41
+ });