@omnizap-system/omnizap 2.6.1 → 2.6.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 (172) hide show
  1. package/.env.example +78 -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 +6 -0
  6. package/app/configParts/adminIdentity.js +36 -7
  7. package/app/configParts/baileysConfig.js +343 -56
  8. package/app/configParts/groupUtils.js +226 -0
  9. package/app/configParts/loggerConfig.js +185 -0
  10. package/app/configParts/messagePersistenceService.js +307 -5
  11. package/app/configParts/sessionConfig.js +242 -0
  12. package/app/connection/baileysCompatibility.test.js +10 -1
  13. package/app/connection/baileysDbAuthState.js +205 -9
  14. package/app/connection/baileysLibsignalPatch.js +210 -0
  15. package/app/connection/groupOwnerWriteStateResolver.js +141 -0
  16. package/app/connection/socketController.js +694 -123
  17. package/app/connection/socketController.multiSession.test.js +128 -0
  18. package/app/controllers/messageController.js +1 -1
  19. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  20. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  21. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  22. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
  23. package/app/controllers/messageProcessingPipeline.js +90 -9
  24. package/app/controllers/messageProcessingPipeline.test.js +202 -0
  25. package/app/modules/adminModule/AGENT.md +1 -1
  26. package/app/modules/adminModule/commandConfig.json +3318 -1347
  27. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  28. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  29. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  30. package/app/modules/aiModule/AGENT.md +47 -30
  31. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  32. package/app/modules/aiModule/catCommand.js +132 -25
  33. package/app/modules/aiModule/commandConfig.json +114 -28
  34. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  35. package/app/modules/gameModule/AGENT.md +1 -1
  36. package/app/modules/gameModule/commandConfig.json +29 -0
  37. package/app/modules/menuModule/AGENT.md +1 -1
  38. package/app/modules/menuModule/commandConfig.json +45 -10
  39. package/app/modules/menuModule/menuCatalogService.js +190 -0
  40. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  41. package/app/modules/menuModule/menuDynamicService.js +511 -0
  42. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  43. package/app/modules/menuModule/menus.js +36 -5
  44. package/app/modules/playModule/AGENT.md +10 -5
  45. package/app/modules/playModule/commandConfig.json +74 -16
  46. package/app/modules/playModule/playCommandConstants.js +13 -7
  47. package/app/modules/playModule/playCommandCore.js +4 -6
  48. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  49. package/app/modules/playModule/playConfigRuntime.js +5 -6
  50. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  51. package/app/modules/quoteModule/AGENT.md +1 -1
  52. package/app/modules/quoteModule/commandConfig.json +29 -0
  53. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  54. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  55. package/app/modules/statsModule/AGENT.md +1 -1
  56. package/app/modules/statsModule/commandConfig.json +58 -0
  57. package/app/modules/stickerModule/AGENT.md +1 -1
  58. package/app/modules/stickerModule/commandConfig.json +145 -0
  59. package/app/modules/stickerPackModule/AGENT.md +1 -1
  60. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  61. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  62. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  63. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  64. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  65. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  66. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  67. package/app/modules/tiktokModule/AGENT.md +1 -1
  68. package/app/modules/tiktokModule/commandConfig.json +29 -0
  69. package/app/modules/userModule/AGENT.md +1 -1
  70. package/app/modules/userModule/commandConfig.json +29 -0
  71. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  72. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  73. package/app/observability/metrics.js +136 -0
  74. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  75. package/app/services/ai/geminiService.js +131 -7
  76. package/app/services/ai/geminiService.test.js +59 -2
  77. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  78. package/app/services/group/groupMetadataService.js +24 -1
  79. package/app/services/infra/dbWriteQueue.js +51 -21
  80. package/app/services/messaging/newsBroadcastService.js +843 -27
  81. package/app/services/multiSession/assignmentBalancerService.js +452 -0
  82. package/app/services/multiSession/groupOwnershipRepository.js +346 -0
  83. package/app/services/multiSession/groupOwnershipService.js +809 -0
  84. package/app/services/multiSession/groupOwnershipService.test.js +317 -0
  85. package/app/services/multiSession/sessionRegistryService.js +239 -0
  86. package/app/store/aiPromptStore.js +36 -19
  87. package/app/store/groupConfigStore.js +41 -5
  88. package/app/store/premiumUserStore.js +21 -7
  89. package/app/utils/antiLink/antiLinkModule.js +391 -25
  90. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  91. package/database/index.js +6 -0
  92. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  93. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  94. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  95. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  96. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  97. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  98. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  99. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  100. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  101. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  102. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  103. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  104. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  105. package/database/schema.sql +102 -1
  106. package/docker-compose.yml +4 -1
  107. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  108. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  109. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  110. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  111. package/docs/security/omnizap-static-security-headers.conf +25 -0
  112. package/ecosystem.prod.config.cjs +31 -11
  113. package/index.js +52 -18
  114. package/observability/alert-rules.yml +20 -0
  115. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  116. package/observability/mysql-setup.sql +4 -4
  117. package/observability/system-admin-observability.md +26 -0
  118. package/package.json +14 -6
  119. package/public/comandos/commands-catalog.json +2253 -78
  120. package/public/css/payments-react.css +478 -0
  121. package/public/js/apps/commandsReactApp.js +267 -87
  122. package/public/js/apps/createPackApp.js +3 -3
  123. package/public/js/apps/homeReactApp.js +2 -2
  124. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  125. package/public/js/apps/paymentsReactApp.js +399 -0
  126. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  127. package/public/js/apps/stickersApp.js +255 -103
  128. package/public/js/apps/termsReactApp.js +57 -8
  129. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  130. package/public/js/apps/userReactApp.js +96 -47
  131. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  132. package/public/pages/pagamentos-cancelado.html +21 -0
  133. package/public/pages/pagamentos-sucesso.html +21 -0
  134. package/public/pages/pagamentos.html +30 -0
  135. package/public/pages/politica-de-privacidade.html +1 -1
  136. package/public/pages/stickers.html +5 -5
  137. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  138. package/public/pages/termos-de-uso.html +1 -1
  139. package/public/pages/user-password-reset.html +3 -4
  140. package/public/pages/user-systemadm.html +8 -462
  141. package/public/pages/user.html +1 -1
  142. package/scripts/clear-whatsapp-session.sh +123 -0
  143. package/scripts/core-ai-mode.mjs +163 -0
  144. package/scripts/deploy.sh +13 -0
  145. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  146. package/scripts/generate-commands-catalog.mjs +155 -0
  147. package/scripts/new-whatsapp-session.sh +564 -0
  148. package/scripts/security-web-surface-check.mjs +218 -0
  149. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  150. package/server/controllers/admin/systemAdminController.js +254 -0
  151. package/server/controllers/payments/paymentsController.js +731 -0
  152. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  153. package/server/controllers/system/contactController.js +9 -17
  154. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  155. package/server/controllers/system/systemController.js +228 -1
  156. package/server/controllers/userController.js +6 -0
  157. package/server/email/emailAutomationRuntime.js +36 -1
  158. package/server/email/emailAutomationService.js +42 -1
  159. package/server/email/emailTemplateService.js +140 -33
  160. package/server/http/httpRequestUtils.js +18 -14
  161. package/server/http/httpServer.js +8 -4
  162. package/server/middleware/securityHeaders.js +35 -3
  163. package/server/routes/admin/systemAdminRouter.js +6 -0
  164. package/server/routes/indexRouter.js +50 -6
  165. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  166. package/server/routes/payments/paymentsRouter.js +47 -0
  167. package/server/routes/static/staticPageRouter.js +30 -1
  168. package/server/utils/publicContact.js +31 -0
  169. package/utils/whatsapp/contactEnv.js +39 -0
  170. package/vite.config.mjs +5 -1
  171. package/app/modules/playModule/local/installYtDlp.js +0 -25
  172. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -67,6 +67,7 @@ export const stripWebpExtension = (value) =>
