@omnizap-system/omnizap 2.6.1 → 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 (156) hide show
  1. package/.env.example +54 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -1,5 +1,6 @@
1
1
  import logger from '#logger';
2
2
  import { buildAnimeMenu, buildAiMenu, buildMediaMenu, buildMenuCaption, buildQuoteMenu, buildStatsMenu, buildStickerMenu, buildAdminMenu } from './common.js';
3
+ import { resolveDynamicMenuText } from './menuDynamicService.js';
3
4
  import getImageBuffer from '../../utils/http/getImageBufferModule.js';
4
5
  import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
5
6
 
@@ -11,10 +12,13 @@ const sanitizeLogValue = (value) =>
11
12
  .trim();
12
13
 
13
14
  const sendMenuImage = async (sock, remoteJid, messageInfo, expirationMessage, caption) => {
15
+ const safeCaption = String(caption || '').trim() || 'Menu indisponível no momento.';
14
16
  const imageUrl = process.env[MENU_IMAGE_ENV];
15
17
  if (!imageUrl) {
16
- logger.error('IMAGE_MENU environment variable not set.');
17
- await sendAndStore(sock, remoteJid, { text: 'Ocorreu um erro ao carregar o menu.' }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
18
+ logger.warn('IMAGE_MENU environment variable not set. Sending plain-text menu fallback.', {
19
+ action: 'menu_image_env_missing',
20
+ });
21
+ await sendAndStore(sock, remoteJid, { text: safeCaption }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
18
22
  return;
19
23
  }
20
24
 
@@ -25,7 +29,7 @@ const sendMenuImage = async (sock, remoteJid, messageInfo, expirationMessage, ca
25
29
  remoteJid,
26
30
  {
27
31
  image: imageBuffer,
28
- caption,
32
+ caption: safeCaption,
29
33
  },
30
34
  { quoted: messageInfo, ephemeralExpiration: expirationMessage },
31
35
  );
@@ -33,11 +37,22 @@ const sendMenuImage = async (sock, remoteJid, messageInfo, expirationMessage, ca
33
37
  logger.error('Error fetching menu image.', {
34
38
  error: sanitizeLogValue(error?.message) || 'unknown_error',
35
39
  });
36
- await sendAndStore(sock, remoteJid, { text: 'Ocorreu um erro ao carregar a imagem do menu.' }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
40
+ await sendAndStore(sock, remoteJid, { text: safeCaption }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
37
41
  }
38
42
  };
39
43
 
40
44
  export async function handleMenuCommand(sock, remoteJid, messageInfo, expirationMessage, senderName, commandPrefix, args = []) {
45
+ const dynamicCaption = await resolveDynamicMenuText({
46
+ args,
47
+ senderName,
48
+ commandPrefix,
49
+ remoteJid,
50
+ });
51
+ if (dynamicCaption) {
52
+ await sendMenuImage(sock, remoteJid, messageInfo, expirationMessage, dynamicCaption.trim());
53
+ return;
54
+ }
55
+
41
56
  const category = args?.[0]?.toLowerCase();
42
57
  const categoryMap = new Map([
43
58
  ['figurinhas', (prefix) => buildStickerMenu(prefix)],
@@ -61,6 +76,22 @@ export async function handleMenuCommand(sock, remoteJid, messageInfo, expiration
61
76
  await sendMenuImage(sock, remoteJid, messageInfo, expirationMessage, caption);
62
77
  }
63
78
 
64
- export async function handleMenuAdmCommand(sock, remoteJid, messageInfo, expirationMessage, commandPrefix) {
79
+ export async function handleMenuAdmCommand(sock, remoteJid, messageInfo, expirationMessage, commandPrefix, args = []) {
80
+ const safeArgs = Array.isArray(args) ? args.map((value) => String(value || '').trim()).filter(Boolean) : [];
81
+ const dynamicArgs = safeArgs.length ? safeArgs : ['categoria', 'admin'];
82
+ const dynamicCaption = await resolveDynamicMenuText({
83
+ args: dynamicArgs,
84
+ senderName: 'admin',
85
+ commandPrefix,
86
+ remoteJid,
87
+ menuCommandName: 'menuadm',
88
+ categoryScopeKeys: ['admin'],
89
+ });
90
+
91
+ if (dynamicCaption) {
92
+ await sendMenuImage(sock, remoteJid, messageInfo, expirationMessage, dynamicCaption.trim());
93
+ return;
94
+ }
95
+
65
96
  await sendMenuImage(sock, remoteJid, messageInfo, expirationMessage, buildAdminMenu(commandPrefix).trim());
66
97
  }
@@ -7,13 +7,18 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
7
7
  - arquivo_base: `app/modules/playModule/commandConfig.json`
8
8
  - schema_version: `2.0.0`
9
9
  - module_enabled: `true`
10
- - generated_at: `2026-03-11T02:35:17.177Z`
10
+ - generated_at: `2026-03-17T04:04:14.195Z`
11
11
 
12
12
  ## Escopo do Modulo
13
13
 
14
14
  - module: `playModule`
15
15
  - source_files:
16
16
  - playCommand.js
17
+ - playCommandHandlers.js
18
+ - playCommandCore.js
19
+ - playCommandMediaClient.js
20
+ - playCommandConstants.js
21
+ - playConfigRuntime.js
17
22
  - total_commands: `2`
18
23
  - total_enabled_commands: `2`
19
24
 
@@ -119,10 +124,10 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
119
124
  - texto do comando e argumentos
120
125
  - contexto da mensagem (citacao e mencoes, quando existir)
121
126
  - link ou termo de busca enviado no comando
122
- - metadados de fila/download retornados pelo servico de midia
127
+ - metadados de busca/download retornados pelo provedor de midia
123
128
  - informacoes de tamanho do arquivo para validacao de limite
124
129
  - dependencias_externas:
125
- - serviço YTDLS/Downloader
130
+ - provedor de midia local (ytmp3 via API)
126
131
  - ffmpeg
127
132
  - ffprobe
128
133
  - efeitos_colaterais:
@@ -244,10 +249,10 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
244
249
  - texto do comando e argumentos
245
250
  - contexto da mensagem (citacao e mencoes, quando existir)
246
251
  - link ou termo de busca enviado no comando
247
- - metadados de fila/download retornados pelo servico de midia
252
+ - metadados de busca/download retornados pelo provedor de midia
248
253
  - informacoes de tamanho e formato de video para validacao
249
254
  - dependencias_externas:
250
- - serviço YTDLS/Downloader
255
+ - provedor de midia local (ytmp3 via API)
251
256
  - ffmpeg
252
257
  - ffprobe
253
258
  - efeitos_colaterais:
@@ -2,7 +2,7 @@
2
2
  "schema_version": "2.0.0",
3
3
  "module": "playModule",
4
4
  "enabled": true,
5
- "source_files": ["playCommand.js", "playCommandHandlers.js", "playCommandCore.js", "playCommandYtDlpClient.js", "playCommandConstants.js", "playConfigRuntime.js"],
5
+ "source_files": ["playCommand.js", "playCommandHandlers.js", "playCommandCore.js", "playCommandMediaClient.js", "playCommandConstants.js", "playConfigRuntime.js"],
6
6
  "defaults": {
7
7
  "inheritance_mode": "deep_merge_with_command_overrides",
8
8
  "compatibility_mode": "legacy_and_v2_fields",
@@ -62,8 +62,8 @@
62
62
  "janela_ms": null,
63
63
  "escopo": "sem_rate_limit_explicito"
64
64
  },
65
- "dependencies": ["yt-dlp local (binario gerenciado pelo modulo)", "ffmpeg", "ffprobe"],
66
- "dependencias_externas": ["yt-dlp local (binario gerenciado pelo modulo)", "ffmpeg", "ffprobe"],
65
+ "dependencies": ["provedor de midia local (ytmp3 via API)", "ffmpeg", "ffprobe"],
66
+ "dependencias_externas": ["provedor de midia local (ytmp3 via API)", "ffmpeg", "ffprobe"],
67
67
  "responses": {
68
68
  "success": "Comando executado com sucesso.",
69
69
  "usage_error": "Formato de uso inválido. Consulte metodos_de_uso.",
@@ -152,7 +152,7 @@
152
152
  "permissao_necessaria": "usuario comum",
153
153
  "limite_de_uso": "arquivo ate PLAY_MAX_MB (padrao 100 MB)",
154
154
  "local_de_uso": ["privado", "grupo"],
155
- "informacoes_coletadas": ["identificador do chat (remoteJid)", "identificador do remetente (senderJid)", "texto do comando e argumentos", "contexto da mensagem (citacao e mencoes, quando existir)", "link ou termo de busca enviado no comando", "metadados de busca/download retornados pelo yt-dlp local", "informacoes de tamanho do arquivo para validacao de limite"],
155
+ "informacoes_coletadas": ["identificador do chat (remoteJid)", "identificador do remetente (senderJid)", "texto do comando e argumentos", "contexto da mensagem (citacao e mencoes, quando existir)", "link ou termo de busca enviado no comando", "metadados de busca/download retornados pelo provedor de midia", "informacoes de tamanho do arquivo para validacao de limite"],
156
156
  "enabled": true,
157
157
  "categoria": "midia",
158
158
  "subcomandos": [],
@@ -179,7 +179,7 @@
179
179
  "janela_ms": null,
180
180
  "escopo": "sem_rate_limit_explicito"
181
181
  },
182
- "dependencias_externas": ["yt-dlp local (binario gerenciado pelo modulo)", "ffmpeg", "ffprobe"],
182
+ "dependencias_externas": ["provedor de midia local (ytmp3 via API)", "ffmpeg", "ffprobe"],
183
183
  "efeitos_colaterais": ["baixa mídia temporária", "envia áudio no chat"],
184
184
  "respostas_padrao": {
185
185
  "sucesso": "Comando executado com sucesso.",
@@ -228,7 +228,7 @@
228
228
  },
229
229
  "permission": "usuario comum",
230
230
  "contexts": ["privado", "grupo"],
231
- "collected_data": ["identificador do chat (remoteJid)", "identificador do remetente (senderJid)", "texto do comando e argumentos", "contexto da mensagem (citacao e mencoes, quando existir)", "link ou termo de busca enviado no comando", "metadados de busca/download retornados pelo yt-dlp local", "informacoes de tamanho do arquivo para validacao de limite"],
231
+ "collected_data": ["identificador do chat (remoteJid)", "identificador do remetente (senderJid)", "texto do comando e argumentos", "contexto da mensagem (citacao e mencoes, quando existir)", "link ou termo de busca enviado no comando", "metadados de busca/download retornados pelo provedor de midia", "informacoes de tamanho do arquivo para validacao de limite"],
232
232
  "requirements": {
233
233
  "require_group": false,
234
234
  "require_group_admin": false,
@@ -290,7 +290,7 @@
290
290
  }
291
291
  }
292
292
  },
293
- "dependencies": ["yt-dlp local (binario gerenciado pelo modulo)", "ffmpeg", "ffprobe"],
293
+ "dependencies": ["provedor de midia local (ytmp3 via API)", "ffmpeg", "ffprobe"],
294
294
  "side_effects": ["baixa mídia temporária", "envia áudio no chat"],
295
295
  "observability": {
296
296
  "event_name": "command.executed",
@@ -357,6 +357,35 @@
357
357
  "schema": "legacy_v1_and_v2",
358
358
  "legacy_name": "tocar",
359
359
  "legacy_fields_present": ["descricao", "metodos_de_uso", "permissao_necessaria", "local_de_uso", "informacoes_coletadas", "argumentos", "pre_condicoes", "dependencias_externas", "efeitos_colaterais", "observabilidade", "privacidade", "acesso", "limite_uso_por_plano"]
360
+ },
361
+ "user_experience": {
362
+ "resumo_usuario": "Tocar reproduz/gera áudio a partir de um link do YouTube ou de um termo de busca. Disponível para todos (não requer premium).",
363
+ "quando_usar": ["Quando quiser ouvir o áudio de um vídeo do YouTube sem abrir o navegador.", "Quando você tem um link do YouTube e quer ouvir rapidamente no chat.", "Quando prefere buscar por um termo e ouvir o áudio resultante."],
364
+ "exemplos_reais": [
365
+ {
366
+ "situacao": "Usuário tem o link de um vídeo e quer ouvir o áudio.",
367
+ "comando": "<prefix>tocar https://youtube.com/watch?v=EXEMPLO",
368
+ "resposta_esperada": "Comando executado com sucesso.",
369
+ "variacao": "<prefix>tocar https://youtu.be/EXEMPLO."
370
+ },
371
+ {
372
+ "situacao": "Usuário quer ouvir áudio a partir de um termo de busca.",
373
+ "comando": "<prefix>tocar melhor trilha sonora de cinema",
374
+ "resposta_esperada": "Comando executado com sucesso.",
375
+ "variacao": "<prefix>tocar <termo de busca>."
376
+ },
377
+ {
378
+ "situacao": "Usuário envia o comando sem argumento ou com formato incorreto.",
379
+ "comando": "<prefix>tocar",
380
+ "resposta_esperada": "Formato de uso inválido. Consulte metodos_de_uso.",
381
+ "variacao": "<prefix>tocar <link>."
382
+ }
383
+ ],
384
+ "resposta_esperada": ["Comando executado com sucesso.", "Formato de uso inválido. Consulte metodos_de_uso.", "Permissão insuficiente para executar este comando."],
385
+ "erros_comuns_usuario": ["Não fornecer argumento (link ou termo de busca).", "Link não é de vídeo YouTube válido.", "Formato do comando não segue <prefix>tocar <link> ou <prefix>tocar <termo>.", "Não estar logado com a conta Google quando solicitado.", "Conectividade ruim ou serviço temporariamente indisponível."],
386
+ "passos_se_der_erro": ["Verifique se o comando usa <prefix>tocar <link> ou <prefix>tocar <termo>.", "Copie o link direto do YouTube ou revise o termo de busca.", "Tente novamente em instantes.", "Caso persista, confirme que a conta Google está disponível e tente novamente.", "Se ainda falhar, informe o contexto (link ou termo usado) para suporte."],
387
+ "resumo_usuario_origem": "auto_ia_assistida",
388
+ "resumo_usuario_revisao_pendente": true
360
389
  }
361
390
  },
362
391
  {
@@ -370,7 +399,7 @@
370
399
  "permissao_necessaria": "usuario comum",
371
400
  "limite_de_uso": "arquivo ate PLAY_MAX_MB (padrao 100 MB)",
372
401
  "local_de_uso": ["privado", "grupo"],
373
- "informacoes_coletadas": ["identificador do chat (remoteJid)", "identificador do remetente (senderJid)", "texto do comando e argumentos", "contexto da mensagem (citacao e mencoes, quando existir)", "link ou termo de busca enviado no comando", "metadados de busca/download retornados pelo yt-dlp local", "informacoes de tamanho e formato de video para validacao"],
402
+ "informacoes_coletadas": ["identificador do chat (remoteJid)", "identificador do remetente (senderJid)", "texto do comando e argumentos", "contexto da mensagem (citacao e mencoes, quando existir)", "link ou termo de busca enviado no comando", "metadados de busca/download retornados pelo provedor de midia", "informacoes de tamanho e formato de video para validacao"],
374
403
  "enabled": true,
375
404
  "categoria": "midia",
376
405
  "subcomandos": [],
@@ -397,7 +426,7 @@
397
426
  "janela_ms": null,
398
427
  "escopo": "sem_rate_limit_explicito"
399
428
  },
400
- "dependencias_externas": ["yt-dlp local (binario gerenciado pelo modulo)", "ffmpeg", "ffprobe"],
429
+ "dependencias_externas": ["provedor de midia local (ytmp3 via API)", "ffmpeg", "ffprobe"],
401
430
  "efeitos_colaterais": ["baixa mídia temporária", "envia vídeo no chat"],
402
431
  "respostas_padrao": {
403
432
  "sucesso": "Comando executado com sucesso.",
@@ -446,7 +475,7 @@
446
475
  },
447
476
  "permission": "usuario comum",
448
477
  "contexts": ["privado", "grupo"],
449
- "collected_data": ["identificador do chat (remoteJid)", "identificador do remetente (senderJid)", "texto do comando e argumentos", "contexto da mensagem (citacao e mencoes, quando existir)", "link ou termo de busca enviado no comando", "metadados de busca/download retornados pelo yt-dlp local", "informacoes de tamanho e formato de video para validacao"],
478
+ "collected_data": ["identificador do chat (remoteJid)", "identificador do remetente (senderJid)", "texto do comando e argumentos", "contexto da mensagem (citacao e mencoes, quando existir)", "link ou termo de busca enviado no comando", "metadados de busca/download retornados pelo provedor de midia", "informacoes de tamanho e formato de video para validacao"],
450
479
  "requirements": {
451
480
  "require_group": false,
452
481
  "require_group_admin": false,
@@ -508,7 +537,7 @@
508
537
  }
509
538
  }
510
539
  },
511
- "dependencies": ["yt-dlp local (binario gerenciado pelo modulo)", "ffmpeg", "ffprobe"],
540
+ "dependencies": ["provedor de midia local (ytmp3 via API)", "ffmpeg", "ffprobe"],
512
541
  "side_effects": ["baixa mídia temporária", "envia vídeo no chat"],
513
542
  "observability": {
514
543
  "event_name": "command.executed",
@@ -575,6 +604,35 @@
575
604
  "schema": "legacy_v1_and_v2",
576
605
  "legacy_name": "tocarvideo",
577
606
  "legacy_fields_present": ["descricao", "metodos_de_uso", "permissao_necessaria", "local_de_uso", "informacoes_coletadas", "argumentos", "pre_condicoes", "dependencias_externas", "efeitos_colaterais", "observabilidade", "privacidade", "acesso", "limite_uso_por_plano"]
607
+ },
608
+ "user_experience": {
609
+ "resumo_usuario": "Baixe ou reproduza vídeos do YouTube a partir de um link ou de termos de busca. Requer login Google e assinatura Premium. O conteúdo fica disponível no chat.",
610
+ "quando_usar": ["Quando você tem um link do YouTube que quer baixar para assistir no chat.", "Quando prefere buscar um vídeo pelo título/termo de busca em vez de colar o link.", "Antes de usar, confirme que você tem acesso à assinatura Premium."],
611
+ "exemplos_reais": [
612
+ {
613
+ "situacao": "Baixar vídeo a partir de um link do YouTube.",
614
+ "comando": "<prefix>tocarvideo https://www.youtube.com/watch?v=dQw4w9WgXcQ",
615
+ "resposta_esperada": "Comando executado com sucesso.",
616
+ "variacao": "uso com link direto (alias: playvid)."
617
+ },
618
+ {
619
+ "situacao": "Buscar vídeo pelo termo de busca.",
620
+ "comando": "<prefix>tocarvideo melhor tutorial de edição de fotos",
621
+ "resposta_esperada": "Comando executado com sucesso.",
622
+ "variacao": "uso com termo de busca."
623
+ },
624
+ {
625
+ "situacao": "Tentar usar sem Premium.",
626
+ "comando": "<prefix>tocarvideo https://www.youtube.com/watch?v=dQw4w9WgXcQ",
627
+ "resposta_esperada": "Permissão insuficiente para executar este comando.",
628
+ "variacao": "sem assinatura Premium."
629
+ }
630
+ ],
631
+ "resposta_esperada": ["Comando executado com sucesso.", "Formato de uso inválido. Consulte metodos_de_uso.", "Permissão insuficiente para executar este comando."],
632
+ "erros_comuns_usuario": ["Esquecer de incluir o link ou termo de busca.", "Enviar link que não é do YouTube.", "Não estar logado com uma conta Google.", "Tentar usar o recurso sem a assinatura Premium."],
633
+ "passos_se_der_erro": ["Verifique se você está usando o formato correto: <prefix>tocarvideo <link_do_youtube> ou <prefix>tocarvideo <termo>.", "Certifique-se de estar logado com uma conta Google válida.", "Confirme que você possui assinatura Premium ativa para este comando.", "Se o erro persistir, tente novamente após confirmar as informações ou entre em contato com o suporte com a captura de tela do erro."],
634
+ "resumo_usuario_origem": "auto_ia_assistida",
635
+ "resumo_usuario_revisao_pendente": true
578
636
  }
579
637
  }
580
638
  ],
@@ -594,7 +652,7 @@
594
652
  "admin_alert_dedupe_window_ms": 120000
595
653
  },
