@kaikybrofc/omnizap-system 2.3.1 → 2.3.3

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 (49) hide show
  1. package/README.md +82 -483
  2. package/app/controllers/messageController.js +473 -255
  3. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  4. package/app/modules/stickerModule/stickerCommand.js +7 -2
  5. package/app/modules/stickerModule/stickerTextCommand.js +7 -2
  6. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +1 -3
  7. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +224 -53
  8. package/app/observability/metrics.js +6 -3
  9. package/app/services/googleWebLinkService.js +77 -0
  10. package/app/services/lidMapService.js +83 -4
  11. package/database/index.js +2 -0
  12. package/database/migrations/20260301_0028_message_analysis_event.sql +32 -0
  13. package/database/migrations/20260301_0029_admin_action_audit.sql +16 -0
  14. package/package.json +1 -1
  15. package/public/index.html +12 -8
  16. package/public/js/apps/createPackApp.js +4 -4
  17. package/public/js/apps/homeApp.js +78 -34
  18. package/public/js/apps/loginApp.js +245 -35
  19. package/public/js/apps/stickersAdminApp.js +4 -10
  20. package/public/js/apps/stickersApp.js +1 -1
  21. package/public/js/apps/userApp.js +956 -55
  22. package/public/js/apps/userProfileApp.js +244 -0
  23. package/public/login/index.html +437 -101
  24. package/public/termos-de-uso/index.html +1 -1
  25. package/public/user/index.html +2 -181
  26. package/public/user/systemadm/index.html +774 -0
  27. package/server/controllers/stickerCatalog/nonCatalogHandlers.js +183 -0
  28. package/server/controllers/stickerCatalogController.js +1289 -368
  29. package/server/controllers/systemAdminController.js +141 -0
  30. package/server/controllers/userController.js +87 -0
  31. package/server/http/httpServer.js +72 -32
  32. package/server/middleware/cachePolicy.js +24 -0
  33. package/server/middleware/cachePolicyHelpers.js +1 -0
  34. package/server/middleware/rateLimit.js +89 -0
  35. package/server/middleware/requestLogger.js +16 -0
  36. package/server/middleware/requireAdminAuth.js +42 -0
  37. package/server/middleware/securityHeaders.js +6 -0
  38. package/server/routes/admin/systemAdminRouter.js +56 -0
  39. package/server/routes/health/healthRouter.js +41 -0
  40. package/server/routes/indexRouter.js +197 -0
  41. package/server/routes/metrics/metricsRouter.js +13 -0
  42. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +44 -0
  43. package/server/routes/stickerCatalog/stickerApiRouter.js +84 -0
  44. package/server/routes/stickerCatalog/stickerDataRouter.js +140 -0
  45. package/server/routes/stickerCatalog/stickerSiteRouter.js +43 -0
  46. package/server/routes/user/userRouter.js +56 -0
  47. package/server/utils/safePath.js +26 -0
  48. package/server/routes/metricsRoute.js +0 -7
  49. package/server/routes/stickerCatalogRoute.js +0 -20
@@ -9,7 +9,7 @@ import { handleRankingCommand } from '../modules/statsModule/rankingCommand.js';
9
9
  import { handleGlobalRankingCommand } from '../modules/statsModule/globalRankingCommand.js';
10
10
  import { handleNoMessageCommand } from '../modules/statsModule/noMessageCommand.js';
11
11
  import { handlePingCommand } from '../modules/systemMetricsModule/pingCommand.js';
12
- import { extractMessageContent, getExpiration, getJidServer, getJidUser, isGroupJid, isSameJidUser, normalizeJid, resolveBotJid } from '../config/baileysConfig.js';
12
+ import { detectAllMediaTypes, extractMessageContent, getExpiration, getJidServer, getJidUser, isGroupJid, isSameJidUser, normalizeJid, resolveBotJid } from '../config/baileysConfig.js';
13
13
  import logger from '../utils/logger/loggerModule.js';
14
14
  import { handleAntiLink } from '../utils/antiLink/antiLinkModule.js';
15
15
  import { handleCatCommand, handleCatImageCommand, handleCatPromptCommand } from '../modules/aiModule/catCommand.js';
@@ -27,6 +27,8 @@ import { sendAndStore } from '../services/messagePersistenceService.js';
27
27
  import { resolveCaptchaByMessage } from '../services/captchaService.js';
28
28
  import { extractSenderInfoFromMessage, resolveUserId } from '../services/lidMapService.js';
29
29
  import { buildWhatsAppGoogleLoginUrl } from '../services/whatsappLoginLinkService.js';
30
+ import { isWhatsAppUserLinkedToGoogleWebAccount } from '../services/googleWebLinkService.js';
31
+ import { createMessageAnalysisEvent } from '../modules/analyticsModule/messageAnalysisEventRepository.js';
30
32
 
31
33
  const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
32
34
  const COMMAND_REACT_EMOJI = process.env.COMMAND_REACT_EMOJI || '🤖';
