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

/embed ──> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ONE SSOT MESSAGE β”‚ Key = (guild, channel, user) β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ LIVE PREVIEW (embed) β”‚ β”‚ Repainted from state every edit β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ [Content][Media][Fields] β”‚ View = STATE MACHINE β”‚ [Author ][Target][Post ] β”‚ render() swaps panels β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ modal/select ────┴───> mutate state{} ──> persist ──> edit_message (same msg)


### Discord Component Boundaries
| Element | Limit | Note |
| :--- | :--- | :--- |
| **Buttons** | 5 per row, 5 rows max | Total 25 per message |
| **String Select** | 25 options max | Consumes 1 entire row |
| **Modal** | **5 components MAX** | Selects require a `Label` wrapper (2.6+) |
| **LayoutView (V2)** | 40 components / 4000 chars | Cannot mix with standard embed/content |
| **custom_id** | **≀ 100 characters** | Store a session key, never the full payload |

---

## 3. πŸ“¦ Core Code Template

### `ssot.py` β€” Pinned Editor Engine
```python
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.