← Dokumentation  /  Entwicklerhandbuch

Erstelle ein Plugin

Die vollständige SDK-Referenz für Plugin-Entwickler — Sandboxed Python, als ZIP-Datei versendet, über den Marketplace verteilt.

These docs are publicly viewable. To publish a plugin you'll need a free YourBot account: sign in to access the Dev Portal.
SDK v0.8.4

Plugins sind isolierte Python-Prozesse. Schreibe einen Handler, deklariere deine Fähigkeiten, lade ein Zip hoch — wir führen es in einem abgesicherten Docker-Container aus und übertragen Discord-Events per JSON-RPC.

Plugin-Dashboards

Jedes Plugin kann eine Einstellungs-/Datenseite im benutzerseitigen Dashboard bereitstellen. Zwei Modi — wählen Sie einen:

  • Manifest-Modus — Widgets in JSON deklarieren, Python-Daten-Handler schreiben. Kostenlos gestaltet, kein HTML zu schreiben.
  • Iframe-Modus — Volle HTML/CSS/JS-Kontrolle. Verwenden Sie diesen Modus, wenn Sie eine benutzerdefinierte Benutzeroberfläche benötigen (Drag-and-Drop, Diagramme, die das Manifest nicht abdeckt, benutzerdefinierte Formulare).

Both modes live in the same file: dashboard_manifest.json, shipped in your version zip (the zip root or any folder works). You can also author it in the Dev Portal's Dashboard Editor, which writes the same JSON for you.

Manifest-Modus

Declare "mode": "manifest" and a list of pages with widgets. rpc_method values are the names you registered with @plugin.on_dashboard(…):

{
  "mode": "manifest",
  "pages": [
    {
      "id": "overview",
      "title": "Overview",
      "widgets": [
        {
          "id": "active_users",
          "type": "stat_card",
          "label": "Active users",
          "icon": "📊",
          "width": "quarter",
          "rpc_method": "get_active_users"
        },
        {
          "id": "messages_chart",
          "type": "chart",
          "chart_type": "line",
          "title": "Messages per day",
          "refresh_seconds": 30,
          "rpc_method": "get_messages_chart"
        },
        {
          "id": "settings_form",
          "type": "form",
          "title": "Settings",
          "rpc_method": "get_settings",
          "save_rpc_method": "save_settings",
          "fields": [
            {"key": "channel_id", "label": "Welcome channel", "type": "text"},
            {"key": "message",    "label": "Welcome text",    "type": "textarea"}
          ]
        }
      ]
    }
  ]
}
  • Widths: "quarter", "third", "half", "two_thirds", "full" (default).
  • Auto-refresh: "refresh_seconds": 30 re-fetches periodically; a manual refresh button appears on every widget.
  • period akzeptiert 1h, 24h, 7d, 30d, 90d. Aggregate: sum (Standard), avg, max, min.
  • Form field types: text (default), textarea, number, select, multi_select, toggle, color, date, range. Every field renders label and help_text. placeholder applies to text, textarea and number. required is enforced on every type except toggle, color and range. select and multi_select need "options": [{"value": "...", "label": "..."}]. number and range accept min, max and step.
  • Extra RPC args: "rpc_params": {"stat": "total"} is merged into the params your handler receives.
  • Shared handlers: a widget may set "path" (e.g. "kpis.0" or "activity") to extract its slice from a bigger RPC response, so one get_overview handler can drive a whole page of widgets.
  • Access control: pages and widgets accept "permission": "viewer" | "manager" | "owner"; manifest values can only raise the bar, never lower it. Saves always require at least manager, enforced server-side regardless of what your UI shows.

Prefer Python over raw JSON? yourbot_sdk.dashboard ships fluent builders (Manifest, Page, StatCard, Chart.line(…), Form, …) that emit exactly this JSON.

Unterstützte Widget-Typen

TypHandler gibt zurück
stat_card{"value": 1234, "change": "+12%", "trend": "up", "color": "green"} (trend: up / down / flat; color: green / red / gold / cyan)
chart{"labels": [...], "series": [{"name": "...", "data": [...]}]}
table{"rows": [{...}, ...], "total": 100, "page_size": 20} — Warum deklarieren? "columns": [{"key": "...", "label": "..."}] on the widget (required); headers and cell order come from it. Your handler receives params["page"] (1-indexed) when the viewer pages with Prev/Next
list{"items": [{"label": "...", "value": "..."}, ...]}
progress_bar{"value": 75, "max": 100, "label": "…"}
text{"content": "Plain text content"}
markdown{"markdown": "# Heading\n- bullet"} (safe markdown, raw HTML is stripped)
alert{"level": "info", "message": "…"} — level: info / warning / error / success
formget → {"values": {...}}; save → {"ok": true} oder {"ok": false, "error": "…"}