67
67
  .replace(/\.webp$/i, '');
68
68
 
69
69
  const clampInt = (value, fallback, min, max) => {
70
+ if (value === undefined || value === null || value === '') return fallback;
70
71
  const parsed = Number(value);
71
72
  if (!Number.isFinite(parsed)) return fallback;
72
73
  return Math.max(min, Math.min(max, Math.floor(parsed)));
@@ -964,8 +965,6 @@ const convertUploadMediaToWebp = async ({ ownerJid, buffer, mimetype }) => {
964
965
  const PACK_TAG_MARKER_REGEX = /\[pack-tags:([^\]]+)\]/i;
965
966
  const AUTO_PACK_MARKER_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/gi;
966
967
  const AUTO_PACK_MARKER_TEST_REGEX = /\[(?:auto-theme|auto-tag):[^\]]+\]/i;
967
- const AUTO_PACK_COLLECTOR_MARKER = '[auto-pack:collector]';
968
- const AUTO_PACK_COLLECTOR_LEGACY_TEXT = 'coleção automática de figurinhas criadas pelo usuário.';
969
968
  const AUTO_PACK_DESCRIPTION_PREFIX_REGEX = /^curadoria automática por tema\.\s*tema:\s*[^.]+\.?\s*(?:score\s*=\s*-?\d+(?:\.\d+)?\.?\s*)?/i;
970
969
  const AUTO_PACK_SCORE_FRAGMENT_REGEX = /\bscore\s*=\s*-?\d+(?:\.\d+)?\.?/gi;
971
970
  const normalizePackTag = (value) =>
@@ -1024,29 +1023,10 @@ const parsePackDescriptionMetadata = (description) => {
1024
1023
  };
1025
1024
  };
1026
1025
 
1027
- const isCollectorAutoPack = (pack) => {
1028
- if (!pack || typeof pack !== 'object') return false;
1029
- const description = String(pack.description || '').toLowerCase();
1030
- return description.includes(AUTO_PACK_COLLECTOR_MARKER) || description.includes(AUTO_PACK_COLLECTOR_LEGACY_TEXT);
1031
- };
1032
-
1033
- const isThemeCurationAutoPack = (pack) => {
1034
- if (!pack || typeof pack !== 'object') return false;
1035
- const name = String(pack.name || '').trim();
1036
- if (/^\[auto\]/i.test(name)) return true;
1037
-
1038
- const description = String(pack.description || '').toLowerCase();
1039
- if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
1040
-
1041
- return Boolean(String(pack.pack_theme_key || '').trim());
1042
- };
1043
-
1044
1026
  const shouldHidePackFromMyProfileDefault = (pack, { includeAutoPacks = false } = {}) => {
1045
1027
  if (!pack || typeof pack !== 'object') return false;
1046
1028
  if (includeAutoPacks) return false;
1047
- if (isCollectorAutoPack(pack)) return false;
1048
- if (isThemeCurationAutoPack(pack)) return true;
1049
- return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
1029
+ return false;
1050
1030
  };
