Créer un plugin
La référence complète du SDK pour les développeurs de plugins — Python sandboxé, livré sous forme de zip, distribué via la marketplace.
Les plugins sont des processus Python isolés. Écrivez un gestionnaire, déclarez vos capacités, envoyez un zip — nous l'exécutons dans un conteneur Docker verrouillé et lui transmettons les événements Discord via JSON-RPC.
Livre de recettes
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) Accueillir les nouveaux membres
Saluez chaque nouveau membre dans un canal qu'un administrateur configure 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) Rôles par réaction (cliquez sur ✓ pour obtenir un rôle)
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 utilisateurs par messages
Suivez le nombre de messages par utilisateur ; `/leaderboard` affiche le 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) Statut AFK
/afk <raison> définit le statut de l'utilisateur ; le bot répond lorsqu'il est mentionné.
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) Commandes personnalisées stockées dans KV
Les admins du serveur ajoutent leurs propres commandes !raccourci sans redéployer.
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) Sondages basés sur les réactions
/poll crée un message avec des réactions ✓/✗ ; le décompte se fait via le nombre de réactions ultérieurement.
@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) Journal de messages
Enregistre chaque message non-bot dans un salon de modération configurable (utile pour les pistes d'audit).
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 — carte de statistiques du serveur
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 rôles (liste déroulante)
L'administrateur exécute /role-menu et obtient une liste déroulante des rôles auto-assignables.
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) Triage de tickets : /ticket ouvre un fil
L'utilisateur exécute /ticket <subject> ; le bot crée un fil privé en mentionnant le rôle de support.
@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) Réponse automatique aux mots-clés
Déclenche une réponse lorsqu'un message contient un mot-clé configuré. Utile pour les FAQ.
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) Rappels d'anniversaires
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) Membre de la semaine
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) Suivi du temps en salon vocal
Suit la durée passée par chaque utilisateur dans les salons vocaux. Utile pour les récompenses basées sur l'activité.
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 · Dernière mise à jour juillet 2026 · Retour au portail développeur