Python-Handler

@plugin.on_dashboard("get_active_users")
def active_users(ctx: Context, params: dict):
    n = ctx.kv.count(prefix="active_user:")
    return {"value": n, "change": "+12%", "trend": "up"}

@plugin.on_dashboard("get_messages_chart")
def messages_chart(ctx: Context, params: dict):
    return ctx.metrics.query("messages", period="7d")

@plugin.on_dashboard("get_settings")
def get_settings(ctx: Context, params: dict):
    return {"values": {
        "channel_id": ctx.kv.get("welcome_channel") or "",
        "message":    ctx.kv.get("welcome_message") or "Welcome!",
    }}

@plugin.on_dashboard("save_settings")
def save_settings(ctx: Context, params: dict):
    # Form saves arrive as params["values"], keyed by each field's "key".
    values = params.get("values") or {}
    message = str(values.get("message", "")).strip()
    if len(message) > 500:
        return {"ok": False, "error": "Keep the welcome text under 500 characters"}
    ctx.kv.set("welcome_channel", values.get("channel_id", ""))
    ctx.kv.set("welcome_message", message)
    return {"ok": True}
  • 8-second timeout per handler call; the widget shows an error past it.
  • Fresh Context per call: every dashboard RPC gets a ctx scoped to the server the viewer is looking at, even in pool mode (one shared worker serves every server that installed your plugin). params carries discord_srv_id (a string on widget reads, an int on form saves and iframe RPCs — compare with str(params.get("discord_srv_id"))) plus the viewer's identity: discord_user_id, viewer_user_id, viewer_username and username.
  • Read caching: widget responses are cached ~5 seconds per (plugin, server, method, params); each table page caches separately and writes bypass the cache.
  • Serialized: only one dashboard RPC runs at a time per worker, and handlers share the process with your event handlers.
  • Cold start: if no worker is running for your plugin yet, the widget shows “Starting plugin…” and retries up to 5 times at 1.5-second intervals.

Iframe-Modus

Iframe mode turns on when your dashboard_manifest.json declares "mode": "iframe" and every page names an HTML file via "src" (the validator rejects iframe pages without one). Ship those files in a dashboard/ directory; the platform serves them on a per-plugin sandboxed origin inside an iframe and gives you a JS bridge (YourBotSDK) for backend RPC, theming and lifecycle.

{
  "mode": "iframe",
  "pages": [
    {"id": "overview", "title": "Overview", "src": "index.html"}
  ]
}
my_plugin/
├── manifest.json
├── dashboard_manifest.json   # the JSON above
├── __main__.py
└── dashboard/
    ├── index.html       # page "src", served relative to dashboard/
    ├── style.css
    └── app.js

The sandbox CSP allows same-origin assets only, so bundle every stylesheet, script, image and font inside dashboard/ and reference them with relative paths. The one exception is the bridge script itself: /static/yourbot-sdk.js is served on the sandboxed origin, so load it directly. Pages may embed third-party <iframe>s only for domains you list in proxy_domains_requested.

Minimales HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>body { color: var(--yourbot-text); background: transparent; }</style>
</head>
<body>
  <h1>My plugin</h1>
  <div id="stat">Loading…</div>

  <script src="/static/yourbot-sdk.js"></script>
  <script>
    YourBotSDK.ready(async function () {
      const ctx = YourBotSDK.getContext();
      // ctx = { plugin_id, server_id, page_id, user: {id, display_name, role} }

      const res = await YourBotSDK.metrics.total("messages_sent", "30d");
      document.getElementById("stat").textContent = (res.total || 0) + " messages";

      YourBotSDK.resize();   // ask the iframe host to resize to fit content
    });
  </script>
</body>
</html>

YourBot SDK

Every method returns a Promise. The bridge only forwards allow-listed RPC namespaces (kv, metrics, sql, dashboard). The kv, metrics and sql calls run host-side against the same per-install storage your Python handlers see: they never wake your plugin code, but they need a live pool worker for your plugin and they enforce the same capabilities as plugin-side calls (storage:kv for kv, storage:sql for sql, none for metrics). Reads need the viewer role; writes (kv.set, kv.setMany, kv.delete, kv.increment, kv.decrement, metrics.record, sql.execute and save_* dashboard handlers) need manager.

