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

Livro de Receitas

Copy-pasteable starter snippets for the most common plugin patterns. Recipes 1 and 2 are complete files; later recipes show only the handlers, so paste them into the scaffold from yourbot new and keep plugin.run() at the bottom. Add the event-type imports each snippet names.

  • Capabilities are auto-detected from the SDK calls in your code at upload. Run yourbot validate before pushing to confirm nothing is missing.
  • Slash commands: command names are auto-registered from your decorators, but descriptions and options are not. Any command that takes arguments needs a slash_commands entry — the block in Manifest declarations covers every recipe here.
  • Rich payloads are privacy-gated: the default install mode strips events down to IDs and timestamps. Recipes that read content (5, 6, 8, 12) or reaction emoji (2) need the events:message_content capability.
  • Reply visibility: the platform acknowledges slash commands publicly before your handler runs, so ephemeral=True on a slash reply is not honored (it works on component interactions, or with "defer_on_dispatch": false).
  • Mentions don't ping: <@id> in a channel message renders as a mention but sends no notification (anti-mass-ping guard). Interaction replies can ping.
  • Scheduled work: production schedules come from the manifest "cron" array (Build: Cron schedules, 5-minute floor). Recipes 3, 13 and 14 use the event-driven daily-gate pattern instead, which needs no manifest entry and also works below the floor.

Manifest declarations for these recipes

One block covers every slash command below — copy the entries you use into your manifest.json:

"slash_commands": [
  {"name": "welcome-channel", "description": "Make this channel the welcome channel"},
  {"name": "react-role-bind", "description": "Bind an emoji on a message to a role", "options": [
    {"name": "message_id", "description": "Message to watch", "type": 3, "required": true},
    {"name": "emoji", "description": "Emoji to watch for", "type": 3, "required": true},
    {"name": "role", "description": "Role to grant", "type": 8, "required": true}]},
  {"name": "leaderboard", "description": "Top 10 talkers"},
  {"name": "afk", "description": "Set your AFK status", "options": [
    {"name": "reason", "description": "Why you are away", "type": 3, "required": false}]},
  {"name": "addcmd", "description": "Add a custom text command", "options": [
    {"name": "name", "description": "Shortcut name (no !)", "type": 3, "required": true},
    {"name": "response", "description": "Reply text", "type": 3, "required": true}]},
  {"name": "poll", "description": "Post a yes/no poll", "options": [
    {"name": "question", "description": "Poll question", "type": 3, "required": true}]},
  {"name": "stats", "description": "Server stat card"},
  {"name": "role-menu", "description": "Show the self-assign role menu"},
  {"name": "ticket", "description": "Open a support ticket", "options": [
    {"name": "subject", "description": "What do you need help with?", "type": 3, "required": true}]},
  {"name": "birthday", "description": "Save your birthday", "options": [
    {"name": "date", "description": "MM-DD", "type": 3, "required": true}]}
]

1) Bem-vindo a novos membros

Cumprimente cada novo membro em um canal que um admin configura via /welcome-channel.

from yourbot_sdk import Plugin, Context
from yourbot_sdk.events import MemberJoin

plugin = Plugin()

@plugin.on_event("member_join")
def greet(ctx: Context, event: MemberJoin):
    channel_id = ctx.kv.get("welcome_channel_id")
    if not channel_id:
        return
    name = event.get("display_name") or event.get("username") or "friend"
    ctx.discord.send_message(channel_id=str(channel_id),
        content=f"Welcome, **{name}**! :wave:")

@plugin.on_slash_command("welcome-channel")
def set_channel(ctx: Context, event: dict):
    ctx.kv.set("welcome_channel_id", event["channel_id"])
    ctx.interaction.respond(content="This is now the welcome channel.")

plugin.run()

2) Papéis de reação (clique ✓ para obter um papel)

Admin posts a message, configures the role+emoji pair, and any user who reacts gets the role. Reaction events carry custom emoji as their bare name, so normalize the stored form before comparing. Needs events:message_content: without it the default privacy mode strips emoji and user_bot from reaction events.

from yourbot_sdk import Plugin, Context
from yourbot_sdk.events import ReactionAdd

plugin = Plugin()

@plugin.on_event("reaction_add")
def grant_role(ctx: Context, event: ReactionAdd):
    if event.get("user_bot"):
        return  # ignore bot reactions, including our own
    cfg = ctx.kv.get(f"react_role:{event['message_id']}")
    if not cfg:
        return
    want = cfg["emoji"]
    if want.startswith("<"):   # "<:pepega:1234>" -> "pepega"
        want = want.split(":")[1]
    if event["emoji"] == want:
        ctx.discord.add_role(user_id=event["user_id"], role_id=cfg["role_id"])

@plugin.on_slash_command("react-role-bind")
def bind(ctx: Context, event: dict):
    opts = {o["name"]: o["value"] for o in event.get("options", [])}
    ctx.kv.set(f"react_role:{opts['message_id']}",
               {"emoji": opts["emoji"], "role_id": opts["role"]})
    ctx.interaction.respond(content="Reaction role bound.")

plugin.run()

3) Daily announcement (the daily-gate pattern)

Two ways to run daily work. The wall-clock way: declare the task in your manifest's "cron" array and handle it with @plugin.cron (Build: Cron schedules). The zero-manifest way shown here: piggyback on message traffic and gate with a once-per-day flag — the recap posts with the first message after 09:00 UTC, so quiet servers skip it for free. Recipes 13 and 14 reuse this pattern.

from datetime import datetime, timezone

@plugin.on_event("message_create")
def count(ctx: Context, event: dict):
    if event.get("author_bot"):
        return
    ctx.metrics.record("messages")
    _maybe_recap(ctx, event["channel_id"])

def _maybe_recap(ctx: Context, fallback_channel: str):
    now = datetime.now(timezone.utc)
    if now.hour < 9:
        return
    if not ctx.ephemeral.dedup(f"recap:{now:%Y-%m-%d}", ttl_seconds=86400):
        return  # already posted today
    channel_id = ctx.kv.get("recap_channel_id") or fallback_channel
    total = int(ctx.metrics.total("messages", period="24h"))
    ctx.discord.send_message(channel_id=str(channel_id),
        content=f"Good morning! {total} messages in the last 24 hours.")

4) /leaderboard top 10 usuários por mensagens

Rastreie contagem de mensagens por usuário; `/leaderboard` posta os top 10.

from yourbot_sdk.events import MessageCreate

@plugin.on_event("message_create")
def count(ctx: Context, event: MessageCreate):
    if event.get("author_bot"):
        return
    ctx.kv.increment(f"msgs:{event['author_id']}")

@plugin.on_slash_command("leaderboard")
def show(ctx: Context, event: dict):
    counts = ctx.kv.list_values(prefix="msgs:", limit=100) or {}  # {key: value}
    top = sorted(counts.items(), key=lambda kv: int(kv[1] or 0), reverse=True)[:10]
    lines = [f"{i+1}. <@{key.split(':')[1]}>: {n}" for i, (key, n) in enumerate(top)]
    ctx.interaction.respond(content="**Top 10 talkers**\n" + "\n".join(lines))

list_values returns at most 100 pairs, so on a server with more than 100 tracked users the true top talker can fall outside the scan. For big servers keep the counts in ctx.sql instead.

5) Status AFK

/afk <razão> define o status do usuário; o bot responde quando são mencionados.

import re

from yourbot_sdk.events import MessageCreate

@plugin.on_slash_command("afk")
def set_afk(ctx: Context, event: dict):
    opts = {o["name"]: o["value"] for o in event.get("options", [])}
    reason = opts.get("reason") or "afk"
    ctx.kv.set(f"afk:{event['user_id']}", reason, ttl_seconds=86400)
    ctx.interaction.respond(content=f"Got it, set you AFK ({reason}).")

@plugin.on_event("message_create")
def notify_mentions(ctx: Context, event: MessageCreate):
    if event.get("author_bot"):
        return
    for uid in set(re.findall(r"<@!?(\d+)>", event.get("content", ""))):
        reason = ctx.kv.get(f"afk:{uid}")
        if reason:
            ctx.discord.send_message(channel_id=event["channel_id"],
                content=f"<@{uid}> is AFK: {reason}")

6) Comandos customizados armazenados em KV

Admins do servidor adicionam seus próprios comandos !shortcut sem reimplantar.

from yourbot_sdk.events import MessageCreate

@plugin.on_event("message_create")
def custom(ctx: Context, event: MessageCreate):
    content = event.get("content", "").strip()
    if not content.startswith("!"):
        return
    name = content[1:].split(" ", 1)[0]
    response = ctx.kv.get(f"cmd:{name}")
    if response:
        ctx.discord.send_message(channel_id=event["channel_id"], content=str(response))

