@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,10 +1,11 @@
1
1
  import { URL } from 'node:url';
2
2
  import { isUserAdmin, updateGroupParticipants } from '../../config/index.js';
3
- import { getJidUser, isLidJid, isSameJidUser, isWhatsAppJid, normalizeJid } from '../../config/index.js';
3
+ import { getJidUser, isGroupJid, isLidJid, isSameJidUser, isSocketOpen, isWhatsAppJid, normalizeJid, parseEnvInt, runActiveSocketMethod, getActiveSocket } from '../../config/index.js';
4
4
  import groupConfigStore from '../../store/groupConfigStore.js';
5
5
  import logger from '#logger';
6
6
  import { sendAndStore } from '../../services/messaging/messagePersistenceService.js';
7
7
  import { extractSenderInfoFromMessage, resolveUserId } from '../../config/index.js';
8
+ import { executeQuery, TABLES } from '../../../database/index.js';
8
9
 
9
10
  /**
10
11
  * Base de redes conhecidas e seus domínios oficiais para permitir por categoria.
@@ -120,6 +121,13 @@ const URL_HINTS = ['https://', 'http://', 'www.'];
120
121
  const STRICT_TLD_SUFFIXES = new Set(['com', 'net', 'org', 'edu', 'gov', 'mil', 'io', 'me', 'tv', 'co', 'cc', 'gg', 'gl', 'ly', 'so', 'br', 'us', 'uk', 'eu', 'de', 'fr', 'es', 'pt', 'it', 'nl', 'be', 'ch', 'at', 'se', 'no', 'fi', 'dk', 'ie', 'pl', 'cz', 'sk', 'hu', 'ro', 'bg', 'gr', 'ru', 'ua', 'tr', 'il', 'ae', 'sa', 'qa', 'eg', 'ma', 'tn', 'dz', 'za', 'ng', 'ke', 'gh', 'in', 'pk', 'bd', 'lk', 'cn', 'jp', 'kr', 'tw', 'hk', 'sg', 'my', 'th', 'vn', 'ph', 'id', 'au', 'nz', 'ca', 'mx', 'ar', 'cl', 'pe', 'uy', 'py', 'bo', 'ec', 've', 'do', 'cu', 'pa', 'cr', 'gt', 'hn', 'ni', 'sv', 'pr', 'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'jus.br', 'mil.br', 'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'co.jp', 'ne.jp', 'or.jp', 'go.jp', 'ac.jp', 'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'com.mx', 'com.ar', 'com.co', 'com.pe', 'com.tr', 'com.sg', 'com.my', 'com.ph', 'co.in', 'firm.in', 'net.in', 'org.in', 'gen.in', 'ind.in', 'co.id', 'or.id', 'go.id', 'web.id', 'co.za', 'org.za', 'net.za', 'com.ng', 'com.gh', 'com.eg', 'com.sa', 'com.qa', 'com.ae', 'page.link', 'g.page']);
121
122
  const EXTRA_TLD_SUFFIXES = new Set(['ai', 'app', 'dev', 'xyz', 'site', 'online', 'store', 'shop', 'blog', 'tech', 'cloud', 'digital', 'live', 'media', 'news', 'one', 'top', 'club', 'vip', 'fun', 'games', 'game', 'space', 'world', 'today', 'agency', 'email', 'center', 'company', 'group', 'solutions', 'systems', 'services', 'network', 'social', 'design', 'studio', 'photo', 'video', 'audio', 'music', 'art', 'wiki', 'finance', 'capital', 'money', 'loans', 'insurance', 'legal', 'law', 'health', 'care', 'clinic', 'dental', 'academy', 'school', 'college', 'university', 'education', 'training', 'support', 'chat', 'forum', 'community', 'events', 'travel', 'tours', 'hotel', 'homes', 'house', 'auto', 'cars', 'bike', 'food', 'restaurant', 'cafe', 'bar', 'pizza', 'delivery', 'fashion', 'beauty', 'style', 'fit', 'fitness', 'sports', 'download']);
122
123
  const ANY_TLD_SUFFIXES = new Set([...STRICT_TLD_SUFFIXES, ...EXTRA_TLD_SUFFIXES]);
124
+ const ANTILINK_DELETE_WINDOW_MS = parseEnvInt(process.env.ANTILINK_DELETE_WINDOW_MS, 5 * 60 * 1000, 60 * 1000, 30 * 60 * 1000);
125
+ const ANTILINK_DELETE_MAX_MESSAGES = parseEnvInt(process.env.ANTILINK_DELETE_MAX_MESSAGES, 40, 1, 300);
126
+ const ANTILINK_QUERY_MAX_CANDIDATES = 20;
127
+ const ANTILINK_DELETE_REVALIDATION_ATTEMPTS = parseEnvInt(process.env.ANTILINK_DELETE_REVALIDATION_ATTEMPTS, 3, 1, 8);
128
+ const ANTILINK_DELETE_REVALIDATION_DELAY_MS = parseEnvInt(process.env.ANTILINK_DELETE_REVALIDATION_DELAY_MS, 450, 100, 5000);
129
+ const ANTILINK_DELETE_WINDOW_MINUTES = Math.max(1, Math.round(ANTILINK_DELETE_WINDOW_MS / (60 * 1000)));
130
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
123
131
 
124
132
  /**
125
133
  * Tokeniza texto por espaço/quebra de linha sem regex.
@@ -591,6 +599,317 @@ const removeParticipantWithFallback = async (sock, remoteJid, candidates = []) =
591
599
  return '';
592
600
  };
593
601
 
602
+ const resolveOperationalSocket = (sock) => {
603
+ if (isSocketOpen(sock)) return sock;
604
+ const activeSocket = getActiveSocket();
605
+ if (isSocketOpen(activeSocket)) return activeSocket;
606
+ return null;
607
+ };
608
+
609
+ const sendMessageWithFallback = async (sock, jid, content) => {
610
+ const operationalSocket = resolveOperationalSocket(sock);
611
+ if (operationalSocket) {
612
+ return sendAndStore(operationalSocket, jid, content);
613
+ }
614
+ return runActiveSocketMethod('sendMessage', jid, content);
615
+ };
616
+
617
+ const sendDeleteWithFallback = async (sock, remoteJid, messageKey) => {
618
+ const operationalSocket = resolveOperationalSocket(sock);
619
+ if (operationalSocket && typeof operationalSocket.sendMessage === 'function') {
620
+ return operationalSocket.sendMessage(remoteJid, { delete: messageKey });
621
+ }
622
+ return runActiveSocketMethod('sendMessage', remoteJid, { delete: messageKey });
623
+ };
624
+
625
+ const safeJsonParse = (value, fallback = null) => {
626
+ if (value === null || value === undefined) return fallback;
627
+ if (typeof value === 'object') return value;
628
+ if (Buffer.isBuffer(value)) {
629
+ return safeJsonParse(value.toString('utf8'), fallback);
630
+ }
631
+ if (typeof value !== 'string') return fallback;
632
+ try {
633
+ return JSON.parse(value);
634
+ } catch {
635
+ return fallback;
636
+ }
637
+ };
638
+
639
+ const toTimestampMs = (value) => {
640
+ if (value === null || value === undefined) return null;
641
+ if (value instanceof Date) {
642
+ const ms = value.getTime();
643
+ return Number.isFinite(ms) ? ms : null;
644
+ }
645
+
646
+ const numeric = Number(value);
647
+ if (Number.isFinite(numeric) && numeric > 0) {
648
+ if (numeric > 1e12) return numeric;
649
+ if (numeric > 1e10) return numeric;
650
+ if (numeric > 1e9) return numeric * 1000;
651
+ }
652
+
653
+ const parsed = Date.parse(String(value));
654
+ return Number.isFinite(parsed) ? parsed : null;
655
+ };
656
+
657
+ const normalizeMessageId = (value) => {
658
+ if (value === null || value === undefined) return '';
659
+ const normalized = String(value).trim();
660
+ if (!normalized || normalized.length > 255) return '';
661
+ return normalized;
662
+ };
663
+
664
+ const buildInClause = (items = []) => items.map(() => '?').join(', ');
665
+
666
+ const isMissingCanonicalSenderColumnError = (error) => {
667
+ const code = String(error?.code || '')
668
+ .trim()
669
+ .toUpperCase();
670
+ if (code === 'ER_BAD_FIELD_ERROR') return true;
671
+ const errno = Number(error?.errno || 0);
672
+ if (errno === 1054) return true;
673
+ const message = String(error?.message || '').toLowerCase();
674
+ return message.includes('canonical_sender_id') && (message.includes('unknown column') || message.includes("doesn't exist"));
675
+ };
676
+
677
+ const isSenderInCandidates = (senderJid, senderCandidates = []) => {
678
+ const normalizedSender = normalizeOptionalJid(senderJid);
679
+ if (!normalizedSender) return false;
680
+
681
+ for (const candidate of senderCandidates) {
682
+ const normalizedCandidate = normalizeOptionalJid(candidate);
683
+ if (!normalizedCandidate) continue;
684
+ if (normalizedCandidate === normalizedSender) return true;
685
+ if (isSameUserSafe(normalizedCandidate, normalizedSender)) return true;
686
+ }
687
+ return false;
688
+ };
689
+
690
+ const normalizeAddressingMode = (value) => {
691
+ const normalized = String(value || '')
692
+ .trim()
693
+ .toLowerCase();
694
+ if (normalized === 'lid' || normalized === 'pn') return normalized;
695
+ return '';
696
+ };
697
+
698
+ const buildDeleteMessageKey = ({ sourceKey = {}, remoteJid, messageId, senderCandidates = [], fallbackParticipant = '' }) => {
699
+ const normalizedRemoteJid = normalizeOptionalJid(sourceKey?.remoteJid || remoteJid);
700
+ const normalizedGroupJid = normalizeOptionalJid(remoteJid);
701
+ if (!normalizedRemoteJid || !normalizedGroupJid || normalizedRemoteJid !== normalizedGroupJid) return null;
702
+
703
+ const normalizedMessageId = normalizeMessageId(sourceKey?.id || messageId);
704
+ if (!normalizedMessageId) return null;
705
+ if (sourceKey?.fromMe === true) return null;
706
+
707
+ const keyParticipant = normalizeOptionalJid(sourceKey?.participant || sourceKey?.participantAlt || fallbackParticipant);
708
+ if (!keyParticipant || !isSenderInCandidates(keyParticipant, senderCandidates)) return null;
709
+
710
+ const deleteKey = {
711
+ remoteJid: normalizedRemoteJid,
712
+ id: normalizedMessageId,
713
+ fromMe: false,
714
+ participant: keyParticipant,
715
+ };
716
+
717
+ const participantAlt = normalizeOptionalJid(sourceKey?.participantAlt);
718
+ if (participantAlt && participantAlt !== keyParticipant && isSenderInCandidates(participantAlt, senderCandidates)) {
719
+ deleteKey.participantAlt = participantAlt;
720
+ }
721
+
722
+ const addressingMode = normalizeAddressingMode(sourceKey?.addressingMode);
723
+ if (addressingMode) {
724
+ deleteKey.addressingMode = addressingMode;
725
+ }
726
+
727
+ return deleteKey;
728
+ };
729
+
730
+ const fetchRecentSenderMessages = async ({ remoteJid, senderCandidates = [], minimumTimestampMs, limit }) => {
731
+ const normalizedCandidates = uniqueNormalizedJids(senderCandidates).slice(0, ANTILINK_QUERY_MAX_CANDIDATES);
732
+ if (!normalizedCandidates.length) return [];
733
+
734
+ const inClause = buildInClause(normalizedCandidates);
735
+ const safeLimit = Math.max(1, Math.min(Number(limit) || ANTILINK_DELETE_MAX_MESSAGES, ANTILINK_DELETE_MAX_MESSAGES));
736
+ const queryParams = [remoteJid, new Date(minimumTimestampMs), ...normalizedCandidates, ...normalizedCandidates, safeLimit];
737
+ const fullQuery = `SELECT message_id, chat_id, sender_id, canonical_sender_id, raw_message, timestamp
738
+ FROM ${TABLES.MESSAGES}
739
+ WHERE chat_id = ?
740
+ AND timestamp IS NOT NULL
741
+ AND timestamp >= ?
742
+ AND (canonical_sender_id IN (${inClause}) OR sender_id IN (${inClause}))
743
+ ORDER BY timestamp DESC
744
+ LIMIT ?`;
745
+
746
+ try {
747
+ return await executeQuery(fullQuery, queryParams);
748
+ } catch (error) {
749
+ if (!isMissingCanonicalSenderColumnError(error)) {
750
+ throw error;
751
+ }
752
+
753
+ const fallbackParams = [remoteJid, new Date(minimumTimestampMs), ...normalizedCandidates, safeLimit];
754
+ const fallbackQuery = `SELECT message_id, chat_id, sender_id, NULL AS canonical_sender_id, raw_message, timestamp
755
+ FROM ${TABLES.MESSAGES}
756
+ WHERE chat_id = ?
757
+ AND timestamp IS NOT NULL
758
+ AND timestamp >= ?
759
+ AND sender_id IN (${inClause})
760
+ ORDER BY timestamp DESC
761
+ LIMIT ?`;
762
+
763
+ return executeQuery(fallbackQuery, fallbackParams);
764
+ }
765
+ };
766
+
767
+ const collectRecentDeleteKeysForSender = async ({ messageInfo, remoteJid, senderCandidates = [] }) => {
768
+ const normalizedRemoteJid = normalizeOptionalJid(remoteJid);
769
+ if (!normalizedRemoteJid || !isGroupJid(normalizedRemoteJid)) return [];
770
+
771
+ const normalizedCandidates = uniqueNormalizedJids(senderCandidates).slice(0, ANTILINK_QUERY_MAX_CANDIDATES);
772
+ if (!normalizedCandidates.length) return [];
773
+ const preferredParticipant = normalizedCandidates.find((candidate) => isWhatsAppJid(candidate)) || normalizedCandidates[0] || '';
774
+
775
+ const minimumTimestampMs = Date.now() - ANTILINK_DELETE_WINDOW_MS;
776
+ const keysById = new Map();
777
+
778
+ const currentMessageKey = buildDeleteMessageKey({
779
+ sourceKey: messageInfo?.key || {},
780
+ remoteJid: normalizedRemoteJid,
781
+ senderCandidates: normalizedCandidates,
782
+ fallbackParticipant: preferredParticipant,
783
+ });
784
+
785
+ if (currentMessageKey) {
786
+ keysById.set(currentMessageKey.id, currentMessageKey);
787
+ }
788
+
789
+ let recentRows = [];
790
+ try {
791
+ recentRows = await fetchRecentSenderMessages({
792
+ remoteJid: normalizedRemoteJid,
793
+ senderCandidates: normalizedCandidates,
794
+ minimumTimestampMs,
795
+ limit: ANTILINK_DELETE_MAX_MESSAGES,
796
+ });
797
+ } catch (error) {
798
+ logger.warn('Falha ao buscar mensagens recentes para limpeza de antilink.', {
799
+ action: 'antilink_recent_fetch_error',
800
+ groupId: normalizedRemoteJid,
801
+ senderCandidates: normalizedCandidates,
802
+ error: error?.message,
803
+ });
804
+ }
805
+
806
+ for (const row of recentRows) {
807
+ if (keysById.size >= ANTILINK_DELETE_MAX_MESSAGES) break;
808
+
809
+ const rowTimestampMs = toTimestampMs(row?.timestamp);
810
+ if (!rowTimestampMs || rowTimestampMs < minimumTimestampMs) continue;
811
+
812
+ const rawMessage = safeJsonParse(row?.raw_message, null);
813
+ const candidateKey = rawMessage?.key && typeof rawMessage.key === 'object' ? rawMessage.key : {};
814
+ const fallbackParticipant = normalizeOptionalJid(row?.canonical_sender_id || row?.sender_id || preferredParticipant);
815
+ const deleteKey = buildDeleteMessageKey({
816
+ sourceKey: candidateKey,
817
+ remoteJid: normalizedRemoteJid,
818
+ messageId: row?.message_id,
819
+ senderCandidates: normalizedCandidates,
820
+ fallbackParticipant,
821
+ });
822
+
823
+ if (!deleteKey) continue;
824
+ if (keysById.has(deleteKey.id)) continue;
825
+ keysById.set(deleteKey.id, deleteKey);
826
+ }
827
+
828
+ return Array.from(keysById.values());
829
+ };
830
+
831
+ const purgeRecentMessagesFromRemovedSender = async ({ sock, messageInfo, remoteJid, senderCandidates = [] }) => {
832
+ const totalRounds = Math.max(1, ANTILINK_DELETE_REVALIDATION_ATTEMPTS);
833
+ const seenMessageIds = new Set();
834
+ const deletedMessageIds = new Set();
835
+ let failedAttempts = 0;
836
+ let roundsWithDeletes = 0;
837
+
838
+ for (let round = 1; round <= totalRounds; round += 1) {
839
+ const deleteKeys = await collectRecentDeleteKeysForSender({
840
+ messageInfo,
841
+ remoteJid,
842
+ senderCandidates,
843
+ });
844
+
845
+ const pendingKeys = [];
846
+ for (const deleteKey of deleteKeys) {
847
+ const messageId = normalizeMessageId(deleteKey?.id);
848
+ if (!messageId) continue;
849
+ seenMessageIds.add(messageId);
850
+ if (deletedMessageIds.has(messageId)) continue;
851
+ pendingKeys.push(deleteKey);
852
+ }
853
+
854
+ let deletedInRound = 0;
855
+ for (const deleteKey of pendingKeys) {
856
+ try {
857
+ await sendDeleteWithFallback(sock, remoteJid, deleteKey);
858
+ const messageId = normalizeMessageId(deleteKey?.id);
859
+ if (messageId) {
860
+ deletedMessageIds.add(messageId);
861
+ deletedInRound += 1;
862
+ }
863
+ } catch (error) {
864
+ failedAttempts += 1;
865
+ logger.debug('Falha ao apagar mensagem durante limpeza do antilink.', {
866
+ action: 'antilink_delete_message_failed',
867
+ groupId: remoteJid,
868
+ messageId: deleteKey?.id,
869
+ participant: deleteKey?.participant || null,
870
+ round,
871
+ totalRounds,
872
+ error: error?.message,
873
+ });
874
+ }
875
+ }
876
+
877
+ if (deletedInRound > 0) {
878
+ roundsWithDeletes += 1;
879
+ }
880
+
881
+ if (round < totalRounds) {
882
+ await wait(ANTILINK_DELETE_REVALIDATION_DELAY_MS);
883
+ }
884
+ }
885
+
886
+ return {
887
+ requested: seenMessageIds.size,
888
+ deleted: deletedMessageIds.size,
889
+ failed: failedAttempts,
890
+ rounds: totalRounds,
891
+ roundsWithDeletes,
892
+ };
893
+ };
894
+
895
+ /**
896
+ * Limpa mensagens recentes (janela de segurança) de um participante alvo.
897
+ * Pode ser reutilizado por outros fluxos de moderação (ex.: comando ban).
898
+ * @param {Object} params
899
+ * @param {import('@whiskeysockets/baileys').WASocket} params.sock
900
+ * @param {Object|null|undefined} [params.messageInfo]
901
+ * @param {string} params.remoteJid
902
+ * @param {string[]} params.senderCandidates
903
+ * @returns {Promise<{requested:number, deleted:number, failed:number}>}
904
+ */
905
+ export const purgeRecentMessagesForSenderCandidates = async ({ sock, messageInfo, remoteJid, senderCandidates = [] }) =>
906
+ purgeRecentMessagesFromRemovedSender({
907
+ sock,
908
+ messageInfo,
909
+ remoteJid,
910
+ senderCandidates,
911
+ });
912
+
594
913
  /**
595
914
  * Aplica a regra de antilink do grupo. Retorna true quando removeu e deve pular o restante.
596
915
  * @param {Object} params
@@ -604,7 +923,10 @@ const removeParticipantWithFallback = async (sock, remoteJid, candidates = []) =
604
923
  * @returns {Promise<boolean>}
605
924
  */
