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.
Auf dieser Seite Schlüssel-Wert-Speicher · Berechtigte Benutzer · Sandboxed SQL · Kurzlebiger Zustand · Ausgehendes HTTP über Proxy · WebSockets · Metriken · Protokollierung
Schlüssel-Wert-Speicher
JSON-Speicher pro Server. Schlüssel sind auf dein Plugin und den aktiven Server beschränkt — du kannst nicht auf die Schlüssel anderer Plugins zugreifen, und deine Schlüssel für Server A sind von Server B isoliert. Berechtigung: storage:kv.
Abrufen / Setzen / Löschen
ctx.kv.set("welcome_channel", "1234567890")
ctx.kv.set("session", {"user": "alice"}, ttl_seconds=3600)
val = ctx.kv.get("welcome_channel")
ctx.kv.delete("session")
exists = ctx.kv.exists("welcome_channel")
Zähler
ctx.kv.increment("messages_today") # +1
ctx.kv.increment("xp:user:42", amount=10)
ctx.kv.increment("stats", path="voice.hours") # dot-path into a JSON value
ctx.kv.decrement("lives_remaining")
increment(key, amount=1, path="") is atomic in a single round-trip — use it instead of get + modify + set. path targets a nested field inside a JSON object value.
Massenoperationen & Auflistung
keys = ctx.kv.list(prefix="user:", limit=100) # [key, key, ...]
values = ctx.kv.list_values(prefix="user:", limit=100) # {key: value}
batch = ctx.kv.get_many(["a", "b", "c"]) # {key: value}, max 50
ctx.kv.set_many({"a": 1, "b": 2}) # max 25 pairs
total = ctx.kv.count(prefix="xp:")
list() returns key names only. When you need the stored values too, use list_values() — it avoids the N+1 of list + get.
Kontingente. 50,000 keys per (server, plugin) plus a global cap of 500,000 keys per plugin across all servers, 64 KB max value, 512-byte key names. Exceeding a key-count quota raises KvQuotaError; an oversize key or value fails with a plain error. list() returns at most 1000 keys and list_values() at most 100 entries per call, so paginate with prefix ranges on big keyspaces.
Berechtigte Benutzer Berechtigung
For third-party API keys, signing secrets and webhook tokens use ctx.secrets instead of ctx.kv. Secrets are encrypted at rest (AES-GCM with the platform master key) and stay out of your repo. The platform never writes secret values into your stdout, logs or KV, but nothing redacts a value your own code prints or logs, so treat ctx.secrets.get results as sensitive. Where you can, prefer secret-backed auth so your code never touches the value at all. Capability: storage:secrets.
Two scopes: an optional per-server override set at runtime (checked first), then the dev-level default you set once in the Dev Portal for every server that installs your plugin.
# Read: per-server override first, then your dev-portal default, else None
api_key = ctx.secrets.get("OPENAI_API_KEY")
if not api_key:
ctx.log("No API key configured for this server", level="warning")
return
ctx.secrets.set("OPENAI_API_KEY", submitted_key) # per-server override
ctx.secrets.delete("OPENAI_API_KEY") # remove the override only
Key format: 1–64 chars, letters/digits/_/-/. only. Values: 1–4096 UTF-8 chars.
Secrets also power secret-backed auth: name a stored secret in an ctx.http or ctx.ws call and the platform sends it as the Authorization header without your code ever reading it.
Sandboxed SQL Berechtigung
For relational data heavier than KV. Each (plugin, server) pair gets its own isolated Postgres schema: your tables for server A and server B are physically separate, so you never need a server_id column and cannot query across servers. Capability: storage:sql — servers consent to it at install time, and it lands in the normal staff review every new developer's submissions go through.
ctx.sql.execute(
"CREATE TABLE IF NOT EXISTS scores ("
" user_id TEXT PRIMARY KEY,"
" points INT NOT NULL DEFAULT 0)"
)
ctx.sql.execute(
"INSERT INTO scores (user_id, points) VALUES (%s, %s) "
"ON CONFLICT (user_id) DO UPDATE SET points = scores.points + EXCLUDED.points",
["user_42", 5],
)
rows = ctx.sql.query("SELECT * FROM scores ORDER BY points DESC LIMIT 10")
top = ctx.sql.query_one("SELECT * FROM scores WHERE user_id = %s", ["user_42"])
total = ctx.sql.scalar("SELECT COUNT(*) FROM scores")
Use %s placeholders (psycopg style); ? and $1 are not accepted. Never build SQL with f-strings or concatenation: it is injection-prone, the upload scan flags it and it will fail review. Queries return at most 1000 rows (hard cap; limit=N only fetches fewer) and the cut is silent — ctx.sql.query returns just the rows, so page with LIMIT/OFFSET or run a COUNT(*) first when the table can outgrow the cap. One statement per call from an allowlist (SELECT, INSERT, UPDATE, DELETE, CREATE/ALTER/DROP TABLE, CREATE/DROP INDEX). Quotas: 100 tables and 10,000,000 rows per (plugin, server), and schema-changing DDL is limited to 20 statements/hour (idempotent IF NOT EXISTS re-runs are exempt).
Kurzlebiger Zustand v0.5.0
Basiert auf Redis — schnelle Zähler, Deduplizierung, Cooldowns und kurzlebige Flags. Verwende dies nicht für Daten, die dauerhaft gespeichert werden müssen (Eviction-Richtlinie ist LRU bei Speicherdruck). Keine Berechtigung erforderlich.
Zähler
n = ctx.ephemeral.counter("ratelimit:user:42", window_seconds=60)
if n > 5:
ctx.interaction.respond(content="Slow down!", ephemeral=True)
return
Abklingzeiten
state = ctx.ephemeral.cooldown_check("daily:user:42")
if state["active"]:
ctx.interaction.respond(
content=f"Try again in {int(state['remaining_seconds'])}s",
ephemeral=True,
)
return
ctx.ephemeral.cooldown_set("daily:user:42", ttl_seconds=86400)
Deduplizierung & Flags
# dedup: returns True the FIRST time a key is seen, False afterwards
if not ctx.ephemeral.dedup(f"welcome:{event['user_id']}", ttl_seconds=3600):
return # already welcomed this user in the last hour
ctx.ephemeral.flag_set("event:black_friday", ttl_seconds=86400)
if ctx.ephemeral.flag_check("event:black_friday"):
...
Limits: keys up to 256 chars, max TTL 24 hours (TTLs above 86400s are silently clamped). Reads are free; writes draw from your 60 outbound actions/min budget at 0.1 each, so roughly 600 ephemeral writes/min if you do nothing else. State is namespaced per (plugin, server): like KV, nothing is shared across servers, so a flag set on server A is invisible on server B. See Limits and quotas.
Ausgehende HTTP-Anfragen
Plugins haben --network none in der Sandbox — jede ausgehende Anfrage wird über den Plattform-Proxy geleitet. Berechtigung: proxy:http.
resp = ctx.http.get("https://api.example.com/v1/status")
print(resp["status"], resp["body_bytes"])
resp = ctx.http.post(
"https://api.example.com/webhook",
body='{"hello": "world"}',
headers={"Content-Type": "application/json"},
)
# Query-string params - values may be strings or lists (repeated keys)
resp = ctx.http.get("https://api.example.com/search",
params={"q": "test", "id": ["123", "456"]})
# Custom method
resp = ctx.http.request("PATCH", "https://api.example.com/x/1", body='{"a":1}')
Antwortstruktur: {"status": int, "body_bytes": str, "headers": dict, "truncated": bool}. Der Body ist auf 1 MB begrenzt; truncated=True, wenn er abgeschnitten wurde. Der Proxy erzwingt 30 Anfragen/Minute pro (Server, Plugin).
- Domain allow-list: only hostnames you declared (manifest or Dev Portal) are reachable. Subdomains are included automatically, so
example.comalso allowsapi.example.com. - Stripped headers:
Authorization,Host,Cookie,Proxy-Authorization,X-Forwarded-*and other smuggling-prone headers are removed. For Bearer-token APIs use secret-backed auth below. For APIs with their own header, a custom header likeX-Api-Keypasses through. Never put keys in the URL: query strings end up in logs. - No redirects, no cookies, no private IPs: redirects are not followed, cookies are never stored, and loopback/private/reserved ranges are rejected.
Authenticated requests (secret-backed auth) v0.8.0
You cannot set Authorization yourself, and you should not hold raw API keys in code. Instead, store the key with ctx.secrets and name it in the call: the platform injects the header server-side and your plugin never sees the value. The secret must be bound to the destination domain (set its allowed domains in the Dev Portal), so a leaked plugin can't replay it elsewhere. Requires storage:secrets.
# Authorization: Bearer <value of MY_API_KEY> is injected by the platform
resp = ctx.http.request("GET", "https://api.example.com/v1/me",
secret_auth="MY_API_KEY")
# Non-bearer schemes
resp = ctx.http.request("POST", "https://api.example.com/v1/charge",
body='{"amount": 5}',
auth={"scheme": "basic", "secret": "MY_API_KEY"})
Note secret_auth and auth live on ctx.http.request(...); the get/post shorthands do not take them.
WebSockets v0.8.0
For live feeds a request-per-poll is the wrong shape. ctx.ws keeps a persistent two-way connection open to a host you declared, held by the platform's broker (your sandbox still has no network access) and reconnected automatically. Capability: proxy:websocket.
@plugin.on_slash_command("feed-start")
def start_feed(ctx, event):
# Idempotent: call it whenever you need the connection alive.
ctx.ws.ensure(
"prices",
"wss://stream.example.com/v1",
secret_auth="STREAM_KEY", # optional, injected on connect
subscribe=['{"op": "subscribe", "channel": "ticker"}'],
)
ctx.interaction.respond(content="Feed running.")
@plugin.on_ws_message("prices")
def on_frame(ctx, msg):
# msg = {"name", "conn_id", "data", "binary"}; binary data is base64
ctx.kv.set("last_tick", msg["data"])
@plugin.on_ws_close("prices")
def on_close(ctx, msg):
ctx.log("price feed closed", level="warning")
ensure(name, url, *, secret_auth=None, auth=None, subscribe=None, binary=False)opens or confirms the named connection.subscribeframes are re-sent after every reconnect, so subscriptions survive drops. Setbinary=Truefor binary protocols; inbound binary frames arrive base64-encoded.ctx.ws.send(name, data)sends one frame (stris a text frame,bytesa binary frame).ctx.ws.close(name)hangs up.- Handlers:
@plugin.on_ws_message(name),on_ws_open,on_ws_close. Since v0.8.2 a wildcard like"rustplus:*"matches every connection sharing the prefix; the concretenameis in the frame. Frames for one connection arrive in order. A broker error surfaces as a close, there is no separate error handler. - Hosts: the exact host must be in your
proxy_domains_requested. When the server admin supplies the address at setup time (their own game server, say), callctx.ws.allow_host(host)from a slash handler run by that admin; the platform verifies the member is an admin and the host is public, then remembers it for that server.ctx.ws.revoke_host(host)undoes it. - Tier and quotas:
proxy:websocketis a staff-reviewed capability. Unlike HTTP, the WebSocket allow-list matches the exact host (no automatic subdomains). Budgets per (server, plugin): 6 connects/min and a single 4 MB/min traffic budget shared across both directions. - Local testing:
MockContext.wsrecordsallowed_hostsandrevoked_hosts, andyourbot validatedetectsctx.wsusage so the capability isn't missing at upload.
Metriken v0.5.0
Zeitreihenzähler mit optionalen Tags. Werden im Dashboard deines Plugins über ctx.metrics.query angezeigt.
ctx.metrics.record("commands_used") # +1
ctx.metrics.record("xp_awarded", value=25)
ctx.metrics.record("commands_used", tags={"command": "ban"})
# Read back
trend = ctx.metrics.query("commands_used", period="7d", group_by="command")
# {"labels": [...], "series": [{"name": "...", "data": [...]}], "total": float}
total = ctx.metrics.total("xp_awarded", period="30d")
period akzeptiert 1h, 24h, 7d, 30d, 90d. Aggregate: sum (Standard), avg, max, min.
Limits: 1,000,000 stored data points per (server, plugin), 10 tag pairs per record (extras are silently dropped), tag values truncated to 128 chars, 90-day query window.
Unlike ctx.log, metrics calls are blocking RPCs that can raise (RateLimitError, RpcTimeoutError). If you record in the hot path of a command handler, wrap the call in try/except so a transient metrics failure can't break the user-visible work.
Protokollierung
Plugin-Logs erscheinen im Entwicklerportal-Log-Viewer mit vollständigem Zeitstempel, Schweregrad und Tags.
ctx.log("Plugin booted")
ctx.log("User joined", level="info", tags=["onboarding"])
ctx.log("Discord error: " + str(exc), level="error", tags=["discord"])
ctx.log("Custom payload", request_id=ctx.request_id, user_id=event["user_id"])
Levels: info (default), warning, error. Messages cap at 4000 chars, up to 10 tags, and any extra **kwargs (up to 20, 500 chars per value) are stored as JSON detail. ctx.log is fire-and-forget: it does not wait for or check a response.
Inside an interaction handler, ctx.request_id returns a stable correlation ID for the current event — thread it through your log calls so one user action can be grepped as a unit.
SDK v0.8.4 · Zuletzt aktualisiert Juli 2026 · Zurück zum Entwicklerportal