@plugin.on_slash_command("addcmd")
def add(ctx: Context, event: dict):
    opts = {o["name"]: o["value"] for o in event.get("options", [])}
    ctx.kv.set(f"cmd:{opts['name']}", opts["response"])
    ctx.interaction.respond(content=f"Saved !{opts['name']}.")

7) Pesquisas baseadas em reação

/poll cria uma mensagem com reações ✓/✗; totaliza a contagem de reações depois.

@plugin.on_slash_command("poll")
def poll(ctx: Context, event: dict):
    opts = {o["name"]: o["value"] for o in event.get("options", [])}
    question = opts.get("question") or "Yes or no?"
    msg = ctx.discord.send_message(channel_id=event["channel_id"], content=f"**Poll:** {question}")
    ctx.discord.add_reaction(channel_id=event["channel_id"], message_id=msg["message_id"], emoji="✅")
    ctx.discord.add_reaction(channel_id=event["channel_id"], message_id=msg["message_id"], emoji="❌")
    ctx.interaction.respond(content="Poll posted!")

8) Registrador de mensagens

Registra todas as mensagens que não são de bot em um canal mod configurável (útil para trilhas de auditoria).

from yourbot_sdk.events import MessageCreate

@plugin.on_event("message_create")
def log(ctx: Context, event: MessageCreate):
    if event.get("author_bot"):
        return
    log_channel = ctx.kv.get("mod_log_channel_id")
    if not log_channel:
        return
    text = event.get("content", "")[:200]
    ctx.discord.send_message(channel_id=str(log_channel),
        content=f"[#{event.get('channel_name', '?')}] <@{event['author_id']}>: {text}")

9) /stats — card de estatísticas do servidor

Show message and join counts. The two counter handlers keep the numbers the command reads; reset them with the daily-gate pattern from recipe 3 if you want true per-day figures.

@plugin.on_event("message_create")
def count_messages(ctx: Context, event: dict):
    if not event.get("author_bot"):
        ctx.kv.increment("messages_today_total")

@plugin.on_event("member_join")
def count_joins(ctx: Context, event: dict):
    ctx.kv.increment("new_members_today")

@plugin.on_slash_command("stats")
def stats(ctx: Context, event: dict):
    msgs_today = ctx.kv.get("messages_today_total") or 0
    new_today = ctx.kv.get("new_members_today") or 0
    ctx.interaction.respond(content=(
        f"**Server stats**\n"
        f"Messages today: {msgs_today}\n"
        f"New members today: {new_today}"
    ))

10) Menu de funções (dropdown)

Admin executa /role-menu, recebe um dropdown listando funções auto-atribuíveis.

from yourbot_sdk import ActionRow, SelectMenu, SelectOption

@plugin.on_slash_command("role-menu")
def menu(ctx: Context, event: dict):
    roles = ctx.kv.get("self_roles") or []  # list of {"id": "...", "label": "..."}
    options = [SelectOption(label=r["label"], value=r["id"]) for r in roles]
    ctx.interaction.respond(content="Pick your role(s):",
        components=[ActionRow(SelectMenu("pick_role", options, min_values=1, max_values=len(options)))])

@plugin.on_component("pick_role")
def picked(ctx: Context, event: dict):
    selected_ids = event.get("values") or []
    for rid in selected_ids:
        ctx.discord.add_role(user_id=event["user_id"], role_id=rid)
    ctx.interaction.respond(content="Roles updated.", ephemeral=True)

11) Triagem de tickets: /ticket abre uma thread

Usuário executa /ticket <assunto>; o bot cria uma thread privada marcando a função de suporte.

@plugin.on_slash_command("ticket")
def open_ticket(ctx: Context, event: dict):
    opts = {o["name"]: o["value"] for o in event.get("options", [])}
    subject = opts.get("subject") or "Support request"
    thread = ctx.discord.create_thread(
        channel_id=event["channel_id"],
        name=f"ticket-{event['user_id']}"[:100],
        thread_type=12,   # 12 = private thread
    )
    ctx.discord.send_message(channel_id=thread["id"],
        content=f"**Ticket from <@{event['user_id']}>**: {subject}")
    ctx.interaction.respond(content="Ticket opened. Staff will reply shortly.")

12) Resposta automática a palavras-chave

Dispara uma resposta quando uma mensagem contém uma palavra-chave configurada. Útil para FAQs.

