@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,511 @@
|
|
|
1
|
+
import logger from '#logger';
|
|
2
|
+
import { getCommandsCatalogSnapshot } from './menuCatalogService.js';
|
|
3
|
+
|
|
4
|
+
const MENU_SITE_URL = 'https://omnizap.shop/comandos/';
|
|
5
|
+
const DEFAULT_USAGE_WINDOW_DAYS = 30;
|
|
6
|
+
const DEFAULT_MANY_COMMANDS_THRESHOLD = 20;
|
|
7
|
+
const DEFAULT_MAIN_PRIMARY_LIMIT = 8;
|
|
8
|
+
const DEFAULT_MAIN_ROTATION_LIMIT = 4;
|
|
9
|
+
const DEFAULT_TOP_MENU_LIMIT = 12;
|
|
10
|
+
const DEFAULT_CATEGORY_MENU_LIMIT = 12;
|
|
11
|
+
const DEFAULT_CATEGORY_PREVIEW_LIMIT = 8;
|
|
12
|
+
|
|
13
|
+
const toPositiveInt = (value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) => {
|
|
14
|
+
const numeric = Number.parseInt(String(value ?? ''), 10);
|
|
15
|
+
if (!Number.isFinite(numeric) || numeric < min) return fallback;
|
|
16
|
+
return Math.max(min, Math.min(max, numeric));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MENU_USAGE_WINDOW_DAYS = toPositiveInt(process.env.MENU_USAGE_WINDOW_DAYS, DEFAULT_USAGE_WINDOW_DAYS, 1, 365);
|
|
20
|
+
const MENU_MANY_COMMANDS_THRESHOLD = toPositiveInt(process.env.MENU_MANY_COMMANDS_THRESHOLD, DEFAULT_MANY_COMMANDS_THRESHOLD, 8, 120);
|
|
21
|
+
const MENU_MAIN_PRIMARY_LIMIT = toPositiveInt(process.env.MENU_MAIN_PRIMARY_LIMIT, DEFAULT_MAIN_PRIMARY_LIMIT, 4, 20);
|
|
22
|
+
const MENU_MAIN_ROTATION_LIMIT = toPositiveInt(process.env.MENU_MAIN_ROTATION_LIMIT, DEFAULT_MAIN_ROTATION_LIMIT, 0, 20);
|
|
23
|
+
const MENU_TOP_LIMIT = toPositiveInt(process.env.MENU_TOP_LIMIT, DEFAULT_TOP_MENU_LIMIT, 5, 30);
|
|
24
|
+
const MENU_CATEGORY_LIMIT = toPositiveInt(process.env.MENU_CATEGORY_LIMIT, DEFAULT_CATEGORY_MENU_LIMIT, 5, 40);
|
|
25
|
+
const MENU_CATEGORY_PREVIEW_LIMIT = toPositiveInt(process.env.MENU_CATEGORY_PREVIEW_LIMIT, DEFAULT_CATEGORY_PREVIEW_LIMIT, 4, 20);
|
|
26
|
+
|
|
27
|
+
const TOP_TOKENS = new Set(['top', 'tops', 'maisusados', 'popular', 'populares', 'trending']);
|
|
28
|
+
const ALL_TOKENS = new Set(['todos', 'todas', 'all', 'completo', 'completa']);
|
|
29
|
+
const CATEGORY_TOKENS = new Set(['categoria', 'cat', 'categorias']);
|
|
30
|
+
const DEFAULT_MENU_COMMAND_NAME = 'menu';
|
|
31
|
+
|
|
32
|
+
const sanitizeLogValue = (value) =>
|
|
33
|
+
String(value ?? '')
|
|
34
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.trim();
|
|
37
|
+
|
|
38
|
+
const normalizeLookupToken = (value) =>
|
|
39
|
+
String(value || '')
|
|
40
|
+
.normalize('NFD')
|
|
41
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, '')
|
|
44
|
+
.trim();
|
|
45
|
+
|
|
46
|
+
const normalizeCommandName = (value) =>
|
|
47
|
+
String(value || '')
|
|
48
|
+
.trim()
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9_-]/g, '')
|
|
51
|
+
.slice(0, 64);
|
|
52
|
+
|
|
53
|
+
const normalizeMenuCommandName = (value) => normalizeCommandName(value) || DEFAULT_MENU_COMMAND_NAME;
|
|
54
|
+
|
|
55
|
+
const buildMenuCall = ({ commandPrefix = '/', menuCommandName = DEFAULT_MENU_COMMAND_NAME, args = '' } = {}) => {
|
|
56
|
+
const safePrefix = String(commandPrefix || '/');
|
|
57
|
+
const safeCommandName = normalizeMenuCommandName(menuCommandName);
|
|
58
|
+
const safeArgs = String(args || '')
|
|
59
|
+
.replace(/\s+/g, ' ')
|
|
60
|
+
.trim();
|
|
61
|
+
return safeArgs ? `${safePrefix}${safeCommandName} ${safeArgs}` : `${safePrefix}${safeCommandName}`;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const shortText = (value, max = 62) => {
|
|
65
|
+
const text = String(value || '')
|
|
66
|
+
.replace(/\s+/g, ' ')
|
|
67
|
+
.trim();
|
|
68
|
+
if (!text) return 'Sem descricao cadastrada.';
|
|
69
|
+
if (text.length <= max) return text;
|
|
70
|
+
return `${text.slice(0, Math.max(0, max - 3)).trim()}...`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const chunk = (items = [], size = 8) => {
|
|
74
|
+
const safeSize = Math.max(1, size);
|
|
75
|
+
const chunks = [];
|
|
76
|
+
for (let index = 0; index < items.length; index += safeSize) {
|
|
77
|
+
chunks.push(items.slice(index, index + safeSize));
|
|
78
|
+
}
|
|
79
|
+
return chunks;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const parseGeneratedAtMs = (snapshot = null) => {
|
|
83
|
+
const explicit = Number(snapshot?.generatedAtMs || 0);
|
|
84
|
+
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
85
|
+
const parsed = Date.parse(String(snapshot?.catalog?.generated_at || '').trim());
|
|
86
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const formatCatalogGeneratedAt = (snapshot = null) => {
|
|
90
|
+
const generatedAtMs = parseGeneratedAtMs(snapshot);
|
|
91
|
+
if (!generatedAtMs) return null;
|
|
92
|
+
try {
|
|
93
|
+
const formatter = new Intl.DateTimeFormat('pt-BR', {
|
|
94
|
+
dateStyle: 'short',
|
|
95
|
+
timeStyle: 'short',
|
|
96
|
+
timeZone: 'UTC',
|
|
97
|
+
});
|
|
98
|
+
return `${formatter.format(new Date(generatedAtMs))} UTC`;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const normalizeCatalogCategory = (rawCategory = {}) => {
|
|
105
|
+
const key = String(rawCategory?.key || '').trim();
|
|
106
|
+
const label = String(rawCategory?.label || key || 'Categoria').trim();
|
|
107
|
+
const rawCommands = Array.isArray(rawCategory?.commands) ? rawCategory.commands : [];
|
|
108
|
+
const seenNames = new Set();
|
|
109
|
+
const commands = rawCommands
|
|
110
|
+
.map((rawCommand) => {
|
|
111
|
+
const name = normalizeCommandName(rawCommand?.name);
|
|
112
|
+
if (!name || seenNames.has(name)) return null;
|
|
113
|
+
seenNames.add(name);
|
|
114
|
+
return {
|
|
115
|
+
...rawCommand,
|
|
116
|
+
name,
|
|
117
|
+
aliases: Array.isArray(rawCommand?.aliases) ? rawCommand.aliases.map((alias) => normalizeCommandName(alias)).filter(Boolean) : [],
|
|
118
|
+
descricao: String(rawCommand?.descricao || rawCommand?.description || '').trim(),
|
|
119
|
+
};
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
|
|
123
|
+
if (!key || !commands.length) return null;
|
|
124
|
+
return {
|
|
125
|
+
...rawCategory,
|
|
126
|
+
key,
|
|
127
|
+
label,
|
|
128
|
+
commands,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const extractCatalogModel = (catalogSnapshot = null) => {
|
|
133
|
+
const catalog = catalogSnapshot?.catalog || catalogSnapshot;
|
|
134
|
+
const rawCategories = Array.isArray(catalog?.categories) ? catalog.categories : [];
|
|
135
|
+
const categories = rawCategories.map(normalizeCatalogCategory).filter(Boolean);
|
|
136
|
+
|
|
137
|
+
const dedupeCommands = new Map();
|
|
138
|
+
for (const category of categories) {
|
|
139
|
+
for (const command of category.commands) {
|
|
140
|
+
if (!dedupeCommands.has(command.name)) {
|
|
141
|
+
dedupeCommands.set(command.name, {
|
|
142
|
+
...command,
|
|
143
|
+
categoryKey: category.key,
|
|
144
|
+
categoryLabel: category.label,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
categories,
|
|
152
|
+
commands: [...dedupeCommands.values()],
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const buildUsageMap = (usageRows = []) => {
|
|
157
|
+
const usageMap = new Map();
|
|
158
|
+
for (const row of usageRows) {
|
|
159
|
+
const commandName = normalizeCommandName(row?.commandName);
|
|
160
|
+
const usageCount = Number(row?.usageCount || 0);
|
|
161
|
+
if (!commandName || !Number.isFinite(usageCount) || usageCount <= 0) continue;
|
|
162
|
+
usageMap.set(commandName, Math.max(1, Math.floor(usageCount)));
|
|
163
|
+
}
|
|
164
|
+
return usageMap;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const getSuggestionPriority = (command = {}) => {
|
|
168
|
+
const direct = Number(command?.suggestion_priority);
|
|
169
|
+
if (Number.isFinite(direct)) return direct;
|
|
170
|
+
const discoveryPriority = Number(command?.discovery?.suggestion_priority);
|
|
171
|
+
if (Number.isFinite(discoveryPriority)) return discoveryPriority;
|
|
172
|
+
return 0;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const rankCommands = (commands = [], usageMap = new Map()) =>
|
|
176
|
+
[...commands].sort((left, right) => {
|
|
177
|
+
const usageDiff = (usageMap.get(right.name) || 0) - (usageMap.get(left.name) || 0);
|
|
178
|
+
if (usageDiff !== 0) return usageDiff;
|
|
179
|
+
|
|
180
|
+
const priorityDiff = getSuggestionPriority(right) - getSuggestionPriority(left);
|
|
181
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
182
|
+
|
|
183
|
+
return left.name.localeCompare(right.name, 'pt-BR');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const mergeUsageRows = (...rowsSets) => {
|
|
187
|
+
const merged = new Map();
|
|
188
|
+
for (const rows of rowsSets) {
|
|
189
|
+
for (const row of rows || []) {
|
|
190
|
+
const commandName = normalizeCommandName(row?.commandName);
|
|
191
|
+
const usageCount = Number(row?.usageCount || 0);
|
|
192
|
+
if (!commandName || !Number.isFinite(usageCount) || usageCount <= 0) continue;
|
|
193
|
+
const current = merged.get(commandName) || 0;
|
|
194
|
+
if (usageCount > current) {
|
|
195
|
+
merged.set(commandName, Math.floor(usageCount));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return [...merged.entries()]
|
|
201
|
+
.map(([commandName, usageCount]) => ({
|
|
202
|
+
commandName,
|
|
203
|
+
usageCount,
|
|
204
|
+
}))
|
|
205
|
+
.sort((left, right) => right.usageCount - left.usageCount);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const pickRotatingCommands = (commands = [], limit = 0, seed = 0) => {
|
|
209
|
+
const safeLimit = Math.max(0, limit);
|
|
210
|
+
if (!commands.length || safeLimit <= 0) return [];
|
|
211
|
+
|
|
212
|
+
const start = Math.abs(Math.floor(seed)) % commands.length;
|
|
213
|
+
const picked = [];
|
|
214
|
+
for (let index = 0; index < commands.length && picked.length < safeLimit; index += 1) {
|
|
215
|
+
picked.push(commands[(start + index) % commands.length]);
|
|
216
|
+
}
|
|
217
|
+
return picked;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const buildCategoryAliasMap = (categories = []) => {
|
|
221
|
+
const aliasMap = new Map();
|
|
222
|
+
|
|
223
|
+
const bindAlias = (alias, category) => {
|
|
224
|
+
const normalizedAlias = normalizeLookupToken(alias);
|
|
225
|
+
if (!normalizedAlias || !category) return;
|
|
226
|
+
if (!aliasMap.has(normalizedAlias)) {
|
|
227
|
+
aliasMap.set(normalizedAlias, category);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
for (const category of categories) {
|
|
232
|
+
bindAlias(category.key, category);
|
|
233
|
+
bindAlias(category.label, category);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const bindKnownAliases = (keys = [], aliases = []) => {
|
|
237
|
+
const normalizedKeys = new Set(keys.map((entry) => normalizeLookupToken(entry)).filter(Boolean));
|
|
238
|
+
const category = categories.find((entry) => normalizedKeys.has(normalizeLookupToken(entry.key)));
|
|
239
|
+
if (!category) return;
|
|
240
|
+
for (const alias of aliases) {
|
|
241
|
+
bindAlias(alias, category);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
bindKnownAliases(['admin'], ['adm', 'administracao', 'moderação', 'moderacao']);
|
|
246
|
+
bindKnownAliases(['figurinhas'], ['figurinha', 'sticker', 'stickers']);
|
|
247
|
+
bindKnownAliases(['midia'], ['media']);
|
|
248
|
+
bindKnownAliases(['ia'], ['ai', 'inteligenciaartificial']);
|
|
249
|
+
bindKnownAliases(['stats'], ['estatistica', 'estatisticas']);
|
|
250
|
+
|
|
251
|
+
return aliasMap;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const resolveCategoryFromQuery = (categories = [], query = '') => {
|
|
255
|
+
const normalizedQuery = normalizeLookupToken(query);
|
|
256
|
+
if (!normalizedQuery) return null;
|
|
257
|
+
|
|
258
|
+
const aliasMap = buildCategoryAliasMap(categories);
|
|
259
|
+
if (aliasMap.has(normalizedQuery)) {
|
|
260
|
+
return aliasMap.get(normalizedQuery);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return categories.find((entry) => normalizeLookupToken(entry.label).includes(normalizedQuery) || normalizeLookupToken(entry.key).includes(normalizedQuery)) || null;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const applyCategoryScope = (categories = [], commands = [], categoryScopeKeys = []) => {
|
|
267
|
+
const normalizedScopeKeys = Array.isArray(categoryScopeKeys) ? categoryScopeKeys.map((entry) => normalizeLookupToken(entry)).filter(Boolean) : [];
|
|
268
|
+
|
|
269
|
+
if (!normalizedScopeKeys.length) {
|
|
270
|
+
return { categories, commands };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const scopeKeySet = new Set(normalizedScopeKeys);
|
|
274
|
+
const scopedCategories = categories.filter((category) => scopeKeySet.has(normalizeLookupToken(category.key)));
|
|
275
|
+
if (!scopedCategories.length) {
|
|
276
|
+
return { categories, commands };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const scopedCategoryKeys = new Set(scopedCategories.map((category) => category.key));
|
|
280
|
+
const scopedCommands = commands.filter((command) => scopedCategoryKeys.has(command.categoryKey));
|
|
281
|
+
return {
|
|
282
|
+
categories: scopedCategories,
|
|
283
|
+
commands: scopedCommands,
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const formatUsageSuffix = (commandName, usageMap, withLabel = true) => {
|
|
288
|
+
const usage = usageMap.get(commandName) || 0;
|
|
289
|
+
if (!usage) return '';
|
|
290
|
+
return withLabel ? ` (${usage} usos)` : ` (${usage})`;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const buildMainMenuText = ({ senderName = '', commandPrefix = '/', menuCommandName = DEFAULT_MENU_COMMAND_NAME, categories = [], commands = [], usageMap = new Map(), catalogSnapshot = null } = {}) => {
|
|
294
|
+
const rankedCommands = rankCommands(commands, usageMap);
|
|
295
|
+
const hasManyCommands = commands.length > MENU_MANY_COMMANDS_THRESHOLD;
|
|
296
|
+
const primary = rankedCommands.slice(0, MENU_MAIN_PRIMARY_LIMIT);
|
|
297
|
+
const remaining = rankedCommands.slice(MENU_MAIN_PRIMARY_LIMIT);
|
|
298
|
+
|
|
299
|
+
const rotationSeed = Math.floor(Date.now() / (24 * 60 * 60 * 1000));
|
|
300
|
+
const rotating = hasManyCommands ? pickRotatingCommands(remaining, MENU_MAIN_ROTATION_LIMIT, rotationSeed) : [];
|
|
301
|
+
const highlighted = [...new Map([...primary, ...rotating].map((command) => [command.name, command])).values()];
|
|
302
|
+
const generatedAtLabel = formatCatalogGeneratedAt(catalogSnapshot);
|
|
303
|
+
const quickCategories = [...categories].sort((left, right) => right.commands.length - left.commands.length).slice(0, MENU_CATEGORY_PREVIEW_LIMIT);
|
|
304
|
+
|
|
305
|
+
const safeName =
|
|
306
|
+
String(senderName || '')
|
|
307
|
+
.trim()
|
|
308
|
+
.split(/\s+/)[0] || 'pessoa';
|
|
309
|
+
const lines = [`Olá, ${safeName}!`, '', '📌 *Menu dinâmico do OmniZap*', `Comandos cadastrados: *${commands.length}*`, `Categorias disponíveis: *${categories.length}*`];
|
|
310
|
+
|
|
311
|
+
if (generatedAtLabel) {
|
|
312
|
+
lines.push(`Catálogo atualizado: *${generatedAtLabel}*`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
lines.push('', '*Categorias rápidas:*');
|
|
316
|
+
for (const category of quickCategories) {
|
|
317
|
+
lines.push(`• ${category.label} (${category.commands.length}) → ${buildMenuCall({ commandPrefix, menuCommandName, args: category.key })}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (categories.length > quickCategories.length) {
|
|
321
|
+
lines.push(`• ...e mais ${categories.length - quickCategories.length} categorias`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
lines.push('', hasManyCommands ? `*🔥 Destaques por uso (${MENU_USAGE_WINDOW_DAYS}d):*` : '*✨ Destaques:*');
|
|
325
|
+
for (const command of highlighted) {
|
|
326
|
+
lines.push(`• ${commandPrefix}${command.name}${formatUsageSuffix(command.name, usageMap, false)} — ${shortText(command.descricao, 56)}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!highlighted.length) {
|
|
330
|
+
lines.push('• Nenhum comando em destaque encontrado.');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
lines.push('', '*Navegação:*', `• ${buildMenuCall({ commandPrefix, menuCommandName, args: 'top' })}`, `• ${buildMenuCall({ commandPrefix, menuCommandName, args: 'categoria <nome>' })}`, `• ${buildMenuCall({ commandPrefix, menuCommandName, args: 'todos' })}`, '', `🌐 ${MENU_SITE_URL}`);
|
|
334
|
+
return lines.join('\n');
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const buildTopMenuText = ({ commandPrefix = '/', menuCommandName = DEFAULT_MENU_COMMAND_NAME, commands = [], usageMap = new Map() } = {}) => {
|
|
338
|
+
const ranked = rankCommands(commands, usageMap).slice(0, MENU_TOP_LIMIT);
|
|
339
|
+
const hasUsage = ranked.some((command) => (usageMap.get(command.name) || 0) > 0);
|
|
340
|
+
const lines = [hasUsage ? `🏆 *Top comandos (${MENU_USAGE_WINDOW_DAYS} dias)*` : '🏆 *Destaques de comandos*', ''];
|
|
341
|
+
|
|
342
|
+
for (const command of ranked) {
|
|
343
|
+
lines.push(`• ${commandPrefix}${command.name}${formatUsageSuffix(command.name, usageMap)} — ${shortText(command.descricao, 58)}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!ranked.length) {
|
|
347
|
+
lines.push('Nenhum comando disponível no catálogo no momento.');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
lines.push('', `Use ${buildMenuCall({ commandPrefix, menuCommandName, args: 'categoria <nome>' })} para abrir uma categoria.`);
|
|
351
|
+
lines.push(`🌐 ${MENU_SITE_URL}`);
|
|
352
|
+
return lines.join('\n');
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const buildCategoryMenuText = ({ commandPrefix = '/', menuCommandName = DEFAULT_MENU_COMMAND_NAME, category = null, usageMap = new Map() } = {}) => {
|
|
356
|
+
if (!category) return null;
|
|
357
|
+
|
|
358
|
+
const ranked = rankCommands(category.commands, usageMap);
|
|
359
|
+
const visible = ranked.slice(0, MENU_CATEGORY_LIMIT);
|
|
360
|
+
const hiddenCount = Math.max(0, ranked.length - visible.length);
|
|
361
|
+
const lines = [`📂 *Categoria: ${category.label}*`, `Total: *${ranked.length}* comando(s)`, ''];
|
|
362
|
+
|
|
363
|
+
for (const command of visible) {
|
|
364
|
+
lines.push(`• ${commandPrefix}${command.name}${formatUsageSuffix(command.name, usageMap)} — ${shortText(command.descricao, 60)}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (hiddenCount > 0) {
|
|
368
|
+
lines.push('', `... e mais *${hiddenCount}* comando(s) nesta categoria.`);
|
|
369
|
+
lines.push(`Use ${buildMenuCall({ commandPrefix, menuCommandName, args: 'todos' })} para ver a lista completa.`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
lines.push('', `🌐 ${MENU_SITE_URL}`);
|
|
373
|
+
return lines.join('\n');
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const buildAllMenuText = ({ commandPrefix = '/', categories = [] } = {}) => {
|
|
377
|
+
const sortedCategories = [...categories].sort((left, right) => right.commands.length - left.commands.length);
|
|
378
|
+
const totalCommands = sortedCategories.reduce((acc, category) => acc + category.commands.length, 0);
|
|
379
|
+
const lines = ['📚 *Catálogo completo de comandos*', `Categorias: *${sortedCategories.length}* | Comandos: *${totalCommands}*`, ''];
|
|
380
|
+
|
|
381
|
+
for (const category of sortedCategories) {
|
|
382
|
+
lines.push(`📁 *${category.label}* (${category.commands.length})`);
|
|
383
|
+
const commandNames = category.commands.map((command) => `${commandPrefix}${command.name}`);
|
|
384
|
+
for (const lineChunk of chunk(commandNames, 8)) {
|
|
385
|
+
lines.push(lineChunk.join(' • '));
|
|
386
|
+
}
|
|
387
|
+
lines.push('');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
lines.push(`🌐 ${MENU_SITE_URL}`);
|
|
391
|
+
return lines.join('\n').trim();
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const buildUnknownCategoryText = ({ commandPrefix = '/', menuCommandName = DEFAULT_MENU_COMMAND_NAME, query = '', categories = [] } = {}) => {
|
|
395
|
+
const quickCategories = [...categories].sort((left, right) => right.commands.length - left.commands.length).slice(0, MENU_CATEGORY_PREVIEW_LIMIT);
|
|
396
|
+
const lines = [`Não encontrei a categoria *${query || 'informada'}*.`, '', '*Tente uma destas:*'];
|
|
397
|
+
|
|
398
|
+
for (const category of quickCategories) {
|
|
399
|
+
lines.push(`• ${buildMenuCall({ commandPrefix, menuCommandName, args: category.key })}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
lines.push('', `Ou acesse: ${MENU_SITE_URL}`);
|
|
403
|
+
return lines.join('\n');
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
export const buildDynamicMenuText = ({ catalogSnapshot = null, usageRows = [], args = [], senderName = '', commandPrefix = '/', menuCommandName = DEFAULT_MENU_COMMAND_NAME, categoryScopeKeys = [] } = {}) => {
|
|
407
|
+
const baseModel = extractCatalogModel(catalogSnapshot);
|
|
408
|
+
const { categories, commands } = applyCategoryScope(baseModel.categories, baseModel.commands, categoryScopeKeys);
|
|
409
|
+
if (!categories.length || !commands.length) return null;
|
|
410
|
+
|
|
411
|
+
const usageMap = buildUsageMap(usageRows);
|
|
412
|
+
const safeArgs = Array.isArray(args) ? args.map((value) => String(value || '').trim()).filter(Boolean) : [];
|
|
413
|
+
const firstToken = normalizeLookupToken(safeArgs[0] || '');
|
|
414
|
+
const safeMenuCommandName = normalizeMenuCommandName(menuCommandName);
|
|
415
|
+
|
|
416
|
+
if (!firstToken) {
|
|
417
|
+
return buildMainMenuText({
|
|
418
|
+
senderName,
|
|
419
|
+
commandPrefix,
|
|
420
|
+
menuCommandName: safeMenuCommandName,
|
|
421
|
+
categories,
|
|
422
|
+
commands,
|
|
423
|
+
usageMap,
|
|
424
|
+
catalogSnapshot,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (TOP_TOKENS.has(firstToken)) {
|
|
429
|
+
return buildTopMenuText({
|
|
430
|
+
commandPrefix,
|
|
431
|
+
menuCommandName: safeMenuCommandName,
|
|
432
|
+
commands,
|
|
433
|
+
usageMap,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (ALL_TOKENS.has(firstToken)) {
|
|
438
|
+
return buildAllMenuText({
|
|
439
|
+
commandPrefix,
|
|
440
|
+
categories,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const isCategorySelector = CATEGORY_TOKENS.has(firstToken);
|
|
445
|
+
const query = isCategorySelector ? safeArgs.slice(1).join(' ') : safeArgs.join(' ');
|
|
446
|
+
|
|
447
|
+
if (isCategorySelector && !query) {
|
|
448
|
+
return [`Use ${buildMenuCall({ commandPrefix, menuCommandName: safeMenuCommandName, args: 'categoria <nome>' })}.`, '', `Exemplo: ${buildMenuCall({ commandPrefix, menuCommandName: safeMenuCommandName, args: 'categoria admin' })}`, `🌐 ${MENU_SITE_URL}`].join('\n');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const category = resolveCategoryFromQuery(categories, query);
|
|
452
|
+
if (!category) {
|
|
453
|
+
return buildUnknownCategoryText({
|
|
454
|
+
commandPrefix,
|
|
455
|
+
menuCommandName: safeMenuCommandName,
|
|
456
|
+
query,
|
|
457
|
+
categories,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return buildCategoryMenuText({
|
|
462
|
+
commandPrefix,
|
|
463
|
+
menuCommandName: safeMenuCommandName,
|
|
464
|
+
category,
|
|
465
|
+
usageMap,
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
export const resolveDynamicMenuText = async ({ args = [], senderName = '', commandPrefix = '/', remoteJid = null, menuCommandName = DEFAULT_MENU_COMMAND_NAME, categoryScopeKeys = [] } = {}) => {
|
|
470
|
+
try {
|
|
471
|
+
const { listTopCommandsByUsage } = await import('./menuCommandUsageRepository.js');
|
|
472
|
+
const catalogSnapshot = await getCommandsCatalogSnapshot();
|
|
473
|
+
const [groupUsage, globalUsage] = await Promise.all([
|
|
474
|
+
listTopCommandsByUsage({
|
|
475
|
+
chatId: remoteJid || null,
|
|
476
|
+
days: MENU_USAGE_WINDOW_DAYS,
|
|
477
|
+
limit: 30,
|
|
478
|
+
}),
|
|
479
|
+
listTopCommandsByUsage({
|
|
480
|
+
days: MENU_USAGE_WINDOW_DAYS,
|
|
481
|
+
limit: 40,
|
|
482
|
+
}),
|
|
483
|
+
]);
|
|
484
|
+
|
|
485
|
+
const usageRows = mergeUsageRows(groupUsage, globalUsage);
|
|
486
|
+
return buildDynamicMenuText({
|
|
487
|
+
catalogSnapshot,
|
|
488
|
+
usageRows,
|
|
489
|
+
args,
|
|
490
|
+
senderName,
|
|
491
|
+
commandPrefix,
|
|
492
|
+
menuCommandName,
|
|
493
|
+
categoryScopeKeys,
|
|
494
|
+
});
|
|
495
|
+
} catch (error) {
|
|
496
|
+
logger.warn('Falha ao montar menu dinamico. Aplicando fallback para menu estatico.', {
|
|
497
|
+
action: 'menu_dynamic_build_failed',
|
|
498
|
+
error: sanitizeLogValue(error?.message) || 'unknown_error',
|
|
499
|
+
});
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
export const __menuDynamicInternals = {
|
|
505
|
+
normalizeLookupToken,
|
|
506
|
+
buildUsageMap,
|
|
507
|
+
rankCommands,
|
|
508
|
+
resolveCategoryFromQuery,
|
|
509
|
+
buildAllMenuText,
|
|
510
|
+
mergeUsageRows,
|
|
511
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { buildDynamicMenuText } from './menuDynamicService.js';
|
|
4
|
+
|
|
5
|
+
const buildCatalogFixture = () => {
|
|
6
|
+
const adminCommands = Array.from({ length: 12 }, (_, index) => ({
|
|
7
|
+
name: `admincmd${index + 1}`,
|
|
8
|
+
descricao: `Comando administrativo ${index + 1}`,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const mediaCommands = Array.from({ length: 10 }, (_, index) => ({
|
|
12
|
+
name: `mediacmd${index + 1}`,
|
|
13
|
+
descricao: `Comando de mídia ${index + 1}`,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const statsCommands = [
|
|
17
|
+
{ name: 'ranking', descricao: 'Mostra ranking do grupo' },
|
|
18
|
+
{ name: 'ping', descricao: 'Mostra latência do bot' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
catalog: {
|
|
23
|
+
generated_at: '2026-03-18T03:17:03.020Z',
|
|
24
|
+
categories: [
|
|
25
|
+
{
|
|
26
|
+
key: 'admin',
|
|
27
|
+
label: 'Moderação e Admin',
|
|
28
|
+
commands: adminCommands,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'midia',
|
|
32
|
+
label: 'Mídia',
|
|
33
|
+
commands: mediaCommands,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'stats',
|
|
37
|
+
label: 'Estatísticas',
|
|
38
|
+
commands: statsCommands,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
test('menu dinâmico principal destaca comandos mais usados quando há muitos comandos', () => {
|
|
46
|
+
const text = buildDynamicMenuText({
|
|
47
|
+
catalogSnapshot: buildCatalogFixture(),
|
|
48
|
+
usageRows: [
|
|
49
|
+
{ commandName: 'admincmd7', usageCount: 321 },
|
|
50
|
+
{ commandName: 'mediacmd3', usageCount: 230 },
|
|
51
|
+
{ commandName: 'ranking', usageCount: 120 },
|
|
52
|
+
],
|
|
53
|
+
args: [],
|
|
54
|
+
senderName: 'Equipe QA',
|
|
55
|
+
commandPrefix: '/',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
assert.match(text, /\*Menu dinâmico do OmniZap\*/);
|
|
59
|
+
assert.match(text, /Destaques por uso/);
|
|
60
|
+
assert.match(text, /\/admincmd7/);
|
|
61
|
+
assert.match(text, /\/menu top/);
|
|
62
|
+
assert.match(text, /https:\/\/omnizap\.shop\/comandos\//);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('menu dinâmico resolve categoria por alias e filtra comandos da categoria', () => {
|
|
66
|
+
const text = buildDynamicMenuText({
|
|
67
|
+
catalogSnapshot: buildCatalogFixture(),
|
|
68
|
+
usageRows: [{ commandName: 'admincmd2', usageCount: 88 }],
|
|
69
|
+
args: ['categoria', 'adm'],
|
|
70
|
+
senderName: 'Teste',
|
|
71
|
+
commandPrefix: '/',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.match(text, /Categoria: Moderação e Admin/);
|
|
75
|
+
assert.match(text, /\/admincmd2/);
|
|
76
|
+
assert.doesNotMatch(text, /\/mediacmd1/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('menu dinâmico informa quando categoria não existe', () => {
|
|
80
|
+
const text = buildDynamicMenuText({
|
|
81
|
+
catalogSnapshot: buildCatalogFixture(),
|
|
82
|
+
usageRows: [],
|
|
83
|
+
args: ['categoria', 'naoexiste'],
|
|
84
|
+
senderName: 'Teste',
|
|
85
|
+
commandPrefix: '/',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.match(text, /Não encontrei a categoria/);
|
|
89
|
+
assert.match(text, /\/menu admin/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('menu dinâmico todos lista categorias e comandos', () => {
|
|
93
|
+
const text = buildDynamicMenuText({
|
|
94
|
+
catalogSnapshot: buildCatalogFixture(),
|
|
95
|
+
usageRows: [],
|
|
96
|
+
args: ['todos'],
|
|
97
|
+
senderName: 'Teste',
|
|
98
|
+
commandPrefix: '/',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
assert.match(text, /Catálogo completo de comandos/);
|
|
102
|
+
assert.match(text, /Moderação e Admin/);
|
|
103
|
+
assert.match(text, /\/admincmd1/);
|
|
104
|
+
assert.match(text, /\/mediacmd1/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('menu dinâmico aceita comando customizado e escopo de categorias', () => {
|
|
108
|
+
const mainText = buildDynamicMenuText({
|
|
109
|
+
catalogSnapshot: buildCatalogFixture(),
|
|
110
|
+
usageRows: [
|
|
111
|
+
{ commandName: 'mediacmd3', usageCount: 500 },
|
|
112
|
+
{ commandName: 'admincmd2', usageCount: 90 },
|
|
113
|
+
],
|
|
114
|
+
args: [],
|
|
115
|
+
senderName: 'Equipe Admin',
|
|
116
|
+
commandPrefix: '/',
|
|
117
|
+
menuCommandName: 'menuadm',
|
|
118
|
+
categoryScopeKeys: ['admin'],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
assert.match(mainText, /\/menuadm top/);
|
|
122
|
+
assert.doesNotMatch(mainText, /\/menu top/);
|
|
123
|
+
assert.doesNotMatch(mainText, /\/mediacmd3/);
|
|
124
|
+
|
|
125
|
+
const topText = buildDynamicMenuText({
|
|
126
|
+
catalogSnapshot: buildCatalogFixture(),
|
|
127
|
+
usageRows: [
|
|
128
|
+
{ commandName: 'mediacmd3', usageCount: 500 },
|
|
129
|
+
{ commandName: 'admincmd2', usageCount: 90 },
|
|
130
|
+
],
|
|
131
|
+
args: ['top'],
|
|
132
|
+
senderName: 'Equipe Admin',
|
|
133
|
+
commandPrefix: '/',
|
|
134
|
+
menuCommandName: 'menuadm',
|
|
135
|
+
categoryScopeKeys: ['admin'],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.match(topText, /\/admincmd2/);
|
|
139
|
+
assert.match(topText, /Use \/menuadm categoria <nome>/);
|
|
140
|
+
assert.doesNotMatch(topText, /\/mediacmd3/);
|
|
141
|
+
});
|