@amatiscorp/disguard 1.2.0 → 1.4.0

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 (72) hide show
  1. package/README.md +356 -27
  2. package/dist/AntiSpam.d.ts +12 -1
  3. package/dist/AntiSpam.d.ts.map +1 -1
  4. package/dist/AntiSpam.js +145 -9
  5. package/dist/AntiSpam.js.map +1 -1
  6. package/dist/defaults.d.ts.map +1 -1
  7. package/dist/defaults.js +89 -1
  8. package/dist/defaults.js.map +1 -1
  9. package/dist/detectors/accounts.d.ts.map +1 -1
  10. package/dist/detectors/accounts.js +27 -0
  11. package/dist/detectors/accounts.js.map +1 -1
  12. package/dist/detectors/attach.d.ts +3 -0
  13. package/dist/detectors/attach.d.ts.map +1 -0
  14. package/dist/detectors/attach.js +31 -0
  15. package/dist/detectors/attach.js.map +1 -0
  16. package/dist/detectors/duplicates.js +1 -1
  17. package/dist/detectors/duplicates.js.map +1 -1
  18. package/dist/detectors/echo.d.ts +3 -0
  19. package/dist/detectors/echo.d.ts.map +1 -0
  20. package/dist/detectors/echo.js +37 -0
  21. package/dist/detectors/echo.js.map +1 -0
  22. package/dist/detectors/files.d.ts.map +1 -1
  23. package/dist/detectors/files.js +10 -0
  24. package/dist/detectors/files.js.map +1 -1
  25. package/dist/detectors/ghost.d.ts +4 -0
  26. package/dist/detectors/ghost.d.ts.map +1 -0
  27. package/dist/detectors/ghost.js +36 -0
  28. package/dist/detectors/ghost.js.map +1 -0
  29. package/dist/detectors/hop.d.ts +3 -0
  30. package/dist/detectors/hop.d.ts.map +1 -0
  31. package/dist/detectors/hop.js +30 -0
  32. package/dist/detectors/hop.js.map +1 -0
  33. package/dist/detectors/invisible.d.ts +3 -0
  34. package/dist/detectors/invisible.d.ts.map +1 -0
  35. package/dist/detectors/invisible.js +25 -0
  36. package/dist/detectors/invisible.js.map +1 -0
  37. package/dist/detectors/links.d.ts.map +1 -1
  38. package/dist/detectors/links.js +11 -0
  39. package/dist/detectors/links.js.map +1 -1
  40. package/dist/detectors/mentions.d.ts.map +1 -1
  41. package/dist/detectors/mentions.js +19 -1
  42. package/dist/detectors/mentions.js.map +1 -1
  43. package/dist/detectors/punctuation.d.ts +3 -0
  44. package/dist/detectors/punctuation.d.ts.map +1 -0
  45. package/dist/detectors/punctuation.js +26 -0
  46. package/dist/detectors/punctuation.js.map +1 -0
  47. package/dist/detectors/secrets.d.ts +3 -0
  48. package/dist/detectors/secrets.d.ts.map +1 -0
  49. package/dist/detectors/secrets.js +59 -0
  50. package/dist/detectors/secrets.js.map +1 -0
  51. package/dist/detectors/spoilers.d.ts +3 -0
  52. package/dist/detectors/spoilers.d.ts.map +1 -0
  53. package/dist/detectors/spoilers.js +25 -0
  54. package/dist/detectors/spoilers.js.map +1 -0
  55. package/dist/detectors/words.d.ts +3 -0
  56. package/dist/detectors/words.d.ts.map +1 -0
  57. package/dist/detectors/words.js +56 -0
  58. package/dist/detectors/words.js.map +1 -0
  59. package/dist/enforcement.d.ts.map +1 -1
  60. package/dist/enforcement.js +71 -6
  61. package/dist/enforcement.js.map +1 -1
  62. package/dist/index.d.ts +3 -2
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +4 -1
  65. package/dist/index.js.map +1 -1
  66. package/dist/types.d.ts +112 -6
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/utils/normalize.d.ts +1 -0
  69. package/dist/utils/normalize.d.ts.map +1 -1
  70. package/dist/utils/normalize.js +4 -0
  71. package/dist/utils/normalize.js.map +1 -1
  72. package/package.json +5 -5
package/README.md CHANGED
@@ -1,17 +1,29 @@
1
1
  # Disguard
2
2
 
