@omnizap-system/omnizap 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.env.example +58 -13
  2. package/.github/workflows/ci.yml +5 -5
  3. package/.github/workflows/codeql.yml +1 -1
  4. package/.github/workflows/db-migration-check.yml +2 -2
  5. package/.github/workflows/dependency-review.yml +1 -1
  6. package/.github/workflows/deploy.yml +2 -2
  7. package/.github/workflows/release.yml +2 -2
  8. package/.github/workflows/security-attest-provenance.yml +2 -2
  9. package/.github/workflows/security-gitleaks.yml +13 -4
  10. package/.github/workflows/security-runner-hardening.yml +2 -2
  11. package/.github/workflows/security-scorecard.yml +1 -1
  12. package/.github/workflows/security-zap-baseline.yml +1 -1
  13. package/.github/workflows/security-zap-full-scan.yml +2 -1
  14. package/.github/workflows/security-zizmor.yml +1 -1
  15. package/.github/workflows/wiki-sync.yml +1 -1
  16. package/.gitleaksignore +9 -0
  17. package/CODE_OF_CONDUCT.md +2 -2
  18. package/GEMINI.md +64 -0
  19. package/README.md +52 -82
  20. package/SECURITY.md +1 -1
  21. package/app/config/index.js +2 -0
  22. package/app/configParts/adminIdentity.js +5 -5
  23. package/app/configParts/baileysConfig.js +230 -58
  24. package/app/configParts/groupUtils.js +5 -0
  25. package/app/configParts/messagePersistenceService.js +145 -4
  26. package/app/configParts/sessionConfig.js +157 -0
  27. package/app/connection/baileysCompatibility.test.js +1 -1
  28. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  29. package/app/connection/socketController.js +660 -158
  30. package/app/connection/socketController.multiSession.test.js +108 -0
  31. package/app/controllers/messageController.js +1 -1
  32. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  33. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  34. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  35. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  36. package/app/controllers/messageProcessingPipeline.js +93 -13
  37. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  38. package/app/modules/adminModule/AGENT.md +1 -1
  39. package/app/modules/adminModule/commandConfig.json +3318 -1347
  40. package/app/modules/adminModule/groupCommandHandlers.js +858 -15
  41. package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
  42. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  43. package/app/modules/aiModule/AGENT.md +47 -30
  44. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  45. package/app/modules/aiModule/catCommand.js +135 -27
  46. package/app/modules/aiModule/commandConfig.json +114 -28
  47. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  48. package/app/modules/gameModule/AGENT.md +1 -1
  49. package/app/modules/gameModule/commandConfig.json +29 -0
  50. package/app/modules/menuModule/AGENT.md +1 -1
  51. package/app/modules/menuModule/commandConfig.json +45 -10
  52. package/app/modules/menuModule/menuCatalogService.js +190 -0
  53. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  54. package/app/modules/menuModule/menuDynamicService.js +511 -0
  55. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  56. package/app/modules/menuModule/menus.js +36 -5
  57. package/app/modules/playModule/AGENT.md +10 -5
  58. package/app/modules/playModule/commandConfig.json +140 -12
  59. package/app/modules/playModule/playCommand.js +1 -1417
  60. package/app/modules/playModule/playCommandConstants.js +80 -0
  61. package/app/modules/playModule/playCommandCore.js +361 -0
  62. package/app/modules/playModule/playCommandHandlers.js +41 -0
  63. package/app/modules/playModule/playCommandMediaClient.js +1872 -0
  64. package/app/modules/playModule/playConfigRuntime.js +245 -4
  65. package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
  66. package/app/modules/quoteModule/AGENT.md +1 -1
  67. package/app/modules/quoteModule/commandConfig.json +29 -0
  68. package/app/modules/quoteModule/quoteCommand.js +3 -2
  69. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  70. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  71. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
  72. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
  73. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
  74. package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
  75. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
  76. package/app/modules/statsModule/AGENT.md +1 -1
  77. package/app/modules/statsModule/commandConfig.json +58 -0
  78. package/app/modules/statsModule/rankingCommon.js +5 -4
  79. package/app/modules/stickerModule/AGENT.md +1 -1
  80. package/app/modules/stickerModule/addStickerMetadata.js +4 -3
  81. package/app/modules/stickerModule/commandConfig.json +145 -0
  82. package/app/modules/stickerModule/stickerCommand.js +1 -1
  83. package/app/modules/stickerPackModule/AGENT.md +1 -1
  84. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  85. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  86. package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
  87. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
  88. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
  89. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
  90. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
  91. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
  92. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
  93. package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
  94. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
  95. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  96. package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
  97. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
  98. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  99. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  100. package/app/modules/systemMetricsModule/pingCommand.js +6 -5
  101. package/app/modules/tiktokModule/AGENT.md +1 -1
  102. package/app/modules/tiktokModule/commandConfig.json +29 -0
  103. package/app/modules/tiktokModule/tiktokCommand.js +2 -1
  104. package/app/modules/userModule/AGENT.md +1 -1
  105. package/app/modules/userModule/commandConfig.json +29 -0
  106. package/app/modules/userModule/userCommand.js +72 -23
  107. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  108. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  109. package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
  110. package/app/observability/metrics.js +136 -0
  111. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  112. package/app/services/ai/conversationRouterService.js +4 -3
  113. package/app/services/ai/geminiService.js +132 -7
  114. package/app/services/ai/geminiService.test.js +59 -2
  115. package/app/services/ai/globalModuleAiHelpService.js +3 -2
  116. package/app/services/ai/messageCommandExecutionService.js +2 -1
  117. package/app/services/ai/moduleAiHelpCoreService.js +45 -14
  118. package/app/services/ai/moduleToolExecutorService.js +3 -2
  119. package/app/services/ai/moduleToolRegistryService.js +2 -1
  120. package/app/services/ai/toolCandidateSelectorService.js +6 -5
  121. package/app/services/auth/googleWebLinkService.js +3 -2
  122. package/app/services/auth/whatsappLoginLinkService.js +3 -2
  123. package/app/services/external/pokeApiService.js +4 -3
  124. package/app/services/group/groupMetadataService.js +24 -1
  125. package/app/services/infra/dbWriteQueue.js +57 -26
  126. package/app/services/infra/featureFlagService.js +2 -1
  127. package/app/services/messaging/captchaService.js +3 -2
  128. package/app/services/messaging/newsBroadcastService.js +846 -29
  129. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  130. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  131. package/app/services/multiSession/groupOwnershipService.js +890 -0
  132. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  133. package/app/services/multiSession/sessionRegistryService.js +293 -0
  134. package/app/services/sticker/stickerFocusService.js +11 -10
  135. package/app/store/aiPromptStore.js +36 -19
  136. package/app/store/conversationSessionStore.js +7 -6
  137. package/app/store/groupConfigStore.js +41 -5
  138. package/app/store/premiumUserStore.js +21 -7
  139. package/app/utils/antiLink/antiLinkModule.js +352 -16
  140. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  141. package/app/workers/aiLearningWorker.js +6 -5
  142. package/app/workers/commandConfigEnrichmentWorker.js +4 -3
  143. package/database/index.js +14 -8
  144. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  145. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  146. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  147. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  148. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  149. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  150. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  151. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  152. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  153. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  154. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  155. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  156. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  157. package/database/schema.sql +102 -1
  158. package/docker-compose.yml +4 -1
  159. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  160. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
  161. package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
  162. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  163. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
  164. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  165. package/docs/security/omnizap-static-security-headers.conf +25 -0
  166. package/docs/wiki/Home.md +1 -1
  167. package/ecosystem.prod.config.cjs +32 -12
  168. package/index.js +57 -23
  169. package/observability/alert-rules.yml +20 -0
  170. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  171. package/observability/mysql-setup.sql +4 -4
  172. package/observability/system-admin-observability.md +26 -0
  173. package/package.json +20 -6
  174. package/public/apple-touch-icon.png +0 -0
  175. package/public/comandos/commands-catalog.json +2853 -3326
  176. package/public/favicon-16x16.png +0 -0
  177. package/public/favicon-32x32.png +0 -0
  178. package/public/favicon.ico +0 -0
  179. package/public/js/apps/apiDocsApp.js +3 -2
  180. package/public/js/apps/commandsReactApp.js +280 -99
  181. package/public/js/apps/createPackApp.js +11 -10
  182. package/public/js/apps/homeReactApp.js +181 -130
  183. package/public/js/apps/loginReactApp.js +1 -1
  184. package/public/js/apps/stickersApp.js +263 -110
  185. package/public/js/apps/termsReactApp.js +73 -24
  186. package/public/js/apps/userApp.js +4 -3
  187. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  188. package/public/js/apps/userReactApp.js +355 -280
  189. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  190. package/public/pages/api-docs.html +1 -1
  191. package/public/pages/aup.html +2 -2
  192. package/public/pages/dpa.html +3 -3
  193. package/public/pages/licenca.html +4 -4
  194. package/public/pages/login.html +1 -1
  195. package/public/pages/notice-and-takedown.html +2 -2
  196. package/public/pages/politica-de-privacidade.html +6 -6
  197. package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
  198. package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
  199. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
  200. package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
  201. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
  202. package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
  203. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
  204. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
  205. package/public/pages/stickers-admin.html +1 -1
  206. package/public/pages/stickers-create.html +1 -1
  207. package/public/pages/stickers.html +6 -6
  208. package/public/pages/suboperadores.html +2 -2
  209. package/public/pages/termos-de-uso-texto-integral.html +6 -6
  210. package/public/pages/termos-de-uso.html +3 -3
  211. package/public/pages/user-password-reset.html +4 -5
  212. package/public/pages/user-systemadm.html +9 -463
  213. package/public/pages/user.html +2 -2
  214. package/scripts/clear-whatsapp-session.sh +123 -0
  215. package/scripts/core-ai-mode.mjs +163 -0
  216. package/scripts/deploy.sh +11 -1
  217. package/scripts/email-broadcast-terms-update.mjs +2 -1
  218. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  219. package/scripts/generate-commands-catalog.mjs +166 -2
  220. package/scripts/generate-module-agents.mjs +2 -1
  221. package/scripts/generate-seo-satellite-pages.mjs +5 -4
  222. package/scripts/github-deploy-notify.mjs +2 -1
  223. package/scripts/github-release-notify.mjs +25 -10
  224. package/scripts/new-whatsapp-session.sh +317 -0
  225. package/scripts/release.sh +2 -19
  226. package/scripts/security-smoketest.mjs +6 -5
  227. package/scripts/security-web-surface-check.mjs +218 -0
  228. package/scripts/sticker-catalog-loadtest.mjs +5 -4
  229. package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
  230. package/server/auth/jwt/webJwtService.js +1 -1
  231. package/server/auth/stickerCatalogAuthContext.js +2 -1
  232. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
  233. package/server/auth/userPassword/userPasswordAuthService.js +2 -1
  234. package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
  235. package/server/auth/webAccount/webAccountHandlers.js +9 -10
  236. package/server/controllers/admin/adminPanelHandlers.js +267 -16
  237. package/server/controllers/admin/systemAdminController.js +267 -0
  238. package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
  239. package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
  240. package/server/controllers/sticker/stickerCatalogController.js +23 -36
  241. package/server/controllers/system/contactController.js +9 -17
  242. package/server/controllers/system/githubController.js +3 -2
  243. package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
  244. package/server/controllers/system/systemController.js +254 -1
  245. package/server/controllers/system/systemMetricsController.js +2 -1
  246. package/server/controllers/userController.js +6 -0
  247. package/server/email/emailTemplateService.js +5 -3
  248. package/server/http/httpServer.js +11 -6
  249. package/server/middleware/rateLimit.js +2 -1
  250. package/server/middleware/securityHeaders.js +20 -1
  251. package/server/routes/admin/systemAdminRouter.js +6 -0
  252. package/server/routes/indexRouter.js +30 -6
  253. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  254. package/server/routes/static/staticPageRouter.js +27 -1
  255. package/server/utils/publicContact.js +31 -0
  256. package/utils/time/timeModule.js +135 -0
  257. package/utils/time/timeModule.test.js +65 -0
  258. package/utils/whatsapp/contactEnv.js +39 -0
  259. package/vite.config.mjs +7 -1
  260. package/public/assets/images/brand-icon-192.png +0 -0
  261. package/scripts/sync-readme-snapshot.mjs +0 -133
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { executeQuery, TABLES } from '../../../database/index.js';
2
3
  import { getJidUser, getProfilePicBuffer, normalizeJid } from '../../config/index.js';
