@amatiscorp/disguard 1.2.0 → 1.3.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 (47) hide show
  1. package/README.md +271 -23
  2. package/dist/AntiSpam.d.ts +10 -1
  3. package/dist/AntiSpam.d.ts.map +1 -1
  4. package/dist/AntiSpam.js +108 -9
  5. package/dist/AntiSpam.js.map +1 -1
  6. package/dist/defaults.d.ts.map +1 -1
  7. package/dist/defaults.js +48 -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 +13 -0
  11. package/dist/detectors/accounts.js.map +1 -1
  12. package/dist/detectors/files.d.ts.map +1 -1
  13. package/dist/detectors/files.js +10 -0
  14. package/dist/detectors/files.js.map +1 -1
  15. package/dist/detectors/ghost.d.ts +4 -0
  16. package/dist/detectors/ghost.d.ts.map +1 -0
  17. package/dist/detectors/ghost.js +34 -0
  18. package/dist/detectors/ghost.js.map +1 -0
  19. package/dist/detectors/hop.d.ts +3 -0
  20. package/dist/detectors/hop.d.ts.map +1 -0
  21. package/dist/detectors/hop.js +30 -0
  22. package/dist/detectors/hop.js.map +1 -0
  23. package/dist/detectors/links.d.ts.map +1 -1
  24. package/dist/detectors/links.js +11 -0
  25. package/dist/detectors/links.js.map +1 -1
  26. package/dist/detectors/punctuation.d.ts +3 -0
  27. package/dist/detectors/punctuation.d.ts.map +1 -0
  28. package/dist/detectors/punctuation.js +26 -0
  29. package/dist/detectors/punctuation.js.map +1 -0
  30. package/dist/detectors/spoilers.d.ts +3 -0
  31. package/dist/detectors/spoilers.d.ts.map +1 -0
  32. package/dist/detectors/spoilers.js +25 -0
  33. package/dist/detectors/spoilers.js.map +1 -0
  34. package/dist/detectors/words.d.ts +3 -0
  35. package/dist/detectors/words.d.ts.map +1 -0
  36. package/dist/detectors/words.js +56 -0
  37. package/dist/detectors/words.js.map +1 -0
  38. package/dist/enforcement.d.ts.map +1 -1
  39. package/dist/enforcement.js +27 -0
  40. package/dist/enforcement.js.map +1 -1
  41. package/dist/index.d.ts +2 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/types.d.ts +59 -6
  46. package/dist/types.d.ts.map +1 -1
  47. package/package.json +2 -2
package/README.md CHANGED
@@ -1,12 +1,28 @@
1
1
  # Disguard
2
2
 
