Crea un plugin
La referencia completa del SDK para desarrolladores de plugins — Python en sandbox, entregado como zip, distribuido a través del marketplace.
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 validatebefore 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_commandsentry — 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 theevents:message_contentcapability. - Reply visibility: the platform acknowledges slash commands publicly before your handler runs, so
ephemeral=Trueon 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