@omnizap-system/omnizap 2.6.1 → 2.6.3

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 (172) hide show
  1. package/.env.example +78 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +6 -0
  6. package/app/configParts/adminIdentity.js +36 -7
  7. package/app/configParts/baileysConfig.js +343 -56
  8. package/app/configParts/groupUtils.js +226 -0
  9. package/app/configParts/loggerConfig.js +185 -0
  10. package/app/configParts/messagePersistenceService.js +307 -5
  11. package/app/configParts/sessionConfig.js +242 -0
  12. package/app/connection/baileysCompatibility.test.js +10 -1
  13. package/app/connection/baileysDbAuthState.js +205 -9
  14. package/app/connection/baileysLibsignalPatch.js +210 -0
  15. package/app/connection/groupOwnerWriteStateResolver.js +141 -0
  16. package/app/connection/socketController.js +694 -123
  17. package/app/connection/socketController.multiSession.test.js +128 -0
  18. package/app/controllers/messageController.js +1 -1
  19. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  20. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  21. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  22. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
  23. package/app/controllers/messageProcessingPipeline.js +90 -9
  24. package/app/controllers/messageProcessingPipeline.test.js +202 -0
  25. package/app/modules/adminModule/AGENT.md +1 -1
  26. package/app/modules/adminModule/commandConfig.json +3318 -1347
  27. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  28. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  29. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  30. package/app/modules/aiModule/AGENT.md +47 -30
  31. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  32. package/app/modules/aiModule/catCommand.js +132 -25
  33. package/app/modules/aiModule/commandConfig.json +114 -28
  34. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  35. package/app/modules/gameModule/AGENT.md +1 -1
  36. package/app/modules/gameModule/commandConfig.json +29 -0
  37. package/app/modules/menuModule/AGENT.md +1 -1
  38. package/app/modules/menuModule/commandConfig.json +45 -10
  39. package/app/modules/menuModule/menuCatalogService.js +190 -0
  40. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  41. package/app/modules/menuModule/menuDynamicService.js +511 -0
  42. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  43. package/app/modules/menuModule/menus.js +36 -5
  44. package/app/modules/playModule/AGENT.md +10 -5
  45. package/app/modules/playModule/commandConfig.json +74 -16
  46. package/app/modules/playModule/playCommandConstants.js +13 -7
  47. package/app/modules/playModule/playCommandCore.js +4 -6
  48. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  49. package/app/modules/playModule/playConfigRuntime.js +5 -6
  50. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  51. package/app/modules/quoteModule/AGENT.md +1 -1
  52. package/app/modules/quoteModule/commandConfig.json +29 -0
  53. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  54. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  55. package/app/modules/statsModule/AGENT.md +1 -1
  56. package/app/modules/statsModule/commandConfig.json +58 -0
  57. package/app/modules/stickerModule/AGENT.md +1 -1
  58. package/app/modules/stickerModule/commandConfig.json +145 -0
  59. package/app/modules/stickerPackModule/AGENT.md +1 -1
  60. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  61. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  62. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  63. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  64. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  65. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  66. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  67. package/app/modules/tiktokModule/AGENT.md +1 -1
  68. package/app/modules/tiktokModule/commandConfig.json +29 -0
  69. package/app/modules/userModule/AGENT.md +1 -1
  70. package/app/modules/userModule/commandConfig.json +29 -0
  71. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  72. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  73. package/app/observability/metrics.js +136 -0
  74. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  75. package/app/services/ai/geminiService.js +131 -7
  76. package/app/services/ai/geminiService.test.js +59 -2
  77. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  78. package/app/services/group/groupMetadataService.js +24 -1
  79. package/app/services/infra/dbWriteQueue.js +51 -21
  80. package/app/services/messaging/newsBroadcastService.js +843 -27
  81. package/app/services/multiSession/assignmentBalancerService.js +452 -0
  82. package/app/services/multiSession/groupOwnershipRepository.js +346 -0
  83. package/app/services/multiSession/groupOwnershipService.js +809 -0
  84. package/app/services/multiSession/groupOwnershipService.test.js +317 -0
  85. package/app/services/multiSession/sessionRegistryService.js +239 -0
  86. package/app/store/aiPromptStore.js +36 -19
  87. package/app/store/groupConfigStore.js +41 -5
  88. package/app/store/premiumUserStore.js +21 -7
  89. package/app/utils/antiLink/antiLinkModule.js +391 -25
  90. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  91. package/database/index.js +6 -0
  92. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  93. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  94. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  95. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  96. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  97. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  98. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  99. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  100. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  101. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  102. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  103. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  104. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  105. package/database/schema.sql +102 -1
  106. package/docker-compose.yml +4 -1
  107. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  108. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  109. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  110. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  111. package/docs/security/omnizap-static-security-headers.conf +25 -0
  112. package/ecosystem.prod.config.cjs +31 -11
  113. package/index.js +52 -18
  114. package/observability/alert-rules.yml +20 -0
  115. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  116. package/observability/mysql-setup.sql +4 -4
  117. package/observability/system-admin-observability.md +26 -0
  118. package/package.json +14 -6
  119. package/public/comandos/commands-catalog.json +2253 -78
  120. package/public/css/payments-react.css +478 -0
  121. package/public/js/apps/commandsReactApp.js +267 -87
  122. package/public/js/apps/createPackApp.js +3 -3
  123. package/public/js/apps/homeReactApp.js +2 -2
  124. package/public/js/apps/paymentsCancelReactApp.js +45 -0
  125. package/public/js/apps/paymentsReactApp.js +399 -0
  126. package/public/js/apps/paymentsSuccessReactApp.js +148 -0
  127. package/public/js/apps/stickersApp.js +255 -103
  128. package/public/js/apps/termsReactApp.js +57 -8
  129. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  130. package/public/js/apps/userReactApp.js +96 -47
  131. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  132. package/public/pages/pagamentos-cancelado.html +21 -0
  133. package/public/pages/pagamentos-sucesso.html +21 -0
  134. package/public/pages/pagamentos.html +30 -0
  135. package/public/pages/politica-de-privacidade.html +1 -1
  136. package/public/pages/stickers.html +5 -5
  137. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  138. package/public/pages/termos-de-uso.html +1 -1
  139. package/public/pages/user-password-reset.html +3 -4
  140. package/public/pages/user-systemadm.html +8 -462
  141. package/public/pages/user.html +1 -1
  142. package/scripts/clear-whatsapp-session.sh +123 -0
  143. package/scripts/core-ai-mode.mjs +163 -0
  144. package/scripts/deploy.sh +13 -0
  145. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  146. package/scripts/generate-commands-catalog.mjs +155 -0
  147. package/scripts/new-whatsapp-session.sh +564 -0
  148. package/scripts/security-web-surface-check.mjs +218 -0
  149. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  150. package/server/controllers/admin/systemAdminController.js +254 -0
  151. package/server/controllers/payments/paymentsController.js +731 -0
  152. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  153. package/server/controllers/system/contactController.js +9 -17
  154. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  155. package/server/controllers/system/systemController.js +228 -1
  156. package/server/controllers/userController.js +6 -0
  157. package/server/email/emailAutomationRuntime.js +36 -1
  158. package/server/email/emailAutomationService.js +42 -1
  159. package/server/email/emailTemplateService.js +140 -33
  160. package/server/http/httpRequestUtils.js +18 -14
  161. package/server/http/httpServer.js +8 -4
  162. package/server/middleware/securityHeaders.js +35 -3
  163. package/server/routes/admin/systemAdminRouter.js +6 -0
  164. package/server/routes/indexRouter.js +50 -6
  165. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  166. package/server/routes/payments/paymentsRouter.js +47 -0
  167. package/server/routes/static/staticPageRouter.js +30 -1
  168. package/server/utils/publicContact.js +31 -0
  169. package/utils/whatsapp/contactEnv.js +39 -0
  170. package/vite.config.mjs +5 -1
  171. package/app/modules/playModule/local/installYtDlp.js +0 -25
  172. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -1,6 +1,31 @@