3
4
  import { isUserAdmin } from '../../config/index.js';
@@ -37,6 +38,44 @@ const SOCIAL_DST_EXPR = `JSON_UNQUOTE(
37
38
  */
38
39
  const buildUsageText = (commandPrefix = DEFAULT_COMMAND_PREFIX) => getUserUsageText('user', { commandPrefix }) || ['Formato de uso:', `${commandPrefix}user perfil <id|telefone>`, '', 'Dica:', '• Você pode mencionar alguém.', '• Ou responder a mensagem do usuário desejado.'].join('\n');
39
40
 
41
+ /**
42
+ * Extrai metadados do remetente considerando payload novo do pipeline/socket.
43
+ * @param {object} params
44
+ * @param {object} [params.messageInfo]
45
+ * @param {string|null} [params.senderJid]
46
+ * @param {string|object|null} [params.senderIdentity]
47
+ * @returns {string|object|null}
48
+ */
49
+ const resolveSenderSource = ({ messageInfo, senderJid, senderIdentity }) => {
50
+ const key = messageInfo?.key || {};
51
+
52
+ if (senderIdentity && typeof senderIdentity === 'object') {
53
+ return {
54
+ participant: senderIdentity.participant || key.participant || null,
55
+ participantAlt: senderIdentity.participantAlt || key.participantAlt || key.remoteJidAlt || null,
56
+ jid: senderIdentity.jid || senderJid || null,
57
+ remoteJid: key.remoteJid || null,
58
+ remoteJidAlt: key.remoteJidAlt || null,
59
+ };
60
+ }
61
+
62
+ return {
63
+ participant: key.participant || null,
64
+ participantAlt: key.participantAlt || key.remoteJidAlt || null,
65
+ jid: senderJid || (typeof senderIdentity === 'string' ? senderIdentity : null),
66
+ remoteJid: key.remoteJid || null,
67
+ remoteJidAlt: key.remoteJidAlt || null,
68
+ };
69
+ };
70
+
71
+ const hasResolvableIdentity = (value) => {
72
+ if (!value) return false;
73
+ if (typeof value === 'string') return Boolean(value.trim());
74
+ if (typeof value !== 'object') return false;
75
+
76
+ return Boolean(value?.jid || value?.lid || value?.participant || value?.participantAlt || value?.remoteJid || value?.remoteJidAlt || value?.id);
77
+ };
78
+
40
79
  /**
41
80
  * Extrai o `contextInfo` da mensagem, incluindo estruturas aninhadas.
42
81
  * @param {object} messageInfo Estrutura da mensagem recebida pelo bot.
@@ -90,24 +129,34 @@ const parseTargetArgument = (rawValue) => {
90
129
  * Define qual usuário será usado como alvo (menção, argumento, reply ou remetente).
91
130
  * @param {object} messageInfo Mensagem usada para inferir contexto.
92
131
  * @param {string|null} senderJid JID do remetente do comando.
132
+ * @param {string|object|null} senderIdentity Identidade recebida do pipeline (participant/participantAlt/jid).
93
133
  * @param {string} targetArg Argumento explícito passado no comando.
94
134
  * @returns {{ source: string | object | null, invalidExplicitTarget: boolean }} Fonte escolhida e sinalizador de argumento inválido.
95
135
  */
96
- const resolveCandidateTarget = (messageInfo, senderJid, targetArg) => {
136
+ const resolveCandidateTarget = (messageInfo, senderJid, senderIdentity, targetArg) => {
97
137
  const contextInfo = getContextInfo(messageInfo);
98
138
  const mentioned = Array.isArray(contextInfo?.mentionedJid) ? contextInfo.mentionedJid.find(Boolean) || null : null;
99
139
  const parsedTarget = parseTargetArgument(targetArg);
140
+ const quotedMessageKey = contextInfo?.quotedMessageKey && typeof contextInfo.quotedMessageKey === 'object' ? contextInfo.quotedMessageKey : null;
100
141
  const repliedSource =
101
- contextInfo?.participant || contextInfo?.participantAlt
142
+ contextInfo?.participant || contextInfo?.participantAlt || quotedMessageKey?.participant || quotedMessageKey?.participantAlt
102
143
  ? {
103
- participant: contextInfo.participant || null,
104
- participantAlt: contextInfo.participantAlt || null,
144
+ participant: contextInfo?.participant || quotedMessageKey?.participant || null,
145
+ participantAlt: contextInfo?.participantAlt || quotedMessageKey?.participantAlt || null,
146
+ remoteJid: quotedMessageKey?.remoteJid || null,
147
+ remoteJidAlt: quotedMessageKey?.remoteJidAlt || null,
105
148
  }
106
149
  : null;
150
+ const senderSource = resolveSenderSource({
151
+ messageInfo,
152
+ senderJid,
153
+ senderIdentity,
154
+ });
107
155
  const hasContextTarget = Boolean(mentioned || repliedSource);
156
+ const normalizedSenderSource = hasResolvableIdentity(senderSource) ? senderSource : senderJid || null;
108
157
 
109
158
  return {
110
- source: mentioned || parsedTarget.jid || repliedSource || senderJid || null,
159
+ source: mentioned || parsedTarget.jid || repliedSource || normalizedSenderSource || null,
111
160
  invalidExplicitTarget: parsedTarget.invalid && !hasContextTarget,
112
161
  };
113
162
  };
@@ -242,10 +291,10 @@ const formatPercent = (value, total) => {
242
291
  /**
243
292
  * Calcula a diferença inteira em dias entre dois timestamps.
244
293
  * @param {number} fromMs Timestamp inicial em milissegundos.
245
- * @param {number} [toMs=Date.now()] Timestamp final em milissegundos.
294
+ * @param {number} [toMs=__timeNowMs()] Timestamp final em milissegundos.
246
295
  * @returns {number} Quantidade de dias inteiros.
247
296
  */
248
- const toIntegerDays = (fromMs, toMs = Date.now()) => {
297
+ const toIntegerDays = (fromMs, toMs = __timeNowMs()) => {
249
298
  if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs < fromMs) return 0;
250
299
  return Math.floor((toMs - fromMs) / DAY_MS);
251
300
  };
@@ -873,7 +922,7 @@ const hasRecentInteraction = (lastMessage) => {
873
922
  const parsed = lastMessage instanceof Date ? lastMessage.getTime() : new Date(lastMessage).getTime();
874
923
  if (!Number.isFinite(parsed)) return false;
875
924
  const maxAgeMs = ACTIVE_DAYS_WINDOW * 24 * 60 * 60 * 1000;
876
- return Date.now() - parsed <= maxAgeMs;
925
+ return __timeNowMs() - parsed <= maxAgeMs;
877
926
  };
878
927
 
879
928
  /**
@@ -901,7 +950,7 @@ const isTargetBlocked = async (targetIds) => {
901
950
  const formatTempoDeCasa = (firstMessage) => {
902
951
  const firstMs = toMillis(firstMessage);
903
952
  if (!Number.isFinite(firstMs)) return 'N/D';
904
- const days = toIntegerDays(firstMs, Date.now());
953
+ const days = toIntegerDays(firstMs, __timeNowMs());
905
954
  return `${days} dia(s)`;
906
955
  };
907
956
 
@@ -913,7 +962,7 @@ const formatTempoDeCasa = (firstMessage) => {
913
962
  const formatDaysSinceLastMessage = (lastMessage) => {
914
963
  const lastMs = toMillis(lastMessage);
915
964
  if (!Number.isFinite(lastMs)) return 'N/D';
916
- return `${toIntegerDays(lastMs, Date.now())} dia(s)`;
965
+ return `${toIntegerDays(lastMs, __timeNowMs())} dia(s)`;
917
966
  };
918
967
 
919
968
  /**
@@ -1016,20 +1065,19 @@ const resolveMentionJid = (ids = []) => ids.find((id) => isWhatsAppUserId(id)) |
1016
1065
  * @param {object} params.messageInfo Mensagem original usada como contexto.
1017
1066
  * @param {number|undefined} params.expirationMessage Configuração de expiração de mensagem.
1018
1067
  * @param {string} params.senderJid JID de quem executou o comando.
1068
+ * @param {string|object|null} [params.senderIdentity=null] Identidade enriquecida do remetente (participant/participantAlt/jid).
1019
1069
  * @param {string[]} [params.args=[]] Argumentos recebidos após o comando.
1020
1070
  * @param {boolean} params.isGroupMessage Indica se o contexto é grupo.
1021
1071
  * @param {string} [params.commandPrefix=DEFAULT_COMMAND_PREFIX] Prefixo de comandos.
1022
1072
  * @returns {Promise<void>} Finaliza após responder ao usuário.
1023
1073
  */
1024
- export async function handleUserCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, args = [], isGroupMessage, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
1025
- const subcommand = args?.[0]?.toLowerCase() || '';
1026
- if (subcommand !== 'perfil' && subcommand !== 'profile') {
1027
- await sendAndStore(sock, remoteJid, { text: buildUsageText(commandPrefix) }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1028
- return;
1029
- }
1030
-
1031
- const explicitTargetArg = args.slice(1).join(' ').trim();
1032
- const { source, invalidExplicitTarget } = resolveCandidateTarget(messageInfo, senderJid, explicitTargetArg);
1074
+ export async function handleUserCommand({ sock, remoteJid, messageInfo, expirationMessage, senderJid, senderIdentity = null, args = [], isGroupMessage, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
1075
+ const firstArg = String(args?.[0] || '')
1076
+ .trim()
1077
+ .toLowerCase();
1078
+ const hasExplicitSubcommand = firstArg === 'perfil' || firstArg === 'profile';
1079
+ const explicitTargetArg = hasExplicitSubcommand ? args.slice(1).join(' ').trim() : args.join(' ').trim();
1080
+ const { source, invalidExplicitTarget } = resolveCandidateTarget(messageInfo, senderJid, senderIdentity, explicitTargetArg);
1033
1081
  if (invalidExplicitTarget) {
1034
1082
  await sendAndStore(sock, remoteJid, { text: `❌ ID ou telefone inválido.\n\n${buildUsageText(commandPrefix)}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1035