@@ -36,6 +38,29 @@ const START_LOGIN_TRIGGER =
36
38
  .toLowerCase() || 'iniciar';
37
39
  const WHATSAPP_USER_SERVERS = new Set(['s.whatsapp.net', 'c.us', 'hosted']);
38
40
  const WHATSAPP_LID_SERVERS = new Set(['lid', 'hosted.lid']);
41
+ const parseEnvBool = (value, fallback) => {
42
+ if (value === undefined || value === null || value === '') return fallback;
43
+ const normalized = String(value).trim().toLowerCase();
44
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
45
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
46
+ return fallback;
47
+ };
48
+ const MESSAGE_ANALYTICS_ENABLED = parseEnvBool(process.env.MESSAGE_ANALYTICS_ENABLED, true);
49
+ const MESSAGE_ANALYTICS_SOURCE =
50
+ String(process.env.MESSAGE_ANALYTICS_SOURCE || 'whatsapp')
51
+ .trim()
52
+ .slice(0, 32) || 'whatsapp';
53
+ const WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN = parseEnvBool(process.env.WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN, true);
54
+ const SITE_ORIGIN =
55
+ String(process.env.SITE_ORIGIN || process.env.WHATSAPP_LOGIN_BASE_URL || 'https://omnizap.shop')
56
+ .trim()
57
+ .replace(/\/+$/, '') || 'https://omnizap.shop';
58
+ const SITE_LOGIN_URL = `${SITE_ORIGIN}/login/`;
59
+ const SITE_GROUP_LOGIN_URL = `${SITE_ORIGIN}/login`;
60
+
61
+ const KNOWN_MESSAGE_COMMANDS = new Set(['menu', 'sticker', 's', 'pack', 'packs', 'toimg', 'tovideo', 'tovid', 'play', 'playvid', 'tiktok', 'tt', 'cat', 'catimg', 'catimage', 'catprompt', 'iaprompt', 'promptia', 'quote', 'qc', 'wp', 'waifupics', 'wpnsfw', 'waifupicsnsfw', 'wppicshelp', 'stickertext', 'st', 'stickertextwhite', 'stw', 'stickertextblink', 'stb', 'ranking', 'rank', 'top5', 'rankingglobal', 'rankglobal', 'globalrank', 'globalranking', 'semmsg', 'zeromsg', 'nomsg', 'inativos', 'ping', 'dado', 'dice', 'user', 'usuario', 'rpg', 'aviso', 'notice']);
62
+
63
+ let messageAnalyticsTableMissingLogged = false;
39
64
 
40
65
  const normalizeTriggerText = (value) =>
41
66
  String(value || '')
@@ -62,6 +87,24 @@ const resolveCanonicalWhatsAppJid = (...candidates) => {
62
87
  return '';
63
88
  };
64
89
 
