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| Version | UI Capabilities | Modal Selects |
|---|---|---|
| β₯ 2.6 | Use Components V2 (LayoutView) | Yes (wrapped in a Label) |
| < 2.6 | Classic Embed + View components | No (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
Configdatabase (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.DynamicItemwith regex-basedcustom_idtemplates 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
discordnamespace and crashes Red instantly. - Button Expiration: Missing
timeout=Noneor failing to define explicitcustom_idon buttons will cause components to stop responding after a bot reboot. - V2 Collision: Mixing
LayoutView(V2 components) with standardembed=orcontent=parameters in the same message triggers a400 Invalid Form Bodyexception. Set them toNoneon V2 messages. - Task Leaks: Uncancelled
asynciotasks duringcog_unloadcreate zombie listeners that execute duplicate calls.
6. π Verification & Build Checklist
- Step 0: Read
pip show discord.pyto confirm target API version (V2 vs Classic). - Step 1: Write project
info.jsonwith requiredend_user_data_statement. - Step 2: Implement
/embedcommand to post the initial SSOT message preview. - Step 3: Add
EditorViewwith custom interaction checks to verify permission boundaries. - Step 4: Implement
ContentModaland configuretimeout=None+ custom button IDs. - Step 5: Hook
Configdatabase to auto-restore views when the bot restarts. - Step 6: Execute local test runs and run
[p]slash syncto verify slash commands.