A Node.js Discord bot that uses OCR and image hashing to analyze attached images and images shown in Discord link previews. When an image contains a withdrawal keyword and a payout keyword, the bot:
- Deletes the message.
- Applies a timeout to the author.
- Sends a report to the moderation channel.
Invite the bot to your server:
The OCR result must contain both of these keyword groups:
- Withdrawal group:
Withdrawal. - Payout group:
Success,Succeeded,Successful,Successfully, orUSDTin uppercase.
The keywords:
- Are case-insensitive.
- Can appear in any order.
- Can appear on different lines or far apart in the image.
- Must appear as complete words.
You can tune the detection sensitivity per server with /setup paranoia:
low- exact visual hash match only.medium- visual hash match or OCR text containingWithdrawal,Succeeded, andUSDT.high- visual hash match or OCR text containingWithdrawaland eitherSucceededorUSDT.
The default paranoia level is high.
The bot includes an anti-raid protection that detects when a user sends the same message in multiple text channels within less than one minute. When a raid is detected, the bot:
- Deletes the repeated messages in the affected channels.
- Applies the configured timeout to the user.
- Sends an alert to the moderation channel, including the deleted message.
Anti-raid is enabled by default at the high level. Configure it per server
with /setup anti-raid, for example:
/setup anti-raid enabled:true level:high
Spam message protection is enabled by default. A matching spam message is
deleted and its author is timed out. Known spammer IDs can be added to
spam-users.json; messages from those users are handled the same way even if
their message does not match a configured spam phrase.
Available levels are:
high- triggers when the same message is sent in 3 or more channels.medium- triggers when the same message is sent in 4 or more channels.low- triggers only when the same message is sent in every text channel in the server.
The bot can also block text messages or embed descriptions that contain an
entry from the list in spam-messages.json. Matching ignores capitalization,
accents, and repeated spaces. Matching messages are deleted, the author is
timed out, and a moderation alert is sent when a moderation channel is
configured.
Spam protection is enabled by default. Server administrators can toggle it with:
/setup spam messages enabled:false
/setup spam messages enabled:true
Add or remove entries directly in the messages array in spam-messages.json,
one message or phrase per JSON array entry. Multi-line messages can be pasted
with their line breaks as-is; the bot accepts and normalizes those line breaks
when it loads the file. The descriptionPatterns entries are regular
expressions for promotional descriptions. They are ignored when a
descriptionExclusions expression detects a negation such as no NSFW or
NSFW not allowed. Its status is also shown by /setup status.
Use enabled:false to disable the protection. The anti-raid configuration is
stored separately for each server and is shown by /setup status.
The bot can resolve Discord invitation links and compare the destination server ID with a blocklist. This protection is enabled by default. When a message contains an invitation to a blocked server, the bot deletes the message, applies the configured timeout to the author, and sends a moderation alert. A new invite link to the same server is still blocked because matching uses the destination server ID, not the invite code.
The global blocklist is malicious-servers.json at the repository root. Add one Discord server ID per JSON array entry:
[
"123456789012345678",
"987654321098765432"
]The file is copied into the Docker image. Rebuild/redeploy the image after
changing it. The /setup malicious-servers add command can also add a local
entry for the current server without modifying the repository file.
Manage it with:
/setup malicious-servers protection enabled:true
/setup malicious-servers add server-id:123456789012345678
/setup malicious-servers remove server-id:123456789012345678
/setup malicious-servers list
The enabled/disabled state and local entries are stored separately for each
server and are shown in /setup status. Expired, deleted, or otherwise
unavailable invites cannot be resolved and are left untouched.
The bot can also inspect the name returned for an invitation's destination
server. It compares that name against the keywords in
nsfw-server-keywords.json, including terms such as
NSFW, +18, 18+, 🔞, squirt, sex, porn, xxx, adult, hentai,
and onlyfans. Matching is case-insensitive and ignores accents.
This protection is enabled by default and can be disabled per server with:
/setup nsfw-servers protection enabled:false
/setup nsfw-servers protection enabled:true
The keyword file is copied into the Docker image, so rebuild/redeploy the image after changing it. If the invitation has expired, is unavailable, or does not expose a server name, the NSFW-name check is skipped.
Server admins can also:
- Set a custom timeout with
/setup timeout. - Exclude roles from detection with
/setup excluded-role add,/setup excluded-role remove, and/setup excluded-role list. - Toggle whether server administrators are excluded with
/setup excluded-administrators enableand/setup excluded-administrators disable. - Enable, disable, or change the sensitivity of anti-raid protection with
/setup anti-raid.
For example, an image containing Withdrawal near the top and Succeeded
near the bottom is considered a match.
The bot scans:
- Images uploaded directly as Discord attachments.
- Images and thumbnails displayed in Discord embeds generated from links.
- Images and embeds contained in forwarded message snapshots.
- Every image in a multi-image message.
Each image is evaluated independently. If any single image contains a matching withdrawal keyword and payout keyword, the entire outer message is deleted and the user who sent or forwarded it is timed out when Discord allows it.
You can also add a public easter egg response by dropping meme images into
easter-egg photos/. The bot builds hash signatures from that folder and, when
one of those images is detected, replies in the channel with Jajaja, piqué.
in Spanish or Hahaha I got it 😜 in English instead of moderating the
message.
Discord may generate a link preview shortly after the original message is created. The bot handles both new-message and message-update events so those delayed previews are scanned as well. A plain link that Discord does not convert into an image embed is not downloaded automatically.
- Node.js 20 or newer.
When generating the bot invitation in the Discord Developer Portal, select:
bot— adds the bot account to the server.applications.commands— installs the/setupslash command. Discord includes this scope by default when thebotscope is selected, but it should remain enabled.
Grant the bot these permissions:
| Permission | Where it is required | Purpose |
|---|---|---|
| View Channels | Monitored channels and the moderation channel | Receives new messages and accesses the configured moderation channel. |
| Manage Messages | Monitored channels | Deletes messages containing a matching image or raids. |
| Moderate Members | Server-wide | Applies the configured timeout to the message author. |
| Send Messages | Moderation channel | Sends moderation alerts. |
| Embed Links | Moderation channel | Sends the formatted moderation report embed. |
The combined permission integer for these five permissions is
1099511655424. It can be entered in an OAuth2 bot invitation as the
permissions value.
The bot does not require Administrator, Ban Members, or Kick Members. Granting Administrator is not recommended.
The bot's highest role must be above the roles of users it needs to time out. Discord does not allow the bot to time out:
- The server owner.
- Members with the Administrator permission.
- Members whose highest role is equal to or above the bot's highest role.
If the bot cannot apply a timeout because of role hierarchy or permissions, it still attempts to delete the message and records the timeout failure in the moderation alert.
By default, messages from server administrators and the server owner are ignored
completely. Use /setup excluded-administrators disable to scan them too. When
administrator exclusion is disabled, Discord may still prevent the timeout, but
the bot can still delete matching messages when it has permission.
Members with an excluded role are also ignored completely. These exclusions apply to image moderation, anti-raid protection, and spam message protection.
The person running /setup moderation-channel, /setup paranoia, or
/setup status must have the Manage Server permission. The bot itself
does not need Manage Server.
In the Discord Developer Portal, open the application, go to Bot > Privileged Gateway Intents, and enable Message Content Intent. Discord considers attachments part of message content; without this intent, the bot receives an empty attachment collection.
pnpm install
cp .env.example .envSet DISCORD_TOKEN in .env.
pnpm startThe first OCR run may download the English language data and take longer. The worker is reused for subsequent images.
Server administrators with Manage Server can configure the bot with:
/setup moderation-channelto choose where alerts are sent./setup paranoiato set the per-server detection sensitivity./setup timeoutto set the per-server timeout./setup excluded-role ...to manage ignored roles./setup excluded-administrators ...to include or exclude server administrators./setup anti-raidto enable or configure anti-raid protection./setup statusto review the current configuration.
Store the meme images you want the bot to recognize in easter-egg photos/.
Run pnpm build:easter-egg-photos to regenerate
generated/easter-egg-photo-manifest.json.
The included image uses node:22-alpine, installs production dependencies
only, and runs the bot as the non-root node user.
Create the environment file before starting the container:
cp .env.example .envSet DISCORD_TOKEN in .env, then build and start the bot:
docker compose up -d --buildView its logs:
docker compose logs -f botStop the bot:
docker compose downThe Compose configuration creates two named volumes:
bot-datastores the per-server moderation channel configuration.ocr-cachestores the downloaded Tesseract English language data.
Both volumes survive container recreation and image upgrades. Running
docker compose down -v deletes them, including the saved moderation channel
configuration.
No ports need to be exposed or published. The bot connects outward to the Discord Gateway over HTTPS and WebSocket connections. It does not run an HTTP server or accept inbound network traffic.
The relevant Compose configuration intentionally contains no ports section:
services:
bot:
build: .
restart: unless-stopped
env_file:
- .env
volumes:
- bot-data:/app/data
- ocr-cache:/app/tessdataThe workflow in .github/workflows/deploy.yml runs on every push to main
and can also be started manually.
It performs these steps:
- Installs dependencies and runs the test suite.
- Builds the Docker image.
- Publishes
latestand commit-specific tags to GitHub Container Registry. - Optionally connects to a server over SSH, pulls the exact commit image, and restarts the bot with Docker Compose.
The published image name is:
ghcr.io/dh-555/spam-guard-bot
Install Docker Engine and the Docker Compose plugin on the destination server. Create the deployment directory and its environment file once:
sudo mkdir -p /opt/d5-spam-guard-bot
sudo chown "$USER":"$USER" /opt/d5-spam-guard-bot
cd /opt/d5-spam-guard-bot
nano .envThe server-side .env must contain at least:
DISCORD_TOKEN=your_real_bot_tokenGitHub Actions deliberately does not overwrite this file.
The SSH user must be able to run docker and docker compose without an
interactive password prompt. No inbound application ports are required; only
SSH access is needed for deployment.
Create these variables under Settings > Secrets and variables > Actions:
| Variable | Location | Required | Example |
|---|---|---|---|
ENABLE_DEPLOY |
Repository variable | Yes | true |
DEPLOY_PATH |
Repository or production environment variable |
No | /opt/d5-spam-guard-bot |
DEPLOY_PORT |
Repository or production environment variable |
No | 22 |
If ENABLE_DEPLOY is not exactly true, the workflow still tests and
publishes the image but skips the SSH deployment.
Create these secrets:
| Secret | Purpose |
|---|---|
DEPLOY_HOST |
Server hostname or IP address. |
DEPLOY_USER |
SSH username. |
DEPLOY_SSH_KEY |
Private SSH key used only for deployment. |
GHCR_USERNAME |
GitHub username used by the server to pull the image. |
GHCR_PULL_TOKEN |
Personal access token (classic) with read:packages. |
The corresponding public SSH key must be added to
~/.ssh/authorized_keys for DEPLOY_USER.
For a private GHCR package, GHCR_PULL_TOKEN needs permission to read
packages. If the package is made public, server-side registry authentication
can be removed from the workflow.
The docs/ directory contains a static legal site with:
- Privacy Policy:
docs/privacy.html - Terms of Service:
docs/terms.html - Legal landing page:
docs/index.html
To publish it with GitHub Pages:
- Open the repository on GitHub.
- Go to Settings > Pages.
- Under Build and deployment, select Deploy from a branch.
- Select the
mainbranch and the/docsfolder. - Save the configuration.
The expected URLs are:
https://dh-555.github.io/Anti-Mr-Scam-bot/
https://dh-555.github.io/Anti-Mr-Scam-bot/privacy.html
https://dh-555.github.io/Anti-Mr-Scam-bot/terms.html
The included policies are project-specific templates, not legal advice. The person or organization operating the Bot should review them for the applicable jurisdiction and deployment practices.
After starting the bot, a server administrator can use Discord's native slash command interface:
/setup moderation-channel channel:#moderation
Discord displays a channel picker for the channel option. The selection is
saved separately for each server and persists across restarts in
data/settings.json.
Use /setup status to view the currently configured moderation channel. The
commands require the Manage Server permission and their responses are only
visible to the administrator who runs them.
If a moderation channel has not been configured yet, the bot still scans and
moderates matching images. In that case, it deletes the message, applies the
timeout when possible, and posts a short notice in the same channel telling
admins to configure /setup moderation-channel for full alerts and details.
| Variable | Required | Default |
|---|---|---|
DISCORD_TOKEN |
Yes | — |
TIMEOUT_MINUTES |
No | 1440 (24 hours) |
MAX_IMAGE_SIZE_MB |
No | 8 |
MAX_IMAGE_PIXELS |
No | 16000000 |
IMAGE_DOWNLOAD_TIMEOUT_MS |
No | 15000 |
OCR_CACHE_PATH |
No | tessdata |
OCR_EFFORT |
No | high |
VISUAL_REFERENCE_MANIFEST_PATH |
No | generated/visual-reference-manifest.json |
VISUAL_MATCH_THRESHOLD |
No | 6 |
OCR_EFFORT can be low, medium, or high. Higher effort tries more
preprocessing passes and crops, which improves blurry screen photos at the cost
of slower OCR.
Reference images live in the repository under visual-references/. The build
step hashes them with a perceptual hash and writes the manifest to
generated/visual-reference-manifest.json. At runtime, the bot reads only that
manifest. Lower thresholds are stricter; 0 means exact hash equality.
To regenerate the manifest locally:
pnpm build:visual-referencesWith the current reference folder, startup logs should include:
[Visual matching] Loaded 19 reference hash(es).
Discord limits timeouts to a maximum of 28 days.
pnpm test