DEV Community

Cover image for Build a Discord NSFW Moderation Bot with Python
AI Engine
AI Engine

Posted on • Originally published at ai-engine.net

Build a Discord NSFW Moderation Bot with Python

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:

  1. Click New Application, name it "NSFW Guard", click Create
  2. Go to Bot → click Reset Token → copy it
  3. Enable Message Content Intent under Privileged Gateway Intents
  4. Go to OAuth2 → check bot scope → check Send Messages + Manage Messages
  5. 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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Run it:

python nsfw_bot.py --discord-token YOUR_TOKEN --api-key YOUR_KEY
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)