1051
1031
 
1052
1032
  const buildPackDescriptionWithTags = (description, tags = []) => {
@@ -1734,6 +1714,12 @@ const getMarketplaceStatsCached = async (visibility) => {
1734
1714
  bucket.expiresAt = __timeNowMs() + HOME_MARKETPLACE_STATS_CACHE_SECONDS * 1000;
1735
1715
  return data;
1736
1716
  })
1717
+ .catch((error) => {
1718
+ if (hasValue && bucket.value) {
1719
+ return bucket.value;
1720
+ }
1721
+ throw error;
1722
+ })
1737
1723
  .finally(() => {
1738
1724
  bucket.pending = null;
1739
1725
  });
@@ -3219,7 +3205,7 @@ const handleCreatePackRequest = async (req, res) => {
3219
3205
  });
3220
3206
  const manualTags = mergeUniqueTags(Array.isArray(payload?.tags) ? payload.tags : []).slice(0, 8);
3221
3207
  const persistedDescription = buildPackDescriptionWithTags(description, manualTags);
3222
- const visibility = String(payload?.visibility || 'public')
3208
+ const visibility = String(payload?.visibility || 'private')
3223
3209
  .trim()
3224
3210
  .toLowerCase();
3225
3211
  const googleSession = await resolveGoogleWebSessionFromRequest(req);
@@ -1,13 +1,12 @@
1
1
  import logger from '#logger';
2
2
  import { getActiveSocket, getAdminPhone, getAdminRawValue, getJidUser, resolveAdminJid, resolveBotJid, extractUserIdInfo, resolveUserId } from '../../../app/config/index.js';
3
+ import { isLikelyWhatsAppPhone, normalizePhoneDigits, resolveAdminPhoneFromEnv, resolveBotPhoneFromEnv, resolveSupportPhoneFromEnv } from '../../../utils/whatsapp/contactEnv.js';
3
4
 
4
5
  const PACK_COMMAND_PREFIX = String(process.env.COMMAND_PREFIX || '/').trim() || '/';
5
6
 
6
- const normalizePhoneDigits = (value) => String(value || '').replace(/\D+/g, '');
7
-
8
7
  const isPlausibleWhatsAppPhone = (value) => {
9
8
  const digits = normalizePhoneDigits(value);
10
- return digits.length >= 10 && digits.length <= 15 ? digits : '';
9
+ return isLikelyWhatsAppPhone(digits) ? digits : '';
11
10
  };
12
11
 
13
12
  const resolveActiveSocketBotJid = (activeSocket) => {
@@ -26,18 +25,12 @@ export const resolveCatalogBotPhone = () => {
26
25
  const jidUser = botJid ? getJidUser(botJid) : null;
27
26
  const fromSocket = normalizePhoneDigits(jidUser || '');
28
27
 
29
- if (fromSocket && fromSocket.length >= 10) {
28
+ if (isLikelyWhatsAppPhone(fromSocket)) {
30
29
  return fromSocket;
31
30
  }
32
31
 
33
- const envCandidates = [process.env.WHATSAPP_BOT_NUMBER, process.env.BOT_NUMBER, process.env.PHONE_NUMBER, process.env.BOT_PHONE_NUMBER, process.env.USER_ADMIN];
34
-
35
- for (const candidate of envCandidates) {
36
- const digits = normalizePhoneDigits(candidate || '');
37
- if (digits && digits.length >= 10 && digits.length <= 15) {
38
- return digits;
39
- }
40
- }
32
+ const fromEnv = resolveBotPhoneFromEnv({ fallback: '' });
33
+ if (fromEnv) return fromEnv;
41
34
 
42
35
  logger.warn('Nao foi possivel resolver o numero do bot para contato.', {
43
36
  action: 'resolve_bot_phone_failed',
@@ -75,12 +68,11 @@ const resolveSupportAdminPhone = async () => {
75
68
  const adminPhone = isPlausibleWhatsAppPhone(getAdminPhone() || '');
76
69
  if (adminPhone) return adminPhone;
77
70
 
78
- const candidates = [process.env.WHATSAPP_SUPPORT_NUMBER, process.env.OWNER_NUMBER, process.env.USER_ADMIN];
71
+ const configuredAdminPhone = resolveAdminPhoneFromEnv({ fallback: '' });
72
+ if (configuredAdminPhone) return configuredAdminPhone;
79
73
 
80
- for (const candidate of candidates) {
81
- const digits = isPlausibleWhatsAppPhone(getJidUser(candidate || '') || candidate);
82
- if (digits) return digits;
83
- }
74
+ const configuredSupportPhone = resolveSupportPhoneFromEnv({ fallback: '' });
75
+ if (configuredSupportPhone) return configuredSupportPhone;
84
76
 
85
77
  return '';
86
78
  };
@@ -1,5 +1,6 @@
1
1
  import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
2
2
  import { withTimeout } from '../../http/httpRequestUtils.js';
3
+ import { resolveBotPhoneFromEnv } from '../../../utils/whatsapp/contactEnv.js';
3
4
 
4
5
  export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger, getSystemMetrics, getActiveSocket, resolveSocketReadyState, resolveActiveSocketBotJid, resolveCatalogBotPhone, fetchPrometheusSummary, metricsEndpoint, systemSummaryCache, systemSummaryCacheSeconds, readmeSummaryCache, readmeSummaryCacheSeconds, readmeMessageTypeSampleLimit, readmeCommandPrefix, buildMenuCaption, buildStickerMenu, buildMediaMenu, buildQuoteMenu, buildAnimeMenu, buildAiMenu, buildStatsMenu, buildAdminMenu, profilePictureUrlFromActiveSocket, normalizeJid, getJidUser, globalRankCache, globalRankRefreshSeconds, marketplaceGlobalStatsCache, marketplaceGlobalStatsCacheSeconds }) => {
5
6
  let globalRankRefreshTimer = null;
@@ -153,6 +154,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
153
154
  systemSummaryCache.expiresAt = __timeNowMs() + systemSummaryCacheSeconds * 1000;
154
155
  return payload;
155
156
  })
157
+ .catch((error) => {
158
+ if (hasValue && systemSummaryCache.value) {
159
+ return systemSummaryCache.value;
160
+ }
161
+ throw error;
162
+ })
156
163
  .finally(() => {
157
164
  systemSummaryCache.pending = null;
158
165
  });
@@ -329,6 +336,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
329
336
  readmeSummaryCache.expiresAt = __timeNowMs() + readmeSummaryCacheSeconds * 1000;
330
337
  return payload;
331
338
  })
