@omnizap-system/omnizap 2.6.1 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.env.example +54 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -17,6 +17,7 @@ const ENV_OVERRIDES = {
17
17
  DB_MONITOR_ENABLED: 'false',
18
18
  METRICS_ENABLED: 'false',
19
19
  ADMIN_AI_HELP_SCHEDULER_ENABLED: 'false',
20
+ WHATSAPP_ADMIN_JID: OWNER_JID,
20
21
  USER_ADMIN: OWNER_PHONE,
21
22
  };
22
23
 
@@ -56,27 +57,113 @@ const normalizeSql = (sql) =>
56
57
  const createDbHarness = () => {
57
58
  const groupConfigRows = new Map();
58
59
  const groupMetadataRows = new Map();
60
+ const premiumUserRows = new Set();
61
+ const groupUserWarningsRows = [];
62
+ let warningAutoIncrement = 1;
59
63
 
60
64
  const execute = async (sql, params = []) => {
61
65
  const normalized = normalizeSql(sql);
66
+ const normalizedNoTicks = normalized.replaceAll('`', '');
62
67
 
63
- if (normalized.startsWith('select * from `groups_metadata` where id = ?')) {
68
+ if (normalizedNoTicks.startsWith('select * from groups_metadata where id = ?')) {
64
69
  const row = groupMetadataRows.get(params[0]);
65
70
  return [[row].filter(Boolean), []];
66
71
  }
67
72
 
68
- if (normalized.startsWith('select * from `group_configs` where id = ?')) {
73
+ if (normalizedNoTicks.startsWith('select * from group_configs where id = ?')) {
69
74
  const row = groupConfigRows.get(params[0]);
70
75
  return [[row].filter(Boolean), []];
71
76
  }
72
77
 
73
- if (normalized.startsWith('insert into `group_configs`')) {
78
+ if (normalizedNoTicks.startsWith('insert into group_configs')) {
74
79
  const [id, config] = params;
75
80
  groupConfigRows.set(id, { id, config: String(config) });
76
81
  return [{ affectedRows: 1 }, []];
77
82
  }
78
83
 
79
- if (normalized.includes('from lid_map') || normalized.includes('from `lid_map`') || normalized.includes('into `lid_map`') || normalized.startsWith('update `messages`')) {
84
+ if (normalizedNoTicks.startsWith('select id from system_premium_users')) {
85
+ const rows = Array.from(premiumUserRows.values())
86
+ .sort((left, right) => String(left).localeCompare(String(right)))
87
+ .map((id) => ({ id }));
88
+ return [rows, []];
89
+ }
90
+
91
+ if (normalizedNoTicks.startsWith('delete from system_premium_users')) {
92
+ premiumUserRows.clear();
93
+ return [{ affectedRows: 1 }, []];
94
+ }
95
+
96
+ if (normalizedNoTicks.startsWith('insert into system_premium_users')) {
97
+ const [id] = params;
98
+ premiumUserRows.add(String(id || ''));
99
+ return [{ affectedRows: 1 }, []];
100
+ }
101
+
102
+ if (normalizedNoTicks.startsWith('insert into group_user_warnings')) {
103
+ const [groupId, participantJid, warnedByJid, reason] = params;
104
+ groupUserWarningsRows.push({
105
+ id: warningAutoIncrement++,
106
+ group_id: String(groupId || ''),
107
+ participant_jid: String(participantJid || '').toLowerCase(),
108
+ warned_by_jid: warnedByJid ? String(warnedByJid).toLowerCase() : null,
109
+ reason: reason ? String(reason) : null,
110
+ created_at: new Date(__timeNowMs()).toISOString(),
111
+ });
112
+ return [{ affectedRows: 1 }, []];
113
+ }
114
+
115
+ if (normalizedNoTicks.startsWith('select count(*) as total from group_user_warnings')) {
116
+ const [groupId, participantJid] = params;
117
+ const filtered = groupUserWarningsRows.filter((row) => row.group_id === String(groupId || '') && row.participant_jid === String(participantJid || '').toLowerCase());
118
+ return [[{ total: filtered.length }], []];
119
+ }
120
+
121
+ if (normalizedNoTicks.startsWith('select id, group_id, participant_jid, warned_by_jid, reason, created_at from group_user_warnings')) {
122
+ const [groupId, participantJid, limit] = params;
123
+ const safeLimit = Number.parseInt(String(limit || 0), 10);
124
+ const filtered = groupUserWarningsRows
125
+ .filter((row) => row.group_id === String(groupId || '') && row.participant_jid === String(participantJid || '').toLowerCase())
126
+ .sort((left, right) => right.id - left.id)
127
+ .slice(0, Number.isFinite(safeLimit) && safeLimit > 0 ? safeLimit : 20)
128
+ .map((row) => ({ ...row }));
129
+ return [filtered, []];
130
+ }
131
+
132
+ if (normalizedNoTicks.startsWith('delete from group_user_warnings') && normalizedNoTicks.includes('order by id desc limit ?')) {
133
+ const [groupId, participantJid, limit] = params;
134
+ const safeGroupId = String(groupId || '');
135
+ const safeParticipantJid = String(participantJid || '').toLowerCase();
136
+ const safeLimit = Number.parseInt(String(limit || 0), 10);
137
+ const rowsToDelete = groupUserWarningsRows
138
+ .filter((row) => row.group_id === safeGroupId && row.participant_jid === safeParticipantJid)
139
+ .sort((left, right) => right.id - left.id)
140
+ .slice(0, Number.isFinite(safeLimit) && safeLimit > 0 ? safeLimit : 1)
141
+ .map((row) => row.id);
142
+
143
+ if (rowsToDelete.length > 0) {
144
+ for (let index = groupUserWarningsRows.length - 1; index >= 0; index -= 1) {
145
+ if (!rowsToDelete.includes(groupUserWarningsRows[index].id)) continue;
146
+ groupUserWarningsRows.splice(index, 1);
147
+ }
148
+ }
149
+
150
+ return [{ affectedRows: rowsToDelete.length }, []];
151
+ }
152
+
153
+ if (normalizedNoTicks.startsWith('delete from group_user_warnings')) {
154
+ const [groupId, participantJid] = params;
155
+ const safeGroupId = String(groupId || '');
156
+ const safeParticipantJid = String(participantJid || '').toLowerCase();
157
+ let removed = 0;
158
+ for (let index = groupUserWarningsRows.length - 1; index >= 0; index -= 1) {
159
+ if (groupUserWarningsRows[index].group_id !== safeGroupId || groupUserWarningsRows[index].participant_jid !== safeParticipantJid) continue;
160
+ groupUserWarningsRows.splice(index, 1);
161
+ removed += 1;
162
+ }
163
+ return [{ affectedRows: removed }, []];
164
+ }
165
+
166
+ if (normalizedNoTicks.includes('from lid_map') || normalizedNoTicks.includes('into lid_map') || normalizedNoTicks.startsWith('update messages')) {
80
167
  return [[], []];
81
168
  }
82
169
 
@@ -102,11 +189,19 @@ const createDbHarness = () => {
102
189
  return row ? JSON.parse(row.config) : {};
103
190
  };
104
191
 
192
+ const setPremiumUsers = (premiumUsers) => {
193
+ premiumUserRows.clear();
194
+ for (const premiumUser of premiumUsers || []) {
195
+ premiumUserRows.add(String(premiumUser || ''));
196
+ }
197
+ };
198
+
105
199
  return {
106
200
  execute,
107
201
  setGroupParticipants,
108
202
  setGroupConfig,
109
203
  getGroupConfig,
204
+ setPremiumUsers,
110
205
  };
111
206
  };
112
207
 
@@ -135,10 +230,26 @@ const createSockStub = () => {
135
230
  };
136
231
  };
137
232
 
138
- const buildMessageInfo = (participant = OWNER_JID) => ({
139
- key: { participant },
140
- message: {},
141
- });
233
+ const buildMessageInfo = (participant = OWNER_JID, { mentionedJid = [], replyParticipant = '' } = {}) => {
234
+ const contextInfo = {};
235
+ if (Array.isArray(mentionedJid) && mentionedJid.length > 0) {
236
+ contextInfo.mentionedJid = mentionedJid;
237
+ }
238
+ if (replyParticipant) {
239
+ contextInfo.participant = replyParticipant;
240
+ }
241
+
242
+ return {
243
+ key: { participant },
244
+ message: Object.keys(contextInfo).length
245
+ ? {
246
+ extendedTextMessage: {
247
+ contextInfo,
248
+ },
249
+ }
250
+ : {},
251
+ };
252
+ };
142
253
 
143
254
  const runAdminCommand = async ({ command, args = [], text = args.join(' '), sock, senderJid = OWNER_JID, remoteJid = GROUP_JID, isGroupMessage = true, messageInfo, botJid = BOT_JID }) =>
144
255
  handleAdminCommand({
@@ -182,6 +293,13 @@ after(() => {
182
293
  test('isAdminCommand reconhece comandos válidos', () => {
183
294
  assert.equal(isAdminCommand('nsfw'), true);
184
295
  assert.equal(isAdminCommand('banir'), true);
296
+ assert.equal(isAdminCommand('warn'), true);
297
+ assert.equal(isAdminCommand('warnings'), true);
298
+ assert.equal(isAdminCommand('clearwarn'), true);
299
+ assert.equal(isAdminCommand('warnlimit'), true);
300
+ assert.equal(isAdminCommand('stickerallowance'), true);
301
+ assert.equal(isAdminCommand('noticiasfiltro'), true);
302
+ assert.equal(isAdminCommand('grupoaudit'), true);
185
303
  assert.equal(isAdminCommand('comando-inexistente'), false);
186
304
  });
187
305
 
@@ -289,6 +407,167 @@ test('ban bloqueia tentativa de remover o próprio bot', async () => {
289
407
  assert.equal(messages[0].content.text, 'Operação cancelada: o bot não pode remover a própria conta.');
290
408
  });
291
409
 
410
+ test('warn registra advertência e warnings lista histórico', async () => {
411
+ const { sock, messages } = createSockStub();
412
+ dbHarness.setGroupParticipants(GROUP_JID, [{ id: OWNER_JID, admin: 'admin' }]);
413
+
414
+ await runAdminCommand({
415
+ command: 'warn',
416
+ args: ['@alvo', 'spam', 'repetitivo'],
417
+ sock,
418
+ messageInfo: buildMessageInfo(OWNER_JID, { mentionedJid: [TARGET_JID] }),
419
+ });
420
+
421
+ assert.match(messages[messages.length - 1].content.text, /Advertência registrada/i);
422
+ assert.match(messages[messages.length - 1].content.text, /spam repetitivo/i);
423
+
424
+ await runAdminCommand({
425
+ command: 'warnings',
426
+ args: [],
427
+ sock,
428
+ messageInfo: buildMessageInfo(OWNER_JID, { replyParticipant: TARGET_JID }),
429
+ });
430
+
431
+ assert.match(messages[messages.length - 1].content.text, /Histórico de advertências/i);
432
+ assert.match(messages[messages.length - 1].content.text, /Total neste grupo: \*1\*/i);
433
+ assert.match(messages[messages.length - 1].content.text, /spam repetitivo/i);
434
+ });
435
+
436
+ test('warn aplica auto-ban no limite padrão de 3 advertências', async () => {
437
+ const { sock, messages, participantUpdates } = createSockStub();
438
+ dbHarness.setGroupParticipants(GROUP_JID, [{ id: OWNER_JID, admin: 'admin' }]);
439
+
440
+ await runAdminCommand({
441
+ command: 'warn',
442
+ args: [TARGET_JID, 'motivo-1'],
443
+ sock,
444
+ });
445
+ await runAdminCommand({
446
+ command: 'warn',
447
+ args: [TARGET_JID, 'motivo-2'],
448
+ sock,
449
+ });
450
+ await runAdminCommand({
451
+ command: 'warn',
452
+ args: [TARGET_JID, 'motivo-3'],
453
+ sock,
454
+ });
455
+
456
+ assert.equal(participantUpdates.length, 1);
457
+ assert.deepEqual(participantUpdates[0], {
458
+ groupId: GROUP_JID,
459
+ participants: [TARGET_JID],
460
+ action: 'remove',
461
+ });
462
+ assert.match(messages[messages.length - 1].content.text, /Auto-ban configurado para: \*3\*/i);
463
+ assert.match(messages[messages.length - 1].content.text, /Limite atingido/i);
464
+ });
465
+
466
+ test('warnlimit permite ajustar limite por grupo e resetar para padrão', async () => {
467
+ const { sock, messages, participantUpdates } = createSockStub();
468
+ dbHarness.setGroupParticipants(GROUP_JID, [{ id: OWNER_JID, admin: 'admin' }]);
469
+
470
+ await runAdminCommand({
471
+ command: 'warnlimit',
472
+ args: ['2'],
473
+ sock,
474
+ });
475
+ assert.equal(dbHarness.getGroupConfig(GROUP_JID).warnAutoBanThreshold, 2);
476
+
477
+ await runAdminCommand({
478
+ command: 'warn',
479
+ args: [TARGET_JID, 'motivo-1'],
480
+ sock,
481
+ });
482
+ await runAdminCommand({
483
+ command: 'warn',
484
+ args: [TARGET_JID, 'motivo-2'],
485
+ sock,
486
+ });
487
+
488
+ assert.equal(participantUpdates.length, 1);
489
+ assert.deepEqual(participantUpdates[0], {
490
+ groupId: GROUP_JID,
491
+ participants: [TARGET_JID],
492
+ action: 'remove',
493
+ });
494
+
495
+ await runAdminCommand({
496
+ command: 'warnlimit',
497
+ args: ['status'],
498
+ sock,
499
+ });
500
+ assert.match(messages[messages.length - 1].content.text, /Limite atual de auto-ban/i);
501
+ assert.match(messages[messages.length - 1].content.text, /\*2\*/);
502
+
503
+ await runAdminCommand({
504
+ command: 'warnlimit',
505
+ args: ['reset'],
506
+ sock,
507
+ });
508
+ assert.equal(dbHarness.getGroupConfig(GROUP_JID).warnAutoBanThreshold, null);
509
+ assert.match(messages[messages.length - 1].content.text, /padrão: \*3\*/i);
510
+ });
511
+
512
+ test('clearwarn remove parcialmente e depois remove todas as advertências', async () => {
513
+ const { sock, messages } = createSockStub();
514
+ dbHarness.setGroupParticipants(GROUP_JID, [{ id: OWNER_JID, admin: 'admin' }]);
515
+
516
+ await runAdminCommand({
517
+ command: 'warn',
518
+ args: [TARGET_JID, 'motivo-1'],
519
+ sock,
520
+ });
521
+ await runAdminCommand({
522
+ command: 'warn',
523
+ args: [TARGET_JID, 'motivo-2'],
524
+ sock,
525
+ });
526
+ await runAdminCommand({
527
+ command: 'warn',
528
+ args: [TARGET_JID, 'motivo-3'],
529
+ sock,
530
+ });
531
+
532
+ await runAdminCommand({
533
+ command: 'clearwarn',
534
+ args: [TARGET_JID, '2'],
535
+ sock,
536
+ });
537
+ assert.match(messages[messages.length - 1].content.text, /removi \*2 advertência\(s\)\*/i);
538
+ assert.match(messages[messages.length - 1].content.text, /Advertências restantes neste grupo: \*1\*/i);
539
+
540
+ await runAdminCommand({
541
+ command: 'clearwarn',
542
+ args: [TARGET_JID, 'all'],
543
+ sock,
544
+ });
545
+ assert.match(messages[messages.length - 1].content.text, /todas as advertências \(1\)/i);
546
+ assert.match(messages[messages.length - 1].content.text, /Advertências restantes neste grupo: \*0\*/i);
547
+
548
+ await runAdminCommand({
549
+ command: 'warnings',
550
+ args: [TARGET_JID],
551
+ sock,
552
+ });
553
+ assert.match(messages[messages.length - 1].content.text, /não possui advertências/i);
554
+ });
555
+
556
+ test('clearwarn retorna uso ao receber quantidade inválida', async () => {
557
+ const { sock, messages } = createSockStub();
558
+ dbHarness.setGroupParticipants(GROUP_JID, [{ id: OWNER_JID, admin: 'admin' }]);
559
+
560
+ await runAdminCommand({
561
+ command: 'clearwarn',
562
+ args: [TARGET_JID, 'zero'],
563
+ sock,
564
+ });
565
+
566
+ assert.equal(messages.length, 1);
567
+ assert.match(messages[0].content.text, /Formato de uso/i);
568
+ assert.match(messages[0].content.text, /clearwarn/i);
569
+ });
570
+
292
571
  test('premium exige admin principal e lista usuários quando autorizado', async () => {
293
572
  const texts = getAdminTextConfig();
294
573
  const denied = createSockStub();
@@ -306,7 +585,7 @@ test('premium exige admin principal e lista usuários quando autorizado', async
306
585
  assert.equal(denied.messages.length, 1);
307
586
  assert.equal(denied.messages[0].content.text, texts.owner_only_command_message);
308
587
 
309
- dbHarness.setGroupConfig('system:premium_users', { premiumUsers: [TARGET_JID] });
588
+ dbHarness.setPremiumUsers([TARGET_JID]);
310
589
  const allowed = createSockStub();
311
590
 
312
591
  await runAdminCommand({
@@ -349,3 +628,90 @@ test('prefix atualiza, consulta status e reseta para padrão', async () => {
349
628
  });
350
629
  assert.equal(dbHarness.getGroupConfig(GROUP_JID).commandPrefix, null);
351
630
  });
631
+
632
+ test('stickerallowance atualiza e consulta limite por janela', async () => {
633
+ const { sock, messages } = createSockStub();
634
+ dbHarness.setGroupParticipants(GROUP_JID, [{ id: OWNER_JID, admin: 'admin' }]);
635
+
636
+ await runAdminCommand({
637
+ command: 'stickerallowance',
638
+ args: ['4'],
639
+ sock,
640
+ });
641
+
642
+ const updatedConfig = dbHarness.getGroupConfig(GROUP_JID);
643
+ assert.equal(updatedConfig.stickerFocusMessageAllowance, 4);
644
+ assert.equal(updatedConfig.stickerFocusMessageAllowanceCount, 4);
645
+
646
+ await runAdminCommand({
647
+ command: 'stickerallowance',
648
+ args: ['status'],
649
+ sock,
650
+ });
651
+
652
+ assert.match(messages[messages.length - 1].content.text, /Limite atual de mensagens por usuário/i);
653
+ assert.match(messages[messages.length - 1].content.text, /\*4\*/);
654
+ });
655
+
656
+ test('noticiasfiltro aplica source/tag e trending no config do grupo', async () => {
657
+ const { sock } = createSockStub();
658
+ dbHarness.setGroupParticipants(GROUP_JID, [{ id: OWNER_JID, admin: 'admin' }]);
659
+
660
+ await runAdminCommand({
661
+ command: 'noticiasfiltro',
662
+ args: ['source', 'add', 'ann,mal'],
663
+ sock,
664
+ });
665
+
666
+ await runAdminCommand({
667
+ command: 'noticiasfiltro',
668
+ args: ['tag', 'add', 'shounen'],
669
+ sock,
670
+ });
671
+
672
+ await runAdminCommand({
673
+ command: 'noticiasfiltro',
674
+ args: ['trending', 'on'],
675
+ sock,
676
+ });
677
+
678
+ const updatedConfig = dbHarness.getGroupConfig(GROUP_JID);
679
+ assert.deepEqual(updatedConfig.newsSourceIds, ['ann', 'mal']);
680
+ assert.deepEqual(updatedConfig.newsEntitySlugs, ['shounen']);
681
+ assert.equal(updatedConfig.newsOnlyTrending, true);
682
+ assert.equal(updatedConfig.newsFilters.onlyTrending, true);
683
+ });
684
+
685
+ test('grupoaudit retorna resumo consolidado do grupo', async () => {
686
+ const { sock, messages } = createSockStub();
687
+ dbHarness.setGroupParticipants(GROUP_JID, [{ id: OWNER_JID, admin: 'admin' }]);
688
+ dbHarness.setGroupConfig(GROUP_JID, {
689
+ commandPrefix: '!',
690
+ nsfwEnabled: true,
691
+ autoStickerEnabled: false,
692
+ stickerFocusEnabled: true,
693
+ stickerFocusMessageCooldownMinutes: 30,
694
+ stickerFocusMessageAllowance: 3,
695
+ captchaEnabled: true,
696
+ autoApproveRequestsEnabled: false,
697
+ antilinkEnabled: true,
698
+ antilinkAllowedNetworks: ['youtube'],
699
+ antilinkAllowedDomains: ['example.com'],
700
+ newsEnabled: true,
701
+ newsSentIds: ['n1', 'n2'],
702
+ newsLastSentAt: '2026-03-18T00:00:00.000Z',
703
+ welcomeMessageEnabled: true,
704
+ farewellMessageEnabled: false,
705
+ });
706
+
707
+ await runAdminCommand({
708
+ command: 'grupoaudit',
709
+ args: [],
710
+ sock,
711
+ });
712
+
713
+ assert.equal(messages.length, 1);
714
+ assert.match(messages[0].content.text, /Auditoria do Grupo/i);
715
+ assert.match(messages[0].content.text, /Notícias enviadas: \*2\*/i);
716
+ assert.match(messages[0].content.text, /Antilink: \*ativado\*/i);
717
+ });
@@ -0,0 +1,152 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const MAX_REASON_CHARS = 500;
4
+ const DEFAULT_LIST_LIMIT = 20;
5
+
6
+ const normalizeGroupId = (value) => {
7
+ const normalized = String(value || '')
8
+ .trim()
9
+ .slice(0, 255);
10
+ return normalized || null;
11
+ };
12
+
13
+ const normalizeParticipantJid = (value) => {
14
+ const normalized = String(value || '')
15
+ .trim()
16
+ .toLowerCase()
17
+ .slice(0, 255);
18
+ return normalized || null;
19
+ };
20
+
21
+ const normalizeReason = (value) => {
22
+ const normalized = String(value || '')
23
+ .replace(/\s+/g, ' ')
24
+ .trim()
25
+ .slice(0, MAX_REASON_CHARS);
26
+ return normalized || null;
27
+ };
28
+
29
+ const normalizeWarnByJid = (value) => {
30
+ const normalized = String(value || '')
31
+ .trim()
32
+ .toLowerCase()
33
+ .slice(0, 255);
34
+ return normalized || null;
35
+ };
36
+
37
+ const toPositiveInt = (value, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) => {
38
+ const numeric = Number.parseInt(String(value ?? ''), 10);
39
+ if (!Number.isFinite(numeric) || numeric < min) return fallback;
40
+ return Math.max(min, Math.min(max, numeric));
41
+ };
42
+
43
+ export const addGroupWarning = async ({ groupId, participantJid, warnedByJid, reason = null } = {}) => {
44
+ const safeGroupId = normalizeGroupId(groupId);
45
+ const safeParticipantJid = normalizeParticipantJid(participantJid);
46
+ const safeWarnedByJid = normalizeWarnByJid(warnedByJid);
47
+
48
+ if (!safeGroupId || !safeParticipantJid) {
49
+ throw new Error('group_warning_invalid_target');
50
+ }
51
+
52
+ await executeQuery(
53
+ `INSERT INTO ${TABLES.GROUP_USER_WARNINGS}
54
+ (group_id, participant_jid, warned_by_jid, reason)
55
+ VALUES (?, ?, ?, ?)`,
56
+ [safeGroupId, safeParticipantJid, safeWarnedByJid, normalizeReason(reason)],
57
+ );
58
+
59
+ return true;
60
+ };
61
+
62
+ export const countGroupWarnings = async ({ groupId, participantJid } = {}) => {
63
+ const safeGroupId = normalizeGroupId(groupId);
64
+ const safeParticipantJid = normalizeParticipantJid(participantJid);
65
+
66
+ if (!safeGroupId || !safeParticipantJid) return 0;
67
+
68
+ const rows = await executeQuery(
69
+ `SELECT COUNT(*) AS total
70
+ FROM ${TABLES.GROUP_USER_WARNINGS}
71
+ WHERE group_id = ? AND participant_jid = ?`,
72
+ [safeGroupId, safeParticipantJid],
73
+ );
74
+
75
+ const total = Number(rows?.[0]?.total || 0);
76
+ return Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;
77
+ };
78
+
79
+ export const listGroupWarnings = async ({ groupId, participantJid, limit = DEFAULT_LIST_LIMIT } = {}) => {
80
+ const safeGroupId = normalizeGroupId(groupId);
81
+ const safeParticipantJid = normalizeParticipantJid(participantJid);
82
+ const safeLimit = toPositiveInt(limit, DEFAULT_LIST_LIMIT, 1, 100);
83
+
84
+ if (!safeGroupId || !safeParticipantJid) return [];
85
+
86
+ const rows = await executeQuery(
87
+ `SELECT id, group_id, participant_jid, warned_by_jid, reason, created_at
88
+ FROM ${TABLES.GROUP_USER_WARNINGS}
89
+ WHERE group_id = ? AND participant_jid = ?
90
+ ORDER BY id DESC
91
+ LIMIT ?`,
92
+ [safeGroupId, safeParticipantJid, safeLimit],
93
+ );
94
+
95
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
96
+ id: Number(row?.id || 0),
97
+ groupId: normalizeGroupId(row?.group_id),
98
+ participantJid: normalizeParticipantJid(row?.participant_jid),
99
+ warnedByJid: normalizeWarnByJid(row?.warned_by_jid),
100
+ reason: normalizeReason(row?.reason),
101
+ createdAt: row?.created_at || null,
102
+ }));
103
+ };
104
+
105
+ export const clearGroupWarnings = async ({ groupId, participantJid, clearAll = false, limit = 1 } = {}) => {
106
+ const safeGroupId = normalizeGroupId(groupId);
107
+ const safeParticipantJid = normalizeParticipantJid(participantJid);
108
+ const safeLimit = toPositiveInt(limit, 1, 1, 500);
109
+
110
+ if (!safeGroupId || !safeParticipantJid) {
111
+ return {
112
+ removedCount: 0,
113
+ remainingCount: 0,
114
+ };
115
+ }
116
+
117
+ const beforeCount = await countGroupWarnings({
118
+ groupId: safeGroupId,
119
+ participantJid: safeParticipantJid,
120
+ });
121
+ if (beforeCount <= 0) {
122
+ return {
123
+ removedCount: 0,
124
+ remainingCount: 0,
125
+ };
126
+ }
127
+
128
+ if (clearAll) {
129
+ await executeQuery(
130
+ `DELETE FROM ${TABLES.GROUP_USER_WARNINGS}
131
+ WHERE group_id = ? AND participant_jid = ?`,
132
+ [safeGroupId, safeParticipantJid],
133
+ );
134
+ } else {
135
+ await executeQuery(
136
+ `DELETE FROM ${TABLES.GROUP_USER_WARNINGS}
137
+ WHERE group_id = ? AND participant_jid = ?
138
+ ORDER BY id DESC
139
+ LIMIT ?`,
140
+ [safeGroupId, safeParticipantJid, safeLimit],
141
+ );
142
+ }
143
+
144
+ const remainingCount = await countGroupWarnings({
145
+ groupId: safeGroupId,
146
+ participantJid: safeParticipantJid,
147
+ });
148
+ return {
149
+ removedCount: Math.max(0, beforeCount - remainingCount),
150
+ remainingCount,
151
+ };
152
+ };