606
925
  export const handleAntiLink = async ({ sock, messageInfo, extractedText, remoteJid, senderJid, senderIdentity, botJid }) => {
607
- const groupConfig = await groupConfigStore.getGroupConfig(remoteJid);
926
+ const normalizedRemoteJid = normalizeOptionalJid(remoteJid);
927
+ if (!normalizedRemoteJid || !isGroupJid(normalizedRemoteJid)) return false;
928
+
929
+ const groupConfig = await groupConfigStore.getGroupConfig(normalizedRemoteJid);
608
930
  if (!groupConfig || !groupConfig.antilinkEnabled) return false;
609
931
 
610
932
  const allowedDomains = getAllowedDomains(groupConfig.antilinkAllowedNetworks || [], groupConfig.antilinkAllowedDomains || []);
@@ -618,7 +940,7 @@ export const handleAntiLink = async ({ sock, messageInfo, extractedText, remoteJ
618
940
  });
619
941
  if (!senderContext.primarySenderId && senderContext.senderCandidates.length === 0) return false;
620
942
 
621
- let isAdmin = await isUserAdmin(remoteJid, {
943
+ let isAdmin = await isUserAdmin(normalizedRemoteJid, {
622
944
  id: senderContext.primarySenderId || null,
623
945
  jid: senderContext.senderInfo?.jid || senderContext.primarySenderId || null,
624
946
  lid: senderContext.senderInfo?.lid || null,
@@ -628,7 +950,7 @@ export const handleAntiLink = async ({ sock, messageInfo, extractedText, remoteJ
628
950
  });
629
951
 
630
952
  if (!isAdmin && senderContext.primarySenderId) {
631
- isAdmin = await isUserAdmin(remoteJid, senderContext.primarySenderId);
953
+ isAdmin = await isUserAdmin(normalizedRemoteJid, senderContext.primarySenderId);
632
954
  }
633
955
 
634
956
  const senderIsBot = isSenderBot(botJid, senderContext.senderCandidates);
@@ -637,37 +959,51 @@ export const handleAntiLink = async ({ sock, messageInfo, extractedText, remoteJ
637
959
  if (senderContext.removalCandidates.length === 0) {
638
960
  logger.warn('Antilink detectou link, mas não encontrou ID válido para remoção.', {
639
961
  action: 'antilink_no_removal_candidate',
640
- groupId: remoteJid,
962
+ groupId: normalizedRemoteJid,
641
963
  senderCandidates: senderContext.senderCandidates,
642
964
  });
643
965
  return false;
644
966
  }
645
967
 
646
968
  try {
647
- const removedParticipantId = await removeParticipantWithFallback(sock, remoteJid, senderContext.removalCandidates);
969
+ const removedParticipantId = await removeParticipantWithFallback(sock, normalizedRemoteJid, senderContext.removalCandidates);
648
970
  if (!removedParticipantId) {
649
971
  throw new Error('Nenhum candidato de participante pôde ser removido.');
650
972
  }
973
+
974
+ const deletionCandidates = uniqueNormalizedJids([removedParticipantId, ...senderContext.senderCandidates]);
975
+ const purgeResult = await purgeRecentMessagesFromRemovedSender({
976
+ sock,
977
+ messageInfo,
978
+ remoteJid: normalizedRemoteJid,
979
+ senderCandidates: deletionCandidates,
980
+ });
981
+
651
982
  const senderMention = senderContext.mentionJid || removedParticipantId || senderContext.primarySenderId;
652
983
  const senderUser = getJidUser(senderMention);
653
- await sendAndStore(sock, remoteJid, {
654
- text: `🚫 @${senderUser || 'usuario'} foi removido por enviar um link.`,
984
+ const recentDeleteLine = purgeResult.deleted > 0 ? `\n🧹 ${purgeResult.deleted} mensagem(ns) dos últimos ${ANTILINK_DELETE_WINDOW_MINUTES} minuto(s) foram apagadas.` : '';
985
+ await sendMessageWithFallback(sock, normalizedRemoteJid, {
986
+ text: `🚫 @${senderUser || 'usuario'} foi removido por enviar um link.${recentDeleteLine}`,
655
987
  mentions: senderMention ? [senderMention] : [],
656
988
  });
657
- await sendAndStore(sock, remoteJid, { delete: messageInfo.key });
658
989
 
659
- logger.info(`Usuário ${removedParticipantId || senderContext.primarySenderId} removido do grupo ${remoteJid} por enviar link.`, {
990
+ logger.info(`Usuário ${removedParticipantId || senderContext.primarySenderId} removido do grupo ${normalizedRemoteJid} por enviar link.`, {
660
991
  action: 'antilink_remove',
661
- groupId: remoteJid,
992
+ groupId: normalizedRemoteJid,
662
993
  userId: removedParticipantId || senderContext.primarySenderId,
663
994
  senderCandidates: senderContext.senderCandidates,
995
+ deletedRecentMessages: purgeResult.deleted,
996
+ failedRecentMessageDeletes: purgeResult.failed,
997
+ requestedRecentMessageDeletes: purgeResult.requested,
998
+ deleteRevalidationRounds: purgeResult.rounds,
999
+ deleteRevalidationRoundsWithDeletes: purgeResult.roundsWithDeletes,
664
1000
  });
665
1001
 
666
1002
  return true;
667
1003
  } catch (error) {
668
1004
  logger.error(`Falha ao remover usuário com antilink: ${error.message}`, {
669
1005
  action: 'antilink_error',
670
- groupId: remoteJid,
1006
+ groupId: normalizedRemoteJid,
671
1007
  userId: senderContext.primarySenderId,
672
1008
  senderCandidates: senderContext.senderCandidates,
673
1009
  error: error.stack,
@@ -677,19 +1013,19 @@ export const handleAntiLink = async ({ sock, messageInfo, extractedText, remoteJ
677
1013
  try {
678
1014
  const senderMention = senderContext.mentionJid || senderContext.primarySenderId;
679
1015
  const senderUser = getJidUser(senderMention);
680
- await sendAndStore(sock, remoteJid, {
1016
+ await sendMessageWithFallback(sock, normalizedRemoteJid, {
681
1017
  text: `ⓘ @${senderUser || 'admin'} (admin) enviou um link.`,
682
1018
  mentions: senderMention ? [senderMention] : [],
683
1019
  });
684
- logger.info(`Admin ${senderContext.primarySenderId} enviou um link no grupo ${remoteJid} (aviso enviado).`, {
1020
+ logger.info(`Admin ${senderContext.primarySenderId} enviou um link no grupo ${normalizedRemoteJid} (aviso enviado).`, {
685
1021
  action: 'antilink_admin_link_detected',
686
- groupId: remoteJid,
1022
+ groupId: normalizedRemoteJid,
687
1023
  userId: senderContext.primarySenderId,
688
1024
  });
689
1025
  } catch (error) {
690
1026
  logger.error(`Falha ao enviar aviso de link de admin: ${error.message}`, {
691
1027
  action: 'antilink_admin_warning_error',
692
- groupId: remoteJid,
1028
+ groupId: normalizedRemoteJid,
693
1029
  userId: senderContext.primarySenderId,
694
1030
  error: error.stack,
695
1031
  });