Discord servers that accept image uploads need automated moderation. Manual review doesn't scale — moderators can't watch every channel 24/7. In this tutorial, you'll build a Discord NSFW moderation bot in Python that automatically scans images and removes explicit content in under a second.
The bot uses discord.py and the NSFW Detection API to classify images and delete flagged messages.
What the Bot Does
- Monitors all text channels for image attachments (JPG, PNG, GIF, WebP)
- Sends each image to an NSFW detection API for classification
- Deletes the message if explicit content is detected above a configurable threshold
- Posts a temporary warning in the channel and logs the action to
#mod-log - Skips channels marked as NSFW in Discord settings
Setup
1. Create a Discord Bot
Go to the Discord Developer Portal:
- Click New Application, name it "NSFW Guard", click Create
- Go to Bot → click Reset Token → copy it
- Enable Message Content Intent under Privileged Gateway Intents
- Go to OAuth2 → check
botscope → check Send Messages + Manage Messages - Copy the generated URL, open it in your browser, authorize on your server
2. Get Your API Key
Go to the NSFW Detection API page, subscribe to the free Basic plan (100 requests/month), and copy your RapidAPI key.
3. Install and Run
pip install discord.py aiohttp
Save the following as nsfw_bot.py:
"""
Discord NSFW Moderation Bot
Usage: python nsfw_bot.py --discord-token TOKEN --api-key KEY
"""
import argparse
import logging
import aiohttp
import discord
NSFW_API_URL = "https://nsfw-detect3.p.rapidapi.com/nsfw-detect"
NSFW_API_HOST = "nsfw-detect3.p.rapidapi.com"
CONFIDENCE_THRESHOLD = 85
LOG_CHANNEL_NAME = "mod-log"
IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("nsfw-bot")
async def check_nsfw(session, image_url, api_key):
headers = {
"x-rapidapi-host": NSFW_API_HOST,
"x-rapidapi-key": api_key,
"Content-Type": "application/x-www-form-urlencoded",
}
async with session.post(
NSFW_API_URL, headers=headers, data={"url": image_url}
) as resp:
if resp.status != 200:
return None
data = await resp.json()
labels = data.get("body", {}).get("ModerationLabels", [])
if not labels:
return None
top = max(labels, key=lambda l: l["Confidence"])
if top["Confidence"] >= CONFIDENCE_THRESHOLD:
return top
return None
def is_image(attachment):
if attachment.content_type and attachment.content_type in IMAGE_TYPES:
return True
return attachment.filename.lower().endswith(
(".jpg", ".jpeg", ".png", ".gif", ".webp")
)
class ModerationBot(discord.Client):
def __init__(self, *args, api_key, **kwargs):
super().__init__(*args, **kwargs)
self.api_key = api_key
async def on_ready(self):
logger.info(f"Bot is online as {self.user}")
async def on_message(self, message):
if message.author.bot or not message.guild:
return
if hasattr(message.channel, "nsfw") and message.channel.nsfw:
return
images = [a for a in message.attachments if is_image(a)]
if not images:
return
async with aiohttp.ClientSession() as session:
for attachment in images:
result = await check_nsfw(session, attachment.url, self.api_key)
if result:
await message.delete()
await message.channel.send(
f"⚠️ {message.author.mention}, your image was removed — "
f"detected as **{result['Name']}** "
f"({result['Confidence']:.0f}% confidence).",
delete_after=10,
)
log_ch = discord.utils.get(
message.guild.text_channels, name=LOG_CHANNEL_NAME
)
if log_ch:
await log_ch.send(
f"🛡️ Removed image from **{message.author}** "
f"in #{message.channel.name}\n"
f"Reason: {result['Name']} ({result['Confidence']:.0f}%)"
)
break
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--discord-token", required=True)
parser.add_argument("--api-key", required=True)
args = parser.parse_args()
intents = discord.Intents.default()
intents.message_content = True
bot = ModerationBot(intents=intents, api_key=args.api_key)
bot.run(args.discord_token)
if __name__ == "__main__":
main()
Run it:
python nsfw_bot.py --discord-token YOUR_TOKEN --api-key YOUR_KEY
The bot is now scanning images in real time. Post an image in any channel — safe images pass through, explicit ones get deleted with a warning.
Customization
Adjust threshold — Change CONFIDENCE_THRESHOLD (70 = strict, 95 = lenient).
Block specific categories only:
BLOCKED_CATEGORIES = {"Explicit Nudity", "Violence", "Visually Disturbing"}
# In check_nsfw, replace the top-label logic with:
for label in labels:
if label["Name"] in BLOCKED_CATEGORIES and label["Confidence"] >= CONFIDENCE_THRESHOLD:
return label
Add a strike system — Track violations per user and timeout after 3 strikes. See the full implementation in the complete tutorial.
👉 Read the full tutorial with 10 step-by-step screenshots, strike system code, and deployment guide
Top comments (0)