339
+ .catch((error) => {
340
+ if (hasValue && readmeSummaryCache.value) {
341
+ return readmeSummaryCache.value;
342
+ }
343
+ throw error;
344
+ })
332
345
  .finally(() => {
333
346
  readmeSummaryCache.pending = null;
334
347
  });
@@ -346,12 +359,8 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
346
359
  const botPhoneFromCatalog = String(resolveCatalogBotPhone() || '').replace(/\D+/g, '');
347
360
  if (botPhoneFromCatalog) candidates.add(botPhoneFromCatalog);
348
361
 
349
- const envCandidates = [process.env.WHATSAPP_BOT_NUMBER, process.env.BOT_NUMBER, process.env.PHONE_NUMBER, process.env.BOT_PHONE_NUMBER];
350
-
351
- for (const candidate of envCandidates) {
352
- const digits = String(candidate || '').replace(/\D+/g, '');
353
- if (digits) candidates.add(digits);
354
- }
362
+ const configuredBotPhone = String(resolveBotPhoneFromEnv({ fallback: '' }) || '').replace(/\D+/g, '');
363
+ if (configuredBotPhone) candidates.add(configuredBotPhone);
355
364
 
356
365
  return Array.from(candidates).filter((value) => value.length >= 8);
357
366
  };
@@ -542,6 +551,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
542
551
  globalRankCache.expiresAt = __timeNowMs() + globalRankRefreshSeconds * 1000;
543
552
  return data;
544
553
  })
554
+ .catch((error) => {
555
+ if (hasValue && globalRankCache.value) {
556
+ return globalRankCache.value;
557
+ }
558
+ throw error;
559
+ })
545
560
  .finally(() => {
546
561
  globalRankCache.pending = null;
547
562
  });
@@ -737,6 +752,12 @@ export const createStickerCatalogSystemContext = ({ executeQuery, tables, logger
737
752
  marketplaceGlobalStatsCache.expiresAt = __timeNowMs() + marketplaceGlobalStatsCacheSeconds * 1000;
738
753
  return data;
739
754
  })
755
+ .catch((error) => {
756
+ if (hasValue && marketplaceGlobalStatsCache.value) {
757
+ return marketplaceGlobalStatsCache.value;
758
+ }
759
+ throw error;
760
+ })
740
761
  .finally(() => {
741
762
  marketplaceGlobalStatsCache.pending = null;
742
763
  });
@@ -1,6 +1,6 @@
1
1
  import logger from '#logger';
2
2
  import { executeQuery, TABLES } from '../../../database/index.js';
3
- import { getActiveSocket, getJidUser, normalizeJid, profilePictureUrlFromActiveSocket } from '../../../app/config/index.js';
3
+ import { getActiveSocket, getActiveSocketsBySession, getJidUser, getMultiSessionRuntimeConfig, isSocketOpen, normalizeJid, profilePictureUrlFromActiveSocket } from '../../../app/config/index.js';
4
4
  import { getSystemMetrics } from '../../../app/utils/systemMetrics/systemMetricsModule.js';
5
5
  import { createStickerCatalogSystemContext } from './stickerCatalogSystemContext.js';
6
6
  import { createStickerCatalogNonCatalogHandlers } from '../sticker/nonCatalogHandlers.js';
@@ -10,6 +10,9 @@ import { fetchPrometheusSummary } from './systemMetricsController.js';
10
10
  import { buildBotContactInfo, buildSupportInfo, resolveCatalogBotPhone } from './contactController.js';
11
11
  import { buildAdminMenu, buildAiMenu, buildAnimeMenu, buildMediaMenu, buildMenuCaption, buildQuoteMenu, buildStatsMenu, buildStickerMenu } from '../../../app/modules/menuModule/common.js';
12
12
  import { trackWebVisitMetric } from './visitController.js';
13
+ import groupOwnershipService from '../../../app/services/multiSession/groupOwnershipService.js';
14
+ import sessionRegistryService from '../../../app/services/multiSession/sessionRegistryService.js';
15
+ import { runGroupAssignmentBalancerCycle } from '../../../app/services/multiSession/assignmentBalancerService.js';
13
16
 