90
+ const resolveCanonicalSenderJidFromMessage = async ({ messageInfo, senderJid }) => {
91
+ const key = messageInfo?.key || {};
92
+ const senderInfo = extractSenderInfoFromMessage(messageInfo);
93
+ let canonicalUserId = resolveCanonicalWhatsAppJid(senderInfo?.jid, senderInfo?.lid, senderInfo?.participantAlt, key.participantAlt, key.participant, key.remoteJid, senderJid);
94
+
95
+ try {
96
+ const resolvedUserId = await resolveUserId(senderInfo);
97
+ canonicalUserId = resolveCanonicalWhatsAppJid(resolvedUserId, canonicalUserId, senderInfo?.jid, senderInfo?.lid);
98
+ } catch (error) {
99
+ logger.warn('Falha ao resolver ID canonico do remetente.', {
100
+ action: 'resolve_sender_canonical_id_failed',
101
+ error: error?.message,
102
+ });
103
+ }
104
+
105
+ return canonicalUserId;
106
+ };
107
+
65
108
  const maybeHandleStartLoginMessage = async ({ sock, messageInfo, extractedText, senderName, senderJid, remoteJid, expirationMessage, isMessageFromBot, isGroupMessage }) => {
66
109
  if (isMessageFromBot || !isStartLoginTrigger(extractedText)) return false;
67
110
 
@@ -79,15 +122,7 @@ const maybeHandleStartLoginMessage = async ({ sock, messageInfo, extractedText,
79
122
 
80
123
  const key = messageInfo?.key || {};
81
124
  const senderInfo = extractSenderInfoFromMessage(messageInfo);
82
- let canonicalUserId = resolveCanonicalWhatsAppJid(senderInfo?.jid, senderInfo?.lid, senderInfo?.participantAlt, key.participantAlt, key.participant, key.remoteJid, senderJid);
83
- try {
84
- const resolvedUserId = await resolveUserId(senderInfo);
85
- canonicalUserId = resolveCanonicalWhatsAppJid(resolvedUserId, canonicalUserId, senderInfo?.jid, senderInfo?.lid);
86
- } catch (error) {
87
- logger.warn('Falha ao resolver ID canonico para fluxo de login do WhatsApp.', {
88
- error: error?.message,
89
- });
90
- }
125
+ const canonicalUserId = await resolveCanonicalSenderJidFromMessage({ messageInfo, senderJid });
91
126
 
92
127
  const loginUrl = buildWhatsAppGoogleLoginUrl({ userId: canonicalUserId });
93
128
  if (!loginUrl) {
@@ -155,6 +190,98 @@ const runCommand = (label, handler) => {
155
190
  }
156
191
  };
157
192
 
193
+ const normalizeMessageKind = (mediaEntries, extractedText) => {
194
+ if (Array.isArray(mediaEntries) && mediaEntries.length > 0) {
195
+ const primaryType =
196
+ String(mediaEntries[0]?.mediaType || '')
197
+ .trim()
198
+ .toLowerCase() || 'media';
199
+ return primaryType.slice(0, 48);
200
+ }
201
+
202
+ const safeText = String(extractedText || '').trim();
203
+ if (!safeText || safeText === 'Mensagem vazia') return 'empty';
204
+ if (safeText.startsWith('[') && safeText.endsWith(']')) {
205
+ return safeText.slice(1, -1).trim().toLowerCase().replace(/\s+/g, '_').slice(0, 48);
206
+ }
207
+ return 'text';
208
+ };
209
+
210
+ const normalizeAnalysisErrorCode = (error) =>
211
+ String(error?.code || error?.name || 'processing_error')
212
+ .trim()
213
+ .toLowerCase()
214
+ .replace(/[^a-z0-9_-]/g, '_')
215
+ .slice(0, 96) || 'processing_error';
216
+
217
+ const persistMessageAnalysisEvent = (payload) => {
218
+ if (!MESSAGE_ANALYTICS_ENABLED) return;
219
+ void createMessageAnalysisEvent(payload).catch((error) => {
220
+ if (error?.code === 'ER_NO_SUCH_TABLE') {
221
+ if (messageAnalyticsTableMissingLogged) return;
222
+ messageAnalyticsTableMissingLogged = true;
223
+ logger.warn('Tabela de analytics de mensagens ainda não existe. Execute a migracao 20260301_0028.', {
224
+ action: 'message_analysis_table_missing',
225
+ });
226
+ return;
227
+ }
228
+
229
+ logger.warn('Falha ao persistir analytics de mensagem.', {
230
+ action: 'message_analysis_insert_failed',
231
+ error: error?.message,
232
+ });
233
+ });
234
+ };
235
+
236
+ const buildSiteLoginUrlForUser = (canonicalUserId) => buildWhatsAppGoogleLoginUrl({ userId: canonicalUserId }) || SITE_LOGIN_URL;
237
+
238
+ const ensureUserHasGoogleWebLoginForCommand = async ({ sock, messageInfo, senderJid, remoteJid, expirationMessage, commandPrefix }) => {
239
+ const isGroupMessage = isGroupJid(remoteJid);
240
+ const canonicalUserId = await resolveCanonicalSenderJidFromMessage({ messageInfo, senderJid });
241
+ let linked = false;
242
+ try {
243
+ linked = await isWhatsAppUserLinkedToGoogleWebAccount({
244
+ ownerJid: canonicalUserId || senderJid,
245
+ });
246
+ } catch (error) {
247
+ logger.warn('Falha ao validar vínculo Google Web para comando do WhatsApp. Comando liberado por fallback.', {
248
+ action: 'whatsapp_command_google_link_check_failed',
249
+ error: error?.message,
250
+ });
251
+ return {
252
+ allowed: true,
253
+ canonicalUserId,
254
+ loginUrl: '',
255
+ };
256
+ }
257
+
258
+ if (linked) {
259
+ return {
260
+ allowed: true,
261
+ canonicalUserId,
262
+ loginUrl: '',
263
+ };
264
+ }
265
+
266
+ const loginUrl = isGroupMessage ? SITE_GROUP_LOGIN_URL : buildSiteLoginUrlForUser(canonicalUserId || senderJid);
267
+ const loginMessage = isGroupMessage ? `Para usar os comandos do bot, você precisa estar logado no site com sua conta Google.\n\nAcesse:\n${loginUrl}` : `Para usar os comandos do bot, você precisa estar logado no site com sua conta Google.\n\nCadastre-se / faça login em:\n${loginUrl}\n\nDepois volte aqui e envie o comando novamente (ex.: ${commandPrefix}menu).`;
268
+
269
+ await sendAndStore(
270
+ sock,
271
+ remoteJid,
272
+ {
273
+ text: loginMessage,
274
+ },
275
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
276
+ );
277
+
278
+ return {
279
+ allowed: false,
280
+ canonicalUserId,
281
+ loginUrl,
282
+ };
283
+ };
284
+
158
285
  /**
159
286
  * Lida com atualizações do WhatsApp, sejam mensagens ou eventos genéricos.
160
287
  *
@@ -180,236 +307,311 @@ export const handleMessages = async (update, sock) => {
180
307
  const botJid = resolveBotJid(sock?.user?.id);
181
308
  const isMessageFromBot = Boolean(messageInfo?.key?.fromMe) || (botJid ? isSameJidUser(senderJid, botJid) : false);
182
309
  let commandPrefix = DEFAULT_COMMAND_PREFIX;
310
+ const mediaEntries = detectAllMediaTypes(messageInfo?.message, false);
311
+ const mediaTypes = mediaEntries
312
+ .map((entry) =>
313
+ String(entry?.mediaType || '')
314
+ .trim()
315
+ .toLowerCase(),
316
+ )
317
+ .filter(Boolean)
318
+ .slice(0, 10);
319
+ const analysisPayload = {
320
+ messageId: messageInfo?.key?.id || null,
321
+ chatId: remoteJid || null,
322
+ senderId: senderJid || null,
323
+ senderName,
324
+ upsertType: update?.type || null,
325
+ source: MESSAGE_ANALYTICS_SOURCE,
326
+ isGroup: isGroupMessage,
327
+ isFromBot: isMessageFromBot,
328
+ isCommand: false,
329
+ commandName: null,
330
+ commandArgsCount: 0,
331
+ commandKnown: null,
332
+ commandPrefix,
333
+ messageKind: normalizeMessageKind(mediaEntries, extractedText),
334
+ hasMedia: mediaEntries.length > 0,
335
+ mediaCount: mediaEntries.length,
336
+ textLength: String(extractedText || '').length,
337
+ processingResult: 'processed',
338
+ errorCode: null,
339
+ metadata: {
340
+ media_types: mediaTypes,
341
+ start_login_trigger: isStartLoginTrigger(extractedText),
342
+ },
343
+ };
344
+
345
+ try {
346
+ /**
347
+ * Executa validações de grupo.
348
+ * Aplica o Anti-Link e resolve o prefixo do grupo.
349
+ * Se a mensagem for bloqueada, interrompe o processamento.
350
+ */
351
+ if (isGroupMessage) {
352
+ const shouldSkip = await handleAntiLink({ sock, messageInfo, extractedText, remoteJid, senderJid, botJid });
353
+
354
+ if (shouldSkip) {
355
+ analysisPayload.processingResult = 'blocked_antilink';
356
+ analysisPayload.metadata = {
357
+ ...analysisPayload.metadata,
358
+ blocked_by: 'anti_link',
359
+ };
360
+ continue;
361
+ }
362
+ commandPrefix = await resolveCommandPrefix(true, remoteJid);
363
+ analysisPayload.commandPrefix = commandPrefix;
364
+ }
183
365
 
184
- /**
185
- * Executa validações de grupo.
186
- * Aplica o Anti-Link e resolve o prefixo do grupo.
187
- * Se a mensagem for bloqueada, interrompe o processamento.
188
- */
189
- if (isGroupMessage) {
190
- const shouldSkip = await handleAntiLink({ sock, messageInfo, extractedText, remoteJid, senderJid, botJid });
191
-
192
- if (shouldSkip) {
193
- continue;
366
+ if (isGroupMessage && !isMessageFromBot) {
367
+ await resolveCaptchaByMessage({
368
+ groupId: remoteJid,
369
+ senderJid,
370
+ senderIdentity,
371
+ messageKey: messageInfo.key,
372
+ messageInfo,
373
+ extractedText,
374
+ });
194
375
  }
195
- commandPrefix = await resolveCommandPrefix(true, remoteJid);
196
- }
197
376
 
198
- if (isGroupMessage && !isMessageFromBot) {
199
- await resolveCaptchaByMessage({
200
- groupId: remoteJid,
201
- senderJid,
202
- senderIdentity,
203
- messageKey: messageInfo.key,
377
+ const handledStartLogin = await maybeHandleStartLoginMessage({
378
+ sock,
204
379
  messageInfo,
205
380
  extractedText,
381
+ senderName,
382
+ senderJid,
383
+ remoteJid,
384
+ expirationMessage,
385
+ isMessageFromBot,
386
+ isGroupMessage,
206
387
  });
207
- }
208
388
 
209
- const handledStartLogin = await maybeHandleStartLoginMessage({
210
- sock,
211
- messageInfo,
212
- extractedText,
213
- senderName,
214
- senderJid,
215
- remoteJid,
216
- expirationMessage,
217
- isMessageFromBot,
218
- isGroupMessage,
219
- });
220
-
221
- if (handledStartLogin) {
222
- continue;
223
- }
389
+ if (handledStartLogin) {
390
+ analysisPayload.processingResult = 'handled_start_login';
391
+ analysisPayload.metadata = {
392
+ ...analysisPayload.metadata,
393
+ flow: 'whatsapp_google_login',
394
+ };
395
+ continue;
396
+ }
224
397
 
225
- /**
226
- * Envia uma reação automática quando a mensagem começa com o prefixo de comando.
227
- * A falha no envio da reação não interrompe o processamento do comando.
228
- */
229
- const isCommandMessage = extractedText.startsWith(commandPrefix);
230
-
231
- if (isCommandMessage) {
232
- if (COMMAND_REACT_EMOJI) {
233
- try {
234
- await sendAndStore(sock, remoteJid, {
235
- react: {
236
- text: COMMAND_REACT_EMOJI,
237
- key: messageInfo.key,
238
- },
398
+ /**
399
+ * Envia uma reação automática quando a mensagem começa com o prefixo de comando.
400
+ * A falha no envio da reação não interrompe o processamento do comando.
401
+ */
402
+ const isCommandMessage = extractedText.startsWith(commandPrefix);
403
+ analysisPayload.isCommand = isCommandMessage;
404
+ analysisPayload.commandPrefix = commandPrefix;
405
+
406
+ if (isCommandMessage) {
407
+ const commandBody = extractedText.substring(commandPrefix.length);
408
+ const match = commandBody.match(/^(\S+)([\s\S]*)$/);
409
+ const command = match ? match[1].toLowerCase() : '';
410
+ const rawArgs = match && match[2] !== undefined ? match[2].trim() : '';
411
+ const args = rawArgs ? rawArgs.split(/\s+/) : [];
412
+ const text = match && match[2] !== undefined ? match[2] : '';
413
+ const isAdminCommandRoute = isAdminCommand(command);
414
+
415
+ analysisPayload.commandName = command || null;
416
+ analysisPayload.commandArgsCount = args.length;
417
+ analysisPayload.commandKnown = KNOWN_MESSAGE_COMMANDS.has(command) || isAdminCommandRoute;
418
+
419
+ if (!isMessageFromBot && WHATSAPP_COMMAND_REQUIRES_GOOGLE_LOGIN) {
420
+ const authCheck = await ensureUserHasGoogleWebLoginForCommand({
421
+ sock,
422
+ messageInfo,
423
+ senderJid,
424
+ remoteJid,
425
+ expirationMessage,
426
+ commandPrefix,
239
427
  });
240
- } catch (error) {
241
- logger.warn('Falha ao enviar reação de comando:', error.message);
428
+
429
+ if (!authCheck.allowed) {
430
+ analysisPayload.processingResult = 'auth_required';
431
+ analysisPayload.metadata = {
432
+ ...analysisPayload.metadata,
433
+ auth_required_for_command: command || null,
434
+ auth_login_url: authCheck.loginUrl || SITE_LOGIN_URL,
435
+ };
436
+ continue;
437
+ }
242
438
  }
243
- }
244
439
 
245
- const commandBody = extractedText.substring(commandPrefix.length);
246
- const match = commandBody.match(/^(\S+)([\s\S]*)$/);
247
- const command = match ? match[1].toLowerCase() : '';
248
- const rawArgs = match && match[2] !== undefined ? match[2].trim() : '';
249
- const args = rawArgs ? rawArgs.split(/\s+/) : [];
250
- const text = match && match[2] !== undefined ? match[2] : '';
251
-
252
- switch (command) {
253
- case 'menu':
254
- runCommand('menu', () => handleMenuCommand(sock, remoteJid, messageInfo, expirationMessage, senderName, commandPrefix, args));
255
- break;
256
-
257
- case 'sticker':
258
- case 's':
259
- runCommand('sticker', () => processSticker(sock, messageInfo, senderJid, remoteJid, expirationMessage, senderName, args.join(' '), { commandPrefix }));
260
- break;
261
-
262
- case 'pack':
263
- case 'packs':
264
- runCommand('pack', () => handlePackCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, senderName, text, commandPrefix }));
265
- break;
266
-
267
- case 'toimg':
268
- case 'tovideo':
269
- case 'tovid':
270
- runCommand('toimg', () => handleStickerConvertCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid }));
271
- break;
272
-
273
- case 'play':
274
- runCommand('play', () => handlePlayCommand(sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix));
275
- break;
276
-
277
- case 'playvid':
278
- runCommand('playvid', () => handlePlayVidCommand(sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix));
279
- break;
280
-
281
- case 'tiktok':
282
- case 'tt':
283
- runCommand('tiktok', () => handleTikTokCommand({ sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix }));
284
- break;
285
-
286
- case 'cat':
287
- runCommand('cat', () => handleCatCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix }));
288
- break;
289
-
290
- case 'catimg':
291
- case 'catimage':
292
- runCommand('catimg', () => handleCatImageCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix }));
293
- break;
294
-
295
- case 'catprompt':
296
- case 'iaprompt':
297
- case 'promptia':
298
- runCommand('catprompt', () => handleCatPromptCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix }));
299
- break;
300
-
301
- case 'quote':
302
- case 'qc':
303
- runCommand('quote', () => handleQuoteCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, senderName, text, commandPrefix }));
304
- break;
305
-
306
- case 'wp':
307
- case 'waifupics':
308
- runCommand('waifupics', () => handleWaifuPicsCommand({ sock, remoteJid, messageInfo, expirationMessage, text, type: 'sfw', commandPrefix }));
309
- break;
310
-
311
- case 'wpnsfw':
312
- case 'waifupicsnsfw':
313
- runCommand('waifupicsnsfw', () => handleWaifuPicsCommand({ sock, remoteJid, messageInfo, expirationMessage, text, type: 'nsfw', commandPrefix }));
314
- break;
315
-
316
- case 'wppicshelp':
317
- runCommand('wppicshelp', () => sendAndStore(sock, remoteJid, { text: getWaifuPicsUsageText(commandPrefix) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage }));
318
- break;
319
-
320
- case 'stickertext':
321
- case 'st':
322
- runCommand('stickertext', () => processTextSticker({ sock, messageInfo, remoteJid, senderJid, senderName, text, extraText: 'PackZoeira', expirationMessage, color: 'black', commandPrefix }));
323
- break;
324
-
325
- case 'stickertextwhite':
326
- case 'stw':
327
- runCommand('stickertextwhite', () => processTextSticker({ sock, messageInfo, remoteJid, senderJid, senderName, text, extraText: 'PackZoeira', expirationMessage, color: 'white', commandPrefix }));
328
- break;
329
-
330
- case 'stickertextblink':
331
- case 'stb':
332
- runCommand('stickertextblink', () => processBlinkingTextSticker({ sock, messageInfo, remoteJid, senderJid, senderName, text, extraText: 'PackZoeira', expirationMessage, color: 'white', commandPrefix }));
333
- break;
334
-
335
- case 'ranking':
336
- case 'rank':
337
- case 'top5':
338
- runCommand('ranking', () => handleRankingCommand({ sock, remoteJid, messageInfo, expirationMessage, isGroupMessage }));
339
- break;
340
-
341
- case 'rankingglobal':
342
- case 'rankglobal':
343
- case 'globalrank':
344
- case 'globalranking':
345
- runCommand('rankingglobal', () => handleGlobalRankingCommand({ sock, remoteJid, messageInfo, expirationMessage, isGroupMessage }));
346
- break;
347
-
348
- case 'semmsg':
349
- case 'zeromsg':
350
- case 'nomsg':
351
- case 'inativos':
352
- runCommand('semmsg', () => handleNoMessageCommand({ sock, remoteJid, messageInfo, expirationMessage, isGroupMessage, senderJid, text }));
353
- break;
354
-
355
- case 'ping':
356
- runCommand('ping', () => handlePingCommand({ sock, remoteJid, messageInfo, expirationMessage }));
357
- break;
358
-
359
- case 'dado':
360
- case 'dice':
361
- runCommand('dado', () => handleDiceCommand({ sock, remoteJid, messageInfo, expirationMessage, args, commandPrefix }));
362
- break;
363
-
364
- case 'user':
365
- case 'usuario':
366
- runCommand('user', () =>
367
- handleUserCommand({
368
- sock,
369
- remoteJid,
370
- messageInfo,
371
- expirationMessage,
372
- senderJid,
373
- args,
374
- isGroupMessage,
375
- commandPrefix,
376
- }),
377
- );
378
- break;
379
-
380
- case 'rpg':
381
- runCommand('rpg', () =>
382
- handleRpgPokemonCommand({
383
- sock,
384
- remoteJid,
385
- messageInfo,
386
- expirationMessage,
387
- senderJid,
388
- senderIdentity,
389
- args,
390
- commandPrefix,
391
- }),
392
- );
393
- break;
394
-
395
- case 'aviso':
396
- case 'notice':
397
- runCommand('aviso', () => handleNoticeCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix }));
398
- break;
399
-
400
- default:
401
- if (isAdminCommand(command)) {
402
- runCommand('admin', () => handleAdminCommand({ command, args, text, sock, messageInfo, remoteJid, senderJid, botJid, isGroupMessage, expirationMessage, commandPrefix }));
403
- break;
440
+ if (COMMAND_REACT_EMOJI) {
441
+ try {
442
+ await sendAndStore(sock, remoteJid, {
443
+ react: {
444
+ text: COMMAND_REACT_EMOJI,
445
+ key: messageInfo.key,
446
+ },
447
+ });
448
+ } catch (error) {
449
+ logger.warn('Falha ao enviar reação de comando:', error.message);
404
450
  }
451
+ }
452
+
453
+ switch (command) {
454
+ case 'menu':
455
+ runCommand('menu', () => handleMenuCommand(sock, remoteJid, messageInfo, expirationMessage, senderName, commandPrefix, args));
456
+ break;
457
+
458
+ case 'sticker':
459
+ case 's':
460
+ runCommand('sticker', () => processSticker(sock, messageInfo, senderJid, remoteJid, expirationMessage, senderName, args.join(' '), { commandPrefix }));
461
+ break;
462
+
463
+ case 'pack':
464
+ case 'packs':
465
+ runCommand('pack', () => handlePackCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, senderName, text, commandPrefix }));
466
+ break;
467
+
468
+ case 'toimg':
469
+ case 'tovideo':
470
+ case 'tovid':
471
+ runCommand('toimg', () => handleStickerConvertCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid }));
472
+ break;
473
+
474
+ case 'play':
475
+ runCommand('play', () => handlePlayCommand(sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix));
476
+ break;
477
+
478
+ case 'playvid':
479
+ runCommand('playvid', () => handlePlayVidCommand(sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix));
480
+ break;
481
+
482
+ case 'tiktok':
483
+ case 'tt':
484
+ runCommand('tiktok', () => handleTikTokCommand({ sock, remoteJid, messageInfo, expirationMessage, text, commandPrefix }));
485
+ break;
486
+
487
+ case 'cat':
488
+ runCommand('cat', () => handleCatCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix }));
489
+ break;
405
490
 
