{"_id":"discord-moderation","_rev":"61-6dca101d8614537b3591ccef782f5ffe","name":"discord-moderation","dist-tags":{"latest":"1.1.0"},"versions":{"0.1.0":{"name":"discord-moderation","version":"0.1.0","keywords":["discord","discord.js","moderation","discord bot","anti-alt","alt detection","alt account","raid protection","risk scoring"],"author":{"name":"aklid"},"license":"MIT","_id":"discord-moderation@0.1.0","maintainers":[{"name":"aklid","email":"ishandev2004@gmail.com"}],"homepage":"https://github.com/aklid01/discord-moderation#readme","bugs":{"url":"https://github.com/aklid01/discord-moderation/issues"},"dist":{"shasum":"85dc26f1e4f733955927ee9e04e46ede8e246173","tarball":"https://registry.npmjs.org/discord-moderation/-/discord-moderation-0.1.0.tgz","fileCount":6,"integrity":"sha512-Z8ZlV61ch7IkQhjjT3snCAJ3P+fOPv2INZ2z3FcZNtbX9XzjAwm7UzgNAXi/YuqcYoF29+err/4L8dyAXMECuw==","signatures":[{"sig":"MEUCIQCwjCTc6WEviw6YAyfMniyvFen3uuiwfDfSuXA14m0f7AIgL95l7j+0BCtVUop7A3GQy1nlQQEY1Maiks6U9djot2c=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":60666},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"scripts":{"build":"tsup"},"_npmUser":{"name":"aklid","email":"ishandev2004@gmail.com"},"repository":{"url":"git+https://github.com/aklid01/discord-moderation.git","type":"git"},"_npmVersion":"11.14.1","description":"A discord moderation suite for discord.js bots with features like Anti Alt.","directories":{},"_nodeVersion":"22.18.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^4.1.7","ts-node":"^10.9.2","discord.js":"^14.26.4","typescript":"^6.0.3"},"peerDependencies":{"discord.js":"^14.26.4"},"_npmOperationalInternal":{"tmp":"tmp/discord-moderation_0.1.0_1779479203728_0.42071779323059477","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."},"1.1.0":{"name":"discord-moderation","version":"1.1.0","keywords":["discord","discord.js","moderation","discord bot","anti-alt","alt detection","alt account","honeytrap","honeytrap detection","bait channel","scam detection","cumulative scoring","risk scoring"],"author":{"name":"aklid"},"license":"MIT","_id":"discord-moderation@1.1.0","maintainers":[{"name":"aklid","email":"ishandev2004@gmail.com"}],"homepage":"https://github.com/aklid01/discord-moderation#readme","bugs":{"url":"https://github.com/aklid01/discord-moderation/issues"},"dist":{"shasum":"c258a692f73e48cf73baa5c36925cb2414679a20","tarball":"https://registry.npmjs.org/discord-moderation/-/discord-moderation-1.1.0.tgz","fileCount":7,"integrity":"sha512-i2ex/GuyDjC31CAS+qpJHn7xadMO68tQLn9UHd9sQQZtd71VGVP4wohXW2QbQDV1vQMFYieWrmOd/lMjuCkDmQ==","signatures":[{"sig":"MEUCIQD2q8jMbZT/2i/HUGxd9UI976fvPzxYaEkpCDwE4tLbVgIgH26CdkyfOloMhyRP27MacnjK1hZHoMu7PghriNCrVfI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":91974},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"0192f0fbb2baf1ca969d96dc73fd205f8dea5277","scripts":{"test":"vitest run","build":"tsup","format":"prettier --write .","typecheck":"tsc --noEmit","test:watch":"vitest","format:check":"prettier --check ."},"_npmUser":{"name":"aklid","email":"ishandev2004@gmail.com"},"repository":{"url":"git+https://github.com/aklid01/discord-moderation.git","type":"git"},"_npmVersion":"11.17.0","description":"A modular discord moderation suite for discord.js bots with features like Anti Alt and Honeytrap.","directories":{},"_nodeVersion":"22.18.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^4.1.7","ts-node":"^10.9.2","prettier":"^3.9.5","discord.js":"^14.26.4","typescript":"^6.0.3"},"peerDependencies":{"discord.js":"^14.26.4"},"_npmOperationalInternal":{"tmp":"tmp/discord-moderation_1.1.0_1783701588808_0.8546605635493709","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."}},"time":{"created":"2026-05-22T19:46:43.661Z","modified":"2026-07-28T14:11:53.415Z","1.0.2":"2020-09-01T20:38:12.036Z","1.0.1":"2020-09-01T20:38:12.036Z","1.0.5":"2020-09-01T20:38:12.036Z","1.2.3":"2020-09-01T20:38:12.036Z","1.0.4":"2020-09-01T20:38:12.036Z","1.0.3":"2020-09-01T20:38:12.036Z","1.0.0":"2021-06-27T16:24:08.467Z","1.0.6":"2021-06-27T19:21:22.957Z","1.0.7":"2021-06-27T19:23:57.531Z","1.0.8":"2021-07-04T17:09:29.937Z","1.0.9":"2021-07-31T14:49:34.290Z","1.0.10":"2021-07-31T15:03:56.323Z","1.0.11":"2021-07-31T15:06:02.281Z","1.0.12":"2021-07-31T15:39:45.911Z","1.0.13":"2021-08-01T11:52:06.225Z","1.0.14":"2021-08-01T15:42:57.178Z","1.0.15":"2021-08-09T13:19:16.109Z","1.0.16":"2021-08-09T16:25:34.247Z","1.0.17":"2021-08-09T16:47:08.068Z","1.0.18":"2021-08-09T19:56:02.692Z","1.0.20":"2021-08-15T16:27:40.232Z","1.0.21":"2021-08-15T22:28:05.692Z","1.0.22":"2021-08-16T08:39:36.611Z","1.0.23":"2021-08-16T09:56:45.020Z","1.0.24":"2021-08-16T10:14:10.114Z","2.0.0":"2021-09-01T14:32:05.586Z","2.0.1":"2021-09-01T14:49:16.664Z","2.0.2":"2021-09-01T14:51:53.082Z","2.0.3":"2021-09-01T14:55:44.754Z","2.0.21":"2021-09-23T13:30:45.718Z","2.0.22":"2021-09-27T18:54:50.332Z","2.0.23":"2021-09-27T20:14:57.888Z","2.1.0":"2021-09-28T15:24:48.127Z","2.1.1":"2021-09-28T15:36:40.745Z","2.1.2":"2021-09-28T17:13:39.854Z","2.1.3":"2021-10-03T17:55:49.316Z","2.1.4":"2021-10-06T14:43:00.418Z","2.1.41":"2021-10-12T14:16:24.602Z","2.1.42":"2021-10-26T16:26:54.637Z","2.1.43":"2021-12-14T11:29:37.560Z","2.1.44":"2021-12-16T18:25:39.654Z","2.1.45":"2021-12-16T18:28:17.634Z","2.1.5":"2021-12-29T18:27:42.214Z","2.1.51":"2021-12-30T19:08:49.256Z","2.2.0":"2022-03-21T18:07:25.033Z","2.2.1":"2022-03-30T09:35:57.672Z","2.2.11":"2022-04-02T12:20:24.836Z","2.2.2":"2022-06-12T17:00:04.463Z","0.1.0":"2026-05-22T19:46:43.878Z","1.1.0":"2026-07-10T16:39:48.964Z"},"bugs":{"url":"https://github.com/aklid01/discord-moderation/issues"},"author":{"name":"aklid"},"license":"MIT","homepage":"https://github.com/aklid01/discord-moderation#readme","keywords":["discord","discord.js","moderation","discord bot","anti-alt","alt detection","alt account","honeytrap","honeytrap detection","bait channel","scam detection","cumulative scoring","risk scoring"],"repository":{"url":"git+https://github.com/aklid01/discord-moderation.git","type":"git"},"description":"A modular discord moderation suite for discord.js bots with features like Anti Alt and Honeytrap.","maintainers":[{"name":"aklid","email":"ishandev2004@gmail.com"}],"readme":"# discord-moderation\n\nA modular, **TypeScript-first** moderation toolkit for Discord.js v14 bots.\n\nIt ships two independent, composable clients built on a shared scoring engine:\n\n- **`AntiAltClient`** — a recommendation-only alt-account detector for new members.\n- **`HoneytrapClient`** — a message analyser for bait (\"honeytrap\") channels that deletes + gently reminds on accidental posts, and escalates on bot/scam posts.\n\n> **Core principle: analyse and recommend, never act unilaterally.** Neither client ever performs a Discord action on its own. `run()` / `analyse()` return a scored result — _you_ decide what to do with it.\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Two Clients, One Engine](#two-clients-one-engine)\n- [AntiAltClient](#antialtclient)\n  - [Quick Start](#quick-start)\n  - [How It Works](#how-it-works)\n  - [Configuration](#configuration)\n  - [Result Shape](#result-shape-antialtresult)\n  - [Built-in Detectors](#built-in-detectors)\n- [HoneytrapClient](#honeytrapclient)\n  - [Quick Start](#honeytrap-quick-start)\n  - [The Grace Tier](#the-grace-tier-delete--remind)\n  - [Configuration](#honeytrap-configuration)\n  - [Result Shape](#result-shape-honeytrapresult)\n  - [Message Detectors](#message-detectors)\n- [Shared Concepts](#shared-concepts)\n  - [Scoring Modes](#scoring-modes)\n  - [Event System](#event-system)\n  - [Embeds](#embeds)\n- [Full Bot Examples](#full-bot-examples)\n  - [TypeScript](#typescript-bot)\n  - [JavaScript](#javascript-bot)\n- [API Exports](#api-exports)\n- [Roadmap](#roadmap)\n- [License](#license)\n\n## Installation\n\n```bash\nnpm install discord-moderation\n# discord.js v14 is a required peer dependency\nnpm install discord.js\n```\n\n**Node.js >= 18** required (uses Unicode property escapes in regex).\n\n## Two Clients, One Engine\n\nBoth clients extend `BaseModerationClient`, which runs all detectors concurrently (`Promise.allSettled`), combines their scores, maps the result to a risk level, and emits typed events. A detector that throws returns a safe fallback (contributes nothing) instead of crashing the analysis.\n\n|               | **AntiAltClient**              | **HoneytrapClient**                         |\n| ------------- | ------------------------------ | ------------------------------------------- |\n| Analyses      | New members (`GuildMemberAdd`) | Messages in bait channels (`MessageCreate`) |\n| Input         | `GuildMember`                  | `HoneytrapInput` (`{ message, member }`)    |\n| Scoring mode  | `weighted-average`             | `cumulative` (signals stack)                |\n| Extra outputs | --                             | `shouldDeleteMessage`, `reminder`           |\n| Entry method  | `run(member)`                  | `analyse(message)` (gates on channel)       |\n\n---\n\n## AntiAltClient\n\nA recommendation-only alt/throwaway-account detector. It analyses an incoming member and returns a 0-100 risk score.\n\n### Quick Start\n\n```ts\nimport { Client, GatewayIntentBits, Events } from \"discord.js\";\nimport { AntiAltClient } from \"discord-moderation\";\n\nconst client = new Client({\n  intents: [\n    GatewayIntentBits.Guilds,\n    GatewayIntentBits.GuildMembers, // privileged - enable in Developer Portal\n  ],\n});\n\nconst antiAlt = new AntiAltClient();\n\nclient.on(Events.GuildMemberAdd, async (member) => {\n  const result = await antiAlt.run(member);\n  console.log(result.score); // 0-100\n  console.log(result.risk); // 'low' | 'medium' | 'high'\n  console.log(result.recommendedAction); // 'none' | 'warn' | 'kick' | 'softban' | 'ban'\n  console.log(result.shouldAct); // true when score >= shouldAct threshold\n\n  if (result.risk === \"high\") {\n    await member.kick(\"Potential alt account\");\n  }\n});\n\nclient.login(\"YOUR_TOKEN\");\n```\n\n### How It Works\n\n```\nGuildMemberAdd\n      |\n      v\n  antiAlt.run(member)\n      |\n      +-- UsernameDetector   --+\n      +-- AvatarDetector     --+-- concurrent Promise.allSettled()\n      +-- AccountAgeDetector --+\n                               |\n                        weighted average\n                               |\n                          0-100 score\n                               |\n              +----------------+----------------+\n            low             medium             high\n              |                |                 |\n        emit 'result'   emit 'result'    emit 'result'\n        emit 'lowRisk'  emit 'mediumRisk' emit 'highRisk'\n                               |\n                       return AntiAltResult\n```\n\n### Configuration\n\nAll config is optional -- pass nothing and safe defaults apply.\n\n```ts\nconst antiAlt = new AntiAltClient({\n  threshold: { high: 65, shouldAct: 60 },\n  actions: { high: \"ban\", medium: \"kick\" },\n  detectors: {\n    accountAge: { weight: 0.6 },\n    username: { enabled: false },\n  },\n});\n```\n\n**Thresholds** (inclusive lower bounds, 0-100):\n\n| Option                | Default | Description                                      |\n| --------------------- | ------- | ------------------------------------------------ |\n| `threshold.low`       | 20      | Minimum score for `'low'` risk                   |\n| `threshold.medium`    | 45      | Minimum score for `'medium'` risk                |\n| `threshold.high`      | 70      | Minimum score for `'high'` risk                  |\n| `threshold.shouldAct` | 65      | Score at which `result.shouldAct` becomes `true` |\n\n**Actions** (suggestions only -- map each risk level to a recommended action):\n\n| Risk   | Default  | Available Values                                   |\n| ------ | -------- | -------------------------------------------------- |\n| low    | `'none'` | `'none' \\| 'warn' \\| 'kick' \\| 'softban' \\| 'ban'` |\n| medium | `'warn'` | same                                               |\n| high   | `'kick'` | same                                               |\n\n**Detectors** (each accepts `enabled` and `weight` overrides):\n\n| Detector     | Default Weight | Description                                     |\n| ------------ | -------------- | ----------------------------------------------- |\n| `username`   | 0.3            | Scam words, alt terms, digit suffix, short name |\n| `avatar`     | 0.2            | No custom avatar                                |\n| `accountAge` | 0.5            | Tiered score based on account creation date     |\n\n> **Weight normalisation:** weights don't need to sum to 1. In `weighted-average` mode the scorer divides by the total weight of all enabled detectors automatically.\n\n### Result Shape: `AntiAltResult`\n\n```ts\ninterface AntiAltResult {\n  score: number; // 0-100 weighted risk score\n  risk: \"low\" | \"medium\" | \"high\";\n  shouldAct: boolean; // true when score >= threshold.shouldAct\n  recommendedAction: \"none\" | \"warn\" | \"kick\" | \"softban\" | \"ban\";\n  factors: DetectorResult[]; // per-detector breakdown\n  metadata: {\n    accountAge: number; // ms since account creation\n    username: string;\n    avatar: { default: boolean; hash?: string };\n    joinedAt: Date;\n    timestamp: Date; // when analysis ran\n  };\n}\n\ninterface DetectorResult {\n  detector: string; // 'username' | 'avatar' | 'accountAge'\n  enabled: boolean;\n  rawScore: number; // 0-100, this detector's own score\n  contribution: number; // weighted points added to the final score\n  reason: string; // human-readable explanation\n  details?: Record<string, unknown>;\n}\n```\n\nReading the breakdown:\n\n```ts\nconst result = await antiAlt.run(member);\nfor (const factor of result.factors) {\n  console.log(`${factor.detector}: ${factor.rawScore} -> ${factor.reason}`);\n}\n// username:   55 -> scam domain word detected: \"n1tr0\", long digit suffix\n// avatar:     50 -> no custom avatar set\n// accountAge: 80 -> account created 8 hours ago\n```\n\n### Built-in Detectors\n\n**Username Detector** -- default weight `0.3`. Checks the username against scam patterns and structural signals (leetspeak-aware, word-boundary protected).\n\n| Check                            | Score | Notes                                                                                         |\n| -------------------------------- | ----- | --------------------------------------------------------------------------------------------- |\n| High-confidence scam word        | +35   | nitro, vbucks, discord impersonation, onlyfans (e.g. `n1tr0`, `N!TR0`, `v-bucks`, `0nlyfans`) |\n| Moderate-confidence alt term     | +20   | alt, smurf, burner, account, scam, nsfw -- `my_alt` matches, `exalt_gamer` does not           |\n| Long digit suffix (4+ digits)    | +30   | e.g. `user8821` -- mass-registration pattern                                                  |\n| Very short username (<= 3 chars) | +15   | unusually short                                                                               |\n\n**Avatar Detector** -- default weight `0.2`.\n\n| Check                              | Score | Notes                  |\n| ---------------------------------- | ----- | ---------------------- |\n| No custom avatar (Discord default) | +50   | `user.avatar === null` |\n\n**Account Age Detector** -- default weight `0.5`.\n\n| Account Age | Score |\n| ----------- | ----- |\n| < 1 hour    | 100   |\n| < 1 day     | 80    |\n| < 7 days    | 60    |\n| < 30 days   | 40    |\n| < 90 days   | 20    |\n| >= 90 days  | 0     |\n\n---\n\n## HoneytrapClient\n\nAnalyses messages posted in **honeytrap channels** -- bait channels no real member should post in. The design is humane: an apparent misclick gets its message deleted plus a friendly reminder (no punishment); a corroborated bot/scam post escalates to kick or ban.\n\nUses **cumulative scoring** so evidence _stacks_ (a scam link + a new account add up toward certainty), rather than averaging toward the mean.\n\n### <a id=\"honeytrap-quick-start\"></a>Quick Start\n\n```ts\nimport { Client, GatewayIntentBits, Events } from \"discord.js\";\nimport { HoneytrapClient } from \"discord-moderation\";\n\nconst client = new Client({\n  intents: [\n    GatewayIntentBits.Guilds,\n    GatewayIntentBits.GuildMessages,\n    GatewayIntentBits.MessageContent, // privileged - required to read message content\n  ],\n});\n\nconst honeytrap = new HoneytrapClient({\n  honeytrapChannelIds: [\"123456789012345678\"], // your bait channel(s)\n});\n\nclient.on(Events.MessageCreate, async (message) => {\n  if (message.author.bot) return;\n\n  const result = await honeytrap.analyse(message);\n  if (!result) return; // not a honeytrap channel - nothing to do\n\n  if (result.shouldDeleteMessage && message.deletable) {\n    await message.delete().catch(() => {});\n  }\n  if (result.reminder) {\n    await message.author.send(result.reminder).catch(() => {}); // gentle path\n  }\n});\n\nclient.login(\"YOUR_TOKEN\");\n```\n\n### The Grace Tier (delete + remind)\n\nEvery message that reaches a honeytrap channel gets deleted (`shouldDeleteMessage` is always `true`). What differs is _how the author is treated_:\n\n| Situation                         | Score band           | `recommendedAction`              | `reminder`                  |\n| --------------------------------- | -------------------- | -------------------------------- | --------------------------- |\n| Clean message / apparent misclick | below `shouldAct`    | `'none'`                         | friendly reminder text      |\n| Corroborated bot / scam post      | at/above `shouldAct` | `kick` / `ban` (per actions map) | `null` (don't tip off bots) |\n\nThis guarantees **no message is ever deleted silently** -- a deletion is always paired with _either_ a member action _or_ a reminder.\n\n### <a id=\"honeytrap-configuration\"></a>Configuration\n\n```ts\nconst honeytrap = new HoneytrapClient({\n  honeytrapChannelIds: [\"123...\", \"456...\"], // REQUIRED, non-empty, valid snowflakes\n  threshold: { high: 60 }, // override cumulative thresholds\n  actions: { medium: \"softban\" }, // override default actions\n  reminderText: \"Please don't post here - your message was removed.\",\n  detectors: {\n    link: { weight: 0.5 },\n    username: { enabled: false },\n  },\n});\n```\n\n**Thresholds** (calibrated for the cumulative scale):\n\n| Option                | Default | Meaning                                                         |\n| --------------------- | ------- | --------------------------------------------------------------- |\n| `threshold.low`       | 15      | Minimum for `'low'` risk                                        |\n| `threshold.medium`    | 35      | Minimum for `'medium'` risk                                     |\n| `threshold.high`      | 55      | Minimum for `'high'` risk                                       |\n| `threshold.shouldAct` | 35      | At/above this, the author is actioned (below = gentle reminder) |\n\n**Actions** (deliberately harsher than AntiAlt -- a bait-channel post is already suspicious):\n\n| Risk   | Default  |\n| ------ | -------- |\n| low    | `'none'` |\n| medium | `'kick'` |\n| high   | `'ban'`  |\n\n**Detector weights** (cumulative scale -- content leads, account age corroborates):\n\n| Detector        | Default Weight |\n| --------------- | -------------- |\n| `link`          | 0.4            |\n| `promotionScam` | 0.4            |\n| `mentionSpam`   | 0.3            |\n| `accountAge`    | 0.25           |\n| `username`      | 0.2            |\n\n### Result Shape: `HoneytrapResult`\n\n```ts\ninterface HoneytrapResult {\n  score: number;\n  risk: \"low\" | \"medium\" | \"high\";\n  shouldAct: boolean;\n  recommendedAction: \"none\" | \"warn\" | \"kick\" | \"softban\" | \"ban\"; // MEMBER action\n  shouldDeleteMessage: boolean; // whether to remove the offending message\n  reminder: string | null; // friendly DM text on the gentle path; null when malicious\n  factors: DetectorResult[];\n  metadata: {\n    channelId: string;\n    isHoneytrapChannel: boolean;\n    authorId: string;\n    username: string;\n    accountAge: number; // ms since account creation\n    messageContentLength: number;\n    timestamp: Date;\n  };\n}\n```\n\n### Message Detectors\n\n**Link Detector** -- default weight `0.4`.\n\n| Check                          | Score |\n| ------------------------------ | ----- |\n| Contains a Discord invite link | +45   |\n| Contains an external link      | +30   |\n\n**Promotion / Scam Detector** -- default weight `0.4`. Phrases are tiered so a single soft phrase can't escalate a new user on its own (capped at 50).\n\n| Tier            | Score each | Examples                                                                |\n| --------------- | ---------- | ----------------------------------------------------------------------- |\n| High-confidence | +25        | `free nitro`, `steam gift`, `airdrop`, `onlyfans`, `buy/sell now/cheap` |\n| Soft            | +12        | `claim your`, `check my profile/bio/server`, `giveaway`, `crypto`       |\n\n**Mention Spam Detector** -- default weight `0.3`.\n\n| Check                   | Score |\n| ----------------------- | ----- |\n| `@everyone` / `@here`   | +40   |\n| >= 5 user/role mentions | +40   |\n| >= 3 user/role mentions | +20   |\n\n**Reused member detectors:** the honeytrap also runs the `AccountAgeDetector` and `UsernameDetector` (from AntiAlt) against the message author via an internal adapter. For webhook/system messages with no member, they contribute a neutral score.\n\n---\n\n## Shared Concepts\n\n### Scoring Modes\n\nThe engine supports two aggregation strategies (set per client, not user-configurable):\n\n- **`weighted-average`** (AntiAlt): `score = sum(raw_i * w_i) / sum(w_i)`. Treats detectors as co-equal opinions about one question (\"is this an alt?\").\n- **`cumulative`** (Honeytrap): `score = min(100, sum(raw_i * w_i))`. Treats detectors as independent pieces of evidence that stack -- ideal when multiple weak signals should add up.\n\nIn both modes, a detector that errors carries zero weight, so it never distorts the score.\n\n### Event System\n\nBoth clients extend Node's `EventEmitter` with fully typed `on` / `once` / `off` / `emit`:\n\n| Event                                 | Fires                                                          |\n| ------------------------------------- | -------------------------------------------------------------- |\n| `result`                              | every analysis, regardless of risk (ideal for audit logging)   |\n| `lowRisk` / `mediumRisk` / `highRisk` | exclusively -- exactly one per run                             |\n| `detectorError`                       | when a detector throws (non-fatal) -- `(detectorName, reason)` |\n\n```ts\n// AntiAlt events emit (member, result)\nantiAlt.on(\"result\", (member, result) => audit(member.id, result.score));\n\n// Honeytrap events emit (input, result) - the Message is input.message\nhoneytrap.on(\"highRisk\", (input, result) => {\n  console.log(`${input.message.author.tag} flagged: ${result.score}`);\n});\n\nhoneytrap.on(\"detectorError\", (name, reason) =>\n  console.error(`detector \"${name}\" failed:`, reason),\n);\n```\n\n> **Tip:** keep your gateway handler a thin trigger and let listeners react:\n\n```ts\nclient.on(Events.GuildMemberAdd, (member) => antiAlt.run(member));\nantiAlt.on(\"highRisk\", (member) => member.kick(\"alt\"));\n```\n\n### Embeds\n\nColour-coded (green / yellow / red) mod-log embeds with a per-detector breakdown:\n\n```ts\nimport { buildEmbed, buildHoneytrapEmbed } from \"discord-moderation\";\n\n// AntiAlt - note the (result, member) order\nantiAlt.on(\"result\", async (member, result) => {\n  await logChannel.send({ embeds: [buildEmbed(result, member)] });\n});\n\n// Honeytrap - takes only the result\nhoneytrap.on(\"highRisk\", async (input, result) => {\n  await logChannel.send({ embeds: [buildHoneytrapEmbed(result)] });\n});\n```\n\n---\n\n## Full Bot Examples\n\n### TypeScript Bot\n\n```ts\nimport { Client, GatewayIntentBits, Events } from \"discord.js\";\nimport {\n  AntiAltClient,\n  HoneytrapClient,\n  buildEmbed,\n  buildHoneytrapEmbed,\n} from \"discord-moderation\";\n\nconst LOG_CHANNEL_ID = \"LOG_CHANNEL_ID\";\n\nconst client = new Client({\n  intents: [\n    GatewayIntentBits.Guilds,\n    GatewayIntentBits.GuildMembers, // AntiAlt (privileged)\n    GatewayIntentBits.GuildMessages,\n    GatewayIntentBits.MessageContent, // Honeytrap (privileged)\n  ],\n});\n\nconst antiAlt = new AntiAltClient({\n  threshold: { high: 65, shouldAct: 60 },\n  actions: { high: \"ban\", medium: \"kick\" },\n});\n\nconst honeytrap = new HoneytrapClient({\n  honeytrapChannelIds: [\"123456789012345678\"],\n});\n\n// --- AntiAlt: analyse new members ---\nclient.on(Events.GuildMemberAdd, (member) => antiAlt.run(member));\n\nantiAlt.on(\"highRisk\", async (member, result) => {\n  const ch = client.channels.cache.get(LOG_CHANNEL_ID);\n  if (ch?.isTextBased()) {\n    await ch.send({\n      content: `High-risk join: <@${member.id}>`,\n      embeds: [buildEmbed(result, member)],\n    });\n  }\n  if (result.shouldAct)\n    await member.kick(\"Potential alt account\").catch(() => {});\n});\n\n// --- Honeytrap: analyse messages in bait channels ---\nclient.on(Events.MessageCreate, async (message) => {\n  if (message.author.bot) return;\n\n  const result = await honeytrap.analyse(message);\n  if (!result) return;\n\n  if (result.shouldDeleteMessage && message.deletable) {\n    await message.delete().catch(() => {});\n  }\n\n  if (result.reminder) {\n    await message.author.send(result.reminder).catch(() => {});\n    return; // gentle path - no punishment\n  }\n\n  const member = message.member;\n  if (result.shouldAct && member) {\n    switch (result.recommendedAction) {\n      case \"ban\":\n        await member\n          .ban({ reason: \"Honeytrap: bot/scam post\" })\n          .catch(() => {});\n        break;\n      case \"softban\":\n        await member\n          .ban({ deleteMessageSeconds: 86_400, reason: \"Honeytrap softban\" })\n          .then(() => member.guild.members.unban(member.id))\n          .catch(() => {});\n        break;\n      case \"kick\":\n        await member.kick(\"Honeytrap: suspicious post\").catch(() => {});\n        break;\n      case \"warn\":\n        await member\n          .send(\"Warning: that channel is not for posting.\")\n          .catch(() => {});\n        break;\n    }\n  }\n});\n\nhoneytrap.on(\"highRisk\", async (input, result) => {\n  const ch = client.channels.cache.get(LOG_CHANNEL_ID);\n  if (ch?.isTextBased())\n    await ch.send({ embeds: [buildHoneytrapEmbed(result)] });\n});\n\nclient.once(Events.ClientReady, (c) => console.log(`Online as ${c.user.tag}`));\nclient.login(process.env.DISCORD_TOKEN);\n```\n\n### JavaScript Bot\n\nSame behaviour in plain JavaScript (CommonJS `require`):\n\n```js\nconst { Client, GatewayIntentBits, Events } = require(\"discord.js\");\nconst {\n  AntiAltClient,\n  HoneytrapClient,\n  buildEmbed,\n  buildHoneytrapEmbed,\n} = require(\"discord-moderation\");\n\nconst LOG_CHANNEL_ID = \"LOG_CHANNEL_ID\";\n\nconst client = new Client({\n  intents: [\n    GatewayIntentBits.Guilds,\n    GatewayIntentBits.GuildMembers,\n    GatewayIntentBits.GuildMessages,\n    GatewayIntentBits.MessageContent,\n  ],\n});\n\nconst antiAlt = new AntiAltClient({\n  threshold: { high: 65, shouldAct: 60 },\n  actions: { high: \"ban\", medium: \"kick\" },\n});\n\nconst honeytrap = new HoneytrapClient({\n  honeytrapChannelIds: [\"123456789012345678\"],\n});\n\n// --- AntiAlt ---\nclient.on(Events.GuildMemberAdd, (member) => antiAlt.run(member));\n\nantiAlt.on(\"highRisk\", async (member, result) => {\n  const ch = client.channels.cache.get(LOG_CHANNEL_ID);\n  if (ch && ch.isTextBased()) {\n    await ch.send({\n      content: `High-risk join: <@${member.id}>`,\n      embeds: [buildEmbed(result, member)],\n    });\n  }\n  if (result.shouldAct)\n    await member.kick(\"Potential alt account\").catch(() => {});\n});\n\n// --- Honeytrap ---\nclient.on(Events.MessageCreate, async (message) => {\n  if (message.author.bot) return;\n\n  const result = await honeytrap.analyse(message);\n  if (!result) return;\n\n  if (result.shouldDeleteMessage && message.deletable) {\n    await message.delete().catch(() => {});\n  }\n\n  if (result.reminder) {\n    await message.author.send(result.reminder).catch(() => {});\n    return;\n  }\n\n  const member = message.member;\n  if (result.shouldAct && member) {\n    if (result.recommendedAction === \"ban\") {\n      await member.ban({ reason: \"Honeytrap: bot/scam post\" }).catch(() => {});\n    } else if (result.recommendedAction === \"kick\") {\n      await member.kick(\"Honeytrap: suspicious post\").catch(() => {});\n    }\n  }\n});\n\nhoneytrap.on(\"highRisk\", async (input, result) => {\n  const ch = client.channels.cache.get(LOG_CHANNEL_ID);\n  if (ch && ch.isTextBased())\n    await ch.send({ embeds: [buildHoneytrapEmbed(result)] });\n});\n\nclient.once(Events.ClientReady, (c) => console.log(`Online as ${c.user.tag}`));\nclient.login(process.env.DISCORD_TOKEN);\n```\n\n## API Exports\n\n```ts\n// Clients + base\nexport { AntiAltClient, HoneytrapClient, BaseModerationClient };\n\n// Embed / result builders\nexport { buildEmbed, buildResult, buildHoneytrapResult, buildHoneytrapEmbed };\n\n// Config validators\nexport { validateConfig, validateHoneytrapConfig };\n\n// Defaults\nexport { DEFAULT_THRESHOLDS, DEFAULT_ACTIONS };\nexport {\n  DEFAULT_HONEYTRAP_THRESHOLDS,\n  DEFAULT_HONEYTRAP_ACTIONS,\n  HONEYTRAP_DETECTOR_WEIGHTS,\n};\n\n// Types\nexport type {\n  AntiAltConfig,\n  AntiAltResult,\n  AntiAltClientEvents,\n  HoneytrapConfig,\n  HoneytrapInput,\n  HoneytrapResult,\n  HoneytrapClientEvents,\n  DetectorConfig,\n  DetectorResult,\n  ActionConfig,\n  ThresholdConfig,\n  RiskLevel,\n  RecommendedAction,\n  IDetector,\n};\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}