14
17
  const SYSTEM_SUMMARY_CACHE_SECONDS = Number(process.env.SYSTEM_SUMMARY_CACHE_SECONDS || 20);
15
18
  const README_SUMMARY_CACHE_SECONDS = Number(process.env.README_SUMMARY_CACHE_SECONDS || 1800);
@@ -132,4 +135,228 @@ export const systemHandlers = createStickerCatalogNonCatalogHandlers({
132
135
  isAuthenticatedGoogleSession: (sess) => Boolean(sess?.sub && (sess?.ownerJid || sess?.ownerPhone || sess?.email)),
133
136
  });
134
137
 
138
+ const clampLimit = (value, fallback = 200, min = 1, max = 5_000) => {
139
+ const parsed = Number.parseInt(String(value ?? ''), 10);
140
+ if (!Number.isFinite(parsed)) return fallback;
141
+ return Math.max(min, Math.min(max, parsed));
142
+ };
143
+
144
+ const normalizeOptional = (value, maxLength = 255) => {
145
+ const normalized = String(value || '')
146
+ .trim()
147
+ .slice(0, maxLength);
148
+ return normalized || null;
149
+ };
150
+
151
+ const normalizeBoolean = (value, fallback = false) => {
152
+ if (value === undefined || value === null || value === '') return fallback;
153
+ const normalized = String(value).trim().toLowerCase();
154
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
155
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
156
+ return fallback;
157
+ };
158
+
159
+ const toIso = (value) => {
160
+ if (!value) return null;
161
+ const parsed = new Date(value);
162
+ if (Number.isNaN(parsed.getTime())) return null;
163
+ return parsed.toISOString();
164
+ };
165
+
166
+ const resolveSocketRuntimeState = (socket) => {
167
+ const open = isSocketOpen(socket);
168
+ const readyState = resolveSocketReadyState(socket);
169
+ const botJid = resolveActiveSocketBotJid(socket);
170
+ return {
171
+ socket_open: open,
172
+ socket_ready_state: readyState,
173
+ socket_bot_jid: botJid || null,
174
+ };
175
+ };
176
+
177
+ export const listSystemAdminSessions = async ({ status = null, limit = 200 } = {}) => {
178
+ const runtimeConfig = getMultiSessionRuntimeConfig();
179
+ const safeStatus = normalizeOptional(status, 24);
180
+ const safeLimit = clampLimit(limit, 200, 1, 5_000);
181
+ const registryRows = await sessionRegistryService.listSessions({
182
+ status: safeStatus,
183
+ limit: safeLimit,
184
+ });
185
+
186
+ const socketsBySession = getActiveSocketsBySession();
187
+ const knownSessionIds = new Set();
188
+ for (const sessionId of runtimeConfig.sessionIds || []) knownSessionIds.add(sessionId);
189
+ for (const row of registryRows || []) {
190
+ if (row?.sessionId) knownSessionIds.add(row.sessionId);
191
+ }
192
+
193
+ const selectedSessionIds = Array.from(knownSessionIds).filter(Boolean).slice(0, safeLimit);
194
+ const registryBySession = new Map((registryRows || []).map((row) => [row.sessionId, row]));
195
+
196
+ const sessions = selectedSessionIds.map((sessionId) => {
197
+ const row = registryBySession.get(sessionId) || null;
198
+ const socket = socketsBySession.get(sessionId) || null;
199
+ const socketRuntime = resolveSocketRuntimeState(socket);
200
+
201
+ return {
202
+ session_id: sessionId,
203
+ is_primary: sessionId === runtimeConfig.primarySessionId,
204
+ configured: (runtimeConfig.sessionIds || []).includes(sessionId),
205
+ configured_weight: Number(runtimeConfig.sessionWeights?.[sessionId] || 1),
206
+ status: row?.status || (socketRuntime.socket_open ? 'online' : 'offline'),
207
+ bot_jid: row?.botJid || socketRuntime.socket_bot_jid || null,
208
+ capacity_weight: Number(row?.capacityWeight || runtimeConfig.sessionWeights?.[sessionId] || 1),
209
+ current_score: Number(row?.currentScore || 0),
210
+ last_heartbeat_at: toIso(row?.lastHeartbeatAt),
211
+ last_connected_at: toIso(row?.lastConnectedAt),
212
+ last_disconnected_at: toIso(row?.lastDisconnectedAt),
213
+ updated_at: toIso(row?.updatedAt),
214
+ metadata: row?.metadata || null,
215
+ ...socketRuntime,
216
+ };
217
+ });
218
+
219
+ return {
220
+ generated_at: new Date().toISOString(),
221
+ primary_session_id: runtimeConfig.primarySessionId,
222
+ configured_session_ids: runtimeConfig.sessionIds || [],
223
+ sessions,
224
+ };
225
+ };
226
+
227
+ export const listSystemAdminAssignments = async ({ groupJid = null, ownerSessionId = null, includeExpired = false, limit = 200 } = {}) => {
228
+ const safeGroupJid = normalizeOptional(groupJid, 255);
229
+ const safeOwnerSessionId = normalizeOptional(ownerSessionId, 64);
230
+ const safeIncludeExpired = normalizeBoolean(includeExpired, false);
231
+ const safeLimit = clampLimit(limit, 200, 1, 5_000);
232
+
233
+ const assignments = await groupOwnershipService.listAssignments({
234
+ groupJid: safeGroupJid,
235
+ ownerSessionId: safeOwnerSessionId,
236
+ includeExpired: safeIncludeExpired,
237
+ limit: safeLimit,
238
+ });
239
+
240
+ return {
241
+ generated_at: new Date().toISOString(),
242
+ filters: {
243
+ group_jid: safeGroupJid,
244
+ owner_session_id: safeOwnerSessionId,
245
+ include_expired: safeIncludeExpired,
246
+ limit: safeLimit,
247
+ },
248
+ assignments: (assignments || []).map((assignment) => ({
249
+ group_jid: assignment?.groupJid || null,
250
+ owner_session_id: assignment?.ownerSessionId || null,
251
+ lease_expires_at: toIso(assignment?.leaseExpiresAt),
252
+ cooldown_until: toIso(assignment?.cooldownUntil),
253
+ assignment_version: Number(assignment?.assignmentVersion || 1),
254
+ pinned: assignment?.pinned === true,
255
+ active: assignment?.active !== false,
256
+ last_reason: assignment?.lastReason || null,
257
+ created_at: toIso(assignment?.createdAt),
258
+ updated_at: toIso(assignment?.updatedAt),
259
+ })),
260
+ };
261
+ };
262
+
263
+ export const setSystemAdminGroupPin = async ({ groupJid, pinned, sessionId = null, reason = null, changedBy = 'admin_api', metadata = null } = {}) => {
264
+ const outcome = await groupOwnershipService.setPinned({
265
+ groupJid,
266
+ pinned,
267
+ sessionId,
268
+ reason: reason || (pinned ? 'admin_pin_group' : 'admin_unpin_group'),
269
+ changedBy,
270
+ metadata,
271
+ });
272
+
273
+ return {
274
+ updated: Boolean(outcome?.updated),
275
+ reason: outcome?.reason || null,
276
+ assignment_version: Number(outcome?.assignmentVersion || 0) || null,
277
+ previous_owner_session_id: outcome?.previousOwnerSessionId || null,
278
+ owner: outcome?.owner
279
+ ? {
280
+ group_jid: outcome.owner.groupJid,
281
+ owner_session_id: outcome.owner.ownerSessionId,
282
+ lease_expires_at: toIso(outcome.owner.leaseExpiresAt),
283
+ cooldown_until: toIso(outcome.owner.cooldownUntil),
284
+ assignment_version: Number(outcome.owner.assignmentVersion || 1),
285
+ pinned: outcome.owner.pinned === true,
286
+ last_reason: outcome.owner.lastReason || null,
287
+ }
288
+ : null,
289
+ };
290
+ };
291
+
292
+ export const forceSystemAdminGroupFailover = async ({ groupJid, targetSessionId, reason = 'admin_force_failover', changedBy = 'admin_api', metadata = null } = {}) => {
293
+ const outcome = await groupOwnershipService.forceAssign({
294
+ groupJid,
295
+ sessionId: targetSessionId,
296
+ reason,
297
+ changedBy,
298
+ metadata,
299
+ });
300
+
301
+ return {
302
+ reassigned: Boolean(outcome?.reassigned),
303
+ reason: outcome?.reason || null,
304
+ assignment_version: Number(outcome?.assignmentVersion || 0) || null,
305
+ previous_owner_session_id: outcome?.previousOwnerSessionId || null,
306
+ owner: outcome?.owner
307
+ ? {
308
+ group_jid: outcome.owner.groupJid,
309
+ owner_session_id: outcome.owner.ownerSessionId,
310
+ lease_expires_at: toIso(outcome.owner.leaseExpiresAt),
311
+ assignment_version: Number(outcome.owner.assignmentVersion || 1),
312
+ pinned: outcome.owner.pinned === true,
313
+ last_reason: outcome.owner.lastReason || null,
314
+ }
315
+ : null,
316
+ };
317
+ };
318
+
319
+ export const triggerSystemAdminManualRebalance = async () => {
320
+ const cycle = await runGroupAssignmentBalancerCycle();
321
+ return {
322
+ generated_at: new Date().toISOString(),
323
+ cycle,
324
+ };
325
+ };
326
+
327
+ export const listSystemAdminAssignmentHistory = async ({ groupJid = null, limit = 100 } = {}) => {
328
+ const safeLimit = clampLimit(limit, 100, 1, 5_000);
329
+ const safeGroupJid = normalizeOptional(groupJid, 255);
330
+ const params = [];
331
+ const where = [];
332
+ if (safeGroupJid) {
333
+ where.push('group_jid = ?');
334
+ params.push(safeGroupJid);
335
+ }
336
+
337
+ const rows = await executeQuery(
338
+ `SELECT id, group_jid, previous_session_id, new_session_id, change_reason, changed_by, assignment_version, metadata, created_at
339
+ FROM ${TABLES.GROUP_ASSIGNMENT_HISTORY}
340
+ ${where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''}
341
+ ORDER BY id DESC
342
+ LIMIT ${safeLimit}`,
343
+ params,
344
+ );
345
+
346
+ return {
347
+ generated_at: new Date().toISOString(),
348
+ history: (Array.isArray(rows) ? rows : []).map((row) => ({
349
+ id: Number(row?.id || 0),
350
+ group_jid: row?.group_jid || null,
351
+ previous_session_id: row?.previous_session_id || null,
352
+ new_session_id: row?.new_session_id || null,
353
+ change_reason: row?.change_reason || null,
354
+ changed_by: row?.changed_by || null,
355
+ assignment_version: Number(row?.assignment_version || 0) || null,
356
+ metadata: row?.metadata || null,
357
+ created_at: toIso(row?.created_at),
358
+ })),
359
+ };
360
+ };
361
+
135
362
  export { scheduleGlobalRankingPreload };
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  import logger from '#logger';
5
5
  import { DEFAULT_LEGACY_STICKER_API_BASE_PATH, DEFAULT_USER_API_BASE_PATH, isUserApiPath, normalizeBasePath, resolveLegacyUserApiPath } from '../routes/user/userApiPaths.js';