406
- logger.info(`Comando desconhecido recebido: ${command}`);
407
- runCommand('unknown', () =>
408
- sendAndStore(
409
- sock,
410
- remoteJid,
411
- {
412
- text: `❌ *Comando não reconhecido*
491
+ case 'catimg':
492
+ case 'catimage':
493
+ runCommand('catimg', () => handleCatImageCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix }));
494
+ break;
495
+
496
+ case 'catprompt':
497
+ case 'iaprompt':
498
+ case 'promptia':
499
+ runCommand('catprompt', () => handleCatPromptCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix }));
500
+ break;
501
+
502
+ case 'quote':
503
+ case 'qc':
504
+ runCommand('quote', () => handleQuoteCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, senderName, text, commandPrefix }));
505
+ break;
506
+
507
+ case 'wp':
508
+ case 'waifupics':
509
+ runCommand('waifupics', () => handleWaifuPicsCommand({ sock, remoteJid, messageInfo, expirationMessage, text, type: 'sfw', commandPrefix }));
510
+ break;
511
+
512
+ case 'wpnsfw':
513
+ case 'waifupicsnsfw':
514
+ runCommand('waifupicsnsfw', () => handleWaifuPicsCommand({ sock, remoteJid, messageInfo, expirationMessage, text, type: 'nsfw', commandPrefix }));
515
+ break;
516
+
517
+ case 'wppicshelp':
518
+ runCommand('wppicshelp', () => sendAndStore(sock, remoteJid, { text: getWaifuPicsUsageText(commandPrefix) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage }));
519
+ break;
520
+
521
+ case 'stickertext':
522
+ case 'st':
523
+ runCommand('stickertext', () => processTextSticker({ sock, messageInfo, remoteJid, senderJid, senderName, text, extraText: 'PackZoeira', expirationMessage, color: 'black', commandPrefix }));
524
+ break;
525
+
526
+ case 'stickertextwhite':
527
+ case 'stw':
528
+ runCommand('stickertextwhite', () => processTextSticker({ sock, messageInfo, remoteJid, senderJid, senderName, text, extraText: 'PackZoeira', expirationMessage, color: 'white', commandPrefix }));
529
+ break;
530
+
531
+ case 'stickertextblink':
532
+ case 'stb':
533
+ runCommand('stickertextblink', () => processBlinkingTextSticker({ sock, messageInfo, remoteJid, senderJid, senderName, text, extraText: 'PackZoeira', expirationMessage, color: 'white', commandPrefix }));
534
+ break;
535
+
536
+ case 'ranking':
537
+ case 'rank':
538
+ case 'top5':
539
+ runCommand('ranking', () => handleRankingCommand({ sock, remoteJid, messageInfo, expirationMessage, isGroupMessage }));
540
+ break;
541
+
542
+ case 'rankingglobal':
543
+ case 'rankglobal':
544
+ case 'globalrank':
545
+ case 'globalranking':
546
+ runCommand('rankingglobal', () => handleGlobalRankingCommand({ sock, remoteJid, messageInfo, expirationMessage, isGroupMessage }));
547
+ break;
548
+
549
+ case 'semmsg':
550
+ case 'zeromsg':
551
+ case 'nomsg':
552
+ case 'inativos':
553
+ runCommand('semmsg', () => handleNoMessageCommand({ sock, remoteJid, messageInfo, expirationMessage, isGroupMessage, senderJid, text }));
554
+ break;
555
+
556
+ case 'ping':
557
+ runCommand('ping', () => handlePingCommand({ sock, remoteJid, messageInfo, expirationMessage }));
558
+ break;
559
+
560
+ case 'dado':
561
+ case 'dice':
562
+ runCommand('dado', () => handleDiceCommand({ sock, remoteJid, messageInfo, expirationMessage, args, commandPrefix }));
563
+ break;
564
+
565
+ case 'user':
566
+ case 'usuario':
567
+ runCommand('user', () =>
568
+ handleUserCommand({
569
+ sock,
570
+ remoteJid,
571
+ messageInfo,
572
+ expirationMessage,
573
+ senderJid,
574
+ args,
575
+ isGroupMessage,
576
+ commandPrefix,
577
+ }),
578
+ );
579
+ break;
580
+
581
+ case 'rpg':
582
+ runCommand('rpg', () =>
583
+ handleRpgPokemonCommand({
584
+ sock,
585
+ remoteJid,
586
+ messageInfo,
587
+ expirationMessage,
588
+ senderJid,
589
+ senderIdentity,
590
+ args,
591
+ commandPrefix,
592
+ }),
593
+ );
594
+ break;
595
+
596
+ case 'aviso':
597
+ case 'notice':
598
+ runCommand('aviso', () => handleNoticeCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, text, commandPrefix }));
599
+ break;
600
+
601
+ default:
602
+ if (isAdminCommandRoute) {
603
+ runCommand('admin', () => handleAdminCommand({ command, args, text, sock, messageInfo, remoteJid, senderJid, botJid, isGroupMessage, expirationMessage, commandPrefix }));
604
+ break;
605
+ }
606
+
607
+ analysisPayload.processingResult = 'unknown_command';
608
+ logger.info(`Comando desconhecido recebido: ${command}`);
609
+ runCommand('unknown', () =>
610
+ sendAndStore(
611
+ sock,
612
+ remoteJid,
613
+ {
614
+ text: `❌ *Comando não reconhecido*
413
615
 
414
616
  O comando *${command}* não está configurado ou ainda não existe.
415
617
 
@@ -418,39 +620,55 @@ Digite *${commandPrefix}menu* para ver a lista de comandos disponíveis.
418
620
 
419
621
  🚧 *Fase Beta*
420
622
  O omnizap-system ainda está em desenvolvimento e novos comandos estão sendo adicionados constantemente.`,
421
- },
422
- { quoted: messageInfo, ephemeralExpiration: expirationMessage },
423
- ),
424
- );
425
- break;
623
+ },
624
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
625
+ ),
626
+ );
627
+ break;
628
+ }
426
629
  }
427
- }
428
630
 
429
- if (!isMessageFromBot) {
430
- runCommand('pack-capture', () =>
431
- maybeCaptureIncomingSticker({
432
- messageInfo,
433
- senderJid,
434
- isMessageFromBot,
435
- }),
436
- );
437
- }
631
+ if (!isMessageFromBot) {
632
+ runCommand('pack-capture', () =>
633
+ maybeCaptureIncomingSticker({
634
+ messageInfo,
635
+ senderJid,
636
+ isMessageFromBot,
637
+ }),
638
+ );
639
+ }
438
640
 
439
- if (isGroupMessage && !isCommandMessage && !isMessageFromBot) {
440
- const autoStickerMedia = extractSupportedStickerMediaDetails(messageInfo, { includeQuoted: false });
441
-
442
- if (autoStickerMedia && autoStickerMedia.mediaType !== 'sticker') {
443
- const groupConfig = await groupConfigStore.getGroupConfig(remoteJid);
444
- if (groupConfig.autoStickerEnabled) {
445
- runCommand('autosticker', () =>
446
- processSticker(sock, messageInfo, senderJid, remoteJid, expirationMessage, senderName, '', {
447
- includeQuotedMedia: false,
448
- showAutoPackNotice: false,
449
- commandPrefix,
450
- }),
451
- );
641
+ if (isGroupMessage && !isCommandMessage && !isMessageFromBot) {
642
+ const autoStickerMedia = extractSupportedStickerMediaDetails(messageInfo, { includeQuoted: false });
643
+
644
+ if (autoStickerMedia && autoStickerMedia.mediaType !== 'sticker') {
645
+ const groupConfig = await groupConfigStore.getGroupConfig(remoteJid);
646
+ if (groupConfig.autoStickerEnabled) {
647
+ analysisPayload.processingResult = 'autosticker_triggered';
648
+ analysisPayload.metadata = {
649
+ ...analysisPayload.metadata,
650
+ auto_sticker_media_type: autoStickerMedia.mediaType || null,
651
+ };
652
+ runCommand('autosticker', () =>
653
+ processSticker(sock, messageInfo, senderJid, remoteJid, expirationMessage, senderName, '', {
654
+ includeQuotedMedia: false,
655
+ showAutoPackNotice: false,
656
+ commandPrefix,
657
+ }),
658
+ );
659
+ }
452
660
  }
453
661
  }
662
+ } catch (messageError) {
663
+ analysisPayload.processingResult = 'error';
664
+ analysisPayload.errorCode = normalizeAnalysisErrorCode(messageError);
665
+ logger.error('Erro ao processar mensagem individual:', {
666
+ error: messageError?.message,
667
+ messageId: messageInfo?.key?.id || null,
668
+ remoteJid,
669
+ });
670
+ } finally {
671
+ persistMessageAnalysisEvent(analysisPayload);
454
672
  }
455
673
  }
456
674
  } catch (error) {