Configurable antispam for discord.js v14
Flood · phishing · forwards · impersonation · join raids · leaked secrets · TypeScript
Install · Quick start · API · Español
npm: @amatiscorp/disguard
Configurable antispam for discord.js v14 bots.
It is not a bot. You plug it into your existing Client and decide every threshold, allowlist, and punishment.
Detects flood, leaked tokens/webhooks, channel hopping, blocked words, phishing, duplicate images, mention spam, ghost pings, forwarded-message dumps, bot impersonation, join raids, and more.
Languages
Forward dumps, sticker floods, bot/staff impersonation, join raids, voice-note spam, poll spam, and harder-to-dodge word/file filters. Kick/ban stay off. joins is off in balanced so a busy invite night does not false-positive.
| Option | Default | What it does |
|---|---|---|
forwards |
on, 4 / 10s | Too many Discord forwarded messages (HasSnapshot) in a short window. |
stickers |
on, 3 per message, 8 / 10s | Sticker dumps. blockOnly (off) flags sticker-only messages. |
impersonate |
on, match bot at 0.84 | Nick / username / display name too close to the bot (or names). |
joins |
off (strict: on, 6 / 8s) |
Too many distinct accounts joining the guild at once. Needs guildMemberAdd. |
voice |
on, 3 / 15s | Too many voice messages in a window. |
polls |
on, 2 / 20s | Too many polls in a window. |
files.blockDoubleExtensions |
true |
Flags virus.exe.png (blocked ext hidden behind a safe one). |
words.scanEmbeds |
true |
Word list also reads embed text. |
words.normalizeHomoglyphs |
true |
Folds Cyrillic / Greek / fullwidth lookalikes (rаid → raid). |
ignoreBoosters |
false |
Skip server boosters. |
punishment.slowmodeSeconds |
0 |
On raid / join hits, set channel slowmode (needs Manage Channels). Sticky until a mod changes it. |
getRecentIncidents() |
— | Last 50 live hits (not analyze()). |
setDetectorEnabled(type, bool) |
— | Toggle one detector without rewriting disabledDetectors. |
onJoin |
— | Callback for join-raid incidents (no message). |
const antispam = new AntiSpam(client, {
preset: "balanced",
forwards: { maxForwards: 3, windowMs: 8_000 },
stickers: { maxStickers: 2, maxInWindow: 6 },
impersonate: { names: ["Admin", "Moderador", "Staff"] },
joins: { enabled: true, maxJoins: 8, windowMs: 10_000 },
voice: { maxVoice: 3 },
polls: { maxPolls: 2 },
ignoreBoosters: true,
words: { enabled: true, list: ["raid-now"], normalizeHomoglyphs: true },
punishment: { slowmodeSeconds: 10 },
onJoin(incident) {
console.log("join raid", incident.details);
},
});
antispam.setDetectorEnabled("poll", false);
console.log(antispam.getRecentIncidents(10));Turn joins on only if you want a burst of accounts joining at once to timeout the newest member. Pair it with accounts.minAgeDays if the raid is all-new alts. impersonate.names is for staff/brand clones; matchBot already covers the bot's own name.
graceMessages now also runs impersonate and forward during the hello window (still skips flood).
Channel raids, reply spam, empty messages, embed walls, file allowlists, and per-user overlays. Kick/ban stay off. raid is off in balanced so busy servers do not false-positive.
| Option | Default | What it does |
|---|---|---|
replies |
on, 6 / 8s | Too many Discord replies in a short window (reply ping / bump spam). |
blank |
on | Empty or whitespace-only messages (no files/stickers/embeds). |
embeds |
on, 6 | Too many embeds in one message. |
raid |
off (strict: on, 6 users / 5s) |
Too many distinct users posting in the same channel at once. |
files.allowedExtensions |
[] |
If set, only those extensions are allowed (after the blocklist). |
emojis.maxInWindow |
0 (off) |
Cap emojis+stickers across several messages. |
ignoreNsfw |
false |
Skip NSFW channels. |
graceMessages |
0 |
First N messages from a user only run file/secret/link/word (say hi without flood). |
userOverrides / setUserConfig |
{} |
Per-user config overlay (after roles, before channel). |
const antispam = new AntiSpam(client, {
preset: "balanced",
replies: { maxReplies: 5, windowMs: 8_000 },
blank: { enabled: true },
embeds: { maxEmbeds: 6 },
raid: { enabled: true, maxUsers: 8, windowMs: 6_000 },
files: { allowedExtensions: ["png", "jpg", "webp", "gif"] },
graceMessages: 2,
ignoreNsfw: true,
emojis: { maxInWindow: 30, windowMs: 10_000 },
});
antispam.setUserConfig("KNOWN_SPAMMER_ID", { flood: { maxMessages: 2 } });Turn raid on only if you want a busy general chat to trip when many people post at once (join raids). For a meme channel, leave it off or raise maxUsers.
Secret scanning, cross-channel copy-paste, invisible unicode, attachment floods, and cleaner logs. Kick/ban stay off.
| Option | Default | What it does |
|---|---|---|
secrets |
on | Flags Discord bot tokens and webhook URLs pasted in chat (also embeds). The incident does not store the secret. |
echo |
on, 3 channels / 12s | Same text pasted across several channels (raid copypasta). Distinct from hop (any messages). |
invisible |
on, 8 | Zero-width / bidi characters used to dodge filters. |
attach |
on, 8 files / 10s | Too many attachments across a short window, not just dangerous extensions. |
duplicates.minLength |
8 |
Short stuff like ok / lol is no longer duplicate-spam. |
mentions.maxInWindow |
0 (off) |
Cap mentions across several messages, not only one. |
accounts.onlyWithLinks |
false (strict: true) |
New accounts are flagged only if the message has a URL. |
punishment.purgeCount |
0 |
After a hit, bulk-delete N of that user's recent messages in the channel. |
punishment.logWebhookUrl |
null |
Log embeds to a Discord webhook (no extra log channel needed). |
ignorePinned |
false |
Skip pinned messages. |
ignoreOlderThanMs |
0 |
Skip old messages (useful on reconnect replay). |
setEnabled(false) |
— | Turn the whole filter off without stop(). |
const antispam = new AntiSpam(client, {
preset: "balanced",
secrets: { enabled: true, botTokens: true, webhooks: true, extraPatterns: ["ghp_[A-Za-z0-9]{20,}"] },
echo: { maxChannels: 3, windowMs: 12_000, minLength: 12 },
invisible: { maxInvisible: 8 },
attach: { maxAttachments: 8, windowMs: 10_000 },
duplicates: { minLength: 8 },
accounts: { enabled: true, minAgeDays: 2, onlyWithLinks: true },
ignorePinned: true,
punishment: {
purgeCount: 5,
logWebhookUrl: process.env.DISGUARD_LOG_WEBHOOK || null,
},
});
antispam.setEnabled(false); // raid party / event
antispam.setEnabled(true);secrets is meant to delete leaks, not to harvest credentials. Reasons only say “token/webhook”, never the value.
| Option | Default | What it does |
|---|---|---|
words |
off, empty list | Block a word list and/or regex. ignoreCase and matchWholeWord are both true by default. |
hop |
on, 5 channels / 8s | Same user posting across too many unique channels in a window (raid / hop). |
punctuation |
on, 10 | aaaaaa / !!!!!! runs longer than maxRepeated. |
spoilers |
on, 8 pairs | Too many ||spoiler|| pairs in one message. |
ghostPing |
off (strict: on) |
Punish deleting a message that mentioned people (messageDelete). |
checkDeletes |
true |
Listen for messageDelete. Set false if you do not want ghost-ping checks. |
ignored.prefixes |
[] |
Skip messages that start with !, /, ., etc. |
ignoreThreads |
false |
Skip thread messages. |
ignoreSystem |
true |
Skip Discord system messages. |
roleOverrides / setRoleConfig |
{} |
Per-role config overlay (before channel overrides). |
links.blockOauth |
true |
Flag non-allowlisted URLs whose path looks like /oauth, /authorize, /login. Official discord.com stays allowed. |
files.maxBytes |
0 |
Max attachment size in bytes. 0 = unlimited. |
accounts.minGuildAgeDays |
0 |
Flag members who joined the guild too recently. 0 = off. |
punishment.addRoleIds / removeRoleIds |
[] |
Add/remove roles on punish (mute role). Needs Manage Roles. |
punishment.minStrikesForRoles |
1 |
Minimum strikes before those role actions run. |
onCooldown |
— | Callback when extra spam is deleted quietly during the punishment cooldown. |
pause() / resume() |
— | Freeze enforcement without removing listeners. |
const antispam = new AntiSpam(client, {
preset: "balanced",
locale: "es",
ignored: { prefixes: ["!", "/", "."], roles: ["STAFF_ROLE_ID"] },
words: { enabled: true, list: ["raid-now"], regex: ["n[i1]tro\\s+free"] },
hop: { maxChannels: 4, windowMs: 6_000 },
ghostPing: { enabled: true, minMentions: 1, maxAgeMs: 15_000 },
links: { blockOauth: true },
files: { maxBytes: 8 * 1024 * 1024 },
roleOverrides: {
"VIP_ROLE_ID": { flood: { maxMessages: 10 } },
},
punishment: {
addRoleIds: ["MUTE_ROLE_ID"],
minStrikesForRoles: 2,
},
onCooldown(message) {
console.log("quiet delete", message.id);
},
});
antispam.setRoleConfig("VIP_ROLE_ID", { emojis: { maxEmojis: 40 } });
antispam.pause(); // raid over? antispam.resume();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.
More knobs, per-channel rules, and bilingual user-facing text.
| Option | What it does |
|---|---|
locale |
"en" or "es" for warn + log labels. Empty warnMessage uses the locale template. |
ignorePermissions |
Skip members with those Discord permission names (ManageMessages, …). |
setChannelConfig(guildId, channelId, patch) |
Rules for one channel (on top of guild + global). |
channelOverrides |
Same thing in static config: { "CHANNEL_ID": { flood: { maxMessages: 10 } } }. |
disabledDetectors |
Turn off detectors by name without touching each block. |
detectorOrder |
Custom detector run order. |
flood.sameChannelOnly / duplicates.sameChannelOnly |
Count only the current channel. |
links.maxLinks / links.scanEmbeds |
Cap URLs per message; toggle embed scanning. |
images.skipContentTypes / images.maxAttachments |
Ignore GIFs, limit images per message. |
mentions.maxRepeatsOfSame |
Same user pinged over and over in one message. |
accounts |
Flag brand-new accounts (minAgeDays) or default avatars. Off by default. |
length |
Flag walls of text. Off by default. |
punishment.timeout.scale |
"none" | "linear" | "exponential" with maxDurationMs. |
punishment.warnAsEmbed |
Warning as an embed. |
const antispam = new AntiSpam(client, {
preset: "balanced",
locale: "es",
ignorePermissions: ["ManageMessages", "ModerateMembers"],
flood: { sameChannelOnly: true },
links: { maxLinks: 3, scanEmbeds: true },
accounts: { enabled: true, minAgeDays: 2 },
punishment: {
warnAsEmbed: true,
timeout: { enabled: true, durationMs: 60_000, scale: "linear", maxDurationMs: 10 * 60_000 },
},
disabledDetectors: ["caps"],
channelOverrides: {
"MEMES_CHANNEL_ID": { images: { maxRepeats: 8 }, emojis: { maxEmojis: 30 } },
},
});
antispam.setChannelConfig(guildId, channelId, { flood: { maxMessages: 12 } });- Punishment cooldown — after a strike, Disguard waits
punishment.cooldownMs(8s by default) before warning / timeout / kick / ban again. Extra spam in that window is deleted quietly. This stops the “4 timeouts in one flood” you saw in 1.0.0. - Per-guild config —
setGuildConfig(guildId, patch),getGuildConfig,clearGuildConfig. - Custom detectors —
antispam.use({ type, inspect }). - Stats —
getStats()/resetStats(). - New detectors: dangerous files (
.exe,.bat, …), zalgo / stacked diacritics, newline walls. - Links in embeds — phishing checks also read embed title, description, fields and footer.
isCoolingDown(guildId, userId).
antispam.setGuildConfig("123456789", {
flood: { maxMessages: 3 },
links: { blockInvites: true },
});
antispam.use({
type: "flood",
inspect({ message, snapshot }) {
if (message.content.includes("raid-now")) {
return {
type: "flood",
severity: "critical",
userId: message.author.id,
guildId: message.guild.id,
channelId: snapshot.channelId,
messageId: snapshot.id,
reason: "Custom raid keyword",
details: {},
recommendedActions: ["delete", "timeout"],
timestamp: snapshot.timestamp,
};
}
return null;
},
});
console.log(antispam.getStats());- What's new in 1.6.0
- What's new in 1.5.0
- What's new in 1.4.0
- What's new in 1.3.0
- What's new in 1.2.0
- What's new in 1.1.0
- Features
- Requirements
- Install
- Quick start
- Intents and permissions
- How it works
- Presets
- Full configuration
- Callbacks
- API
- Recipes
- Local testing
- What to send when testing
- FAQ
- Development
- License
| Detector | What it catches |
|---|---|
| flood | Too many messages in a short sliding window |
| duplicate | Same (or very similar) text sent repeatedly |
| link | Shorteners, raw IPs, punycode, brand lookalikes (dlscord, steamcommunnity), phishing keyword + URL, custom blocklists |
| image | Repeated images, stickers, and embed media |
| mention | @everyone, @here, too many unique mentions |
| caps | Messages that are mostly uppercase |
| emoji | Too many emojis or stickers in one message |
| file | Dangerous attachments (.exe, .bat, .dll, .msi, …) |
| zalgo | Obfuscated / zalgo text (stacked combining marks) |
| newline | Message walls made of empty lines |
| account | Brand-new Discord accounts / default avatar / new guild members (off by default) |
| length | Over-long messages (off by default) |
| word | Custom blocked words and regex (off until you fill list / regex) |
| hop | Same user jumping across too many channels |
| punctuation | Long runs of the same character (!!!!, aaaa) |
| spoiler | Spoiler-tag walls |
| ghost | Deleted messages that mentioned users (off by default) |
| secret | Accidental Discord bot tokens / webhook URLs in chat |
| echo | Same text pasted in several channels |
| invisible | Zero-width / bidi obfuscation |
| attach | Attachment floods over a sliding window |
| reply | Too many replies in a short window |
| blank | Empty / whitespace-only messages |
| embed | Too many embeds in one message |
| raid | Many distinct users posting in one channel (off by default) |
| forward | Too many Discord forwarded messages in a window |
| sticker | Sticker dumps / optional sticker-only messages |
| impersonate | Nick/username too close to the bot or protected names |
| join | Mass joins in a short window (off by default) |
| voice | Too many voice messages in a window |
| poll | Too many polls in a window |
Also included:
- Three presets:
lenient,balanced,strict - Ignore lists for users, roles, channels, categories, guilds, and command prefixes
- Per-guild, per-channel, and per-role overlays
- Strike system with decay
- Optional delete / warn / timeout / kick / ban / addRole / removeRole / slowmode (kick and ban are off by default)
dryRunto tune rules without punishing anyone- Edit scanning (links, mentions, and words)
- Zero extra runtime dependencies (peer:
discord.js) - In-memory store only — no database required
- Full TypeScript types
- Node.js 18+
- discord.js ^14
- Privileged intents: Message Content Intent (and Server Members Intent if you use timeouts)
npm install @amatiscorp/disguard discord.jsconst { Client, GatewayIntentBits, Partials } = require("discord.js");
const { AntiSpam } = require("@amatiscorp/disguard");
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
],
partials: [Partials.Message],
});
const antispam = new AntiSpam(client, {
preset: "balanced",
ignored: {
roles: ["STAFF_ROLE_ID"],
channels: ["BOT_COMMANDS_CHANNEL_ID"],
},
punishment: {
deleteMessage: true,
warnUser: true,
timeout: { enabled: true, durationMs: 60_000, minStrikes: 2 },
logChannelId: "MOD_LOG_CHANNEL_ID",
},
});
client.once("ready", () => {
antispam.start();
console.log(`Ready as ${client.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);
process.on("SIGINT", () => {
antispam.stop();
client.destroy();
});import { Client, GatewayIntentBits, Partials } from "discord.js";
import { AntiSpam } from "@amatiscorp/disguard";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
],
partials: [Partials.Message],
});
const antispam = new AntiSpam(client, {
preset: "strict",
dryRun: false,
onDetect(incident, message) {
console.log(incident.type, incident.reason, message.id);
},
});
client.once("ready", () => antispam.start());
await client.login(process.env.DISCORD_TOKEN);You can also use the factory:
const { createAntiSpam } = require("@amatiscorp/disguard");
const antispam = createAntiSpam(client, { preset: "balanced" });A full runnable example lives in examples/basic.js. From this repo:
copy .env.example .env
# put your bot token in .env
npm run dev| Goal | Intent / permission |
|---|---|
| Read message text | MessageContent + GuildMessages |
| Timeouts | GuildMembers + Moderate Members |
| Delete messages | Manage Messages |
| Warn in the channel | Send Messages |
| Log embeds | Embed Links |
| Kick / ban (opt-in) | Kick Members / Ban Members |
| Mute / add / remove roles (opt-in) | Manage Roles |
| Slowmode on raid/join (opt-in) | Manage Channels |
| Join-raid detector | Server Members Intent (guildMemberAdd) |
The bot will not sanction the guild owner or anyone above it in the role hierarchy. Those actions are skipped and reported in onAction.
Enable Message Content Intent in the Discord Developer Portal → your app → Bot → Privileged Gateway Intents.
- Ignores bots, webhooks, system messages, the owner, administrators, command prefixes, and anything in your ignore lists.
- Keeps a short in-memory history per user and guild (no database).
- Runs detectors in this order: files → secrets → words → impersonate → flood → hop → echo → duplicates → attachments → raid → replies → blank → embeds → forwards → voice → polls → stickers → links → images → mentions → zalgo → newlines → punctuation → spoilers → invisible → accounts → length → caps → emojis. The first match wins.
- Adds one strike (with optional decay) and applies the configured punishment.
- Message edits re-check links, mentions, words, secrets, and invisible.
- Deletes can trigger ghost pings if
ghostPing.enabledis true. - Joins can trigger join raids if
joins.enabledis true (guildMemberAdd).
Call antispam.stop() on shutdown, hot reload, or plugin unload.
Pass preset and then override only what you care about.
| Preset | When to use |
|---|---|
lenient |
Busy community. Higher limits, no automatic timeout, shorteners allowed. |
balanced |
Default. Reasonable coverage with few false positives. |
strict |
Small servers or raid-prone ones. Blocks invites, lower thresholds, faster timeouts. |
new AntiSpam(client, {
preset: "strict",
flood: { maxMessages: 4 }, // overrides only this field
});| Setting | lenient |
balanced |
strict |
|---|---|---|---|
| Flood | 8 / 4s | 5 / 4s | 3 / 4s |
| Duplicate repeats | 4 | 3 | 2 |
| Duplicate similarity | 0.95 | 0.90 | 0.85 |
| Image repeats | 4 | 3 | 2 |
| Mentions | 10 | 6 | 3 |
| Block invites | no | no | yes |
| Block shorteners | no | yes | yes |
| Channel hop | off | 5 / 8s | 3 / 6s |
| Ghost pings | off | off | on |
| Join raids | off | off | on |
| Auto timeout | off | 60s from 2 strikes | 5 min from 1 strike |
Every option is optional. Missing fields fall back to the preset (or balanced if you omit preset).
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Master switch. |
dryRun |
boolean |
false |
Detect and fire callbacks without deleting or punishing. Use this to tune. |
ignoreBots |
boolean |
true |
Skip other bots. |
ignoreWebhooks |
boolean |
true |
Skip webhooks. |
ignoreOwner |
boolean |
true |
Skip the guild owner. |
ignoreAdministrators |
boolean |
true |
Skip members with Administrator. |
checkEdits |
boolean |
true |
Re-scan edits for links, mentions, and words. |
checkDeletes |
boolean |
true |
Listen for messageDelete (ghost pings). |
checkJoins |
boolean |
true |
Listen for guildMemberAdd (join raids). |
ignoreThreads |
boolean |
false |
Skip messages in threads. |
ignoreSystem |
boolean |
true |
Skip Discord system messages. |
ignoreBoosters |
boolean |
false |
Skip server boosters. |
cleanupIntervalMs |
number |
60000 |
How often the memory store is pruned. |
locale |
"en" | "es" |
"en" |
Language for warnings and log embeds. |
ignorePermissions |
string[] |
[] |
Permission names that bypass checks. |
disabledDetectors |
DetectorType[] |
[] |
Detectors to skip. |
detectorOrder |
DetectorType[] |
[] |
Custom order. Empty = default. |
channelOverrides |
object |
{} |
Per-channel patches keyed by channel id. |
roleOverrides |
object |
{} |
Per-role patches keyed by role id. |
ignored: {
users: ["123"],
roles: ["456"],
channels: ["789"],
categories: ["101"],
guilds: ["202"],
prefixes: ["!", "/", "."],
}IDs are snowflakes as strings. A staff role in ignored.roles bypasses every detector. prefixes skip command messages (!help is ignored if "!" is listed).
flood: {
enabled: true,
maxMessages: 5, // the 5th message inside the window triggers
windowMs: 4000,
severity: "medium",
}Uses a sliding window, not a fixed clock. Five different messages in 4 seconds count as flood.
Text is normalized first: lowercase, markdown stripped, URLs removed, whitespace collapsed.
similarity is 0–1. 1 means exact match only. 0.9 also catches hello!!! vs hello!.
duplicates: {
enabled: true,
maxRepeats: 3,
windowMs: 12_000,
similarity: 0.9,
severity: "medium",
}links: {
enabled: true,
blockInvites: false,
blockShorteners: true,
blockIpLinks: true,
blockPunycode: true,
blockBrandLookalikes: true,
detectPhishingKeywords: true,
allowList: ["youtube.com", "github.com"],
blockList: ["bad-domain.test"],
suspiciousTlds: [], // e.g. ["tk", "gq"]
customPatterns: [], // regex against the whole message
extraPhishingKeywords: ["fake giveaway"],
maxLinks: 0,
scanEmbeds: true,
blockOauth: true,
severity: "high",
}allowList wins over heuristics. Official Discord, YouTube, GitHub, Spotify, and a few others are already allowed.
Built-in checks (each can be turned off):
- URL shorteners (
bit.ly,t.co,tinyurl.com, …) - Literal IPs (
http://1.2.3.4) - Punycode / homographs (
xn--...) - Brand clones of Discord, Steam, GitHub, PayPal, Roblox, and others
- Scam phrases plus any link (
free nitro,steam gift,verifica tu cuenta, …) - Discord invite links if
blockInvitesistrue - Markdown-masked links:
[click](https://evil.test) - Login / OAuth-looking paths on non-allowlisted hosts if
blockOauthistrue
// Block foreign server invites
links: { blockInvites: true }
// Allow one shortener you actually use
links: {
blockShorteners: true,
allowList: ["youtube.com", "youtu.be", "bit.ly"],
}
// Extra regex + extra scam words
links: {
customPatterns: ["steamcommunity\\.ru"],
extraPhishingKeywords: ["wallet drain", "airdrop now"],
}images: {
enabled: true,
maxRepeats: 3,
windowMs: 20_000,
hashMode: "meta", // or "content"
maxDownloadBytes: 2_097_152,
includeStickers: true,
includeEmbeds: true,
crossUserThreshold: 0, // e.g. 4 = same image from 4 different users
severity: "medium",
}meta(recommended): hash of size + MIME type + filename. No download.content: downloads the file and SHA-256s it. Better when the same bytes are re-uploaded under another name. Slower and uses bandwidth.
crossUserThreshold: 4 is useful against copypasta / raid image floods.
mentions: {
enabled: true,
maxMentions: 6,
blockEveryone: true,
blockHere: true,
severity: "high",
}
caps: {
enabled: true,
minLength: 16, // ignore short shouts
maxPercent: 75, // 0–100
severity: "low",
}
emojis: {
enabled: true,
maxEmojis: 12,
maxStickers: 3,
severity: "low",
}accounts: {
enabled: false, // turn on to flag new users
minAgeDays: 3,
blockDefaultAvatar: false,
minGuildAgeDays: 0, // 0 = off; e.g. 1 = joined this guild today
severity: "medium",
}
length: {
enabled: false,
maxCharacters: 1000,
severity: "low",
}files: {
enabled: true,
blockedExtensions: ["exe", "bat", "cmd", "com", "scr", "dll", "msi", "vbs", "ps1", "jar", "apk"],
maxBytes: 0, // 0 = unlimited
blockDoubleExtensions: true, // virus.exe.png
severity: "critical",
}
zalgo: {
enabled: true,
maxCombining: 20,
severity: "medium",
}
newlines: {
enabled: true,
maxNewlines: 15,
severity: "low",
}words: {
enabled: false, // stays off until you add a list or regex
list: ["raid-now"],
regex: ["n[i1]tro\\s+free"],
ignoreCase: true,
matchWholeWord: true,
scanEmbeds: true,
normalizeHomoglyphs: true, // rаid-now (Cyrillic a) still matches
severity: "high",
}
hop: {
enabled: true,
maxChannels: 5, // 5th unique channel in the window triggers
windowMs: 8_000,
severity: "high",
}
punctuation: {
enabled: true,
maxRepeated: 10, // 11+ of the same character in a row
severity: "low",
}
spoilers: {
enabled: true,
maxSpoilers: 8,
severity: "low",
}lenient turns hop and punctuation off. strict tightens hop to 3 channels / 6s.
Off in balanced / lenient. On in strict. Needs checkDeletes: true (default) and the deleted message in cache (Partials.Message in your Client).
ghostPing: {
enabled: false,
minMentions: 1,
maxAgeMs: 15_000, // 0 = any age
severity: "high",
}
checkDeletes: trueDisable with ghostPing: { enabled: false } or disabledDetectors: ["ghost"].
forwards: {
enabled: true,
maxForwards: 4, // 4th forward in the window triggers
windowMs: 10_000,
severity: "medium",
}
stickers: {
enabled: true,
maxStickers: 3, // per message
maxInWindow: 8, // 0 = off
windowMs: 10_000,
blockOnly: false, // true = sticker-only messages (no text)
severity: "low",
}
voice: {
enabled: true,
maxVoice: 3,
windowMs: 15_000,
severity: "medium",
}
polls: {
enabled: true,
maxPolls: 2,
windowMs: 20_000,
severity: "medium",
}lenient turns forwards, voice, and polls off. strict tightens the windows and sets stickers.blockOnly: true.
Compares nick / username / display name to the bot (matchBot) and any extra names. Text is folded (homoglyphs stripped, alphanumeric only) before similarity.
impersonate: {
enabled: true,
matchBot: true,
names: ["Admin", "Moderador", "Staff"],
minSimilarity: 0.84,
checkNickname: true,
checkUsername: true,
checkDisplayName: true,
severity: "high",
}Skip the bot itself. Short names under 3 letters after folding are ignored.
Off in balanced / lenient. On in strict. Needs checkJoins: true (default) and Server Members Intent. There is no message to delete: Disguard timeouts/kicks the newest joiner if those punishments are enabled.
joins: {
enabled: false,
maxJoins: 8, // 8th distinct join in the window triggers
windowMs: 10_000,
minAccountAgeDays: 0, // e.g. 1 = only count accounts younger than 1 day
severity: "critical",
}
checkJoins: trueonJoin(incident) {
// no Message — use incident.userId / guildId
}Kick and ban stay disabled on purpose. Turn them on only if you really want that.
punishment: {
deleteMessage: true,
warnUser: true,
dmUser: false,
warnMessage: "{user}, your message was blocked: {reason}. Strikes: {strikes}.",
timeout: { enabled: true, durationMs: 60_000, minStrikes: 2 },
kick: { enabled: false, minStrikes: 5 },
ban: { enabled: false, minStrikes: 8 },
escalate: true,
logChannelId: "CHANNEL_ID_OR_NULL",
strikeDecayMs: 15 * 60_000, // strikes expire after 15 minutes
cooldownMs: 8_000, // no second timeout/warn during this window
deleteDuringCooldown: true, // still delete extra spam quietly
warnAsEmbed: false,
addRoleIds: [], // e.g. ["MUTE_ROLE_ID"] — needs Manage Roles
removeRoleIds: [],
minStrikesForRoles: 1,
slowmodeSeconds: 0, // e.g. 10 — on raid/join, needs Manage Channels
}
// timeout.scale: "none" | "linear" (base * strikes) | "exponential" (base * 2^n)
// timeout.maxDurationMs caps the result (Discord max = 28 days)warnMessage placeholders: {user} {reason} {type} {strikes}.
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. slowmodeSeconds only runs on raid and join hits; it stays until a moderator changes it.
If the bot lacks a permission, or the target is higher in the hierarchy, that action is skipped and listed in result.skipped.
new AntiSpam(client, {
dryRun: true,
onDetect(incident, message) {
// Fired after a detector matches, before punishment.
},
onAction(result) {
// applied / skipped / dryRun / error
},
onError(error, context) {
console.error("[disguard]", context, error);
},
onCooldown(message) {
// Extra spam during punishment.cooldownMs (already deleted if deleteDuringCooldown).
},
onJoin(incident) {
// Join raid. No message object.
},
});{
type: "flood" | "duplicate" | "link" | "image" | "mention" | "caps" | "emoji"
| "file" | "zalgo" | "newline" | "account" | "length"
| "word" | "hop" | "punctuation" | "spoiler" | "ghost"
| "secret" | "echo" | "invisible" | "attach" | "reply" | "blank" | "embed" | "raid"
| "forward" | "sticker" | "impersonate" | "join" | "voice" | "poll",
severity: "low" | "medium" | "high" | "critical",
userId: string,
guildId: string,
channelId: string,
messageId: string,
reason: string,
details: Record<string, unknown>,
recommendedActions: Array<"delete" | "warn" | "timeout" | "kick" | "ban" | "addRole" | "removeRole" | "purge" | "slowmode">,
timestamp: number,
}{
incident: Incident,
dryRun: boolean,
applied: ActionType[],
skipped: Array<{ action: ActionType; reason: string }>,
error?: string,
}import { AntiSpam, createAntiSpam, resolveConfig, DEFAULT_CONFIG } from "@amatiscorp/disguard";
const antispam = new AntiSpam(client, options);
// same as createAntiSpam(client, options)
antispam.start();
antispam.stop();
antispam.pause();
antispam.resume();
antispam.isPaused();
antispam.setEnabled(false);
antispam.isEnabled();
const config = antispam.getConfig();
antispam.setConfig({ flood: { maxMessages: 8 } }); // deep merge, other keys stay
antispam.getStrikes(guildId, userId);
antispam.resetUser(guildId, userId);
antispam.isCoolingDown(guildId, userId);
antispam.setGuildConfig(guildId, { flood: { maxMessages: 3 } });
antispam.getGuildConfig(guildId);
antispam.clearGuildConfig(guildId);
antispam.setChannelConfig(guildId, channelId, { images: { maxRepeats: 8 } });
antispam.getChannelConfig(guildId, channelId);
antispam.clearChannelConfig(guildId, channelId);
antispam.setRoleConfig(roleId, { flood: { maxMessages: 12 } });
antispam.setUserConfig(userId, { flood: { maxMessages: 2 } });
antispam.clearUserConfig(userId);
antispam.clearRoleConfig(roleId);
antispam.use(customDetector);
antispam.getStats();
antispam.resetStats();
antispam.getRecentIncidents(20);
antispam.setDetectorEnabled("poll", false);
// Analyze only — no punishment, no callbacks
const incident = await antispam.analyze(message);
const edited = await antispam.analyze(message, { isEdit: true });
antispam.shouldIgnore(message); // booleanHelpers you can import for your own tools:
import {
extractUrls,
normalizeText,
foldHomoglyphs,
similarity,
isForwardedMessage,
DEFAULT_PHISHING_KEYWORDS,
DEFAULT_SHORTENERS,
OFFICIAL_BRANDS,
resolveConfig,
DEFAULT_CONFIG,
} from "@amatiscorp/disguard";
const urls = extractUrls("see [x](https://evil.test) and discord.gg/abc");
const near = similarity("hello world", "hello world!");
const config = resolveConfig("strict", { flood: { maxMessages: 2 } });Detect only — you handle sanctions
const antispam = new AntiSpam(client, {
punishment: {
deleteMessage: false,
warnUser: false,
timeout: { enabled: false, durationMs: 0, minStrikes: 99 },
},
onDetect(incident, message) {
// tickets, database, your own Automod pipeline...
},
});Tune without touching anyone
new AntiSpam(client, {
dryRun: true,
onAction(result) {
console.log(result.incident.type, result.applied);
},
});Per-guild rules at runtime
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === "antispam-strict") {
antispam.setConfig({ preset: undefined, flood: { maxMessages: 3 } });
await interaction.reply("Flood limit set to 3.");
}
});setConfig deep-merges. It does not re-apply a preset unless you build one with resolveConfig yourself:
const { resolveConfig } = require("@amatiscorp/disguard");
antispam.setConfig(resolveConfig("strict", { ignored: antispam.getConfig().ignored }));Real image hashing
images: { hashMode: "content", maxDownloadBytes: 1_000_000 }Mute role instead of timeout
punishment: {
timeout: { enabled: false, durationMs: 60_000, minStrikes: 99 },
addRoleIds: ["MUTE_ROLE_ID"],
minStrikesForRoles: 1,
}The bot role must sit above the mute role. Needs Manage Roles.
Reset a user after a false positive
antispam.resetUser(guildId, userId);This repository includes a demo bot.
- Create an application in the Developer Portal.
- Enable Message Content Intent and Server Members Intent.
- Invite the bot with Manage Messages + Moderate Members + Send Messages.
- Copy the env template and put the bot token (not a user token):
copy .env.example .envDISCORD_TOKEN=your_bot_token_here
- Run:
npm run devNode does not load .env by itself. The example reads it via examples/load-env.js.
Test with an account that is not the server owner and not an Administrator, or Disguard will ignore you.
Wait a few seconds between categories so flood does not eat the next test.
| Test | What to send |
|---|---|
| Flood | 5 messages in under 4 seconds |
| Duplicate | The same line 3 times, or hello then hello!!! |
| Shortener | https://bit.ly/abc123 |
| Raw IP | http://1.2.3.4/login |
| Brand clone | https://dlscord.com/nitro or https://steamcommunnity.com/gift |
| Phishing combo | Free Nitro https://totally-legit.gift/claim |
| Custom blocklist | https://malicioso.ejemplo/x (demo blockList) |
| Allowed | https://youtube.com and https://github.com should pass |
| Images | Same photo or sticker 3 times |
| Mentions | @everyone, @here, or 7 different users |
| Caps | THIS IS A MESSAGE IN ALL CAPS (16+ letters) |
| Emojis | 13+ emojis, or 4 stickers |
| File | Upload a dummy .exe or .bat |
| Zalgo | Text with many stacked combining marks |
| Newlines | A message with more than 15 line breaks |
| Channel hop | Same user posts in 5 different channels within 8 seconds |
| Punctuation | aaaaaaaaaaa or !!!!!!!!!!! |
| Spoilers | More than 8 ||spoiler|| pairs |
| Blocked word | Enable words first, then send a listed word |
| Ghost ping | Mention someone and delete the message within 15s (ghostPing.enabled: true) |
| Secret | Paste a dummy webhook URL https://discord.com/api/webhooks/123/abc |
| Echo | Same long sentence in 3 channels within 12s |
| Invisible | A message padded with many zero-width spaces |
| Attachments | 8+ files in 10 seconds |
| Replies | Reply to someone 6 times in 8s |
| Blank | A message with only spaces |
| Embeds | A webhook-style dump with 7+ embeds |
| Raid | Enable raid and have 8 people post in the same channel in 6s |
| Forwards | Forward the same / any messages 4 times in 10s |
| Stickers | Send 4 stickers in one message |
| Impersonate | Set your nick to the bot's username (or a name in impersonate.names) |
| Voice | Send 3 voice messages in 15s |
| Polls | Post 2 polls in 20s |
| Double ext | Upload foto.exe.png |
| Homoglyph word | Enable words with raid-now, send rаid-now (Cyrillic а) |
| Joins | Enable joins and have 8 accounts join within 10s |
| Edit | Send hello, then edit it to Free Nitro https://bit.ly/test |
Watch the terminal: [detect] flood|duplicate|link|image|mention|caps|emoji|file|zalgo|newline|word|hop|punctuation|spoiler|ghost|secret|echo|invisible|attach|forward|sticker|impersonate|voice|poll|join.
The second strike applies a 1 minute timeout with the default example config. Strikes decay after 15 minutes, or call resetUser.
Why does nothing happen when I spam?
You are probably the guild owner or an Administrator. Those are ignored by default. Use a second account, or set ignoreOwner: false / ignoreAdministrators: false while testing.
TokenInvalid on npm run dev?
The token never reached Node, or it is a user token / revoked bot token. The example now loads .env automatically. Do not wrap the token in quotes. Do not prefix it with Bot .
Can I use this without deleting messages?
Yes. Set punishment.deleteMessage: false and handle onDetect yourself.
Does it work in DMs?
No. Guild messages only.
Is there a database?
No. History lives in memory and is dropped on restart.
Will it ban people by default?
No. Ban and kick are opt-in.
npm install
npm test
npm run build
npm run devMIT
Reenvíos, dumps de stickers, impersonación del bot/staff, raids de joins, mensajes de voz, encuestas y filtros de palabras/archivos más difíciles de esquivar. Kick/ban siguen apagados. joins va apagado en balanced.
| Opción | Default | Qué hace |
|---|---|---|
forwards |
on, 4 / 10s | Demasiados mensajes reenviados de Discord. |
stickers |
on, 3 por mensaje, 8 / 10s | Dump de stickers. blockOnly (off) caza mensajes solo-sticker. |
impersonate |
on, bot a 0.84 | Nick / username / display name demasiado parecido al bot (o a names). |
joins |
off (strict: on, 6 / 8s) |
Demasiadas cuentas distintas entrando al servidor. Hace falta guildMemberAdd. |
voice |
on, 3 / 15s | Demasiados mensajes de voz. |
polls |
on, 2 / 20s | Demasiadas encuestas. |
files.blockDoubleExtensions |
true |
Caza virus.exe.png. |
words.scanEmbeds |
true |
La lista de palabras también lee embeds. |
words.normalizeHomoglyphs |
true |
Pliega lookalikes cirílicos / griegos / fullwidth. |
ignoreBoosters |
false |
Ignora boosters. |
punishment.slowmodeSeconds |
0 |
En hits raid / join, pone slowmode (hace falta Manage Channels). |
getRecentIncidents() |
— | Últimos 50 hits en vivo (no analyze()). |
setDetectorEnabled(type, bool) |
— | Enciende/apaga un detector. |
onJoin |
— | Callback de raid de joins (sin mensaje). |
new AntiSpam(client, {
forwards: { maxForwards: 3 },
impersonate: { names: ["Admin", "Moderador"] },
joins: { enabled: true, maxJoins: 8 },
ignoreBoosters: true,
words: { enabled: true, list: ["raid-now"], normalizeHomoglyphs: true },
punishment: { slowmodeSeconds: 10 },
onJoin(incident) {
console.log("join raid", incident.details);
},
});
antispam.setDetectorEnabled("poll", false);Activa joins solo si quieres que un burst de cuentas nuevas dispare timeout al último en entrar.
Raids de canal, spam de replies, mensajes vacíos, paredes de embeds, allowlist de archivos y config por usuario. Kick/ban siguen apagados. raid va apagado en balanced.
| Opción | Default | Qué hace |
|---|---|---|
replies |
on, 6 / 8s | Demasiadas respuestas seguidas. |
blank |
on | Mensajes vacíos o solo espacios. |
embeds |
on, 6 | Demasiados embeds en un mensaje. |
raid |
off (strict: on) |
Demasiados usuarios distintos escribiendo en el mismo canal. |
files.allowedExtensions |
[] |
Si lo rellenas, solo esas extensiones pasan. |
emojis.maxInWindow |
0 (off) |
Tope de emojis+stickers entre varios mensajes. |
ignoreNsfw |
false |
Ignora canales NSFW. |
graceMessages |
0 |
Los primeros N mensajes solo miran file/secret/link/word. |
setUserConfig |
— | Overlay por usuario. |
new AntiSpam(client, {
raid: { enabled: true, maxUsers: 8 },
files: { allowedExtensions: ["png", "jpg", "webp"] },
graceMessages: 2,
ignoreNsfw: true,
});
antispam.setUserConfig("ID_USER", { flood: { maxMessages: 2 } });Escaneo de secretos, copypasta entre canales, unicode invisible, flood de adjuntos y logs por webhook. Kick/ban siguen apagados.
| Opción | Default | Qué hace |
|---|---|---|
secrets |
on | Caza tokens de bot y webhooks pegados en el chat. El incidente no guarda el secreto. |
echo |
on, 3 canales / 12s | El mismo texto en varios canales (raid). No es lo mismo que hop. |
invisible |
on, 8 | Caracteres de ancho cero / bidi para saltarse filtros. |
attach |
on, 8 / 10s | Demasiados adjuntos en una ventana, no solo .exe. |
duplicates.minLength |
8 |
ok / lol ya no cuentan como duplicado. |
mentions.maxInWindow |
0 (off) |
Tope de menciones entre varios mensajes. |
accounts.onlyWithLinks |
false (strict: true) |
Cuentas nuevas solo si el mensaje trae URL. |
punishment.purgeCount |
0 |
Tras un hit, borra N mensajes recientes de ese user en el canal. |
punishment.logWebhookUrl |
null |
Logs a un webhook de Discord. |
ignorePinned |
false |
Ignora pineados. |
ignoreOlderThanMs |
0 |
Ignora mensajes viejos (replay al reconectar). |
setEnabled(false) |
— | Apaga el filtro sin stop(). |
new AntiSpam(client, {
secrets: { enabled: true, extraPatterns: ["ghp_[A-Za-z0-9]{20,}"] },
echo: { maxChannels: 3 },
accounts: { enabled: true, onlyWithLinks: true },
punishment: { purgeCount: 5, logWebhookUrl: process.env.DISGUARD_LOG_WEBHOOK || null },
});Más detectores, overlays por rol, mute por rol y más ignores. Todo es opcional.
| Opción | Default | Qué hace |
|---|---|---|
words |
apagado, lista vacía | Palabras y/o regex bloqueados. ignoreCase y matchWholeWord en true. |
hop |
on, 5 canales / 8s | El mismo usuario escribe en demasiados canales (raid / hop). |
punctuation |
on, 10 | Rachas de aaaaaa / !!!!!! más largas que maxRepeated. |
spoilers |
on, 8 pares | Demasiados ||spoiler|| en un mensaje. |
ghostPing |
off (strict: on) |
Castiga borrar un mensaje que mencionaba gente. |
checkDeletes |
true |
Escucha messageDelete. |
ignored.prefixes |
[] |
Ignora mensajes que empiezan por !, /, .. |
ignoreThreads |
false |
Ignora hilos. |
ignoreSystem |
true |
Ignora mensajes de sistema. |
roleOverrides / setRoleConfig |
{} |
Config por rol (antes de la de canal). |
links.blockOauth |
true |
URLs de login/OAuth fuera de la allowList. discord.com sigue permitido. |
files.maxBytes |
0 |
Tamaño máximo de adjunto. 0 = sin límite. |
accounts.minGuildAgeDays |
0 |
Miembros demasiado nuevos en este servidor. 0 = off. |
punishment.addRoleIds / removeRoleIds |
[] |
Añadir/quitar roles al castigar (mute). Hace falta Manage Roles. |
onCooldown |
— | Callback cuando el cooldown borra spam extra en silencio. |
pause() / resume() |
— | Congela el antispam sin quitar listeners. |
const antispam = new AntiSpam(client, {
locale: "es",
ignored: { prefixes: ["!", "/", "."], roles: ["ID_STAFF"] },
words: { enabled: true, list: ["raid-now"] },
hop: { maxChannels: 4, windowMs: 6_000 },
ghostPing: { enabled: true, maxAgeMs: 15_000 },
punishment: { addRoleIds: ["ID_MUTE"], minStrikesForRoles: 2 },
});
antispam.setRoleConfig("ID_VIP", { flood: { maxMessages: 10 } });
antispam.pause();Kick y ban siguen apagados. Ghost ping necesita el mensaje en caché (Partials.Message).
Más opciones, reglas por canal y textos al usuario en inglés o español.
| Opción | Qué hace |
|---|---|
locale |
"en" o "es" para avisos y logs. Si warnMessage está vacío, usa la plantilla del idioma. |
ignorePermissions |
Ignora quien tenga esos permisos (ManageMessages, …). |
setChannelConfig |
Config de un canal (encima de guild + global). |
channelOverrides |
Lo mismo en la config estática. |
disabledDetectors |
Apaga detectores por nombre. |
detectorOrder |
Orden de ejecución. |
flood.sameChannelOnly / duplicates.sameChannelOnly |
Solo cuenta el canal actual. |
links.maxLinks / links.scanEmbeds |
Tope de URLs; escanear embeds. |
images.skipContentTypes / images.maxAttachments |
Ignorar gifs, limitar adjuntos. |
mentions.maxRepeatsOfSame |
El mismo ping repetido. |
accounts |
Cuentas nuevas (minAgeDays) o avatar por defecto. Apagado por defecto. |
length |
Mensajes kilométricos. Apagado por defecto. |
punishment.timeout.scale |
"none" | "linear" | "exponential". |
punishment.warnAsEmbed |
Aviso en embed. |
new AntiSpam(client, {
locale: "es",
ignorePermissions: ["ManageMessages"],
flood: { sameChannelOnly: true },
links: { maxLinks: 3 },
accounts: { enabled: true, minAgeDays: 2 },
punishment: {
warnAsEmbed: true,
timeout: { durationMs: 60_000, scale: "linear", maxDurationMs: 600_000 },
},
channelOverrides: {
"ID_CANAL_MEMES": { emojis: { maxEmojis: 30 } },
},
});- Cooldown de castigo — tras un strike espera
punishment.cooldownMs(8s por defecto) antes de volver a avisar o timeout. El spam extra de esa ventana se borra en silencio. Así no salen 4 timeouts en el mismo flood. - Config por servidor —
setGuildConfig,getGuildConfig,clearGuildConfig. - Detectores propios —
antispam.use({ type, inspect }). - Estadísticas —
getStats()/resetStats(). - Nuevos detectores: archivos peligrosos, zalgo, paredes de saltos de línea.
- Enlaces en embeds — también se revisan título, descripción, fields y footer.
isCoolingDown(guildId, userId).
- Novedades de 1.6.0
- Novedades de 1.5.0
- Novedades de 1.4.0
- Novedades de 1.3.0
- Novedades de 1.2.0
- Novedades de 1.1.0
- Qué es
- Requisitos
- Instalación
- Inicio rápido
- Intents y permisos
- Cómo funciona
- Presets
- Configuración completa
- Callbacks
- API
- Recetas
- Probar en local
- Qué enviar para testear
- Preguntas frecuentes
- Desarrollo
- Licencia
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.
Detecta flood, salto de canales, palabras bloqueadas, phishing / enlaces no deseados (también en embeds), imágenes repetidas, spam de menciones, ghost pings, reenvíos, impersonación, raids de joins, mayúsculas, emojis, archivos peligrosos, zalgo y paredes de líneas.
- Node.js 18+
- discord.js ^14
- Intent de contenido de mensaje (y miembros del servidor si usas timeouts)
npm install @amatiscorp/disguard discord.jsconst { Client, GatewayIntentBits, Partials } = require("discord.js");
const { AntiSpam } = require("@amatiscorp/disguard");
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
],
partials: [Partials.Message],
});
const antispam = new AntiSpam(client, {
preset: "balanced",
ignored: {
roles: ["ID_ROL_STAFF"],
channels: ["ID_CANAL_BOTS"],
},
punishment: {
deleteMessage: true,
warnUser: true,
timeout: { enabled: true, durationMs: 60_000, minStrikes: 2 },
logChannelId: "ID_CANAL_LOGS",
},
});
client.once("ready", () => {
antispam.start();
console.log(`Listo como ${client.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);import { AntiSpam } from "@amatiscorp/disguard";
const antispam = new AntiSpam(client, {
preset: "strict",
onDetect(incident) {
console.log(incident.type, incident.reason);
},
});
antispam.start();Ejemplo completo en examples/basic.js:
copy .env.example .env
npm run dev| Qué quieres | Intent / permiso |
|---|---|
| Leer el texto | MessageContent + GuildMessages |
| Timeouts | GuildMembers + Moderate Members |
| Borrar mensajes | Manage Messages |
| Avisar en el canal | Send Messages |
| Embed de logs | Embed Links |
| Kick / ban (opt-in) | Kick Members / Ban Members |
| Mute / add / remove roles (opt-in) | Manage Roles |
| Slowmode en raid/join (opt-in) | Manage Channels |
| Raid de joins | Server Members Intent (guildMemberAdd) |
No puede sancionar al dueño del servidor ni a quien esté por encima del bot en la jerarquía.
Activa Message Content Intent en el Portal de Discord → tu app → Bot.
- Ignora bots, webhooks, mensajes de sistema, el dueño, administradores, prefijos de comando y tus listas de ignore.
- Guarda en memoria un historial corto por usuario y servidor. Sin base de datos.
- Pasa el mensaje por: archivos → secretos → palabras → impersonate → flood → hop → echo → duplicados → adjuntos → raid → replies → vacíos → embeds → reenvíos → voz → encuestas → stickers → enlaces → imágenes → menciones → zalgo → líneas → puntuación → spoilers → invisibles → cuentas → longitud → caps → emojis. El primero que dispare gana.
- Suma un strike (con caducidad) y aplica el castigo configurado.
- Las ediciones revisan enlaces, menciones y palabras.
- Los borrados pueden disparar ghost ping si
ghostPing.enabledes true. - Los joins pueden disparar join si
joins.enabledes true.
Llama antispam.stop() al apagar el proceso.
| Preset | Uso |
|---|---|
lenient |
Comunidad activa. Más holgura, sin timeout automático. |
balanced |
Por defecto. Equilibrio entre cobertura y falsos positivos. |
strict |
Servidores pequeños o con raids. Bloquea invitaciones y aprieta umbrales. |
new AntiSpam(client, {
preset: "strict",
flood: { maxMessages: 4 }, // solo pisa esto
});Todo es opcional. Lo que no pongas usa el preset.
| Opción | Tipo | Default | Qué hace |
|---|---|---|---|
enabled |
boolean |
true |
Interruptor maestro. |
dryRun |
boolean |
false |
Detecta y dispara callbacks sin borrar ni sancionar. |
ignoreBots |
boolean |
true |
Ignora otros bots. |
ignoreWebhooks |
boolean |
true |
Ignora webhooks. |
ignoreOwner |
boolean |
true |
Ignora al dueño. |
ignoreAdministrators |
boolean |
true |
Ignora quien tenga Administrator. |
checkEdits |
boolean |
true |
Revisa ediciones (enlaces, menciones y palabras). |
checkDeletes |
boolean |
true |
Escucha messageDelete (ghost pings). |
checkJoins |
boolean |
true |
Escucha guildMemberAdd (raids de joins). |
ignoreThreads |
boolean |
false |
Ignora hilos. |
ignoreSystem |
boolean |
true |
Ignora mensajes de sistema. |
ignoreBoosters |
boolean |
false |
Ignora boosters. |
cleanupIntervalMs |
number |
60000 |
Limpieza de memoria. |
roleOverrides |
object |
{} |
Parches por rol. |
ignored: {
users: ["id"],
roles: ["id"],
channels: ["id"],
categories: ["id"],
guilds: ["id"],
prefixes: ["!", "/", "."],
}flood: {
enabled: true,
maxMessages: 5, // el 5º mensaje dentro de la ventana dispara
windowMs: 4000,
severity: "medium",
}El texto se normaliza (minúsculas, sin markdown, sin URLs). similarity de 0.9 caza hola!!! ≈ hola!. 1 exige igualdad exacta.
duplicates: {
enabled: true,
maxRepeats: 3,
windowMs: 12_000,
similarity: 0.9,
severity: "medium",
}links: {
enabled: true,
blockInvites: false,
blockShorteners: true,
blockIpLinks: true,
blockPunycode: true,
blockBrandLookalikes: true,
detectPhishingKeywords: true,
allowList: ["youtube.com", "github.com"],
blockList: ["dominio-malo.test"],
suspiciousTlds: [],
customPatterns: ["steamcommunity\\.ru"],
extraPhishingKeywords: ["sorteo falso"],
maxLinks: 0,
scanEmbeds: true,
blockOauth: true,
severity: "high",
}La allowList gana a las heurísticas. discord.com y youtube.com ya vienen permitidos.
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.
images: {
enabled: true,
maxRepeats: 3,
windowMs: 20_000,
hashMode: "meta", // o "content"
maxDownloadBytes: 2_097_152,
includeStickers: true,
includeEmbeds: true,
crossUserThreshold: 0, // p.ej. 4 = misma imagen por 4 usuarios
severity: "medium",
}meta(recomendado): tamaño + tipo + nombre. No descarga nada.content: descarga el archivo y hace SHA-256. Más preciso y más lento.
mentions: { enabled: true, maxMentions: 6, blockEveryone: true, blockHere: true, severity: "high" },
caps: { enabled: true, minLength: 16, maxPercent: 75, severity: "low" },
emojis: { enabled: true, maxEmojis: 12, maxStickers: 3, severity: "low" },files: { enabled: true, blockedExtensions: ["exe", "bat", "cmd", "dll", "msi"], maxBytes: 0, blockDoubleExtensions: true, severity: "critical" },
zalgo: { enabled: true, maxCombining: 20, severity: "medium" },
newlines: { enabled: true, maxNewlines: 15, severity: "low" },words: { enabled: false, list: ["raid-now"], regex: [], ignoreCase: true, matchWholeWord: true, scanEmbeds: true, normalizeHomoglyphs: true, severity: "high" },
hop: { enabled: true, maxChannels: 5, windowMs: 8_000, severity: "high" },
punctuation: { enabled: true, maxRepeated: 10, severity: "low" },
spoilers: { enabled: true, maxSpoilers: 8, severity: "low" },
ghostPing: { enabled: false, minMentions: 1, maxAgeMs: 15_000, severity: "high" },
forwards: { enabled: true, maxForwards: 4, windowMs: 10_000, severity: "medium" },
stickers: { enabled: true, maxStickers: 3, maxInWindow: 8, blockOnly: false, severity: "low" },
impersonate: { enabled: true, matchBot: true, names: ["Admin", "Moderador"], minSimilarity: 0.84, severity: "high" },
joins: { enabled: false, maxJoins: 8, windowMs: 10_000, severity: "critical" },
voice: { enabled: true, maxVoice: 3, windowMs: 15_000, severity: "medium" },
polls: { enabled: true, maxPolls: 2, windowMs: 20_000, severity: "medium" },lenient apaga hop, puntuación, reenvíos, voz y encuestas. strict enciende ghost ping, joins y aprieta umbrales.
Kick y ban van apagados. Actívalos solo si lo tienes claro.
punishment: {
deleteMessage: true,
warnUser: true,
dmUser: false,
warnMessage: "{user}, tu mensaje se ha bloqueado: {reason}. Strikes: {strikes}.",
timeout: { enabled: true, durationMs: 60_000, minStrikes: 2 },
kick: { enabled: false, minStrikes: 5 },
ban: { enabled: false, minStrikes: 8 },
escalate: true,
logChannelId: "ID_O_NULL",
strikeDecayMs: 15 * 60_000,
cooldownMs: 8_000,
deleteDuringCooldown: true,
addRoleIds: [],
removeRoleIds: [],
minStrikesForRoles: 1,
slowmodeSeconds: 0,
}Placeholders: {user} {reason} {type} {strikes}.
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.
new AntiSpam(client, {
dryRun: true,
onDetect(incident, message) {},
onAction(result) {},
onError(error, context) {
console.error(context, error);
},
onCooldown(message) {},
onJoin(incident) {},
});Incident y ActionResult tienen la misma forma que en la sección en inglés.
const antispam = new AntiSpam(client, options);
antispam.start();
antispam.stop();
antispam.pause();
antispam.resume();
antispam.setEnabled(false);
antispam.getConfig();
antispam.setConfig({ flood: { maxMessages: 8 } });
antispam.getStrikes(guildId, userId);
antispam.resetUser(guildId, userId);
antispam.setGuildConfig(guildId, { flood: { maxMessages: 3 } });
antispam.setChannelConfig(guildId, channelId, { flood: { maxMessages: 12 } });
antispam.setRoleConfig(roleId, { emojis: { maxEmojis: 40 } });
antispam.setUserConfig(userId, { flood: { maxMessages: 2 } });
antispam.getStats();
antispam.getRecentIncidents(10);
antispam.setDetectorEnabled("poll", false);
const incident = await antispam.analyze(message);
const editado = await antispam.analyze(message, { isEdit: true });También: createAntiSpam, resolveConfig, DEFAULT_CONFIG, extractUrls, normalizeText, similarity.
Solo detectar
new AntiSpam(client, {
punishment: {
deleteMessage: false,
warnUser: false,
timeout: { enabled: false, durationMs: 0, minStrikes: 99 },
},
onDetect(incident, message) {
// tu lógica
},
});Probar sin tocar a nadie: dryRun: true.
Hash real de imágenes: images: { hashMode: "content" }.
Quitar strikes: antispam.resetUser(guildId, userId).
Mute por rol: punishment: { addRoleIds: ["ID_MUTE"], timeout: { enabled: false, durationMs: 0, minStrikes: 99 } }. El rol del bot debe estar por encima del mute.
Cambiar a strict en caliente
const { resolveConfig } = require("@amatiscorp/disguard");
antispam.setConfig(resolveConfig("strict", { ignored: antispam.getConfig().ignored }));- Crea el bot en el portal y activa Message Content + Server Members.
- Invítalo con Manage Messages, Moderate Members y Send Messages.
- Copia
.env.examplea.envy pon el token del bot. npm run dev.
Node no carga el .env solo; el ejemplo sí lo lee. Prueba con una cuenta que no sea owner ni admin.
Espera unos segundos entre categorías.
| Test | Qué mandar |
|---|---|
| Flood | 5 mensajes en menos de 4 segundos |
| Duplicado | La misma línea 3 veces, o hola y hola!!! |
| Acortador | https://bit.ly/abc123 |
| IP | http://1.2.3.4/login |
| Clon | https://dlscord.com/nitro |
| Phishing | Free Nitro https://totally-legit.gift/claim |
| Blocklist | https://malicioso.ejemplo/x |
| Permitidos | https://youtube.com y https://github.com no deben saltar |
| Imágenes | La misma foto o sticker 3 veces |
| Menciones | @everyone, @here, o 7 usuarios |
| Caps | ESTO ES UN MENSAJE TODO EN MAYUSCULAS |
| Emojis | Más de 12 emojis, o 4 stickers |
| Archivo | Sube un .exe o .bat (aunque sea de mentira) |
| Zalgo | Texto con muchos diacríticos apilados |
| Líneas | Un mensaje con más de 15 enters |
| Hop | El mismo usuario en 5 canales distintos en 8s |
| Puntuación | aaaaaaaaaaa o !!!!!!!!!!! |
| Spoilers | Más de 8 pares ||spoiler|| |
| Palabra | Activa words y manda una de la lista |
| Ghost ping | Menciona a alguien y borra el mensaje en menos de 15s (ghostPing.enabled: true) |
| Secreto | Pega un webhook de mentira https://discord.com/api/webhooks/123/abc |
| Echo | La misma frase larga en 3 canales en 12s |
| Invisible | Un mensaje con muchos espacios de ancho cero |
| Adjuntos | 8+ archivos en 10 segundos |
| Replies | Responder 6 veces en 8s |
| Vacío | Un mensaje solo con espacios |
| Embeds | 7+ embeds en un mensaje |
| Raid | Activa raid y que 8 personas escriban en el mismo canal |
| Reenvíos | Reenvía 4 mensajes en 10s |
| Stickers | 4 stickers en un mensaje |
| Impersonar | Pon tu nick igual al del bot (o un nombre de impersonate.names) |
| Voz | 3 mensajes de voz en 15s |
| Encuestas | 2 polls en 20s |
| Doble ext | Sube foto.exe.png |
| Homoglyph | Activa words con raid-now y manda rаid-now (а cirílica) |
| Joins | Activa joins y que 8 cuentas entren en 10s |
| Edición | Manda hola y edítalo a Free Nitro https://bit.ly/test |
En la terminal: [detect] flood|duplicate|link|image|mention|caps|emoji|file|zalgo|newline|word|hop|punctuation|spoiler|ghost|secret|echo|invisible|attach|forward|sticker|impersonate|voice|poll|join.
Al segundo strike el ejemplo mete timeout de 1 minuto. Los strikes caducan a los 15 minutos, o usa resetUser.
No pasa nada cuando spameo.
Seguramente eres el dueño o tienes Administrator. Usa otra cuenta, o pon ignoreOwner: false / ignoreAdministrators: false mientras pruebas.
TokenInvalid.
El .env no se estaba leyendo, o el token es de usuario / está revocado. El ejemplo ya carga el .env. Sin comillas y sin prefijo Bot .
¿Funciona en DMs?
No. Solo servidores.
¿Hay base de datos?
No. Todo vive en memoria y se pierde al reiniciar.
¿Banea por defecto?
No. Kick y ban son opt-in.
npm install
npm test
npm run build
npm run devMIT