@omnizap-system/omnizap 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.env.example +58 -13
  2. package/.github/workflows/ci.yml +5 -5
  3. package/.github/workflows/codeql.yml +1 -1
  4. package/.github/workflows/db-migration-check.yml +2 -2
  5. package/.github/workflows/dependency-review.yml +1 -1
  6. package/.github/workflows/deploy.yml +2 -2
  7. package/.github/workflows/release.yml +2 -2
  8. package/.github/workflows/security-attest-provenance.yml +2 -2
  9. package/.github/workflows/security-gitleaks.yml +13 -4
  10. package/.github/workflows/security-runner-hardening.yml +2 -2
  11. package/.github/workflows/security-scorecard.yml +1 -1
  12. package/.github/workflows/security-zap-baseline.yml +1 -1
  13. package/.github/workflows/security-zap-full-scan.yml +2 -1
  14. package/.github/workflows/security-zizmor.yml +1 -1
  15. package/.github/workflows/wiki-sync.yml +1 -1
  16. package/.gitleaksignore +9 -0
  17. package/CODE_OF_CONDUCT.md +2 -2
  18. package/GEMINI.md +64 -0
  19. package/README.md +52 -82
  20. package/SECURITY.md +1 -1
  21. package/app/config/index.js +2 -0
  22. package/app/configParts/adminIdentity.js +5 -5
  23. package/app/configParts/baileysConfig.js +230 -58
  24. package/app/configParts/groupUtils.js +5 -0
  25. package/app/configParts/messagePersistenceService.js +145 -4
  26. package/app/configParts/sessionConfig.js +157 -0
  27. package/app/connection/baileysCompatibility.test.js +1 -1
  28. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  29. package/app/connection/socketController.js +660 -158
  30. package/app/connection/socketController.multiSession.test.js +108 -0
  31. package/app/controllers/messageController.js +1 -1
  32. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  33. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  34. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  35. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  36. package/app/controllers/messageProcessingPipeline.js +93 -13
  37. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  38. package/app/modules/adminModule/AGENT.md +1 -1
  39. package/app/modules/adminModule/commandConfig.json +3318 -1347
  40. package/app/modules/adminModule/groupCommandHandlers.js +858 -15
  41. package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
  42. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  43. package/app/modules/aiModule/AGENT.md +47 -30
  44. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  45. package/app/modules/aiModule/catCommand.js +135 -27
  46. package/app/modules/aiModule/commandConfig.json +114 -28
  47. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  48. package/app/modules/gameModule/AGENT.md +1 -1
  49. package/app/modules/gameModule/commandConfig.json +29 -0
  50. package/app/modules/menuModule/AGENT.md +1 -1
  51. package/app/modules/menuModule/commandConfig.json +45 -10
  52. package/app/modules/menuModule/menuCatalogService.js +190 -0
  53. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  54. package/app/modules/menuModule/menuDynamicService.js +511 -0
  55. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  56. package/app/modules/menuModule/menus.js +36 -5
  57. package/app/modules/playModule/AGENT.md +10 -5
  58. package/app/modules/playModule/commandConfig.json +140 -12
  59. package/app/modules/playModule/playCommand.js +1 -1417
  60. package/app/modules/playModule/playCommandConstants.js +80 -0
  61. package/app/modules/playModule/playCommandCore.js +361 -0
  62. package/app/modules/playModule/playCommandHandlers.js +41 -0
  63. package/app/modules/playModule/playCommandMediaClient.js +1872 -0
  64. package/app/modules/playModule/playConfigRuntime.js +245 -4
  65. package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
  66. package/app/modules/quoteModule/AGENT.md +1 -1
  67. package/app/modules/quoteModule/commandConfig.json +29 -0
  68. package/app/modules/quoteModule/quoteCommand.js +3 -2
  69. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  70. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  71. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
  72. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
  73. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
  74. package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
  75. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
  76. package/app/modules/statsModule/AGENT.md +1 -1
  77. package/app/modules/statsModule/commandConfig.json +58 -0
  78. package/app/modules/statsModule/rankingCommon.js +5 -4
  79. package/app/modules/stickerModule/AGENT.md +1 -1
  80. package/app/modules/stickerModule/addStickerMetadata.js +4 -3
  81. package/app/modules/stickerModule/commandConfig.json +145 -0
  82. package/app/modules/stickerModule/stickerCommand.js +1 -1
  83. package/app/modules/stickerPackModule/AGENT.md +1 -1
  84. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  85. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  86. package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
  87. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
  88. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
  89. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
  90. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
  91. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
  92. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
  93. package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
  94. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
  95. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  96. package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
  97. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
  98. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  99. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  100. package/app/modules/systemMetricsModule/pingCommand.js +6 -5
  101. package/app/modules/tiktokModule/AGENT.md +1 -1
  102. package/app/modules/tiktokModule/commandConfig.json +29 -0
  103. package/app/modules/tiktokModule/tiktokCommand.js +2 -1
  104. package/app/modules/userModule/AGENT.md +1 -1
  105. package/app/modules/userModule/commandConfig.json +29 -0
  106. package/app/modules/userModule/userCommand.js +72 -23
  107. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  108. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  109. package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
  110. package/app/observability/metrics.js +136 -0
  111. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  112. package/app/services/ai/conversationRouterService.js +4 -3
  113. package/app/services/ai/geminiService.js +132 -7
  114. package/app/services/ai/geminiService.test.js +59 -2
  115. package/app/services/ai/globalModuleAiHelpService.js +3 -2
  116. package/app/services/ai/messageCommandExecutionService.js +2 -1
  117. package/app/services/ai/moduleAiHelpCoreService.js +45 -14
  118. package/app/services/ai/moduleToolExecutorService.js +3 -2
  119. package/app/services/ai/moduleToolRegistryService.js +2 -1
  120. package/app/services/ai/toolCandidateSelectorService.js +6 -5
  121. package/app/services/auth/googleWebLinkService.js +3 -2
  122. package/app/services/auth/whatsappLoginLinkService.js +3 -2
  123. package/app/services/external/pokeApiService.js +4 -3
  124. package/app/services/group/groupMetadataService.js +24 -1
  125. package/app/services/infra/dbWriteQueue.js +57 -26
  126. package/app/services/infra/featureFlagService.js +2 -1
  127. package/app/services/messaging/captchaService.js +3 -2
  128. package/app/services/messaging/newsBroadcastService.js +846 -29
  129. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  130. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  131. package/app/services/multiSession/groupOwnershipService.js +890 -0
  132. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  133. package/app/services/multiSession/sessionRegistryService.js +293 -0
  134. package/app/services/sticker/stickerFocusService.js +11 -10
  135. package/app/store/aiPromptStore.js +36 -19
  136. package/app/store/conversationSessionStore.js +7 -6
  137. package/app/store/groupConfigStore.js +41 -5
  138. package/app/store/premiumUserStore.js +21 -7
  139. package/app/utils/antiLink/antiLinkModule.js +352 -16
  140. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  141. package/app/workers/aiLearningWorker.js +6 -5
  142. package/app/workers/commandConfigEnrichmentWorker.js +4 -3
  143. package/database/index.js +14 -8
  144. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  145. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  146. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  147. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  148. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  149. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  150. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  151. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  152. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  153. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  154. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  155. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  156. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  157. package/database/schema.sql +102 -1
  158. package/docker-compose.yml +4 -1
  159. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  160. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
  161. package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
  162. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  163. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
  164. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  165. package/docs/security/omnizap-static-security-headers.conf +25 -0
  166. package/docs/wiki/Home.md +1 -1
  167. package/ecosystem.prod.config.cjs +32 -12
  168. package/index.js +57 -23
  169. package/observability/alert-rules.yml +20 -0
  170. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  171. package/observability/mysql-setup.sql +4 -4
  172. package/observability/system-admin-observability.md +26 -0
  173. package/package.json +20 -6
  174. package/public/apple-touch-icon.png +0 -0
  175. package/public/comandos/commands-catalog.json +2853 -3326
  176. package/public/favicon-16x16.png +0 -0
  177. package/public/favicon-32x32.png +0 -0
  178. package/public/favicon.ico +0 -0
  179. package/public/js/apps/apiDocsApp.js +3 -2
  180. package/public/js/apps/commandsReactApp.js +280 -99
  181. package/public/js/apps/createPackApp.js +11 -10
  182. package/public/js/apps/homeReactApp.js +181 -130
  183. package/public/js/apps/loginReactApp.js +1 -1
  184. package/public/js/apps/stickersApp.js +263 -110
  185. package/public/js/apps/termsReactApp.js +73 -24
  186. package/public/js/apps/userApp.js +4 -3
  187. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  188. package/public/js/apps/userReactApp.js +355 -280
  189. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  190. package/public/pages/api-docs.html +1 -1
  191. package/public/pages/aup.html +2 -2
  192. package/public/pages/dpa.html +3 -3
  193. package/public/pages/licenca.html +4 -4
  194. package/public/pages/login.html +1 -1
  195. package/public/pages/notice-and-takedown.html +2 -2
  196. package/public/pages/politica-de-privacidade.html +6 -6
  197. package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
  198. package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
  199. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
  200. package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
  201. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
  202. package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
  203. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
  204. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
  205. package/public/pages/stickers-admin.html +1 -1
  206. package/public/pages/stickers-create.html +1 -1
  207. package/public/pages/stickers.html +6 -6
  208. package/public/pages/suboperadores.html +2 -2
  209. package/public/pages/termos-de-uso-texto-integral.html +6 -6
  210. package/public/pages/termos-de-uso.html +3 -3
  211. package/public/pages/user-password-reset.html +4 -5
  212. package/public/pages/user-systemadm.html +9 -463
  213. package/public/pages/user.html +2 -2
  214. package/scripts/clear-whatsapp-session.sh +123 -0
  215. package/scripts/core-ai-mode.mjs +163 -0
  216. package/scripts/deploy.sh +11 -1
  217. package/scripts/email-broadcast-terms-update.mjs +2 -1
  218. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  219. package/scripts/generate-commands-catalog.mjs +166 -2
  220. package/scripts/generate-module-agents.mjs +2 -1
  221. package/scripts/generate-seo-satellite-pages.mjs +5 -4
  222. package/scripts/github-deploy-notify.mjs +2 -1
  223. package/scripts/github-release-notify.mjs +25 -10
  224. package/scripts/new-whatsapp-session.sh +317 -0
  225. package/scripts/release.sh +2 -19
  226. package/scripts/security-smoketest.mjs +6 -5
  227. package/scripts/security-web-surface-check.mjs +218 -0
  228. package/scripts/sticker-catalog-loadtest.mjs +5 -4
  229. package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
  230. package/server/auth/jwt/webJwtService.js +1 -1
  231. package/server/auth/stickerCatalogAuthContext.js +2 -1
  232. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
  233. package/server/auth/userPassword/userPasswordAuthService.js +2 -1
  234. package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
  235. package/server/auth/webAccount/webAccountHandlers.js +9 -10
  236. package/server/controllers/admin/adminPanelHandlers.js +267 -16
  237. package/server/controllers/admin/systemAdminController.js +267 -0
  238. package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
  239. package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
  240. package/server/controllers/sticker/stickerCatalogController.js +23 -36
  241. package/server/controllers/system/contactController.js +9 -17
  242. package/server/controllers/system/githubController.js +3 -2
  243. package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
  244. package/server/controllers/system/systemController.js +254 -1
  245. package/server/controllers/system/systemMetricsController.js +2 -1
  246. package/server/controllers/userController.js +6 -0
  247. package/server/email/emailTemplateService.js +5 -3
  248. package/server/http/httpServer.js +11 -6
  249. package/server/middleware/rateLimit.js +2 -1
  250. package/server/middleware/securityHeaders.js +20 -1
  251. package/server/routes/admin/systemAdminRouter.js +6 -0
  252. package/server/routes/indexRouter.js +30 -6
  253. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  254. package/server/routes/static/staticPageRouter.js +27 -1
  255. package/server/utils/publicContact.js +31 -0
  256. package/utils/time/timeModule.js +135 -0
  257. package/utils/time/timeModule.test.js +65 -0
  258. package/utils/whatsapp/contactEnv.js +39 -0
  259. package/vite.config.mjs +7 -1
  260. package/public/assets/images/brand-icon-192.png +0 -0
  261. package/scripts/sync-readme-snapshot.mjs +0 -133
@@ -1,18 +1,20 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import { handleMenuAdmCommand } from '../menuModule/menus.js';
2
3
  import { downloadMediaMessage, getJidServer, isLidJid, isSameJidUser, isWhatsAppJid, LID_USER_JID_SERVERS, normalizeJid, WHATSAPP_USER_JID_SERVERS } from '../../config/index.js';
3
4
  import { isUserAdmin, createGroup, acceptGroupInvite, getGroupInfo, getGroupRequestParticipantsList, updateGroupAddMode, updateGroupSettings, updateGroupParticipants, leaveGroup, getGroupInviteCode, revokeGroupInviteCode, getGroupInfoFromInvite, updateGroupRequestParticipants, updateGroupSubject, updateGroupDescription, toggleEphemeral } from '../../config/index.js';
4
5
  import groupConfigStore from '../../store/groupConfigStore.js';
5
6
  import premiumUserStore from '../../store/premiumUserStore.js';
6
7
  import logger from '#logger';
7
- import { KNOWN_NETWORKS } from '../../utils/antiLink/antiLinkModule.js';
8
+ import { KNOWN_NETWORKS, purgeRecentMessagesForSenderCandidates } from '../../utils/antiLink/antiLinkModule.js';
8
9
  import { getNewsStatusForGroup, startNewsBroadcastForGroup, stopNewsBroadcastForGroup } from '../../services/messaging/newsBroadcastService.js';