1
1
  import logger from '#logger';
2
+ import { isGroupJid, normalizeJid } from '../config/index.js';
2
3
  import { findById, upsert } from '../../database/index.js';
3
4
 
5
+ const SYSTEM_CONFIG_PREFIX = 'system:';
6
+
7
+ const normalizeGroupConfigId = (groupId) => {
8
+ const raw = String(groupId || '').trim();
9
+ if (!raw) return '';
10
+ return normalizeJid(raw) || raw;
11
+ };
12
+
13
+ const isReservedSystemConfigId = (groupId) => groupId.startsWith(SYSTEM_CONFIG_PREFIX);
14
+
15
+ const assertWritableGroupConfigId = (groupId) => {
16
+ if (!groupId) {
17
+ throw new Error('O identificador do grupo é obrigatório para persistir configurações.');
18
+ }
19
+
20
+ if (isReservedSystemConfigId(groupId)) {
21
+ throw new Error(`O id ${groupId} é reservado para configurações de sistema e não pode ser salvo em group_configs.`);
22
+ }
23
+
24
+ if (!isGroupJid(groupId)) {
25
+ throw new Error(`O id ${groupId} não representa um grupo válido para group_configs.`);
26
+ }
27
+ };
28
+
4
29
  const groupConfigStore = {
5
30
  /**
6
31
  * Recupera a configuracao de um grupo especifico.
@@ -8,8 +33,17 @@ const groupConfigStore = {
8
33
  * @returns {object} A configuracao do grupo, ou um objeto vazio se nao encontrado.
9
34
  */
10
35
  getGroupConfig: async function (groupId) {
36
+ const normalizedGroupId = normalizeGroupConfigId(groupId);
37
+ if (!normalizedGroupId || !isGroupJid(normalizedGroupId)) {
38
+ return {};
39
+ }
40
+ if (isReservedSystemConfigId(normalizedGroupId)) {
41
+ logger.warn('Tentativa bloqueada de leitura de configuração reservada em group_configs.', { groupId: normalizedGroupId });
42
+ return {};
43
+ }
44
+
11
45
  try {
12
- const record = await findById('group_configs', groupId);
46
+ const record = await findById('group_configs', normalizedGroupId);
13
47
  if (!record || record.config === null || record.config === undefined) {
14
48
  return {};
15
49
  }
@@ -23,7 +57,7 @@ const groupConfigStore = {
23
57
  } catch (error) {
24
58
  logger.error('Error loading group configuration from DB:', {
25
59
  error: error.message,
26
- groupId,
60
+ groupId: normalizedGroupId,
27
61
  });
28
62
  return {};
29
63
  }
@@ -37,18 +71,20 @@ const groupConfigStore = {
37
71
  * @param {string} [newConfig.farewellMedia] - Caminho opcional para midia de despedida.
38
72
  */
39
73
  updateGroupConfig: async function (groupId, newConfig) {
40
- const currentConfig = await this.getGroupConfig(groupId);
74
+ const normalizedGroupId = normalizeGroupConfigId(groupId);
75
+ assertWritableGroupConfigId(normalizedGroupId);
76
+ const currentConfig = await this.getGroupConfig(normalizedGroupId);
41
77
  const updatedConfig = { ...currentConfig, ...newConfig };
42
78
  try {
43
79
  await upsert('group_configs', {
44
- id: groupId,
80
+ id: normalizedGroupId,
45
81
  config: JSON.stringify(updatedConfig),
46
82
  });
47
83
  return updatedConfig;
48
84
  } catch (error) {
49
85
  logger.error('Error updating group configuration in DB:', {
50
86
  error: error.message,
51
- groupId,
87
+ groupId: normalizedGroupId,
52
88
  });
53
89
  throw error;
54
90
  }
@@ -1,7 +1,10 @@
1
- import groupConfigStore from './groupConfigStore.js';
1
+ import { TABLES, executeQuery, withTransaction } from '../../database/index.js';
2
2
  import { isSameJidUser, normalizeJid } from '../config/index.js';
3
3
 
4
- const PREMIUM_CONFIG_ID = 'system:premium_users';
4
+ const PREMIUM_USERS_TABLE = TABLES.SYSTEM_PREMIUM_USERS;
5
+ const SELECT_PREMIUM_USERS_SQL = `SELECT id FROM \`${PREMIUM_USERS_TABLE}\` ORDER BY id ASC`;
6
+ const DELETE_ALL_PREMIUM_USERS_SQL = `DELETE FROM \`${PREMIUM_USERS_TABLE}\``;
7
+ const INSERT_PREMIUM_USER_SQL = `INSERT INTO \`${PREMIUM_USERS_TABLE}\` (id) VALUES (?)`;
5
8
 
6
9
  const normalizePremiumEntry = (value) => {
7
10
  const raw = String(value || '').trim();
@@ -23,22 +26,33 @@ const normalizeList = (list) => {
23
26
  return normalizedList;
24
27
  };
25
28
 
29
+ const loadPremiumUsersFromDb = async () => {
30
+ const rows = await executeQuery(SELECT_PREMIUM_USERS_SQL);
31
+ return normalizeList(rows.map((row) => row.id));
32
+ };
33
+
26
34
  const premiumUserStore = {
27
35
  getPremiumUsers: async function () {
28
- const config = await groupConfigStore.getGroupConfig(PREMIUM_CONFIG_ID);
29
- return normalizeList(config.premiumUsers);
36
+ return loadPremiumUsersFromDb();
30
37
  },
31
38
 
32
39
  setPremiumUsers: async function (premiumUsers) {
33
40
  const normalized = normalizeList(premiumUsers);
34
- await groupConfigStore.updateGroupConfig(PREMIUM_CONFIG_ID, { premiumUsers: normalized });
41
+
42
+ await withTransaction(async (connection) => {
43
+ await executeQuery(DELETE_ALL_PREMIUM_USERS_SQL, [], connection);
44
+ for (const premiumJid of normalized) {
45
+ await executeQuery(INSERT_PREMIUM_USER_SQL, [premiumJid], connection);
46
+ }
47
+ });
48
+
35
49
  return normalized;
36
50
  },
37
51
 
38
52
  addPremiumUsers: async function (usersToAdd) {
39
53
  const current = await this.getPremiumUsers();
40
54
  const updated = normalizeList([...current, ...usersToAdd]);
41
- await groupConfigStore.updateGroupConfig(PREMIUM_CONFIG_ID, { premiumUsers: updated });
55
+ await this.setPremiumUsers(updated);
42
56
  return updated;
43
57
  },
44
58
 
@@ -46,7 +60,7 @@ const premiumUserStore = {
46
60
  const current = await this.getPremiumUsers();
47
61
  const normalizedTargets = normalizeList(usersToRemove);
48
62
  const updated = current.filter((jid) => !normalizedTargets.some((target) => target === jid || isSameJidUser(target, jid)));
49
- await groupConfigStore.updateGroupConfig(PREMIUM_CONFIG_ID, { premiumUsers: updated });
63
+ await this.setPremiumUsers(updated);
50
64
  return updated;
51
65
  },
52
66
  };
@@ -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,59 +959,103 @@ 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
 
968
+ let removedParticipantId = '';
646
969
  try {
647
- const removedParticipantId = await removeParticipantWithFallback(sock, remoteJid, senderContext.removalCandidates);
970
+ removedParticipantId = await removeParticipantWithFallback(sock, normalizedRemoteJid, senderContext.removalCandidates);
648
971
  if (!removedParticipantId) {
649
972
  throw new Error('Nenhum candidato de participante pôde ser removido.');
650
973
  }
651
- const senderMention = senderContext.mentionJid || removedParticipantId || senderContext.primarySenderId;
652
- const senderUser = getJidUser(senderMention);
653
- await sendAndStore(sock, remoteJid, {
654
- text: `🚫 @${senderUser || 'usuario'} foi removido por enviar um link.`,
655
- mentions: senderMention ? [senderMention] : [],
974
+ } catch (error) {
975
+ logger.error(`Falha ao remover usuário com antilink: ${error.message}`, {
976
+ action: 'antilink_error',
977
+ groupId: normalizedRemoteJid,
978
+ userId: senderContext.primarySenderId,
979
+ senderCandidates: senderContext.senderCandidates,
980
+ error: error.stack,
656
981
  });
657
- await sendAndStore(sock, remoteJid, { delete: messageInfo.key });
982
+ return false;
983
+ }
658
984
 
659
- logger.info(`Usuário ${removedParticipantId || senderContext.primarySenderId} removido do grupo ${remoteJid} por enviar link.`, {
660
- action: 'antilink_remove',
661
- groupId: remoteJid,
985
+ const deletionCandidates = uniqueNormalizedJids([removedParticipantId, ...senderContext.senderCandidates]);
986
+ let purgeResult = {
987
+ requested: 0,
988
+ deleted: 0,
989
+ failed: 0,
990
+ rounds: 0,
991
+ roundsWithDeletes: 0,
992
+ };
993
+ try {
994
+ purgeResult = await purgeRecentMessagesFromRemovedSender({
995
+ sock,
996
+ messageInfo,
997
+ remoteJid: normalizedRemoteJid,
998
+ senderCandidates: deletionCandidates,
999
+ });
1000
+ } catch (error) {
1001
+ logger.warn('Falha ao limpar mensagens recentes após remoção por antilink.', {
1002
+ action: 'antilink_recent_delete_error',
1003
+ groupId: normalizedRemoteJid,
662
1004
  userId: removedParticipantId || senderContext.primarySenderId,
663
1005
  senderCandidates: senderContext.senderCandidates,
1006
+ error: error?.message,
664
1007
  });
1008
+ }
1009
+
1010
+ const senderMention = senderContext.mentionJid || removedParticipantId || senderContext.primarySenderId;
1011
+ const senderUser = getJidUser(senderMention);
1012
+ const recentDeleteLine = purgeResult.deleted > 0 ? `\n🧹 ${purgeResult.deleted} mensagem(ns) dos últimos ${ANTILINK_DELETE_WINDOW_MINUTES} minuto(s) foram apagadas.` : '';
665
1013
 
666
- return true;
1014
+ try {
1015
+ await sendMessageWithFallback(sock, normalizedRemoteJid, {
1016
+ text: `🚫 @${senderUser || 'usuario'} foi removido por enviar um link.${recentDeleteLine}`,
1017
+ mentions: senderMention ? [senderMention] : [],
1018
+ });
667
1019
  } catch (error) {
668
- logger.error(`Falha ao remover usuário com antilink: ${error.message}`, {
669
- action: 'antilink_error',
670
- groupId: remoteJid,
671
- userId: senderContext.primarySenderId,
1020
+ logger.warn('Falha ao enviar aviso de remoção por antilink.', {
1021
+ action: 'antilink_remove_notice_error',
1022
+ groupId: normalizedRemoteJid,
1023
+ userId: removedParticipantId || senderContext.primarySenderId,
672
1024
  senderCandidates: senderContext.senderCandidates,
673
- error: error.stack,
1025
+ error: error?.message,
674
1026
  });
675
1027
  }
1028
+
1029
+ logger.info(`Usuário ${removedParticipantId || senderContext.primarySenderId} removido do grupo ${normalizedRemoteJid} por enviar link.`, {
1030
+ action: 'antilink_remove',
1031
+ groupId: normalizedRemoteJid,
1032
+ userId: removedParticipantId || senderContext.primarySenderId,
1033
+ senderCandidates: senderContext.senderCandidates,
1034
+ deletedRecentMessages: purgeResult.deleted,
1035
+ failedRecentMessageDeletes: purgeResult.failed,
1036
+ requestedRecentMessageDeletes: purgeResult.requested,
1037
+ deleteRevalidationRounds: purgeResult.rounds,
1038
+ deleteRevalidationRoundsWithDeletes: purgeResult.roundsWithDeletes,
1039
+ });
1040
+
1041
+ return true;
676
1042
  } else if (isAdmin && !senderIsBot) {
677
1043
  try {
678
1044
  const senderMention = senderContext.mentionJid || senderContext.primarySenderId;
679
1045
  const senderUser = getJidUser(senderMention);
680
- await sendAndStore(sock, remoteJid, {
1046
+ await sendMessageWithFallback(sock, normalizedRemoteJid, {
681
1047
  text: `ⓘ @${senderUser || 'admin'} (admin) enviou um link.`,
682
1048
  mentions: senderMention ? [senderMention] : [],
683
1049
  });
684
- logger.info(`Admin ${senderContext.primarySenderId} enviou um link no grupo ${remoteJid} (aviso enviado).`, {
1050
+ logger.info(`Admin ${senderContext.primarySenderId} enviou um link no grupo ${normalizedRemoteJid} (aviso enviado).`, {
685
1051
  action: 'antilink_admin_link_detected',
686
- groupId: remoteJid,
1052
+ groupId: normalizedRemoteJid,
687
1053
  userId: senderContext.primarySenderId,
688
1054
  });
689
1055
  } catch (error) {
690
1056
  logger.error(`Falha ao enviar aviso de link de admin: ${error.message}`, {
691
1057
  action: 'antilink_admin_warning_error',
692
- groupId: remoteJid,
1058
+ groupId: normalizedRemoteJid,
693
1059
  userId: senderContext.primarySenderId,
694
1060
  error: error.stack,
695
1061
  });