Créer un plugin
La référence complète du SDK pour les développeurs de plugins — Python sandboxé, livré sous forme de zip, distribué via la marketplace.
Les plugins sont des processus Python isolés. Écrivez un gestionnaire, déclarez vos capacités, envoyez un zip — nous l'exécutons dans un conteneur Docker verrouillé et lui transmettons les événements Discord via JSON-RPC.
Gestion des erreurs documentée
Le SDK lève des exceptions typées plutôt que de renvoyer des dictionnaires d'erreur. Toutes héritent de SdkError, vous pouvez donc les intercepter de manière large ou ciblée.
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")
Référence
| Exception | Levée quand |
|---|---|
| SdkError | Classe de base — interceptez-la pour gérer tout ce qui provient du SDK. SDK_ERROR |
| CapabilityError | Vous avez appelé une API que votre plugin n'avait pas demandée via capabilities_required. CAPABILITY_DENIED |
| RateLimitError | Quota dépassé. Possède .retry_after (en secondes). RATE_LIMITED / QUOTA_EXCEEDED |
| DiscordApiError | Le REST de Discord a renvoyé un code non-2xx. Possède .status_code. DISCORD_API_ERROR |
| SdkPermissionError | Le bot n'a pas la permission Discord requise sur le serveur. Possède .permission (ex. "manage_channels"). BOT_MISSING_PERMISSION |
| ValidationError | Vous avez passé des arguments invalides (channel_id vide, emoji incorrect, clé avec des octets nuls…). 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 | Le runner n'a pas répondu dans le délai imparti par appel. RPC_TIMEOUT |
| PermissionError alias | Alias rétrocompatible pour SdkPermissionError. |
| TimeoutError alias | Alias rétrocompatible pour RpcTimeoutError. |
Tests documentée
Les plugins se testent comme n'importe quel code Python — pas de Docker, pas de connexion à la plateforme, pas de mocks Discord. Importez depuis 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
Ce que MockContext enregistre
Chaque effet de bord Discord est capturé pour les assertions. Lisez-les sous forme de listes de dicts dans l'ordre où ils ont été appelés :
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.
Simuler les requêtes HTTP sortantes
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 fournit des valeurs par défaut sensées pour les 16 types d'événements — passez des remplacements en kwargs.
Capacités
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.
Appeler une API que votre plugin n'a pas déclarée lève une CapabilityError à l'exécution — le runner bloque l'appel, vous n'obtenez pas de dommages partiels. L'utilisateur voit une invite « nécessite une permission » et peut l'accorder sans désinstaller.
Sandbox — ce qu'elle garantit
Les plugins de la Marketplace s'exécutent dans des conteneurs Docker isolés. La sandbox n'est pas une limitation imposée — c'est un ensemble de garanties offertes en votre nom.
Ce que la sandbox vous garantit
- Votre plugin ne peut pas faire fuiter les tokens des utilisateurs, les DM des clients ou les secrets de la plateforme. L'environnement est vide — pas de variables d'environnement, pas d'identifiants de base de données, pas de tokens de bot Discord. Si votre code est compromis, le rayon d'impact est exactement ce que l'utilisateur a consenti à installer. Vous n'êtes pas à une mauvaise dépendance d'un incident de sécurité.
- Votre plugin ne peut pas faire planter la plateforme pour les autres utilisateurs. Si votre plugin s'emballe, seul votre plugin s'arrête ; le reste du serveur continue de fonctionner.
- Les bugs d'un plugin ne peuvent pas devenir les bugs d'un autre plugin. Chaque plugin dispose de son propre conteneur, de son propre espace de noms KV, de son propre schéma SQL. Vous ne pouvez pas lire accidentellement les données d'un autre plugin, et un autre plugin ne peut pas corrompre les vôtres.
- Les clients n'ont pas besoin d'auditer votre code pour faire confiance à votre plugin. Le sélecteur de capacités lors de l'installation leur montre exactement ce que votre plugin peut toucher — pas d'imports mystérieux, pas d'effets de bord surprenants. C'est ce qui fait fonctionner la Marketplace.
Comment ces garanties sont appliquées
- Réseau :
--network none. Seul le pipe JSON-RPC vers le runner est accessible. Tout le HTTP passe parctx.httpvia le proxy de la plateforme — vous déclarez les domaines dont vous avez besoin, les clients les voient lors de l'installation. - Système de fichiers : racine en lecture seule,
/tmpéphémère, aucune persistance entre les redémarrages. L'état réside dansctx.kv/ctx.sqloù les sauvegardes et les audits peuvent le consulter. - Mémoire : limite stricte de 64 Mo par worker. La fuite d'un plugin n'entraîne pas les autres.
- CPU : 0,25 vCPU. Les tâches en arrière-plan n'affament pas les autres plugins.
- Processus : 32 maximum (PIDs). Limite les dommages des fork-bombs.
- Utilisateur : non-root (
nobody, uid 65534). Aucun chemin d'escalade de privilèges. - Secrets : env est vide. Il n'y a rien à divulguer, même si vous le vouliez.
- Limites de débit par (serveur, plugin) : 50 événements/s, 60 actions Discord sortantes/min, 30 requêtes HTTP proxy/min. Un serveur ne peut pas mettre votre plugin hors service en le submergeant.
Si votre plugin dépasse une limite, il reçoit RateLimitError avec un indice retry_after — reculez, n'entrez pas dans une boucle active.
Rapports de plantage
- Quarantine: 5 consecutive crashes within 5 minutes stops event delivery. Restart the plugin from the dashboard to clear it.
Mode bot
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.
Publier & réviser
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:
- Le manifeste est valide : champs obligatoires présents, capacités légales, commandes slash bien formées.
- Le code n'importe aucun module interdit (le bac à sable les bloque de toute façon, mais la vérification préalable le détecte avant que les utilisateurs ne voient des erreurs).
- Pas de SQL non paramétré, pas d'interpolation f-string dans
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.
Le délai de traitement est de 1–3 jours ouvrables. En cas de refus, vous verrez des retours précis et exploitables dans le portail développeur.
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.
Nommez votre 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.
Créer votre première FAQ
- 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.
plans payants
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.
Ce que vous faites
| Your trailing 30-day gross | vous conservez |
|---|---|
| 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 · Dernière mise à jour juillet 2026 · Retour au portail développeur