@omnizap-system/omnizap 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +54 -9
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/security-runner-hardening.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +1 -0
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +226 -55
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +143 -3
- package/app/configParts/sessionConfig.js +157 -0
- package/app/connection/baileysCompatibility.test.js +1 -1
- package/app/connection/groupOwnerWriteStateResolver.js +109 -0
- package/app/connection/socketController.js +625 -124
- package/app/connection/socketController.multiSession.test.js +108 -0
- package/app/controllers/messageController.js +1 -1
- package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
- package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
- package/app/controllers/messageProcessingPipeline.js +88 -9
- package/app/controllers/messageProcessingPipeline.test.js +200 -0
- package/app/modules/adminModule/AGENT.md +1 -1
- package/app/modules/adminModule/commandConfig.json +3318 -1347
- package/app/modules/adminModule/groupCommandHandlers.js +856 -14
- package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
- package/app/modules/adminModule/groupWarningRepository.js +152 -0
- package/app/modules/aiModule/AGENT.md +47 -30
- package/app/modules/aiModule/aiConfigRuntime.js +1 -0
- package/app/modules/aiModule/catCommand.js +132 -25
- package/app/modules/aiModule/commandConfig.json +114 -28
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
- package/app/modules/gameModule/AGENT.md +1 -1
- package/app/modules/gameModule/commandConfig.json +29 -0
- package/app/modules/menuModule/AGENT.md +1 -1
- package/app/modules/menuModule/commandConfig.json +45 -10
- package/app/modules/menuModule/menuCatalogService.js +190 -0
- package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
- package/app/modules/menuModule/menuDynamicService.js +511 -0
- package/app/modules/menuModule/menuDynamicService.test.js +141 -0
- package/app/modules/menuModule/menus.js +36 -5
- package/app/modules/playModule/AGENT.md +10 -5
- package/app/modules/playModule/commandConfig.json +74 -16
- package/app/modules/playModule/playCommandConstants.js +13 -7
- package/app/modules/playModule/playCommandCore.js +4 -6
- package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
- package/app/modules/playModule/playConfigRuntime.js +5 -6
- package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerPackModule/AGENT.md +1 -1
- package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
- package/app/modules/stickerPackModule/commandConfig.json +29 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/geminiService.js +131 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/moduleAiHelpCoreService.js +33 -4
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +51 -21
- package/app/services/messaging/newsBroadcastService.js +843 -27
- package/app/services/multiSession/assignmentBalancerService.js +457 -0
- package/app/services/multiSession/groupOwnershipRepository.js +381 -0
- package/app/services/multiSession/groupOwnershipService.js +890 -0
- package/app/services/multiSession/groupOwnershipService.test.js +309 -0
- package/app/services/multiSession/sessionRegistryService.js +293 -0
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/groupConfigStore.js +41 -5
- package/app/store/premiumUserStore.js +21 -7
- package/app/utils/antiLink/antiLinkModule.js +352 -16
- package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
- package/database/index.js +6 -0
- package/database/migrations/20260307_d0_hardening_down.sql +1 -1
- package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
- package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
- package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
- package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
- package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
- package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
- package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
- package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
- package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
- package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
- package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
- package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
- package/database/schema.sql +102 -1
- package/docker-compose.yml +4 -1
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
- package/docs/security/omnizap-static-security-headers.conf +25 -0
- package/ecosystem.prod.config.cjs +31 -11
- package/index.js +52 -18
- package/observability/alert-rules.yml +20 -0
- package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
- package/observability/mysql-setup.sql +4 -4
- package/observability/system-admin-observability.md +26 -0
- package/package.json +12 -5
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/stickersApp.js +255 -103
- package/public/js/apps/termsReactApp.js +57 -8
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +96 -47
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/politica-de-privacidade.html +1 -1
- package/public/pages/stickers.html +5 -5
- package/public/pages/termos-de-uso-texto-integral.html +1 -1
- package/public/pages/termos-de-uso.html +1 -1
- package/public/pages/user-password-reset.html +3 -4
- package/public/pages/user-systemadm.html +8 -462
- package/public/pages/user.html +1 -1
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +10 -0
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +155 -0
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/sticker/stickerCatalogController.js +9 -23
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +3 -2
- package/server/http/httpServer.js +8 -4
- package/server/middleware/securityHeaders.js +20 -1
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +30 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/static/staticPageRouter.js +27 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +2 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import logger from '#logger';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_LOCAL_CATALOG_PATH = path.resolve(process.cwd(), 'public/comandos/commands-catalog.json');
|
|
6
|
+
const DEFAULT_REMOTE_CATALOG_URL = 'https://omnizap.shop/comandos/commands-catalog.json';
|
|
7
|
+
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
8
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 2500;
|
|
9
|
+
|
|
10
|
+
const toPositiveInt = (value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) => {
|
|
11
|
+
const numeric = Number.parseInt(String(value ?? ''), 10);
|
|
12
|
+
if (!Number.isFinite(numeric) || numeric < min) return fallback;
|
|
13
|
+
return Math.max(min, Math.min(max, numeric));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MENU_CATALOG_LOCAL_PATH = String(process.env.MENU_CATALOG_LOCAL_PATH || DEFAULT_LOCAL_CATALOG_PATH).trim() || DEFAULT_LOCAL_CATALOG_PATH;
|
|
17
|
+
const MENU_CATALOG_REMOTE_URL = String(process.env.MENU_CATALOG_REMOTE_URL || DEFAULT_REMOTE_CATALOG_URL).trim() || DEFAULT_REMOTE_CATALOG_URL;
|
|
18
|
+
const MENU_CATALOG_CACHE_TTL_MS = toPositiveInt(process.env.MENU_CATALOG_CACHE_TTL_MS, DEFAULT_CACHE_TTL_MS, 15_000, 30 * 60 * 1000);
|
|
19
|
+
const MENU_CATALOG_FETCH_TIMEOUT_MS = toPositiveInt(process.env.MENU_CATALOG_FETCH_TIMEOUT_MS, DEFAULT_FETCH_TIMEOUT_MS, 500, 15_000);
|
|
20
|
+
const MENU_CATALOG_REMOTE_ENABLED =
|
|
21
|
+
String(process.env.MENU_CATALOG_REMOTE_ENABLED ?? 'true')
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase() !== 'false';
|
|
24
|
+
|
|
25
|
+
const sanitizeLogValue = (value) =>
|
|
26
|
+
String(value ?? '')
|
|
27
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
28
|
+
.replace(/\s+/g, ' ')
|
|
29
|
+
.trim();
|
|
30
|
+
|
|
31
|
+
const normalizeCatalogCategories = (categories = []) =>
|
|
32
|
+
categories
|
|
33
|
+
.map((entry) => {
|
|
34
|
+
const key = String(entry?.key || '').trim();
|
|
35
|
+
const label = String(entry?.label || key || 'Categoria').trim();
|
|
36
|
+
const commands = Array.isArray(entry?.commands) ? entry.commands : [];
|
|
37
|
+
if (!key || !commands.length) return null;
|
|
38
|
+
return {
|
|
39
|
+
...entry,
|
|
40
|
+
key,
|
|
41
|
+
label,
|
|
42
|
+
commands,
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
|
|
47
|
+
const parseGeneratedAtMs = (catalog = null) => {
|
|
48
|
+
const parsed = Date.parse(String(catalog?.generated_at || '').trim());
|
|
49
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const normalizeCatalogPayload = (rawCatalog = null) => {
|
|
53
|
+
if (!rawCatalog || typeof rawCatalog !== 'object') {
|
|
54
|
+
throw new Error('catalogo_invalido: payload ausente');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const categories = normalizeCatalogCategories(rawCatalog.categories);
|
|
58
|
+
if (!categories.length) {
|
|
59
|
+
throw new Error('catalogo_invalido: sem categorias');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...rawCatalog,
|
|
64
|
+
categories,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const buildSnapshot = ({ catalog, source = 'unknown' } = {}) => ({
|
|
69
|
+
catalog,
|
|
70
|
+
source,
|
|
71
|
+
generatedAtMs: parseGeneratedAtMs(catalog),
|
|
72
|
+
fetchedAtMs: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const compareSnapshots = (left, right) => {
|
|
76
|
+
const leftGenerated = Number(left?.generatedAtMs || 0);
|
|
77
|
+
const rightGenerated = Number(right?.generatedAtMs || 0);
|
|
78
|
+
if (leftGenerated !== rightGenerated) return rightGenerated - leftGenerated;
|
|
79
|
+
|
|
80
|
+
const priority = {
|
|
81
|
+
remote: 3,
|
|
82
|
+
local: 2,
|
|
83
|
+
cache: 1,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (priority[right?.source] || 0) - (priority[left?.source] || 0);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let catalogCache = {
|
|
90
|
+
snapshot: null,
|
|
91
|
+
expiresAt: 0,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const readLocalCatalogSnapshot = async () => {
|
|
95
|
+
const text = await readFile(MENU_CATALOG_LOCAL_PATH, 'utf8');
|
|
96
|
+
const parsed = JSON.parse(text);
|
|
97
|
+
const catalog = normalizeCatalogPayload(parsed);
|
|
98
|
+
return buildSnapshot({
|
|
99
|
+
catalog,
|
|
100
|
+
source: 'local',
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const fetchRemoteCatalogSnapshot = async () => {
|
|
105
|
+
if (!MENU_CATALOG_REMOTE_ENABLED) return null;
|
|
106
|
+
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timeoutId = setTimeout(() => controller.abort(), MENU_CATALOG_FETCH_TIMEOUT_MS);
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(MENU_CATALOG_REMOTE_URL, {
|
|
111
|
+
method: 'GET',
|
|
112
|
+
headers: {
|
|
113
|
+
accept: 'application/json',
|
|
114
|
+
},
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`http_${response.status}`);
|
|
119
|
+
}
|
|
120
|
+
const parsed = await response.json();
|
|
121
|
+
const catalog = normalizeCatalogPayload(parsed);
|
|
122
|
+
return buildSnapshot({
|
|
123
|
+
catalog,
|
|
124
|
+
source: 'remote',
|
|
125
|
+
});
|
|
126
|
+
} finally {
|
|
127
|
+
clearTimeout(timeoutId);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const getCommandsCatalogSnapshot = async ({ forceRefresh = false } = {}) => {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
if (!forceRefresh && catalogCache.snapshot && now < catalogCache.expiresAt) {
|
|
134
|
+
return catalogCache.snapshot;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const localPromise = readLocalCatalogSnapshot();
|
|
138
|
+
const remotePromise = MENU_CATALOG_REMOTE_ENABLED ? fetchRemoteCatalogSnapshot() : Promise.resolve(null);
|
|
139
|
+
const [localResult, remoteResult] = await Promise.allSettled([localPromise, remotePromise]);
|
|
140
|
+
|
|
141
|
+
const candidates = [];
|
|
142
|
+
|
|
143
|
+
if (localResult.status === 'fulfilled' && localResult.value) {
|
|
144
|
+
candidates.push(localResult.value);
|
|
145
|
+
} else if (localResult.status === 'rejected') {
|
|
146
|
+
logger.warn('Falha ao carregar catalogo local de comandos para o menu dinamico.', {
|
|
147
|
+
action: 'menu_catalog_local_load_failed',
|
|
148
|
+
error: sanitizeLogValue(localResult.reason?.message) || 'unknown_error',
|
|
149
|
+
catalogPath: MENU_CATALOG_LOCAL_PATH,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (remoteResult.status === 'fulfilled' && remoteResult.value) {
|
|
154
|
+
candidates.push(remoteResult.value);
|
|
155
|
+
} else if (remoteResult.status === 'rejected') {
|
|
156
|
+
logger.warn('Falha ao atualizar catalogo remoto de comandos para o menu dinamico.', {
|
|
157
|
+
action: 'menu_catalog_remote_fetch_failed',
|
|
158
|
+
error: sanitizeLogValue(remoteResult.reason?.message) || 'unknown_error',
|
|
159
|
+
catalogUrl: MENU_CATALOG_REMOTE_URL,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (catalogCache.snapshot) {
|
|
164
|
+
candidates.push({
|
|
165
|
+
...catalogCache.snapshot,
|
|
166
|
+
source: 'cache',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
candidates.sort(compareSnapshots);
|
|
171
|
+
const selectedSnapshot = candidates[0] || null;
|
|
172
|
+
|
|
173
|
+
if (!selectedSnapshot?.catalog) {
|
|
174
|
+
throw new Error('catalogo_indisponivel');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
catalogCache = {
|
|
178
|
+
snapshot: selectedSnapshot,
|
|
179
|
+
expiresAt: now + MENU_CATALOG_CACHE_TTL_MS,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return selectedSnapshot;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const resetMenuCatalogCacheForTests = () => {
|
|
186
|
+
catalogCache = {
|
|
187
|
+
snapshot: null,
|
|
188
|
+
expiresAt: 0,
|
|
189
|
+
};
|
|
190
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import logger from '#logger';
|
|
2
|
+
import { executeQuery, TABLES } from '../../../database/index.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_USAGE_WINDOW_DAYS = 30;
|
|
5
|
+
const DEFAULT_TOP_LIMIT = 20;
|
|
6
|
+
const DEFAULT_CACHE_TTL_MS = 90_000;
|
|
7
|
+
|
|
8
|
+
const toPositiveInt = (value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) => {
|
|
9
|
+
const numeric = Number.parseInt(String(value ?? ''), 10);
|
|
10
|
+
if (!Number.isFinite(numeric) || numeric < min) return fallback;
|
|
11
|
+
return Math.max(min, Math.min(max, numeric));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const MENU_USAGE_CACHE_TTL_MS = toPositiveInt(process.env.MENU_USAGE_CACHE_TTL_MS, DEFAULT_CACHE_TTL_MS, 5000, 10 * 60 * 1000);
|
|
15
|
+
|
|
16
|
+
const sanitizeLogValue = (value) =>
|
|
17
|
+
String(value ?? '')
|
|
18
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
19
|
+
.replace(/\s+/g, ' ')
|
|
20
|
+
.trim();
|
|
21
|
+
|
|
22
|
+
const normalizeCommandName = (value) =>
|
|
23
|
+
String(value || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9_-]/g, '')
|
|
27
|
+
.slice(0, 64);
|
|
28
|
+
|
|
29
|
+
const normalizeChatId = (value) => {
|
|
30
|
+
const normalized = String(value || '')
|
|
31
|
+
.trim()
|
|
32
|
+
.slice(0, 255);
|
|
33
|
+
return normalized || null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const buildCacheKey = ({ days, limit, chatId }) => `days:${days}|limit:${limit}|chat:${chatId || 'global'}`;
|
|
37
|
+
|
|
38
|
+
let usageCache = new Map();
|
|
39
|
+
|
|
40
|
+
export const listTopCommandsByUsage = async ({ days = DEFAULT_USAGE_WINDOW_DAYS, limit = DEFAULT_TOP_LIMIT, chatId = null, forceRefresh = false } = {}) => {
|
|
41
|
+
const safeDays = toPositiveInt(days, DEFAULT_USAGE_WINDOW_DAYS, 1, 365);
|
|
42
|
+
const safeLimit = toPositiveInt(limit, DEFAULT_TOP_LIMIT, 1, 100);
|
|
43
|
+
const safeChatId = normalizeChatId(chatId);
|
|
44
|
+
const cacheKey = buildCacheKey({
|
|
45
|
+
days: safeDays,
|
|
46
|
+
limit: safeLimit,
|
|
47
|
+
chatId: safeChatId,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const cached = usageCache.get(cacheKey);
|
|
52
|
+
if (!forceRefresh && cached && cached.expiresAt > now) {
|
|
53
|
+
return cached.rows;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const where = ['is_command = 1', 'command_known = 1', 'command_name IS NOT NULL', "command_name <> ''", 'created_at >= (UTC_TIMESTAMP() - INTERVAL ? DAY)'];
|
|
57
|
+
const params = [safeDays];
|
|
58
|
+
|
|
59
|
+
if (safeChatId) {
|
|
60
|
+
where.push('chat_id = ?');
|
|
61
|
+
params.push(safeChatId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
params.push(safeLimit);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const rows = await executeQuery(
|
|
68
|
+
`SELECT command_name AS command_name, COUNT(*) AS usage_count
|
|
69
|
+
FROM ${TABLES.MESSAGE_ANALYSIS_EVENT}
|
|
70
|
+
WHERE ${where.join(' AND ')}
|
|
71
|
+
GROUP BY command_name
|
|
72
|
+
ORDER BY usage_count DESC
|
|
73
|
+
LIMIT ?`,
|
|
74
|
+
params,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const normalizedRows = (Array.isArray(rows) ? rows : [])
|
|
78
|
+
.map((row) => {
|
|
79
|
+
const commandName = normalizeCommandName(row?.command_name);
|
|
80
|
+
const usageCount = Number(row?.usage_count || 0);
|
|
81
|
+
if (!commandName || !Number.isFinite(usageCount) || usageCount <= 0) return null;
|
|
82
|
+
return {
|
|
83
|
+
commandName,
|
|
84
|
+
usageCount: Math.max(1, Math.floor(usageCount)),
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
|
|
89
|
+
usageCache.set(cacheKey, {
|
|
90
|
+
rows: normalizedRows,
|
|
91
|
+
expiresAt: now + MENU_USAGE_CACHE_TTL_MS,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return normalizedRows;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.warn('Falha ao consultar top comandos por uso para o menu dinamico.', {
|
|
97
|
+
action: 'menu_usage_query_failed',
|
|
98
|
+
error: sanitizeLogValue(error?.message) || 'unknown_error',
|
|
99
|
+
chatId: safeChatId || null,
|
|
100
|
+
days: safeDays,
|
|
101
|
+
limit: safeLimit,
|
|
102
|
+
});
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const resetMenuUsageCacheForTests = () => {
|
|
108
|
+
usageCache = new Map();
|
|
109
|
+
};
|