@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.
Files changed (156) hide show
  1. package/.env.example +54 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -5,15 +5,16 @@ import { isUserAdmin, createGroup, acceptGroupInvite, getGroupInfo, getGroupRequ
5
5
  import groupConfigStore from '../../store/groupConfigStore.js';
6
6
  import premiumUserStore from '../../store/premiumUserStore.js';
7
7
  import logger from '#logger';
8
- import { KNOWN_NETWORKS } from '../../utils/antiLink/antiLinkModule.js';
8
+ import { KNOWN_NETWORKS, purgeRecentMessagesForSenderCandidates } from '../../utils/antiLink/antiLinkModule.js';
9
9
  import { getNewsStatusForGroup, startNewsBroadcastForGroup, stopNewsBroadcastForGroup } from '../../services/messaging/newsBroadcastService.js';
10
10
  import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
11
11
  import { clearCaptchasForGroup } from '../../services/messaging/captchaService.js';
12
12
  import { getAdminJid, isAdminSenderAsync } from '../../config/index.js';
13
13
  import { extractUserIdInfo, resolveUserId } from '../../config/index.js';
14
- import { DEFAULT_STICKER_FOCUS_CHAT_WINDOW_MINUTES, DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES, MAX_STICKER_FOCUS_CHAT_WINDOW_MINUTES, MAX_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES, MIN_STICKER_FOCUS_CHAT_WINDOW_MINUTES, MIN_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES, clampStickerFocusChatWindowMinutes, clampStickerFocusMessageCooldownMinutes, resolveStickerFocusState } from '../../services/sticker/stickerFocusService.js';
14
+ import { DEFAULT_STICKER_FOCUS_CHAT_WINDOW_MINUTES, DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE, DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES, MAX_STICKER_FOCUS_CHAT_WINDOW_MINUTES, MAX_STICKER_FOCUS_MESSAGE_ALLOWANCE, MAX_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES, MIN_STICKER_FOCUS_CHAT_WINDOW_MINUTES, MIN_STICKER_FOCUS_MESSAGE_ALLOWANCE, MIN_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES, clampStickerFocusChatWindowMinutes, clampStickerFocusMessageAllowance, clampStickerFocusMessageCooldownMinutes, resolveStickerFocusState } from '../../services/sticker/stickerFocusService.js';
15
15
  import { getAdminTextConfig, getAdminUsageText, isAdminCommandName, resolveAdminCommandName } from './adminConfigRuntime.js';
16
16
  import { explicarComando, gerarFaqAutomatica, responderPergunta, startAdminAiHelpScheduler } from './adminAiHelpService.js';
17
+ import { addGroupWarning, clearGroupWarnings, countGroupWarnings, listGroupWarnings } from './groupWarningRepository.js';
17
18
  const OWNER_JID = getAdminJid();
18
19
  const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
19
20
  const ADMIN_TEXTS = getAdminTextConfig();
@@ -21,6 +22,11 @@ const GROUP_ONLY_COMMAND_MESSAGE = ADMIN_TEXTS.group_only_command_message;
21
22
  const NO_PERMISSION_COMMAND_MESSAGE = ADMIN_TEXTS.no_permission_command_message;
22
23
  const OWNER_ONLY_COMMAND_MESSAGE = ADMIN_TEXTS.owner_only_command_message;
23
24
  const USER_JID_SERVERS = new Set([...WHATSAPP_USER_JID_SERVERS, ...LID_USER_JID_SERVERS]);
25
+ const MAX_WARN_REASON_CHARS = 500;
26
+ const WARNINGS_LIST_PREVIEW_LIMIT = 8;
27
+ const DEFAULT_WARN_AUTO_BAN_THRESHOLD = 3;
28
+ const MIN_WARN_AUTO_BAN_THRESHOLD = 1;
29
+ const MAX_WARN_AUTO_BAN_THRESHOLD = 50;
24
30
  const normalizePhoneDigits = (value) => String(value || '').replace(/\D+/g, '');
25
31
 
26
32
  const LEGACY_ADMIN_ROUTE_BY_CANONICAL = {
@@ -106,6 +112,94 @@ const getParticipantJids = (messageInfo, args) => {
106
112
  return dedupeParticipantJids(args);
107
113
  };
108
114
 
115
+ const normalizeWarnReason = (value) =>
116
+ String(value || '')
117
+ .replace(/\s+/g, ' ')
118
+ .trim()
119
+ .slice(0, MAX_WARN_REASON_CHARS);
120
+
121
+ const formatUserMentionToken = (jid) => {
122
+ const userPart = String(jid || '')
123
+ .split('@')[0]
124
+ .trim();
125
+ return userPart ? `@${userPart}` : '@usuario';
126
+ };
127
+
128
+ const formatWarnTimestamp = (value) => {
129
+ if (!value) return 'data desconhecida';
130
+ const parsed = Date.parse(String(value));
131
+ if (!Number.isFinite(parsed)) return String(value);
132
+ return new Date(parsed).toLocaleString('pt-BR');
133
+ };
134
+
135
+ const resolveSingleTargetFromMessage = (messageInfo, args = []) => {
136
+ const safeArgs = Array.isArray(args) ? args.map((value) => String(value || '').trim()).filter(Boolean) : [];
137
+ const contextInfo = messageInfo?.message?.extendedTextMessage?.contextInfo || {};
138
+ const mentionedJids = dedupeParticipantJids(contextInfo?.mentionedJid || []);
139
+
140
+ if (mentionedJids.length > 1) {
141
+ return {
142
+ targetJid: '',
143
+ remainingArgs: safeArgs,
144
+ multipleTargets: true,
145
+ };
146
+ }
147
+
148
+ if (mentionedJids.length === 1) {
149
+ const targetJid = mentionedJids[0];
150
+ const remainingArgs = [...safeArgs];
151
+
152
+ while (remainingArgs.length > 0) {
153
+ const token = String(remainingArgs[0] || '').trim();
154
+ if (!token) {
155
+ remainingArgs.shift();
156
+ continue;
157
+ }
158
+
159
+ const normalizedToken = normalizeParticipantJid(token);
160
+ if (normalizedToken && (normalizedToken === targetJid || isSameJidUser(normalizedToken, targetJid))) {
161
+ remainingArgs.shift();
162
+ continue;
163
+ }
164
+ if (token.startsWith('@')) {
165
+ remainingArgs.shift();
166
+ continue;
167
+ }
168
+ break;
169
+ }
170
+
171
+ return {
172
+ targetJid,
173
+ remainingArgs,
174
+ multipleTargets: false,
175
+ };
176
+ }
177
+
178
+ const firstArgTarget = normalizeParticipantJid(safeArgs[0] || '');
179
+ if (firstArgTarget) {
180
+ return {
181
+ targetJid: firstArgTarget,
182
+ remainingArgs: safeArgs.slice(1),
183
+ multipleTargets: false,
184
+ };
185
+ }
186
+
187
+ const repliedTo = dedupeParticipantJids([contextInfo?.participant || '']);
188
+ if (repliedTo.length === 1) {
189
+ return {
190
+ targetJid: repliedTo[0],
191
+ remainingArgs: safeArgs,
192
+ multipleTargets: false,
193
+ };
194
+ }
195
+
196
+ return {
197
+ targetJid: '',
198
+ remainingArgs: safeArgs,
199
+ multipleTargets: false,
200
+ };
201
+ };
202
+
109
203
  const resolvePremiumTargetJid = async (targetJid) => {
110
204
  const normalizedTarget = normalizeParticipantJid(targetJid);
111
205
  if (!normalizedTarget) return '';
@@ -149,6 +243,12 @@ const parsePositiveInteger = (value) => {
149
243
  return normalized;
150
244
  };
151
245
 
246
+ const clampWarnAutoBanThreshold = (value) => {
247
+ const parsed = parsePositiveInteger(value);
248
+ if (!parsed) return DEFAULT_WARN_AUTO_BAN_THRESHOLD;
249
+ return Math.max(MIN_WARN_AUTO_BAN_THRESHOLD, Math.min(MAX_WARN_AUTO_BAN_THRESHOLD, parsed));
250
+ };
251
+
152
252
  const formatStickerFocusRule = ({ messageAllowanceCount, messageCooldownMinutes }) => {
153
253
  const allowanceCount = Math.max(1, Math.floor(Number(messageAllowanceCount) || 1));
154
254
  const cooldownMinutes = Math.max(1, Math.floor(Number(messageCooldownMinutes) || 1));
@@ -163,6 +263,96 @@ const buildStickerFocusStatusText = ({ state, commandPrefix }) => {
163
263
  return ['🖼️ *Status do modo Sticker*', '', `Modo sticker: *${state.enabled ? 'ativado' : 'desativado'}*`, `Janela de chat: *${chatWindowStatus}*`, `Regra fora da janela: *${formatStickerFocusRule(state)}*`, '', `Comandos:`, `${commandPrefix}stickermode <on|off|status>`, `${commandPrefix}chatwindow <on|off|status> [minutos]`, `${commandPrefix}stickermsglimit <minutos|status|reset>`].join('\n');
164
264
  };
165
265
 
266
+ const formatListForMessage = (items = [], emptyLabel = 'nenhum') => {
267
+ const safeItems = Array.isArray(items) ? items.map((item) => String(item || '').trim()).filter(Boolean) : [];
268
+ return safeItems.length ? safeItems.join(', ') : emptyLabel;
269
+ };
270
+
271
+ const parseFilterValues = (values = []) => {
272
+ const rawText = Array.isArray(values) ? values.join(' ') : String(values || '');
273
+ if (!rawText.trim()) return [];
274
+ const normalized = rawText
275
+ .split(/[\s,]+/)
276
+ .map((value) =>
277
+ String(value || '')
278
+ .trim()
279
+ .toLowerCase(),
280
+ )
281
+ .filter(Boolean);
282
+ return Array.from(new Set(normalized)).sort((left, right) => left.localeCompare(right));
283
+ };
284
+
285
+ const normalizeStringList = (value) => {
286
+ const source = Array.isArray(value) ? value : typeof value === 'string' ? value.split(',') : [];
287
+ return source
288
+ .map((entry) =>
289
+ String(entry || '')
290
+ .trim()
291
+ .toLowerCase(),
292
+ )
293
+ .filter(Boolean);
294
+ };
295
+
296
+ const normalizeNewsFilterState = (groupConfig = {}) => {
297
+ const nested = groupConfig?.newsFilters && typeof groupConfig.newsFilters === 'object' ? groupConfig.newsFilters : {};
298
+ const sourceIds = Array.from(new Set([...normalizeStringList(groupConfig.newsSources), ...normalizeStringList(groupConfig.newsSourceIds), ...normalizeStringList(nested.sources), ...normalizeStringList(nested.sourceIds)])).sort((left, right) => left.localeCompare(right));
299
+ const franchiseSlugs = Array.from(new Set([...normalizeStringList(groupConfig.newsFranchises), ...normalizeStringList(groupConfig.newsFranchiseSlugs), ...normalizeStringList(nested.franchises), ...normalizeStringList(nested.franchiseSlugs)])).sort((left, right) => left.localeCompare(right));
300
+ const entitySlugs = Array.from(new Set([...normalizeStringList(groupConfig.newsEntities), ...normalizeStringList(groupConfig.newsEntitySlugs), ...normalizeStringList(groupConfig.newsTags), ...normalizeStringList(nested.entities), ...normalizeStringList(nested.entitySlugs), ...normalizeStringList(nested.tags)])).sort((left, right) => left.localeCompare(right));
301
+ const onlyTrending = Boolean(groupConfig.newsOnlyTrending || nested.onlyTrending);
302
+
303
+ return {
304
+ sourceIds,
305
+ franchiseSlugs,
306
+ entitySlugs,
307
+ onlyTrending,
308
+ };
309
+ };
310
+
311
+ const buildNewsFilterConfigPatch = (groupConfig, nextState) => {
312
+ const nested = groupConfig?.newsFilters && typeof groupConfig.newsFilters === 'object' ? groupConfig.newsFilters : {};
313
+ const safeSources = Array.isArray(nextState?.sourceIds) ? nextState.sourceIds : [];
314
+ const safeFranchises = Array.isArray(nextState?.franchiseSlugs) ? nextState.franchiseSlugs : [];
315
+ const safeEntities = Array.isArray(nextState?.entitySlugs) ? nextState.entitySlugs : [];
316
+ const safeOnlyTrending = Boolean(nextState?.onlyTrending);
317
+
318
+ return {
319
+ newsSourceIds: safeSources,
320
+ newsSources: safeSources,
321
+ newsFranchiseSlugs: safeFranchises,
322
+ newsFranchises: safeFranchises,
323
+ newsEntitySlugs: safeEntities,
324
+ newsEntities: safeEntities,
325
+ newsTags: safeEntities,
326
+ newsOnlyTrending: safeOnlyTrending,
327
+ newsFilters: {
328
+ ...nested,
329
+ sourceIds: safeSources,
330
+ sources: safeSources,
331
+ franchiseSlugs: safeFranchises,
332
+ franchises: safeFranchises,
333
+ entitySlugs: safeEntities,
334
+ entities: safeEntities,
335
+ tags: safeEntities,
336
+ onlyTrending: safeOnlyTrending,
337
+ },
338
+ };
339
+ };
340
+
341
+ const formatDateTimeLabel = (value) => {
342
+ if (!value) return 'nunca';
343
+ const parsed = Date.parse(String(value));
344
+ if (!Number.isFinite(parsed)) return String(value);
345
+ return new Date(parsed).toLocaleString('pt-BR');
346
+ };
347
+
348
+ const buildGroupAuditText = ({ config, stickerState, newsStatus, commandPrefix }) => {
349
+ const prefix = String(config?.commandPrefix || '').trim() || DEFAULT_COMMAND_PREFIX;
350
+ const newsFilters = normalizeNewsFilterState(config);
351
+ const stickerWindowText = stickerState.isChatWindowOpen ? `aberta (~${Math.max(1, Math.ceil(stickerState.chatWindowRemainingMs / (60 * 1000)))} min restantes)` : 'fechada';
352
+
353
+ return ['🧾 *Auditoria do Grupo*', '', `Prefixo: *${prefix}*`, `NSFW: *${config?.nsfwEnabled ? 'ativado' : 'desativado'}*`, `AutoSticker: *${config?.autoStickerEnabled ? 'ativado' : 'desativado'}*`, `Modo Sticker: *${stickerState.enabled ? 'ativado' : 'desativado'}*`, `Janela de chat: *${stickerWindowText}*`, `Regra de texto: *${formatStickerFocusRule(stickerState)}*`, `Captcha: *${config?.captchaEnabled ? 'ativado' : 'desativado'}*`, `Auto-aprovação de solicitações: *${config?.autoApproveRequestsEnabled ? 'ativada' : 'desativada'}*`, `Antilink: *${config?.antilinkEnabled ? 'ativado' : 'desativado'}*`, `Antilink redes permitidas: ${formatListForMessage(config?.antilinkAllowedNetworks || [])}`, `Antilink domínios permitidos: ${formatListForMessage(config?.antilinkAllowedDomains || [])}`, `Notícias: *${newsStatus?.enabled ? 'ativado' : 'desativado'}*`, `Notícias enviadas: *${Number(newsStatus?.sentCount || 0)}*`, `Último envio de notícias: *${formatDateTimeLabel(newsStatus?.lastSentAt)}*`, `Filtro notícias [source]: ${formatListForMessage(newsFilters.sourceIds)}`, `Filtro notícias [franchise]: ${formatListForMessage(newsFilters.franchiseSlugs)}`, `Filtro notícias [tag/entity]: ${formatListForMessage(newsFilters.entitySlugs)}`, `Filtro notícias [somente em alta]: *${newsFilters.onlyTrending ? 'sim' : 'não'}*`, `Boas-vindas: *${config?.welcomeMessageEnabled ? 'ativadas' : 'desativadas'}*`, `Despedida: *${config?.farewellMessageEnabled ? 'ativadas' : 'desativadas'}*`, '', `Dica: use *${commandPrefix}noticiasfiltro status* para ver os filtros em detalhe.`].join('\n');
354
+ };
355
+
166
356
  export const isAdminCommand = (command) => isAdminCommandName(command);
167
357
 
168
358
  export async function handleAdminCommand({ command, args, text, sock, messageInfo, remoteJid, senderJid, botJid, isGroupMessage, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
@@ -194,7 +384,7 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
194
384
  const isOwner = Boolean(OWNER_JID && (await isAdminSenderAsync(senderIdentity)));
195
385
 
196
386
  if (!subAction) {
197
- await handleMenuAdmCommand(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
387
+ await handleMenuAdmCommand(sock, remoteJid, messageInfo, expirationMessage, commandPrefix, []);
198
388
  break;
199
389
  }
200
390
 
@@ -265,14 +455,7 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
265
455
  break;
266
456
  }
267
457
 
268
- await sendAndStore(
269
- sock,
270
- remoteJid,
271
- {
272
- text: getAdminUsageText('menuadm', { commandPrefix, variant: 'default' }),
273
- },
274
- { quoted: messageInfo, ephemeralExpiration: expirationMessage },
275
- );
458
+ await handleMenuAdmCommand(sock, remoteJid, messageInfo, expirationMessage, commandPrefix, args);
276
459
  break;
277
460
  }
278
461
 
@@ -627,7 +810,6 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
627
810
  if (['reset', 'default', 'padrao', 'padrão'].includes(normalized)) {
628
811
  await groupConfigStore.updateGroupConfig(remoteJid, {
629
812
  stickerFocusMessageCooldownMinutes: DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES,
630
- // compatibilidade com configuração antiga
631
813
  stickerFocusTextCooldownMinutes: DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES,
632
814
  });
633
815
  await sendAndStore(
@@ -669,7 +851,6 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
669
851
  const minutes = clampStickerFocusMessageCooldownMinutes(parsed, DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES);
670
852
  await groupConfigStore.updateGroupConfig(remoteJid, {
671
853
  stickerFocusMessageCooldownMinutes: minutes,
672
- // compatibilidade com configuração antiga
673
854
  stickerFocusTextCooldownMinutes: minutes,
674
855
  });
675
856
 
@@ -684,6 +865,93 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
684
865
  break;
685
866
  }
686
867
 
868
+ case 'stickerallowance': {
869
+ if (!isGroupMessage) {
870
+ await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
871
+ break;
872
+ }
873
+ if (!(await isUserAdmin(remoteJid, senderIdentity))) {
874
+ await sendAndStore(sock, remoteJid, { text: NO_PERMISSION_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
875
+ break;
876
+ }
877
+
878
+ const rawValue = args[0];
879
+ const normalized = String(rawValue || '')
880
+ .trim()
881
+ .toLowerCase();
882
+
883
+ if (!normalized || normalized === 'status') {
884
+ const config = await groupConfigStore.getGroupConfig(remoteJid);
885
+ const state = resolveStickerFocusState(config);
886
+ await sendAndStore(
887
+ sock,
888
+ remoteJid,
889
+ {
890
+ text: `📏 Limite atual de mensagens por usuário no modo sticker: *${state.messageAllowanceCount}*.` + `\nRegra vigente: *${formatStickerFocusRule(state)}*.` + `\nUse *${commandPrefix}stickerallowance <quantidade>* para alterar.`,
891
+ },
892
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
893
+ );
894
+ break;
895
+ }
896
+
897
+ if (['reset', 'default', 'padrao', 'padrão'].includes(normalized)) {
898
+ await groupConfigStore.updateGroupConfig(remoteJid, {
899
+ stickerFocusMessageAllowance: DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE,
900
+ stickerFocusMessageAllowanceCount: DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE,
901
+ });
902
+ await sendAndStore(
903
+ sock,
904
+ remoteJid,
905
+ {
906
+ text: `✅ Limite restaurado para o padrão: *${DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE}* mensagem(ns) por janela.`,
907
+ },
908
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
909
+ );
910
+ break;
911
+ }
912
+
913
+ const parsed = parsePositiveInteger(rawValue);
914
+ if (!parsed) {
915
+ await sendAndStore(
916
+ sock,
917
+ remoteJid,
918
+ {
919
+ text: getAdminUsageText('stickerallowance', { commandPrefix }),
920
+ },
921
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
922
+ );
923
+ break;
924
+ }
925
+
926
+ if (parsed < MIN_STICKER_FOCUS_MESSAGE_ALLOWANCE || parsed > MAX_STICKER_FOCUS_MESSAGE_ALLOWANCE) {
927
+ await sendAndStore(
928
+ sock,
929
+ remoteJid,
930
+ {
931
+ text: `Informe uma quantidade entre ${MIN_STICKER_FOCUS_MESSAGE_ALLOWANCE} e ${MAX_STICKER_FOCUS_MESSAGE_ALLOWANCE}.`,
932
+ },
933
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
934
+ );
935
+ break;
936
+ }
937
+
938
+ const allowance = clampStickerFocusMessageAllowance(parsed, DEFAULT_STICKER_FOCUS_MESSAGE_ALLOWANCE);
939
+ await groupConfigStore.updateGroupConfig(remoteJid, {
940
+ stickerFocusMessageAllowance: allowance,
941
+ stickerFocusMessageAllowanceCount: allowance,
942
+ });
943
+
944
+ await sendAndStore(
945
+ sock,
946
+ remoteJid,
947
+ {
948
+ text: `✅ Limite de mensagens por usuário atualizado para *${allowance}* no modo sticker.`,
949
+ },
950
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
951
+ );
952
+ break;
953
+ }
954
+
687
955
  case 'newgroup': {
688
956
  if (args.length < 2) {
689
957
  await sendAndStore(
@@ -766,7 +1034,33 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
766
1034
  }
767
1035
  try {
768
1036
  await updateGroupParticipants(sock, remoteJid, participants, 'remove');
769
- await sendAndStore(sock, remoteJid, { text: 'Participantes removidos com sucesso.' }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1037
+
1038
+ let deletedRecentMessages = 0;
1039
+ let failedRecentDeletes = 0;
1040
+ let requestedRecentDeletes = 0;
1041
+ for (const participant of participants) {
1042
+ const cleanupResult = await purgeRecentMessagesForSenderCandidates({
1043
+ sock,
1044
+ remoteJid,
1045
+ senderCandidates: [participant],
1046
+ });
1047
+ deletedRecentMessages += Number(cleanupResult?.deleted || 0);
1048
+ failedRecentDeletes += Number(cleanupResult?.failed || 0);
1049
+ requestedRecentDeletes += Number(cleanupResult?.requested || 0);
1050
+ }
1051
+
1052
+ const successText = deletedRecentMessages > 0 ? `Participantes removidos com sucesso.\n🧹 ${deletedRecentMessages} mensagem(ns) recentes apagadas.` : 'Participantes removidos com sucesso.';
1053
+ await sendAndStore(sock, remoteJid, { text: successText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1054
+
1055
+ logger.info('Comando ban executado com limpeza de mensagens recentes.', {
1056
+ action: 'admin_ban_with_recent_cleanup',
1057
+ groupId: remoteJid,
1058
+ participants,
1059
+ deletedRecentMessages,
1060
+ failedRecentDeletes,
1061
+ requestedRecentDeletes,
1062
+ });
1063
+
770
1064
  const repliedTo = messageInfo.message?.extendedTextMessage?.contextInfo;
771
1065
  if (repliedTo && containsParticipantJid(participants, repliedTo.participant)) {
772
1066
  await sendAndStore(sock, remoteJid, {
@@ -779,6 +1073,329 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
779
1073
  break;
780
1074
  }
781
1075
 
1076
+ case 'warn': {
1077
+ if (!isGroupMessage) {
1078
+ await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1079
+ break;
1080
+ }
1081
+ if (!(await isUserAdmin(remoteJid, senderIdentity))) {
1082
+ await sendAndStore(sock, remoteJid, { text: NO_PERMISSION_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1083
+ break;
1084
+ }
1085
+
1086
+ const { targetJid, remainingArgs, multipleTargets } = resolveSingleTargetFromMessage(messageInfo, args);
1087
+ if (multipleTargets || !targetJid) {
1088
+ await sendAndStore(
1089
+ sock,
1090
+ remoteJid,
1091
+ {
1092
+ text: getAdminUsageText('warn', { commandPrefix, variant: 'missing_targets' }),
1093
+ },
1094
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1095
+ );
1096
+ break;
1097
+ }
1098
+
1099
+ const reason = normalizeWarnReason(remainingArgs.join(' '));
1100
+ const targetMention = formatUserMentionToken(targetJid);
1101
+
1102
+ try {
1103
+ await addGroupWarning({
1104
+ groupId: remoteJid,
1105
+ participantJid: targetJid,
1106
+ warnedByJid: senderJid,
1107
+ reason: reason || null,
1108
+ });
1109
+
1110
+ const totalWarnings = await countGroupWarnings({
1111
+ groupId: remoteJid,
1112
+ participantJid: targetJid,
1113
+ });
1114
+
1115
+ const groupConfig = await groupConfigStore.getGroupConfig(remoteJid);
1116
+ const warnAutoBanThreshold = clampWarnAutoBanThreshold(groupConfig?.warnAutoBanThreshold);
1117
+ const shouldAutoBan = totalWarnings >= warnAutoBanThreshold && !containsParticipantJid([targetJid], botJid);
1118
+ let autoBanSucceeded = false;
1119
+ let autoBanError = '';
1120
+
1121
+ if (shouldAutoBan) {
1122
+ try {
1123
+ await updateGroupParticipants(sock, remoteJid, [targetJid], 'remove');
1124
+ autoBanSucceeded = true;
1125
+ } catch (error) {
1126
+ autoBanError = error?.message || 'falha desconhecida';
1127
+ logger.warn('Falha ao aplicar auto-ban por limite de advertências.', {
1128
+ action: 'admin_warn_auto_ban_failed',
1129
+ groupId: remoteJid,
1130
+ targetJid,
1131
+ warnAutoBanThreshold,
1132
+ totalWarnings,
1133
+ error: autoBanError,
1134
+ });
1135
+ }
1136
+ }
1137
+
1138
+ const replyLines = [`⚠️ Advertência registrada para ${targetMention}.`, `Motivo: ${reason || 'não informado'}`, `Total de advertências neste grupo: *${totalWarnings}*.`, `Auto-ban configurado para: *${warnAutoBanThreshold}* advertência(s).`];
1139
+ if (autoBanSucceeded) {
1140
+ replyLines.push(`🚫 Limite atingido: ${targetMention} foi removido(a) automaticamente do grupo.`);
1141
+ } else if (autoBanError) {
1142
+ replyLines.push(`⚠️ Limite atingido, mas não consegui remover automaticamente: ${autoBanError}`);
1143
+ }
1144
+
1145
+ await sendAndStore(
1146
+ sock,
1147
+ remoteJid,
1148
+ {
1149
+ text: replyLines.join('\n'),
1150
+ mentions: [targetJid],
1151
+ },
1152
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1153
+ );
1154
+ } catch (error) {
1155
+ logger.warn('Falha ao registrar advertência no grupo.', {
1156
+ action: 'admin_warn_failed',
1157
+ groupId: remoteJid,
1158
+ senderJid,
1159
+ targetJid,
1160
+ error: error?.message,
1161
+ });
1162
+ await sendAndStore(sock, remoteJid, { text: `Não foi possível registrar a advertência. Detalhes: ${error.message}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1163
+ }
1164
+ break;
1165
+ }
1166
+
1167
+ case 'warnings': {
1168
+ if (!isGroupMessage) {
1169
+ await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1170
+ break;
1171
+ }
1172
+ if (!(await isUserAdmin(remoteJid, senderIdentity))) {
1173
+ await sendAndStore(sock, remoteJid, { text: NO_PERMISSION_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1174
+ break;
1175
+ }
1176
+
1177
+ const { targetJid, multipleTargets } = resolveSingleTargetFromMessage(messageInfo, args);
1178
+ if (multipleTargets || !targetJid) {
1179
+ await sendAndStore(
1180
+ sock,
1181
+ remoteJid,
1182
+ {
1183
+ text: getAdminUsageText('warnings', { commandPrefix, variant: 'missing_targets' }),
1184
+ },
1185
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1186
+ );
1187
+ break;
1188
+ }
1189
+
1190
+ const targetMention = formatUserMentionToken(targetJid);
1191
+
1192
+ try {
1193
+ const [totalWarnings, warningRows] = await Promise.all([
1194
+ countGroupWarnings({
1195
+ groupId: remoteJid,
1196
+ participantJid: targetJid,
1197
+ }),
1198
+ listGroupWarnings({
1199
+ groupId: remoteJid,
1200
+ participantJid: targetJid,
1201
+ limit: WARNINGS_LIST_PREVIEW_LIMIT,
1202
+ }),
1203
+ ]);
1204
+
1205
+ if (totalWarnings <= 0) {
1206
+ await sendAndStore(
1207
+ sock,
1208
+ remoteJid,
1209
+ {
1210
+ text: `✅ ${targetMention} não possui advertências registradas neste grupo.`,
1211
+ mentions: [targetJid],
1212
+ },
1213
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1214
+ );
1215
+ break;
1216
+ }
1217
+
1218
+ const historyLines = (warningRows || []).map((warningRow, index) => `${index + 1}. ${formatWarnTimestamp(warningRow?.createdAt)} - ${warningRow?.reason || 'Sem motivo informado'}`);
1219
+ if (totalWarnings > historyLines.length) {
1220
+ historyLines.push(`... e mais ${totalWarnings - historyLines.length} advertência(s).`);
1221
+ }
1222
+
1223
+ await sendAndStore(
1224
+ sock,
1225
+ remoteJid,
1226
+ {
1227
+ text: [`📋 Histórico de advertências de ${targetMention}`, `Total neste grupo: *${totalWarnings}*`, '', ...historyLines].join('\n'),
1228
+ mentions: [targetJid],
1229
+ },
1230
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1231
+ );
1232
+ } catch (error) {
1233
+ logger.warn('Falha ao consultar histórico de advertências no grupo.', {
1234
+ action: 'admin_warnings_failed',
1235
+ groupId: remoteJid,
1236
+ senderJid,
1237
+ targetJid,
1238
+ error: error?.message,
1239
+ });
1240
+ await sendAndStore(sock, remoteJid, { text: `Não foi possível consultar as advertências. Detalhes: ${error.message}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1241
+ }
1242
+ break;
1243
+ }
1244
+
1245
+ case 'clearwarn': {
1246
+ if (!isGroupMessage) {
1247
+ await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1248
+ break;
1249
+ }
1250
+ if (!(await isUserAdmin(remoteJid, senderIdentity))) {
1251
+ await sendAndStore(sock, remoteJid, { text: NO_PERMISSION_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1252
+ break;
1253
+ }
1254
+
1255
+ const { targetJid, remainingArgs, multipleTargets } = resolveSingleTargetFromMessage(messageInfo, args);
1256
+ if (multipleTargets || !targetJid) {
1257
+ await sendAndStore(
1258
+ sock,
1259
+ remoteJid,
1260
+ {
1261
+ text: getAdminUsageText('clearwarn', { commandPrefix, variant: 'missing_targets' }),
1262
+ },
1263
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1264
+ );
1265
+ break;
1266
+ }
1267
+
1268
+ const rawScope = String(remainingArgs?.[0] || '')
1269
+ .trim()
1270
+ .toLowerCase();
1271
+ const clearAll = rawScope === 'all';
1272
+ const amountToClear = !rawScope || clearAll ? 1 : parsePositiveInteger(rawScope);
1273
+
1274
+ if (!clearAll && rawScope && !amountToClear) {
1275
+ await sendAndStore(
1276
+ sock,
1277
+ remoteJid,
1278
+ {
1279
+ text: getAdminUsageText('clearwarn', { commandPrefix, variant: 'invalid_amount' }),
1280
+ },
1281
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1282
+ );
1283
+ break;
1284
+ }
1285
+
1286
+ try {
1287
+ const clearResult = await clearGroupWarnings({
1288
+ groupId: remoteJid,
1289
+ participantJid: targetJid,
1290
+ clearAll,
1291
+ limit: amountToClear || 1,
1292
+ });
1293
+
1294
+ const targetMention = formatUserMentionToken(targetJid);
1295
+ if (clearResult.removedCount <= 0) {
1296
+ await sendAndStore(
1297
+ sock,
1298
+ remoteJid,
1299
+ {
1300
+ text: `ℹ️ ${targetMention} não possui advertências para remover neste grupo.`,
1301
+ mentions: [targetJid],
1302
+ },
1303
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1304
+ );
1305
+ break;
1306
+ }
1307
+
1308
+ const removedLabel = clearAll ? `todas as advertências (${clearResult.removedCount})` : `${clearResult.removedCount} advertência(s)`;
1309
+ await sendAndStore(
1310
+ sock,
1311
+ remoteJid,
1312
+ {
1313
+ text: `🧹 Limpeza concluída para ${targetMention}: removi *${removedLabel}*.\nAdvertências restantes neste grupo: *${clearResult.remainingCount}*.`,
1314
+ mentions: [targetJid],
1315
+ },
1316
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1317
+ );
1318
+ } catch (error) {
1319
+ logger.warn('Falha ao limpar advertências no grupo.', {
1320
+ action: 'admin_clearwarn_failed',
1321
+ groupId: remoteJid,
1322
+ senderJid,
1323
+ targetJid,
1324
+ clearAll,
1325
+ error: error?.message,
1326
+ });
1327
+ await sendAndStore(sock, remoteJid, { text: `Não foi possível limpar advertências. Detalhes: ${error.message}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1328
+ }
1329
+ break;
1330
+ }
1331
+
1332
+ case 'warnlimit': {
1333
+ if (!isGroupMessage) {
1334
+ await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1335
+ break;
1336
+ }
1337
+ if (!(await isUserAdmin(remoteJid, senderIdentity))) {
1338
+ await sendAndStore(sock, remoteJid, { text: NO_PERMISSION_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1339
+ break;
1340
+ }
1341
+
1342
+ const action = String(args?.[0] || '')
1343
+ .trim()
1344
+ .toLowerCase();
1345
+ const groupConfig = await groupConfigStore.getGroupConfig(remoteJid);
1346
+ const currentThreshold = clampWarnAutoBanThreshold(groupConfig?.warnAutoBanThreshold);
1347
+
1348
+ if (!action || action === 'status') {
1349
+ await sendAndStore(
1350
+ sock,
1351
+ remoteJid,
1352
+ {
1353
+ text: `⚖️ Limite atual de auto-ban por advertências: *${currentThreshold}*.\nUse ${commandPrefix}warnlimit <qtd> para atualizar ou ${commandPrefix}warnlimit reset para voltar ao padrão.`,
1354
+ },
1355
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1356
+ );
1357
+ break;
1358
+ }
1359
+
1360
+ if (action === 'reset') {
1361
+ await groupConfigStore.updateGroupConfig(remoteJid, { warnAutoBanThreshold: null });
1362
+ await sendAndStore(
1363
+ sock,
1364
+ remoteJid,
1365
+ {
1366
+ text: `✅ Limite de auto-ban resetado para o padrão: *${DEFAULT_WARN_AUTO_BAN_THRESHOLD}* advertência(s).`,
1367
+ },
1368
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1369
+ );
1370
+ break;
1371
+ }
1372
+
1373
+ const parsedThreshold = parsePositiveInteger(action);
1374
+ if (!parsedThreshold) {
1375
+ await sendAndStore(
1376
+ sock,
1377
+ remoteJid,
1378
+ {
1379
+ text: getAdminUsageText('warnlimit', { commandPrefix, variant: 'invalid_value' }),
1380
+ },
1381
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1382
+ );
1383
+ break;
1384
+ }
1385
+
1386
+ const nextThreshold = Math.max(MIN_WARN_AUTO_BAN_THRESHOLD, Math.min(MAX_WARN_AUTO_BAN_THRESHOLD, parsedThreshold));
1387
+ await groupConfigStore.updateGroupConfig(remoteJid, { warnAutoBanThreshold: nextThreshold });
1388
+ await sendAndStore(
1389
+ sock,
1390
+ remoteJid,
1391
+ {
1392
+ text: `✅ Limite de auto-ban atualizado para *${nextThreshold}* advertência(s).`,
1393
+ },
1394
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1395
+ );
1396
+ break;
1397
+ }
1398
+
782
1399
  case 'up': {
783
1400
  if (!isGroupMessage) {
784
1401
  await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
@@ -1759,6 +2376,231 @@ ${JSON.stringify(response, null, 2)}`,
1759
2376
  break;
1760
2377
  }
1761
2378
 
2379
+ case 'noticiasfiltro': {
2380
+ if (!isGroupMessage) {
2381
+ await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
2382
+ break;
2383
+ }
2384
+ if (!(await isUserAdmin(remoteJid, senderIdentity))) {
2385
+ await sendAndStore(sock, remoteJid, { text: NO_PERMISSION_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
2386
+ break;
2387
+ }
2388
+
2389
+ const groupConfig = await groupConfigStore.getGroupConfig(remoteJid);
2390
+ const currentFilters = normalizeNewsFilterState(groupConfig);
2391
+ const scope = String(args[0] || '')
2392
+ .trim()
2393
+ .toLowerCase();
2394
+ const scopeToKey = {
2395
+ source: 'sourceIds',
2396
+ franchise: 'franchiseSlugs',
2397
+ tag: 'entitySlugs',
2398
+ entity: 'entitySlugs',
2399
+ };
2400
+
2401
+ const buildStatusText = (filters) => ['🧪 *Filtros de notícias deste grupo*', '', `Sources: ${formatListForMessage(filters.sourceIds)}`, `Franchises: ${formatListForMessage(filters.franchiseSlugs)}`, `Tags/Entities: ${formatListForMessage(filters.entitySlugs)}`, `Somente em alta: *${filters.onlyTrending ? 'sim' : 'não'}*`, '', 'Exemplos:', `${commandPrefix}noticiasfiltro source add ann,mal`, `${commandPrefix}noticiasfiltro franchise add one-piece`, `${commandPrefix}noticiasfiltro tag add shounen`, `${commandPrefix}noticiasfiltro trending on`].join('\n');
2402
+
2403
+ if (!scope || scope === 'status' || scope === 'list') {
2404
+ await sendAndStore(
2405
+ sock,
2406
+ remoteJid,
2407
+ {
2408
+ text: buildStatusText(currentFilters),
2409
+ },
2410
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2411
+ );
2412
+ break;
2413
+ }
2414
+
2415
+ if (scope === 'reset') {
2416
+ const resetFilters = {
2417
+ sourceIds: [],
2418
+ franchiseSlugs: [],
2419
+ entitySlugs: [],
2420
+ onlyTrending: false,
2421
+ };
2422
+ await groupConfigStore.updateGroupConfig(remoteJid, buildNewsFilterConfigPatch(groupConfig, resetFilters));
2423
+ await sendAndStore(
2424
+ sock,
2425
+ remoteJid,
2426
+ {
2427
+ text: '✅ Filtros de notícias resetados com sucesso.',
2428
+ },
2429
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2430
+ );
2431
+ break;
2432
+ }
2433
+
2434
+ if (scope === 'trending') {
2435
+ const action = String(args[1] || '')
2436
+ .trim()
2437
+ .toLowerCase();
2438
+ if (!action || !['on', 'off', 'status'].includes(action)) {
2439
+ await sendAndStore(
2440
+ sock,
2441
+ remoteJid,
2442
+ {
2443
+ text: getAdminUsageText('noticiasfiltro', { commandPrefix, variant: 'trending' }),
2444
+ },
2445
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2446
+ );
2447
+ break;
2448
+ }
2449
+
2450
+ if (action === 'status') {
2451
+ await sendAndStore(
2452
+ sock,
2453
+ remoteJid,
2454
+ {
2455
+ text: `📈 Filtro "somente em alta" está *${currentFilters.onlyTrending ? 'ativado' : 'desativado'}*.`,
2456
+ },
2457
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2458
+ );
2459
+ break;
2460
+ }
2461
+
2462
+ const nextFilters = {
2463
+ ...currentFilters,
2464
+ onlyTrending: action === 'on',
2465
+ };
2466
+ await groupConfigStore.updateGroupConfig(remoteJid, buildNewsFilterConfigPatch(groupConfig, nextFilters));
2467
+ await sendAndStore(
2468
+ sock,
2469
+ remoteJid,
2470
+ {
2471
+ text: `✅ Filtro "somente em alta" ${nextFilters.onlyTrending ? 'ativado' : 'desativado'} com sucesso.`,
2472
+ },
2473
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2474
+ );
2475
+ break;
2476
+ }
2477
+
2478
+ const filterKey = scopeToKey[scope];
2479
+ if (!filterKey) {
2480
+ await sendAndStore(
2481
+ sock,
2482
+ remoteJid,
2483
+ {
2484
+ text: getAdminUsageText('noticiasfiltro', { commandPrefix, variant: 'invalid_scope' }),
2485
+ },
2486
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2487
+ );
2488
+ break;
2489
+ }
2490
+
2491
+ const action = String(args[1] || '')
2492
+ .trim()
2493
+ .toLowerCase();
2494
+ if (!action || !['add', 'remove', 'list', 'clear'].includes(action)) {
2495
+ await sendAndStore(
2496
+ sock,
2497
+ remoteJid,
2498
+ {
2499
+ text: getAdminUsageText('noticiasfiltro', { commandPrefix, variant: 'invalid_action' }),
2500
+ },
2501
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2502
+ );
2503
+ break;
2504
+ }
2505
+
2506
+ const scopeLabel = filterKey === 'sourceIds' ? 'sources' : filterKey === 'franchiseSlugs' ? 'franchises' : 'tags/entities';
2507
+
2508
+ if (action === 'list') {
2509
+ await sendAndStore(
2510
+ sock,
2511
+ remoteJid,
2512
+ {
2513
+ text: `📋 Filtro de notícias (${scopeLabel}): ${formatListForMessage(currentFilters[filterKey])}`,
2514
+ },
2515
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2516
+ );
2517
+ break;
2518
+ }
2519
+
2520
+ if (action === 'clear') {
2521
+ const nextFilters = {
2522
+ ...currentFilters,
2523
+ [filterKey]: [],
2524
+ };
2525
+ await groupConfigStore.updateGroupConfig(remoteJid, buildNewsFilterConfigPatch(groupConfig, nextFilters));
2526
+ await sendAndStore(
2527
+ sock,
2528
+ remoteJid,
2529
+ {
2530
+ text: `✅ Filtro de notícias (${scopeLabel}) limpo com sucesso.`,
2531
+ },
2532
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2533
+ );
2534
+ break;
2535
+ }
2536
+
2537
+ const values = parseFilterValues(args.slice(2));
2538
+ if (values.length === 0) {
2539
+ await sendAndStore(
2540
+ sock,
2541
+ remoteJid,
2542
+ {
2543
+ text: getAdminUsageText('noticiasfiltro', { commandPrefix, variant: 'missing_values' }),
2544
+ },
2545
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2546
+ );
2547
+ break;
2548
+ }
2549
+
2550
+ const currentSet = new Set(currentFilters[filterKey]);
2551
+ if (action === 'add') {
2552
+ values.forEach((value) => currentSet.add(value));
2553
+ } else {
2554
+ values.forEach((value) => currentSet.delete(value));
2555
+ }
2556
+
2557
+ const nextFilters = {
2558
+ ...currentFilters,
2559
+ [filterKey]: Array.from(currentSet).sort((left, right) => left.localeCompare(right)),
2560
+ };
2561
+ await groupConfigStore.updateGroupConfig(remoteJid, buildNewsFilterConfigPatch(groupConfig, nextFilters));
2562
+ await sendAndStore(
2563
+ sock,
2564
+ remoteJid,
2565
+ {
2566
+ text: `✅ Filtro de notícias (${scopeLabel}) atualizado: ${formatListForMessage(nextFilters[filterKey])}`,
2567
+ },
2568
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2569
+ );
2570
+ break;
2571
+ }
2572
+
2573
+ case 'grupoaudit': {
2574
+ if (!isGroupMessage) {
2575
+ await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
2576
+ break;
2577
+ }
2578
+ if (!(await isUserAdmin(remoteJid, senderIdentity))) {
2579
+ await sendAndStore(sock, remoteJid, { text: NO_PERMISSION_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
2580
+ break;
2581
+ }
2582
+
2583
+ const groupConfig = await groupConfigStore.getGroupConfig(remoteJid);
2584
+ const stickerState = resolveStickerFocusState(groupConfig);
2585
+ const newsStatus = await getNewsStatusForGroup(remoteJid);
2586
+ const auditText = buildGroupAuditText({
2587
+ config: groupConfig,
2588
+ stickerState,
2589
+ newsStatus,
2590
+ commandPrefix,
2591
+ });
2592
+
2593
+ await sendAndStore(
2594
+ sock,
2595
+ remoteJid,
2596
+ {
2597
+ text: auditText,
2598
+ },
2599
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
2600
+ );
2601
+ break;
2602
+ }
2603
+
1762
2604
  case 'noticias': {
1763
2605
  if (!isGroupMessage) {
1764
2606
  await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });