← Documentation  /  Guide du développeur

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.

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

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