Build a Discord bot for image processing with Python

I build small tools that do one job and stay out of the way. This Discord image bot does the same. Set up the app, invite it with the right permissions, store the token properly, process uploads with Pillow, and pass heavier work to an API when needed.

  1. Open the Discord Developer Portal and create a new application. Give it a clear name.
  2. Under “Bot”, add a bot user and copy the token into a .env file. Treat the token like a password.
  3. In “Privileged Gateway Intents”, enable Message Content Intent if the bot needs to read message text. Only switch it on if you need it.

Store the token in a .env file and never commit it.

Example .env:

TOKEN=your_bot_token_here
PREFIX=!
  1. In the OAuth2 section, create an OAuth URL. Select bot and the permissions the bot needs: Send Messages, Read Message History, Attach Files, Embed Links, Use Slash Commands if you plan to add them.
  2. Paste the URL in a browser and invite the bot to a server you control.

Give the bot only the permissions it needs. If it only fetches attachments and sends images back, Attach Files and Send Messages are enough.

I use a modern async library. You can use discord.py or a maintained fork such as Pycord. The example below uses the familiar decorator style.

Minimal bot file app.py:

import os
import discord
from discord.ext import commands
from dotenv import load_dotenv

load_dotenv()

TOKEN = os.getenv("TOKEN")
PREFIX = os.getenv("PREFIX", "!")

bot = commands.Bot(command_prefix=PREFIX, intents=discord.Intents.all())

@bot.event
async def on_ready():
    print(f"Connected to Discord as {bot.user}")

bot.run(TOKEN)

Enable only the intents you need in production. Keep the log output short. I look for the “Connected to Discord as …” line to check the bot is live.

Installing Necessary Libraries

Install the libraries I use:

  • discord.py or Pycord
  • python-dotenv
  • Pillow for image processing
  • requests for API calls

Command:

pip install py-cord python-dotenv Pillow requests

Pick the exact Discord library that matches the examples you follow. API and decorator names can differ between forks.

Run it from a virtual environment:

  1. python -m venv .venv
  2. source .venv/bin/activate
  3. pip install -r requirements.txt
  4. python app.py

Watch the console for the connected message. If the bot fails to connect, check the token, gateway intents, and whether the bot is invited to the server.

Decide how users will upload images. I prefer two options:

  • Attach an image to a message with a caption command, for example !process.
  • Use a slash command /process that accepts an attachment.

Example command handler for attachments:

@bot.command()
async def process(ctx):
    if not ctx.message.attachments:
        await ctx.send("Attach an image with the command.")
        return
    attachment = ctx.message.attachments[0]
    # proceed to download and process

This keeps the user flow simple. Commands give predictable triggers for automation.

I use Pillow for basic image work: resizing, format conversion, simple filters.

Download and process example:

from io import BytesIO
from PIL import Image

async def process_image(attachment):
    data = await attachment.read()
    img = Image.open(BytesIO(data)).convert("RGB")
    img = img.resize((800, 800))  # example resize
    out = BytesIO()
    img.save(out, format="JPEG", quality=85)
    out.seek(0)
    return out

# Then send back:
file = discord.File(fp=out, filename="processed.jpg")
await ctx.send(file=file)

Concrete examples: crop to square for avatars, convert PNG to JPEG to reduce size, or run a sharpen filter. Keep processing fast so users are not left waiting.

Integrating APIs for Enhanced Functionality

For heavier tasks, call an external API. Typical cases: background removal, ML-based upscaling, OCR. The pattern is the same:

  1. Download the attachment.
  2. POST it to the API as multipart/form-data or base64.
  3. Receive the processed image or URL.
  4. Send the result back into Discord.

Example using requests:

r = requests.post(api_url, files={"file": ("image.jpg", out, "image/jpeg")}, headers={"Authorization": f"Bearer {API_KEY}"})
result = r.content  # or r.json()["output_url"]

I keep blocking HTTP off the main path by using an async library or an executor. Check API rate limits and error responses. If the API fails, send a short message and log the details for diagnosis.

Test locally before wider use:

  • Use a test server and multiple image types: JPEG, PNG, GIF.
  • Test small and large files. Note Discord attachment limits.
  • Check behaviour on corrupt files and non-image attachments. Reject unsupported types clearly.

Automated verification helps here. Write unit tests that simulate a BytesIO image and run the processing function. I also test latency and measure time to first byte for API calls. If processing takes longer than a few seconds, send a “processing…” message or a progress update.

Troubleshooting Common Issues

Permissions: missing Attach Files causes silent failures. Check both OAuth and channel overrides.
Intents: if Message Content Intent is off, command detection fails.
Dependencies: mismatched library versions cause attribute errors. Use a requirements.txt file and pin versions.
Rate limits: Discord and external APIs will throttle. Add retry logic with exponential backoff and return clear error messages.
File size: Discord caps uploads. If the workflow needs larger files, ask for an external link or use an upload API like Cloudinary.

Practical debugging steps:

  1. Reproduce the problem with a minimal test case.
  2. Add logging around the network call or Pillow call.
  3. Check Discord permissions in the channel and the server role.
  4. Use exception handlers to capture stack traces and write them to a log file.

Start small, keep processing quick, and hand the heavy lifting to specialised APIs when needed. That keeps the bot responsive.

Related posts

Metadata schema choices for content libraries

Structured metadata only works when it matches how people actually retrieve content. I have seen neat schemas fail as soon as the library meets real records, and tarot makes the problem obvious. If...

Federation trade-offs in self-hosted social feeds

Federation looks tidy until you let it touch the edges, and then the odd cases arrive fast. I prefer self-hosted social feeds that stay explicit about what is local, what is remote, and what should...

FireAvert Z-Wave stove shutoffs for offline safety

FireAvert’s setup does the part that matters without asking Home Assistant to babysit it, which is exactly how I want stove protection to behave. The Home Assistant Z-Wave stove shutoffs badge is...