3
- npm: [`@amatiscorp/disguard`](https://www.npmjs.com/package/@amatiscorp/disguard)
3
+ <div align="center">
4
4
 
5
- Configurable antispam for [discord.js](https://discord.js.org) v14 bots.
5
+ **Configurable antispam for discord.js v14**
6
6
 
7
- It is **not** a bot. You plug it into your existing `Client` and decide every threshold, allowlist, and punishment.
7
+ Flood · phishing · leaked secrets · channel hop · ghost pings · TypeScript
8
8
 
9
- Detects flood, repeated text, phishing / unwanted links, duplicate images, mention spam, excessive caps, and emoji spam.
9
+ [![npm version](https://img.shields.io/npm/v/@amatiscorp/disguard.svg?style=flat-square)](https://www.npmjs.com/package/@amatiscorp/disguard)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
10
12
 
11
- > **Español:** librería antispam configurable para bots de discord.js. No es un bot. La documentación en español está más abajo: [Español](#español).
13
+ [Install](#install) · [Quick start](#quick-start) · [API](#api) · [Español](#español)
14
+
15
+ </div>
12
16
 
13
17
  ---
14
18
 
19
+ npm: [`@amatiscorp/disguard`](https://www.npmjs.com/package/@amatiscorp/disguard)
20
+
21
+ Configurable antispam for [discord.js](https://discord.js.org) v14 bots.
22
+
23
+ It is **not** a bot. You plug it into your existing `Client` and decide every threshold, allowlist, and punishment.
24
+
25
+ Detects flood, leaked tokens/webhooks, channel hopping, blocked words, phishing, duplicate images, mention spam, ghost pings, and more.
26
+
15
27
  **Languages**
16
28
 
17
29
  - [English](#english)
@@ -21,6 +33,97 @@ Detects flood, repeated text, phishing / unwanted links, duplicate images, menti
21
33
 
22
34
  # English
23
35
 
36
+ ## What's new in 1.4.0
37
+
38
+ Secret scanning, cross-channel copy-paste, invisible unicode, attachment floods, and cleaner logs. Kick/ban stay **off**.
39
+
40
+ | Option | Default | What it does |
41
+ | --- | --- | --- |
42
+ | `secrets` | **on** | Flags Discord **bot tokens** and **webhook URLs** pasted in chat (also embeds). The incident does **not** store the secret. |
43
+ | `echo` | **on**, 3 channels / 12s | Same text pasted across several channels (raid copypasta). Distinct from `hop` (any messages). |
44
+ | `invisible` | **on**, 8 | Zero-width / bidi characters used to dodge filters. |
45
+ | `attach` | **on**, 8 files / 10s | Too many attachments across a short window, not just dangerous extensions. |
46
+ | `duplicates.minLength` | `8` | Short stuff like `ok` / `lol` is no longer duplicate-spam. |
47
+ | `mentions.maxInWindow` | `0` (off) | Cap mentions across several messages, not only one. |
48
+ | `accounts.onlyWithLinks` | `false` (`strict`: true) | New accounts are flagged only if the message has a URL. |
49
+ | `punishment.purgeCount` | `0` | After a hit, bulk-delete N of that user's recent messages in the channel. |
50
+ | `punishment.logWebhookUrl` | `null` | Log embeds to a Discord webhook (no extra log channel needed). |
51
+ | `ignorePinned` | `false` | Skip pinned messages. |
52
+ | `ignoreOlderThanMs` | `0` | Skip old messages (useful on reconnect replay). |
53
+ | `setEnabled(false)` | — | Turn the whole filter off without `stop()`. |
54
+
55
+ ```js
56
+ const antispam = new AntiSpam(client, {
57
+ preset: "balanced",
58
+ secrets: { enabled: true, botTokens: true, webhooks: true, extraPatterns: ["ghp_[A-Za-z0-9]{20,}"] },
59
+ echo: { maxChannels: 3, windowMs: 12_000, minLength: 12 },
60
+ invisible: { maxInvisible: 8 },
61
+ attach: { maxAttachments: 8, windowMs: 10_000 },
62
+ duplicates: { minLength: 8 },
63
+ accounts: { enabled: true, minAgeDays: 2, onlyWithLinks: true },
64
+ ignorePinned: true,
65
+ punishment: {
66
+ purgeCount: 5,
67
+ logWebhookUrl: process.env.DISGUARD_LOG_WEBHOOK || null,
68
+ },
69
+ });
70
+
71
+ antispam.setEnabled(false); // raid party / event
72
+ antispam.setEnabled(true);
73
+ ```
74
+
75
+ `secrets` is meant to **delete leaks**, not to harvest credentials. Reasons only say “token/webhook”, never the value.
76
+
77
+ ## What's new in 1.3.0
78
+
79
+ | Option | Default | What it does |
80
+ | --- | --- | --- |
81
+ | `words` | **off**, empty list | Block a word list and/or regex. `ignoreCase` and `matchWholeWord` are both `true` by default. |
82
+ | `hop` | **on**, 5 channels / 8s | Same user posting across too many unique channels in a window (raid / hop). |
83
+ | `punctuation` | **on**, 10 | `aaaaaa` / `!!!!!!` runs longer than `maxRepeated`. |
84
+ | `spoilers` | **on**, 8 pairs | Too many `\|\|spoiler\|\|` pairs in one message. |
85
+ | `ghostPing` | **off** (`strict`: on) | Punish deleting a message that mentioned people (`messageDelete`). |
86
+ | `checkDeletes` | `true` | Listen for `messageDelete`. Set `false` if you do not want ghost-ping checks. |
87
+ | `ignored.prefixes` | `[]` | Skip messages that start with `!`, `/`, `.`, etc. |
88
+ | `ignoreThreads` | `false` | Skip thread messages. |
89
+ | `ignoreSystem` | `true` | Skip Discord system messages. |
90
+ | `roleOverrides` / `setRoleConfig` | `{}` | Per-role config overlay (before channel overrides). |
91
+ | `links.blockOauth` | `true` | Flag non-allowlisted URLs whose path looks like `/oauth`, `/authorize`, `/login`. Official `discord.com` stays allowed. |
92
+ | `files.maxBytes` | `0` | Max attachment size in bytes. `0` = unlimited. |
93
+ | `accounts.minGuildAgeDays` | `0` | Flag members who joined the guild too recently. `0` = off. |
94
+ | `punishment.addRoleIds` / `removeRoleIds` | `[]` | Add/remove roles on punish (mute role). Needs **Manage Roles**. |
95
+ | `punishment.minStrikesForRoles` | `1` | Minimum strikes before those role actions run. |
96
+ | `onCooldown` | — | Callback when extra spam is deleted quietly during the punishment cooldown. |
97
+ | `pause()` / `resume()` | — | Freeze enforcement without removing listeners. |
98
+
99
+ ```js
100
+ const antispam = new AntiSpam(client, {
101
+ preset: "balanced",
102
+ locale: "es",
103
+ ignored: { prefixes: ["!", "/", "."], roles: ["STAFF_ROLE_ID"] },
104
+ words: { enabled: true, list: ["raid-now"], regex: ["n[i1]tro\\s+free"] },
105
+ hop: { maxChannels: 4, windowMs: 6_000 },
106
+ ghostPing: { enabled: true, minMentions: 1, maxAgeMs: 15_000 },
107
+ links: { blockOauth: true },
108
+ files: { maxBytes: 8 * 1024 * 1024 },
109
+ roleOverrides: {
110
+ "VIP_ROLE_ID": { flood: { maxMessages: 10 } },
111
+ },
112
+ punishment: {
113
+ addRoleIds: ["MUTE_ROLE_ID"],
114
+ minStrikesForRoles: 2,
115
+ },
116
+ onCooldown(message) {
117
+ console.log("quiet delete", message.id);
118
+ },
119
+ });
120
+
121
+ antispam.setRoleConfig("VIP_ROLE_ID", { emojis: { maxEmojis: 40 } });
122
+ antispam.pause(); // raid over? antispam.resume();
123
+ ```
124
+
125
+ Ghost pings need the message in cache (or `Partials.Message`) so `messageDelete` still has mentions. Kick/ban stay **off**. Role add/remove is opt-in via empty arrays.
126
+
24
127
  ## What's new in 1.2.0
25
128
 
26
129
  More knobs, per-channel rules, and bilingual user-facing text.
@@ -105,6 +208,8 @@ console.log(antispam.getStats());
105
208
 
106
209
  ## Table of contents
107
210
 
211
+ - [What's new in 1.4.0](#whats-new-in-140)
212
+ - [What's new in 1.3.0](#whats-new-in-130)
108
213
  - [What's new in 1.2.0](#whats-new-in-120)
109
214
  - [What's new in 1.1.0](#whats-new-in-110)
110
215
  - [Features](#features)
@@ -138,17 +243,27 @@ console.log(antispam.getStats());
138
243
  | **file** | Dangerous attachments (`.exe`, `.bat`, `.dll`, `.msi`, …) |
139
244
  | **zalgo** | Obfuscated / zalgo text (stacked combining marks) |
140
245
  | **newline** | Message walls made of empty lines |
141
- | **account** | Brand-new Discord accounts / default avatar (off by default) |
246
+ | **account** | Brand-new Discord accounts / default avatar / new guild members (off by default) |
142
247
  | **length** | Over-long messages (off by default) |
248
+ | **word** | Custom blocked words and regex (off until you fill `list` / `regex`) |
249
+ | **hop** | Same user jumping across too many channels |
250
+ | **punctuation** | Long runs of the same character (`!!!!`, `aaaa`) |
251
+ | **spoiler** | Spoiler-tag walls |
252
+ | **ghost** | Deleted messages that mentioned users (off by default) |
253
+ | **secret** | Accidental Discord bot tokens / webhook URLs in chat |
254
+ | **echo** | Same text pasted in several channels |
255
+ | **invisible** | Zero-width / bidi obfuscation |
256
+ | **attach** | Attachment floods over a sliding window |
143
257
 
144
258
  Also included:
145
259
 
146
260
  - Three presets: `lenient`, `balanced`, `strict`
147
- - Ignore lists for users, roles, channels, categories, and guilds
261
+ - Ignore lists for users, roles, channels, categories, guilds, and command prefixes
262
+ - Per-guild, per-channel, and per-role overlays
148
263
  - Strike system with decay
149
- - Optional delete / warn / timeout / kick / ban (kick and ban are **off** by default)
264
+ - Optional delete / warn / timeout / kick / ban / addRole / removeRole (kick and ban are **off** by default)
150
265
  - `dryRun` to tune rules without punishing anyone
151
- - Edit scanning (links and mentions)
266
+ - Edit scanning (links, mentions, and words)
152
267
  - Zero extra runtime dependencies (peer: `discord.js`)
153
268
  - In-memory store only — no database required
154
269
  - Full TypeScript types
@@ -263,6 +378,7 @@ npm run dev
263
378
  | Warn in the channel | **Send Messages** |
264
379
  | Log embeds | **Embed Links** |
265
380
  | Kick / ban (opt-in) | **Kick Members** / **Ban Members** |
381
+ | Mute / add / remove roles (opt-in) | **Manage Roles** |
266
382
 
267
383
  The bot will not sanction the guild owner or anyone above it in the role hierarchy. Those actions are skipped and reported in `onAction`.
268
384
 
@@ -270,11 +386,12 @@ Enable **Message Content Intent** in the [Discord Developer Portal](https://disc
270
386
 
271
387
  ## How it works
272
388
 
273
- 1. Ignores bots, webhooks, the owner, administrators, and anything in your ignore lists.
389
+ 1. Ignores bots, webhooks, system messages, the owner, administrators, command prefixes, and anything in your ignore lists.
274
390
  2. Keeps a short **in-memory** history per user and guild (no database).
275
- 3. Runs detectors in this order: files → flood → duplicates → links → images → mentions → zalgo → newlines → caps → emojis. The first match wins.
391
+ 3. Runs detectors in this order: files → secrets → words → flood → hop → echo → duplicates → attachments → links → images → mentions → zalgo → newlines → punctuation → spoilers → invisible → accounts → length → caps → emojis. The first match wins.
276
392
  4. Adds one strike (with optional decay) and applies the configured punishment.
277
- 5. Message edits only re-check **links** and **mentions**, so editing `hello` into a phishing URL is still caught without counting as flood.
393
+ 5. Message edits re-check **links**, **mentions**, **words**, **secrets**, and **invisible**.
394
+ 6. Deletes can trigger **ghost** pings if `ghostPing.enabled` is true.
278
395
 
279
396
  Call `antispam.stop()` on shutdown, hot reload, or plugin unload.
280
397
 
@@ -304,6 +421,8 @@ new AntiSpam(client, {
304
421
  | Mentions | 10 | 6 | 3 |
305
422
  | Block invites | no | no | yes |
306
423
  | Block shorteners | no | yes | yes |
424
+ | Channel hop | off | 5 / 8s | 3 / 6s |
425
+ | Ghost pings | off | off | on |
307
426
  | Auto timeout | off | 60s from 2 strikes | 5 min from 1 strike |
308
427
 
309
428
  ## Full configuration
@@ -320,13 +439,17 @@ Every option is optional. Missing fields fall back to the preset (or `balanced`
320
439
  | `ignoreWebhooks` | `boolean` | `true` | Skip webhooks. |
321
440
  | `ignoreOwner` | `boolean` | `true` | Skip the guild owner. |
322
441
  | `ignoreAdministrators` | `boolean` | `true` | Skip members with Administrator. |
323
- | `checkEdits` | `boolean` | `true` | Re-scan edits for links and mentions. |
442
+ | `checkEdits` | `boolean` | `true` | Re-scan edits for links, mentions, and words. |
443
+ | `checkDeletes` | `boolean` | `true` | Listen for `messageDelete` (ghost pings). |
444
+ | `ignoreThreads` | `boolean` | `false` | Skip messages in threads. |
445
+ | `ignoreSystem` | `boolean` | `true` | Skip Discord system messages. |
324
446
  | `cleanupIntervalMs` | `number` | `60000` | How often the memory store is pruned. |
325
447
  | `locale` | `"en"` \| `"es"` | `"en"` | Language for warnings and log embeds. |
326
448
  | `ignorePermissions` | `string[]` | `[]` | Permission names that bypass checks. |
327
449
  | `disabledDetectors` | `DetectorType[]` | `[]` | Detectors to skip. |
328
450
  | `detectorOrder` | `DetectorType[]` | `[]` | Custom order. Empty = default. |
329
451
  | `channelOverrides` | `object` | `{}` | Per-channel patches keyed by channel id. |
452
+ | `roleOverrides` | `object` | `{}` | Per-role patches keyed by role id. |
330
453
 
331
454
  ### Ignore lists
332
455
 
@@ -337,10 +460,11 @@ ignored: {
337
460
  channels: ["789"],
338
461
  categories: ["101"],
339
462
  guilds: ["202"],
463
+ prefixes: ["!", "/", "."],
340
464
  }
341
465
  ```
342
466
 
343
- IDs are snowflakes as strings. A staff role in `ignored.roles` bypasses every detector.
467
+ IDs are snowflakes as strings. A staff role in `ignored.roles` bypasses every detector. `prefixes` skip command messages (`!help` is ignored if `"!"` is listed).
344
468
 
345
469
  ### Flood
346
470
 
@@ -387,6 +511,9 @@ links: {
387
511
  suspiciousTlds: [], // e.g. ["tk", "gq"]
388
512
  customPatterns: [], // regex against the whole message
389
513
  extraPhishingKeywords: ["fake giveaway"],
514
+ maxLinks: 0,
515
+ scanEmbeds: true,
516
+ blockOauth: true,
390
517
  severity: "high",
391
518
  }
392
519
  ```
@@ -402,6 +529,7 @@ Built-in checks (each can be turned off):
402
529
  - Scam phrases plus any link (`free nitro`, `steam gift`, `verifica tu cuenta`, …)
403
530
  - Discord invite links if `blockInvites` is `true`
404
531
  - Markdown-masked links: `[click](https://evil.test)`
532
+ - Login / OAuth-looking paths on non-allowlisted hosts if `blockOauth` is `true`
405
533
 
406
534
  ```js
407
535
  // Block foreign server invites
@@ -474,6 +602,7 @@ accounts: {
474
602
  enabled: false, // turn on to flag new users
475
603
  minAgeDays: 3,
476
604
  blockDefaultAvatar: false,
605
+ minGuildAgeDays: 0, // 0 = off; e.g. 1 = joined this guild today
477
606
  severity: "medium",
478
607
  }
479
608
 
@@ -490,6 +619,7 @@ length: {
490
619
  files: {
491
620
  enabled: true,
492
621
  blockedExtensions: ["exe", "bat", "cmd", "com", "scr", "dll", "msi", "vbs", "ps1", "jar", "apk"],
622
+ maxBytes: 0, // 0 = unlimited
493
623
  severity: "critical",
494
624
  }
495
625
 
@@ -506,6 +636,57 @@ newlines: {
506
636
  }
507
637
  ```
508
638
 
639
+ ### Words, channel hop, punctuation, spoilers
640
+
641
+ ```js
642
+ words: {
643
+ enabled: false, // stays off until you add a list or regex
644
+ list: ["raid-now"],
645
+ regex: ["n[i1]tro\\s+free"],
646
+ ignoreCase: true,
647
+ matchWholeWord: true,
648
+ severity: "high",
649
+ }
650
+
651
+ hop: {
652
+ enabled: true,
653
+ maxChannels: 5, // 5th unique channel in the window triggers
654
+ windowMs: 8_000,
655
+ severity: "high",
656
+ }
657
+
658
+ punctuation: {
659
+ enabled: true,
660
+ maxRepeated: 10, // 11+ of the same character in a row
661
+ severity: "low",
662
+ }
663
+
664
+ spoilers: {
665
+ enabled: true,
666
+ maxSpoilers: 8,
667
+ severity: "low",
668
+ }
669
+ ```
670
+
671
+ `lenient` turns hop and punctuation off. `strict` tightens hop to 3 channels / 6s.
672
+
673
+ ### Ghost pings
674
+
675
+ Off in `balanced` / `lenient`. On in `strict`. Needs `checkDeletes: true` (default) and the deleted message in cache (`Partials.Message` in your Client).
676
+
677
+ ```js
678
+ ghostPing: {
679
+ enabled: false,
680
+ minMentions: 1,
681
+ maxAgeMs: 15_000, // 0 = any age
682
+ severity: "high",
683
+ }
684
+
685
+ checkDeletes: true
686
+ ```
687
+
688
+ Disable with `ghostPing: { enabled: false }` or `disabledDetectors: ["ghost"]`.
689
+
509
690
  ### Punishment
510
691
 
511
692
  Kick and ban stay **disabled** on purpose. Turn them on only if you really want that.
@@ -525,6 +706,9 @@ punishment: {
525
706
  cooldownMs: 8_000, // no second timeout/warn during this window
526
707
  deleteDuringCooldown: true, // still delete extra spam quietly
527
708
  warnAsEmbed: false,
709
+ addRoleIds: [], // e.g. ["MUTE_ROLE_ID"] — needs Manage Roles
710
+ removeRoleIds: [],
711
+ minStrikesForRoles: 1,
528
712
  }
529
713
 
530
714
  // timeout.scale: "none" | "linear" (base * strikes) | "exponential" (base * 2^n)
@@ -534,7 +718,7 @@ punishment: {
534
718
 
535
719
  `warnMessage` placeholders: `{user}` `{reason}` `{type}` `{strikes}`.
536
720
 
537
- With `escalate: true`, Disguard applies **one** hard action (timeout, or kick, or ban — whichever threshold you hit). Delete and warn can still run together.
721
+ With `escalate: true`, Disguard applies **one** hard action (timeout, or kick, or ban — whichever threshold you hit). Delete and warn can still run together. `addRole` / `removeRole` run if those ID lists are non-empty and strikes ≥ `minStrikesForRoles`.
538
722
 
539
723
  If the bot lacks a permission, or the target is higher in the hierarchy, that action is skipped and listed in `result.skipped`.
540
724
 
@@ -552,6 +736,9 @@ new AntiSpam(client, {
552
736
  onError(error, context) {
553
737
  console.error("[disguard]", context, error);
554
738
  },
739
+ onCooldown(message) {
740
+ // Extra spam during punishment.cooldownMs (already deleted if deleteDuringCooldown).
741
+ },
555
742
  });
556
743
  ```
557
744
 
@@ -559,7 +746,10 @@ new AntiSpam(client, {
559
746
 
560
747
  ```ts
561
748
  {
562
- type: "flood" | "duplicate" | "link" | "image" | "mention" | "caps" | "emoji",
749
+ type: "flood" | "duplicate" | "link" | "image" | "mention" | "caps" | "emoji"
750
+ | "file" | "zalgo" | "newline" | "account" | "length"
751
+ | "word" | "hop" | "punctuation" | "spoiler" | "ghost"
752
+ | "secret" | "echo" | "invisible" | "attach",
563
753
  severity: "low" | "medium" | "high" | "critical",
564
754
  userId: string,
565
755
  guildId: string,
@@ -567,7 +757,7 @@ new AntiSpam(client, {
567
757
  messageId: string,
568
758
  reason: string,
569
759
  details: Record<string, unknown>,
570
- recommendedActions: Array<"delete" | "warn" | "timeout" | "kick" | "ban">,
760
+ recommendedActions: Array<"delete" | "warn" | "timeout" | "kick" | "ban" | "addRole" | "removeRole" | "purge">,
571
761
  timestamp: number,
572
762
  }
573
763
  ```
@@ -594,6 +784,11 @@ const antispam = new AntiSpam(client, options);
594
784
 
595
785
  antispam.start();
596
786
  antispam.stop();
787
+ antispam.pause();
788
+ antispam.resume();
789
+ antispam.isPaused();
790
+ antispam.setEnabled(false);
791
+ antispam.isEnabled();
597
792
 
598
793
  const config = antispam.getConfig();
599
794
  antispam.setConfig({ flood: { maxMessages: 8 } }); // deep merge, other keys stay
@@ -608,6 +803,8 @@ antispam.clearGuildConfig(guildId);
608
803
  antispam.setChannelConfig(guildId, channelId, { images: { maxRepeats: 8 } });
609
804
  antispam.getChannelConfig(guildId, channelId);
610
805
  antispam.clearChannelConfig(guildId, channelId);
806
+ antispam.setRoleConfig(roleId, { flood: { maxMessages: 12 } });
807
+ antispam.clearRoleConfig(roleId);
611
808
 
612
809
  antispam.use(customDetector);
613
810
  antispam.getStats();
@@ -692,6 +889,18 @@ antispam.setConfig(resolveConfig("strict", { ignored: antispam.getConfig().ignor
692
889
  images: { hashMode: "content", maxDownloadBytes: 1_000_000 }
693
890
  ```
694
891
 
892
+ **Mute role instead of timeout**
893
+
894
+ ```js
895
+ punishment: {
896
+ timeout: { enabled: false, durationMs: 60_000, minStrikes: 99 },
897
+ addRoleIds: ["MUTE_ROLE_ID"],
898
+ minStrikesForRoles: 1,
899
+ }
900
+ ```
901
+
902
+ The bot role must sit **above** the mute role. Needs **Manage Roles**.
903
+
695
904
  **Reset a user after a false positive**
696
905
 
697
906
  ```js
@@ -746,9 +955,18 @@ Wait a few seconds between categories so flood does not eat the next test.
746
955
  | File | Upload a dummy `.exe` or `.bat` |
747
956
  | Zalgo | Text with many stacked combining marks |
748
957
  | Newlines | A message with more than 15 line breaks |
958
+ | Channel hop | Same user posts in 5 different channels within 8 seconds |
959
+ | Punctuation | `aaaaaaaaaaa` or `!!!!!!!!!!!` |
960
+ | Spoilers | More than 8 `\|\|spoiler\|\|` pairs |
961
+ | Blocked word | Enable `words` first, then send a listed word |
962
+ | Ghost ping | Mention someone and delete the message within 15s (`ghostPing.enabled: true`) |
963
+ | Secret | Paste a dummy webhook URL `https://discord.com/api/webhooks/123/abc` |
964
+ | Echo | Same long sentence in 3 channels within 12s |
965
+ | Invisible | A message padded with many zero-width spaces |
966
+ | Attachments | 8+ files in 10 seconds |
749
967
  | Edit | Send `hello`, then edit it to `Free Nitro https://bit.ly/test` |
750
968
 
751
- Watch the terminal: `[detect] flood|duplicate|link|image|mention|caps|emoji`.
969
+ Watch the terminal: `[detect] flood|duplicate|link|image|mention|caps|emoji|file|zalgo|newline|word|hop|punctuation|spoiler|ghost|secret|echo|invisible|attach`.
752
970
 
753
971
  The second strike applies a 1 minute timeout with the default example config. Strikes decay after 15 minutes, or call `resetUser`.
754
972
 
@@ -789,6 +1007,73 @@ MIT
789
1007
 
790
1008
  # Español
791
1009
 
1010
+ ## Novedades de 1.4.0
1011
+
1012
+ Escaneo de secretos, copypasta entre canales, unicode invisible, flood de adjuntos y logs por webhook. Kick/ban siguen **apagados**.
1013
+
1014
+ | Opción | Default | Qué hace |
1015
+ | --- | --- | --- |
1016
+ | `secrets` | **on** | Caza **tokens de bot** y **webhooks** pegados en el chat. El incidente **no** guarda el secreto. |
1017
+ | `echo` | **on**, 3 canales / 12s | El mismo texto en varios canales (raid). No es lo mismo que `hop`. |
1018
+ | `invisible` | **on**, 8 | Caracteres de ancho cero / bidi para saltarse filtros. |
1019
+ | `attach` | **on**, 8 / 10s | Demasiados adjuntos en una ventana, no solo `.exe`. |
1020
+ | `duplicates.minLength` | `8` | `ok` / `lol` ya no cuentan como duplicado. |
1021
+ | `mentions.maxInWindow` | `0` (off) | Tope de menciones entre varios mensajes. |
1022
+ | `accounts.onlyWithLinks` | `false` (`strict`: true) | Cuentas nuevas solo si el mensaje trae URL. |
1023
+ | `punishment.purgeCount` | `0` | Tras un hit, borra N mensajes recientes de ese user en el canal. |
1024
+ | `punishment.logWebhookUrl` | `null` | Logs a un webhook de Discord. |
1025
+ | `ignorePinned` | `false` | Ignora pineados. |
1026
+ | `ignoreOlderThanMs` | `0` | Ignora mensajes viejos (replay al reconectar). |
1027
+ | `setEnabled(false)` | — | Apaga el filtro sin `stop()`. |
1028
+
1029
+ ```js
1030
+ new AntiSpam(client, {
1031
+ secrets: { enabled: true, extraPatterns: ["ghp_[A-Za-z0-9]{20,}"] },
1032
+ echo: { maxChannels: 3 },
1033
+ accounts: { enabled: true, onlyWithLinks: true },
1034
+ punishment: { purgeCount: 5, logWebhookUrl: process.env.DISGUARD_LOG_WEBHOOK || null },
1035
+ });
1036
+ ```
1037
+
1038
+ ## Novedades de 1.3.0
1039
+
1040
+ Más detectores, overlays por rol, mute por rol y más ignores. Todo es opcional.
1041
+
1042
+ | Opción | Default | Qué hace |
1043
+ | --- | --- | --- |
1044
+ | `words` | **apagado**, lista vacía | Palabras y/o regex bloqueados. `ignoreCase` y `matchWholeWord` en `true`. |
1045
+ | `hop` | **on**, 5 canales / 8s | El mismo usuario escribe en demasiados canales (raid / hop). |
1046
+ | `punctuation` | **on**, 10 | Rachas de `aaaaaa` / `!!!!!!` más largas que `maxRepeated`. |
1047
+ | `spoilers` | **on**, 8 pares | Demasiados `\|\|spoiler\|\|` en un mensaje. |
1048
+ | `ghostPing` | **off** (`strict`: on) | Castiga borrar un mensaje que mencionaba gente. |
1049
+ | `checkDeletes` | `true` | Escucha `messageDelete`. |
1050
+ | `ignored.prefixes` | `[]` | Ignora mensajes que empiezan por `!`, `/`, `.`. |
1051
+ | `ignoreThreads` | `false` | Ignora hilos. |
1052
+ | `ignoreSystem` | `true` | Ignora mensajes de sistema. |
1053
+ | `roleOverrides` / `setRoleConfig` | `{}` | Config por rol (antes de la de canal). |
1054
+ | `links.blockOauth` | `true` | URLs de login/OAuth fuera de la allowList. `discord.com` sigue permitido. |
1055
+ | `files.maxBytes` | `0` | Tamaño máximo de adjunto. `0` = sin límite. |
1056
+ | `accounts.minGuildAgeDays` | `0` | Miembros demasiado nuevos en **este** servidor. `0` = off. |
1057
+ | `punishment.addRoleIds` / `removeRoleIds` | `[]` | Añadir/quitar roles al castigar (mute). Hace falta **Manage Roles**. |
1058
+ | `onCooldown` | — | Callback cuando el cooldown borra spam extra en silencio. |
1059
+ | `pause()` / `resume()` | — | Congela el antispam sin quitar listeners. |
1060
+
1061
+ ```js
1062
+ const antispam = new AntiSpam(client, {
1063
+ locale: "es",
1064
+ ignored: { prefixes: ["!", "/", "."], roles: ["ID_STAFF"] },
1065
+ words: { enabled: true, list: ["raid-now"] },
1066
+ hop: { maxChannels: 4, windowMs: 6_000 },
1067
+ ghostPing: { enabled: true, maxAgeMs: 15_000 },
1068
+ punishment: { addRoleIds: ["ID_MUTE"], minStrikesForRoles: 2 },
1069
+ });
1070
+
1071
+ antispam.setRoleConfig("ID_VIP", { flood: { maxMessages: 10 } });
1072
+ antispam.pause();
1073
+ ```
1074
+
1075
+ Kick y ban siguen **apagados**. Ghost ping necesita el mensaje en caché (`Partials.Message`).
1076
+
792
1077
  ## Novedades de 1.2.0
793
1078
 
794
1079
  Más opciones, reglas por canal y textos al usuario en inglés o español.
@@ -839,6 +1124,8 @@ new AntiSpam(client, {
839
1124
 
840
1125
  ## Tabla de contenidos
841
1126
 
1127
+ - [Novedades de 1.4.0](#novedades-de-140)
1128
+ - [Novedades de 1.3.0](#novedades-de-130)
842
1129
  - [Novedades de 1.2.0](#novedades-de-120)
843
1130
  - [Novedades de 1.1.0](#novedades-de-110)
844
1131
  - [Qué es](#qué-es)
@@ -862,7 +1149,7 @@ new AntiSpam(client, {
862
1149
 
863
1150
  **Disguard** es una librería antispam para bots de discord.js v14. No es un bot: la enchufas a tu `Client` y tú decides umbrales, listas y castigos.
864
1151
 
865
- Detecta flood, texto repetido, phishing / enlaces no deseados (también en embeds), imágenes repetidas, spam de menciones, mayúsculas, emojis, archivos peligrosos, zalgo y paredes de líneas.
1152
+ Detecta flood, salto de canales, palabras bloqueadas, phishing / enlaces no deseados (también en embeds), imágenes repetidas, spam de menciones, ghost pings, mayúsculas, emojis, archivos peligrosos, zalgo y paredes de líneas.
866
1153
 
867
1154
  ## Requisitos
868
1155
 
@@ -948,6 +1235,7 @@ npm run dev
948
1235
  | Avisar en el canal | **Send Messages** |
949
1236
  | Embed de logs | **Embed Links** |
950
1237
  | Kick / ban (opt-in) | **Kick Members** / **Ban Members** |
1238
+ | Mute / add / remove roles (opt-in) | **Manage Roles** |
951
1239
 
952
1240
  No puede sancionar al dueño del servidor ni a quien esté por encima del bot en la jerarquía.
953
1241
 
@@ -955,11 +1243,12 @@ Activa **Message Content Intent** en el [Portal de Discord](https://discord.com/
955
1243
 
956
1244
  ## Cómo funciona
957
1245
 
958
- 1. Ignora bots, webhooks, el dueño, administradores y tus listas de ignore.
1246
+ 1. Ignora bots, webhooks, mensajes de sistema, el dueño, administradores, prefijos de comando y tus listas de ignore.
959
1247
  2. Guarda en **memoria** un historial corto por usuario y servidor. Sin base de datos.
960
- 3. Pasa el mensaje por: flood → duplicados → enlaces → imágenes → menciones → caps → emojis. El primero que dispare gana.
1248
+ 3. Pasa el mensaje por: archivos → secretos → palabras → flood → hop → echo → duplicados → adjuntos → enlaces → imágenes → menciones → zalgo → líneas → puntuación → spoilers → invisibles → cuentas → longitud → caps → emojis. El primero que dispare gana.
961
1249
  4. Suma un strike (con caducidad) y aplica el castigo configurado.
962
- 5. Las ediciones solo revisan **enlaces** y **menciones**.
1250
+ 5. Las ediciones revisan **enlaces**, **menciones** y **palabras**.
1251
+ 6. Los borrados pueden disparar **ghost** ping si `ghostPing.enabled` es true.
963
1252
 
964
1253
  Llama `antispam.stop()` al apagar el proceso.
965
1254
 
@@ -992,8 +1281,12 @@ Todo es opcional. Lo que no pongas usa el preset.
992
1281
  | `ignoreWebhooks` | `boolean` | `true` | Ignora webhooks. |
993
1282
  | `ignoreOwner` | `boolean` | `true` | Ignora al dueño. |
994
1283
  | `ignoreAdministrators` | `boolean` | `true` | Ignora quien tenga Administrator. |
995
- | `checkEdits` | `boolean` | `true` | Revisa ediciones (enlaces y menciones). |
1284
+ | `checkEdits` | `boolean` | `true` | Revisa ediciones (enlaces, menciones y palabras). |
1285
+ | `checkDeletes` | `boolean` | `true` | Escucha `messageDelete` (ghost pings). |
1286
+ | `ignoreThreads` | `boolean` | `false` | Ignora hilos. |
1287
+ | `ignoreSystem` | `boolean` | `true` | Ignora mensajes de sistema. |
996
1288
  | `cleanupIntervalMs` | `number` | `60000` | Limpieza de memoria. |
1289
+ | `roleOverrides` | `object` | `{}` | Parches por rol. |
997
1290
 
998
1291
  ### Listas de ignore
999
1292
 
@@ -1004,6 +1297,7 @@ ignored: {
1004
1297
  channels: ["id"],
1005
1298
  categories: ["id"],
1006
1299
  guilds: ["id"],
1300
+ prefixes: ["!", "/", "."],
1007
1301
  }
1008
1302
  ```
1009
1303
 
@@ -1048,13 +1342,16 @@ links: {
1048
1342
  suspiciousTlds: [],
1049
1343
  customPatterns: ["steamcommunity\\.ru"],
1050
1344
  extraPhishingKeywords: ["sorteo falso"],
1345
+ maxLinks: 0,
1346
+ scanEmbeds: true,
1347
+ blockOauth: true,
1051
1348
  severity: "high",
1052
1349
  }
1053
1350
  ```
1054
1351
 
1055
1352
  La `allowList` gana a las heurísticas. `discord.com` y `youtube.com` ya vienen permitidos.
1056
1353
 
1057
- Heurísticas incluidas (todas se pueden apagar): acortadores, IPs, punycode, clones de marcas, palabras de estafa + enlace, invitaciones si `blockInvites` es `true`, y enlaces enmascarados `[texto](url)`.
1354
+ Heurísticas incluidas (todas se pueden apagar): acortadores, IPs, punycode, clones de marcas, palabras de estafa + enlace, invitaciones si `blockInvites` es `true`, enlaces enmascarados `[texto](url)`, y rutas de login/OAuth si `blockOauth` es `true`.
1058
1355
 
1059
1356
  ### Imágenes
1060
1357
 
@@ -1086,11 +1383,23 @@ emojis: { enabled: true, maxEmojis: 12, maxStickers: 3, severity: "low" },
1086
1383
  ### Archivos, zalgo, saltos de línea
1087
1384
 
1088
1385
  ```js
1089
- files: { enabled: true, blockedExtensions: ["exe", "bat", "cmd", "dll", "msi"], severity: "critical" },
1386
+ files: { enabled: true, blockedExtensions: ["exe", "bat", "cmd", "dll", "msi"], maxBytes: 0, severity: "critical" },
1090
1387
  zalgo: { enabled: true, maxCombining: 20, severity: "medium" },
1091
1388
  newlines: { enabled: true, maxNewlines: 15, severity: "low" },
1092
1389
  ```
1093
1390
 
1391
+ ### Palabras, hop, puntuación, spoilers, ghost ping
1392
+
1393
+ ```js
1394
+ words: { enabled: false, list: ["raid-now"], regex: [], ignoreCase: true, matchWholeWord: true, severity: "high" },
1395
+ hop: { enabled: true, maxChannels: 5, windowMs: 8_000, severity: "high" },
1396
+ punctuation: { enabled: true, maxRepeated: 10, severity: "low" },
1397
+ spoilers: { enabled: true, maxSpoilers: 8, severity: "low" },
1398
+ ghostPing: { enabled: false, minMentions: 1, maxAgeMs: 15_000, severity: "high" },
1399
+ ```
1400
+
1401
+ `lenient` apaga hop y puntuación. `strict` enciende ghost ping y aprieta hop a 3 canales / 6s.
1402
+
1094
1403
  ### Castigos
1095
1404
 
1096
1405
  Kick y ban van **apagados**. Actívalos solo si lo tienes claro.
@@ -1109,12 +1418,15 @@ punishment: {
1109
1418
  strikeDecayMs: 15 * 60_000,
1110
1419
  cooldownMs: 8_000,
1111
1420
  deleteDuringCooldown: true,
1421
+ addRoleIds: [],
1422
+ removeRoleIds: [],
1423
+ minStrikesForRoles: 1,
1112
1424
  }
1113
1425
  ```
1114
1426
 
1115
1427
  Placeholders: `{user}` `{reason}` `{type}` `{strikes}`.
1116
1428
 
1117
- Con `escalate: true` se aplica **un** castigo fuerte (timeout, kick o ban). Aviso y borrado se pueden sumar.
1429
+ Con `escalate: true` se aplica **un** castigo fuerte (timeout, kick o ban). Aviso y borrado se pueden sumar. `addRole` / `removeRole` corren si las listas no están vacías.
1118
1430
 
1119
1431
  ## Callbacks
1120
1432
 
@@ -1126,6 +1438,7 @@ new AntiSpam(client, {
1126
1438
  onError(error, context) {
1127
1439
  console.error(context, error);
1128
1440
  },
1441
+ onCooldown(message) {},
1129
1442
  });
1130
1443
  ```
1131
1444
 
@@ -1138,11 +1451,16 @@ const antispam = new AntiSpam(client, options);
1138
1451
 
1139
1452
  antispam.start();
1140
1453
  antispam.stop();
1454
+ antispam.pause();
1455
+ antispam.resume();
1456
+ antispam.setEnabled(false);
1141
1457
  antispam.getConfig();
1142
1458
  antispam.setConfig({ flood: { maxMessages: 8 } });
1143
1459
  antispam.getStrikes(guildId, userId);
1144
1460
  antispam.resetUser(guildId, userId);
1145
1461
  antispam.setGuildConfig(guildId, { flood: { maxMessages: 3 } });
1462
+ antispam.setChannelConfig(guildId, channelId, { flood: { maxMessages: 12 } });
1463
+ antispam.setRoleConfig(roleId, { emojis: { maxEmojis: 40 } });
1146
1464
  antispam.getStats();
1147
1465
 
1148
1466
  const incident = await antispam.analyze(message);
@@ -1174,6 +1492,8 @@ new AntiSpam(client, {
1174
1492
 
1175
1493
  **Quitar strikes:** `antispam.resetUser(guildId, userId)`.
1176
1494
 
1495
+ **Mute por rol:** `punishment: { addRoleIds: ["ID_MUTE"], timeout: { enabled: false, durationMs: 0, minStrikes: 99 } }`. El rol del bot debe estar por encima del mute.
1496
+
1177
1497
  **Cambiar a strict en caliente**
1178
1498
 
1179
1499
  ```js
@@ -1211,9 +1531,18 @@ Espera unos segundos entre categorías.
1211
1531
  | Archivo | Sube un `.exe` o `.bat` (aunque sea de mentira) |
1212
1532
  | Zalgo | Texto con muchos diacríticos apilados |
1213
1533
  | Líneas | Un mensaje con más de 15 enters |
1534
+ | Hop | El mismo usuario en 5 canales distintos en 8s |
1535
+ | Puntuación | `aaaaaaaaaaa` o `!!!!!!!!!!!` |
1536
+ | Spoilers | Más de 8 pares `\|\|spoiler\|\|` |
1537
+ | Palabra | Activa `words` y manda una de la lista |
1538
+ | Ghost ping | Menciona a alguien y borra el mensaje en menos de 15s (`ghostPing.enabled: true`) |
1539
+ | Secreto | Pega un webhook de mentira `https://discord.com/api/webhooks/123/abc` |
1540
+ | Echo | La misma frase larga en 3 canales en 12s |
1541
+ | Invisible | Un mensaje con muchos espacios de ancho cero |
1542
+ | Adjuntos | 8+ archivos en 10 segundos |
1214
1543
  | Edición | Manda `hola` y edítalo a `Free Nitro https://bit.ly/test` |
1215
1544
 
1216
- En la terminal: `[detect] flood|duplicate|link|image|mention|caps|emoji|file|zalgo|newline`.
1545
+ En la terminal: `[detect] flood|duplicate|link|image|mention|caps|emoji|file|zalgo|newline|word|hop|punctuation|spoiler|ghost|secret|echo|invisible|attach`.
1217
1546
 
1218
1547
  Al segundo strike el ejemplo mete timeout de 1 minuto. Los strikes caducan a los 15 minutos, o usa `resetUser`.
1219
1548