AufrufZweck
YourBotSDK.ready(fn)fn nach Abschluss des Bridge-Handshakes ausführen.
YourBotSDK.getContext()Sync — gibt Server / Plugin / Benutzer / Theme zurück.
YourBotSDK.getUser()Sync — the viewer: {id, display_name, role}; role is viewer, manager or owner.
YourBotSDK.getTheme()Sync — the current theme's CSS variable values as a map.
YourBotSDK.on(event, fn) / off(event, fn)Listen for "ready", "theme" or "navigate". The theme event arrives once, at the handshake.
YourBotSDK.resize(height?)Weist den Host-iframe an, die Größe anzupassen. Erkennt den Inhalt automatisch, wenn keine Höhe übergeben wird.
YourBotSDK.navigateTo(pageId)Zu einer anderen Seite in deinem Dashboard-Manifest wechseln.
YourBotSDK.rpc(method, params)Einen Python-Handler @plugin.on_dashboard aufrufen.
YourBotSDK.kv.get / set / delete / list / listValues / getMany / setMany / increment / decrement / countDirect KV access (needs storage:kv). Resolves to result objects: get → {ok, value}; set / setMany / delete → {ok}; list → {ok, keys} (limit cap 1000); listValues → {ok, values} (limit cap 100); getMany → {ok, values} (max 50 keys); increment / decrement → {ok, value}; count → {ok, count}.
YourBotSDK.metrics.query / total / recordPull metrics for charts or record a data point. total → {ok, total}, so unwrap before rendering. No capability required; record needs a manager viewer.
YourBotSDK.sql.query / executeSQL access (needs storage:sql). query → {ok, rows, truncated} (limit cap 1000); execute → {ok, rowcount}.
YourBotSDK.openPlatformPopup(path, features?)Open a platform path (e.g. /buy/…) in a popup resolved against the parent dashboard's origin. Returns the popup window, or null before the handshake. Pass a path, not a full URL.

Bridge calls are rate-limited (600/min) and rejected errors carry a machine-readable .code (e.g. RPC_TIMEOUT). If no worker is running for your plugin yet, kv / metrics / sql calls fail with a “no running worker” error until it starts.

Theme-Variablen

Der iframe erbt diese benutzerdefinierten CSS-Eigenschaften vom Dashboard, damit dein Widget sich einfügt, ohne dass du Farben zurückentwickeln musst:

:root {
  --yourbot-bg;       --yourbot-surface;  --yourbot-surface-alt;
  --yourbot-border;   --yourbot-gold;     --yourbot-text;
  --yourbot-muted;    --yourbot-success;  --yourbot-error;
  --yourbot-warning;  --yourbot-info;
}

Legacy --maid-* aliases are also set for older dashboards. Delivery order matters: ready fires before the theme message lands, so inside a ready callback getTheme() is still empty and the CSS variables are not yet set. Register an on("theme") listener (it fires once, right after ready) and do theme-dependent work there; plain CSS that uses var(--yourbot-*) just works once the variables land.

Drop-in-Style-Paket neu

Manifest widgets are themed automatically. For iframe dashboards the platform ships a small CSS + JS pack so your custom HTML matches the host without copying styles. Bundle both files into your zip under dashboard/sdk/ (grab the current copies from /static/sdk/ui.css?v=2 and /static/sdk/ui.js?v=2) and reference them relative to dashboard/:

<link rel="stylesheet" href="sdk/ui.css">
<script src="sdk/ui.js" defer></script>

Was im Paket enthalten ist:

  • .maid-btn / .maid-btn-primary / .maid-btn-danger — Schaltflächen passend zur Plattform-Palette
  • .maid-input / .maid-textarea / .maid-select — Formularfelder mit theme-bewussten Fokusringen
  • .maid-card — Gestalteter Container mit Header-/Hilfe-Slots
  • .maid-badge / .maid-pill — Statusanzeigen mit semantischen Farben (ok / warn / error / info / muted)
  • .maid-table — Theme-bewusste Datentabelle
  • .maid-stack / .maid-grid / .maid-row — Layout-Helfer
  • .maid-empty — Leer-Zustand-Container

JS-Helfer (unter window.MaidUI namensbasiert):

YourBotUI.toast("Saved!", "ok");
YourBotUI.confirm("Delete this record?").then((yes) => { if (yes) /* ... */ });
YourBotUI.modal({ title: "Details", body: bodyElement, actions: [/* ... */] });
YourBotUI.tabs(container);                      // wire [data-yourbot-tab] tabs
YourBotUI.table(target, { columns, rows });     // themed, click-to-sort table
YourBotUI.copy("text to clipboard");

// Form saves: call your @plugin.on_dashboard("save_settings") handler.
// It receives the object as params["values"].
const values = YourBotUI.serializeForm(myFormElement);
await YourBotSDK.rpc("save_settings", { values });

YourBotUI.fetchJSON(url, init) is bridge-aware: inside a sandboxed iframe it routes /p/{plugin_id}/dashboard/rpc/{method} URLs through YourBotSDK.rpc (load yourbot-sdk.js first) and resolves to the endpoint's {ok, data} body, while on main-origin pages it fetches directly and adds the page's CSRF token to non-GET requests.

SDK v0.8.4 · Zuletzt aktualisiert Juli 2026 · Zurück zum Entwicklerportal