3
+ <div align="center">
4
+
5
+ **Configurable antispam for discord.js v14**
6
+
7
+ Flood · phishing · channel hop · blocked words · ghost pings · TypeScript
8
+
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/)
12
+
13
+ [Install](#install) · [Quick start](#quick-start) · [API](#api) · [Español](#español)
14
+
15
+ </div>
16
+
17
+ ---
18
+
3
19
  npm: [`@amatiscorp/disguard`](https://www.npmjs.com/package/@amatiscorp/disguard)
4
20
 
5
21
  Configurable antispam for [discord.js](https://discord.js.org) v14 bots.
6
22
 
7
23
  It is **not** a bot. You plug it into your existing `Client` and decide every threshold, allowlist, and punishment.
8
24
 
9
- Detects flood, repeated text, phishing / unwanted links, duplicate images, mention spam, excessive caps, and emoji spam.
25
+ Detects flood, channel hopping, blocked words, phishing / unwanted links, duplicate images, mention spam, ghost pings, excessive caps, and emoji spam.
10
26
 
11
27
  > **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).
12
28
 
@@ -21,6 +37,58 @@ Detects flood, repeated text, phishing / unwanted links, duplicate images, menti
21
37
 
22
38
  # English
23
39
 
40
+ ## What's new in 1.3.0
41
+
42
+ More detectors, role overlays, mute-style role punishments, and extra ignore knobs. Everything is optional and falls back to the preset.
43
+
44
+ | Option | Default | What it does |
45
+ | --- | --- | --- |
46
+ | `words` | **off**, empty list | Block a word list and/or regex. `ignoreCase` and `matchWholeWord` are both `true` by default. |
47
+ | `hop` | **on**, 5 channels / 8s | Same user posting across too many unique channels in a window (raid / hop). |
48
+ | `punctuation` | **on**, 10 | `aaaaaa` / `!!!!!!` runs longer than `maxRepeated`. |
49
+ | `spoilers` | **on**, 8 pairs | Too many `\|\|spoiler\|\|` pairs in one message. |
50
+ | `ghostPing` | **off** (`strict`: on) | Punish deleting a message that mentioned people (`messageDelete`). |
51
+ | `checkDeletes` | `true` | Listen for `messageDelete`. Set `false` if you do not want ghost-ping checks. |
52
+ | `ignored.prefixes` | `[]` | Skip messages that start with `!`, `/`, `.`, etc. |
53
+ | `ignoreThreads` | `false` | Skip thread messages. |
54
+ | `ignoreSystem` | `true` | Skip Discord system messages. |
55
+ | `roleOverrides` / `setRoleConfig` | `{}` | Per-role config overlay (before channel overrides). |
56
+ | `links.blockOauth` | `true` | Flag non-allowlisted URLs whose path looks like `/oauth`, `/authorize`, `/login`. Official `discord.com` stays allowed. |
57
+ | `files.maxBytes` | `0` | Max attachment size in bytes. `0` = unlimited. |
58
+ | `accounts.minGuildAgeDays` | `0` | Flag members who joined the guild too recently. `0` = off. |
59
+ | `punishment.addRoleIds` / `removeRoleIds` | `[]` | Add/remove roles on punish (mute role). Needs **Manage Roles**. |
60
+ | `punishment.minStrikesForRoles` | `1` | Minimum strikes before those role actions run. |
61
+ | `onCooldown` | — | Callback when extra spam is deleted quietly during the punishment cooldown. |
62
+ | `pause()` / `resume()` | — | Freeze enforcement without removing listeners. |
63
+
64
+ ```js
65
+ const antispam = new AntiSpam(client, {
66
+ preset: "balanced",
67
+ locale: "es",
68
+ ignored: { prefixes: ["!", "/", "."], roles: ["STAFF_ROLE_ID"] },
69
+ words: { enabled: true, list: ["raid-now"], regex: ["n[i1]tro\\s+free"] },
70
+ hop: { maxChannels: 4, windowMs: 6_000 },
71
+ ghostPing: { enabled: true, minMentions: 1, maxAgeMs: 15_000 },
72
+ links: { blockOauth: true },
73
+ files: { maxBytes: 8 * 1024 * 1024 },
74
+ roleOverrides: {
75
+ "VIP_ROLE_ID": { flood: { maxMessages: 10 } },
76
+ },
77
+ punishment: {
78
+ addRoleIds: ["MUTE_ROLE_ID"],
79
+ minStrikesForRoles: 2,
80
+ },
81
+ onCooldown(message) {
82
+ console.log("quiet delete", message.id);
83
+ },
84
+ });
85
+
86
+ antispam.setRoleConfig("VIP_ROLE_ID", { emojis: { maxEmojis: 40 } });
87
+ antispam.pause(); // raid over? antispam.resume();
88
+ ```
89
+
90
+ 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.
91
+
24
92
  ## What's new in 1.2.0
25
93
 
26
94
  More knobs, per-channel rules, and bilingual user-facing text.
@@ -105,6 +173,7 @@ console.log(antispam.getStats());
105
173
 
106
174
  ## Table of contents
107
175
 
176
+ - [What's new in 1.3.0](#whats-new-in-130)
108
177
  - [What's new in 1.2.0](#whats-new-in-120)
109
178
  - [What's new in 1.1.0](#whats-new-in-110)
110
179
  - [Features](#features)
@@ -138,17 +207,23 @@ console.log(antispam.getStats());
138
207
  | **file** | Dangerous attachments (`.exe`, `.bat`, `.dll`, `.msi`, …) |
139
208
  | **zalgo** | Obfuscated / zalgo text (stacked combining marks) |
140
209
  | **newline** | Message walls made of empty lines |
141
- | **account** | Brand-new Discord accounts / default avatar (off by default) |
210
+ | **account** | Brand-new Discord accounts / default avatar / new guild members (off by default) |
142
211
  | **length** | Over-long messages (off by default) |
212
+ | **word** | Custom blocked words and regex (off until you fill `list` / `regex`) |
213
+ | **hop** | Same user jumping across too many channels |
214
+ | **punctuation** | Long runs of the same character (`!!!!`, `aaaa`) |
215
+ | **spoiler** | Spoiler-tag walls |
216
+ | **ghost** | Deleted messages that mentioned users (off by default) |
143
217
 
144
218
  Also included:
145
219
 
146
220
  - Three presets: `lenient`, `balanced`, `strict`
147
- - Ignore lists for users, roles, channels, categories, and guilds
221
+ - Ignore lists for users, roles, channels, categories, guilds, and command prefixes
222
+ - Per-guild, per-channel, and per-role overlays
148
223
  - Strike system with decay
149
- - Optional delete / warn / timeout / kick / ban (kick and ban are **off** by default)
224
+ - Optional delete / warn / timeout / kick / ban / addRole / removeRole (kick and ban are **off** by default)
150
225
  - `dryRun` to tune rules without punishing anyone
151
- - Edit scanning (links and mentions)
226
+ - Edit scanning (links, mentions, and words)
152
227
  - Zero extra runtime dependencies (peer: `discord.js`)
153
228
  - In-memory store only — no database required
154
229
  - Full TypeScript types
@@ -263,6 +338,7 @@ npm run dev
263
338
  | Warn in the channel | **Send Messages** |
264
339
  | Log embeds | **Embed Links** |
265
340
  | Kick / ban (opt-in) | **Kick Members** / **Ban Members** |
341
+ | Mute / add / remove roles (opt-in) | **Manage Roles** |
266
342
 
267
343
  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
344
 
@@ -270,11 +346,12 @@ Enable **Message Content Intent** in the [Discord Developer Portal](https://disc
270
346
 
271
347
  ## How it works
272
348
 
273
- 1. Ignores bots, webhooks, the owner, administrators, and anything in your ignore lists.
349
+ 1. Ignores bots, webhooks, system messages, the owner, administrators, command prefixes, and anything in your ignore lists.
274
350
  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.
351
+ 3. Runs detectors in this order: files → words → flood → hop → duplicates → links → images → mentions → zalgo → newlines → punctuation → spoilers → accounts → length → caps → emojis. The first match wins.
276
352
  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.
353
+ 5. Message edits re-check **links**, **mentions**, and **words**, so editing `hello` into a phishing URL or a blocked word is still caught without counting as flood.
354
+ 6. Deletes can trigger **ghost** pings if `ghostPing.enabled` is true.
278
355
 
279
356
  Call `antispam.stop()` on shutdown, hot reload, or plugin unload.
280
357
 
@@ -304,6 +381,8 @@ new AntiSpam(client, {
304
381
  | Mentions | 10 | 6 | 3 |
305
382
  | Block invites | no | no | yes |
306
383
  | Block shorteners | no | yes | yes |
384
+ | Channel hop | off | 5 / 8s | 3 / 6s |
385
+ | Ghost pings | off | off | on |
307
386
  | Auto timeout | off | 60s from 2 strikes | 5 min from 1 strike |
308
387
 
309
388
  ## Full configuration
@@ -320,13 +399,17 @@ Every option is optional. Missing fields fall back to the preset (or `balanced`
320
399
  | `ignoreWebhooks` | `boolean` | `true` | Skip webhooks. |
321
400
  | `ignoreOwner` | `boolean` | `true` | Skip the guild owner. |
322
401
  | `ignoreAdministrators` | `boolean` | `true` | Skip members with Administrator. |
323
- | `checkEdits` | `boolean` | `true` | Re-scan edits for links and mentions. |
402
+ | `checkEdits` | `boolean` | `true` | Re-scan edits for links, mentions, and words. |
403
+ | `checkDeletes` | `boolean` | `true` | Listen for `messageDelete` (ghost pings). |
404
+ | `ignoreThreads` | `boolean` | `false` | Skip messages in threads. |
405
+ | `ignoreSystem` | `boolean` | `true` | Skip Discord system messages. |
324
406
  | `cleanupIntervalMs` | `number` | `60000` | How often the memory store is pruned. |
325
407
  | `locale` | `"en"` \| `"es"` | `"en"` | Language for warnings and log embeds. |
326
408
  | `ignorePermissions` | `string[]` | `[]` | Permission names that bypass checks. |
327
409
  | `disabledDetectors` | `DetectorType[]` | `[]` | Detectors to skip. |
328
410
  | `detectorOrder` | `DetectorType[]` | `[]` | Custom order. Empty = default. |
329
411
  | `channelOverrides` | `object` | `{}` | Per-channel patches keyed by channel id. |
412
+ | `roleOverrides` | `object` | `{}` | Per-role patches keyed by role id. |
330
413
 
331
414
  ### Ignore lists
332
415
 
@@ -337,10 +420,11 @@ ignored: {
337
420
  channels: ["789"],
338
421
  categories: ["101"],
339
422
  guilds: ["202"],
423
+ prefixes: ["!", "/", "."],
340
424
  }
341
425
  ```
342
426
 
343
- IDs are snowflakes as strings. A staff role in `ignored.roles` bypasses every detector.
427
+ 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
428
 
345
429
  ### Flood
346
430
 
@@ -387,6 +471,9 @@ links: {
387
471
  suspiciousTlds: [], // e.g. ["tk", "gq"]
388
472
  customPatterns: [], // regex against the whole message
389
473
  extraPhishingKeywords: ["fake giveaway"],
474
+ maxLinks: 0,
475
+ scanEmbeds: true,
476
+ blockOauth: true,
390
477
  severity: "high",
391
478
  }
392
479
  ```
@@ -402,6 +489,7 @@ Built-in checks (each can be turned off):
402
489
  - Scam phrases plus any link (`free nitro`, `steam gift`, `verifica tu cuenta`, …)
403
490
  - Discord invite links if `blockInvites` is `true`
404
491
  - Markdown-masked links: `[click](https://evil.test)`
492
+ - Login / OAuth-looking paths on non-allowlisted hosts if `blockOauth` is `true`
405
493
 
406
494
  ```js
407
495
  // Block foreign server invites
@@ -474,6 +562,7 @@ accounts: {
474
562
  enabled: false, // turn on to flag new users
475
563
  minAgeDays: 3,
476
564
  blockDefaultAvatar: false,
565
+ minGuildAgeDays: 0, // 0 = off; e.g. 1 = joined this guild today
477
566
  severity: "medium",
478
567
  }
479
568
 
@@ -490,6 +579,7 @@ length: {
490
579
  files: {
491
580
  enabled: true,
492
581
  blockedExtensions: ["exe", "bat", "cmd", "com", "scr", "dll", "msi", "vbs", "ps1", "jar", "apk"],
582
+ maxBytes: 0, // 0 = unlimited
493
583
  severity: "critical",
494
584
  }
495
585
 
@@ -506,6 +596,57 @@ newlines: {
506
596
  }
507
597
  ```
508
598
 
599
+ ### Words, channel hop, punctuation, spoilers
600
+
601
+ ```js
602
+ words: {
603
+ enabled: false, // stays off until you add a list or regex
604
+ list: ["raid-now"],
605
+ regex: ["n[i1]tro\\s+free"],
606
+ ignoreCase: true,
607
+ matchWholeWord: true,
608
+ severity: "high",
609
+ }
610
+
611
+ hop: {
612
+ enabled: true,
613
+ maxChannels: 5, // 5th unique channel in the window triggers
614
+ windowMs: 8_000,
615
+ severity: "high",
616
+ }
617
+
618
+ punctuation: {
619
+ enabled: true,
620
+ maxRepeated: 10, // 11+ of the same character in a row
621
+ severity: "low",
622
+ }
623
+
624
+ spoilers: {
625
+ enabled: true,
626
+ maxSpoilers: 8,
627
+ severity: "low",
628
+ }
629
+ ```
630
+
631
+ `lenient` turns hop and punctuation off. `strict` tightens hop to 3 channels / 6s.
632
+
633
+ ### Ghost pings
634
+
635
+ Off in `balanced` / `lenient`. On in `strict`. Needs `checkDeletes: true` (default) and the deleted message in cache (`Partials.Message` in your Client).
636
+
637
+ ```js
638
+ ghostPing: {
639
+ enabled: false,
640
+ minMentions: 1,
641
+ maxAgeMs: 15_000, // 0 = any age
642
+ severity: "high",
643
+ }
644
+
645
+ checkDeletes: true
646
+ ```
647
+
648
+ Disable with `ghostPing: { enabled: false }` or `disabledDetectors: ["ghost"]`.
649
+
509
650
  ### Punishment
510
651
 
511
652
  Kick and ban stay **disabled** on purpose. Turn them on only if you really want that.
@@ -525,6 +666,9 @@ punishment: {
525
666
  cooldownMs: 8_000, // no second timeout/warn during this window
526
667
  deleteDuringCooldown: true, // still delete extra spam quietly
527
668
  warnAsEmbed: false,
669
+ addRoleIds: [], // e.g. ["MUTE_ROLE_ID"] — needs Manage Roles
670
+ removeRoleIds: [],
671
+ minStrikesForRoles: 1,
528
672
  }
529
673
 
530
674
  // timeout.scale: "none" | "linear" (base * strikes) | "exponential" (base * 2^n)
@@ -534,7 +678,7 @@ punishment: {
534
678
 
535
679
  `warnMessage` placeholders: `{user}` `{reason}` `{type}` `{strikes}`.
536
680
 
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.
681
+ 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
682
 
539
683
  If the bot lacks a permission, or the target is higher in the hierarchy, that action is skipped and listed in `result.skipped`.
540
684
 
@@ -552,6 +696,9 @@ new AntiSpam(client, {
552
696
  onError(error, context) {
553
697
  console.error("[disguard]", context, error);
554
698
  },
699
+ onCooldown(message) {
700
+ // Extra spam during punishment.cooldownMs (already deleted if deleteDuringCooldown).
701
+ },
555
702
  });
556
703
  ```
557
704
 
@@ -559,7 +706,9 @@ new AntiSpam(client, {
559
706
 
560
707
  ```ts
561
708
  {
562
- type: "flood" | "duplicate" | "link" | "image" | "mention" | "caps" | "emoji",
709
+ type: "flood" | "duplicate" | "link" | "image" | "mention" | "caps" | "emoji"
710
+ | "file" | "zalgo" | "newline" | "account" | "length"
711
+ | "word" | "hop" | "punctuation" | "spoiler" | "ghost",
563
712
  severity: "low" | "medium" | "high" | "critical",
564
713
  userId: string,
565
714
  guildId: string,
@@ -567,7 +716,7 @@ new AntiSpam(client, {
567
716
  messageId: string,
568
717
  reason: string,
569
718
  details: Record<string, unknown>,
570
- recommendedActions: Array<"delete" | "warn" | "timeout" | "kick" | "ban">,
719
+ recommendedActions: Array<"delete" | "warn" | "timeout" | "kick" | "ban" | "addRole" | "removeRole">,
571
720
  timestamp: number,
572
721
  }
573
722
  ```
@@ -594,6 +743,9 @@ const antispam = new AntiSpam(client, options);
594
743
 
595
744
  antispam.start();
596
745
  antispam.stop();
746
+ antispam.pause();
747
+ antispam.resume();
748
+ antispam.isPaused();
597
749
 
598
750
  const config = antispam.getConfig();
599
751
  antispam.setConfig({ flood: { maxMessages: 8 } }); // deep merge, other keys stay
@@ -608,6 +760,8 @@ antispam.clearGuildConfig(guildId);
608
760
  antispam.setChannelConfig(guildId, channelId, { images: { maxRepeats: 8 } });
609
761
  antispam.getChannelConfig(guildId, channelId);
610
762
  antispam.clearChannelConfig(guildId, channelId);
763
+ antispam.setRoleConfig(roleId, { flood: { maxMessages: 12 } });
764
+ antispam.clearRoleConfig(roleId);
611
765
 
612
766
  antispam.use(customDetector);
613
767
  antispam.getStats();
@@ -692,6 +846,18 @@ antispam.setConfig(resolveConfig("strict", { ignored: antispam.getConfig().ignor
692
846
  images: { hashMode: "content", maxDownloadBytes: 1_000_000 }
693
847
  ```
694
848
 
849
+ **Mute role instead of timeout**
850
+
851
+ ```js
852
+ punishment: {
853
+ timeout: { enabled: false, durationMs: 60_000, minStrikes: 99 },
854
+ addRoleIds: ["MUTE_ROLE_ID"],
855
+ minStrikesForRoles: 1,
856
+ }
857
+ ```
858
+
859
+ The bot role must sit **above** the mute role. Needs **Manage Roles**.
860
+
695
861
  **Reset a user after a false positive**
696
862
 
697
863
  ```js
@@ -746,9 +912,14 @@ Wait a few seconds between categories so flood does not eat the next test.
746
912
  | File | Upload a dummy `.exe` or `.bat` |
747
913
  | Zalgo | Text with many stacked combining marks |
748
914
  | Newlines | A message with more than 15 line breaks |
915
+ | Channel hop | Same user posts in 5 different channels within 8 seconds |
916
+ | Punctuation | `aaaaaaaaaaa` or `!!!!!!!!!!!` |
917
+ | Spoilers | More than 8 `\|\|spoiler\|\|` pairs |
918
+ | Blocked word | Enable `words` first, then send a listed word |
919
+ | Ghost ping | Mention someone and delete the message within 15s (`ghostPing.enabled: true`) |
749
920
  | Edit | Send `hello`, then edit it to `Free Nitro https://bit.ly/test` |
750
921
 
751
- Watch the terminal: `[detect] flood|duplicate|link|image|mention|caps|emoji`.
922
+ Watch the terminal: `[detect] flood|duplicate|link|image|mention|caps|emoji|file|zalgo|newline|word|hop|punctuation|spoiler|ghost`.
752
923
 
753
924
  The second strike applies a 1 minute timeout with the default example config. Strikes decay after 15 minutes, or call `resetUser`.
754
925
 
@@ -789,6 +960,45 @@ MIT
789
960
 
790
961
  # Español
791
962
 
963
+ ## Novedades de 1.3.0
964
+
965
+ Más detectores, overlays por rol, mute por rol y más ignores. Todo es opcional.
966
+
967
+ | Opción | Default | Qué hace |
968
+ | --- | --- | --- |
969
+ | `words` | **apagado**, lista vacía | Palabras y/o regex bloqueados. `ignoreCase` y `matchWholeWord` en `true`. |
970
+ | `hop` | **on**, 5 canales / 8s | El mismo usuario escribe en demasiados canales (raid / hop). |
971
+ | `punctuation` | **on**, 10 | Rachas de `aaaaaa` / `!!!!!!` más largas que `maxRepeated`. |
972
+ | `spoilers` | **on**, 8 pares | Demasiados `\|\|spoiler\|\|` en un mensaje. |
973
+ | `ghostPing` | **off** (`strict`: on) | Castiga borrar un mensaje que mencionaba gente. |
974
+ | `checkDeletes` | `true` | Escucha `messageDelete`. |
975
+ | `ignored.prefixes` | `[]` | Ignora mensajes que empiezan por `!`, `/`, `.`. |
976
+ | `ignoreThreads` | `false` | Ignora hilos. |
977
+ | `ignoreSystem` | `true` | Ignora mensajes de sistema. |
978
+ | `roleOverrides` / `setRoleConfig` | `{}` | Config por rol (antes de la de canal). |
979
+ | `links.blockOauth` | `true` | URLs de login/OAuth fuera de la allowList. `discord.com` sigue permitido. |
980
+ | `files.maxBytes` | `0` | Tamaño máximo de adjunto. `0` = sin límite. |
981
+ | `accounts.minGuildAgeDays` | `0` | Miembros demasiado nuevos en **este** servidor. `0` = off. |
982
+ | `punishment.addRoleIds` / `removeRoleIds` | `[]` | Añadir/quitar roles al castigar (mute). Hace falta **Manage Roles**. |
983
+ | `onCooldown` | — | Callback cuando el cooldown borra spam extra en silencio. |
984
+ | `pause()` / `resume()` | — | Congela el antispam sin quitar listeners. |
985
+
986
+ ```js
987
+ const antispam = new AntiSpam(client, {
988
+ locale: "es",
989
+ ignored: { prefixes: ["!", "/", "."], roles: ["ID_STAFF"] },
990
+ words: { enabled: true, list: ["raid-now"] },
991
+ hop: { maxChannels: 4, windowMs: 6_000 },
992
+ ghostPing: { enabled: true, maxAgeMs: 15_000 },
993
+ punishment: { addRoleIds: ["ID_MUTE"], minStrikesForRoles: 2 },
994
+ });
995
+
996
+ antispam.setRoleConfig("ID_VIP", { flood: { maxMessages: 10 } });
997
+ antispam.pause();
998
+ ```
999
+
1000
+ Kick y ban siguen **apagados**. Ghost ping necesita el mensaje en caché (`Partials.Message`).
1001
+
792
1002
  ## Novedades de 1.2.0
793
1003
 
794
1004
  Más opciones, reglas por canal y textos al usuario en inglés o español.
@@ -839,6 +1049,7 @@ new AntiSpam(client, {
839
1049
 
840
1050
  ## Tabla de contenidos
841
1051
 
1052
+ - [Novedades de 1.3.0](#novedades-de-130)
842
1053
  - [Novedades de 1.2.0](#novedades-de-120)
843
1054
  - [Novedades de 1.1.0](#novedades-de-110)
844
1055
  - [Qué es](#qué-es)
@@ -862,7 +1073,7 @@ new AntiSpam(client, {
862
1073
 
863
1074
  **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
1075
 
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.
1076
+ 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
1077
 
867
1078
  ## Requisitos
868
1079
 
@@ -948,6 +1159,7 @@ npm run dev
948
1159
  | Avisar en el canal | **Send Messages** |
949
1160
  | Embed de logs | **Embed Links** |
950
1161
  | Kick / ban (opt-in) | **Kick Members** / **Ban Members** |
1162
+ | Mute / add / remove roles (opt-in) | **Manage Roles** |
951
1163
 
952
1164
  No puede sancionar al dueño del servidor ni a quien esté por encima del bot en la jerarquía.
953
1165
 
@@ -955,11 +1167,12 @@ Activa **Message Content Intent** en el [Portal de Discord](https://discord.com/
955
1167
 
956
1168
  ## Cómo funciona
957
1169
 
958
- 1. Ignora bots, webhooks, el dueño, administradores y tus listas de ignore.
1170
+ 1. Ignora bots, webhooks, mensajes de sistema, el dueño, administradores, prefijos de comando y tus listas de ignore.
959
1171
  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.
1172
+ 3. Pasa el mensaje por: archivos → palabras → flood → hop → duplicados → enlaces → imágenes → menciones → zalgo → líneas → puntuación → spoilers → cuentas → longitud → caps → emojis. El primero que dispare gana.
961
1173
  4. Suma un strike (con caducidad) y aplica el castigo configurado.
962
- 5. Las ediciones solo revisan **enlaces** y **menciones**.
1174
+ 5. Las ediciones revisan **enlaces**, **menciones** y **palabras**.
1175
+ 6. Los borrados pueden disparar **ghost** ping si `ghostPing.enabled` es true.
963
1176
 
964
1177
  Llama `antispam.stop()` al apagar el proceso.
965
1178
 
@@ -992,8 +1205,12 @@ Todo es opcional. Lo que no pongas usa el preset.
992
1205
  | `ignoreWebhooks` | `boolean` | `true` | Ignora webhooks. |
993
1206
  | `ignoreOwner` | `boolean` | `true` | Ignora al dueño. |
994
1207
  | `ignoreAdministrators` | `boolean` | `true` | Ignora quien tenga Administrator. |
995
- | `checkEdits` | `boolean` | `true` | Revisa ediciones (enlaces y menciones). |
1208
+ | `checkEdits` | `boolean` | `true` | Revisa ediciones (enlaces, menciones y palabras). |
1209
+ | `checkDeletes` | `boolean` | `true` | Escucha `messageDelete` (ghost pings). |
1210
+ | `ignoreThreads` | `boolean` | `false` | Ignora hilos. |
1211
+ | `ignoreSystem` | `boolean` | `true` | Ignora mensajes de sistema. |
996
1212
  | `cleanupIntervalMs` | `number` | `60000` | Limpieza de memoria. |
1213
+ | `roleOverrides` | `object` | `{}` | Parches por rol. |
997
1214
 
998
1215
  ### Listas de ignore
999
1216
 
@@ -1004,6 +1221,7 @@ ignored: {
1004
1221
  channels: ["id"],
1005
1222
  categories: ["id"],
1006
1223
  guilds: ["id"],
1224
+ prefixes: ["!", "/", "."],
1007
1225
  }
1008
1226
  ```
1009
1227
 
@@ -1048,13 +1266,16 @@ links: {
1048
1266
  suspiciousTlds: [],
1049
1267
  customPatterns: ["steamcommunity\\.ru"],
1050
1268
  extraPhishingKeywords: ["sorteo falso"],
1269
+ maxLinks: 0,
1270
+ scanEmbeds: true,
1271
+ blockOauth: true,
1051
1272
  severity: "high",
1052
1273
  }
1053
1274
  ```
1054
1275
 
1055
1276
  La `allowList` gana a las heurísticas. `discord.com` y `youtube.com` ya vienen permitidos.
1056
1277
 
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)`.
1278
+ 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
1279
 
1059
1280
  ### Imágenes
1060
1281
 
@@ -1086,11 +1307,23 @@ emojis: { enabled: true, maxEmojis: 12, maxStickers: 3, severity: "low" },
1086
1307
  ### Archivos, zalgo, saltos de línea
1087
1308
 
1088
1309
  ```js
1089
- files: { enabled: true, blockedExtensions: ["exe", "bat", "cmd", "dll", "msi"], severity: "critical" },
1310
+ files: { enabled: true, blockedExtensions: ["exe", "bat", "cmd", "dll", "msi"], maxBytes: 0, severity: "critical" },
1090
1311
  zalgo: { enabled: true, maxCombining: 20, severity: "medium" },
1091
1312
  newlines: { enabled: true, maxNewlines: 15, severity: "low" },
1092
1313
  ```
1093
1314
 
1315
+ ### Palabras, hop, puntuación, spoilers, ghost ping
1316
+
1317
+ ```js
1318
+ words: { enabled: false, list: ["raid-now"], regex: [], ignoreCase: true, matchWholeWord: true, severity: "high" },
1319
+ hop: { enabled: true, maxChannels: 5, windowMs: 8_000, severity: "high" },
1320
+ punctuation: { enabled: true, maxRepeated: 10, severity: "low" },
1321
+ spoilers: { enabled: true, maxSpoilers: 8, severity: "low" },
1322
+ ghostPing: { enabled: false, minMentions: 1, maxAgeMs: 15_000, severity: "high" },
1323
+ ```
1324
+
1325
+ `lenient` apaga hop y puntuación. `strict` enciende ghost ping y aprieta hop a 3 canales / 6s.
1326
+
1094
1327
  ### Castigos
1095
1328
 
1096
1329
  Kick y ban van **apagados**. Actívalos solo si lo tienes claro.
@@ -1109,12 +1342,15 @@ punishment: {
1109
1342
  strikeDecayMs: 15 * 60_000,
1110
1343
  cooldownMs: 8_000,
1111
1344
  deleteDuringCooldown: true,
1345
+ addRoleIds: [],
1346
+ removeRoleIds: [],
1347
+ minStrikesForRoles: 1,
1112
1348
  }
1113
1349
  ```
1114
1350
 
1115
1351
  Placeholders: `{user}` `{reason}` `{type}` `{strikes}`.
1116
1352
 
1117
- Con `escalate: true` se aplica **un** castigo fuerte (timeout, kick o ban). Aviso y borrado se pueden sumar.
1353
+ 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
1354
 
1119
1355
  ## Callbacks
1120
1356
 
@@ -1126,6 +1362,7 @@ new AntiSpam(client, {
1126
1362
  onError(error, context) {
1127
1363
  console.error(context, error);
1128
1364
  },
1365
+ onCooldown(message) {},
1129
1366
  });
1130
1367
  ```
1131
1368
 
@@ -1138,11 +1375,15 @@ const antispam = new AntiSpam(client, options);
1138
1375
 
1139
1376
  antispam.start();
1140
1377
  antispam.stop();
1378
+ antispam.pause();
1379
+ antispam.resume();
1141
1380
  antispam.getConfig();
1142
1381
  antispam.setConfig({ flood: { maxMessages: 8 } });
1143
1382
  antispam.getStrikes(guildId, userId);
1144
1383
  antispam.resetUser(guildId, userId);
1145
1384
  antispam.setGuildConfig(guildId, { flood: { maxMessages: 3 } });
1385
+ antispam.setChannelConfig(guildId, channelId, { flood: { maxMessages: 12 } });
1386
+ antispam.setRoleConfig(roleId, { emojis: { maxEmojis: 40 } });
1146
1387
  antispam.getStats();
1147
1388
 
1148
1389
  const incident = await antispam.analyze(message);
@@ -1174,6 +1415,8 @@ new AntiSpam(client, {
1174
1415
 
1175
1416
  **Quitar strikes:** `antispam.resetUser(guildId, userId)`.
1176
1417
 
1418
+ **Mute por rol:** `punishment: { addRoleIds: ["ID_MUTE"], timeout: { enabled: false, durationMs: 0, minStrikes: 99 } }`. El rol del bot debe estar por encima del mute.
1419
+
1177
1420
  **Cambiar a strict en caliente**
1178
1421
 
1179
1422
  ```js
@@ -1211,9 +1454,14 @@ Espera unos segundos entre categorías.
1211
1454
  | Archivo | Sube un `.exe` o `.bat` (aunque sea de mentira) |
1212
1455
  | Zalgo | Texto con muchos diacríticos apilados |
1213
1456
  | Líneas | Un mensaje con más de 15 enters |
1457
+ | Hop | El mismo usuario en 5 canales distintos en 8s |
1458
+ | Puntuación | `aaaaaaaaaaa` o `!!!!!!!!!!!` |
1459
+ | Spoilers | Más de 8 pares `\|\|spoiler\|\|` |
1460
+ | Palabra | Activa `words` y manda una de la lista |
1461
+ | Ghost ping | Menciona a alguien y borra el mensaje en menos de 15s (`ghostPing.enabled: true`) |
1214
1462
  | Edición | Manda `hola` y edítalo a `Free Nitro https://bit.ly/test` |
1215
1463
 
1216
- En la terminal: `[detect] flood|duplicate|link|image|mention|caps|emoji|file|zalgo|newline`.
1464
+ En la terminal: `[detect] flood|duplicate|link|image|mention|caps|emoji|file|zalgo|newline|word|hop|punctuation|spoiler|ghost`.
1217
1465
 
1218
1466
  Al segundo strike el ejemplo mete timeout de 1 minuto. Los strikes caducan a los 15 minutos, o usa `resetUser`.
1219
1467
 
@@ -11,14 +11,20 @@ export declare class AntiSpam {
11
11
  private readonly channelConfigs;
12
12
  private stats;
13
13
  private started;
14
+ private paused;
14
15
  private cleanupTimer;
15
16
  private readonly onMessage;
16
17
  private readonly onEdit;
18
+ private readonly onDelete;
17
19
  constructor(client: Client, options?: AntiSpamOptions);
18
- /** Starts listening to `messageCreate` (and `messageUpdate` if enabled). */
20
+ /** Starts listening to `messageCreate` (and `messageUpdate` / `messageDelete` if enabled). */
19
21
  start(): this;
20
22
  /** Stops listening. Call this on shutdown. */
21
23
  stop(): this;
24
+ /** Temporarily skip all enforcement without removing listeners. */
25
+ pause(): this;
26
+ resume(): this;
27
+ isPaused(): boolean;
22
28
  /** Adds a custom detector. It runs after the built-in ones. */
23
29
  use(detector: Detector): this;
24
30
  getConfig(): ResolvedConfig;
@@ -30,6 +36,8 @@ export declare class AntiSpam {
30
36
  getChannelConfig(guildId: string, channelId: string): ResolvedConfig;
31
37
  setChannelConfig(guildId: string, channelId: string, patch: DeepPartial<ResolvedConfig>): ResolvedConfig;
32
38
  clearChannelConfig(guildId: string, channelId: string): void;
39
+ setRoleConfig(roleId: string, patch: DeepPartial<ResolvedConfig>): void;
40
+ clearRoleConfig(roleId: string): void;
33
41
  getStrikes(guildId: string, userId: string): number;
34
42
  isCoolingDown(guildId: string, userId: string): boolean;
35
43
  resetUser(guildId: string, userId: string): void;
@@ -43,6 +51,7 @@ export declare class AntiSpam {
43
51
  isEdit?: boolean;
44
52
  }): Promise<Incident | null>;
45
53
  private handle;
54
+ private handleDelete;
46
55
  shouldIgnore(message: Message): boolean;
47
56
  private configFor;
48
57
  private orderedDetectors;
@@ -1 +1 @@
1
- {"version":3,"file":"AntiSpam.d.ts","sourceRoot":"","sources":["../src/AntiSpam.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,OAAO,EACb,MAAM,YAAY,CAAC;AAepB,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAEV,eAAe,EACf,aAAa,EACb,WAAW,EACX,QAAQ,EAER,QAAQ,EAER,cAAc,EACf,MAAM,SAAS,CAAC;AA8BjB,qBAAa,QAAQ;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,cAAqB;IAEnC,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkD;IAC/E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAkD;IACjF,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,YAAY,CAA+B;IAEnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAExB;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGrB;gBAEU,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB;IAqBzD,4EAA4E;IAC5E,KAAK,IAAI,IAAI;IAcb,8CAA8C;IAC9C,IAAI,IAAI,IAAI;IAYZ,+DAA+D;IAC/D,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAK7B,SAAS,IAAI,cAAc;IAI3B,SAAS,CAAC,KAAK,EAAE,WAAW,CAAC,cAAc,CAAC,GAAG,cAAc;IAK7D,gDAAgD;IAChD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc;IAI/C,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,cAAc,CAAC,GAAG,cAAc;IAMnF,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIvC,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc;IAIpE,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,cAAc,CAAC,GAAG,cAAc;IAOxG,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAI5D,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IASnD,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAKvD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAIhD,QAAQ,IAAI,aAAa;IAIzB,UAAU,IAAI,IAAI;IAIlB;;;OAGG;IACG,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;YA0C/E,MAAM;IAkDpB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO;IAgCvC,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,cAAc;CAUvB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,QAAQ,CAElF"}
1
+ {"version":3,"file":"AntiSpam.d.ts","sourceRoot":"","sources":["../src/AntiSpam.ts"],"names":[],"mappings":"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"}