1083
  return;
@@ -1044,11 +1092,12 @@ export async function handleUserCommand({ sock, remoteJid, messageInfo, expirati
1044
1092
  const senderIds = await resolveSenderIdsForTarget(canonicalTarget);
1045
1093
  const normalizedTargetIds = Array.from(new Set([canonicalTarget, ...senderIds].map((value) => normalizeJid(value) || value).filter(Boolean)));
1046
1094
  const mentionJid = resolveMentionJid(normalizedTargetIds);
1047
- const senderCanonical = resolveUserIdCached({
1048
- jid: senderJid,
1049
- lid: senderJid,
1050
- participantAlt: null,
1095
+ const senderSource = resolveSenderSource({
1096
+ messageInfo,
1097
+ senderJid,
1098
+ senderIdentity,
1051
1099
  });
1100
+ const senderCanonical = await resolveCanonicalTarget(senderSource);
1052
1101
  const rankingTargetId = mentionJid || canonicalTarget;
1053
1102
 
1054
1103
  const [stats, ranking, latestPushName, premiumUsers, blocked, groupAdmin] = await Promise.all([fetchUserStats({ canonicalId: rankingTargetId, senderIds: normalizedTargetIds }), fetchUserRanking(rankingTargetId), fetchLatestPushName(normalizedTargetIds), premiumUserStore.getPremiumUsers(), isTargetBlocked(normalizedTargetIds), isGroupMessage ? isUserAdmin(remoteJid, mentionJid || canonicalTarget) : Promise.resolve(false)]);
@@ -7,7 +7,7 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
7
7
  - arquivo_base: `app/modules/waifuPicsModule/commandConfig.json`
8
8
  - schema_version: `2.0.0`
9
9
  - module_enabled: `true`
10
- - generated_at: `2026-03-11T02:35:17.177Z`
10
+ - generated_at: `2026-03-17T04:04:14.195Z`
11
11
 
12
12
  ## Escopo do Modulo
13
13
 
@@ -90,9 +90,9 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
90
90
  - <prefix>wp neko
91
91
  - mensagens_uso (variantes):
92
92
  - default:
93
- - 💡 _Uso:_ <prefix>waifu <categoria>
94
- Exemplo: <prefix>waifu neko
95
- - Dica: Use <prefix>waifuajuda para ver todas as categorias! 🎨
93
+ - 🖼️ _Como usar:_ <prefix>waifu <categoria>
94
+ - Exemplos rápidos: <prefix>waifu neko | <prefix>wp hug
95
+ - Quer ver todas as categorias disponíveis? Use <prefix>waifuajuda.
96
96
  - subcomandos:
97
97
  - (nenhum)
98
98
  - argumentos:
@@ -125,12 +125,22 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
125
125
  - consulta API externa
126
126
  - envia imagem no chat
127
127
  - respostas_padrao:
128
- - success: Prontinho! Aqui está sua imagem.
129
- - usage_error: Ops! Parece que você esqueceu a categoria. Use <prefix>waifuajuda para ver as opções disponíveis. 🧐
130
- - permission_error: Eita! Você não tem permissão para usar este comando. Entre em contato com um administrador. 🔐
131
- - sucesso: Comando executado com sucesso.
132
- - erro_uso: Formato de uso inválido. Consulte metodos_de_uso.
133
- - erro_permissao: Permissão insuficiente para executar este comando.
128
+ - success: ✅ Pedido recebido! Aqui vai sua imagem.
129
+ Dica: você pode pedir outra categoria no próximo comando (ex.: <prefix>waifu neko).
130
+ - usage_error: Não entendi o formato do comando.
131
+ Exemplos:
132
+ <prefix>waifu neko
133
+ <prefix>waifunsfw waifu
134
+ Para ver todas as categorias: <prefix>waifuajuda
135
+ - permission_error: 🔒 Este comando não está liberado para você neste contexto.
136
+ Se for NSFW em grupo, peça para um admin usar <prefix>nsfw on.
137
+ - sucesso: ✅ Imagem enviada com sucesso!
138
+ Exemplo de próximo pedido: <prefix>waifu hug
139
+ - erro_uso: ❗ Categoria inválida ou ausente.
140
+ Exemplos: <prefix>waifu neko | <prefix>wp waifu
141
+ Veja a lista completa em <prefix>waifuajuda.
142
+ - erro_permissao: 🔒 Não consegui liberar este pedido para seu perfil.
143
+ Se achar que é engano, verifique seu plano e tente novamente.
134
144
  - mensagens_sistema:
135
145
  - (nao informado)
136
146
  - limites_operacionais:
@@ -211,9 +221,9 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
211
221
  - <prefix>wpnsfw waifu
212
222
  - mensagens_uso (variantes):
213
223
  - default:
214
- - 🔞 _Uso:_ <prefix>waifunsfw <categoria>
215
- Exemplo: <prefix>wpnsfw blowjob
216
- - Atenção: O grupo deve estar com NSFW ligado!
224
+ - 🔞 _Como usar:_ <prefix>waifunsfw <categoria>
225
+ - Exemplos rápidos: <prefix>waifunsfw waifu | <prefix>wpnsfw neko
226
+ - Pré-requisitos: plano Premium; em grupo, NSFW ativo com <prefix>nsfw on.
217
227
  - subcomandos:
218
228
  - (nenhum)
219
229
  - argumentos:
@@ -247,12 +257,22 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
247
257
  - consulta API externa
248
258
  - envia imagem NSFW no chat
249
259
  - respostas_padrao:
250
- - success: Prontinho! Aqui está sua imagem.
251
- - usage_error: Ops! Parece que você esqueceu a categoria. Use <prefix>waifuajuda para ver as opções disponíveis. 🧐
252
- - permission_error: Eita! Você não tem permissão para usar este comando. Entre em contato com um administrador. 🔐
253
- - sucesso: Comando executado com sucesso.
254
- - erro_uso: Formato de uso inválido. Consulte metodos_de_uso.
255
- - erro_permissao: Permissão insuficiente para executar este comando.
260
+ - success: ✅ Pedido recebido! Aqui vai sua imagem.
261
+ Dica: você pode pedir outra categoria no próximo comando (ex.: <prefix>waifu neko).
262
+ - usage_error: Não entendi o formato do comando.
263
+ Exemplos:
264
+ <prefix>waifu neko
265
+ <prefix>waifunsfw waifu
266
+ Para ver todas as categorias: <prefix>waifuajuda
267
+ - permission_error: 🔒 Este comando não está liberado para você neste contexto.
268
+ Se for NSFW em grupo, peça para um admin usar <prefix>nsfw on.
269
+ - sucesso: 🔞 Imagem NSFW enviada.
270
+ Exemplo de próximo pedido: <prefix>waifunsfw neko
271
+ - erro_uso: ❗ Categoria NSFW inválida ou ausente.
272
+ Exemplos: <prefix>waifunsfw waifu | <prefix>wpnsfw neko
273
+ Veja opções em <prefix>waifuajuda.
274
+ - erro_permissao: 🔒 Este comando NSFW exige acesso Premium.
275
+ Em grupos, também é preciso NSFW ativo com <prefix>nsfw on.
256
276
  - mensagens_sistema:
257
277
  - (nao informado)
258
278
  - limites_operacionais:
@@ -332,8 +352,9 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
332
352
  - <prefix>waifuajuda
333
353
  - mensagens_uso (variantes):
334
354
  - default:
335
- - 📖 _Guia Rápido:_ Digite <prefix>waifuajuda para ver as categorias SFW e NSFW disponíveis.
336
- - Use <prefix>waifu <categoria> para receber uma imagem!
355
+ - 📖 _Como usar:_ <prefix>waifuajuda
356
+ - Depois do guia, teste: <prefix>waifu neko
357
+ - Para NSFW: <prefix>waifunsfw waifu (Premium; em grupo exige <prefix>nsfw on).
337
358
  - subcomandos:
338
359
  - (nenhum)
339
360
  - argumentos:
@@ -364,12 +385,21 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
364
385
  - efeitos_colaterais:
365
386
  - envia mensagem de ajuda
366
387
  - respostas_padrao:
367
- - success: Prontinho! Aqui está sua imagem.
368
- - usage_error: Ops! Parece que você esqueceu a categoria. Use <prefix>waifuajuda para ver as opções disponíveis. 🧐
369
- - permission_error: Eita! Você não tem permissão para usar este comando. Entre em contato com um administrador. 🔐
370
- - sucesso: Comando executado com sucesso.
371
- - erro_uso: Formato de uso inválido. Consulte metodos_de_uso.
372
- - erro_permissao: Permissão insuficiente para executar este comando.
388
+ - success: ✅ Pedido recebido! Aqui vai sua imagem.
389
+ Dica: você pode pedir outra categoria no próximo comando (ex.: <prefix>waifu neko).
390
+ - usage_error: Não entendi o formato do comando.
391
+ Exemplos:
392
+ <prefix>waifu neko
393
+ <prefix>waifunsfw waifu
394
+ Para ver todas as categorias: <prefix>waifuajuda
395
+ - permission_error: 🔒 Este comando não está liberado para você neste contexto.
396
+ Se for NSFW em grupo, peça para um admin usar <prefix>nsfw on.
397
+ - sucesso: 📚 Guia enviado com sucesso.
398
+ Agora experimente: <prefix>waifu neko
399
+ - erro_uso: ℹ️ Este comando não precisa de argumentos.
400
+ Use apenas: <prefix>waifuajuda
401
+ - erro_permissao: 🔒 Não consegui mostrar o guia neste contexto.
402
+ Tente novamente no privado ou em um grupo permitido.
373
403
  - mensagens_sistema:
374
404
  - (nao informado)
375
405
  - limites_operacionais:
@@ -357,6 +357,35 @@
357
357
  "schema": "legacy_v1_and_v2",
358
358
  "legacy_name": "waifu",
359
359
  "legacy_fields_present": ["descricao", "metodos_de_uso", "permissao_necessaria", "local_de_uso", "informacoes_coletadas", "argumentos", "pre_condicoes", "dependencias_externas", "efeitos_colaterais", "observabilidade", "privacidade", "acesso", "limite_uso_por_plano"]
360
+ },
361
+ "user_experience": {
362
+ "resumo_usuario": "Guia rápido para usar o comando waifu e receber uma imagem de anime (SFW) da categoria escolhida.",
363
+ "quando_usar": ["Quando você quer uma imagem de anime específica (ex.: neko, waifu) sem enviar outras perguntas."],
364
+ "exemplos_reais": [
365
+ {
366
+ "situacao": "Quero uma imagem de neko.",
367
+ "comando": "<prefix>waifu neko",
368
+ "resposta_esperada": "✅ Imagem enviada com sucesso! Exemplo de próximo pedido: <prefix>waifu hug.",
369
+ "variacao": "Usando alias: <prefix>wp neko."
370
+ },
371
+ {
372
+ "situacao": "Não informei a categoria.",
373
+ "comando": "<prefix>waifu",
374
+ "resposta_esperada": "❗ Categoria inválida ou ausente. Exemplos: <prefix>waifu neko | <prefix>wp waifu Veja a lista completa em <prefix>waifuajuda.",
375
+ "variacao": "Tente: <prefix>waifu neko."
376
+ },
377
+ {
378
+ "situacao": "Categoria não suportada.",
379
+ "comando": "<prefix>waifu dragon",
380
+ "resposta_esperada": "❗ Categoria inválida ou ausente. Exemplos: <prefix>waifu neko | <prefix>wp waifu Veja a lista completa em <prefix>waifuajuda.",
381
+ "variacao": "Tente uma categoria comum como neko ou waifu."
382
+ }
383
+ ],
384
+ "resposta_esperada": ["O bot envia uma imagem de anime da categoria solicitada (SFW)."],
385
+ "erros_comuns_usuario": ["Esquecer de indicar a categoria", "Usar uma categoria não suportada", "Usar o comando sem o prefixo quando necessário", "Erro de conexão temporário"],
386
+ "passos_se_der_erro": ["1. Verifique se digitou a categoria corretamente. 2. Use o formato com prefixo: <prefix>waifu <categoria> ou <prefix>wp <categoria>. 3. Se receber erro de categoria, peça a lista de categorias com <prefix>waifuajuda e es"],
387
+ "resumo_usuario_origem": "auto_ia_assistida",
388
+ "resumo_usuario_revisao_pendente": true
360
389
  }
361
390
  },
362
391
  {
@@ -575,6 +604,35 @@
575
604
  "schema": "legacy_v1_and_v2",
576
605
  "legacy_name": "waifunsfw",
577
606
  "legacy_fields_present": ["descricao", "metodos_de_uso", "permissao_necessaria", "local_de_uso", "informacoes_coletadas", "argumentos", "pre_condicoes", "dependencias_externas", "efeitos_colaterais", "observabilidade", "privacidade", "acesso", "limite_uso_por_plano"]
607
+ },
608
+ "user_experience": {
609
+ "resumo_usuario": "Envie imagens NSFW de waifus em grupos autorizados. Requer Premium e NSFW ativo. Use <prefix>waifunsfw <categoria> ou <prefix>wpnsfw <waifu>.",
610
+ "quando_usar": ["Quando o grupo tiver NSFW ativo e você for assinante Premium.", "Para solicitar uma imagem NSFW de uma categoria válida (ex.: neko, waifu, etc.).", "Para confirmar qual variedade de conteúdo NSFW está disponível no momento."],
611
+ "exemplos_reais": [
612
+ {
613
+ "situacao": "Grupo com NSFW ativo e usuário Premium solicita neko.",
614
+ "comando": "<prefix>waifunsfw neko",
615
+ "resposta_esperada": "🔞 Imagem NSFW enviada. Exemplo de próximo pedido: <prefix>waifunsfw neko.",
616
+ "variacao": "neko."
617
+ },
618
+ {
619
+ "situacao": "Grupo sem Premium ou NSFW ativo.",
620
+ "comando": "<prefix>waifunsfw neko",
621
+ "resposta_esperada": "🔒 Este comando NSFW exige acesso Premium. Em grupos, também é preciso NSFW ativo com <prefix>nsfw on.",
622
+ "variacao": "premium_required."
623
+ },
624
+ {
625
+ "situacao": "Categoria NSFW inválida.",
626
+ "comando": "<prefix>waifunsfw manga",
627
+ "resposta_esperada": "❗ Categoria NSFW inválida ou ausente. Exemplos: <prefix>waifunsfw waifu | <prefix>wpnsfw neko Veja opções em <prefix>waifuajuda.",
628
+ "variacao": "categoria_invalida."
629
+ }
630
+ ],
631
+ "resposta_esperada": ["🔞 Imagem NSFW enviada. Exemplo de próximo pedido: <prefix>waifunsfw neko", "❗ Categoria NSFW inválida ou ausente. Exemplos: <prefix>waifunsfw waifu | <prefix>wpnsfw neko Veja opções em <prefix>waifuajuda.", "🔒 Este comando NSFW exige acesso Premium. Em grupos, também é preciso NSFW ativo com <prefix>nsfw on."],
632
+ "erros_comuns_usuario": ["Não informar a categoria NSFW", "Categoria informada não é suportada", "NSFW não está ativo no grupo", "Usuário não possui assinatura Premium"],
633
+ "passos_se_der_erro": ["Verifique se o NSFW está ativo no grupo com <prefix>nsfw on e peça ao admin para ativar, se necessário.", "Confirme que você possui uma assinatura Premium válida para usar o comando.", "Certifique-se de digitar uma categoria NSFW suportada, por exemplo neko, waifu, etc.", "Tente novamente com uma categoria diferente ou em outro momento; se o problema persistir, peça ajuda ao administrador do grupo."],
634
+ "resumo_usuario_origem": "auto_ia_assistida",
635
+ "resumo_usuario_revisao_pendente": true
578
636
  }
579
637
  },
580
638
  {
@@ -771,6 +829,35 @@
771
829
  "schema": "legacy_v1_and_v2",
772
830
  "legacy_name": "waifuajuda",
773
831
  "legacy_fields_present": ["descricao", "metodos_de_uso", "permissao_necessaria", "local_de_uso", "informacoes_coletadas", "argumentos", "pre_condicoes", "dependencias_externas", "efeitos_colaterais", "observabilidade", "privacidade", "acesso", "limite_uso_por_plano"]
832
+ },
833
+ "user_experience": {
834
+ "resumo_usuario": "Guia rápido das categorias de fotos anime que posso enviar. Mostra todas as opções disponíveis e como usar o comando. Não é necessário Premium; funciona para planos comum e premium.",
835
+ "quando_usar": ["Quando você quer conhecer todas as categorias de fotos disponíveis sem digitar argumentos", "Quando precisa confirmar que o comando não aceita parâmetros", "Quando desejar usar o alias wppicshelp para abrir o guia rapidamente"],
836
+ "exemplos_reais": [
837
+ {
838
+ "situacao": "Usuário quer abrir o guia rapidamente com o prefix.",
839
+ "comando": "<prefix>waifuajuda",
840
+ "resposta_esperada": "Guia de categorias exibido. Exemplo: <prefix>waifu neko.",
841
+ "variacao": "uso direto com <prefix>."
842
+ },
843
+ {
844
+ "situacao": "Usuário prefere usar o alias.",
845
+ "comando": "wppicshelp",
846
+ "resposta_esperada": "Guia de categorias exibido. Observação: você pode usar <prefix>waifu neko para abrir uma categoria específica.",
847
+ "variacao": "alias."
848
+ },
849
+ {
850
+ "situacao": "Usuário está em contexto privado ou grupo permitido.",
851
+ "comando": "<prefix>waifuajuda",
852
+ "resposta_esperada": "Guia pronto. Veja as categorias disponíveis e escolha uma: neko, shinobu, etc.",
853
+ "variacao": "contexto privado ou grupo permitido."
854
+ }
855
+ ],
856
+ "resposta_esperada": ["Guia de categorias exibido com sucesso. Use <prefix>waifu neko para ver fotos de uma categoria específica."],
857
+ "erros_comuns_usuario": ["Tentar usar o comando com argumentos, quando ele não exige nenhum.", "Esquecer de incluir o prefixo, por exemplo usar 'waifuajuda' sem o prefixo.", "Tentar usar em contexto onde o guia não pode ser exibido (contextos não suportados)."],
858
+ "passos_se_der_erro": ["Verifique se o comando está sendo usado em um contexto permitido (privado ou grupo permitido).", "Tente novamente com o prefix correto: <prefix>waifuajuda.", "Se o guia não aparecer, tente novamente mais tarde ou entre em contato com o suporte."],
859
+ "resumo_usuario_origem": "auto_ia_assistida",
860
+ "resumo_usuario_revisao_pendente": true
774
861
  }
775
862
  }
