← Documentation  /  Guide du développeur

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.

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

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

ExceptionLevée quand
SdkErrorClasse de base — interceptez-la pour gérer tout ce qui provient du SDK. SDK_ERROR
CapabilityErrorVous avez appelé une API que votre plugin n'avait pas demandée via capabilities_required. CAPABILITY_DENIED
RateLimitErrorQuota dépassé. Possède .retry_after (en secondes). RATE_LIMITED / QUOTA_EXCEEDED
DiscordApiErrorLe REST de Discord a renvoyé un code non-2xx. Possède .status_code. DISCORD_API_ERROR
SdkPermissionErrorLe bot n'a pas la permission Discord requise sur le serveur. Possède .permission (ex. "manage_channels"). BOT_MISSING_PERMISSION
ValidationErrorVous avez passé des arguments invalides (channel_id vide, emoji incorrect, clé avec des octets nuls…). VALIDATION_ERROR
KvQuotaErrorHit 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
RpcTimeoutErrorLe runner n'a pas répondu dans le délai imparti par appel. RPC_TIMEOUT
PermissionError aliasAlias rétrocompatible pour SdkPermissionError.
TimeoutError aliasAlias 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 par ctx.http via 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 dans ctx.kv / ctx.sql où 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_ready fires per server, not per boot. It runs once per (worker, server) right before that server's first event reaches you, with a tenant-scoped ctx — the designed place for per-server init like KV defaults or SQL schema bootstrap.
  • Schedules run server-side, from your manifest. Declare each @plugin.cron task 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.schedule tasks do not run pooled — use cron or the event-driven pattern below.
  • Module globals are unreliable. The Context is rebuilt per event, your plugin runs on more than one worker, and workers restart freely. Anything that must survive belongs in ctx.kv or ctx.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 validate checks 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_update on, 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 grossvous conservez
Up to $1,00070%
$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