Erstelle ein Plugin
Die vollständige SDK-Referenz für Plugin-Entwickler — Sandboxed Python, als ZIP-Datei versendet, über den Marketplace verteilt.
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.
Fehlerbehandlung dokumentiert
Das SDK wirft typisierte Ausnahmen statt Fehler-Dicts zu. Alle erben von SdkError, also kannst du breit oder eng fangen.
from yourbot_sdk import (
SdkError, CapabilityError, RateLimitError, DiscordApiError,
SdkPermissionError, ValidationError, KvQuotaError, RpcTimeoutError,
)
@plugin.on_event("message_create")
def on_message(ctx: Context, event: dict):
try:
ctx.discord.send_message(channel_id=event["channel_id"], content="Pong")
except RateLimitError as exc:
ctx.log(f"rate limited; retry in {exc.retry_after}s", level="warning")
except SdkPermissionError as exc:
ctx.log(f"missing Discord permission: {exc.permission}", level="error")
except DiscordApiError as exc:
if exc.status_code == 404:
ctx.log("channel was deleted", level="info")
else:
ctx.log(f"discord {exc.status_code}: {exc}", level="error")
except SdkError as exc:
# Catch-all - logs and keeps the plugin running.
ctx.log(f"unexpected: {exc}", level="error")
Every exception also carries a stable machine-readable .code so you can branch on failures without string-matching: v0.6.1
except SdkError as e:
if e.code == "RATE_LIMITED":
time.sleep(getattr(e, "retry_after", 5))
elif e.code == "CAPABILITY_DENIED":
ctx.log(f"missing capability: {e}", level="error")
Referenz
| Ausnahme | Ausgelöst wenn |
|---|---|
| SdkError | Basisklasse — fange dies, um alles vom SDK zu verarbeiten. SDK_ERROR |
| CapabilityError | Du hast eine API aufgerufen, die dein Plugin nicht über capabilities_required angefordert hat. CAPABILITY_DENIED |
| RateLimitError | Kontingent überschritten. Hat .retry_after (Sekunden). RATE_LIMITED / QUOTA_EXCEEDED |
| DiscordApiError | Discords REST gab nicht-2xx zurück. Hat .status_code. DISCORD_API_ERROR |
| SdkPermissionError | Bot fehlt eine Discord-Guild-Berechtigung. Hat .permission (z. B. "manage_channels"). BOT_MISSING_PERMISSION |
| ValidationError | Du hast ungültige Argumente übergeben (leere channel_id, schlechtes Emoji, Schlüssel mit Null-Bytes…). VALIDATION_ERROR |
| KvQuotaError | Hit a KV key-count quota (50k per server or 500k global). An oversized value or key currently surfaces as a generic RPC error instead. KV_QUOTA_EXCEEDED |
| RpcTimeoutError | Der Runner hat nicht innerhalb des Pro-Call-Timeouts geantwortet. RPC_TIMEOUT |
| PermissionError Alias | Rückwärtskompatibles Alias für SdkPermissionError. |
| TimeoutError Alias | Rückwärtskompatibles Alias für RpcTimeoutError. |
Testen dokumentiert
Plugins werden wie jeder andere Python-Code getestet — kein Docker, keine Plattformverbindung, keine Discord-Mocks. Importiere aus mmo_maid_sdk.testing:
from yourbot_sdk.testing import MockContext, make_event
def test_ping_replies_pong():
ctx = MockContext()
event = make_event("message_create", content="!ping", channel_id="42")
on_message(ctx, event) # the handler from your __main__.py
assert len(ctx.messages_sent) == 1
sent = ctx.messages_sent[0]
assert sent["channel_id"] == "42"
assert sent["content"] == "Pong!"
def test_kv_counter_increments():
ctx = MockContext()
ctx.kv.increment("hits")
ctx.kv.increment("hits")
assert ctx.kv.get("hits") == 2
def test_capability_gate():
ctx = MockContext(capabilities=["discord:send_message"])
assert ctx.has_capability("discord:send_message")
assert not ctx.has_capability("storage:sql")
MockContext enforces capabilities by default. Calling a gated method without the matching capability raises CapabilityError, exactly like production — so a passing test means a working manifest. With no capabilities= argument you get the full standard set except proxy:websocket; WebSocket tests need MockContext(capabilities=[..., "proxy:websocket"]). capabilities=[] means none. Pass MockContext(strict_capabilities=False) to opt out. v0.6.1
Was MockContext aufzeichnet
Jeder Discord-Seiteneffekt wird zur Überprüfung erfasst. Lese diese als Listen von Dicts in der Reihenfolge, in der sie aufgerufen wurden:
ctx.messages_sent ctx.messages_edited ctx.messages_deleted
ctx.roles_added ctx.roles_removed
ctx.members_banned ctx.members_kicked ctx.modals_sent
ctx.kv_writes ctx.log_lines ctx.interaction.responses
ctx.metrics.recorded ctx.sql.executed
ctx.discord.messages_pinned ctx.discord.messages_unpinned
ctx.discord.reactions_added ctx.discord.members_timed_out ctx.ws.ensured
Everything else your handlers touch is recorded on ctx.discord (webhooks, channels, threads, nicknames, permissions) and ctx.ws (ensured, sent, closed, allowed_hosts).
Deterministic time and canned data: MockContext(clock=MockClock(start=1000.0)) lets you clock.advance(31) for cooldown tests (a bare MockClock() is wall-clock passthrough and cannot be advanced), and ctx.discord.set_messages([…]) feeds get_messages / iter_messages.
Ausgehende HTTP-Anfragen mocken
def test_calls_external_api():
ctx = MockContext()
ctx.http.mock_response(
"api.example.com/status",
status=200,
body='{"online": true}',
)
my_status_handler(ctx, make_event("message_create", content="!status"))
assert ctx.http.requests[0]["url"].startswith("https://api.example.com")
make_event liefert sinnvolle Standardwerte für alle 16 Ereignistypen — übergib Überschreibungen als kwargs.
Funktionen
Every API your plugin uses corresponds to a capability. Declare them in capabilities_required; server admins choose which to grant at install time. The full table of capabilities, tiers and the calls each one unlocks lives in the capability catalog on the Reference tab.
The upload pipeline auto-adds any capability it detects from your code (with file and line attribution in the Dev Portal), plus two manifest implications: slash_commands implies interaction:respond, and a non-empty proxy_domains_requested implies proxy:http. So forgetting a capability does not make calls fail at upload; it silently appears on your consent screen, and if it is a reviewed tier it can send the version to staff review.
Tiers are what customers see at install: Safe, Standard and Dangerous badges on the consent screen. Dangerous-tier calls are also audit-logged at WARNING level.
Der Aufruf einer API, die dein Plugin nicht angefordert hat, löst zur Laufzeit einen CapabilityError aus — der Runner blockiert den Aufruf, es entsteht kein Teilschaden. Der Benutzer sieht eine "Berechtigung erforderlich"-Aufforderung und kann sie gewähren, ohne das Plugin zu deinstallieren.
Sandbox — was sie garantiert
Marketplace-Plugins laufen in isolierten Docker-Containern. Die Sandbox ist keine Einschränkung für dich — sie ist eine Reihe von Garantien, die in deinem Namen gemacht werden.
Was die Sandbox dir garantiert
- Dein Plugin kann keine Benutzer-Tokens, Kunden-DMs oder Plattformgeheimnisse leaken. Die Umgebung ist leer — keine Umgebungsvariablen, keine DB-Zugangsdaten, keine Discord-Bot-Tokens. Falls dein Code jemals kompromittiert wird, beschränkt sich der Schaden genau auf das, was der Benutzer der Installation zugestimmt hat. Du bist nicht eine schlechte Abhängigkeit von einem Sicherheitsvorfall entfernt.
- Dein Plugin kann die Plattform nicht für andere Benutzer zum Absturz bringen. Falls dein Plugin abstürzt, stirbt nur dein Plugin; der Rest des Servers läuft weiter.
- Fehler in einem Plugin können nicht zu Fehlern in einem anderen Plugin werden. Jedes Plugin erhält seinen eigenen Container, seinen eigenen KV-Namespace, sein eigenes SQL-Schema. Du kannst nicht versehentlich die Daten eines anderen Plugins lesen, und ein anderes Plugin kann deine nicht beschädigen.
- Kunden müssen deinen Code nicht prüfen, um deinem Plugin zu vertrauen. Die Berechtigungsauswahl zur Installationszeit zeigt ihnen genau, worauf dein Plugin zugreifen kann — keine versteckten Imports, keine unerwarteten Nebeneffekte. Das macht den Marketplace funktionsfähig.
Wie diese Garantien durchgesetzt werden
- Netzwerk:
--network none. Nur die JSON-RPC-Pipe zum Runner ist erreichbar. Alle HTTP-Anfragen laufen überctx.httpvia den Plattform-Proxy — du deklarierst die benötigten Domains, Kunden sehen sie bei der Installation. - Dateisystem: schreibgeschütztes Root, flüchtiges
/tmp, keine Persistenz zwischen Neustarts. Der Zustand liegt inctx.kv/ctx.sql, wo Backups und Audits ihn einsehen können. - Arbeitsspeicher: 64 MB hartes Limit pro Worker. Der Speicherleck eines Plugins beeinträchtigt keine anderen.
- CPU: 0,25 vCPU. Hintergrundarbeit verhungert keine anderen Plugins.
- Prozesse: maximal 32 (PIDs). Begrenzt den Schaden durch Fork-Bomben.
- Benutzer: nicht-root (
nobody, uid 65534). Keine Möglichkeiten zur Rechteausweitung. - Geheimnisse: env ist leer. Es gibt nichts preiszugeben, selbst wenn du es wolltest.
- Ratenlimits pro (Server, Plugin): 50 Ereignisse/Sek., 60 ausgehende Discord-Aktionen/Min., 30 Proxy-HTTP-Anfragen/Min. Ein Server kann dein Plugin nicht durch Überflutung lahmlegen.
Wenn dein Plugin ein Limit überschreitet, erhält es RateLimitError mit einem retry_after-Hinweis — warte ab, keine Busy-Loop.
Absturzberichte
- Quarantine: 5 consecutive crashes within 5 minutes stops event delivery. Restart the plugin from the dashboard to clear it.
Bot-Modus
Plugins share a container to cut overhead: the platform assigns whole plugins to pools (a deployment-level decision per plugin, not per install), and marketplace plugins run pooled today. Your code runs unchanged; what changes is the runtime shape:
@plugin.on_readyfires per server, not per boot. It runs once per (worker, server) right before that server's first event reaches you, with a tenant-scopedctx— the designed place for per-server init like KV defaults or SQL schema bootstrap.- Schedules run server-side, from your manifest. Declare each
@plugin.crontask in the manifest's"cron"array and the platform fires it per installed server (UTC, 5-minute floor, up to 5 entries; delivery is at-least-once and missed ticks are not replayed, so keep handlers idempotent). Interval-style@plugin.scheduletasks do not run pooled — use cron or the event-driven pattern below. - Module globals are unreliable. The
Contextis rebuilt per event, your plugin runs on more than one worker, and workers restart freely. Anything that must survive belongs inctx.kvorctx.ephemeral. - Tighter limits: 24 MB memory and 0.1 vCPU per plugin (vs 64 MB / 0.25 solo).
Periodic work without schedules
For work tighter than the 5-minute cron floor, or when you'd rather skip the manifest entry, ride on event traffic and throttle with a cooldown — on an active server this runs your job roughly once per interval:
@plugin.on_event("message_create")
def on_message(ctx: Context, event: dict):
# ... your normal handling ...
# Piggyback: roll the daily summary at most once per 24h.
if not ctx.ephemeral.cooldown_check("daily_summary")["active"]:
ctx.ephemeral.cooldown_set("daily_summary", ttl_seconds=86400)
post_daily_summary(ctx)
Quiet servers produce no events, so nothing fires — which is usually what you want (no one is reading the summary anyway). When your feature genuinely needs wall-clock delivery, declare it in the manifest "cron" array instead: those fire on schedule whether or not the server is chatting.
Veröffentlichen & prüfen
Upload your zip on the Dev Portal (or link a GitHub repo and pull versions from it). Run yourbot validate first — it applies the same checks locally. The reviewer checks:
- Manifest ist valide: Pflichtfelder vorhanden, Berechtigungen zulässig, Slash-Befehle korrekt formatiert.
- Code importiert keine unerlaubten Module (die Sandbox blockiert sie ohnehin, aber die Vorabprüfung verhindert, dass Nutzer Fehler sehen).
- Kein unparametrisiertes SQL, keine f-String-Interpolation in
ctx.sql.execute. - No surprises — every submission from a new developer gets human review, and a Dangerous-tier request (like
discord:ban_members) is exactly what reviewers weigh hardest. Established developers with public repos publish without the queue. - Slash command names are legal: no names reserved by built-in plugins (see the definitive list), no duplicates within your manifest.
yourbot validatechecks this locally.
Die Bearbeitungszeit beträgt 1–3 Werktage. Bei Ablehnung erhältst du spezifisches, umsetzbares Feedback im Dev-Portal.
After you publish: updates and rollback
- Auto-updates: installs default to
auto_updateon, so servers pick up your new published version automatically. Include a changelog with every version; it shows on the marketplace page and in update notifications. - Pinning is rollback: server owners can pin any published version; pinning disables auto-update for that server. There is no separate revert button.
- Capability changes: adding any new capability or proxy domain, even a Safe-tier one, pauses auto-update on each install until the admin re-consents — a harmless-looking new capability fragments your install base across versions. Removing one is silent.
- Slash commands propagate per install: Discord sees the commands of each server's installed version, not your latest published one. A renamed option only reaches a server after it updates.
Benenne dein Plugin
A price is optional: free plugins are welcome and many of the most-installed ones are free. When you do charge, the platform handles checkout, invoicing, refunds and payouts.
Erste FAQ erstellen
- Accept the Developer Agreement (you'll be prompted in the Dev Portal).
- Complete Stripe Connect onboarding from Earnings — that's where your money lands. Checkout stays disabled for your paid plans until Stripe enables charges on your account, so buyers can never pay into a void.
kostenpflichtige Pläne
Plans are set per plugin in the Dev Portal: monthly, yearly or one-time billing, with optional per-seat quantities. You can run limited-time sales, offer bundles of your plugins and buyers can gift purchases to a server.
Was du tust
| Your trailing 30-day gross | du behältst |
|---|---|
| Up to $1,000 | 70% |
| $1,000+ | 75% |
| $5,000+ | 80% |
| $10,000+ | 85% |
The tier is computed from your last 30 days of sales net of refunds, so a good month makes every plugin you sell cheaper to run. Your current tier and progress to the next one show on Earnings.
Payouts, refunds and disputes
- Payouts are automatic. Each sale's funds release about 30 days after payment, then pay out to your connected bank through Stripe. No invoices to send, no payout button to press.
- Refunds don't cost you the platform fee. When a sale is refunded the buyer is made whole, your share of that sale is reversed and the platform returns its fee share to you — a fully refunded sale nets you zero, not negative.
- Buyers can escalate refund requests. You get 7 days to respond before staff decide. Disputes (chargebacks) pause the buyer's access and claw back the sale while under review.
SDK v0.8.4 · Zuletzt aktualisiert Juli 2026 · Zurück zum Entwicklerportal