← Documentación  /  Guía del desarrollador

Crea un plugin

La referencia completa del SDK para desarrolladores de plugins — Python en sandbox, entregado como zip, distribuido a través del 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

Los plugins son procesos Python en sandbox. Escribe un handler, declara tus capacidades, envía un zip — lo ejecutamos en un contenedor Docker restringido y transmitimos eventos de Discord mediante JSON-RPC.

Recetario

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) Dar la bienvenida a nuevos miembros

Saluda a cada nuevo miembro en un canal que un administrador configura mediante /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) Roles por reacción (haz clic en ✓ para obtener un rol)

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 usuarios por mensajes

Registra el recuento de mensajes por usuario; `/leaderboard` publica el 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) Estado AFK

/afk <reason> establece el estado del usuario; el bot responde cuando se le menciona.

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 personalizados almacenados en KV

Los administradores del servidor añaden sus propios comandos !shortcut sin necesidad de redesplegar.

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) Encuestas basadas en reacciones

/poll crea un mensaje con reacciones ✓/✗; los recuentos se obtienen más tarde por conteo de reacciones.

@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 mensajes

Registra cada mensaje que no sea de bot en un canal de moderación configurable (útil para auditorías).

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 — tarjeta de estadísticas del 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) Menú de roles (desplegable)

El administrador ejecuta /role-menu y obtiene un menú desplegable con los roles autoasignables.

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) Gestión de tickets: /ticket abre un hilo

El usuario ejecuta /ticket <asunto>; el bot crea un hilo privado etiquetando el rol de soporte.

@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) Respuesta automática a palabras clave

Activa una respuesta cuando un mensaje contiene una palabra clave configurada. Útil para preguntas frecuentes.

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) Recordatorios de cumpleaños

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) Miembro de la 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 tiempo en canales de voz

Rastrea cuánto tiempo pasa cada usuario en los canales de voz. Útil para recompensas basadas en actividad.

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 actualización: julio de 2026 · Volver al Portal de desarrolladores