776
863
  ],
@@ -1,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import axios from 'axios';
2
3
 
3
4
  import logger from '#logger';
@@ -278,7 +279,7 @@ const buildRateScopeKey = ({ scope, senderKey, remoteJid }) => {
278
279
  return `user:${String(senderKey || '').trim() || 'unknown'}`;
279
280
  };
280
281
 
281
- const pruneRateMapIfNeeded = (now = Date.now()) => {
282
+ const pruneRateMapIfNeeded = (now = __timeNowMs()) => {
282
283
  if (userPlanRateMap.size <= USER_RATE_LIMIT_MAP_MAX_SIZE) return;
283
284
 
284
285
  for (const [key, value] of userPlanRateMap.entries()) {
@@ -302,7 +303,7 @@ const pruneRateMapIfNeeded = (now = Date.now()) => {
302
303
  const checkPlanUsageRateLimit = ({ commandKey, userPlan, scope, max, windowMs, senderKey, remoteJid }) => {
303
304
  if (!max || !windowMs) return { limited: false, remainingMs: 0 };
304
305
 
305
- const now = Date.now();
306
+ const now = __timeNowMs();
306
307
  const scopeKey = buildRateScopeKey({ scope, senderKey, remoteJid });
307
308
  const cacheKey = `${String(commandKey || 'waifu')}:${normalizePlanName(userPlan)}:${scopeKey}`;
308
309
  const current = userPlanRateMap.get(cacheKey);
@@ -32,6 +32,8 @@ const METRICS_SERVICE = process.env.METRICS_SERVICE_NAME || process.env.ECOSYSTE
32
32
  const HTTP_SLO_TARGET_MS = Math.max(50, parseEnvNumber(process.env.HTTP_SLO_TARGET_MS, 750));
33
33
 
34
34
  const QUERY_THRESHOLDS_MS = parseThresholds(process.env.DB_QUERY_ALERT_THRESHOLDS, [500, 1000]);
35
+ const ADMIN_ALERT_SEVERITIES = Object.freeze(['critical', 'high', 'medium', 'low', 'unknown']);
36
+ const ADMIN_FEATURE_FLAG_STATES = Object.freeze(['enabled', 'disabled', 'total']);
35
37
 
36
38
  const registry = new client.Registry();
37
39
  let metrics = null;
@@ -375,6 +377,53 @@ const ensureMetrics = () => {
375
377
  labelNames: ['outcome'],
376
378
  registers: [registry],
377
379
  }),
380
+ adminOverviewUpdatedAtSeconds: new client.Gauge({
381
+ name: 'omnizap_admin_overview_updated_at_seconds',
382
+ help: 'Timestamp Unix (s) da ultima atualizacao de snapshot do painel admin',
383
+ registers: [registry],
384
+ }),
385
+ adminOverviewRequestsTotal: new client.Counter({
386
+ name: 'omnizap_admin_overview_requests_total',
387
+ help: 'Total de snapshots do painel admin publicados em metricas',
388
+ labelNames: ['source'],
389
+ registers: [registry],
390
+ }),
391
+ adminCounters: new client.Gauge({
392
+ name: 'omnizap_admin_counters',
393
+ help: 'Contadores agregados do painel admin',
394
+ labelNames: ['counter'],
395
+ registers: [registry],
396
+ }),
397
+ adminDashboardQuick: new client.Gauge({
398
+ name: 'omnizap_admin_dashboard_quick',
399
+ help: 'Metricas rapidas do dashboard admin',
400
+ labelNames: ['metric'],
401
+ registers: [registry],
402
+ }),
403
+ adminSystemHealth: new client.Gauge({
404
+ name: 'omnizap_admin_system_health',
405
+ help: 'Indicadores de saude expostos no painel admin',
406
+ labelNames: ['metric'],
407
+ registers: [registry],
408
+ }),
409
+ adminAlertsTotal: new client.Gauge({
410
+ name: 'omnizap_admin_alerts_total',
411
+ help: 'Total de alertas ativos por severidade no painel admin',
412
+ labelNames: ['severity'],
413
+ registers: [registry],
414
+ }),
415
+ adminFeatureFlagsTotal: new client.Gauge({
416
+ name: 'omnizap_admin_feature_flags_total',
417
+ help: 'Distribuicao de feature flags no painel admin',
418
+ labelNames: ['state'],
419
+ registers: [registry],
420
+ }),
421
+ adminSnapshotItemsTotal: new client.Gauge({
422
+ name: 'omnizap_admin_snapshot_items_total',
423
+ help: 'Total de itens por secao no snapshot do painel admin',
424
+ labelNames: ['section'],
425
+ registers: [registry],
426
+ }),
378
427
  };
379
428
 
380
429
  return metrics;
@@ -502,6 +551,93 @@ export const recordMessagesUpsert = ({ durationMs, type, messagesCount, ok }) =>
502
551
  }
503
552
  };
504
553
 
554
+ export const setAdminOverviewSnapshot = ({ overview = null, source = 'admin_overview' } = {}) => {
555
+ const m = ensureMetrics();
556
+ if (!m) return;
557
+ if (!overview || typeof overview !== 'object') return;
558
+
559
+ const setGaugeIfFinite = (gauge, labels, value, { clampToZero = false } = {}) => {
560
+ const numeric = Number(value);
561
+ if (!Number.isFinite(numeric)) return;
562
+ gauge.set(labels, clampToZero ? Math.max(0, numeric) : numeric);
563
+ };
564
+
565
+ const sourceLabel = normalizeLabel(source, 'admin_overview').slice(0, 32);
566
+ m.adminOverviewRequestsTotal.inc({ source: sourceLabel });
567
+
568
+ const updatedAtMs = Date.parse(String(overview?.updated_at || ''));
569
+ const updatedAtSeconds = Number.isFinite(updatedAtMs) ? updatedAtMs / 1000 : Date.now() / 1000;
570
+ m.adminOverviewUpdatedAtSeconds.set(updatedAtSeconds);
571
+
572
+ const counters = overview?.counters && typeof overview.counters === 'object' ? overview.counters : {};
573
+ const counterKeys = ['total_packs_any_status', 'total_stickers_any_status', 'active_google_sessions', 'known_google_users', 'active_bans', 'visit_events_24h', 'visit_events_7d', 'unique_visitors_7d'];
574
+ for (const key of counterKeys) {
575
+ setGaugeIfFinite(m.adminCounters, { counter: key }, counters?.[key], { clampToZero: true });
576
+ }
577
+
578
+ const dashboardQuick = overview?.dashboard_quick && typeof overview.dashboard_quick === 'object' ? overview.dashboard_quick : {};
579
+ const quickKeys = ['bots_online', 'messages_today', 'spam_blocked_today', 'errors_5xx'];
580
+ for (const key of quickKeys) {
581
+ setGaugeIfFinite(m.adminDashboardQuick, { metric: key }, dashboardQuick?.[key], { clampToZero: true });
582
+ }
583
+
584
+ const systemHealth = overview?.system_health && typeof overview.system_health === 'object' ? overview.system_health : {};
585
+ const healthKeys = ['cpu_percent', 'ram_percent', 'http_latency_p95_ms', 'queue_pending', 'db_total_queries', 'db_slow_queries'];
586
+ for (const key of healthKeys) {
587
+ setGaugeIfFinite(m.adminSystemHealth, { metric: key }, systemHealth?.[key], { clampToZero: true });
588
+ }
589
+
590
+ const moderationQueue = Array.isArray(overview?.moderation_queue) ? overview.moderation_queue : [];
591
+ const auditLog = Array.isArray(overview?.audit_log) ? overview.audit_log : [];
592
+ const users = Array.isArray(overview?.users_sessions?.users) ? overview.users_sessions.users : [];
593
+ const activeSessions = Array.isArray(overview?.users_sessions?.active_sessions) ? overview.users_sessions.active_sessions : [];
594
+ const blockedAccounts = Array.isArray(overview?.users_sessions?.blocked_accounts) ? overview.users_sessions.blocked_accounts : [];
595
+ const alerts = Array.isArray(overview?.alerts) ? overview.alerts : [];
596
+ const featureFlags = Array.isArray(overview?.feature_flags) ? overview.feature_flags : [];
597
+
598
+ m.adminSnapshotItemsTotal.set({ section: 'moderation_queue' }, moderationQueue.length);
599
+ m.adminSnapshotItemsTotal.set({ section: 'audit_log' }, auditLog.length);
600
+ m.adminSnapshotItemsTotal.set({ section: 'users' }, users.length);
601
+ m.adminSnapshotItemsTotal.set({ section: 'active_sessions' }, activeSessions.length);
602
+ m.adminSnapshotItemsTotal.set({ section: 'blocked_accounts' }, blockedAccounts.length);
603
+ m.adminSnapshotItemsTotal.set({ section: 'alerts' }, alerts.length);
604
+ m.adminSnapshotItemsTotal.set({ section: 'feature_flags' }, featureFlags.length);
605
+
606
+ const severityCounts = Object.fromEntries(ADMIN_ALERT_SEVERITIES.map((severity) => [severity, 0]));
607
+ for (const alert of alerts) {
608
+ const severityRaw = String(alert?.severity || '')
609
+ .trim()
610
+ .toLowerCase();
611
+ const severity = ADMIN_ALERT_SEVERITIES.includes(severityRaw) ? severityRaw : 'unknown';
612
+ severityCounts[severity] = Number(severityCounts[severity] || 0) + 1;
613
+ }
614
+ for (const severity of ADMIN_ALERT_SEVERITIES) {
615
+ m.adminAlertsTotal.set({ severity }, Number(severityCounts[severity] || 0));
616
+ }
617
+
618
+ let enabledFlags = 0;
619
+ let disabledFlags = 0;
620
+ for (const flag of featureFlags) {
621
+ if (flag?.is_enabled) {
622
+ enabledFlags += 1;
623
+ } else {
624
+ disabledFlags += 1;
625
+ }
626
+ }
627
+ const totalFlags = enabledFlags + disabledFlags;
628
+ for (const state of ADMIN_FEATURE_FLAG_STATES) {
629
+ if (state === 'enabled') {
630
+ m.adminFeatureFlagsTotal.set({ state }, enabledFlags);
631
+ continue;
632
+ }
633
+ if (state === 'disabled') {
634
+ m.adminFeatureFlagsTotal.set({ state }, disabledFlags);
635
+ continue;
636
+ }
637
+ m.adminFeatureFlagsTotal.set({ state }, totalFlags);
638
+ }
639
+ };
640
+
505
641
  export const recordRpgPlayerCreated = (value = 1) => {
506
642
  const m = ensureMetrics();
507
643
  if (!m) return;