596
654
  "opcoes_execucao": {
597
- "ytdlp_base_args": ["--ignore-config", "--no-playlist", "--no-warnings", "--js-runtimes", "node", "--extractor-args", "youtube:player_client=android,web"],
655
+ "media_provider_args": [],
598
656
  "estrategias_formato": {
599
657
  "audio": ["bestaudio/best", "best"],
600
658
  "video": ["bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best", "bestvideo*+bestaudio/best", "best"],
@@ -619,13 +677,13 @@
619
677
  "ready_title_video": "🎬 Vídeo pronto!",
620
678
  "video_fallback_to_audio": "⚠️ Este link retornou somente áudio. Enviando no formato de áudio.",
621
679
  "anti_bot_with_cookies": "YouTube solicitou verificação anti-bot. Atualize o arquivo .secrets/cookies.txt e tente novamente.",
622
- "anti_bot_with_browser_profile": "YouTube solicitou verificação anti-bot. Verifique o perfil informado em PLAY_YTDLP_COOKIES_FROM_BROWSER e tente novamente.",
623
- "anti_bot_without_cookies": "YouTube solicitou verificação anti-bot. Configure PLAY_YTDLP_COOKIES_PATH com um cookies.txt válido e tente novamente.",
680
+ "anti_bot_with_browser_profile": "YouTube solicitou verificação anti-bot no provedor de mídia. Verifique suas credenciais e tente novamente.",
681
+ "anti_bot_without_cookies": "YouTube solicitou verificação anti-bot no provedor de mídia. Tente novamente em alguns minutos.",
624
682
  "usage_fallback_audio": "🎵 Uso: <prefix>play <link do YouTube ou termo de busca>",
625
683
  "usage_fallback_video": "🎬 Uso: <prefix>playvid <link do YouTube ou termo de busca>",
626
684
  "invalid_media_type": "Tipo de mídia inválido.",
627
- "ytdlp_error_generic": "Falha ao processar mídia com yt-dlp.",
628
- "ytdlp_timeout_generic": "Timeout ao processar mídia com yt-dlp.",
685
+ "provider_error_generic": "Falha ao processar mídia no provedor.",
686
+ "provider_timeout_generic": "Timeout ao processar mídia no provedor.",
629
687
  "binary_exec_failed": "Falha ao executar <command>.",
630
688
  "search_invalid_input": "Você precisa informar um link do YouTube ou termo de busca.",
631
689
  "search_not_found": "Nenhum resultado encontrado para a busca.",
@@ -1,21 +1,23 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { DEFAULT_YTDLP_BINARY_PATH } from './local/ytDlpInstaller.js';
5
4
 
6
5
  export const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
7
6
 
8
7
  export const DEFAULT_TIMEOUT_MS = Number.parseInt(process.env.PLAY_TIMEOUT_MS || '900000', 10);
9
8
  export const DOWNLOAD_TIMEOUT_MS = Number.parseInt(process.env.PLAY_DOWNLOAD_TIMEOUT_MS || '1800000', 10);
10
- export const YTDLP_INFO_TIMEOUT_MS = Number.parseInt(process.env.PLAY_YTDLP_INFO_TIMEOUT_MS || '120000', 10);
11
- export const YTDLP_BINARY_PATH = (process.env.PLAY_YTDLP_BINARY_PATH || DEFAULT_YTDLP_BINARY_PATH).trim();
12
- export const YTDLP_COOKIES_FROM_BROWSER = (process.env.PLAY_YTDLP_COOKIES_FROM_BROWSER || '').trim();
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();
13
17
 
14
18
  const PLAY_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
15
19
  export const PLAY_LOCAL_DIR = path.join(PLAY_MODULE_DIR, 'local');
16
20
  export const PLAY_DOWNLOADS_DIR = path.join(PLAY_LOCAL_DIR, 'downloads');
17
- export const PROJECT_ROOT_DIR = path.resolve(PLAY_MODULE_DIR, '../../..');
18
- export const DEFAULT_COOKIES_PATH = path.join(PROJECT_ROOT_DIR, '.secrets', 'cookies.txt');
19
21
  export const MAX_SEARCH_RESULTS = Math.min(10, Math.max(1, Number.parseInt(process.env.PLAY_SEARCH_RESULTS || '5', 10)));
20
22
 
21
23
  const MAX_MEDIA_MB = Number.parseInt(process.env.PLAY_MAX_MB || '100', 10);
@@ -42,8 +44,12 @@ export const YTDLS_ENDPOINTS = {
42
44
  search: 'local:search',
43
45
  queueStatus: 'local:queue-status',
44
46
  download: 'local:download',
45
- install: 'local:install',
46
47
  thumbnail: 'thumbnail',
48
+ ytmp3Create: 'ytmp3:create',
49
+ ytmp3Poll: 'ytmp3:poll',
50
+ ytmp3Download: 'ytmp3:download',
51
+ ytmp3Search: 'ytmp3:search',
52
+ ytmp3Metadata: 'ytmp3:metadata',
47
53
  };
48
54
 
49
55
  export const ERROR_CODES = {
@@ -5,7 +5,7 @@ import { sendAndStore } from '../../services/messaging/messagePersistenceService
5
5
  import { getAdminJid } from '../../config/index.js';
6
6
  import { getPlayOperationalLimits, getPlayText, getPlayUsageFallbackText, getPlayUsageText, getPlayWaitText } from './playConfigRuntime.js';
7
7
  import { DEFAULT_COMMAND_PREFIX, ERROR_CODES, KNOWN_ERROR_CODES, TYPE_CONFIG, YTDLS_ENDPOINTS } from './playCommandConstants.js';
8
- import { createError, withErrorMeta, normalizePlayError, truncateText, ytdlsClient, formatters, fileUtils, isYouTubeBotCheckCause, buildYouTubeBotCheckUserMessage } from './playCommandYtDlpClient.js';
8
+ import { createError, withErrorMeta, normalizePlayError, truncateText, playMediaClient, formatters, fileUtils, isYouTubeBotCheckCause, buildYouTubeBotCheckUserMessage } from './playCommandMediaClient.js';
9
9
 
10
10
  const adminJid = getAdminJid();
11
11
  const adminAlertDedupCache = new Map();
@@ -113,7 +113,7 @@ const processPlayRequest = async ({ sock, remoteJid, messageInfo, expirationMess
113
113
  let filePath = null;
114
114
 
115
115
  try {
116
- const candidateLinks = await ytdlsClient.resolveYoutubeCandidates(text);
116
+ const candidateLinks = await playMediaClient.resolveYoutubeCandidates(text);
117
117
  await sendAndStore(sock, remoteJid, { text: getPlayWaitText(type) || config.waitText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
118
118
 
119
119
  let downloadResult = null;
@@ -125,13 +125,12 @@ const processPlayRequest = async ({ sock, remoteJid, messageInfo, expirationMess
125
125
  const candidateLink = candidateLinks[index];
126
126
  selectedLink = candidateLink;
127
127
  try {
128
- [downloadResult, videoInfo] = await Promise.all([ytdlsClient.requestDownloadToFile(candidateLink, type, requestId), ytdlsClient.fetchVideoInfo(candidateLink, text)]);
128
+ [downloadResult, videoInfo] = await Promise.all([playMediaClient.requestDownloadToFile(candidateLink, type, requestId), playMediaClient.fetchVideoInfo(candidateLink, text)]);
129
129
  lastDownloadError = null;
130
130
  break;
131
131
  } catch (error) {
132
132
  lastDownloadError = error;
133
133
  if (isYouTubeBotCheckCause(error)) {
134
- const cookiesPath = ytdlsClient.resolveYtDlpCookiesPath();
135
134
  logger.warn('Play download: bloqueio anti-bot detectado; abortando novas tentativas de candidato.', {
136
135
  requestId,
137
136
  remoteJid,
@@ -139,7 +138,6 @@ const processPlayRequest = async ({ sock, remoteJid, messageInfo, expirationMess
139
138
  endpoint: error?.meta?.endpoint || YTDLS_ENDPOINTS.download,
140
139
  attempt: index + 1,
141
140
  candidateLink,
142
- cookiesPath: cookiesPath || null,
143
141
  cause: truncateText(error?.meta?.cause || error?.message || ''),
144
142
  });
145
143
  throw withErrorMeta(createError(ERROR_CODES.API, buildYouTubeBotCheckUserMessage()), {
@@ -197,7 +195,7 @@ const processPlayRequest = async ({ sock, remoteJid, messageInfo, expirationMess
197
195
 
198
196
  if (thumbUrl) {
199
197
  try {
200
- thumbBuffer = await ytdlsClient.fetchThumbnailBuffer(thumbUrl);
198
+ thumbBuffer = await playMediaClient.fetchThumbnailBuffer(thumbUrl);
201
199
  } catch (error) {
202
200
  logger.warn('Falha ao baixar thumbnail.', {
203
201
  requestId,