9
10
  import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
10
11
  import { clearCaptchasForGroup } from '../../services/messaging/captchaService.js';
11
12
  import { getAdminJid, isAdminSenderAsync } from '../../config/index.js';
12
13
  import { extractUserIdInfo, resolveUserId } from '../../config/index.js';
13
- 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';
14
15
  import { getAdminTextConfig, getAdminUsageText, isAdminCommandName, resolveAdminCommandName } from './adminConfigRuntime.js';
15
16
  import { explicarComando, gerarFaqAutomatica, responderPergunta, startAdminAiHelpScheduler } from './adminAiHelpService.js';
17
+ import { addGroupWarning, clearGroupWarnings, countGroupWarnings, listGroupWarnings } from './groupWarningRepository.js';
16
18
  const OWNER_JID = getAdminJid();
17
19
  const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
18
20
  const ADMIN_TEXTS = getAdminTextConfig();
@@ -20,6 +22,11 @@ const GROUP_ONLY_COMMAND_MESSAGE = ADMIN_TEXTS.group_only_command_message;
20
22
  const NO_PERMISSION_COMMAND_MESSAGE = ADMIN_TEXTS.no_permission_command_message;
21
23
  const OWNER_ONLY_COMMAND_MESSAGE = ADMIN_TEXTS.owner_only_command_message;