6
+ import { buildWhatsappUrl, resolvePublicWhatsappNumber } from '../utils/publicContact.js';
6
7
 
7
8
  const LEGACY_STICKER_API_BASE_PATH = normalizeBasePath(process.env.STICKER_API_BASE_PATH, DEFAULT_LEGACY_STICKER_API_BASE_PATH);
8
9
  const USER_API_BASE_PATH = normalizeBasePath(process.env.USER_API_BASE_PATH || process.env.AUTH_API_BASE_PATH, DEFAULT_USER_API_BASE_PATH);
@@ -11,6 +12,8 @@ const USER_PROFILE_WEB_PATH = normalizeBasePath(process.env.USER_PROFILE_WEB_PAT
11
12
  const USER_PASSWORD_RESET_WEB_PATH = normalizeBasePath(process.env.USER_PASSWORD_RESET_WEB_PATH, '/user/password-reset');
12
13
  const USER_DASHBOARD_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user.html');
13
14
  const USER_PASSWORD_RESET_TEMPLATE_PATH = path.join(process.cwd(), 'public', 'pages', 'user-password-reset.html');
15
+ const PUBLIC_WHATSAPP_NUMBER = resolvePublicWhatsappNumber();
16
+ const PUBLIC_WHATSAPP_URL = buildWhatsappUrl(PUBLIC_WHATSAPP_NUMBER);
14
17
 
15
18
  const hasPathPrefix = (pathname, prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`);
16
19
  const escapeHtmlAttribute = (value) =>
@@ -52,7 +55,10 @@ const renderUserDashboardHtml = async ({ passwordReset = false } = {}) => {
52
55
  const dataAttributes = {
53
56
  'data-api-base-path': USER_API_BASE_PATH,
54
57
  'data-login-path': STICKER_LOGIN_WEB_PATH,
58
+ 'data-panel-path': USER_PROFILE_WEB_PATH,
55
59
  'data-password-reset-web-path': USER_PASSWORD_RESET_WEB_PATH,
60
+ 'data-support-whatsapp-number': PUBLIC_WHATSAPP_NUMBER,
61
+ 'data-support-whatsapp-url': PUBLIC_WHATSAPP_URL,
56
62
  };
57
63
 
58
64
  let html = template;
@@ -38,6 +38,19 @@ let inFlight = false;
38
38
  let timerHandle = null;
39
39
  let nextDelayMs = EMAIL_AUTOMATION_POLL_INTERVAL_MS;
40
40
 
41
+ const maskEmailForLogs = (value) => {
42
+ const normalized = String(value || '')
43
+ .trim()
44
+ .toLowerCase()
45
+ .slice(0, 255);
46
+ const [localPartRaw, domainRaw] = normalized.split('@');
47
+ const localPart = String(localPartRaw || '').trim();
48
+ const domain = String(domainRaw || '').trim();
49
+ if (!localPart || !domain) return 'invalid-email';
50
+ const localMasked = localPart.length <= 2 ? `${localPart.charAt(0) || '*'}*` : `${localPart.slice(0, 2)}***`;
51
+ return `${localMasked}@${domain}`;
52
+ };
53
+
41
54
  const applyDelayJitter = (delayMs) => {
42
55
  const baseDelay = Math.max(250, Math.floor(Number(delayMs) || 0));
43
56
  if (EMAIL_AUTOMATION_IDLE_JITTER_PERCENT <= 0) return baseDelay;
@@ -119,6 +132,16 @@ export const runEmailAutomationTick = async ({ maxPerTick = EMAIL_AUTOMATION_MAX
119
132
  });
120
133
 
121
134
  stats.sent += 1;
135
+ logger.debug('E-mail da fila entregue com sucesso.', {
136
+ action: 'email_automation_delivery_succeeded',
137
+ task_id: task.id,
138
+ template_key: task.template_key || null,
139
+ recipient_email_masked: maskEmailForLogs(task.recipient_email),
140
+ attempts: task.attempts,
141
+ max_attempts: task.max_attempts,
142
+ provider_message_id: delivery?.messageId || null,
143
+ provider_response: delivery?.response || null,
144
+ });
122
145
  } catch (error) {
123
146
  stats.failed += 1;
124
147
  await failEmailOutboxTask(task.id, {
@@ -129,8 +152,12 @@ export const runEmailAutomationTick = async ({ maxPerTick = EMAIL_AUTOMATION_MAX
129
152
  logger.warn('Falha ao entregar e-mail da fila.', {
130
153
  action: 'email_automation_delivery_failed',
131
154
  task_id: task.id,
132
- recipient_email: task.recipient_email,
155
+ template_key: task.template_key || null,
156
+ recipient_email_masked: maskEmailForLogs(task.recipient_email),
133
157
  attempts: task.attempts,
158
+ max_attempts: task.max_attempts,
159
+ retry_delay_seconds: safeRetryDelay,
160
+ metadata_keys: Object.keys(task?.metadata || {}).slice(0, 20),
134
161
  error: error?.message,
135
162
  });
136
163
  }
@@ -138,6 +165,14 @@ export const runEmailAutomationTick = async ({ maxPerTick = EMAIL_AUTOMATION_MAX
138
165
 
139
166
  if (stats.claimed > 0) {
140
167
  await refreshQueueDepthMetrics().catch(() => null);
168
+ logger.info('Processamento da fila de e-mail concluído.', {
169
+ action: 'email_automation_tick_processed',
170
+ claimed: stats.claimed,
171
+ sent: stats.sent,
172
+ failed: stats.failed,
173
+ max_per_tick: safeMaxPerTick,
174
+ retry_delay_seconds: safeRetryDelay,
175
+ });
141
176
  }
142
177
 
143
178
  return stats;
@@ -1,3 +1,4 @@
1
+ import logger from '#logger';
1
2
  import { enqueueEmailOutbox, getEmailOutboxStatusSnapshot } from './emailOutboxRepository.js';
2
3
  import { renderEmailTemplate } from './emailTemplateService.js';
3
4
  import { getEmailTransportMetadata } from './emailTransportService.js';
@@ -8,6 +9,22 @@ const normalizeEmail = (value) =>
8
9
  .toLowerCase()
9
10
  .slice(0, 255);
10
11
 
12
+ const maskEmailForLogs = (value) => {
13
+ const normalized = normalizeEmail(value);
14
+ const [localPartRaw, domainRaw] = normalized.split('@');
15
+ const localPart = String(localPartRaw || '').trim();
16
+ const domain = String(domainRaw || '').trim();
17
+ if (!localPart || !domain) return 'invalid-email';
18
+ const localMasked = localPart.length <= 2 ? `${localPart.charAt(0) || '*'}*` : `${localPart.slice(0, 2)}***`;
19
+ return `${localMasked}@${domain}`;
20
+ };
21
+
22
+ const resolveEmailDomain = (value) => {
23
+ const normalized = normalizeEmail(value);
24
+ if (!normalized.includes('@')) return null;
25
+ return normalized.split('@')[1] || null;
26
+ };
27
+
11
28
  const normalizeOptionalText = (value, maxLength = 500_000) => {
12
29
  const normalized =
13
30
  String(value || '')
@@ -31,6 +48,12 @@ const resolveEmailBodyFromPayload = ({ templateKey = '', templateData = {}, subj
31
48
 
32
49
  const renderedTemplate = normalizedTemplateKey ? renderEmailTemplate(normalizedTemplateKey, normalizedTemplateData) : null;
33
50
 
51
+ if (normalizedTemplateKey && !renderedTemplate && !normalizeOptionalText(subject, 180) && !normalizeOptionalText(text, 120_000) && !normalizeOptionalText(html, 500_000)) {
52
+ const error = new Error(`Template de e-mail inválido ou indisponível: "${normalizedTemplateKey}".`);
53
+ error.statusCode = 400;
54
+ throw error;
55
+ }
56
+
34
57
  const normalizedSubject = normalizeOptionalText(subject, 180) || renderedTemplate?.subject || '';
35
58
  const normalizedText = normalizeOptionalText(text, 120_000) || renderedTemplate?.text || null;
36
59
  const normalizedHtml = normalizeOptionalText(html, 500_000) || renderedTemplate?.html || null;
@@ -72,6 +95,8 @@ export const queueAutomatedEmail = async ({ to, name = '', templateKey = '', tem
72
95
  html,
73
96
  });
74
97
 
98
+ const safeMetadata = normalizePayloadObject(metadata);
99
+
75
100
  const taskId = await enqueueEmailOutbox({
76
101
  recipientEmail: normalizedEmail,
77
102
  recipientName: normalizeOptionalText(name, 120),
@@ -80,7 +105,7 @@ export const queueAutomatedEmail = async ({ to, name = '', templateKey = '', tem
80
105
  htmlBody: body.html_body,
81
106
  templateKey: body.template_key,
82
107
  templatePayload: body.template_payload,
83
- metadata: normalizePayloadObject(metadata),
108
+ metadata: safeMetadata,
84
109
  priority,
85
110
  scheduledAt,
86
111
  maxAttempts,
@@ -93,6 +118,22 @@ export const queueAutomatedEmail = async ({ to, name = '', templateKey = '', tem
93
118
  throw error;
94
119
  }
95
120
 
121
+ logger.info('E-mail enfileirado para processamento.', {
122
+ action: 'email_outbox_enqueued',
123
+ task_id: taskId,
124
+ template_key: body.template_key || null,
125
+ recipient_email_masked: maskEmailForLogs(normalizedEmail),
126
+ recipient_domain: resolveEmailDomain(normalizedEmail),
127
+ subject_length: body.subject.length,
128
+ has_text_body: Boolean(body.text_body),
129
+ has_html_body: Boolean(body.html_body),
130
+ metadata_keys: Object.keys(safeMetadata).slice(0, 20),
131
+ priority: Number.isFinite(Number(priority)) ? Number(priority) : null,
132
+ scheduled_at: scheduledAt ? String(scheduledAt) : null,
133
+ max_attempts: Number.isFinite(Number(maxAttempts)) ? Number(maxAttempts) : null,
134
+ idempotency_key_present: Boolean(String(idempotencyKey || '').trim()),
135
+ });
136
+
96
137
  return {
97
138
  task_id: taskId,
98
139
  recipient_email: normalizedEmail,