from yourbot_sdk.events import MessageCreate

@plugin.on_event("message_create")
def autoresponder(ctx: Context, event: MessageCreate):
    if event.get("author_bot"):
        return
    content = event.get("content", "").lower()
    triggers = ctx.kv.get("keyword_triggers") or {}  # {keyword: reply}
    for kw, reply in (triggers.items() if isinstance(triggers, dict) else []):
        if kw.lower() in content:
            ctx.discord.send_message(channel_id=event["channel_id"], content=str(reply))
            break  # one auto-reply per message

13) Lembretes de aniversário

Users register their birthday via /birthday MM-DD; the daily gate (recipe 3) announces the day's birthdays with the first message after midnight UTC. The mentions render but do not ping (anti-mass-ping guard).

from datetime import datetime, timezone

@plugin.on_slash_command("birthday")
def set_bday(ctx: Context, event: dict):
    opts = {o["name"]: o["value"] for o in event.get("options", [])}
    ctx.kv.set(f"bday:{event['user_id']}", opts.get("date"))
    ctx.interaction.respond(content=f"Saved {opts.get('date')}.")

@plugin.on_event("message_create")
def announce(ctx: Context, event: dict):
    today = datetime.now(timezone.utc).strftime("%m-%d")
    stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    if not ctx.ephemeral.dedup(f"bday_check:{stamp}", ttl_seconds=86400):
        return  # already checked today
    channel_id = ctx.kv.get("birthday_channel_id")
    if not channel_id:
        return
    bdays = ctx.kv.list_values(prefix="bday:") or {}   # {key: "MM-DD"}
    user_ids = [k.split(":")[1] for k, d in bdays.items() if d == today]
    if not user_ids:
        return
    mentions = " ".join(f"<@{uid}>" for uid in user_ids)
    ctx.discord.send_message(channel_id=str(channel_id),
        content=f"🎂 Happy birthday {mentions}!")

14) Membro da semana

Count messages per user, then announce the week's top sender on Sunday after 09:00 UTC using the daily-gate pattern. The winner mention renders without pinging.

from datetime import datetime, timezone

@plugin.on_event("message_create")
def count_week(ctx: Context, event: dict):
    if event.get("author_bot"):
        return
    ctx.kv.increment(f"msgs_week:{event['author_id']}")
    now = datetime.now(timezone.utc)
    if now.weekday() != 6 or now.hour < 9:   # Sunday, after 09:00 UTC
        return
    if not ctx.ephemeral.dedup(f"mvp:{now:%Y-%m-%d}", ttl_seconds=86400):
        return
    counts = ctx.kv.list_values(prefix="msgs_week:") or {}   # {key: count}
    if not counts:
        return
    top_key, top_count = max(counts.items(), key=lambda kv: int(kv[1] or 0))
    uid = top_key.split(":", 1)[1]
    channel_id = ctx.kv.get("announce_channel_id")
    if channel_id:
        ctx.discord.send_message(channel_id=str(channel_id),
            content=f"🏆 Member of the week: <@{uid}> with {top_count} messages.")
    for key in counts:   # reset weekly counters
        ctx.kv.delete(key)

15) Rastreador de tempo do canal de voz

Rastreia quanto tempo cada usuário passa em canais de voz. Útil para recompensas baseadas em atividade.

from yourbot_sdk.events import VoiceStateUpdate
import time

@plugin.on_event("voice_state_update")
def track(ctx: Context, event: VoiceStateUpdate):
    uid = event["user_id"]
    if event.get("after_channel_id") and not event.get("before_channel_id"):
        # joined voice
        ctx.kv.set(f"voice_in:{uid}", str(int(time.time())))
    elif event.get("before_channel_id") and not event.get("after_channel_id"):
        # left voice
        join_t = ctx.kv.get(f"voice_in:{uid}")
        if join_t:
            duration = int(time.time()) - int(join_t)
            ctx.kv.increment(f"voice_secs:{uid}", amount=duration)
            ctx.kv.delete(f"voice_in:{uid}")

Tip: capabilities and command names are auto-detected from these snippets, so a fresh yourbot new my_plugin plus one recipe uploads cleanly. Commands that take arguments still need their slash_commands options declared — copy them from Manifest declarations. Run yourbot validate before pushing to catch any gaps.

SDK v0.8.4 · Última atualização em julho de 2026 · Voltar ao Dev Portal