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