22
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;
23
30
  const normalizePhoneDigits = (value) => String(value || '').replace(/\D+/g, '');
24
31
 
25
32
  const LEGACY_ADMIN_ROUTE_BY_CANONICAL = {
@@ -105,6 +112,94 @@ const getParticipantJids = (messageInfo, args) => {
105
112
  return dedupeParticipantJids(args);
106
113
  };
107
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
+
108
203
  const resolvePremiumTargetJid = async (targetJid) => {
109
204
  const normalizedTarget = normalizeParticipantJid(targetJid);
110
205
  if (!normalizedTarget) return '';
@@ -148,6 +243,12 @@ const parsePositiveInteger = (value) => {
148
243
  return normalized;
149
244
  };
150
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
+
151
252
  const formatStickerFocusRule = ({ messageAllowanceCount, messageCooldownMinutes }) => {
152
253
  const allowanceCount = Math.max(1, Math.floor(Number(messageAllowanceCount) || 1));
153
254
  const cooldownMinutes = Math.max(1, Math.floor(Number(messageCooldownMinutes) || 1));
@@ -162,6 +263,96 @@ const buildStickerFocusStatusText = ({ state, commandPrefix }) => {
162
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');
163
264
  };
164
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
+
165
356
  export const isAdminCommand = (command) => isAdminCommandName(command);
166
357
 
167
358
  export async function handleAdminCommand({ command, args, text, sock, messageInfo, remoteJid, senderJid, botJid, isGroupMessage, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX }) {
@@ -193,7 +384,7 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
193
384
  const isOwner = Boolean(OWNER_JID && (await isAdminSenderAsync(senderIdentity)));
194
385
 
195
386
  if (!subAction) {
196
- await handleMenuAdmCommand(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
387
+ await handleMenuAdmCommand(sock, remoteJid, messageInfo, expirationMessage, commandPrefix, []);
197
388
  break;
198
389
  }
199
390
 
@@ -264,14 +455,7 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
264
455
  break;
265
456
  }
266
457
 
267
- await sendAndStore(
268
- sock,
269
- remoteJid,
270
- {
271
- text: getAdminUsageText('menuadm', { commandPrefix, variant: 'default' }),
272
- },
273
- { quoted: messageInfo, ephemeralExpiration: expirationMessage },
274
- );
458
+ await handleMenuAdmCommand(sock, remoteJid, messageInfo, expirationMessage, commandPrefix, args);
275
459
  break;
276
460
  }
277
461
 
@@ -582,7 +766,7 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
582
766
  minutes = clampStickerFocusChatWindowMinutes(parsed, DEFAULT_STICKER_FOCUS_CHAT_WINDOW_MINUTES);
583
767
  }
584
768
 
585
- const untilMs = Date.now() + minutes * 60 * 1000;
769
+ const untilMs = __timeNowMs() + minutes * 60 * 1000;
586
770
  await groupConfigStore.updateGroupConfig(remoteJid, {
587
771
  stickerFocusChatWindowUntilMs: untilMs,
588
772
  });
@@ -626,7 +810,6 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
626
810
  if (['reset', 'default', 'padrao', 'padrão'].includes(normalized)) {
627
811
  await groupConfigStore.updateGroupConfig(remoteJid, {
628
812
  stickerFocusMessageCooldownMinutes: DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES,
629
- // compatibilidade com configuração antiga
630
813
  stickerFocusTextCooldownMinutes: DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES,
631
814
  });
632
815
  await sendAndStore(
@@ -668,7 +851,6 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
668
851
  const minutes = clampStickerFocusMessageCooldownMinutes(parsed, DEFAULT_STICKER_FOCUS_MESSAGE_COOLDOWN_MINUTES);
669
852
  await groupConfigStore.updateGroupConfig(remoteJid, {
670
853
  stickerFocusMessageCooldownMinutes: minutes,
671
- // compatibilidade com configuração antiga
672
854
  stickerFocusTextCooldownMinutes: minutes,
673
855
  });
674
856
 
@@ -683,6 +865,93 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
683
865
  break;
684
866
  }
685
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
+
686
955
  case 'newgroup': {
687
956
  if (args.length < 2) {
688
957
  await sendAndStore(
@@ -765,7 +1034,33 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
765
1034
  }
766
1035
  try {
767
1036
  await updateGroupParticipants(sock, remoteJid, participants, 'remove');
768
- 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
+
769
1064
  const repliedTo = messageInfo.message?.extendedTextMessage?.contextInfo;
770
1065
  if (repliedTo && containsParticipantJid(participants, repliedTo.participant)) {
771
1066
  await sendAndStore(sock, remoteJid, {
@@ -778,6 +1073,329 @@ export async function handleAdminCommand({ command, args, text, sock, messageInf
778
1073
  break;
779
1074
  }
780
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
+
781
1399
  case 'up': {
782
1400
  if (!isGroupMessage) {
783
1401
  await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
@@ -1758,6 +2376,231 @@ ${JSON.stringify(response, null, 2)}`,
1758
2376
  break;
1759
2377
  }
1760
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
+
1761
2604
  case 'noticias': {
1762
2605
  if (!isGroupMessage) {
1763
2606
  await sendAndStore(sock, remoteJid, { text: GROUP_ONLY_COMMAND_MESSAGE }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });