← Documentação  /  Guia do Desenvolvedor

Construir um plugin

A referência completa do SDK para desenvolvedores de plugins — Python em sandbox, distribuído como zip, disponibilizado através do 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

Plugins são processos Python em sandbox. Escreva um handler, declare suas capacidades, envie um zip — executamos em um container Docker bloqueado e transmitimos eventos do Discord para ele via JSON-RPC.

Tratamento de erros documentado

O SDK levanta exceções tipadas em vez de retornar dicionários de erro. Todas herdam de SdkError para que você possa capturar amplamente ou especificamente.

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")

Referência

ExceçãoLevantado quando
SdkErrorClasse base — capture isso para lidar com qualquer coisa do SDK. SDK_ERROR
CapabilityErrorVocê chamou uma API que seu plugin não solicitou via capabilities_required. CAPABILITY_DENIED
RateLimitErrorCota excedida. Possui .retry_after (segundos). RATE_LIMITED / QUOTA_EXCEEDED
DiscordApiErrorREST do Discord retornou não-2xx. Possui .status_code. DISCORD_API_ERROR
SdkPermissionErrorBot está faltando uma permissão de servidor Discord. Possui .permission (ex. "manage_channels"). BOT_MISSING_PERMISSION
ValidationErrorVocê passou argumentos inválidos (channel_id vazio, emoji ruim, chave com bytes nulos…). 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
RpcTimeoutErrorO runner não respondeu dentro do timeout por chamada. RPC_TIMEOUT
PermissionError aliasAlias compatível com versões anteriores para SdkPermissionError.
TimeoutError aliasAlias compatível com versões anteriores para RpcTimeoutError.

Testando documentado

Plugins são testados como qualquer outro código Python — sem Docker, sem conexão de plataforma, sem mocks do Discord. Importe de 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

O que MockContext registra

Todo efeito colateral do Discord é capturado para asserção. Leia estes como listas de dicts na ordem em que foram chamados:

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.

Mock de HTTP de saída

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 fornece padrões sensatos para todos os 16 tipos de evento — passe substituições como kwargs.

Recursos

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.

Chamar uma API que seu plugin não requisitou levanta CapabilityError em tempo de execução — o runner bloqueia a chamada, você não sofre danos parciais. O usuário vê um prompt "precisa de permissão" e pode concedê-la sem desinstalar.

Sandbox — o que garante

Plugins do Marketplace são executados em contêineres Docker isolados. O sandbox não é uma limitação imposta a você — é um conjunto de garantias feitas em seu nome.

O que o sandbox garante a você

  • Seu plugin não pode vazar tokens de usuário, DMs de cliente ou segredos da plataforma. O ambiente está vazio — sem variáveis de ambiente, sem credenciais de BD, sem tokens de bot do Discord. Se seu código for comprometido, o raio de explosão é exatamente o que o usuário consentiu em instalar. Você não está a uma dependência ruim de distância de um incidente de segurança.
  • Seu plugin não pode derrubar a plataforma para outros usuários. Se seu plugin entrar em espiral, apenas seu plugin morre; o resto do servidor continua funcionando.
  • Os bugs de um plugin não podem se tornar os bugs de outro plugin. Cada plugin tem seu próprio contêiner, seu próprio namespace KV, seu próprio schema SQL. Você não pode ler acidentalmente dados de outro plugin, e outro plugin não pode corromper os seus.
  • Clientes não precisam auditar seu código para confiar em seu plugin. O seletor de capacidade na instalação mostra a eles exatamente o que seu plugin pode tocar — sem importações misteriosas, sem efeitos colaterais surpresa. É isso que faz o marketplace funcionar.

Como essas garantias são impostas

  • Rede: --network none. Apenas o pipe JSON-RPC para o runner é alcançável. Todo HTTP passa por ctx.http via proxy da plataforma — você declara os domínios que precisa, clientes os veem na instalação.
  • Filesystem: raiz somente leitura, /tmp efêmero, sem persistência entre reinicializações. O estado reside em ctx.kv / ctx.sql onde backups e auditorias podem vê-lo.
  • Memória: limite rígido de 64 MB por worker. Um vazamento de um plugin não derruba os outros.
  • CPU: 0,25 vCPU. Trabalho em segundo plano não vai negligenciar outros plugins.
  • Processos: máximo 32 (PIDs). Limita danos de fork-bomb.
  • Usuário: não-root (nobody, uid 65534). Sem caminhos de escalação de privilégios.
  • Segredos: env está vazio. Não há nada para vazar mesmo que você quisesse.
  • Limites de taxa por (servidor, plugin): 50 eventos/seg, 60 ações Discord de saída/min, 30 requisições HTTP proxy/min. Um servidor não pode fazer DoS no seu plugin inundando-o.

Se seu plugin ultrapassa um limite, recebe RateLimitError com uma dica retry_after — recue, não faça busy-loop.

Relatórios de falha

  • Quarantine: 5 consecutive crashes within 5 minutes stops event delivery. Restart the plugin from the dashboard to clear it.

Modo 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.

Publicar & revisar

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 valida: campos obrigatórios presentes, capacidades legais, comandos slash bem formados.
  • Código não importa nenhum módulo não permitido (a sandbox os bloqueia mesmo assim, mas a verificação prévia detecta antes que os usuários vejam erros).
  • Sem SQL não parametrizado, sem interpolação f-string em 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.

O tempo é de 1 a 3 dias úteis. Se negado, você verá feedback específico e acionável no portal dev.

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.

Nomeie seu 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.

Criar sua primeira 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.

planos pagos

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.

O que você faz

Your trailing 30-day grossvocê retém
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 · Última atualização em julho de 2026 · Voltar ao Dev Portal