Skip to content
Amatis-CorpPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

11 Commits

Folders and files

Repository files navigation

Disguard

Configurable antispam for discord.js v14

Flood · phishing · forwards · impersonation · join raids · leaked secrets · TypeScript

npm version License: MIT 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


English

What's new in 1.6.0

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).

What's new in 1.5.0

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.

What's new in 1.4.0

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.

What's new in 1.3.0

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.

What's new in 1.2.0

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 } });

What's new in 1.1.0

  • 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());

Table of contents

Features

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)
  • dryRun to 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

Requirements

  • Node.js 18+
  • discord.js ^14
  • Privileged intents: Message Content Intent (and Server Members Intent if you use timeouts)

Install

npm install @amatiscorp/disguard discord.js

Quick start

CommonJS

const { 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();
});

TypeScript / ESM

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

Intents and permissions

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.

How it works

  1. Ignores bots, webhooks, system messages, the owner, administrators, command prefixes, and anything in your ignore lists.
  2. Keeps a short in-memory history per user and guild (no database).
  3. 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.
  4. Adds one strike (with optional decay) and applies the configured punishment.
  5. Message edits re-check links, mentions, words, secrets, and invisible.
  6. Deletes can trigger ghost pings if ghostPing.enabled is true.
  7. Joins can trigger join raids if joins.enabled is true (guildMemberAdd).

Call antispam.stop() on shutdown, hot reload, or plugin unload.

Presets

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

Full configuration

Every option is optional. Missing fields fall back to the preset (or balanced if you omit preset).

Global

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.

Ignore lists

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

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.

Duplicates

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 and phishing

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 blockInvites is true
  • Markdown-masked links: [click](https://evil.test)
  • Login / OAuth-looking paths on non-allowlisted hosts if blockOauth is true
// 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

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, caps, emojis

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 and length

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, zalgo, newlines

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, channel hop, punctuation, spoilers

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.

Ghost pings

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: true

Disable with ghostPing: { enabled: false } or disabledDetectors: ["ghost"].

Forwards, stickers, voice, polls

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.

Impersonation

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.

Join raids

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: true
onJoin(incident) {
  // no Message — use incident.userId / guildId
}

Punishment

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.

Callbacks

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.
  },
});

Incident

{
  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,
}

ActionResult

{
  incident: Incident,
  dryRun: boolean,
  applied: ActionType[],
  skipped: Array<{ action: ActionType; reason: string }>,
  error?: string,
}

API

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); // boolean

Helpers 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 } });

Recipes

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);

Local testing

This repository includes a demo bot.

  1. Create an application in the Developer Portal.
  2. Enable Message Content Intent and Server Members Intent.
  3. Invite the bot with Manage Messages + Moderate Members + Send Messages.
  4. Copy the env template and put the bot token (not a user token):
copy .env.example .env
DISCORD_TOKEN=your_bot_token_here
  1. Run:
npm run dev

Node 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.

What to send when testing

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.

FAQ

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.

Development

npm install
npm test
npm run build
npm run dev

License

MIT


Español

Novedades de 1.6.0

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.

Novedades de 1.5.0

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 } });

Novedades de 1.4.0

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 },
});

Novedades de 1.3.0

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).

Novedades de 1.2.0

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 } },
  },
});

Novedades de 1.1.0

  • 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).

Tabla de contenidos

Qué es

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.

Requisitos

  • Node.js 18+
  • discord.js ^14
  • Intent de contenido de mensaje (y miembros del servidor si usas timeouts)

Instalación

npm install @amatiscorp/disguard discord.js

Inicio rápido

CommonJS

const { 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);

TypeScript

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

Intents y permisos

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.

Cómo funciona

  1. Ignora bots, webhooks, mensajes de sistema, el dueño, administradores, prefijos de comando y tus listas de ignore.
  2. Guarda en memoria un historial corto por usuario y servidor. Sin base de datos.
  3. 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.
  4. Suma un strike (con caducidad) y aplica el castigo configurado.
  5. Las ediciones revisan enlaces, menciones y palabras.
  6. Los borrados pueden disparar ghost ping si ghostPing.enabled es true.
  7. Los joins pueden disparar join si joins.enabled es true.

Llama antispam.stop() al apagar el proceso.

Presets

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
});

Configuración completa

Todo es opcional. Lo que no pongas usa el preset.

Global

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.

Listas de ignore

ignored: {
  users: ["id"],
  roles: ["id"],
  channels: ["id"],
  categories: ["id"],
  guilds: ["id"],
  prefixes: ["!", "/", "."],
}

Flood

flood: {
  enabled: true,
  maxMessages: 5, // el 5º mensaje dentro de la ventana dispara
  windowMs: 4000,
  severity: "medium",
}

Duplicados

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",
}

Enlaces y phishing

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.

Imágenes

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.

Menciones, mayúsculas, emojis

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" },

Archivos, zalgo, saltos de línea

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" },

Palabras, hop, puntuación, spoilers, ghost ping

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.

Castigos

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.

Callbacks

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.

API

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.

Recetas

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 }));

Probar en local

  1. Crea el bot en el portal y activa Message Content + Server Members.
  2. Invítalo con Manage Messages, Moderate Members y Send Messages.
  3. Copia .env.example a .env y pon el token del bot.
  4. npm run dev.

Node no carga el .env solo; el ejemplo sí lo lee. Prueba con una cuenta que no sea owner ni admin.

Qué enviar para testear

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.

Preguntas frecuentes

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.

Desarrollo

npm install
npm test
npm run build
npm run dev

Licencia

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages