8.7 KiB
Impersonate Panel — Design Spec
A Discohook-style webhook message editor inside the Aurora admin panel for sending messages as custom characters, with reusable presets.
Summary of Decisions
| Decision | Choice |
|---|---|
| Channel targeting | Pick channel each time (dropdown) |
| Preset storage | PostgreSQL (new table) |
| Editor layout | Side-by-side (builder left, preview right) |
| Component adding | Drag & drop from palette |
| Preset management | Separate "Presets" tab with card grid |
| JSON editing | Bidirectional visual ↔ JSON toggle |
| Format support | Classic (content + embeds) AND Components V2 |
Data Model
New table: webhook_presets
| Column | Type | Description |
|---|---|---|
id |
serial PK | Auto-increment ID |
name |
varchar(100) | Preset display name |
username |
varchar(80) | Webhook display name |
avatar_url |
text, nullable | Avatar image URL |
payload |
jsonb | Full webhook payload (content, embeds, components) |
created_by |
bigint | Discord user ID of creator |
created_at |
timestamp | Creation time |
updated_at |
timestamp | Last modified |
The payload column stores the complete webhook JSON body and is the source of truth. The visual editor reads/writes this JSONB directly.
Notes:
created_byusesbigint('created_by', { mode: 'bigint' })with a foreign key reference tousers.id(onDelete: CASCADE)created_atuses.defaultNow()updated_atis set by the application on every write (no database trigger)- No
guild_idscoping — Aurora is a single-guild bot - The new schema file must be re-exported from
shared/db/schema/index.ts - Backend validation: reject payloads larger than 100KB
API Endpoints
All endpoints are protected behind existing admin auth.
Presets CRUD
GET /api/impersonate/presets— list all presetsPOST /api/impersonate/presets— create presetPUT /api/impersonate/presets/:id— update presetDELETE /api/impersonate/presets/:id— delete preset
Sending
POST /api/impersonate/send— send webhook message to a channel- Body:
{ channelId: string, username: string, avatarUrl?: string, payload: object } - Bridge pattern: The route handler imports
BotClientto resolve the channel by ID (client.channels.fetch(channelId)) and obtain the client user. These discord.js objects are passed to the existingsendWebhookMessageutility frombot/lib/webhookUtils.ts. This is acceptable becauseapi/already runs in the same Bun process as the bot.
- Body:
Channels
GET /api/impersonate/channels— fetch guild text channels for the channel picker- Returns
{ id, name, parentName }grouped by category
- Returns
Frontend Architecture
Page Structure
Two tabs at the top of the Impersonate page: Editor and Presets.
Editor Tab (Side-by-Side)
Left pane — Builder:
- Top bar: Username input, avatar URL input, channel dropdown, format toggle (Classic / Components V2), JSON/Visual toggle
- Component palette: Draggable component types. Components V2: Text Display, Section, Media Gallery, Separator, Container, File, Action Row. Classic: Content, Embed
- Message canvas: Drop zone where components are arranged. Each dropped component expands into an inline collapsible form editor. Drag to reorder via
@dnd-kit/core - Bottom bar: "Send" button and "Save as Preset" button
Right pane — Preview:
- Discord-styled message preview (dark theme,
#313338background) - Avatar circle + username + timestamp header
- Live-renders the current payload on every change
- Visual approximation of Discord's rendering, not pixel-perfect
JSON Mode
Toggling to JSON replaces the visual builder with a monospace code editor. Edits sync bidirectionally. Invalid JSON shows an inline error and blocks switching back to visual mode until fixed.
Presets Tab
- Card grid of saved presets showing avatar, name, and truncated payload preview
- Click a card to load it into the editor tab
- Edit/delete actions on each card
File Structure
panel/src/
├── pages/
│ ├── Impersonate.tsx # Main page, tab switching, top-level state
│ └── impersonate/
│ ├── Editor.tsx # Side-by-side builder + preview layout
│ ├── Preview.tsx # Discord-style message renderer
│ ├── Presets.tsx # Preset card grid
│ ├── ComponentPalette.tsx # Draggable component type list
│ └── components/
│ ├── TextDisplayEditor.tsx
│ ├── SectionEditor.tsx
│ ├── MediaGalleryEditor.tsx
│ ├── SeparatorEditor.tsx
│ ├── ContainerEditor.tsx
│ ├── FileEditor.tsx
│ ├── ActionRowEditor.tsx
│ ├── EmbedEditor.tsx # Classic mode
│ └── ContentEditor.tsx # Classic mode
├── lib/
│ └── useImpersonate.ts # API hook for presets CRUD + send + channels
Backend:
shared/db/schema/
│ └── webhook-presets.ts # New schema file (re-export from index.ts)
shared/modules/impersonate/
│ └── impersonate.service.ts # Preset CRUD + send logic
api/src/routes/
│ └── impersonate.routes.ts # Route handler (register in index.ts protectedRoutes)
Panel Wiring
- Add
"impersonate"to thePageunion type inLayout.tsx - Add nav item to
navItemsarray inLayout.tsxwith appropriate Lucide icon - Add conditional render branch in
App.tsx
Note: The pages/impersonate/ sub-directory is a new pattern — existing pages are flat files. This is justified by the complexity of this feature (9+ component files). Flat pages remain appropriate for simpler pages.
Component Editors
Each component type gets an inline collapsible editor card on the canvas.
Components V2
| Component | Editable Fields |
|---|---|
| Text Display | Markdown content textarea |
| Section | Text content, accessory type (button or thumbnail), accessory config (URL, label, style) |
| Media Gallery | List of media items: URL, alt text, spoiler toggle. Add/remove items |
| Separator | Spacing size toggle (small/large) |
| Container | Accent color picker, nested drop zone (accepts Text Display, Section, Media Gallery, Separator, Action Row, File) |
| File | URL input, filename |
| Action Row | Buttons: label, style (Primary/Secondary/Success/Danger/Link), URL/custom ID, emoji, disabled toggle. Select menus: placeholder, options list, min/max values |
Classic Mode
| Component | Editable Fields |
|---|---|
| Content | Markdown textarea |
| Embed | Title, description, URL, color picker, timestamp, author (name, icon URL), footer (text, icon URL), image URL, thumbnail URL, fields (array of name, value, inline toggle) |
Webhook-Level Options
ttstogglethread_nameinput (for forum channels)flags(suppress embeds/notifications)- When Components V2 format is selected, the payload must include
flags: 32768(IS_COMPONENTS_V2flag,1 << 15). This is set automatically by the editor when the format toggle is on Components V2.
Preview Renderer
Renders a Discord-style message mock in the right pane:
- Dark background (
#313338) - Avatar circle + username + "Today at HH:MM" timestamp header
- Components V2: containers with accent-colored left border, text blocks with markdown rendering, media gallery as responsive image grid, buttons as pill-shaped elements with Discord color scheme, separators as horizontal rules
- Classic: content as rendered markdown, embeds with colored left border, field grids, inline images
- Live updates on every change
This is a visual approximation for authoring purposes, not a 1:1 Discord replica.
Error Handling
| Scenario | Behavior |
|---|---|
| Invalid JSON on toggle | Show inline error, block switch to visual until fixed |
| Send failure | Display Discord API error message inline (e.g., "Missing permissions") |
| Empty payload | Disable Send button |
| Discord payload limits | Validate against limits (6000 char embeds, 10 components per action row, 5 action rows) and show warnings |
| Channel permission errors | Surface "Bot lacks MANAGE_WEBHOOKS permission" clearly |
| Invalid avatar URL | Lightweight https:// check; Discord rejects bad URLs on send |
| Preset name collision | Allowed — presets identified by ID |
Dependencies
@dnd-kit/core+@dnd-kit/sortable— drag and drop- No other new dependencies expected; existing stack (React, Tailwind, Lucide) covers the rest