@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,3 +1,4 @@
1
+ import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
1
2
  import assert from 'node:assert/strict';
2
3
  import { after, afterEach, beforeEach, test } from 'node:test';
3
4
 
@@ -16,6 +17,7 @@ const ENV_OVERRIDES = {
16
17
  DB_MONITOR_ENABLED: 'false',
17
18
  METRICS_ENABLED: 'false',
18
19
  ADMIN_AI_HELP_SCHEDULER_ENABLED: 'false',
20
+ WHATSAPP_ADMIN_JID: OWNER_JID,
19
21
  USER_ADMIN: OWNER_PHONE,
20
22
  };
21
23
 
@@ -26,7 +28,7 @@ for (const [key, value] of Object.entries(ENV_OVERRIDES)) {
26
28
  }
27
29
 
28
30
  const originalArgv1 = process.argv[1];
29
- process.argv[1] = '/root/omnizap-system/database/init.js';
31
+ process.argv[1] = new URL('../../../database/init.js', import.meta.url).pathname;
30
32
 
31
33
  let pool;
32
34
  let handleAdminCommand;
@@ -55,27 +57,113 @@ const normalizeSql = (sql) =>
55
57
  const createDbHarness = () => {
56
58
  const groupConfigRows = new Map();
57
59
  const groupMetadataRows = new Map();
60
+ const premiumUserRows = new Set();
61
+ const groupUserWarningsRows = [];
62
+ let warningAutoIncrement = 1;
58
63
 
59
64
  const execute = async (sql, params = []) => {
60
65
  const normalized = normalizeSql(sql);
66
+ const normalizedNoTicks = normalized.replaceAll('`', '');
61
67
 
62
- if (normalized.startsWith('select * from `groups_metadata` where id = ?')) {
68
+ if (normalizedNoTicks.startsWith('select * from groups_metadata where id = ?')) {
63
69
  const row = groupMetadataRows.get(params[0]);
64
70
  return [[row].filter(Boolean), []];
65
71
  }
66
72
 
67
- if (normalized.startsWith('select * from `group_configs` where id = ?')) {
73
+ if (normalizedNoTicks.startsWith('select * from group_configs where id = ?')) {
68
74
  const row = groupConfigRows.get(params[0]);
69
75
  return [[row].filter(Boolean), []];
70
76
  }
71
77
 
72
- if (normalized.startsWith('insert into `group_configs`')) {
78
+ if (normalizedNoTicks.startsWith('insert into group_configs')) {
73
79
  const [id, config] = params;
74
80
  groupConfigRows.set(id, { id, config: String(config) });
75
81
  return [{ affectedRows: 1 }, []];
76
82
  }
77
83
 
78
- 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')) {
79
167
  return [[], []];
80
168
  }
81
169
 
@@ -101,11 +189,19 @@ const createDbHarness = () => {
101
189
  return row ? JSON.parse(row.config) : {};
102
190
  };
103
191
 
192
+ const setPremiumUsers = (premiumUsers) => {
193
+ premiumUserRows.clear();
194
+ for (const premiumUser of premiumUsers || []) {
195
+ premiumUserRows.add(String(premiumUser || ''));
196
+ }
197
+ };
198
+
104
199
  return {
105
200
  execute,
106
201
  setGroupParticipants,
107
202
  setGroupConfig,
108
203
  getGroupConfig,
204
+ setPremiumUsers,
109
205
  };
110
206
  };
111
207
 
@@ -123,7 +219,7 @@ const createSockStub = () => {
123
219
  return {
124
220
  key: { remoteJid: jid },
125
221
  message: content,
126
- messageTimestamp: Math.floor(Date.now() / 1000),
222
+ messageTimestamp: Math.floor(__timeNowMs() / 1000),
127
223
  };
128
224
  },
129
225
  groupParticipantsUpdate: async (groupId, participants, action) => {
@@ -134,10 +230,26 @@ const createSockStub = () => {
134
230
  };
135
231
  };
136
232
 
137
- const buildMessageInfo = (participant = OWNER_JID) => ({
138
- key: { participant },
139
- message: {},
140
- });
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
+ };
141
253
 
142
254
  const runAdminCommand = async ({ command, args = [], text = args.join(' '), sock, senderJid = OWNER_JID, remoteJid = GROUP_JID, isGroupMessage = true, messageInfo, botJid = BOT_JID }) =>
143
255
  handleAdminCommand({
@@ -181,6 +293,13 @@ after(() => {
181
293
  test('isAdminCommand reconhece comandos válidos', () => {
182
294
  assert.equal(isAdminCommand('nsfw'), true);
183
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);
184
303
  assert.equal(isAdminCommand('comando-inexistente'), false);
185
304
  });
186
305
 
@@ -288,6 +407,167 @@ test('ban bloqueia tentativa de remover o próprio bot', async () => {
288
407
  assert.equal(messages[0].content.text, 'Operação cancelada: o bot não pode remover a própria conta.');
289
408
  });
290
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
+
291
571
  test('premium exige admin principal e lista usuários quando autorizado', async () => {
292
572
  const texts = getAdminTextConfig();
293
573
  const denied = createSockStub();
@@ -305,7 +585,7 @@ test('premium exige admin principal e lista usuários quando autorizado', async
305
585
  assert.equal(denied.messages.length, 1);
306
586
  assert.equal(denied.messages[0].content.text, texts.owner_only_command_message);
307
587
 
308
- dbHarness.setGroupConfig('system:premium_users', { premiumUsers: [TARGET_JID] });
588
+ dbHarness.setPremiumUsers([TARGET_JID]);
309
589
  const allowed = createSockStub();
310
590
 
311
591
  await runAdminCommand({
@@ -348,3 +628,90 @@ test('prefix atualiza, consulta status e reseta para padrão', async () => {
348
628
  });
349
629
  assert.equal(dbHarness.getGroupConfig(GROUP_JID).commandPrefix, null);
350
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
+ };