High-density blueprint for building Red-DiscordBot embed editors with persistent Views and session isolation.


1. πŸš€ Quick Start & Installation

Version Panning

Before writing any components, verify the installed discord.py version inside your Red virtual environment:

pip show discord.py
VersionUI CapabilitiesModal Selects
β‰₯ 2.6Use Components V2 (LayoutView)Yes (wrapped in a Label)
< 2.6Classic Embed + View componentsNo (text-only text inputs)

Red-DiscordBot Bootstrapping

# venv = mandatory
python3.11 -m venv ~/redenv && source ~/redenv/bin/activate
 
# Install and initialize Red
pip install -U Red-DiscordBot
redbot-setup
redbot <instance>

2. πŸ”§ Architecture & Limits

Message State Machine

graph TB
    %% Definitions of styles
    classDef container fill:#f9f9f9,stroke:#333,stroke-width:2px;
    classDef inner fill:#fff,stroke:#666,stroke-dasharray: 5 5;
    classDef action fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
    classDef state fill:#fff3e0,stroke:#f57c00,stroke-width:2px;

    %% Nodes and Connections
    EmbedInput["/embed"] --> SSOT
    ModalInput["modal/select"] --> Mutate["mutate state{}"]
    
    Mutate --> Persist["persist"]
    Persist --> EditMsg["edit_message (same msg)"]
    EditMsg -.->|"Updates"| SSOT

    %% SSOT Container
    subgraph SSOT ["ONE SSOT MESSAGE (Key = guild, channel, user)"]
        direction TB
        
        subgraph LivePreview ["LIVE PREVIEW (embed)"]
            Repaint["Repainted from state every edit"]
        end
        
        subgraph ViewState ["View = STATE MACHINE (render() swaps panels)"]
            Panels["[Content] [Media] [Fields]<br>[Author] [Target] [Post]"]
        end
    end

    %% Apply Styles
    class SSOT container;
    class LivePreview,ViewState inner;
    class EmbedInput,ModalInput action;
    class Mutate,Persist,EditMsg action;

Discord Component Boundaries

ElementLimitNote
Buttons5 per row, 5 rows maxTotal 25 per message
String Select25 options maxConsumes 1 entire row
Modal5 components MAXSelects require a Label wrapper (2.6+)
LayoutView (V2)40 components / 4000 charsCannot mix with standard embed/content
custom_id≀ 100 charactersStore a session key, never the full payload

3. πŸ“¦ Core Code Template

ssot.py β€” Pinned Editor Engine

import discord
from redbot.core import commands
 
# Modal editor for title, description, and color
class ContentModal(discord.ui.Modal, title="Edit Content"):
    ttl   = discord.ui.TextInput(label="Title", required=False, max_length=256)
    desc  = discord.ui.TextInput(label="Description", style=discord.TextStyle.paragraph, required=False, max_length=4000)
    color = discord.ui.TextInput(label="Hex Color (e.g. 5865F2)", required=False, max_length=6)
    def __init__(self, view):
        super().__init__()
        self.view = view
 
    async def on_submit(self, itx: discord.Interaction):
        s = self.view.state
        s["title"] = self.ttl.value or None
        s["description"] = self.desc.value or None
        if self.color.value:
            s["color"] = int(self.color.value, 16)
        await itx.response.edit_message(embed=self.view.preview(), view=self.view)
 
# View container for session state
class EditorView(discord.ui.View):
    def __init__(self, owner_id: int):
        super().__init__(timeout=None) # timeout=None ensures buttons survive restarts
        self.owner_id = owner_id
        self.state = {"title": None, "description": None, "color": 0x5865F2, "fields": []}
 
    async def interaction_check(self, itx) -> bool:
        if itx.user.id != self.owner_id:
            await itx.response.send_message("Not your editor.", ephemeral=True)
            return False
        return True
 
    def preview(self) -> discord.Embed:
        s = self.state
        e = discord.Embed(title=s["title"], description=s["description"], color=s["color"])
        for f in s["fields"]:
            e.add_field(name=f["n"], value=f["v"], inline=f.get("i", False))
        return e
 
    @discord.ui.button(label="Content", style=discord.ButtonStyle.primary, custom_id="ssot:content")
    async def content(self, itx, _):
        await itx.response.send_modal(ContentModal(self))
 
    @discord.ui.button(label="Post", style=discord.ButtonStyle.success, custom_id="ssot:post")
    async def post(self, itx, _):
        await itx.channel.send(embed=self.preview())
        await itx.response.send_message("Posted!", ephemeral=True)
 
# Cog Commands
class SSOTEmbed(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
 
    @commands.hybrid_command(name="embed")
    @commands.admin_or_permissions(manage_messages=True)
    async def embed(self, ctx):
        view = EditorView(ctx.author.id)
        await ctx.send(embed=view.preview(), view=view)

__init__.py β€” Entrypoint

from .ssot import SSOTEmbed
 
async def setup(bot):
    await bot.add_cog(SSOTEmbed(bot))

4. πŸ’‘ Best Practices & Persistence

  • Config Storage: Store states in Red’s built-in Config database (Config.get_conf(self, identifier=<unique_int>)). All config actions must be awaited.
  • Cog Re-load Sync: Re-attach persistent views on reload inside the cog_load() lifecycle hook:
    self.bot.add_view(EditorView(owner), message_id=stored_id)
  • DynamicItem Scaling: Utilize discord.ui.DynamicItem with regex-based custom_id templates to support infinite concurrent sessions without caching message IDs.

5. 🚨 Gotchas / Warnings

  • Namespace Hijacking: Never install other discord wrapper libraries (like pycord, disnake, or nextcord) into the same venv. It corrupts the discord namespace and crashes Red instantly.
  • Button Expiration: Missing timeout=None or failing to define explicit custom_id on buttons will cause components to stop responding after a bot reboot.
  • V2 Collision: Mixing LayoutView (V2 components) with standard embed= or content= parameters in the same message triggers a 400 Invalid Form Body exception. Set them to None on V2 messages.
  • Task Leaks: Uncancelled asyncio tasks during cog_unload create zombie listeners that execute duplicate calls.

6. πŸ” Verification & Build Checklist

  • Step 0: Read pip show discord.py to confirm target API version (V2 vs Classic).
  • Step 1: Write project info.json with required end_user_data_statement.
  • Step 2: Implement /embed command to post the initial SSOT message preview.
  • Step 3: Add EditorView with custom interaction checks to verify permission boundaries.
  • Step 4: Implement ContentModal and configure timeout=None + custom button IDs.
  • Step 5: Hook Config database to auto-restore views when the bot restarts.
  • Step 6: Execute local test runs and run [p]slash sync to verify slash commands.