@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
@@ -0,0 +1,83 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const sanitizeText = (value, maxLength = 255) => {
4
+ const normalized = String(value || '')
5
+ .trim()
6
+ .replace(/\s+/g, ' ')
7
+ .slice(0, maxLength);
8
+ return normalized || null;
9
+ };
10
+
11
+ const sanitizeCommandName = (value) => {
12
+ const normalized = String(value || '')
13
+ .trim()
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9_-]/g, '')
16
+ .slice(0, 64);
17
+ return normalized || null;
18
+ };
19
+
20
+ const sanitizeBool = (value) => (value ? 1 : 0);
21
+
22
+ const clampInt = (value, fallback, min, max) => {
23
+ const numeric = Number(value);
24
+ if (!Number.isFinite(numeric)) return fallback;
25
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
26
+ };
27
+
28
+ const sanitizeMetadata = (value) => {
29
+ if (!value || typeof value !== 'object') return null;
30
+ try {
31
+ const asJson = JSON.stringify(value);
32
+ if (!asJson || asJson === '{}') return null;
33
+ return asJson;
34
+ } catch {
35
+ return null;
36
+ }
37
+ };
38
+
39
+ export async function createMessageAnalysisEvent(payload = {}, connection = null) {
40
+ const messageId = sanitizeText(payload.messageId, 255);
41
+ const chatId = sanitizeText(payload.chatId, 255);
42
+ const senderId = sanitizeText(payload.senderId, 255);
43
+ const senderName = sanitizeText(payload.senderName, 120);
44
+ const upsertType = sanitizeText(payload.upsertType, 32);
45
+ const source = sanitizeText(payload.source, 32) || 'whatsapp';
46
+ const commandPrefix = sanitizeText(payload.commandPrefix, 8);
47
+ const commandName = sanitizeCommandName(payload.commandName);
48
+ const messageKind = sanitizeText(payload.messageKind, 48) || 'other';
49
+ const processingResult = sanitizeText(payload.processingResult, 64) || 'processed';
50
+ const errorCode = sanitizeText(payload.errorCode, 96);
51
+ const metadata = sanitizeMetadata(payload.metadata);
52
+
53
+ await executeQuery(
54
+ `INSERT INTO ${TABLES.MESSAGE_ANALYSIS_EVENT}
55
+ (
56
+ message_id,
57
+ chat_id,
58
+ sender_id,
59
+ sender_name,
60
+ upsert_type,
61
+ source,
62
+ is_group,
63
+ is_from_bot,
64
+ is_command,
65
+ command_name,
66
+ command_args_count,
67
+ command_known,
68
+ command_prefix,
69
+ message_kind,
70
+ has_media,
71
+ media_count,
72
+ text_length,
73
+ processing_result,
74
+ error_code,
75
+ metadata
76
+ )
77
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
78
+ [messageId, chatId, senderId, senderName, upsertType, source, sanitizeBool(payload.isGroup), sanitizeBool(payload.isFromBot), sanitizeBool(payload.isCommand), commandName, clampInt(payload.commandArgsCount, 0, 0, 64), payload.commandKnown === null || payload.commandKnown === undefined ? null : sanitizeBool(payload.commandKnown), commandPrefix, messageKind, sanitizeBool(payload.hasMedia), clampInt(payload.mediaCount, 0, 0, 25), clampInt(payload.textLength, 0, 0, 16_000), processingResult, errorCode, metadata],
79
+ connection,
80
+ );
81
+
82
+ return true;
83
+ }
@@ -223,9 +223,14 @@ function buildAutoPackNoticeText(result, commandPrefix = DEFAULT_COMMAND_PREFIX)
223
223
  return duplicateLines.join('\n');
224
224
  }
225
225
 
226
- const savedLines = [`📦 Figurinha salva automaticamente no pack *${packName}*${countLabel}.\n\n`, `Dica: use *${commandPrefix}pack list* para gerenciar seus packs.`, `Para enviar agora: *${commandPrefix}pack send ${packCommandTarget}*.`];
226
+ const savedLines = [
227
+ `✅ Figurinha adicionada ao pack *${packName}*${countLabel}.`,
228
+ '',
229
+ `📋 Gerencie seus packs com *${commandPrefix}pack list*.`,
230
+ `🚀 Envie agora com *${commandPrefix}pack send ${packCommandTarget}*.`,
231
+ ];
227
232
  if (packWebUrl) {
228
- savedLines.push(`🌐 Abrir no site: ${packWebUrl}`);
233
+ savedLines.push(`🌐 Veja no site: ${packWebUrl}`);
229
234
  } else {
230
235
  savedLines.push(`🔒 Pack privado/não publicado. Gerencie em: ${profileUrl}`);
231
236
  }
@@ -150,9 +150,14 @@ function buildAutoPackNoticeText(result, commandPrefix = DEFAULT_COMMAND_PREFIX)
150
150
  return duplicateLines.join('\n');
151
151
  }
152
152
 
153
- const savedLines = [`📦 Figurinha salva automaticamente no pack *${packName}*${countLabel}.\n\n`, `Dica: use *${commandPrefix}pack list* para gerenciar seus packs.`, `Para enviar agora: *${commandPrefix}pack send ${packCommandTarget}*.`];
153
+ const savedLines = [
154
+ `✅ Figurinha adicionada ao pack *${packName}*${countLabel}.`,
155
+ '',
156
+ `📋 Gerencie seus packs com *${commandPrefix}pack list*.`,
157
+ `🚀 Envie agora com *${commandPrefix}pack send ${packCommandTarget}*.`,
158
+ ];
154
159
  if (packWebUrl) {
155
- savedLines.push(`🌐 Abrir no site: ${packWebUrl}`);
160
+ savedLines.push(`🌐 Veja no site: ${packWebUrl}`);
156
161
  } else {
157
162
  savedLines.push(`🔒 Pack privado/não publicado. Gerencie em: ${profileUrl}`);
158
163
  }
@@ -108,9 +108,7 @@ const handleDomainEvent = async (event) => {
108
108
  if (packId) {
109
109
  enqueuePackScoreSnapshotRefresh([packId]);
110
110
  }
111
- const rebuildIdempotency = packId
112
- ? `evt:${eventType}:${packId}:${coalesceBucket}:rebuild_cycle`
113
- : `evt:${eventType}:${coalesceBucket}:rebuild_cycle`;
111
+ const rebuildIdempotency = packId ? `evt:${eventType}:${packId}:${coalesceBucket}:rebuild_cycle` : `evt:${eventType}:${coalesceBucket}:rebuild_cycle`;
114
112
  await enqueueTaskSafely({
115
113
  taskType: 'rebuild_cycle',
116
114
  payload: { reason: 'domain_event', event_type: eventType, aggregate_id: aggregateId, pack_id: packId || null, coalesced: true },
@@ -1,11 +1,14 @@
1
1
  import logger from '../../utils/logger/loggerModule.js';
2
2
  import { sendAndStore } from '../../services/messagePersistenceService.js';
3
- import { isUserJid } from '../../config/baileysConfig.js';
3
+ import { getJidServer, isUserJid, normalizeJid } from '../../config/baileysConfig.js';
4
4
  import stickerPackService from './stickerPackServiceRuntime.js';
5
5
  import { STICKER_PACK_ERROR_CODES, StickerPackError } from './stickerPackErrors.js';
6
6
  import { captureIncomingStickerAsset, resolveStickerAssetForCommand } from './stickerStorageService.js';
7
7
  import { buildStickerPackMessage, sendStickerPackWithFallback } from './stickerPackMessageService.js';
8
8
  import { sanitizeText } from './stickerPackUtils.js';
9
+ import { executeQuery, TABLES } from '../../../database/index.js';
10
+ import { extractSenderInfoFromMessage, extractUserIdInfo, resolveUserId } from '../../services/lidMapService.js';
11
+ import { toWhatsAppPhoneDigits } from '../../services/whatsappLoginLinkService.js';
9
12
 
10
13
  /**
11
14
  * Handlers de comando textual para gerenciamento de packs de figurinha.
@@ -14,6 +17,7 @@ const RATE_WINDOW_MS = Math.max(10_000, Number(process.env.STICKER_PACK_RATE_WIN
14
17
  const RATE_MAX_ACTIONS = Math.max(1, Number(process.env.STICKER_PACK_RATE_MAX_ACTIONS) || 20);
15
18
  const MAX_PACK_ITEMS = Math.max(1, Number(process.env.STICKER_PACK_MAX_ITEMS) || 30);
16
19
  const MAX_PACK_NAME_LENGTH = 120;
20
+ const LID_SERVERS = new Set(['lid', 'hosted.lid']);
17
21
 
18
22
  const rateMap = new Map();
19
23
 
@@ -159,20 +163,23 @@ const formatVisibilityLabel = (visibility) => {
159
163
  };
160
164
 
161
165
  /**
162
- * Detecta packs automáticos para ocultar em listagens do usuário.
166
+ * Detecta packs automáticos de curadoria temática para ocultar em listagens padrão.
167
+ * Mantém visível o auto-pack coletor do usuário (ex.: "minhasfigurinhas1").
163
168
  *
164
169
  * @param {object|null|undefined} pack Pack retornado pelo serviço.
165
- * @returns {boolean} Verdadeiro quando o pack é automático.
170
+ * @returns {boolean} Verdadeiro quando for auto-pack temático/curadoria.
166
171
  */
167
- const isAutomaticPack = (pack) => {
172
+ const isThemeCurationPack = (pack) => {
168
173
  if (!pack || typeof pack !== 'object') return false;
169
- if (pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1) return true;
170
174
 
171
175
  const name = String(pack.name || '').trim();
172
176
  if (/^\[auto\]/i.test(name)) return true;
173
177
 
174
178
  const description = String(pack.description || '').toLowerCase();
175
- return description.includes('[auto-theme:') || description.includes('[auto-tag:');
179
+ if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
180
+
181
+ const themeKey = String(pack.pack_theme_key || '').trim();
182
+ return Boolean(themeKey);
176
183
  };
177
184
 
178
185
  /**
@@ -448,6 +455,142 @@ const readSingleArgument = (input) => {
448
455
  return value ? value : null;
449
456
  };
450
457
 
458
+ const buildOwnerLookupJids = (value) => {
459
+ const normalized = normalizeJid(value) || '';
460
+ if (!normalized || !normalized.includes('@')) return [];
461
+ const lookup = new Set([normalized]);
462
+ const digits = toWhatsAppPhoneDigits(normalized);
463
+ if (!digits) return Array.from(lookup);
464
+ lookup.add(normalizeJid(`${digits}@s.whatsapp.net`) || '');
465
+ lookup.add(normalizeJid(`${digits}@c.us`) || '');
466
+ lookup.add(normalizeJid(`${digits}@hosted`) || '');
467
+ return Array.from(lookup).filter(Boolean);
468
+ };
469
+
470
+ const appendOwnerCandidate = (candidateSet, lookupSet, value) => {
471
+ const normalized = normalizeJid(value) || '';
472
+ if (!normalized || !normalized.includes('@')) return;
473
+ candidateSet.add(normalized);
474
+ for (const lookupJid of buildOwnerLookupJids(normalized)) {
475
+ lookupSet.add(lookupJid);
476
+ }
477
+ };
478
+
479
+ const dedupePacksById = (packs = []) => {
480
+ const dedup = new Map();
481
+ for (const pack of Array.isArray(packs) ? packs : []) {
482
+ if (!pack?.id) continue;
483
+ const existing = dedup.get(pack.id);
484
+ if (!existing) {
485
+ dedup.set(pack.id, pack);
486
+ continue;
487
+ }
488
+ const currentUpdatedAt = Date.parse(String(pack.updated_at || pack.created_at || ''));
489
+ const existingUpdatedAt = Date.parse(String(existing.updated_at || existing.created_at || ''));
490
+ if (Number.isFinite(currentUpdatedAt) && (!Number.isFinite(existingUpdatedAt) || currentUpdatedAt > existingUpdatedAt)) {
491
+ dedup.set(pack.id, pack);
492
+ }
493
+ }
494
+
495
+ return Array.from(dedup.values()).sort((a, b) => {
496
+ const aUpdatedAt = Date.parse(String(a?.updated_at || a?.created_at || ''));
497
+ const bUpdatedAt = Date.parse(String(b?.updated_at || b?.created_at || ''));
498
+ if (!Number.isFinite(aUpdatedAt) && !Number.isFinite(bUpdatedAt)) return 0;
499
+ if (!Number.isFinite(aUpdatedAt)) return 1;
500
+ if (!Number.isFinite(bUpdatedAt)) return -1;
501
+ return bUpdatedAt - aUpdatedAt;
502
+ });
503
+ };
504
+
505
+ const resolveOwnerCandidatesForPackCommand = async ({ senderJid, messageInfo }) => {
506
+ const candidates = new Set();
507
+ const lookupByJid = new Set();
508
+
509
+ const senderInfo = extractSenderInfoFromMessage(messageInfo);
510
+ appendOwnerCandidate(candidates, lookupByJid, senderJid);
511
+ appendOwnerCandidate(candidates, lookupByJid, senderInfo?.jid);
512
+ appendOwnerCandidate(candidates, lookupByJid, senderInfo?.participantAlt);
513
+ appendOwnerCandidate(candidates, lookupByJid, senderInfo?.lid);
514
+
515
+ const directResolved = await resolveUserId(extractUserIdInfo(senderJid)).catch(() => null);
516
+ if (directResolved) {
517
+ appendOwnerCandidate(candidates, lookupByJid, directResolved);
518
+ }
519
+
520
+ const senderResolved = await resolveUserId({
521
+ lid: senderInfo?.lid,
522
+ jid: senderInfo?.jid || senderJid || null,
523
+ participantAlt: senderInfo?.participantAlt || null,
524
+ }).catch(() => null);
525
+ if (senderResolved) {
526
+ appendOwnerCandidate(candidates, lookupByJid, senderResolved);
527
+ }
528
+
529
+ const lookupValues = Array.from(lookupByJid).filter(Boolean);
530
+ for (let offset = 0; offset < lookupValues.length; offset += 200) {
531
+ const chunk = lookupValues.slice(offset, offset + 200);
532
+ if (!chunk.length) continue;
533
+ const placeholders = chunk.map(() => '?').join(', ');
534
+ const lookupParams = [...chunk, ...chunk];
535
+ const rows = await executeQuery(
536
+ `SELECT lid, jid
537
+ FROM ${TABLES.LID_MAP}
538
+ WHERE jid IN (${placeholders})
539
+ OR lid IN (${placeholders})
540
+ ORDER BY last_seen DESC
541
+ LIMIT 500`,
542
+ lookupParams,
543
+ ).catch(() => []);
544
+
545
+ for (const row of Array.isArray(rows) ? rows : []) {
546
+ appendOwnerCandidate(candidates, lookupByJid, row?.jid || '');
547
+ appendOwnerCandidate(candidates, lookupByJid, row?.lid || '');
548
+ }
549
+ }
550
+
551
+ const lidCandidates = Array.from(candidates).filter((candidate) => LID_SERVERS.has(getJidServer(candidate)));
552
+ for (const lidValue of lidCandidates) {
553
+ const resolved = await resolveUserId(extractUserIdInfo(lidValue)).catch(() => null);
554
+ if (resolved) {
555
+ appendOwnerCandidate(candidates, lookupByJid, resolved);
556
+ }
557
+ }
558
+
559
+ return Array.from(candidates);
560
+ };
561
+
562
+ const pickPrimaryOwnerCandidate = (ownerCandidates, senderJid) => {
563
+ const preferred = (Array.isArray(ownerCandidates) ? ownerCandidates : []).find((candidate) => {
564
+ const server = getJidServer(candidate);
565
+ if (!server || LID_SERVERS.has(server)) return false;
566
+ return server !== 'google.oauth';
567
+ });
568
+ if (preferred) return preferred;
569
+
570
+ const normalizedSender = normalizeJid(senderJid) || '';
571
+ if (normalizedSender) return normalizedSender;
572
+ return Array.isArray(ownerCandidates) && ownerCandidates.length ? ownerCandidates[0] : senderJid;
573
+ };
574
+
575
+ const runWithOwnerFallback = async (ownerCandidates, action) => {
576
+ const owners = Array.isArray(ownerCandidates) && ownerCandidates.length ? ownerCandidates : [];
577
+ let notFoundError = null;
578
+ for (const candidateOwner of owners) {
579
+ try {
580
+ return await action(candidateOwner);
581
+ } catch (error) {
582
+ if (error instanceof StickerPackError && error.code === STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND) {
583
+ notFoundError = notFoundError || error;
584
+ continue;
585
+ }
586
+ throw error;
587
+ }
588
+ }
589
+
590
+ if (notFoundError) throw notFoundError;
591
+ throw new StickerPackError(STICKER_PACK_ERROR_CODES.PACK_NOT_FOUND, 'Pack não encontrado para este usuário.');
592
+ };
593
+
451
594
  /**
452
595
  * Normaliza e valida nome de pack (permite espaços e emojis).
453
596
  *
@@ -530,7 +673,9 @@ const resolveStickerFromCommandContext = async ({ messageInfo, ownerJid, include
530
673
  * @returns {Promise<void>}
531
674
  */
532
675
  export async function handlePackCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, senderName, text, commandPrefix }) {
533
- const ownerJid = senderJid;
676
+ const ownerCandidatesRaw = await resolveOwnerCandidatesForPackCommand({ senderJid, messageInfo }).catch(() => []);
677
+ const ownerJid = pickPrimaryOwnerCandidate(ownerCandidatesRaw, senderJid);
678
+ const ownerCandidates = Array.from(new Set([ownerJid, ...ownerCandidatesRaw].filter(Boolean)));
534
679
  const rate = checkRateLimit(ownerJid);
535
680
 
536
681
  if (rate.limited) {
@@ -590,8 +735,9 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
590
735
  }
591
736
 
592
737
  case 'list': {
593
- const packs = await stickerPackService.listPacks({ ownerJid, limit: 100 });
594
- const manualPacks = packs.filter((pack) => !isAutomaticPack(pack));
738
+ const packLists = await Promise.all(ownerCandidates.map((candidateOwner) => stickerPackService.listPacks({ ownerJid: candidateOwner, limit: 100 })));
739
+ const packs = dedupePacksById(packLists.flatMap((items) => (Array.isArray(items) ? items : [])));
740
+ const manualPacks = packs.filter((pack) => !isThemeCurationPack(pack));
595
741
 
596
742
  await sendReply({
597
743
  sock,
@@ -605,7 +751,7 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
605
751
 
606
752
  case 'info': {
607
753
  const identifier = readSingleArgument(rest);
608
- const pack = await stickerPackService.getPackInfo({ ownerJid, identifier });
754
+ const pack = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.getPackInfo({ ownerJid: candidateOwner, identifier }));
609
755
 
610
756
  await sendReply({
611
757
  sock,
@@ -620,7 +766,7 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
620
766
  case 'rename': {
621
767
  const { identifier, value } = parseIdentifierAndValue(rest);
622
768
  const normalizedName = normalizePackName(value, { label: 'Novo nome do pack' });
623
- const updated = await stickerPackService.renamePack({ ownerJid, identifier, name: normalizedName });
769
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.renamePack({ ownerJid: candidateOwner, identifier, name: normalizedName }));
624
770
 
625
771
  await sendReply({
626
772
  sock,
@@ -639,7 +785,7 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
639
785
 
640
786
  case 'setpub': {
641
787
  const { identifier, value } = parseIdentifierAndValue(rest);
642
- const updated = await stickerPackService.setPackPublisher({ ownerJid, identifier, publisher: value });
788
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.setPackPublisher({ ownerJid: candidateOwner, identifier, publisher: value }));
643
789
 
644
790
  await sendReply({
645
791
  sock,
@@ -659,7 +805,7 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
659
805
  case 'setdesc': {
660
806
  const { identifier, value } = parseIdentifierAndValue(rest);
661
807
  const description = value === '-' || value.toLowerCase() === 'clear' ? '' : value;
662
- const updated = await stickerPackService.setPackDescription({ ownerJid, identifier, description });
808
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.setPackDescription({ ownerJid: candidateOwner, identifier, description }));
663
809
 
664
810
  await sendReply({
665
811
  sock,
@@ -684,11 +830,13 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
684
830
  throw new StickerPackError(STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND, 'Não encontrei uma figurinha para definir como capa.');
685
831
  }
686
832
 
687
- const updated = await stickerPackService.setPackCover({
688
- ownerJid,
689
- identifier,
690
- stickerId: asset.id,
691
- });
833
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
834
+ stickerPackService.setPackCover({
835
+ ownerJid: candidateOwner,
836
+ identifier,
837
+ stickerId: asset.id,
838
+ }),
839
+ );
692
840
 
693
841
  await sendReply({
694
842
  sock,
@@ -715,13 +863,15 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
715
863
  throw new StickerPackError(STICKER_PACK_ERROR_CODES.STICKER_NOT_FOUND, 'Não encontrei uma figurinha para adicionar.');
716
864
  }
717
865
 
718
- const updated = await stickerPackService.addStickerToPack({
719
- ownerJid,
720
- identifier,
721
- asset,
722
- emojis: options.emojis,
723
- accessibilityLabel: options.label || options.accessibility || null,
724
- });
866
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
867
+ stickerPackService.addStickerToPack({
868
+ ownerJid: candidateOwner,
869
+ identifier,
870
+ asset,
871
+ emojis: options.emojis,
872
+ accessibilityLabel: options.label || options.accessibility || null,
873
+ }),
874
+ );
725
875
 
726
876
  await sendReply({
727
877
  sock,
@@ -742,11 +892,13 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
742
892
  const { token: identifier, rest: selectorRest } = readToken(rest);
743
893
  const { token: selector } = readToken(selectorRest);
744
894
 
745
- const result = await stickerPackService.removeStickerFromPack({
746
- ownerJid,
747
- identifier,
748
- selector,
749
- });
895
+ const result = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
896
+ stickerPackService.removeStickerFromPack({
897
+ ownerJid: candidateOwner,
898
+ identifier,
899
+ selector,
900
+ }),
901
+ );
750
902
 
751
903
  await sendReply({
752
904
  sock,
@@ -765,16 +917,18 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
765
917
 
766
918
  case 'reorder': {
767
919
  const { token: identifier, rest: rawOrder } = readToken(rest);
768
- const orderStickerIds = await parseReorderInput({
769
- ownerJid,
770
- identifier,
771
- rawOrder,
772
- });
920
+ const updated = await runWithOwnerFallback(ownerCandidates, async (candidateOwner) => {
921
+ const orderStickerIds = await parseReorderInput({
922
+ ownerJid: candidateOwner,
923
+ identifier,
924
+ rawOrder,
925
+ });
773
926
 
774
- const updated = await stickerPackService.reorderPackItems({
775
- ownerJid,
776
- identifier,
777
- orderStickerIds,
927
+ return stickerPackService.reorderPackItems({
928
+ ownerJid: candidateOwner,
929
+ identifier,
930
+ orderStickerIds,
931
+ });
778
932
  });
779
933
 
780
934
  await sendReply({
@@ -796,11 +950,13 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
796
950
  const { token: identifier, rest: cloneNameRaw } = readToken(rest);
797
951
  const cloneName = normalizePackName(cloneNameRaw, { label: 'Novo nome do clone' });
798
952
 
799
- const cloned = await stickerPackService.clonePack({
800
- ownerJid,
801
- identifier,
802
- newName: cloneName,
803
- });
953
+ const cloned = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
954
+ stickerPackService.clonePack({
955
+ ownerJid: candidateOwner,
956
+ identifier,
957
+ newName: cloneName,
958
+ }),
959
+ );
804
960
 
805
961
  await sendReply({
806
962
  sock,
@@ -819,7 +975,7 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
819
975
 
820
976
  case 'delete': {
821
977
  const identifier = readSingleArgument(rest);
822
- const deleted = await stickerPackService.deletePack({ ownerJid, identifier });
978
+ const deleted = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.deletePack({ ownerJid: candidateOwner, identifier }));
823
979
 
824
980
  await sendReply({
825
981
  sock,
@@ -840,11 +996,13 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
840
996
  const { token: identifier, rest: visibilityRaw } = readToken(rest);
841
997
  const visibility = unquote(visibilityRaw);
842
998
 
843
- const updated = await stickerPackService.setPackVisibility({
844
- ownerJid,
845
- identifier,
846
- visibility,
847
- });
999
+ const updated = await runWithOwnerFallback(ownerCandidates, (candidateOwner) =>
1000
+ stickerPackService.setPackVisibility({
1001
+ ownerJid: candidateOwner,
1002
+ identifier,
1003
+ visibility,
1004
+ }),
1005
+ );
848
1006
 
849
1007
  await sendReply({
850
1008
  sock,
@@ -863,7 +1021,7 @@ export async function handlePackCommand({ sock, remoteJid, messageInfo, expirati
863
1021
 
864
1022
  case 'send': {
865
1023
  const identifier = readSingleArgument(rest);
866
- const packDetails = await stickerPackService.getPackInfoForSend({ ownerJid, identifier });
1024
+ const packDetails = await runWithOwnerFallback(ownerCandidates, (candidateOwner) => stickerPackService.getPackInfoForSend({ ownerJid: candidateOwner, identifier }));
867
1025
  const packBuild = await buildStickerPackMessage(packDetails);
868
1026
  const sendResult = await sendStickerPackWithFallback({
869
1027
  sock,
@@ -942,15 +1100,28 @@ export async function maybeCaptureIncomingSticker({ messageInfo, senderJid, isMe
942
1100
  if (isMessageFromBot) return null;
943
1101
  if (!isUserJid(senderJid)) return null;
944
1102
 
1103
+ const senderInfo = extractSenderInfoFromMessage(messageInfo);
1104
+ let ownerJid = normalizeJid(senderJid) || senderJid;
1105
+ try {
1106
+ const resolvedOwner = await resolveUserId({
1107
+ lid: senderInfo?.lid,
1108
+ jid: senderInfo?.jid || senderJid || null,
1109
+ participantAlt: senderInfo?.participantAlt || null,
1110
+ });
1111
+ ownerJid = normalizeJid(resolvedOwner || ownerJid) || ownerJid;
1112
+ } catch {
1113
+ ownerJid = normalizeJid(senderJid) || senderJid;
1114
+ }
1115
+
945
1116
  try {
946
1117
  return await captureIncomingStickerAsset({
947
1118
  messageInfo,
948
- ownerJid: senderJid,
1119
+ ownerJid,
949
1120
  });
950
1121
  } catch (error) {
951
1122
  logger.warn('Falha ao capturar figurinha recebida para storage.', {
952
1123
  action: 'pack_capture_warning',
953
- owner_jid: senderJid,
1124
+ owner_jid: ownerJid,
954
1125
  error: error.message,
955
1126
  });
956
1127
  return null;
@@ -57,15 +57,17 @@ const toStatusClass = (statusCode) => {
57
57
  return `${head}xx`;
58
58
  };
59
59
 
60
- export const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null } = {}) => {
60
+ export const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null, userConfig = null, systemAdminConfig = null } = {}) => {
61
61
  if (pathname?.startsWith(metricsPath)) return 'metrics';
62
+ if (pathname === '/healthz' || pathname === '/readyz') return 'health';
62
63
  if (pathname === '/sitemap.xml') return 'sitemap';
63
64
  if (pathname === '/api/marketplace/stats') return 'marketplace_stats';
64
65
 
65
- const apiBasePath = catalogConfig?.apiBasePath || '';
66
+ const apiBasePath = catalogConfig?.apiBasePath || userConfig?.apiBasePath || '';
66
67
  const webPath = catalogConfig?.webPath || '';
67
68
  const dataPublicPath = catalogConfig?.dataPublicPath || '';
68
- const userProfilePath = catalogConfig?.userProfilePath || '';
69
+ const userProfilePath = userConfig?.webPath || '';
70
+ const systemAdminPath = systemAdminConfig?.webPath || '';
69
71
 
70
72
  if (apiBasePath && (pathname === apiBasePath || pathname?.startsWith(`${apiBasePath}/`))) {
71
73
  if (pathname === `${apiBasePath}/auth/google/session` || pathname === `${apiBasePath}/me` || pathname === `${apiBasePath}/admin/session`) {
@@ -78,6 +80,7 @@ export const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null
78
80
  return 'catalog_api_public';
79
81
  }
80
82
  if (dataPublicPath && (pathname === dataPublicPath || pathname?.startsWith(`${dataPublicPath}/`))) return 'catalog_data_asset';
83
+ if (systemAdminPath && (pathname === systemAdminPath || pathname === `${systemAdminPath}/`)) return 'system_admin_web';
81
84
  if (userProfilePath && (pathname === userProfilePath || pathname === `${userProfilePath}/`)) return 'catalog_user_profile';
82
85
  if (webPath && (pathname === webPath || pathname?.startsWith(`${webPath}/`))) return 'catalog_web';
83
86
 
@@ -0,0 +1,77 @@
1
+ import { executeQuery, TABLES } from '../../database/index.js';
2
+ import { normalizeJid } from '../config/baileysConfig.js';
3
+ import { toWhatsAppPhoneDigits } from './whatsappLoginLinkService.js';
4
+
5
+ const parseEnvInt = (value, fallback, min, max) => {
6
+ const numeric = Number(value);
7
+ if (!Number.isFinite(numeric)) return fallback;
8
+ return Math.max(min, Math.min(max, Math.floor(numeric)));
9
+ };
10
+
11
+ const GOOGLE_LINK_CHECK_CACHE_TTL_MS = parseEnvInt(process.env.WHATSAPP_GOOGLE_LINK_CHECK_CACHE_TTL_MS, 60_000, 1_000, 10 * 60_000);
12
+ const googleLinkCheckCache = new Map();
13
+ let googleLinkTableMissingLogged = false;
14
+
15
+ const normalizeCacheKey = ({ ownerJid = '', ownerPhone = '' }) => {
16
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
17
+ const normalizedOwnerPhone = toWhatsAppPhoneDigits(ownerPhone || ownerJid) || '';
18
+ return `${normalizedOwnerJid}|${normalizedOwnerPhone}`;
19
+ };
20
+
21
+ const getCachedGoogleLinkStatus = (cacheKey) => {
22
+ const cached = googleLinkCheckCache.get(cacheKey);
23
+ if (!cached) return null;
24
+ if (Number(cached.expiresAt || 0) <= Date.now()) {
25
+ googleLinkCheckCache.delete(cacheKey);
26
+ return null;
27
+ }
28
+ return Boolean(cached.linked);
29
+ };
30
+
31
+ const setCachedGoogleLinkStatus = (cacheKey, linked) => {
32
+ googleLinkCheckCache.set(cacheKey, {
33
+ linked: Boolean(linked),
34
+ expiresAt: Date.now() + GOOGLE_LINK_CHECK_CACHE_TTL_MS,
35
+ });
36
+ };
37
+
38
+ export const isWhatsAppUserLinkedToGoogleWebAccount = async ({ ownerJid = '', ownerPhone = '' } = {}) => {
39
+ const normalizedOwnerJid = normalizeJid(ownerJid) || '';
40
+ const normalizedOwnerPhone = toWhatsAppPhoneDigits(ownerPhone || ownerJid) || '';
41
+ if (!normalizedOwnerJid && !normalizedOwnerPhone) return false;
42
+
43
+ const cacheKey = normalizeCacheKey({ ownerJid: normalizedOwnerJid, ownerPhone: normalizedOwnerPhone });
44
+ const cached = getCachedGoogleLinkStatus(cacheKey);
45
+ if (cached !== null) return cached;
46
+
47
+ const whereClauses = [];
48
+ const params = [];
49
+ if (normalizedOwnerJid) {
50
+ whereClauses.push('owner_jid = ?');
51
+ params.push(normalizedOwnerJid);
52
+ }
53
+ if (normalizedOwnerPhone) {
54
+ whereClauses.push('owner_phone = ?');
55
+ params.push(normalizedOwnerPhone);
56
+ }
57
+
58
+ const rows = await executeQuery(
59
+ `SELECT google_sub
60
+ FROM ${TABLES.STICKER_WEB_GOOGLE_USER}
61
+ WHERE ${whereClauses.join(' OR ')}
62
+ LIMIT 1`,
63
+ params,
64
+ ).catch((error) => {
65
+ if (error?.code === 'ER_NO_SUCH_TABLE') {
66
+ if (!googleLinkTableMissingLogged) {
67
+ googleLinkTableMissingLogged = true;
68
+ }
69
+ return [];
70
+ }
71
+ throw error;
72
+ });
73
+
74
+ const linked = Array.isArray(rows) && rows.length > 0;
75
+ setCachedGoogleLinkStatus(cacheKey, linked);
76
+ return linked;
77
+ };