← Back to Home

Custom Discord Bot Documentation

Introduction

This documentation covers the custom Discord bot built in Python using discord.py with slash command support via app_commands. The bot has grown from a simple status-rotating bot into a full-featured server utility, including an OpenAI integration for answering questions and a complete D&D Party Inventory System with interactive Discord UI components.

The bot uses environment variables for sensitive credentials — the Discord token and OpenAI API key are loaded via os.getenv() and never hardcoded.

Command Reference

Command Type Description
/invSlashOpens the D&D Party Inventory panel
/sbSlashSends a random image from the bot's images folder
/bxSlashPosts the bxbrian YouTube link
/duduSlashPosts the dulonkk YouTube link
/sikeSlashSends a playful message
!infoPrefixDisplays the current server name
!pollPrefixCreates a reaction-based poll

D&D Party Inventory System (/inv)

The most substantial feature of the bot. Running /inv posts a persistent embed panel in the channel that anyone in the server can interact with using buttons and dropdown menus — no additional commands needed. Inventory data is stored in JSON files on disk, sharded by guild ID so each server has its own isolated inventory.

Inventory Panel Buttons

Button Description
📖 Add from CompendiumSearch the D&D compendium and add an official item to inventory
✏️ Add Custom ItemAdd a homebrew or custom item with name, quantity, cost, weight, and description
🗑️ Remove ItemSelect an item from inventory and remove some or all of it
📋 Item DetailsView full stats and description for any item in the inventory
🪙 GoldManage party gold, personal gold, and transfers between the two
✨ Spell LookupSearch the compendium for spells and view full spell details
🔄 RefreshRefresh the embed to show the latest inventory state

Compendium Search

The bot loads item and spell data at startup by parsing a local compendium.xml file using Python's xml.etree.ElementTree. The parser also sanitizes bare & characters that would otherwise cause XML parse errors. Search results are paginated in groups of 25 using Discord select menus and next/previous buttons.

def load_compendium() -> list[dict]:
    import re
    items = []
    with open(COMPENDIUM_PATH, "r", encoding="utf-8", errors="ignore") as f:
        raw = f.read()
    # Fix bare & that aren't part of an XML entity
    raw = re.sub(r"&(?!(?:[a-zA-Z][a-zA-Z0-9]*|#\d+|#x[0-9a-fA-F]+);)", "&", raw)
    root = ET.fromstring(raw)
    for item in root.findall(".//item"):
        name = (item.findtext("name") or "").strip()
        if name:
            items.append({ "name": name, "type": item.findtext("type") or "—", ... })
    return sorted(items, key=lambda x: x["name"])

Gold System

The inventory tracks two separate gold pools — a shared party gold pool and individual player gold wallets stored per Discord user ID. The panel displays both along with a grand total. Players can add/spend their own gold, modify party gold (if they have permission), or transfer gold between themselves and the party pool in either direction.

Data Storage

Each guild's inventory is saved to inventories/<guild_id>/party.json. The file stores items (with quantity, custom flag, and optional metadata), party gold, and a per-user gold map. Files are created automatically on first use.

# Example party.json structure
{
  "items": {
    "Longsword": { "qty": 2, "custom": false },
    "Bag of Holding": { "qty": 1, "custom": false },
    "Mystery Potion": { "qty": 3, "custom": true, "description": "Glows faintly blue." }
  },
  "gold": 450,
  "player_gold": {
    "123456789": 120,
    "987654321": 75
  }
}

Commands and Usage

Rotating Status Messages

The bot automatically rotates its status every 5 seconds using a background task loop. No user interaction is needed — it starts automatically when the bot comes online.

@tasks.loop(seconds=5)
async def change_status():
    await bot.change_presence(activity=discord.Game(random.choice(statuses)))

/sb — Random Image

Picks a random file from the bot's local images directory and sends it in the channel. If the folder is empty, the bot replies with an ephemeral error message only visible to the user.

@bot.tree.command(name="sb", description="Send a random image from the 'images' folder.")
async def send_image(interaction: discord.Interaction):
    image_dir = 'images'
    images = os.listdir(image_dir)
    if not images:
        await interaction.response.send_message('No images found.', ephemeral=True)
        return
    image_file = random.choice(images)
    image_path = os.path.join(image_dir, image_file)
    await interaction.response.send_message(file=discord.File(image_path))

!poll — Reaction Poll

Creates a poll with ✅/❌ reactions. Supports a plain yes/no poll or two custom options. The command also purges the user's original message to keep the channel clean.

Usage:

@bot.command()
async def poll(ctx, question, option1=None, option2=None):
    await ctx.channel.purge(limit=1)
    if option1 is None and option2 is None:
        message = await ctx.send(f"```New poll: \n{question}```\n**✅ = Yes**\n**❌ = No**")
    elif option1 is not None and option2 is None:
        message = await ctx.send(f"```New poll: \n{question}```\n**✅ = {option1}**\n**❌ = No**")
    elif option1 is None and option2 is not None:
        message = await ctx.send(f"```New poll: \n{question}```\n**✅ = Yes**\n**❌ = {option2}**")
    else:
        message = await ctx.send(f"```New poll: \n{question}```\n**✅ = {option1}**\n**❌ = {option2}**")
    await message.add_reaction('❌')
    await message.add_reaction('✅')

Setup & Requirements

Notes

← Back to Home