Crée ton bot,
vis-le en temps réel
Programme un bot Python directement dans ton navigateur. Pas de serveur à gérer, juste du code et de la créativité.
📚 Explorer la documentation
🌟 Ce que tu peux créer
🗺️ Par où commencer ?
on_message(self, message).⚡ Code minimal pour démarrer
class MonBot:
def on_ready(self):
print("🤖 Bot prêt !")
def on_message(self, message):
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
if author.get('is_bot'): return # anti-boucle obligatoire
if content == '!ping':
self.send_message(channel, "🏓 Pong !")
elif content == '!aide':
self.send_message(channel, embed={
"title": "📚 Commandes",
"description": "• `!ping` — test\n• `!aide` — ce menu",
"color": "#7c6cf0"
})Créer un bot
Configure ton premier bot depuis l'éditeur intégré en quelques étapes. De zéro à un bot actif en moins de 2 minutes.
requests, random, datetime, json… tous les modules standards disponibles.🗺️ Étapes de création
📋 Templates prêts à l'emploi
Copie un de ces templates dans l'éditeur, sauvegarde avec Ctrl+S, et ton bot est opérationnel immédiatement.
class MonBot:
def on_ready(self):
print("🤖 Bot prêt !") # visible dans la console de l'éditeur
def on_message(self, message):
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
if author.get('is_bot'): return # Anti-boucle OBLIGATOIRE
if content == '!ping':
self.send_message(channel, "🏓 Pong !")
elif content == '!bonjour':
nom = author.get('username', 'toi')
self.send_message(channel, f"👋 Bonjour {nom} !")
elif content == '!aide':
self.send_message(channel, embed={
"title": "📚 Commandes disponibles",
"description": "• `!ping` — test de latence\n• `!bonjour` — salutation\n• `!aide` — ce menu",
"color": "#7c6cf0"
})class BotScore:
def __init__(self):
self.scores = {} # {user_id: score} — persiste entre les messages
def on_message(self, message):
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
if author.get('is_bot'): return
uid = author.get('id')
name = author.get('username', '?')
if content == '!+1':
self.scores[uid] = self.scores.get(uid, 0) + 1
self.send_message(channel, f"✅ Score de {name} : {self.scores[uid]} pt(s)")
elif content == '!scores':
if not self.scores:
self.send_message(channel, "Aucun score enregistré.")
else:
lignes = "\n".join(f"ID {uid}: {s} pts" for uid, s in sorted(
self.scores.items(), key=lambda x: -x[1]))
self.send_message(channel, embed={"title": "🏆 Classement", "description": lignes, "color": "#f59e0b"})class BotInteractif:
def on_message(self, message):
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
if author.get('is_bot'): return
if content == '!menu':
self.send_message(channel, "Que veux-tu faire ?", components=[
{"type": "button", "label": "⚔️ Combattre", "custom_id": "combat", "style": "danger"},
{"type": "button", "label": "🏪 Boutique", "custom_id": "boutique", "style": "primary"},
{"type": "button", "label": "📊 Mon profil", "custom_id": "profil", "style": "secondary"},
])
def on_button_click(self, interaction):
ch = interaction['channel_id']
cid = interaction.get('custom_id')
usr = interaction['user']['username']
if cid == 'combat': self.send_message(ch, f"⚔️ {usr} entre dans l'arène !")
elif cid == 'boutique': self.send_message(ch, f"🏪 {usr} ouvre la boutique…")
elif cid == 'profil': self.send_message(ch, f"📊 Profil de {usr} : niveau 1")import requests
class BotMeteo:
API_KEY = "ta_cle_api" # https://openweathermap.org/api
def on_message(self, message):
content = message.get('content', '').strip()
channel = message.get('channel_id')
author = message.get('author', {})
if author.get('is_bot'): return
if content.lower().startswith('!meteo '):
ville = content[7:].strip()
try:
r = requests.get(
f"https://api.openweathermap.org/data/2.5/weather"
f"?q={ville}&appid={self.API_KEY}&lang=fr&units=metric",
timeout=5)
d = r.json()
temp = d['main']['temp']
desc = d['weather'][0]['description']
self.send_message(channel, f"🌤️ **{ville.title()}** : {temp:.0f}°C, {desc}")
except:
self.send_message(channel, "❌ Ville introuvable ou API indisponible.")⚙️ Paramètres de configuration du bot
| Paramètre | Description | Modifiable après ? |
|---|---|---|
| Nom | Nom affiché dans les salons et les messages. Max 32 caractères. | Oui |
| Description | Courte description du bot, visible dans la liste des bots du serveur. | Oui |
| Avatar | Image du bot. Formats acceptés : JPG, PNG, GIF animé. Max 2 Mo. | Oui |
| Code Python | Le code du bot. Éditable à tout moment dans l'éditeur Monaco. | Oui |
| Serveurs associés | Liste des serveurs où le bot est actif. Ajout/suppression libres. | Oui |
| Statut | EN LIGNE / HORS LIGNE. Contrôlé via les boutons ▶️ et ⏹️. | Oui |
💡 Bonnes pratiques
if author.get('is_bot'): return, ton bot répondra à ses propres messages — boucle infinie assurée.self.* pour mémoriserself.scores, self.etape, etc. persistent entre les messages tant que le bot tourne.print()print(message) affiche l'objet complet dans la console de l'éditeur. Indispensable pour inspecter les données.L'éditeur de code
Un éditeur Python complet dans le navigateur, basé sur Monaco (VS Code). Coloration syntaxique, autocomplétion, console intégrée et déploiement instantané avec Ctrl+S.
print(), erreurs de syntaxe et confirmations de sauvegarde en temps réel.🖼️ Interface de l'éditeur
print(), les erreurs Python et les logs du système. Effaçable en 1 clic.⌨️ Raccourcis clavier
| Raccourci | Action | Contexte |
|---|---|---|
| 💾 Sauvegarde & navigation | ||
| Ctrl+S | Sauvegarder & déployer le bot | Partout |
| Ctrl+Z | Annuler la dernière action | Partout |
| Ctrl+Shift+Z / Ctrl+Y | Rétablir | Partout |
| 📝 Édition | ||
| Ctrl+/ | Commenter / décommenter la ligne | Code |
| Ctrl+D | Sélectionner le mot suivant identique | Code |
| Ctrl+Shift+K | Supprimer la ligne | Code |
| Alt+↑ / ↓ | Déplacer la ligne vers le haut / bas | Code |
| Alt+Shift+↑ / ↓ | Copier-coller la ligne vers le haut / bas | Code |
| Tab | Indenter (4 espaces) | Code |
| Shift+Tab | Désindenter | Code |
| 🔍 Recherche | ||
| Ctrl+F | Rechercher dans le code | Code |
| Ctrl+H | Rechercher & remplacer | Code |
| F3 / Shift+F3 | Résultat suivant / précédent | Recherche |
| 📋 Sélection | ||
| Ctrl+A | Tout sélectionner | Code |
| Ctrl+L | Sélectionner la ligne entière | Code |
| Alt+clic | Ajouter un curseur supplémentaire | Code |
| Ctrl+Shift+L | Sélectionner toutes les occurrences | Code |
🚀 Ton premier bot en 2 minutes
Copie ce code dans l'éditeur, appuie sur Ctrl+S, et ton bot est actif.
class MonBot:
def on_ready(self):
print("🤖 Bot prêt !") # visible dans la console
def on_message(self, message):
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
if author.get('is_bot'): return # ignorer les bots
if content == '!ping':
self.send_message(channel, "🏓 Pong !")
elif content == '!aide':
self.send_message(channel, "Commandes : !ping · !aide · !bonjour")
elif content.startswith('!bonjour'):
nom = author.get('username', 'toi')
self.send_message(channel, f"👋 Bonjour, {nom} !")💡 Conseils pour bien coder
is_boton_message par if author.get('is_bot'): return pour éviter les boucles infinies.print() pour débuggerprint(message) affiche l'objet complet dans la console. Indispensable pour voir la structure des données.self.* pour mémoriserself.compteur, self.scores, etc. persistent entre les messages tant que le bot tourne.Comment ça marche
Comprends le cycle de vie complet — de l'écriture du code à la réponse du bot en temps réel dans le chat.
🔄 Cycle d'exécution — Message
écrit un message
Le Navire reçoit
on_message()ton code s'exécute
send_message()appelé
affichée
🔄 Cycle d'exécution — Interaction
clique un bouton
détecte le clic
on_button_click()ton code s'exécute
ou mise à jour
mise à jour
on_message() est appelé avec toutes les données en quelques millisecondes.self.xxx persiste entre les appels. Compteurs, scores, données — tout reste en mémoire.requests.on_ready, on_message, on_button_click — chaque événement a sa méthode.📨 L'objet message
Tout ce que contient l'objet reçu dans on_message(self, message) :
| Clé | Type | Description | Exemple |
|---|---|---|---|
content | str | Texte du message | "!ping" |
channel_id | int | ID du salon | 1042 |
server_id | int | ID du serveur | 42 |
message_id | int | ID unique du message | 8801 |
author.id | int | ID de l'auteur | 485921 |
author.username | str | Nom d'utilisateur | "Léa" |
author.is_bot | bool | L'auteur est un bot ? | False |
attachments | list | Fichiers joints au message | [{"name":"…"}] |
🧪 Exemple complet — de la réception à la réponse
class MonBot:
def on_ready(self):
print("🤖 Démarré !") # s'exécute au démarrage
def on_message(self, message):
# 1. Extraire les données utiles
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
# 2. Ignorer les messages de bots (anti-boucle)
if author.get('is_bot'): return
# 3. Répondre selon le contenu
if content == '!ping':
self.send_message(channel, "🏓 Pong !")
elif content == '!vote':
# Envoyer des boutons interactifs
self.send_message(channel, "Vote ici :", components=[
{"type":"button", "label":"👍 Oui", "custom_id":"vote_oui", "style":"success"},
{"type":"button", "label":"👎 Non", "custom_id":"vote_non", "style":"danger"},
])
def on_button_click(self, interaction):
# 4. Répondre au clic sur les boutons
ch = interaction['channel_id']
cid = interaction.get('custom_id')
usr = interaction['user']['username']
if cid == 'vote_oui':
self.send_message(ch, f"👍 {usr} a voté OUI !")
elif cid == 'vote_non':
self.send_message(ch, f"👎 {usr} a voté NON.")🌟 Ce que ton bot peut faire
!commande → réponse instantanée. Supporte les préfixes, sous-commandes et arguments.random.self.*. Persistant jusqu'au redémarrage.requests ou aiohttp.Structure du code
Tout bot est une classe Python avec des méthodes spéciales reconnues par Le Navire.
class MonBot:
def on_ready(self):
# Appelé une fois au démarrage
print("Bot prêt !")
def on_message(self, message):
# Appelé à chaque message — méthode PRINCIPALE
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
if author.get('is_bot'): return
if content == '!ping':
self.send_message(channel, "🏓 Pong !")
def on_button_click(self, interaction):
# Appelé sur clic bouton / sélection menu / timer expiré
cid = interaction.get('custom_id')
ch = interaction.get('channel_id')
self.send_message(ch, f"Tu as cliqué sur `{cid}`")Méthodes reconnues
| Méthode | Déclencheur | Obligatoire |
|---|---|---|
on_ready(self) | Démarrage du bot | Non |
on_message(self, message) | Chaque message dans un salon du bot | Oui |
on_button_click(self, interaction) | Clic bouton / menu / timer — handler universel | Non |
on_select_menu(self, interaction) | Sélection menu déroulant (prioritaire) | Non |
on_timer_expired(self, interaction) | Timer arrivé à zéro (prioritaire) | Non |
Recevoir des messages
on_message(self, message) est appelée pour chaque message dans les salons où ton bot est présent.
L'objet message
| Clé | Type | Description | Exemple |
|---|---|---|---|
content | str | Texte du message | "!bonjour" |
channel_id | int | ID du salon | 42 |
server_id | int | ID du serveur | 7 |
message_id | int | ID unique du message | 1234 |
author.id | int | ID de l'auteur | 3 |
author.username | str | Nom d'utilisateur | "Alice" |
author.is_bot | bool | Vrai si c'est un bot | False |
def on_message(self, message):
content = message.get('content', '')
clean = content.lower().strip() # normalisé
channel = message.get('channel_id')
author = message.get('author', {})
username = author.get('username', 'Inconnu')
if author.get('is_bot'): return
# Commande exacte
if clean == '!ping':
self.send_message(channel, "🏓 Pong !")
# Commande avec arguments : "!dis Bonjour le monde"
elif clean.startswith('!dis '):
texte = content[5:]
self.send_message(channel, f"📢 {username} dit : {texte}")
# Mot-clé dans le message
elif 'bonjour' in clean:
self.send_message(channel, f"👋 Salut {username} !")!Bonjour, !bonjour ou !BONJOUR. Normalise pour éviter les faux négatifs.Envoyer des messages
self.send_message() permet d'envoyer n'importe quel type de contenu depuis ton bot.
Signature complète
self.send_message(
channel_id, # int — message.get('channel_id')
content = None, # str — Texte (supporte Markdown)
embed = None, # dict — Carte embed colorée
image_url = None, # str — URL d'une image
components= None, # list — Boutons, menus, barres, timers
)# 1. Texte simple (Markdown supporté)
self.send_message(channel, "**Gras**, *italique*, `code`, ~~barré~~")
# 2. Embed (carte colorée)
self.send_message(channel, embed={
"title": "🌟 Mon embed",
"description": "Un texte formaté",
"color": "#7c6cf0"
})
# 3. Image
self.send_message(channel, image_url="https://example.com/image.png")
# 4. Tout combiné
self.send_message(
channel,
content="Voici les résultats :",
embed={"title": "📊 Stats", "color": "#00d9a3"},
components=[{
"type": "action_row", "components": [
{"type": "button", "label": "Voir détails", "style": "primary", "custom_id": "details"}
]
}]
)self.send_message() plusieurs fois dans le même handler pour envoyer plusieurs messages à la suite.Embeds
Des cartes visuelles colorées pour afficher des informations structurées et attrayantes.
🧪 Démo interactive
**gras***italique****gras+ital***__souligné__~~barré~~||spoiler||`code````bloc```> citation# Titre- liste
Tous les champs
Apparence
| Champ | Type | Description |
|---|---|---|
color | str | Couleur accent ("#hex") — barre, titres, glow |
theme | str | "default" · "glass" · "solid" · "dark" · "minimal" |
background | str | Fond CSS custom (hex, rgba, gradient). Écrase theme. |
border_position | str | "left" (défaut) · "top" · "bottom" · "all" · "none" |
border_width | int | Épaisseur de la bordure en px (défaut : 4) |
glow | bool | Ajoute un halo lumineux autour de la carte |
compact | bool | Padding réduit, police plus petite |
max_width | int | Largeur max en px (défaut : 520) |
Contenu
| Champ | Type | Description |
|---|---|---|
title | str | Titre principal (Markdown supporté) |
description | str | Corps du texte (Markdown complet) |
url | str | Lien sur le titre |
author | dict | { name, icon_url, url, badge } — ligne d'auteur avec badge optionnel |
status | dict | { text, color, dot } — pastille de statut colorée |
fields | list | Liste de champs (voir ci-dessous) |
badges | list | [{ label, color, icon, filled }] — rangée de badges |
footer | str / dict | Texte ou { text, icon_url } |
timestamp | str | Date ISO 8601 affichée à côté du footer |
Images
| Champ | Type | Description |
|---|---|---|
banner | str / dict | Image pleine-largeur en haut de la carte. Str = URL, dict = { url, height } |
thumbnail | str / dict | Petite image à droite. Dict : { url, round, radius } |
image | str / dict | Image principale. Dict : { url, radius } |
images | list | Grille de 2–4 images : ["url1","url2"] ou [{ url }] |
Champs spéciaux (fields)
| Propriété | Description |
|---|---|
type: "progress" | Affiche une barre de progression. Utilise value(0-100), bar_color, height, label |
type: "divider" | Ligne de séparation fine entre deux champs |
inline: True | Deux champs côte à côte (50% chacun) |
self.send_message(channel, embed={
"title": "📊 Stats du serveur",
"color": "#00d9a3",
"fields": [
{"name": "Membres", "value": "42", "inline": True},
{"name": "Messages", "value": "1337", "inline": True},
{"name": "Bots actifs", "value": "3", "inline": True},
],
"footer": "Mis à jour à l'instant"
})Exemples avancés
self.send_message(channel, embed={
"title" : "🛡️ Profil Joueur",
"color" : "#a855f7",
"theme" : "glass",
"glow" : True,
"status" : {"text": "En ligne", "color": "#00d9a3"},
"author" : {"name": "Sylvain42", "badge": "VIP"},
"description": "Un grand guerrier du désert numérique.",
"badges": [
{"label": "Niveau 42", "color": "#a855f7", "icon": "⭐"},
{"label": "Top Joueur", "color": "#f59e0b", "icon": "🏆"},
],
"fields": [
{"name": "XP", "type": "progress", "value": 73, "bar_color": "#a855f7"},
{"name": "Santé", "type": "progress", "value": 55, "bar_color": "#ef4444"},
{"type": "divider"},
{"name": "Kills", "value": "284", "inline": True},
{"name": "Morts", "value": "91", "inline": True},
],
"footer": {"text": "Dernière connexion : il y a 5 min"}
})self.send_message(channel, embed={
"title" : "🏰 Annonce du serveur",
"color" : "#f59e0b",
"theme" : "dark",
"banner" : {"url": "https://example.com/banner.jpg", "height": 100},
"border_position": "top",
"border_width" : 3,
"description": "Découvrez nos nouvelles zones !",
"images": [
"https://example.com/zone1.jpg",
"https://example.com/zone2.jpg",
"https://example.com/zone3.jpg",
],
"footer": "Patch 2.4 — Aujourd'hui"
})Images
Affiche des images directement dans les messages avec image_url ou dans un embed avec le champ image.
# 1. Image seule (pleine largeur)
self.send_message(channel, image_url="https://example.com/image.png")
# 2. Image dans un embed (avec titre et couleur)
self.send_message(channel, embed={
"title": "📸 Photo du jour",
"image": "https://example.com/photo.jpg",
"color": "#f97316"
})
# 3. Image + bouton
self.send_message(
channel,
content="Regarde cette image 👇",
image_url="https://example.com/img.png",
components=[{
"type": "action_row", "components": [
{"type": "button", "label": "❤️ J'aime", "style": "danger", "custom_id": "like"}
]
}]
)image_url donne un meilleur rendu.Boutons
Les boutons se placent dans des ActionRow (max 5 par ligne). Styles prédéfinis ou couleurs hex totalement custom.
🧪 Démo interactive — clique sur les boutons !

on_button_click !Champs d'un bouton
| Champ | Type | Description |
|---|---|---|
type | str | Toujours "button" |
label | str | Texte affiché sur le bouton |
style | str | primary / secondary / success / danger / link |
color | str | Couleur hex custom "#ff6b9d" — remplace style |
text_color | str | Couleur du texte (auto-détecté si absent) |
custom_id | str | Identifiant reçu dans on_button_click |
emoji | str | Emoji affiché avant le label |
disabled | bool | Bouton grisé dès l'envoi (statique) |
| — Comportement interactif — | ||
expires_in | int | Grisé après N secondes (30 = 30s après l'envoi) |
lock_on_click | bool | Grisé définitivement après le 1er clic (usage unique) |
max_uses | int | Grisé après N clics au total |
exclusive | bool | Cliquer grise tous les autres boutons de la ligne (choix unique) |
Couleur hex custom — testeur
if content == '!vote':
self.send_message(channel,
content="🗳️ Quel est ton OS préféré ?",
components=[{
"type": "action_row", "components": [
{"type": "button", "label": "🐧 Linux", "style": "primary", "custom_id": "vote_linux"},
{"type": "button", "label": "🪟 Windows", "style": "secondary", "custom_id": "vote_windows"},
{"type": "button", "label": "🍎 macOS", "color": "#c0c0c0", "custom_id": "vote_mac"},
]
}]
)
# exclusive — cliquer un bouton grise immédiatement les autres
elif content == '!sondage':
self.send_message(channel,
content="🗳️ Pour ou contre ?",
components=[{"type": "action_row", "components": [
{"type": "button", "label": "✅ Pour", "style": "success", "custom_id": "vote_pour", "exclusive": True},
{"type": "button", "label": "❌ Contre", "style": "danger", "custom_id": "vote_contre", "exclusive": True},
]}]
)
# expires_in + lock_on_click — disponible 60s, 1 seul clic
elif content == '!offre':
self.send_message(channel,
embed={"title": "⚡ Offre flash — 60s !", "color": "#f1c40f"},
components=[{"type": "action_row", "components": [
{"type": "button", "label": "🎁 Réclamer", "style": "primary",
"custom_id": "claim", "expires_in": 60, "lock_on_click": True},
]}]
)Barre de progression
Composant visuel — affiche des barres colorées, inversées, verticales, segmentées, avec timer ou lecteur multimédia intégré. Combinable avec edit_message pour animer en temps réel.
🧪 Démo — Barres classiques (RPG)
🎵 Démo — Lecteur musique / vidéo / son
🎨 Démo — Variantes visuelles
↔️↕️ Démo — Inverse & Vertical
🟦 Démo — Barres segmentées
Champs — progress_bar
| Champ | Type | Requis | Défaut | Description |
|---|---|---|---|---|
type | str | ✓ | — | Toujours "progress_bar" |
value | int | ✓ | — | Pourcentage rempli — 0 à 100 (clampé auto) |
label | str | — | None | Texte au-dessus — supporte les emojis |
color | str | — | #00d9a3 | Couleur hex ou rgba du remplissage |
height | int | — | 8 | Hauteur en pixels (2–20 recommandé) |
shape | str | — | "round" | "round" / "square" / "flat" — style des bords |
direction | str | — | "ltr" | "ltr" normal · "rtl" inverse · "vertical" colonne |
animated | bool | — | False | Active les rayures animées en mouvement |
pulse | bool | — | False | Fait pulser (opacité) la barre — utile pour "en cours" |
glow | bool | — | False | Ajoute un halo lumineux autour du remplissage |
segments | int | — | None | Divise la barre en N segments discrets (vies, niveaux…) |
track_color | str | — | rgba(255,255,255,.07) | Couleur du fond de piste |
Exemple — Stats RPG multi-barres
if content == '!stats':
xp, hp, mana = 73, 45, 88
self.send_message(channel,
embed={"title": "🧙 Tes stats", "color": "#5865f2"},
components=[
{"type": "progress_bar", "label": "⭐ XP — Niveau 7",
"value": xp, "color": "#10d9a3", "height": 10, "glow": True},
{"type": "progress_bar", "label": f"❤️ HP {hp}/100",
"value": hp, "color": "#ef4444", "height": 6},
{"type": "progress_bar", "label": f"💙 Mana {mana}/100",
"value": mana, "color": "#5865f2", "height": 6},
{"type": "action_row", "components": [
{"type": "button", "label": "⚔️ Attaquer", "style": "primary", "custom_id": "attack"},
{"type": "button", "label": "🏥 Se soigner", "style": "success", "custom_id": "heal"},
{"type": "button", "label": "✨ Lancer un sort", "style": "secondary", "custom_id": "cast"},
]},
]
)Exemple — Lecteur musique / vidéo / son
import threading, time
if content == '!play':
title = "Midnight Drive"
artist = "Synthwave King"
duration = 210 # secondes
# Message initial — barre à 0
msg = self.send_message(channel,
embed={"title": f"🎵 {title}", "description": f"👤 {artist}", "color": "#8875f5"},
components=[
{"type": "progress_bar", "label": "0:00 ◄━━━━━━━━━━━━━━━━━━ 3:30",
"value": 0, "color": "#8875f5", "height": 4},
{"type": "action_row", "components": [
{"type": "button", "label": "⏮", "style": "secondary", "custom_id": "prev"},
{"type": "button", "label": "⏸ Pause", "style": "primary", "custom_id": "pause"},
{"type": "button", "label": "⏭", "style": "secondary", "custom_id": "next"},
{"type": "button", "label": "🔁", "style": "secondary", "custom_id": "loop"},
]},
]
)
mid = msg.get('id')
def _fmt(s): return f"{s//60}:{s%60:02d}"
def run_player():
for elapsed in range(1, duration + 1):
time.sleep(1)
pct = int(elapsed / duration * 100)
lbl = f"{_fmt(elapsed)} ◄{'━'*int(pct/5)}{'─'*(20-int(pct/5))} {_fmt(duration)}"
self.edit_message(channel, mid,
embed={"title": f"🎵 {title}", "description": f"👤 {artist}", "color": "#8875f5"},
components=[
{"type": "progress_bar", "label": lbl,
"value": pct, "color": "#8875f5", "height": 4},
{"type": "action_row", "components": [
{"type": "button", "label": "⏮", "style": "secondary", "custom_id": "prev"},
{"type": "button", "label": "⏸ Pause", "style": "primary", "custom_id": "pause"},
{"type": "button", "label": "⏭", "style": "secondary", "custom_id": "next"},
{"type": "button", "label": "🔁", "style": "secondary", "custom_id": "loop"},
]},
]
)
threading.Thread(target=run_player, daemon=True).start()Exemple — Timer avec barre inverse (compte à rebours)
import threading, time
if content == '!vote':
duration = 60 # secondes
msg = self.send_message(channel,
embed={"title": "🗳️ Vote en cours !", "color": "#f1c40f"},
components=[
{"type": "progress_bar", "label": "⏳ 60s restantes",
"value": 100, "color": "#f1c40f", "height": 8,
"direction": "rtl", "animated": True},
{"type": "action_row", "components": [
{"type": "button", "label": "✅ Pour", "style": "success", "custom_id": "vote_yes"},
{"type": "button", "label": "❌ Contre", "style": "danger", "custom_id": "vote_no"},
]},
]
)
mid = msg.get('id')
def run_timer():
for remaining in range(duration - 1, -1, -1):
time.sleep(1)
pct = int(remaining / duration * 100)
color = "#ef4444" if remaining <= 10 else ("#f59e0b" if remaining <= 20 else "#f1c40f")
self.edit_message(channel, mid,
embed={"title": "🗳️ Vote en cours !", "color": color},
components=[
{"type": "progress_bar",
"label": f"⏳ {remaining}s restantes",
"value": pct, "color": color, "height": 8,
"direction": "rtl", "pulse": remaining <= 10},
{"type": "action_row", "components": [
{"type": "button", "label": "✅ Pour", "style": "success", "custom_id": "vote_yes"},
{"type": "button", "label": "❌ Contre", "style": "danger", "custom_id": "vote_no"},
]},
]
)
self.send_message(channel, "⏰ Temps écoulé ! Vote terminé.")
threading.Thread(target=run_timer, daemon=True).start()Exemple — Barres verticales (comparaison / stats)
if content == '!classement':
scores = {"Alice": 85, "Bob": 62, "Carla": 94, "Dan": 41}
colors = ["#10d9a3", "#5865f2", "#f1c40f", "#ef4444"]
comps = []
for i, (name, score) in enumerate(scores.items()):
comps.append({
"type": "progress_bar",
"label": f"{name} — {score}pts",
"value": score,
"color": colors[i],
"height": 6,
"direction": "vertical",
})
self.send_message(channel,
embed={"title": "📊 Classement de la semaine", "color": "#10d9a3"},
components=comps
)Exemple — Barre segmentée (vies / niveaux)
# 3 vies sur 5 restantes
{"type": "progress_bar", "label": "❤️ Vies",
"value": 60, "color": "#ef4444", "height": 14,
"segments": 5, "shape": "square"}
# Niveau 4 sur 10 débloqués
{"type": "progress_bar", "label": "🏅 Niveaux débloqués",
"value": 40, "color": "#f1c40f", "height": 10,
"segments": 10, "shape": "round"}
# Chargement par étapes (5 blocs)
{"type": "progress_bar", "label": "⚙️ Chargement",
"value": 80, "color": "#8875f5", "height": 12,
"segments": 5, "animated": True}Exemple — Palette de couleurs & styles
# Vert XP / succès
{"type": "progress_bar", "label": "⭐ XP", "value": 73, "color": "#10d9a3"}
# Rouge HP / danger
{"type": "progress_bar", "label": "❤️ HP", "value": 45, "color": "#ef4444"}
# Bleu Mana / énergie
{"type": "progress_bar", "label": "💙 Mana", "value": 88, "color": "#5865f2"}
# Violet Rang / niveau
{"type": "progress_bar", "label": "🔮 Rang", "value": 60, "color": "#8875f5"}
# Or Score / coins
{"type": "progress_bar", "label": "🪙 Score", "value": 55, "color": "#f1c40f"}
# Orange Chaleur / temps
{"type": "progress_bar", "label": "🔥 Stamina", "value": 30, "color": "#f59e0b"}
# Rayures animées — téléchargement, traitement
{"type": "progress_bar", "label": "⬇️ Download", "value": 65, "color": "#10d9a3", "animated": True}
# Pulsation — en attente
{"type": "progress_bar", "label": "⏳ En attente","value": 50, "color": "#f59e0b", "pulse": True}
# Halo glow — barre principale
{"type": "progress_bar", "label": "✨ Pouvoir", "value": 95, "color": "#a78bfa", "glow": True}value > 100 ou value < 0, le SDK la clamp automatiquement — pas d'erreur.edit_messagethreading.Thread qui appelle edit_message à chaque étape. Fonctionne pour lecteurs audio/vidéo, timers, téléchargements — à environ 1 update/sec pour rester fluide sans rate-limit.progress_bar n'émet jamais d'interaction. Pour réagir à 100 %, combine avec un Timer ou un bouton.Timer
Compte à rebours animé entièrement personnalisable — format, style, barre liée, alerte couleur, fond, emoji. Déclenche "timer_expired" dans on_button_click quand il atteint zéro.
mm:ss, h:mm:ss, secondes seules.timer_expired à zéro.🧪 Démo — Timer interactif complet
📊 Démo — Barre de progression liée au timer
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
type | str | — | Toujours "timer" |
duration | int | — | Durée totale en secondes |
label | str | — | Texte au-dessus du compte à rebours |
color | hex | #f1c40f | Couleur du chiffre et de la barre |
emoji | str | — | Icône affichée devant le chiffre |
format | str | "mm:ss" | Format : "mm:ss", "h:mm:ss", "seconds" |
style | str | "large" | Taille : "large", "normal", "compact", "minimal" |
progress_bar | str/bool | false | true / "above" / "inverse" pour afficher une barre liée |
background | hex | transparent | Couleur de fond du bloc timer |
alert_at | int | — | % restant déclenchant la couleur d'alerte (ex: 25) |
alert_color | hex | #ef4444 | Couleur quand alert_at est atteint |
pulse | bool | true | Pulsation de la barre/chiffre en mode alerte |
custom_id | str | — | Si fourni, déclenche timer_expired dans on_button_click |
self.send_message(channel,
content="⏳ Le bot sera prêt dans :",
components=[{
"type": "timer",
"duration": 30,
"color": "#10d9a3",
"emoji": "🟢",
}])self.send_message(channel,
content="⚡ Offre flash ! Plus que :",
components=[{
"type": "timer",
"duration": 120,
"label": "Offre expire dans",
"color": "#f59e0b",
"emoji": "🔥",
"format": "mm:ss",
"style": "large",
"progress_bar": True,
"background": "#1a1208",
"alert_at": 20,
"alert_color": "#ef4444",
"pulse": True,
"custom_id": "flash_sale",
}])
def on_button_click(self, interaction):
if interaction.get('type') == 'timer_expired':
ch = interaction['channel_id']
self.send_message(ch, embed={"title": "❌ Offre expirée !", "color": "#ef4444"})if content == '!quiz':
self.send_message(channel,
embed={"title": "❓ Capitale de la France ?", "color": "#9b59b6"},
components=[
{"type": "timer", "label": "Temps restant", "duration": 20,
"color": "#e74c3c", "progress_bar": True, "alert_at": 30,
"custom_id": "quiz_timeout"},
{"type": "action_row", "components": [
{"type": "button", "label": "Lyon", "style": "secondary", "custom_id": "quiz_lyon"},
{"type": "button", "label": "Paris", "style": "primary", "custom_id": "quiz_paris"},
{"type": "button", "label": "Nice", "style": "secondary", "custom_id": "quiz_nice"},
]},
]
)
def on_button_click(self, interaction):
itype = interaction.get('type'); cid = interaction.get('custom_id')
ch = interaction.get('channel_id')
user = interaction.get('user', {}).get('username', '?')
if itype == 'timer_expired' and cid == 'quiz_timeout':
self.send_message(ch, embed={"title": "⏰ Temps écoulé !", "description": "C'était **Paris** 🗼", "color": "#ef4444"})
elif cid == 'quiz_paris':
self.send_message(ch, f"🎉 {user} a trouvé ! C'est Paris !")
elif cid in ('quiz_lyon', 'quiz_nice'):
self.send_message(ch, f"❌ {user}, mauvaise réponse ! C'était Paris 🗼")import datetime
if content == '!event':
now = datetime.datetime.now()
target = now.replace(hour=20, minute=0, second=0) + datetime.timedelta(days=1)
secs = int((target - now).total_seconds())
self.send_message(channel,
content="🎤 Prochain live dans :",
components=[{
"type": "timer",
"duration": secs,
"label": "📡 Live dans",
"format": "h:mm:ss",
"style": "large",
"progress_bar": "inverse",
"color": "#8875f5",
"background": "#0f0820",
"alert_at": 5,
"alert_color": "#10d9a3",
"custom_id": "event_start",
}]
)
def on_button_click(self, interaction):
if interaction.get('type') == 'timer_expired' and interaction.get('custom_id') == 'event_start':
self.send_message(interaction['channel_id'], "🟢 @everyone Le live commence MAINTENANT !")timer_expired est envoyée par chaque client qui voit le message. Gère les doublons côté bot (ex: dict Python pour ignorer les appels dupliqués).progress_bar: true génère automatiquement une barre synchronisée. Pas besoin de 2 composants séparés — la barre et le timer partagent le même objet JSON.Affichage texte — text_display
Bloc de texte ultra-personnalisable : taille, police, gras, italique, souligné, barré, espacement, fond plein ou dégradé, bordure flexible (côté, style, épaisseur), ombre et opacité.
🧪 Démo — Personnalise en temps réel
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
content | str | — | Texte à afficher (supporte \n pour sauts de ligne) |
color | hex | #fff | Couleur du texte |
size | str | "md" | "xs" 11px / "sm" 12px / "md" 14px / "lg" 17px / "xl" 20px |
font | str | — | Police : "mono", "serif" (défaut : sans-serif) |
bold | bool | False | Gras |
italic | bool | False | Italique |
underline | bool | False | Souligné |
strikethrough | bool | False | Barré (texte rayé) |
align | str | "left" | Alignement : "left", "center", "right" |
letter_spacing | float | — | Espacement des lettres en em (ex: 0.1) |
icon | str | — | Emoji ou icône affiché avant le texte |
background | hex | — | Couleur de fond du bloc |
gradient_colors | list | — | Dégradé de fond : ["#color1", "#color2"] (prioritaire sur background) |
border_radius | int | 7 | Rayon des coins du fond (px) |
border_color | hex | — | Couleur de la bordure |
border_side | str | "left" | Côté : "left", "right", "top", "bottom", "all" |
border_style | str | "solid" | Style : "solid", "dashed", "dotted", "double" |
border_width | int | 3 | Épaisseur de la bordure (px) |
shadow | bool | False | Ombre portée sous le bloc |
opacity | float | 1 | Opacité du bloc (0.0 → 1.0) |
self.send_message(channel,
components=[{
"type": "text_display",
"content": "Chaque jour est une nouvelle chance.",
"italic": True,
"color": "#a3cfff",
"border_color": "#5865f2",
"icon": "💬",
}])self.send_message(channel,
components=[{
"type": "text_display",
"content": "⚔️ Combat débuté !",
"size": "xl",
"bold": True,
"align": "center",
"color": "#f59e0b",
"background": "#1a0a00",
"border_color": "#f59e0b",
"border_side": "all",
"border_width": 2,
"shadow": True,
}])self.send_message(channel,
components=[{
"type": "text_display",
"content": "Joueur XP: 12 450 Rang: #14",
"font": "mono",
"color": "#e2e8f0",
"gradient_colors": ["#1e1b4b", "#312e81"],
"border_color": "#8875f5",
"border_width": 4,
"border_radius": 10,
"letter_spacing": 0.05,
"shadow": True,
}])self.send_message(channel,
components=[
{"type": "text_display", "content": "Vaincre 10 ennemis",
"bold": True, "color": "#10d9a3", "icon": "✅"},
{"type": "text_display", "content": "Collecter 5 pièces",
"strikethrough": True, "color": "#666", "opacity": 0.55},
{"type": "text_display", "content": "🔒 Atteindre le niveau 10",
"color": "#555", "italic": True, "size": "sm"},
])text_display s'affiche dans la liste components avec les boutons, barres de progression, timers, etc. Empile plusieurs blocs consécutifs pour construire des mises en page riches.Séparateur — separator
Ligne de séparation visuelle entre les autres composants. Optionnellement agrémentée d'un label centré.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
style | solid / dashed / dotted / double | solid | Style de la ligne |
color | hex / rgba | rgba(255,255,255,.12) | Couleur de la ligne |
thickness | int (px) | 1 | Épaisseur de la ligne |
label | str | None | Texte affiché au centre |
margin | int (px) | 7 | Marge verticale |
⚡ Bonnes pratiques
title + msg_color systématiquement pour identifier visuellement l'origine du message en un coup d'œil — sans lire le contenu.
priority="urgent" aux vraies urgences — le badge clignote et attire immédiatement l'attention. Si tout est urgent, plus rien ne l'est.
delete_after + ephemeral=True ensemble pour les messages d'erreur ou d'aide : ils disparaissent automatiquement et ne polluent pas l'historique.
color_scheme="neon" avec msg_color coordonné donne un rendu très impactant pour les annonces importantes — mais utilise-le avec parcimonie pour garder l'effet "wow".
tags sont cliquables côté client — pense à les utiliser pour filtrer les messages dans l'UI. Nomme-les en lowercase sans espaces pour la cohérence.
embeds et components dans le même send_message — les embeds sont pour le message texte enrichi, les components sont pour les cartes interactives. Utilise l'un ou l'autre.
📝 Exemples
# Titre stylé + bande colorée gauche
self.send_message(channel_id,
"Voici les dernières nouvelles du serveur.",
title="📰 Annonce",
msg_color="#f59e0b",
category="Annonce",
tags=["news", "important"],
pin=True
)
# Badge URGENT clignotant + footer + TTS
self.send_message(channel_id,
"⚠️ Le serveur redémarre dans 5 minutes ! Sauvegardez vos données.",
priority="urgent",
msg_color="#ef4444",
footer_text="Maintenance planifiée · 22:00 UTC",
tts=True
)
# Visible uniquement par le destinataire, supprimé après 10s
self.send_message(channel_id,
"Cette commande n'existe pas. Essaie /help pour voir toutes les commandes.",
ephemeral=True,
delete_after=10
)
# Féliciter un utilisateur avec mention et réactions prédéfinies
self.send_message(channel_id,
"Bravo pour ta contribution sur la PR #142 !",
mention="alice",
reactions_preset=["🎉", "🔥", "👏", "❤️"]
)
# Discussion avec code Python intégré dans le message
self.send_message(channel_id,
"Voici le résultat de l'analyse :",
thread_title="Discussion #42",
priority="low",
code={
"language": "python",
"content": "score = analyse_logs()
print(f'Score: {score:.2f}')"
}
)
# Résumé multi-sections dans un seul message
self.send_message(channel_id,
"Résumé de la journée :",
title="📋 Daily recap",
msg_color="#6366f1",
embeds=[
{"title": "🌅 Matin", "description": "Réunion standup à 9h30 — 5 participants.", "color": "#3b82f6"},
{"title": "☀️ Après-midi", "description": "Code review + 3 PRs mergées.", "color": "#f59e0b"},
{"title": "🌙 Soir", "description": "Démo client à 18h — feedback positif.", "color": "#8b5cf6"},
]
)
# Release avec bannière image, tags et scheme néon
self.send_message(channel_id,
"Nouvelle version disponible — mets à jour maintenant !",
title="🚀 v2.1 publiée",
banner_url="https://example.com/release-banner.png",
category="Release",
tags=["python", "api", "v2"],
color_scheme="neon",
msg_color="#00d9a3",
priority="high"
)
# Offre limitée : épinglée, expire dans 1 heure avec compte à rebours en direct
import datetime
exp = (datetime.datetime.utcnow() + datetime.timedelta(hours=1)).isoformat()
self.send_message(channel_id,
"🎁 Offre spéciale — durée limitée !",
title="🎯 Deal exclusif",
pin=True,
priority="high",
expires_at=exp,
msg_color="#f59e0b"
)
# Rapport nocturne : silencieux, dark scheme, low priority
self.send_message(channel_id,
"📊 Rapport nocturne généré automatiquement.",
title="Rapport 03:00",
msg_color="#3b82f6",
priority="low",
color_scheme="dark",
category="Rapport",
tags=["rapport", "auto"],
footer_text="Généré automatiquement",
silent=True
)
# Tous les paramètres avancés ensemble pour voir les interactions visuelles
self.send_message(channel_id,
"Voici un résumé complet avec toutes les options activées.",
title="🎨 Message ultra-enrichi",
msg_color="#8875f5",
priority="high",
color_scheme="glass",
category="Demo",
tags=["test", "showcase"],
thread_title="Discussion ouverte",
footer_text="LeNavire SDK · v2.1",
tts=True,
reactions_preset=["🚀", "🎉", "💜"]
)
Carte de statistique — stat_card
Affiche une métrique clé avec valeur, icône, tendance, barre de progression et couleur de fond configurables.
🧪 Démo — Personnalise en temps réel
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
title | str | — | Label au-dessus de la valeur |
value | str | — | Valeur principale affichée en gros |
icon | str | — | Emoji affiché à gauche |
color | hex | #5865f2 | Couleur d'accentuation (valeur, barre, fond auto) |
value_color | hex | =color | Couleur de la valeur si différente de color |
size | str | "md" | "sm", "md", "lg" |
subtitle | str | — | Texte sous la valeur (ex: delta) |
trend | str | — | "up" ▲ vert, "down" ▼ rouge, "neutral" |
footer | str | — | Texte discret en bas |
background | hex/bool | auto | Couleur de fond, ou false pour transparent |
gradient_colors | list | — | Fond dégradé : ["#c1","#c2"] |
border_color | hex | — | Couleur de la bordure |
shadow | bool | False | Ombre portée |
progress_bar | int | — | Valeur 0–100 de la barre de progression |
progress_color | hex | =color | Couleur de la barre de progression |
self.send_message(channel,
components=[{
"type": "stat_card",
"title": "XP Totaux",
"value": "12 450",
"icon": "⭐",
"color": "#f59e0b",
"progress_bar": 72,
"subtitle": "+320 cette semaine",
"trend": "up",
}])self.send_message(channel,
components=[{
"type": "stat_card",
"title": "Membres actifs",
"value": "1 284",
"icon": "👥",
"color": "#00d9a3",
"gradient_colors": ["#0a2018", "#0d3020"],
"shadow": True,
"size": "lg",
}])Rangée de badges — badge_row
Affiche une ou plusieurs lignes de badges colorés avec icône et label. Idéal pour rôles, statuts, récompenses.
Démonstration
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
badges | list | — | Liste de badges (voir ci-dessous) |
label | str | — | Texte de groupe affiché en fin de ligne |
align | str | "left" | "left", "center", "right" |
Champs d'un badge
| Champ | Type | Défaut | Description |
|---|---|---|---|
label | str | — | Texte du badge |
color | hex | #5865f2 | Couleur d'accentuation |
icon | str | — | Emoji affiché avant le label |
filled | bool | True | True = fond coloré, False = contour |
self.send_message(channel,
components=[{
"type": "badge_row",
"label": "Rôles",
"badges": [
{"label": "Fondateur", "color": "#f1c40f", "icon": "👑"},
{"label": "Niveau 42", "color": "#5865f2", "icon": "⭐"},
{"label": "Actif", "color": "#00d9a3", "filled": False},
]
}])self.send_message(channel,
components=[
{"type": "badge_row", "align": "center",
"badges": [{"label": "Speed Runner", "color": "#f59e0b"},
{"label": "Top 10", "color": "#00d9a3"},
{"label": "OG", "color": "#5865f2", "icon": "🏅"}]},
])Image embarquée — image
Affiche une image avec coins arrondis, légende optionnelle, largeur et hauteur configurables.
Démonstration
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
url | str | — | URL de l'image (publique ou base64) |
alt | str | — | Texte alternatif |
caption | str | — | Légende affichée sous l'image |
width | int | — | Largeur max en px |
height | int | — | Hauteur max en px |
border_radius | int | 10 | Arrondi des coins (px) |
link_url | str | — | URL ouverte au clic sur l'image |
self.send_message(channel,
components=[{
"type": "image",
"url": "https://example.com/banner.png",
"caption": "Bannière du serveur — saison 3",
"width": 300,
"border_radius": 12,
}])Alerte — alert
Bloc d'alerte coloré avec icône, titre, message et types prédéfinis (success, warning, danger, tip, info).
🧪 Démo — Personnalise en temps réel
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
alert_type | str | "info" | Type prédéfini : success, warning, danger, tip, info |
title | str | — | Titre de l'alerte |
message | str | — | Contenu de l'alerte (supporte \n) |
icon | str | auto | Emoji d'icône (surcharge le défaut du type) |
color | hex | auto | Couleur d'accentuation (surcharge le type) |
border_side | str | "left" | left, right, top, bottom, all |
border_width | int | 3 | Épaisseur de la bordure (px) |
border_radius | int | 8 | Rayon des coins (px) |
background | hex | auto | Couleur de fond personnalisée |
shadow | bool | False | Ombre portée |
compact | bool | False | Padding réduit |
dismissible | bool | False | Bouton ✕ pour fermer |
footer | str | — | Note de bas d'alerte |
title_size | str | "md" | "sm", "md", "lg" |
self.send_message(channel,
components=[{
"type": "alert",
"alert_type": "success",
"title": "Paiement confirmé",
"message": "Ta commande #1042 a bien été validée.",
}])self.send_message(channel,
components=[{
"type": "alert",
"alert_type": "danger",
"title": "Erreur critique",
"message": "La base de données est inaccessible.",
"shadow": True,
"dismissible": True,
"border_side": "all",
}])Champ de saisie — input_field
Zone de texte interactive permettant à l'utilisateur de saisir une valeur. Supporte validation, compteur de caractères et placeholder.
Démonstration
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
label | str | — | Texte au-dessus du champ |
placeholder | str | — | Texte indicatif dans le champ |
max_length | int | 200 | Nombre de caractères maximum |
submit_label | str | "Valider" | Texte du bouton de soumission |
multiline | bool | False | Zone de texte multi-lignes |
required | bool | True | Empêche la soumission si vide |
self.send_message(channel,
components=[{
"type": "input_field",
"label": "Ton pseudo",
"placeholder": "Ex : Sylvain42",
"max_length": 32,
"submit_label": "Choisir",
}])Étoiles / Rating — rating
Système de notation par étoiles interactif. L'utilisateur survole et clique pour noter.
Démonstration
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
label | str | — | Texte affiché au-dessus des étoiles |
max | int | 5 | Nombre d'étoiles maximum |
color | hex | #f59e0b | Couleur des étoiles sélectionnées |
size | int | 28 | Taille des étoiles (px) |
self.send_message(channel,
components=[{
"type": "rating",
"label": "Évalue ce service",
"max": 5,
"color": "#f59e0b",
}])Sondage / Poll — poll
Sondage interactif avec question, options colorées et résultats en barres animées après vote.
Démonstration
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
question | str | — | Question posée aux utilisateurs |
options | list | — | Liste d'options (voir ci-dessous) |
multiple | bool | False | Autoriser plusieurs votes |
anonymous | bool | False | Résultats anonymes |
Champs d'une option
| Champ | Type | Description |
|---|---|---|
label | str | Texte de l'option |
value | str | Valeur retournée lors du vote |
emoji | str | Emoji affiché devant le label |
color | hex | Couleur de la barre et sélection |
self.send_message(channel,
components=[{
"type": "poll",
"question": "Quel est ton langage préféré ?",
"options": [
{"label": "Python", "value": "py", "emoji": "🐍", "color": "#3b82f6"},
{"label": "JavaScript", "value": "js", "emoji": "🟨", "color": "#f59e0b"},
{"label": "Rust", "value": "rs", "emoji": "🦀", "color": "#ef4444"},
],
}])Compte à rebours — countdown
Compte à rebours en temps réel affiché sous forme de blocs heures / minutes / secondes avec couleur personnalisable.
Démonstration
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
target | str/int | — | Timestamp ISO ou durée en secondes |
label | str | — | Texte affiché au-dessus |
color | hex | #f59e0b | Couleur des blocs et valeurs |
end_message | str | "Terminé !" | Message affiché à l'expiration |
show_hours | bool | True | Afficher les heures |
import datetime
target = (datetime.datetime.utcnow() + datetime.timedelta(hours=1)).isoformat()
self.send_message(channel,
components=[{
"type": "countdown",
"target": target,
"label": "🎉 Prochain événement :",
"color": "#f59e0b",
"end_message": "L'événement est commencé !",
}])Bloc de code — code_block
Affiche du code source avec coloration syntaxique, numéros de ligne, bouton copier et header façon éditeur.
Démonstration
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
content | str | — | Code source à afficher |
language | str | "text" | Langage pour la coloration syntaxique |
show_line_numbers | bool | True | Afficher les numéros de ligne |
show_copy_button | bool | True | Bouton de copie dans le header |
filename | str | — | Nom de fichier affiché dans le header |
theme | str | "monokai" | Thème couleur : monokai, dark, light |
self.send_message(channel,
components=[{
"type": "code_block",
"language": "python",
"filename": "fibonacci.py",
"content": "def fibonacci(n):\n if n <= 1:\n return n\n return fibonacci(n-1) + fibonacci(n-2)",
}])Tableau de données — data_table
Tableau HTML stylé avec en-têtes, lignes alternées, couleur d'accentuation et données structurées.
Démonstration
Champs
| Champ | Type | Défaut | Description |
|---|---|---|---|
headers | list | — | Liste de noms de colonnes |
rows | list | — | Liste de listes : chaque sous-liste = une ligne |
color | hex | #5865f2 | Couleur d'accentuation des en-têtes |
striped | bool | True | Zébrage des lignes |
self.send_message(channel,
components=[{
"type": "data_table",
"color": "#5865f2",
"headers": ["Joueur", "Score", "Rang"],
"rows": [
["Alice", "9 500", "🥇"],
["Bob", "7 200", "🥈"],
["Carol", "5 100", "🥉"],
],
}])Onglets — tabs
Système d'onglets cliquables avec contenu indépendant par onglet. Couleur, style, arrondi, taille, espacement, icônes et interactions entièrement configurables.
tab_select déclenché à chaque changement d'onglet.⚡ Préréglages
🧪 Démo — Personnalise en temps réel
Paramètres globaux
| Champ | Type | Défaut | Description |
|---|---|---|---|
type | str | — | Toujours "tabs" |
tabs | list | — | Liste d'onglets (voir tableau ci-dessous) |
color | hex | #5865f2 | Couleur d'accent : bordure et fond de l'onglet actif |
bg_color | hex | #1e1e2e | Couleur de fond de la zone de contenu |
text_color | hex | #e2e8f0 | Couleur du texte dans la zone de contenu |
default_tab | int | 0 | Index (0-based) de l'onglet affiché par défaut |
border_radius | int | 7 | Arrondi des onglets en pixels |
font_size | int | 13 | Taille de police des onglets en pixels |
padding_h | int | 14 | Padding horizontal des onglets en pixels |
padding_v | int | 6 | Padding vertical des onglets en pixels |
tab_gap | int | 2 | Espacement entre les onglets en pixels |
content_padding | int | 12 | Padding intérieur de la zone de contenu |
content_font_size | int | 13 | Taille de police du contenu en pixels |
full_width | bool | false | Onglets en pleine largeur (répartis équitablement) |
show_border | bool | true | Afficher la bordure autour de la zone de contenu |
custom_id | str | null | Déclenche un événement tab_select à chaque changement d'onglet |
Paramètres d'un onglet
| Champ | Type | Défaut | Description |
|---|---|---|---|
label | str | — | Texte affiché dans l'onglet |
icon | str | null | Emoji ou icône affiché avant le label |
content | str | — | Contenu affiché quand l'onglet est actif. Supporte \n |
custom_id pour recevoir un événement tab_select dans ton bot à chaque fois qu'un utilisateur change d'onglet. values[0] = index, values[1] = label de l'onglet cliqué.Exemples
self.send_message(channel,
components=[{
"type": "tabs",
"color": "#5865f2",
"tabs": [
{"label": "Infos", "icon": "ℹ️", "content": "Bienvenue sur le serveur !"},
{"label": "Stats", "icon": "📊", "content": "500 membres · 12 salons"},
{"label": "Règles", "icon": "📜", "content": "1. Sois respectueux\n2. Pas de spam"},
],
}])self.send_message(channel,
components=[{
"type": "tabs",
"color": "#00d9a3",
"tabs": [
{"label": "Statut", "icon": "🟢", "content": "En ligne depuis 3j 14h · Latence : 42 ms"},
{"label": "Commandes", "icon": "⚙️", "content": "32 commandes disponibles · /help pour la liste"},
{"label": "Logs", "icon": "📋", "content": "Dernière action : ban #user123 il y a 2 min."},
],
}])self.send_message(channel,
components=[{
"type": "tabs",
"color": "#f59e0b",
"custom_id": "shop_tabs",
"tabs": [
{"label": "Packs", "icon": "🛒", "content": "Starter 2,99 € · Pro 6,99 € · Elite 14,99 €"},
{"label": "Cosmétiques", "icon": "🎨", "content": "Avatars · Bannières · Effets de pseudo"},
{"label": "Boosts", "icon": "⚡", "content": "XP x2 · Coins x3 · Invocations gratuites"},
{"label": "FAQ", "icon": "❓", "content": "Paiement sécurisé · Remboursement 7j · Support 24/7"},
],
}])self.send_message(channel,
components=[{
"type": "tabs",
"color": "#a855f7",
"tabs": [
{"label": "Stats", "icon": "⚔️", "content": "Force : 87 · Agilité : 65 · Magie : 42", "open": True},
{"label": "Équip.", "icon": "🛡️", "content": "Épée légendaire · Armure en mithril"},
{"label": "Quêtes", "icon": "📜", "content": "3 actives · 47 terminées · 2 échouées"},
{"label": "Guilde", "icon": "🏰", "content": "Ordre du Soleil — Grade : Capitaine"},
],
}])def on_button_click(self, interaction):
if interaction['custom_id'] == 'shop_tabs':
idx = interaction['values'][0] # index de l'onglet
label = interaction['values'][1] # label de l'onglet
self.send_message(channel, f"Onglet '{label}' ouvert (index {idx})")Accordéon — accordion
Sections dépliables/repliables avec animation fluide. Idéal pour FAQ, aide, règles du serveur ou contenu structuré.
⚡ Préréglages
🧪 Démo — Personnalise en temps réel
Paramètres globaux
| Champ | Type | Défaut | Description |
|---|---|---|---|
type | str | — | Toujours "accordion" |
sections | list | — | Liste de sections (voir tableau ci-dessous) |
color | hex | #00d9a3 | Couleur d'accentuation (fond ouvert + flèche) |
custom_id | str | null | Déclenche un événement bot à l'ouverture si défini |
Paramètres d'une section
| Champ | Type | Défaut | Description |
|---|---|---|---|
title | str | — | Titre cliquable de la section |
content | str | — | Contenu affiché à l'ouverture |
open | bool | False | Ouverte par défaut à l'affichage |
icon | str | null | Emoji ou icône affiché avant le titre |
custom_id pour recevoir un événement accordion_open dans ton bot chaque fois qu'un utilisateur ouvre une section. Utile pour des FAQ analytiques ou du contenu dynamique.Exemples
self.send_message(channel,
components=[{
"type": "accordion",
"color": "#00d9a3",
"sections": [
{"title": "Comment rejoindre ?", "icon": "❓", "content": "Clique sur Rejoindre...", "open": True},
{"title": "Prix de l'abonnement", "icon": "💳", "content": "4,99 €/mois, annulable."},
{"title": "Contacter le support", "icon": "🆘", "content": "DM à @support ou #aide."},
],
}])self.send_message(channel,
components=[{
"type": "accordion",
"color": "#ef4444",
"sections": [
{"title": "Règle 1 — Respect", "icon": "🤝", "content": "Traite chaque membre avec respect.", "open": True},
{"title": "Règle 2 — Spam", "icon": "🚫", "content": "Le spam entraîne un mute automatique."},
{"title": "Règle 3 — NSFW", "icon": "🔞", "content": "Contenu adulte interdit hors canaux dédiés."},
{"title": "Règle 4 — Pub", "icon": "📢", "content": "Toute publicité nécessite l'accord des admins."},
],
}])self.send_message(channel,
components=[{
"type": "accordion",
"color": "#f59e0b",
"custom_id": "shop_faq",
"sections": [
{"title": "Pack Starter — 2,99 €", "icon": "🥉", "content": "50 coins + badge exclusif + accès salon VIP.", "open": True},
{"title": "Pack Pro — 6,99 €", "icon": "🥇", "content": "200 coins + rôle Pro + avatar animé."},
{"title": "Pack Elite — 14,99 €", "icon": "💎", "content": "Accès illimité + bot privé + support 24/7."},
],
}])self.send_message(channel,
components=[{
"type": "accordion",
"color": "#3b82f6",
"sections": [
{"title": "Le bot ne répond pas", "icon": "⚙️", "content": "Vérifie les permissions du canal et redémarre avec /restart."},
{"title": "Commandes inconnues", "icon": "❓", "content": "Tape /help pour voir toutes les commandes disponibles."},
{"title": "Erreur de permission", "icon": "🔒", "content": "Le bot doit avoir les rôles admin et gestionnaire de canaux."},
],
}])self.send_message(channel,
components=[{
"type": "accordion",
"color": "#a855f7",
"sections": [
{"title": "Faction : Ordre du Soleil", "icon": "☀️", "content": "Paladins et mages de lumière. Bonus : +20% défense.", "open": True},
{"title": "Faction : Ombre Nocturne", "icon": "🌑", "content": "Assassins furtifs. Bonus : +30% vitesse + invisibilité."},
{"title": "Faction : Ingénieurs", "icon": "⚙️", "content": "Constructeurs et inventeurs. Bonus : tourelles auto."},
{"title": "Faction : Gardiens Nature", "icon": "🌿", "content": "Druides et bêtes sauvages. Bonus : régénération +15%."},
],
}])Carrousel images — carousel
Galerie d'images défilante avec flèches ‹/›, dots pill cliquables, légende superposée, overlay gradient, compteur et autoplay configurable. Chaque paramètre est modifiable en temps réel.
⚡ Préréglages
🧪 Démo — Personnalise en temps réel
Champs globaux
| Champ | Type | Défaut | Description |
|---|---|---|---|
images | list | — | Liste d'images (voir tableau ci-dessous) |
color | hex | #f59e0b | Couleur des dots actifs et hover des flèches |
width | int | 360 | Largeur max du carrousel (px) |
height | int | 180 | Hauteur des images (px) |
border_radius | int | 10 | Arrondi des coins (px) |
autoplay | int | — | Défilement auto en ms (ex: 3000). Reprend après clic manuel. |
show_captions | bool | True | Afficher les légendes superposées |
show_arrows | bool | True | Afficher les flèches ‹/› |
show_dots | bool | True | Afficher les dots de navigation |
show_counter | bool | True | Badge compteur X/N en haut à droite |
overlay_gradient | bool | True | Dégradé sombre en bas des images |
shadow | bool | False | Ombre portée sous le carrousel |
Champs d'une image
| Champ | Type | Description |
|---|---|---|
url | str | URL publique de l'image |
caption | str | Légende superposée (optionnel) |
link_url | str | URL ouverte au clic sur l'image (défaut : image en grand) |
self.send_message(channel,
components=[{
"type": "carousel",
"color": "#10d9a3",
"autoplay": 3000,
"overlay_gradient": True,
"shadow": True,
"images": [
{"url": "https://example.com/alpes.jpg", "caption": "🏔️ Alpes suisses"},
{"url": "https://example.com/ocean.jpg", "caption": "🌊 Côte sauvage"},
{"url": "https://example.com/foret.jpg", "caption": "🌲 Forêt ancienne"},
],
}])self.send_message(channel,
components=[{
"type": "carousel",
"color": "#f59e0b",
"width": 420,
"height": 220,
"border_radius": 14,
"show_captions": False,
"show_counter": False,
"images": [
{"url": "https://example.com/ui1.jpg", "link_url": "https://behance.net/project1"},
{"url": "https://example.com/ui2.jpg", "link_url": "https://behance.net/project2"},
{"url": "https://example.com/ui3.jpg", "link_url": "https://behance.net/project3"},
],
}])self.send_message(channel,
components=[{
"type": "carousel",
"color": "#a855f7",
"shadow": True,
"images": [
{"url": "https://example.com/g1.jpg", "caption": "⚔️ Combat épique"},
{"url": "https://example.com/g2.jpg", "caption": "🗺️ Monde ouvert"},
{"url": "https://example.com/g3.jpg", "caption": "🏆 Victoire !"},
{"url": "https://example.com/g4.jpg", "caption": "🎯 Objectif atteint"},
],
}])self.send_message(channel,
components=[{
"type": "carousel",
"color": "#ec4899",
"show_arrows": False,
"show_dots": False,
"autoplay": 4000,
"border_radius": 0,
"images": [
{"url": "https://example.com/art1.jpg"},
{"url": "https://example.com/art2.jpg"},
{"url": "https://example.com/art3.jpg"},
],
}])self.send_message(channel,
"Voici les captures de la dernière session de jeu !",
title="🎮 Session du soir — highlights",
msg_color="#a855f7",
components=[{
"type": "carousel",
"color": "#a855f7",
"height": 200,
"overlay_gradient":True,
"shadow": True,
"images": [
{"url": "https://example.com/sc1.jpg", "caption": "Kill #47 🔥"},
{"url": "https://example.com/sc2.jpg", "caption": "Snipe parfait 🎯"},
],
}])autoplay est actif et que l'utilisateur clique une flèche ou un dot, l'intervalle redémarre depuis zéro — pas de saut brusque. Privilégie un intervalle ≥ 2500 ms pour laisser le temps de lire les légendes.Lecteur média — media_player
Lecteur audio ou vidéo HTML5 intégré dans le message, avec titre et pochette optionnels.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
url | str | — | URL du fichier audio ou vidéo |
media_type | audio / video | auto-détecté | Type de média |
title | str | — | Titre affiché |
cover | url | — | Pochette (audio seulement) |
color | hex | #00d9a3 | Couleur de la barre de lecture |
Exemple
self.send_message(channel_id, components=[
{"type": "media_player",
"url": "https://example.com/music.mp3",
"media_type": "audio",
"title": "🎵 Ma playlist du soir",
"cover": "https://example.com/cover.jpg",
"color": "#8b5cf6"}
])Carte lien — link_card
Prévisualisation enrichie d'un lien web : image, titre, description, favicon et URL.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
url | str | — | URL de destination |
title | str | URL | Titre du lien |
description | str | — | Résumé ou extrait |
image | url | — | Image de prévisualisation |
favicon | url | — | Icône du site |
site_name | str | — | Nom du site |
color | hex | #5865f2 | Couleur de la bordure haute |
Exemple
self.send_message(channel_id, components=[
{"type": "link_card",
"url": "https://lenavire.xyz",
"title": "LeNavire — Plateforme de communication",
"description": "Créez des serveurs, des bots et des expériences riches.",
"image": "https://lenavire.xyz/banner.png",
"site_name": "lenavire.xyz",
"color": "#00d9a3"}
])Curseur — slider
Barre glissante pour sélectionner une valeur numérique. L'interaction slider_change est déclenchée au clic sur ✓.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
min | int | 0 | Valeur minimale |
max | int | 100 | Valeur maximale |
step | int | 1 | Pas |
value | int | (min+max)/2 | Valeur initiale |
unit | str | — | Unité affichée après la valeur (ex: "%") |
label | str | — | Texte au-dessus |
color | hex | #00d9a3 | Couleur d'accent |
custom_id | str | auto | ID pour slider_change |
Interaction reçue
slider_changevalues[0] = valeur sélectionnée en string.Exemple
self.send_message(channel_id, components=[
{"type": "slider",
"label": "🔊 Volume", "min": 0, "max": 100, "value": 50,
"unit": "%", "color": "#8b5cf6", "custom_id": "vol"}
])
def on_button_click(self, interaction):
if interaction['type'] == 'slider_change':
vol = interaction['values'][0]
self.send_message(interaction['channel_id'], f"Volume réglé à {vol}% 🔊")Confirmation — confirm
Boîte de dialogue avec texte de prompt et deux boutons Confirmer / Annuler.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
prompt | str | — | Question / texte explicatif |
ok_label | str | "✅ Confirmer" | Texte du bouton oui |
no_label | str | "❌ Annuler" | Texte du bouton non |
ok_color | hex | #22c55e | Couleur du bouton oui |
no_color | hex | #ef4444 | Couleur du bouton non |
custom_id | str | auto | ID pour les interactions |
Interactions reçues
confirm_yes et confirm_novalues[0] = "confirm_yes" ou "confirm_no".Exemple
self.send_message(channel_id, components=[
{"type": "confirm",
"prompt": "⚠️ Veux-tu vraiment réinitialiser ton score ?",
"ok_label": "🗑️ Oui, supprimer", "ok_color": "#ef4444",
"no_label": "Annuler", "no_color": "#5865f2",
"custom_id": "reset_score"}
])
def on_button_click(self, interaction):
if interaction['type'] == 'confirm_yes':
self.send_message(interaction['channel_id'], "Score réinitialisé ! 🗑️")
elif interaction['type'] == 'confirm_no':
self.send_message(interaction['channel_id'], "Annulé. Ton score est préservé ✅")Saisie numérique — number_input
Contrôle +/− avec limite min/max, pas configurable et bouton d'envoi.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
min | int | 0 | Valeur minimale |
max | int | ∞ | Valeur maximale |
step | int | 1 | Incrément |
value | int | min | Valeur initiale |
unit | str | — | Unité (ex: " pts") |
label | str | — | Label affiché au-dessus |
color | hex | #5865f2 | Couleur d'accent |
custom_id | str | auto | ID pour number_input_change |
Interaction reçue
number_input_changevalues[0] = valeur sélectionnée en string.Exemple
self.send_message(channel_id, components=[
{"type": "number_input",
"label": "Combien de jetons veux-tu utiliser ?",
"min": 1, "max": 100, "value": 10, "step": 5,
"unit": " 🪙", "color": "#f59e0b", "custom_id": "jetons"}
])
def on_button_click(self, interaction):
if interaction['type'] == 'number_input_change':
n = interaction['values'][0]
self.send_message(interaction['channel_id'], f"Tu as utilisé {n} jetons 🪙")Menu déroulant — dropdown
Composant de sélection stylisé utilisable hors ActionRow Discord. Supporte emoji, descriptions, multi-sélection et placeholder personnalisé.
min_values / max_values pour N choix.dropdown_select dans on_button_click.🧪 Démonstration interactive
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
options | list | — | Liste d'options : str ou objet {"label","value","emoji","description","default","disabled"} |
placeholder | str | — | Texte affiché si aucune option choisie |
label | str | — | Titre au-dessus du menu |
color | hex | #00d9a3 | Couleur de la bordure / highlight |
custom_id | str | auto | ID reçu dans on_button_click (type dropdown_select) |
min_values | int | 1 | Nombre minimum d'options à sélectionner |
max_values | int | 1 | Nombre maximum (>1 = multi-sélection) |
| — Objet option — | |||
option.label | str | — | Texte affiché dans la liste |
option.value | str | — | Valeur reçue dans interaction['values'] |
option.emoji | str | — | Icône devant le label |
option.description | str | — | Sous-texte grisé sous le label |
option.default | bool | — | Pré-sélectionne cette option |
option.disabled | bool | — | Griser / rendre non-sélectionnable |
Interaction reçue
dropdown_selectvalues est toujours une liste. Mono-sélection → values[0]. Multi-sélection → liste complète.self.send_message(channel_id, components=[
{"type": "dropdown",
"label": "🌍 Choisir une région",
"placeholder": "-- Sélectionner --",
"custom_id": "region",
"options": [
{"label": "Europe", "value": "eu", "emoji": "🇪🇺", "description": "Paris, Berlin, Madrid..."},
{"label": "Amérique", "value": "us", "emoji": "🌎", "description": "New York, São Paulo..."},
{"label": "Asie", "value": "as", "emoji": "🌏", "description": "Tokyo, Séoul, Singapour..."},
{"label": "Afrique", "value": "af", "emoji": "🌍", "description": "Dakar, Le Caire, Nairobi..."},
]}
])
def on_button_click(self, interaction):
if interaction.get('type') == 'dropdown_select':
region = interaction['values'][0]
user = interaction['user']['username']
self.send_message(interaction['channel_id'],
f"🌐 {user} a sélectionné **{region}**")self.send_message(channel_id, components=[
{"type": "dropdown",
"label": "🔔 Notifications à activer",
"custom_id": "notif_dd",
"min_values": 1,
"max_values": 4,
"options": [
{"label": "Messages directs", "value": "dm", "emoji": "💬", "default": True},
{"label": "Mentions", "value": "mention", "emoji": "🔔"},
{"label": "Annonces", "value": "news", "emoji": "📢"},
{"label": "Récompenses", "value": "reward", "emoji": "🎁"},
]}
])
def on_button_click(self, interaction):
if interaction.get('custom_id') == 'notif_dd':
choices = interaction['values'] # liste de valeurs
self.send_message(interaction['channel_id'], embed={
"title": "✅ Préférences sauvegardées",
"description": "• " + "\n• ".join(choices),
"color": "#00d9a3"
})self.send_message(channel_id, components=[
{"type": "dropdown",
"label": "🏆 Plan d'abonnement",
"placeholder": "— Sélectionner —",
"custom_id": "plan",
"color": "#8875f5",
"options": [
{"label": "Free", "value": "free", "emoji": "🆓", "default": True},
{"label": "Pro", "value": "pro", "emoji": "⭐"},
{"label": "Business", "value": "business", "emoji": "🚀"},
{"label": "Enterprise", "value": "enterprise", "emoji": "🏢",
"description": "Sur devis — contactez-nous", "disabled": True},
]}
])dropdown est un composant autonome rendu dans le chat. Il génère des interactions de type dropdown_select. select_menu s'intègre dans une ActionRow Discord et génère du type select_menu. Les deux partagent la même structure d'options.Sélecteur de couleur — color_picker
Palette de swatches + champ de saisie libre pour choisir une couleur hex.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
palette | list | 8 couleurs | Swatches de raccourci |
value | hex | #00d9a3 | Couleur initiale |
label | str | — | Label au-dessus |
color | hex | #00d9a3 | Couleur du bouton Valider |
custom_id | str | auto | ID pour color_pick |
Interaction reçue
color_pickvalues[0] = couleur hex (ex: "#ef4444").Exemple
self.send_message(channel_id, components=[
{"type": "color_picker",
"label": "🎨 Couleur de ton rôle",
"value": "#8b5cf6",
"custom_id": "role_color"}
])Sélecteur de date — date_picker
Champ date ou date+heure natif avec min/max et validation.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
include_time | bool | false | Inclure l'heure |
value | str | — | Valeur initiale (YYYY-MM-DD) |
min | str | — | Date minimale sélectionnable |
max | str | — | Date maximale sélectionnable |
label | str | — | Label au-dessus |
color | hex | #5865f2 | Couleur du bouton Valider |
custom_id | str | auto | ID pour date_pick |
Interaction reçue
date_pickvalues[0] = date au format ISO (ex: "2026-03-15T14:30").Exemple
self.send_message(channel_id, components=[
{"type": "date_picker",
"label": "📅 Date du rendez-vous",
"include_time": True,
"min": "2026-01-01",
"custom_id": "rdv"}
])Liste à cocher — checklist
Cases à cocher multiples avec sélection pré-remplie et bouton de validation.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
items | list | — | Items : str ou {"label","value","emoji"} |
checked | list | [] | Valeurs pré-cochées |
label | str | — | Titre au-dessus |
submit_label | str | "✓ Valider" | Texte du bouton |
color | hex | #00d9a3 | Couleur d'accent |
custom_id | str | auto | ID pour checklist_change |
Interaction reçue
checklist_changevalues = tableau des valeurs cochées.Exemple
self.send_message(channel_id, components=[
{"type": "checklist",
"label": "⚙️ Notifications à activer",
"checked": ["dm"],
"custom_id": "notifs",
"items": [
{"label": "Messages directs", "value": "dm", "emoji": "💬"},
{"label": "Mentions", "value": "mention", "emoji": "🔔"},
{"label": "Annonces", "value": "news", "emoji": "📢"},
]}
])Étapes — progress_steps
Barre de progression horizontale par étapes numérotées (purement visuelle).
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
steps | list | — | Étapes : str ou {"label"} |
current | int | 0 | Index de l'étape courante (0-based) |
label | str | — | Titre au-dessus |
color | hex | #00d9a3 | Couleur des étapes complétées |
Exemple
self.send_message(channel_id, components=[
{"type": "progress_steps",
"label": "Commande #2847",
"current": 2,
"color": "#22c55e",
"steps": [
{"label": "Reçue"},
{"label": "En préparation"},
{"label": "Expédiée"},
{"label": "Livrée"},
]}
])Saisie de tags — tag_input
Widget d'ajout/suppression de tags dynamique. Chaque action déclenche une interaction en temps réel.
Ajout par touche Entrée ou bouton, suppression par ✕ sur chaque tag.
Couleur, forme (round/square/pill), taille, pills pleines ou transparentes.
preset_tags : chips cliquables pour ajouter un tag en un clic.
Limite max, longueur par tag, doublons optionnels, tri automatique.
🧪 Démonstration interactive
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
tags | list | [] | Tags déjà présents à l'affichage |
max | int | 10 | Nombre maximal de tags |
max_length | int | 30 | Longueur max d'un tag (caractères) |
placeholder | str | "Ajouter…" | Texte indicatif du champ vide |
label | str | — | Label affiché au-dessus |
color | hex | #00d9a3 | Couleur des pills |
size | sm / md / lg | md | Taille globale du widget |
shape | round / square / pill | round | Forme des pills |
filled | bool | False | Pills à fond plein (au lieu de transparent) |
sorted | bool | False | Trie les tags alphabétiquement |
allow_duplicates | bool | False | Autorise les tags en double |
preset_tags | list | — | Suggestions cliquables sous le champ pour ajouter en un clic |
custom_id | str | auto | ID reçu dans tag_add / tag_remove |
Interactions reçues
tag_add et tag_removevalues[0] = tag concerné. Déclenché à chaque ajout ou suppression individuel.Exemples
{"type": "tag_input", "label": "🏷️ Tes centres d'intérêt",
"tags": ["Python"], "max": 5, "custom_id": "interests"}{"type": "tag_input", "label": "🎮 Jeux préférés",
"color": "#5865f2", "filled": True, "shape": "pill",
"preset_tags": ["Minecraft", "Valorant", "LoL", "FIFA"],
"max": 4, "custom_id": "games"}def on_button_click(self, interaction):
if interaction['custom_id'] == 'interests':
tag = interaction['values'][0]
if interaction['type'] == 'tag_add':
self.send_message(channel, f"Tag ajouté : {tag} ✅")
elif interaction['type'] == 'tag_remove':
self.send_message(channel, f"Tag supprimé : {tag} 🗑")Upload fichier — file_upload
Zone glisser-déposer ou clic pour sélectionner un fichier. Déclenche file_upload avec le nom, type et taille.
Démonstration
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
accept | str | tous | Types acceptés ex: ".pdf,.docx" ou "image/*" |
multiple | bool | false | Autoriser plusieurs fichiers |
placeholder | str | — | Texte de la zone de dépôt |
label | str | — | Label au-dessus |
color | hex | #5865f2 | Couleur de la bordure |
custom_id | str | auto | ID pour file_upload |
Interaction reçue
file_uploadvalues[0] = nom, values[1] = type MIME, values[2] = taille en octets.Exemple
self.send_message(channel_id, components=[
{"type": "file_upload",
"label": "📄 Envoie ton CV",
"accept": ".pdf",
"placeholder": "Glisse ton fichier PDF ici",
"custom_id": "cv"}
])Carte utilisateur — user_card
Profil visuel complet : bannière dégradée, avatar, bio, rôle, badges, stats, niveau avec barre XP.
Couleur, dégradé 2 tons ou image URL. Avatar lettre auto ou image.
Dictionnaire de stats, niveau badge + barre de progression XP.
Liste de badges colorés, rôle / titre sous le pseudo, indicateur de statut.
Mode compact, couleur d'accent indépendante, texte de statut, footer.
🧪 Démonstration interactive
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
username | str | — | Pseudo affiché |
display_name | str | — | Nom d'affichage (sous le pseudo) |
role | str | — | Rôle / titre coloré affiché sous le pseudo |
avatar | url | lettre auto | URL de l'image d'avatar |
bio | str | — | Courte biographie (max ~120 caractères recommandé) |
online | bool | — | Indicateur vert (en ligne) / gris (hors ligne) |
status_text | str | — | Texte de statut (ex : "En train de coder 💻") |
banner_color | hex | #8b5cf6 | Couleur unie du bandeau haut |
banner_gradient | [hex,hex] | — | Dégradé de bannière (prioritaire sur banner_color) |
banner_image | url | — | Image URL pour la bannière (prioritaire sur tout) |
accent_color | hex | = banner_color | Couleur d'accent des badges, stats et rôle |
badges | list | — | Liste de {"label","icon","color"} |
stats | dict | — | Ex: {"Messages": 142, "Niveau": 7} |
level | int | — | Niveau affiché sous forme de badge coloré |
level_progress | int (0-100) | — | Barre de progression XP sous le niveau |
join_date | str | — | Texte date d'inscription (affiché en pied) |
footer | str | — | Texte de pied de carte |
compact | bool | False | Version réduite (bannière et padding réduits) |
Exemples
{"type": "user_card", "username": "alice", "role": "Modératrice",
"bio": "Passionnée de code et d'IA 🤖", "online": True,
"banner_gradient": ["#8b5cf6", "#3b82f6"],
"badges": [{"label": "Fondatrice", "icon": "⭐", "color": "#f59e0b"}],
"stats": {"Messages": 1420, "Niveau": 12},
"level": 12, "level_progress": 65}{"type": "user_card", "username": "bob",
"banner_color": "#ef4444", "compact": True,
"online": False, "join_date": "Membre depuis janvier 2025",
"stats": {"Score": 8420, "Rang": "#3"}}Classement — leaderboard
Tableau de scores avec podium, barres de visualisation, icônes par entrée et styles avancés.
🥇🥈🥉 automatiques pour le top 3. Numéros #N pour le reste.
bar_mode : barres horizontales proportionnelles au score maximum.
Icône emoji, sous-titre, couleur de nom et mise en surbrillance par entrée.
Mode compact, limite, label de colonne personnalisé, tri automatique.
🧪 Démonstration interactive
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
entries | list | — | Liste d'entrées (voir propriétés ci-dessous) |
title | str | — | Titre dans l'en-tête |
unit | str | — | Suffixe ajouté au score (ex: " pts") |
limit | int | 10 | Nombre max d'entrées affichées |
color | hex | #00d9a3 | Couleur d'accent |
column_label | str | — | Label de la colonne score (affiché dans l'en-tête à droite) |
bar_mode | bool | False | Affiche les scores sous forme de barres proportionnelles |
bar_color | hex | = color | Couleur des barres en mode bar_mode |
compact | bool | False | Réduit le padding de chaque ligne |
Propriétés d'une entrée
| Clé | Type | Description |
|---|---|---|
name | str | Nom du joueur / participant |
score | int/float | Score numérique |
icon | emoji | Icône affichée avant le nom |
subtitle | str | Petite ligne sous le nom (rôle, badge, niveau…) |
color | hex | Couleur du nom pour cette entrée uniquement |
highlight | bool | Met l'entrée en surbrillance ("c'est toi") |
Exemples
{"type": "leaderboard", "title": "🏆 Top Serveur", "unit": " pts", "color": "#f59e0b",
"entries": [
{"name": "alice", "score": 2340, "highlight": True, "icon": "👑"},
{"name": "bob", "score": 1820, "subtitle": "Niveau 8"},
{"name": "carol", "score": 1650},
]}{"type": "leaderboard", "title": "📊 Activité hebdo",
"bar_mode": True, "compact": True, "color": "#5865f2",
"column_label": "Messages",
"entries": [
{"name": "alice", "score": 342},
{"name": "bob", "score": 218},
{"name": "carol", "score": 187},
]}Chronologie — timeline
Événements verticaux avec connecteurs colorés, badges, icônes, statuts, descriptions, marqueurs temporels et mode compact. Idéal pour journaux d'activité, parcours utilisateur, historiques de commandes, étapes de build et roadmaps.
Chaque point peut avoir sa propre couleur via event.color — indépendamment de la couleur globale.
event.badge + badge_color, event.icon remplace le point par un emoji personnalisé.
solid, dashed, dotted — et 3 tailles de point : sm, md, lg.
event.status : done ✅, active ⚡ (pulsant), error ❌, pending ⏳ — affiché automatiquement.
compact: true réduit les espacements — parfait pour les timelines denses à nombreux événements.
event.text affiche un sous-texte descriptif sous le titre — support multi-lignes.
title affiche un en-tête au-dessus de la timeline, avec la couleur d'accent.
event.url rend le titre cliquable — idéal pour pointer vers un commit, ticket ou page.
🧪 Démonstration interactive
Paramètres globaux
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
events | list | [] | Liste d'événements à afficher dans l'ordre |
title | str | — | En-tête affiché au-dessus de la timeline, coloré avec line_color |
line_color | hex | #8875f5 | Couleur globale du connecteur et des points |
connector_style | str | solid | solid / dashed / dotted |
dot_size | str | md | sm (10 px) · md (14 px) · lg (18 px) |
compact | bool | false | Réduit les espacements entre événements |
Paramètres d'un événement
| Clé | Type | Défaut | Description |
|---|---|---|---|
title | str | — | Titre principal de l'événement |
text | str | — | Description / sous-texte sous le titre |
date | str | — | Horodatage ou date affichée en petit à gauche du badge |
color | hex | line_color | Couleur du point pour cet événement uniquement |
icon | emoji/str | — | Remplace le point par un emoji (ex: 🚀) |
badge | str | — | Étiquette affichée à côté de la date |
badge_color | hex | event.color | Couleur du badge |
status | str | — | done · active (pulsant) · pending · error — icône automatique dans le titre |
url | str | — | Rend le titre cliquable (lien externe) |
status: "active" pour l'étape en cours — son point pulse automatiquement. Combine icon + color par événement pour des timelines très colorées. Pour les logs de build, connector_style: "dashed" + compact: true donne un rendu dense et lisible.Exemples
self.send_message(channel_id, components=[{
"type": "timeline",
"title": "👤 Parcours de alice",
"line_color": "#8875f5",
"connector_style": "dashed",
"events": [
{"title": "Inscription", "date": "01/01/2026", "icon": "🎯", "badge": "Nouveau", "badge_color": "#22c55e", "status": "done"},
{"title": "Premier message", "date": "15/01/2026", "text": "Bienvenue sur le serveur !", "color": "#00d9a3", "status": "done"},
{"title": "Niveau 10 atteint", "date": "02/02/2026", "icon": "⭐", "badge": "XP", "badge_color": "#f59e0b", "status": "active"},
{"title": "Badge Fondateur", "color": "#8b5cf6", "icon": "🏆", "status": "pending"},
]
}])self.send_message(channel_id, components=[{
"type": "timeline",
"title": "🚀 Déploiement v2.1 — pipeline",
"line_color": "#22c55e",
"connector_style": "solid",
"dot_size": "sm",
"compact": True,
"events": [
{"title": "Checkout", "date": "14:31:02", "icon": "📦", "status": "done", "text": "Branch: main @ abc1234"},
{"title": "Install deps", "date": "14:31:08", "icon": "📥", "status": "done", "text": "npm ci — 127 packages"},
{"title": "Tests unitaires", "date": "14:31:44", "icon": "🧪", "status": "done", "text": "48 passed, 0 failed"},
{"title": "Build", "date": "14:32:01", "icon": "🔨", "status": "active", "text": "Compilation en cours…"},
{"title": "Deploy prod", "icon": "🚀", "status": "pending"},
]
}])self.send_message(channel_id, components=[{
"type": "timeline",
"title": "📦 Commande #2847",
"line_color": "#f59e0b",
"dot_size": "lg",
"events": [
{"title": "Commande reçue", "date": "07/03 10:12", "icon": "✅", "status": "done", "badge": "Confirmé", "badge_color": "#22c55e"},
{"title": "En préparation", "date": "07/03 11:30", "icon": "📦", "status": "done", "text": "Entrepôt Paris Nord"},
{"title": "Expédiée", "date": "07/03 14:00", "icon": "🚚", "status": "active", "text": "Transporteur DHL — TRK928374"},
{"title": "Livraison prévue", "date": "08/03", "icon": "🏠", "status": "pending"},
]
}])self.send_message(channel_id, components=[{
"type": "timeline",
"title": "🚨 Incident #4821 — Stripe",
"line_color": "#ef4444",
"connector_style": "dotted",
"events": [
{"title": "Alerte détectée", "date": "03:12 UTC", "icon": "🔴", "status": "error", "text": "Timeouts Stripe API > 30s", "color": "#ef4444"},
{"title": "Équipe notifiée", "date": "03:14 UTC", "icon": "📣", "status": "done", "text": "On-call @devops alerté"},
{"title": "Diagnostic en cours", "date": "03:17 UTC", "icon": "🔍", "status": "active", "text": "Analyse des logs et traces", "color": "#f59e0b"},
{"title": "Résolution", "icon": "✅", "status": "pending"},
]
}])self.send_message(channel_id, components=[{
"type": "timeline",
"title": "🗺️ Roadmap LeNavire SDK",
"line_color": "#8b5cf6",
"connector_style": "dashed",
"dot_size": "lg",
"events": [
{"title": "v1.0 — Sortie publique", "date": "Janv. 2025", "icon": "🚀", "status": "done", "badge": "Publié", "badge_color": "#22c55e"},
{"title": "v1.5 — Composants UI", "date": "Juin 2025", "icon": "🧩", "status": "done", "badge": "Publié", "badge_color": "#22c55e", "text": "Timeline, Leaderboard, Tag Input…"},
{"title": "v2.0 — Message enrichi", "date": "Mars 2026", "icon": "✨", "status": "active", "badge": "En cours", "badge_color": "#f59e0b", "text": "meta, sticker, code, banner…"},
{"title": "v2.5 — Marketplace", "date": "Été 2026", "icon": "🛍️","status": "pending", "badge": "Prévu", "badge_color": "#8b5cf6"},
{"title": "v3.0 — IA native", "date": "2027", "icon": "🤖", "status": "pending", "badge": "Vision", "badge_color": "#5865f2"},
]
}])Clé-Valeur — key_value
Tableau propriétés/valeurs ultra-personnalisable : layout liste ou grille, lignes alternées, couleurs globales et par paire, icônes, compact mode, monospace.
list vertical ou grid sur 2 colonnes pour afficher plus d'infos en moins d'espace.
striped: true — fond subtil sur les lignes paires pour faciliter la lecture en scan.
key_color, value_color, background, border_color — palette complète.
pair.color override individuel : mets en rouge les erreurs, en vert les succès.
pair.bold_value: true met la valeur d'une paire spécifique en gras pour la mettre en avant.
pair.icon ajoute un emoji ou symbole devant la clé pour un scan visuel rapide.
compact: true réduit le padding des lignes — idéal pour les tableaux denses avec 6+ paires.
monospace: true — police Consolas parfaite pour versions, hashes Git, adresses IP.
🧪 Démonstration interactive
Paramètres globaux
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
pairs | list | [] | Liste de paires clé/valeur |
title | str | — | En-tête affiché en majuscule au-dessus du tableau |
layout | str | list | list (vertical) ou grid (2 colonnes côte à côte) |
striped | bool | false | Fond subtil sur les lignes paires pour la lisibilité |
compact | bool | false | Padding réduit — idéal pour afficher 6+ paires |
monospace | bool | false | Police Consolas pour clés et valeurs |
background | hex/rgba | rgba(…,.04) | Couleur de fond du tableau |
border_color | hex/rgba | rgba(…,.08) | Couleur des séparateurs et bordure externe |
key_color | hex | rgba(…,.45) | Couleur du texte de toutes les clés |
value_color | hex | rgba(…,.85) | Couleur globale des valeurs (remplacé par pair.color) |
Paramètres par paire
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
key | str | — | Nom de la propriété (affiché à gauche) |
value | str | — | Valeur à afficher (à droite ou en-dessous) |
icon | str | — | Emoji/symbole préfixant la clé — ex: "🔴", "✅" |
color | hex | value_color | Couleur individuelle de la valeur (override global) |
bold_value | bool | false | Met la valeur en gras (font-weight: 700) |
pair.color s'applique seulement si aucun value_color global n'est défini. Pour des couleurs individuelles, laisse value_color vide. Combine compact: true + monospace: true pour les sorties techniques (logs, API).Exemples
self.send_message(channel_id, components=[{
"type": "key_value",
"title": "🖥️ Infos système",
"striped": True,
"key_color": "#7070a0",
"pairs": [
{"key": "Version", "value": "2.1.0", "bold_value": True, "icon": "🔖"},
{"key": "CPU", "value": "42%", "color": "#22c55e", "icon": "💻"},
{"key": "Mémoire", "value": "1.2 GB", "icon": "🧠"},
{"key": "Uptime", "value": "3j 14h", "icon": "⏱️"},
{"key": "Statut", "value": "OK", "color": "#22c55e", "bold_value": True, "icon": "✅"},
]
}])self.send_message(channel_id, components=[{
"type": "key_value",
"title": "🎮 Stats de alice",
"layout": "grid",
"striped": True,
"pairs": [
{"key": "Parties", "value": "142", "icon": "🎯"},
{"key": "Victoires", "value": "89", "color": "#22c55e", "bold_value": True, "icon": "🏆"},
{"key": "Défaites", "value": "53", "color": "#ef4444", "icon": "💀"},
{"key": "KD Ratio", "value": "2.3", "bold_value": True, "icon": "⚔️"},
{"key": "Rang", "value": "Diamant", "color": "#60d9fa", "bold_value": True, "icon": "💎"},
{"key": "Streak", "value": "5 wins", "color": "#f59e0b", "icon": "🔥"},
]
}])self.send_message(channel_id, components=[{
"type": "key_value",
"title": "🤖 Profil — ModBot",
"compact": True,
"monospace": True,
"background": "#0a0a12",
"border_color": "#8875f533",
"key_color": "#8875f5",
"pairs": [
{"key": "ID", "value": "bot_7f2a9c"},
{"key": "Version", "value": "v3.2.1", "bold_value": True},
{"key": "Latence", "value": "14ms", "color": "#22c55e"},
{"key": "Serveurs", "value": "1 247"},
{"key": "Commandes", "value": "42 680"},
{"key": "Uptime", "value": "99.94%", "color": "#22c55e", "bold_value": True},
]
}])self.send_message(channel_id, components=[{
"type": "key_value",
"title": "📦 Dernier commit — main",
"compact": True,
"monospace": True,
"key_color": "#7070a0",
"pairs": [
{"key": "Hash", "value": "a3ef508", "bold_value": True},
{"key": "Auteur", "value": "alice"},
{"key": "Date", "value": "07/03 14:31"},
{"key": "Branch", "value": "main", "color": "#22c55e"},
{"key": "Message", "value": "fix: timeline preset icons", "bold_value": True},
{"key": "+/-", "value": "+42 / -7", "color": "#f59e0b"},
]
}])self.send_message(channel_id, components=[{
"type": "key_value",
"title": "🌍 Géolocalisation — 91.198.174.192",
"striped": True,
"background": "#0a1015",
"border_color": "#00d9a333",
"key_color": "#00d9a3",
"pairs": [
{"key": "Pays", "value": "🇫🇷 France", "bold_value": True},
{"key": "Ville", "value": "Paris"},
{"key": "FAI", "value": "Wikimedia Foundation"},
{"key": "Latitude", "value": "48.8534", "icon": "📍"},
{"key": "Longitude", "value": "2.3488", "icon": "📍"},
{"key": "Timezone", "value": "Europe/Paris"},
]
}])Diff de code — code_diff
Affiche un diff Git coloré : lignes ajoutées/supprimées, numéros de ligne, stats, 3 thèmes visuels.
Vert = ajout, rouge = suppression, bleu = hunk header, gris = contexte.
show_line_numbers : numéros à gauche, style GitHub.
show_stats : comptage +X -Y affiché dans l'en-tête du bloc.
dark (défaut), github (fond clair), nord (bleu-gris).
🧪 Démonstration interactive
Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
diff | str | — | Contenu du diff (+=ajout, -=suppression, @@=hunk) |
filename | str | — | Nom du fichier affiché dans l'en-tête |
language | str | — | Étiquette du langage (ex: "python", "javascript") |
theme | dark / github / nord | dark | Thème visuel du bloc |
show_line_numbers | bool | False | Affiche les numéros de ligne à gauche |
show_stats | bool | False | Affiche le comptage +X -Y dans l'en-tête |
max_height | int (px) | — | Hauteur max du bloc (défilement si dépassé) |
diff+ = ajoutée, - = supprimée, @@ = header de hunk, espace ou rien = contexte. Compatible format git diff standard.Exemples
{"type": "code_diff", "filename": "app.py",
"diff": "@@ -10,3 +10,4 @@\n def hello():\n- print('world')\n+ print('LeNavire')\n+ return True"}{"type": "code_diff", "filename": "bot.js",
"language": "javascript", "theme": "github",
"show_line_numbers": True, "show_stats": True,
"diff": "@@ -1,4 +1,5 @@\n const x = 1;\n-const y = 2;\n+const y = 3;\n+const z = 4;\n"}Message Riche — rich_message
6 styles de cartes riches — embed, notification, announcement, minimal, warning, success. Auteur + avatar, champs en grille, thumbnail, image pleine largeur, footer et timestamp. Le composant le plus complet et polyvalent du SDK LeNavire.
embed, notification, announcement, minimal, warning, success.
fields avec inline pour un affichage 2 colonnes flexible, jusqu'à 10 champs.
Auteur + avatar, miniature, image principale, pied de page + timestamp.
title_url rend le titre cliquable. button_label/button_url en mode announcement.
Styles pré-stylisés avec icône automatique, fond teinté et bordure colorée.
thumbnail (coin haut-droite) et image (largeur complète) intégrées dans l'embed.
compact=True réduit marges et padding — idéal dans les sidebars et interfaces denses.
Associe rich_message + button_row dans un même appel send_message pour des cards interactives.
🎨 Aperçu des styles
🧪 Démonstration interactive
📐 Anatomie d'un embed
color bande latérale · thumbnail coin haut-droite · image pleine largeur sous la descriptionauthor · author_icon · author_urltitle · title_urldescriptionfields [{name, value, inline}]thumbnail · imagefooter · footer_icon · timestampParamètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
style | str | embed | embed / notification / announcement / minimal / warning / success |
color | hex | #8875f5 | Couleur d'accent — bande latérale (embed), fond teinté (notification/warning/success) |
title | str | — | Titre principal |
title_url | str | — | Rend le titre cliquable (lien hypertexte) |
description | str | — | Corps du message (supporte le Markdown de base) |
author | str | — | Ligne auteur au-dessus du titre |
author_icon | url | — | Avatar circulaire à côté du nom d'auteur |
author_url | url | — | Lien sur le nom d'auteur |
fields | list | [] | Liste de {name, value, inline} — jusqu'à 10 champs en grille 2 col |
thumbnail | url | — | Petite image coin haut-droite (mode embed) |
image | url | — | Grande image pleine largeur sous la description |
footer | str | — | Pied de page (texte) |
footer_icon | url | — | Icône ronde dans le pied de page |
timestamp | str | — | Horodatage affiché à droite du footer (ex: "14:32", "now", ou ISO 8601) |
icon | emoji/str | — | Emoji principal (notification, announcement, warning, success) |
button_label | str | — | Texte du bouton d'action (mode announcement) |
button_url | str | — | URL du bouton |
button_color | hex | — | Couleur personnalisée du bouton announcement |
compact | bool | false | Réduit les marges internes pour les espaces contraints |
💡 Quand utiliser quel style ?
- Résultats CI/CD, déploiements
- Rapports multi-métriques avec champs
- Profils utilisateur, résumés d'entité
fields, author, thumbnail, title_url- Alertes basse priorité, rappels
- Nouveaux messages, événements
- Maintenance planifiée, mises à jour
icon, title, description, footer- Événements communautaires, concours
- Nouvelles fonctionnalités, releases
- Messages avec bouton d'action clair
icon, button_label, button_url, button_color- Logs, audit trail, historique
- Connexions / déconnexions
- Actions courtes ne nécessitant pas de détails
title, description, color- Erreurs critiques, services down
- Incidents, timeouts, sécurité
- Actions irréversibles à confirmer
icon, title, description, footer, timestamp- Confirmations de paiement, commandes
- Uploads réussis, actions complétées
- Bienvenues, inscriptions validées
icon, title, description, footer🔗 Combiner avec d'autres composants
Le composant rich_message peut être placé avec d'autres composants dans le même appel send_message.
# rich_message + boutons d'action dans le même message
self.send_message(channel_id, components=[
{
"type": "rich_message",
"style": "embed",
"color": "#8875f5",
"title": "🚀 Nouvelle version disponible",
"description": "LeNavire SDK v3.0 est disponible. Découvre les nouveautés.",
"fields": [
{"name": "Version", "value": "3.0.0", "inline": True},
{"name": "Breaking ?", "value": "Non", "inline": True},
{"name": "Highlights", "value": "Dark mode, perf +40%, API simplifiée", "inline": False},
],
"footer": "Publié le 7 mars 2026"
},
{
"type": "button_row",
"buttons": [
{"label": "📋 Changelog", "id": "changelog", "style": "primary"},
{"label": "⬇️ Mettre à jour", "id": "update", "style": "success"},
{"label": "❌ Ignorer", "id": "dismiss", "style": "secondary"}
]
}
])
def on_member_join(self, user):
welcome = self.get_channel_by_name("bienvenue")
if not welcome:
return
self.send_message(welcome["id"], components=[{
"type": "rich_message",
"style": "announcement",
"color": "#10d9a3",
"icon": "👋",
"title": f"Bienvenue {user['username']} !",
"description": "Tu viens de rejoindre LeNavire. Présente-toi dans #général !",
"button_label": "📖 Règlement",
"button_url": "https://lenavire.app/rules",
"button_color": "#10d9a3"
}])
# Envoyer le même rapport sur plusieurs channels (status, alertes...)
STATUS_CHANNELS = ["général", "ops", "monitoring"]
status_color = {"ok": "#22c55e", "warn": "#f59e0b", "error": "#ef4444"}
status_style = {"ok": "success", "warn": "notification", "error": "warning"}
def broadcast_status(self, level="ok", message=""):
for name in STATUS_CHANNELS:
ch = self.get_channel_by_name(name)
if ch:
self.send_message(ch["id"], components=[{
"type": "rich_message",
"style": status_style[level],
"color": status_color[level],
"title": f"Statut [{level.upper()}]",
"description": message,
"timestamp": "now"
}])
⚡ Bonnes pratiques
title_url à un bouton séparé pour les liens simples — ça garde la carte épurée et évite un button_row supplémentaire.
compact=True dans les sidebars, listes d'activité ou any interface qui affiche plusieurs messages d'affilée — le rendu reste propre sans surcharger l'espace vertical.
fields à 6 maximum en pratique — techniquement 10 sont supportés, mais au-delà la grille 2 colonnes perd en lisibilité. Regroupe les données dans la description si nécessaire.
announcement aux messages user-facing (annonces, concours). Pour les alertes système ou erreurs techniques, utilise warning — ça communique la bonne sémantique visuellement.
#3b82f6 pour les infos, orange #f59e0b pour les warnings légers, rouge #ef4444 pour les critiques. Les utilisateurs apprennent vite à lire la couleur avant le texte.
timestamp sur les messages liés à un événement daté (incident, déploiement, rapport) — ça évite la confusion sur "quand ça s'est passé" dans un long historique de messages.
📝 Exemples complets
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "embed",
"color": "#8875f5",
"author": "LeNavire Bot",
"title": "🚀 Déploiement v2.1 réussi",
"description": "La version 2.1 est en ligne sur le serveur de production.",
"fields": [
{"name": "Environnement", "value": "Production", "inline": True},
{"name": "Version", "value": "2.1.0", "inline": True},
{"name": "Durée build", "value": "1m 23s", "inline": False},
],
"footer": "Déployé via GitHub Actions",
"timestamp": "14:32"
}])
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "notification",
"color": "#f59e0b",
"icon": "🔔",
"title": "Maintenance prévue",
"description": "Le bot sera hors ligne de 02h00 à 02h30.",
"footer": "Planifié",
"timestamp": "01:45"
}])
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "announcement",
"color": "#10d9a3",
"icon": "🎉",
"title": "Tournoi LeNavire ce soir !",
"description": "Inscris-toi avant 20h. Lots exceptionnels à gagner !",
"button_label": "S'inscrire",
"button_url": "https://lenavire.app/tournoi",
"button_color": "#10d9a3"
}])
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "warning",
"color": "#ef4444",
"icon": "⚠️",
"title": "Erreur critique — Stripe inaccessible",
"description": "Le service de paiement répond en timeout depuis 03:12 UTC.",
"footer": "Incident #4821",
"timestamp": "03:17"
}])
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "success",
"color": "#22c55e",
"icon": "✅",
"title": "Commande #2847 confirmée",
"description": "Ton paiement a été accepté. Expédition sous 24h.",
"footer": "Merci pour ta confiance !"
}])
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "embed",
"color": "#f59e0b",
"author": "Système de monitoring",
"author_icon": "https://example.com/bot.png",
"title": "📊 Rapport journalier",
"title_url": "https://example.com/dashboard",
"description": "Résumé des métriques des dernières 24 heures.",
"fields": [
{"name": "Requêtes", "value": "48 201", "inline": True},
{"name": "Erreurs", "value": "3 (0.006%)", "inline": True},
{"name": "Uptime", "value": "99.98%", "inline": True},
{"name": "Latence P99", "value": "42ms", "inline": True},
{"name": "Détails", "value": "Consulte le dashboard pour les graphiques complets.", "inline": False},
],
"footer": "Généré automatiquement",
"timestamp": "now"
}])
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "minimal",
"color": "#8875f5",
"title": "Connexion détectée",
"description": "alice@example.com · 185.42.1.1 · Paris, FR"
}])
# compact=True réduit marges + padding pour espaces contraints
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "embed",
"color": "#6366f1",
"compact": True,
"author": "Bot",
"title": "Mise à jour disponible",
"description": "v3.0.0 est disponible.",
"fields": [{"name": "Nouveautés", "value": "Dark mode, perf +40%", "inline": False}],
"footer": "Changelog complet sur GitHub"
}])
# thumbnail = coin haut-droite (petit) | image = pleine largeur sous la description
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "embed",
"color": "#ec4899",
"author": "LeNavire Design",
"author_icon": "https://lenavire.app/assets/design-bot.png",
"title": "🎨 Nouvelles illustrations disponibles",
"description": "Le pack Saison 2 vient d'être mis en ligne. 42 nouveaux visuels haute résolution.",
"thumbnail": "https://lenavire.app/assets/pack-thumb.png",
"image": "https://lenavire.app/assets/pack-banner.png",
"footer": "Design by @lenaart",
"timestamp": "now"
}])
# author_url rend le nom d'auteur cliquable | title_url rend le titre cliquable
self.send_message(channel_id, components=[{
"type": "rich_message",
"style": "embed",
"color": "#6366f1",
"author": "@lenavire",
"author_url": "https://github.com/lenavire",
"author_icon": "https://lenavire.app/assets/github.png",
"title": "Pull Request #142 mergée",
"title_url": "https://github.com/lenavire/sdk/pull/142",
"description": "feat: ajout du composant rich_message v2 avec 6 styles.",
"fields": [
{"name": "Branche", "value": "main ← feat/rich-msg", "inline": True},
{"name": "Reviewers", "value": "@alice, @bob", "inline": True},
{"name": "Commits", "value": "8 commits, +423 / -71", "inline": False},
],
"timestamp": "now"
}])
# footer_icon + compact=True : parfait pour afficher une liste d'événements
EVENTS = [
("alice", "A ouvert un ticket", "🎫", "14:01"),
("bob", "A fusionné la PR #142", "✅", "14:12"),
("bot", "Déploiement lancé", "🚀", "14:13"),
]
for (user, action, icon, ts) in EVENTS:
self.send_message(log_channel_id, components=[{
"type": "rich_message",
"style": "minimal",
"color": "#8875f5",
"compact": True,
"title": f"{icon} {action}",
"description": f"Par @{user}",
"footer_icon": f"https://lenavire.app/avatar/{user}.png",
"footer": user,
"timestamp": ts
}])
Message enrichi — meta
Enrichis n'importe quel message avec des métadonnées visuelles puissantes : titre coloré, priorité animée, thread, tags, catégorie, TTS, éphémère, épinglé, silent, code intégré, multi-embed, compte à rebours — tout ça directement dans send_message().
title + msg_color — en-tête stylé avec bande colorée gauche.
low, high, urgent — badge rouge animé en mode urgent.
tts=True ajoute un bouton 🔊 pour lire le message à voix haute.
glass, neon, dark, light — style global du message.
thread_title, category, tags organisent visuellement le contenu.
ephemeral, silent, delete_after, pin — contrôle visibilité et durée de vie.
🧪 Démonstration interactive
Paramètres de send_message
| Paramètre | Type | Description |
|---|---|---|
title | str | Titre stylé au-dessus du contenu |
msg_color | hex | Couleur de la bordure gauche du message |
sticker | url | Grande image décorative (128 px) après le texte |
code | dict | {"language": "py", "content": "..."} — bloc de code |
tts | bool | Bouton 🔊 pour lire le message à voix haute |
embeds | list | Liste d'embeds supplémentaires (multi-embed) |
reply_to | int | ID d'un message cité (affiche un aperçu au-dessus) |
mention | str | Username mis en évidence dans le message |
delete_after | int (sec) | Auto-suppression après N secondes (badge décompte) |
ephemeral | bool | Message discret (opacité réduite + 👻 notice) |
footer_text | str | Texte gris en pied de message |
thread_title | str | Badge 🧵 avec titre de fil |
priority | low/high/urgent | Badge coloré (urgent = animation pulsante) |
reactions_preset | list | Emojis suggérés à cliquer sous le message |
| — PARAMÈTRES AVANCÉS — | ||
banner_url | url | Image pleine largeur (100px) en haut du message |
tags | list | Liste de tags affichés sous le titre (ex: ["python","api"]) |
category | str | Badge de catégorie teal (ex: "Annonce") |
color_scheme | glass/neon/dark/light | Style visuel global du message |
expires_at | ISO str / timestamp | Compte à rebours en direct ⏳ jusqu'à expiration |
silent | bool | Badge 🔕 Silencieux — sans notification |
pin | bool | Badge 📌 Épinglé |
scheduled_at | ISO str | Badge 📅 date/heure de publication planifiée |
⚡ Bonnes pratiques
title + msg_color systématiquement — identifier visuellement l'origine d'un message en un coup d'œil, sans lire le contenu.
priority="urgent" aux vraies urgences — le badge clignote et attire immédiatement l'attention. Si tout est urgent, plus rien ne l'est.
delete_after + ephemeral=True ensemble pour les messages d'erreur ou d'aide : ils disparaissent automatiquement et ne polluent pas l'historique.
color_scheme="neon" avec msg_color coordonné donne un rendu très impactant pour les annonces — mais utilise-le avec parcimonie pour garder l'effet "wow".
tags sont cliquables côté client — nomme-les en lowercase sans espaces pour une cohérence optimale dans les filtres de l'UI.
embeds et components dans le même appel — les embeds enrichissent le message texte, les components sont pour les cartes interactives.
📝 Exemples
# Titre stylé + bande colorée gauche + tags + épinglé
self.send_message(channel_id,
"Voici les dernières nouvelles du serveur.",
title="📰 Annonce",
msg_color="#f59e0b",
category="Annonce",
tags=["news", "important"],
pin=True
)
# Badge URGENT clignotant + footer + TTS
self.send_message(channel_id,
"⚠️ Le serveur redémarre dans 5 minutes ! Sauvegardez vos données.",
priority="urgent",
msg_color="#ef4444",
footer_text="Maintenance planifiée · 22:00 UTC",
tts=True
)
# Visible uniquement par le destinataire, supprimé après 10s
self.send_message(channel_id,
"Cette commande n'existe pas. Essaie /help pour voir toutes les commandes.",
ephemeral=True,
delete_after=10
)
# Féliciter un utilisateur avec mention et réactions prédéfinies
self.send_message(channel_id,
"Bravo pour ta contribution sur la PR #142 !",
mention="alice",
reactions_preset=["🎉", "🔥", "👏", "❤️"]
)
# Discussion avec code Python intégré dans le message
self.send_message(channel_id,
"Voici le résultat de l'analyse :",
thread_title="Discussion #42",
priority="low",
code={
"language": "python",
"content": "score = analyse_logs()\nprint(f'Score: {score:.2f}')"
}
)
# Résumé multi-sections dans un seul message
self.send_message(channel_id,
"Résumé de la journée :",
title="📋 Daily recap",
msg_color="#6366f1",
embeds=[
{"title": "🌅 Matin", "description": "Réunion standup à 9h30 — 5 participants.", "color": "#3b82f6"},
{"title": "☀️ Après-midi", "description": "Code review + 3 PRs mergées.", "color": "#f59e0b"},
{"title": "🌙 Soir", "description": "Démo client à 18h — feedback positif.", "color": "#8b5cf6"},
]
)
# Release avec bannière image, tags et scheme neon
self.send_message(channel_id,
"Nouvelle version disponible — mets à jour maintenant !",
title="🚀 v2.1 publiée",
banner_url="https://example.com/release-banner.png",
category="Release",
tags=["python", "api", "v2"],
color_scheme="neon",
msg_color="#00d9a3",
priority="high"
)
# Offre limitée : épinglée, expire dans 1 heure avec compte à rebours en direct
import datetime
exp = (datetime.datetime.utcnow() + datetime.timedelta(hours=1)).isoformat()
self.send_message(channel_id,
"🎁 Offre spéciale — durée limitée !",
title="🎯 Deal exclusif",
pin=True,
priority="high",
expires_at=exp,
msg_color="#f59e0b"
)
# Rapport nocturne : silencieux, dark scheme, low priority
self.send_message(channel_id,
"📊 Rapport nocturne généré automatiquement.",
title="Rapport 03:00",
msg_color="#3b82f6",
priority="low",
color_scheme="dark",
category="Rapport",
tags=["rapport", "auto"],
footer_text="Généré automatiquement",
silent=True
)
# Tous les paramètres avancés combinés
self.send_message(channel_id,
"Voici un résumé complet avec toutes les options activées.",
title="🎨 Message ultra-enrichi",
msg_color="#8875f5",
priority="high",
color_scheme="glass",
category="Demo",
tags=["test", "showcase"],
thread_title="Discussion ouverte",
footer_text="LeNavire SDK · v2.1",
tts=True,
reactions_preset=["🚀", "🎉", "💜"]
)
📡 Interactions — référence complète
Chaque clic, sélection, saisie ou timer déclenche une interaction. Toutes arrivent dans on_button_click avec un champ type — ou dans des méthodes dédiées si tu les définis.
button_click, confirm_accept, confirm_declineselect_menu, dropdown_select, input_submit, number_input_changetimer_expired, poll_vote, rating_submit, slider_changeL'objet interaction
| Clé | Type | Description |
|---|---|---|
type | str | Type d'interaction — voir tableau complet ci-dessous |
custom_id | str | Identifiant du composant défini dans ton code |
values | list | None | Données de l'interaction (option choisie, texte saisi, fichier…) |
message_id | int | ID du message contenant le composant |
channel_id | int | ID du salon où l'interaction a eu lieu |
server_id | int | ID du serveur |
user | dict | {"id": int, "username": str, "is_bot": bool} — qui a interagi |
Tous les types d'interaction
| Type | Composant source | values | Description |
|---|---|---|---|
| 🖱️ CLICS | |||
button_click | button | — | Un bouton a été cliqué |
confirm_accept | confirm | — | L'utilisateur a confirmé |
confirm_decline | confirm | — | L'utilisateur a annulé |
| 📋 MENUS & SAISIES | |||
select_menu | select_menu | [valeur_choisie] | Option sélectionnée dans un menu |
dropdown_select | dropdown | [valeur_option] | Option choisie dans un dropdown |
input_submit | input_field | [texte_saisi] | Texte soumis par l'utilisateur |
number_input_change | number_input | [valeur] | Valeur numérique modifiée |
color_pick | color_picker | ["#rrggbb"] | Couleur sélectionnée |
date_pick | date_picker | ["YYYY-MM-DDTHH:MM"] | Date/heure sélectionnée |
checklist_change | checklist | [val1, val2, …] | Liste des cases cochées |
tag_add | tag_input | [tag_ajouté] | Un tag a été ajouté |
tag_remove | tag_input | [tag_retiré] | Un tag a été supprimé |
file_upload | file_upload | [nom, mime, taille] | Fichier envoyé par l'utilisateur |
| ⏱️ TIMERS & EVENTS | |||
timer_expired | timer | — | Le timer a atteint zéro |
slider_change | slider | [valeur_float] | Curseur déplacé |
rating_submit | rating | [note_int] | Note soumise (1–5 étoiles) |
poll_vote | poll | [index_option] | Vote dans un sondage |
Méthodes disponibles
Il y a deux façons de gérer les interactions : un handler universel, ou des méthodes dédiées par type.
on_select_menu ou on_timer_expired, ces méthodes sont appelées en priorité. Sinon, tout passe par on_button_click.Option A — Handler universel
def on_button_click(self, interaction):
itype = interaction.get('type')
ch = interaction.get('channel_id')
cid = interaction.get('custom_id')
vals = interaction.get('values') or []
user = interaction.get('user', {}).get('username', '?')
if itype == 'button_click': self.send_message(ch, f"🖱️ {user} a cliqué sur `{cid}`")
elif itype == 'confirm_accept': self.send_message(ch, "✅ Confirmé !")
elif itype == 'confirm_decline': self.send_message(ch, "❌ Annulé.")
elif itype == 'select_menu': self.send_message(ch, f"📋 Sélection : {vals[0]}")
elif itype == 'dropdown_select': self.send_message(ch, f"📋 Menu → {vals[0]}")
elif itype == 'input_submit': self.send_message(ch, f"✏️ Texte reçu : {vals[0]}")
elif itype == 'number_input_change': self.send_message(ch, f"🔢 Valeur : {vals[0]}")
elif itype == 'color_pick': self.send_message(ch, f"🎨 Couleur : {vals[0]}")
elif itype == 'date_pick': self.send_message(ch, f"📅 Date : {vals[0]}")
elif itype == 'slider_change': self.send_message(ch, f"🎚️ Curseur : {vals[0]}")
elif itype == 'rating_submit': self.send_message(ch, f"⭐ Note : {vals[0]}/5")
elif itype == 'poll_vote': self.send_message(ch, f"📊 Vote option #{vals[0]}")
elif itype == 'checklist_change': self.send_message(ch, f"✅ Cochés : {', '.join(vals)}")
elif itype == 'tag_add': self.send_message(ch, f"🏷️ Tag ajouté : {vals[0]}")
elif itype == 'tag_remove': self.send_message(ch, f"🏷️ Tag retiré : {vals[0]}")
elif itype == 'file_upload': self.send_message(ch, f"📎 Fichier : {vals[0]} ({vals[2]} octets)")
elif itype == 'timer_expired': self.send_message(ch, "⏰ Timer terminé !")Option B — Méthodes dédiées (prioritaires)
Si tu définis ces méthodes, elles sont appelées directement au lieu de passer par on_button_click.
def on_select_menu(self, interaction):
"""Appelée pour select_menu — prioritaire sur on_button_click."""
ch = interaction['channel_id']
vals = interaction.get('values', [])
self.send_message(ch, f"Tu as sélectionné : **{vals[0]}**")
def on_timer_expired(self, interaction):
"""Appelée quand un timer atteint zéro — prioritaire sur on_button_click."""
ch = interaction['channel_id']
cid = interaction.get('custom_id')
self.send_message(ch, f"⏰ Timer `{cid}` terminé ! Action déclenchée.")Accéder à l'utilisateur
Chaque interaction contient un objet user avec les infos de qui a interagi.
def on_button_click(self, interaction):
user = interaction.get('user', {})
user_id = user.get('id')
username = user.get('username', 'Inconnu')
is_bot = user.get('is_bot', False)
ch = interaction['channel_id']
if is_bot:
return # ignorer les bots
self.send_message(ch, f"Bonjour **{username}** (id={user_id}) !")Répondre avec un composant mis à jour
Après une interaction, tu peux envoyer un nouveau message avec des composants actualisés (ex: désactiver un bouton, changer un score).
def on_button_click(self, interaction):
ch = interaction['channel_id']
cid = interaction.get('custom_id')
if cid == 'vote_oui':
# Mettre à jour l'état interne
self.votes = getattr(self, 'votes', 0) + 1
# Renvoyer un message mis à jour
self.send_message(ch, f"👍 {self.votes} vote(s) pour OUI !", components=[
{"type": "button", "label": f"👍 OUI ({self.votes})", "custom_id": "vote_oui", "style": "success"},
{"type": "button", "label": "👎 NON", "custom_id": "vote_non", "style": "danger"}
])Utiliser l'état entre interactions
Les attributs self.* persistent entre les appels — parfait pour stocker un score, une étape de jeu, etc.
class MonBot:
def __init__(self):
self.etape = 0 # étape courante
self.scores = {} # {user_id: score}
self.actif = False # partie en cours ?
def on_button_click(self, interaction):
cid = interaction.get('custom_id')
ch = interaction['channel_id']
uid = interaction['user']['id']
if cid == 'start':
self.actif = True
self.etape = 1
self.send_message(ch, "🎮 Partie lancée ! Étape 1…")
elif cid == 'bonne_rep' and self.actif:
self.scores[uid] = self.scores.get(uid, 0) + 10
self.etape += 1
self.send_message(ch, f"✅ +10 pts ! Score total : {self.scores[uid]}")Filtrer par custom_id avec préfixe
Pour gérer beaucoup de boutons proprement, utilise des préfixes dans les custom_id.
def on_button_click(self, interaction):
cid = interaction.get('custom_id', '')
ch = interaction['channel_id']
# Préfixe "quiz_" → question de quiz
if cid.startswith('quiz_'):
rep = cid.split('_', 1)[1]
self.send_message(ch, f"Tu as répondu : {rep}")
# Préfixe "nav_" → navigation entre pages
elif cid.startswith('nav_'):
page = cid.split('_', 1)[1]
self._show_page(ch, page)
# Préfixe "action_" → action générique
elif cid.startswith('action_'):
action = cid.removeprefix('action_')
self._handle_action(ch, action)on_message — filtre avec if message.get('author',{}).get('is_bot'): return.type + custom_id pour un routage ultra-précis. Par exemple type == 'button_click' and custom_id == 'confirm_delete' ne se déclenchera que sur CE bouton précis.🧪 Simulateur d'interaction
Sélectionne un type et un custom_id pour voir à quoi ressemble l'objet interaction reçu par ton bot.
🧩 Patterns avancés
Routeur par type
Centralise la logique avec un dictionnaire de handlers — plus lisible qu'une longue chaîne if/elif.
class RouterBot:
def on_ready(self):
self.routes = {
'button_click': self._on_btn,
'select_menu': self._on_select,
'input_submit': self._on_input,
'rating_submit': self._on_rating,
'timer_expired': self._on_timer,
'file_upload': self._on_file,
}
def on_button_click(self, interaction):
handler = self.routes.get(interaction.get('type'))
if handler:
handler(interaction)
def _on_btn(self, i):
self.send_message(i['channel_id'], f"🖱️ Bouton : {i['custom_id']}")
def _on_select(self, i):
self.send_message(i['channel_id'], f"📋 Choix : {i['values'][0]}")
def _on_input(self, i):
self.send_message(i['channel_id'], f"✏️ Texte : {i['values'][0]}")
def _on_rating(self, i):
stars = "⭐" * int(i['values'][0])
self.send_message(i['channel_id'], f"Note : {stars}")
def _on_timer(self, i):
self.send_message(i['channel_id'], "⏰ Temps écoulé !")
def _on_file(self, i):
nom, mime, taille = i['values'][:3]
self.send_message(i['channel_id'], f"📎 {nom} ({taille} octets)")Confirmation à 2 étapes
Demande d'abord une confirmation avant d'exécuter une action irréversible.
class SafeBot:
def on_ready(self):
self.pending = {} # {user_id: action}
def on_message(self, message):
ch = message['channel_id']
uid = message['author']['id']
if message.get('content','').strip() == '!reset':
self.pending[uid] = 'reset_scores'
self.send_message(ch, "⚠️ Confirmer la remise à zéro ?", components=[
{"type": "button", "label": "✅ Confirmer", "custom_id": "do_confirm", "style": "danger"},
{"type": "button", "label": "❌ Annuler", "custom_id": "do_cancel", "style": "secondary"},
])
def on_button_click(self, interaction):
ch = interaction['channel_id']
cid = interaction.get('custom_id')
uid = interaction['user']['id']
if cid == 'do_confirm':
action = self.pending.pop(uid, None)
if action == 'reset_scores':
self.scores = {}
self.send_message(ch, "🗑️ Scores remis à zéro !")
elif cid == 'do_cancel':
self.pending.pop(uid, None)
self.send_message(ch, "↩️ Action annulée.")Anti-spam par utilisateur
Limite la fréquence d'interactions par utilisateur avec un cooldown en secondes.
import time
class CooldownBot:
COOLDOWN = 5 # secondes entre chaque interaction
def on_ready(self):
self.last_action = {} # {user_id: timestamp}
def _check_cooldown(self, uid, ch) -> bool:
now = time.time()
last = self.last_action.get(uid, 0)
reste = self.COOLDOWN - (now - last)
if reste > 0:
self.send_message(ch, f"⏳ Attends encore {reste:.1f}s !")
return False
self.last_action[uid] = now
return True
def on_button_click(self, interaction):
uid = interaction['user']['id']
ch = interaction['channel_id']
if not self._check_cooldown(uid, ch):
return
# logique principale ici…
self.send_message(ch, "✅ Action effectuée !")Middleware de log
Enregistre chaque interaction dans un historique pour débugger ou auditer.
import datetime
class AuditBot:
def on_ready(self):
self.log = [] # liste des 100 dernières interactions
def _log(self, interaction):
entry = {
'at': datetime.datetime.now().strftime("%H:%M:%S"),
'type': interaction.get('type'),
'cid': interaction.get('custom_id'),
'user': interaction.get('user',{}).get('username'),
'vals': interaction.get('values'),
}
self.log.append(entry)
if len(self.log) > 100:
self.log.pop(0)
def on_button_click(self, interaction):
self._log(interaction)
ch = interaction['channel_id']
cid = interaction.get('custom_id')
if cid == 'show_log':
lines = [f"[{e['at']}] {e['type']} · {e['user']} · {e['cid']}" for e in self.log[-5:]]
self.send_message(ch, "📋 Dernières interactions :\n" + "\n".join(lines))Machine à états (wizard multi-étapes)
Guide l'utilisateur étape par étape avec des composants différents à chaque étape.
class WizardBot:
def on_ready(self):
self.state = {} # {user_id: {step, data}}
def on_message(self, message):
ch = message['channel_id']
uid = message['author']['id']
if message.get('content','').strip() == '!inscription':
self.state[uid] = {'step': 1, 'data': {}}
self.send_message(ch, "📝 Étape 1/3 — Choisis ton rôle :", components=[
{"type":"select_menu", "custom_id":"wiz_role",
"options":[{"label":"Développeur","value":"dev"},{"label":"Designer","value":"design"},{"label":"Manager","value":"mgr"}]}
])
def on_button_click(self, interaction):
ch = interaction['channel_id']
cid = interaction.get('custom_id')
uid = interaction['user']['id']
s = self.state.get(uid)
if not s: return
if cid == 'wiz_role' and s['step'] == 1:
s['data']['role'] = interaction['values'][0]
s['step'] = 2
self.send_message(ch, "📝 Étape 2/3 — Entre ton pseudo :", components=[
{"type":"input_field", "custom_id":"wiz_pseudo", "placeholder":"Ton pseudo…"}
])
elif cid == 'wiz_pseudo' and s['step'] == 2:
s['data']['pseudo'] = interaction['values'][0]
s['step'] = 3
role = s['data']['role']
pseudo = s['data']['pseudo']
self.send_message(ch, f"✅ Inscription terminée ! Rôle : **{role}** · Pseudo : **{pseudo}**")
del self.state[uid]Interaction conditionnelle par rôle
Restreins certaines actions aux utilisateurs ayant un rôle ou un niveau donné.
class RoleBot:
ADMINS = {1001, 1002} # user IDs autorisés
def on_button_click(self, interaction):
ch = interaction['channel_id']
cid = interaction.get('custom_id')
uid = interaction['user']['id']
if cid == 'admin_reset':
if uid not in self.ADMINS:
self.send_message(ch, "🚫 Accès refusé — réservé aux admins.")
return
# action admin sécurisée
self.send_message(ch, "🔑 Reset effectué par admin.")
elif cid == 'user_action':
# accessible à tout le monde
self.send_message(ch, "👤 Action utilisateur OK !")is_bot pour éviter les boucles.2. Utiliser des préfixes dans les
custom_id pour grouper les interactions.3. Stocker l'état dans
self.* — persistant pendant la session, perdu au redémarrage.4. Définir des méthodes dédiées (
on_select_menu, etc.) pour un code plus propre.5. Ajouter un cooldown anti-spam sur les boutons publics.
6. Toujours répondre à une interaction — un silence est confusant pour l'utilisateur.
Liberté totale
Ton code Python s'exécute côté serveur. Tu peux importer des modules, répondre à n'importe quel trigger, ou ignorer les messages à ta guise.
N'importe quel trigger
def on_message(self, message):
clean = message.get('content', '').lower().strip()
channel = message.get('channel_id')
# Commande exacte
if clean == '!ping': ...
# Contient un mot-clé
elif 'merci' in clean:
self.send_message(channel, "De rien ! 😊")
# Commence par un préfixe
elif clean.startswith('!meteo '):
ville = clean[8:]
self.send_message(channel, f"🌤️ Météo de {ville} : beau temps !")
# Tous les messages (attention aux boucles !)
elif not message.get('author',{}).get('is_bot'):
# Répondre à tout sans filtrer (à utiliser avec précaution)
pass
# Condition complexe
elif len(clean) > 100 and 'aide' in clean:
self.send_message(channel, "Ton message est long, mais je vais t'aider !")Modules Python
Importe des modules Python standard directement dans ton code de bot.
| Module | Utilité | Exemple |
|---|---|---|
random | Aléatoire | random.choice(['Pile', 'Face']) |
math | Calculs | math.sqrt(16) → 4.0 |
json | JSON | json.dumps({"key": "val"}) |
datetime | Date/heure | datetime.now().strftime("%H:%M") |
re | Regex | re.search(r'\d+', content) |
requests | HTTP | requests.get("https://api.example.com") |
hashlib | Hash | hashlib.md5(b"text").hexdigest() |
import random, math, datetime
class UtilBot:
def on_message(self, message):
clean = message.get('content','').lower().strip()
ch = message.get('channel_id')
if message.get('author',{}).get('is_bot'): return
if clean == '!pile':
self.send_message(ch, random.choice(["🟡 Pile !", "⚪ Face !"]))
elif clean.startswith('!sqrt '):
n = float(clean[6:])
self.send_message(ch, f"√{n} = {math.sqrt(n):.4f}")
elif clean == '!heure':
now = datetime.datetime.now().strftime("%H:%M:%S")
self.send_message(ch, f"⏰ Il est {now}")État & Mémoire
Les attributs self.xxx persistent entre les appels à on_message tant que le bot tourne.
import random
class MemoryBot:
def on_ready(self):
self.compteur = 0
self.scores = {} # {user_id: score}
self.blagues = [
"Pourquoi les plongeurs plongent-ils toujours en arrière ?",
"Parce que sinon ils tomberaient dans le bateau !"
]
def on_message(self, message):
clean = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
uid = author.get('id')
name = author.get('username', '?')
if author.get('is_bot'): return
if clean == '!compter':
self.compteur += 1
self.send_message(channel, f"Compteur : **{self.compteur}**")
elif clean == '!point':
self.scores[uid] = self.scores.get(uid, 0) + 1
self.send_message(channel, f"✅ {name} a maintenant **{self.scores[uid]}** point(s) !")
elif clean == '!blague':
self.send_message(channel, random.choice(self.blagues))self.xxx est perdu si le bot est redémarré. Pour une persistance totale, utilise un fichier JSON ou une base de données.API externes
Appelle n'importe quelle API depuis ton bot : REST, JSON, authentification, async — documentation complète avec exemples réels.
Toutes les méthodes HTTP avec requests ou aiohttp.
Bearer token, API Key header/query, Basic Auth, OAuth2 flow.
aiohttp pour ne pas bloquer le bot pendant une requête longue.
Codes HTTP, timeouts, rate limiting, retries automatiques.
Installation
pip install requests aiohttp
1 — Requêtes synchrones (requests)
Le plus simple — bloque le thread jusqu'à la réponse. Suffisant pour la plupart des bots.
import requests
class ApiBot:
def on_message(self, message):
ch = message['channel_id']
if message.get('content','').strip() == '!prix':
resp = requests.get('https://api.coindesk.com/v1/bpi/currentprice.json', timeout=5)
if resp.status_code == 200:
data = resp.json()
prix = data['bpi']['EUR']['rate']
self.send_message(ch, f"💰 Bitcoin : {prix} €")
else:
self.send_message(ch, f"❌ Erreur {resp.status_code}")import requests
resp = requests.post(
'https://api.example.com/translate',
json={'text': 'Bonjour', 'target': 'en'}, # body JSON auto
headers={'Content-Type': 'application/json'},
timeout=10
)
result = resp.json()
print(result['translation']) # Helloimport requests
# Mise à jour
resp = requests.put('https://api.example.com/items/42', json={'name': 'Nouveau nom'}, timeout=5)
# Suppression
resp = requests.delete('https://api.example.com/items/42', timeout=5)
print(resp.status_code) # 204 No Content2 — Authentification
import requests
TOKEN = "mon_token_secret"
headers = {"Authorization": f"Bearer {TOKEN}"}
resp = requests.get('https://api.example.com/user/me', headers=headers, timeout=5)
print(resp.json())import requests
API_KEY = "ma_cle_api"
# Via header
resp1 = requests.get('https://api.openweathermap.org/data/2.5/weather',
params={'q': 'Paris', 'units': 'metric', 'lang': 'fr'},
headers={'x-api-key': API_KEY},
timeout=5)
# Via query param (méthode alternative)
resp2 = requests.get('https://api.openweathermap.org/data/2.5/weather',
params={'q': 'Paris', 'units': 'metric', 'lang': 'fr', 'appid': API_KEY},
timeout=5)
if resp2.ok:
d = resp2.json()
print(f"🌡️ {d['main']['temp']}°C — {d['weather'][0]['description']}")import requests
resp = requests.get(
'https://api.exemple.com/protected',
auth=('mon_login', 'mon_mot_de_passe'),
timeout=5
)
print(resp.status_code)3 — Async avec aiohttp
Non-bloquant : le bot continue de traiter les messages pendant la requête.
import asyncio
import aiohttp
class AsyncApiBot:
def on_message(self, message):
ch = message['channel_id']
if '!meteo' in message.get('content', ''):
asyncio.create_task(self._fetch_meteo(ch))
async def _fetch_meteo(self, channel_id):
async with aiohttp.ClientSession() as session:
async with session.get(
'https://api.openweathermap.org/data/2.5/weather',
params={'q': 'Paris', 'units': 'metric', 'lang': 'fr', 'appid': 'CLE_API'},
timeout=aiohttp.ClientTimeout(total=5)
) as resp:
if resp.status == 200:
data = await resp.json()
temp = data['main']['temp']
desc = data['weather'][0]['description']
self.send_message(channel_id, f"🌡️ Paris : {temp}°C, {desc}")
else:
self.send_message(channel_id, f"❌ API erreur {resp.status}")import asyncio, aiohttp
async def appel_openai(prompt: str) -> str:
API_KEY = "sk-..."
async with aiohttp.ClientSession() as sess:
async with sess.post(
'https://api.openai.com/v1/chat/completions',
headers={'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'},
json={
'model': 'gpt-4o-mini',
'messages': [{'role': 'user', 'content': prompt}],
'max_tokens': 200
},
timeout=aiohttp.ClientTimeout(total=15)
) as resp:
data = await resp.json()
return data['choices'][0]['message']['content']
# Utilisation dans le bot :
class GptBot:
def on_message(self, message):
ch = message['channel_id']
txt = message.get('content','')
if txt.startswith('!ask '):
asyncio.create_task(self._reply(ch, txt[5:]))
async def _reply(self, ch, q):
answer = await appel_openai(q)
self.send_message(ch, answer)4 — Gestion des erreurs et rate limiting
import requests, time
def appel_api_robuste(url, params=None, headers=None, max_retries=3):
"""Appel avec retry exponentiel sur 429/503."""
for attempt in range(max_retries):
try:
resp = requests.get(url, params=params, headers=headers, timeout=8)
if resp.status_code == 200:
return resp.json()
elif resp.status_code == 429:
wait = int(resp.headers.get('Retry-After', 2 ** attempt))
print(f"Rate limit, attente {wait}s")
time.sleep(wait)
elif resp.status_code == 401:
raise PermissionError("Token invalide ou expiré")
elif resp.status_code == 404:
return None # Ressource introuvable
elif resp.status_code >= 500:
time.sleep(2 ** attempt) # Backoff exponentiel
else:
resp.raise_for_status()
except requests.Timeout:
print(f"Timeout tentative {attempt+1}")
time.sleep(1)
except requests.ConnectionError:
print("Connexion impossible")
break
return None5 — Exemples réels
import requests
class MeteoBot:
API_KEY = 'TON_API_KEY' # https://openweathermap.org/api (gratuit)
def on_message(self, message):
content = message.get('content', '').strip()
ch = message['channel_id']
if content.startswith('!meteo '):
ville = content[7:].strip()
resp = requests.get(
'https://api.openweathermap.org/data/2.5/weather',
params={'q': ville, 'units': 'metric', 'lang': 'fr', 'appid': self.API_KEY},
timeout=5
)
if resp.status_code == 200:
d = resp.json()
self.send_message(ch, components=[{
'type': 'key_value',
'title': f'🌡️ Météo — {d["name"]}',
'pairs': [
{'key': 'Température', 'value': f'{d["main"]["temp"]}°C', 'bold_value': True},
{'key': 'Ressenti', 'value': f'{d["main"]["feels_like"]}°C'},
{'key': 'Humidité', 'value': f'{d["main"]["humidity"]}%'},
{'key': 'Ciel', 'value': d['weather'][0]['description'].capitalize()},
]
}])
else:
self.send_message(ch, f'❌ Ville "{ville}" introuvable')import requests
class GptBot:
OPENAI_KEY = 'sk-...' # https://platform.openai.com/api-keys
def on_message(self, message):
content = message.get('content', '').strip()
ch = message['channel_id']
if content.startswith('!gpt '):
prompt = content[5:]
resp = requests.post(
'https://api.openai.com/v1/chat/completions',
headers={'Authorization': f'Bearer {self.OPENAI_KEY}'},
json={'model': 'gpt-4o-mini', 'messages': [{'role': 'user', 'content': prompt}], 'max_tokens': 300},
timeout=15
)
if resp.ok:
answer = resp.json()['choices'][0]['message']['content']
self.send_message(ch, answer)
else:
self.send_message(ch, f'❌ OpenAI erreur {resp.status_code}')import requests
class CryptoBot:
def on_message(self, message):
content = message.get('content', '').strip().lower()
ch = message['channel_id']
coins = {'btc': 'bitcoin', 'eth': 'ethereum', 'sol': 'solana'}
for sym, cid in coins.items():
if content == f'!{sym}':
resp = requests.get(
f'https://api.coingecko.com/api/v3/simple/price',
params={'ids': cid, 'vs_currencies': 'eur,usd'},
timeout=5
)
if resp.ok:
d = resp.json()[cid]
self.send_message(ch, components=[{
'type': 'key_value',
'title': f'💹 {cid.capitalize()}',
'pairs': [
{'key': 'EUR', 'value': f'{d["eur"]:,.2f} €', 'bold_value': True},
{'key': 'USD', 'value': f'${d["usd"]:,.2f}'},
]
}])6 — Plus d'exemples réels
import requests
class GitHubBot:
TOKEN = 'ghp_...' # https://github.com/settings/tokens (optionnel, augmente les limites)
def on_message(self, message):
content = message.get('content','').strip()
ch = message['channel_id']
if content.startswith('!commits '):
repo = content[9:].strip() # ex: "microsoft/vscode"
headers = {'Authorization': f'Bearer {self.TOKEN}'} if self.TOKEN else {}
resp = requests.get(f'https://api.github.com/repos/{repo}/commits',
params={'per_page': 5}, headers=headers, timeout=5)
if resp.ok:
commits = resp.json()
pairs = [{'key': c['sha'][:7], 'value': c['commit']['message'].split('\n')[0][:60]}
for c in commits]
self.send_message(ch, components=[{
'type': 'key_value', 'title': f'📦 {repo} — derniers commits',
'monospace': True, 'pairs': pairs
}])
else:
self.send_message(ch, f'❌ Repo "{repo}" introuvable ou API limit atteinte')import requests
class TraducteurBot:
# API gratuite : https://libretranslate.com ou héberge toi-même
BASE = 'https://libretranslate.com'
KEY = '' # Optionnel sur instance publique
def on_message(self, message):
content = message.get('content','').strip()
ch = message['channel_id']
if content.startswith('!tr '):
# Format: !tr en Bonjour tout le monde
parts = content[4:].split(' ', 1)
lang_target = parts[0] if len(parts)>1 else 'en'
text = parts[1] if len(parts)>1 else parts[0]
resp = requests.post(f'{self.BASE}/translate',
json={'q': text, 'source': 'auto', 'target': lang_target, 'api_key': self.KEY},
timeout=8)
if resp.ok:
self.send_message(ch, f'🌐 {resp.json()["translatedText"]}')
else:
self.send_message(ch, '❌ Erreur de traduction')import requests, base64
class SpotifyBot:
# https://developer.spotify.com/dashboard — créer une app
CLIENT_ID = 'ton_client_id'
CLIENT_SECRET = 'ton_client_secret'
REFRESH_TOKEN = 'ton_refresh_token' # obtenu via OAuth2 flow
def _get_token(self):
creds = base64.b64encode(f'{self.CLIENT_ID}:{self.CLIENT_SECRET}'.encode()).decode()
resp = requests.post('https://accounts.spotify.com/api/token',
data={'grant_type': 'refresh_token', 'refresh_token': self.REFRESH_TOKEN},
headers={'Authorization': f'Basic {creds}'}, timeout=5)
return resp.json().get('access_token')
def on_message(self, message):
ch = message['channel_id']
if message.get('content','').strip() == '!spotify':
token = self._get_token()
resp = requests.get('https://api.spotify.com/v1/me/player/currently-playing',
headers={'Authorization': f'Bearer {token}'}, timeout=5)
if resp.status_code == 200:
d = resp.json()
artists = ', '.join(a['name'] for a in d['item']['artists'])
self.send_message(ch, f"🎵 {artists} — {d['item']['name']}")
elif resp.status_code == 204:
self.send_message(ch, '⏸️ Rien en cours de lecture')
else:
self.send_message(ch, '❌ Erreur Spotify')import requests, re
class YouTubeBot:
API_KEY = 'AIza...' # https://console.cloud.google.com — YouTube Data API v3 (gratuit 10k/j)
def on_message(self, message):
content = message.get('content','').strip()
ch = message['channel_id']
if 'youtube.com' in content or 'youtu.be' in content:
vid = re.search(r'(?:v=|youtu\.be/)([A-Za-z0-9_-]{11})', content)
if not vid: return
resp = requests.get('https://www.googleapis.com/youtube/v3/videos',
params={'id': vid.group(1), 'part': 'snippet,statistics', 'key': self.API_KEY},
timeout=5)
if resp.ok:
item = resp.json()['items'][0]
sn = item['snippet']; st = item['statistics']
self.send_message(ch, components=[{
'type': 'rich_message', 'style': 'embed', 'color': '#ff0000',
'author': sn['channelTitle'],
'title': sn['title'],
'description': sn['description'][:200] + '...',
'fields': [
{'name': '👁️ Vues', 'value': f"{int(st.get('viewCount',0)):,}", 'inline': True},
{'name': '👍 Likes', 'value': f"{int(st.get('likeCount',0)):,}", 'inline': True},
{'name': '💬 Commentaires', 'value': f"{int(st.get('commentCount',0)):,}", 'inline': True},
],
'thumbnail': sn['thumbnails']['medium']['url'],
'footer': 'YouTube Data API'
}])import requests
class HuggingFaceBot:
# https://huggingface.co/settings/tokens — token gratuit
HF_TOKEN = 'hf_...'
MODEL = 'mistralai/Mistral-7B-Instruct-v0.2' # ou tout autre modèle
def on_message(self, message):
content = message.get('content','').strip()
ch = message['channel_id']
if content.startswith('!ia '):
prompt = content[4:]
resp = requests.post(
f'https://api-inference.huggingface.co/models/{self.MODEL}',
headers={'Authorization': f'Bearer {self.HF_TOKEN}'},
json={'inputs': prompt, 'parameters': {'max_new_tokens': 200}},
timeout=30
)
if resp.ok:
result = resp.json()
if isinstance(result, list):
text = result[0].get('generated_text', '?')
self.send_message(ch, text.replace(prompt, '').strip()[:500])
else:
self.send_message(ch, '⏳ Modèle en cours de chargement, réessaie dans 20s')
else:
self.send_message(ch, f'❌ HuggingFace erreur {resp.status_code}')import requests
class GeoBot:
def on_message(self, message):
content = message.get('content','').strip()
ch = message['channel_id']
if content.startswith('!geo '):
ip = content[5:].strip()
resp = requests.get(f'http://ip-api.com/json/{ip}?lang=fr', timeout=5)
if resp.ok:
d = resp.json()
if d['status'] == 'success':
self.send_message(ch, components=[{
'type': 'key_value', 'title': f'🌍 Géolocalisation — {ip}',
'striped': True,
'pairs': [
{'key': 'Pays', 'value': f'{d["country"]} {d.get("countryCode","")}'},
{'key': 'Région', 'value': d.get('regionName','?')},
{'key': 'Ville', 'value': d.get('city','?')},
{'key': 'FAI', 'value': d.get('isp','?')},
{'key': 'Timezone', 'value': d.get('timezone','?')},
]
}])
else:
self.send_message(ch, f'❌ IP introuvable ou privée')import requests
class NewsBot:
API_KEY = '...' # https://newsapi.org (gratuit 100 req/j)
def on_message(self, message):
content = message.get('content','').strip()
ch = message['channel_id']
if content.startswith('!news '):
query = content[6:].strip()
resp = requests.get('https://newsapi.org/v2/everything',
params={'q': query, 'language': 'fr', 'pageSize': 3, 'sortBy': 'publishedAt', 'apiKey': self.API_KEY},
timeout=5)
if resp.ok:
articles = resp.json().get('articles', [])
for a in articles[:3]:
self.send_message(ch, components=[{
'type': 'rich_message', 'style': 'embed', 'color': '#0ea5e9',
'author': a.get('source',{}).get('name',''),
'title': a['title'],
'description': (a.get('description') or '')[:200],
'title_url': a['url'],
'footer': a.get('publishedAt','')[:10],
}])
else:
self.send_message(ch, '❌ Erreur NewsAPI')import requests
class RedditBot:
HEADERS = {'User-Agent': 'LeNavireBot/1.0'} # Reddit exige un User-Agent
def on_message(self, message):
content = message.get('content','').strip()
ch = message['channel_id']
if content.startswith('!reddit '):
sub = content[8:].strip()
resp = requests.get(f'https://www.reddit.com/r/{sub}/hot.json',
params={'limit': 5}, headers=self.HEADERS, timeout=5)
if resp.ok:
posts = resp.json()['data']['children']
pairs = [{'key': f'#{i+1}', 'value': p['data']['title'][:60],
'color': '#ff4500'} for i, p in enumerate(posts)]
self.send_message(ch, components=[{
'type': 'key_value',
'title': f'🔴 r/{sub} — Hot',
'pairs': pairs
}])
else:
self.send_message(ch, f'❌ Subreddit "{sub}" introuvable')import requests
class AnimalBot:
def on_message(self, message):
content = message.get('content','').strip().lower()
ch = message['channel_id']
if content == '!chien':
r = requests.get('https://dog.ceo/api/breeds/image/random', timeout=5)
if r.ok:
self.send_message(ch, r.json()['message']) # URL directe de l'image
elif content == '!chat':
r = requests.get('https://api.thecatapi.com/v1/images/search', timeout=5)
if r.ok:
self.send_message(ch, r.json()[0]['url'])import requests
# Envoie un message dans un salon Discord via webhook (sans bot)
def notifier_discord(webhook_url: str, titre: str, contenu: str, couleur: int = 0x8875f5):
resp = requests.post(webhook_url, json={
'embeds': [{
'title': titre,
'description': contenu,
'color': couleur,
'footer': {'text': 'LeNavire Bot'}
}]
}, timeout=5)
return resp.status_code == 204 # Discord renvoie 204 si OK
# Utilisation :
# notifier_discord(WEBHOOK_URL, '🚀 Déploiement', 'Version 2.1 en production')import requests
class TelegramNotifier:
# https://core.telegram.org/bots — crée un bot avec @BotFather
BOT_TOKEN = '123456789:AAH...'
CHAT_ID = '-100...' # ID du groupe ou channel
def envoyer(self, texte: str):
requests.post(
f'https://api.telegram.org/bot{self.BOT_TOKEN}/sendMessage',
json={'chat_id': self.CHAT_ID, 'text': texte, 'parse_mode': 'HTML'},
timeout=5
)Tableau récapitulatif
| API | Auth | Gratuit ? | Lien |
|---|---|---|---|
| OpenWeatherMap | API Key (query) | ✅ 60 req/min | openweathermap.org/api |
| OpenAI | Bearer Token | 💳 Payant (crédits) | platform.openai.com |
| CoinGecko | Aucune (basique) | ✅ 30 req/min | coingecko.com/api |
| REST Countries | Aucune | ✅ Illimité | restcountries.com |
| The Cat API | API Key optionnel | ✅ 10 req/min | thecatapi.com |
| Dog API | Aucune | ✅ Illimité | dog.ceo/api |
| GitHub API | Bearer (PAT optionnel) | ✅ 60/h anon, 5000/h auth | docs.github.com/rest |
| Giphy | API Key (query) | ✅ 100 req/h | developers.giphy.com |
| LibreTranslate | Aucune/Key optionnel | ✅ Gratuit (self-host) | libretranslate.com |
| Spotify Web API | OAuth2 (Bearer) | ✅ Gratuit (limité) | developer.spotify.com |
| YouTube Data API v3 | API Key (query) | ✅ 10 000 quota/j | console.cloud.google.com |
| Hugging Face Inference | Bearer Token | ✅ Gratuit (limité) | huggingface.co/inference-api |
| ip-api.com | Aucune | ✅ 45 req/min | ip-api.com |
| NewsAPI | API Key (query) | ✅ 100 req/j | newsapi.org |
| Reddit JSON | User-Agent header | ✅ Illimité | reddit.com/r/x/hot.json |
| Discord Webhook | URL secrète | ✅ Illimité | discord.com/developers |
| Telegram Bot API | Bot Token | ✅ Illimité | core.telegram.org/bots |
| Open Trivia DB | Aucune | ✅ Illimité | opentdb.com/api_config.php |
| JokeAPI | Aucune | ✅ Illimité | jokeapi.dev |
| ExchangeRate-API | API Key | ✅ 1500 req/mois | exchangerate-api.com |
Bot de commandes
Un bot complet avec aide, info serveur, dé à rouler et easter eggs.
import random
class CommandBot:
def on_ready(self):
self.commandes = ["!aide", "!de", "!info", "!8ball"]
def on_message(self, message):
clean = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
name = author.get('username', '?')
if author.get('is_bot'): return
if clean == '!aide':
self.send_message(channel, embed={
"title": "📚 Commandes disponibles",
"color": "#7c6cf0",
"fields": [
{"name": "!aide", "value": "Affiche ce menu", "inline": True},
{"name": "!de [N]", "value": "Lance un dé à N faces", "inline": True},
{"name": "!8ball [Q]", "value": "Pose une question au sort", "inline": True},
],
"footer": "CommandBot — Le Navire"
})
elif clean.startswith('!de'):
parts = clean.split()
faces = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 6
result = random.randint(1, faces)
self.send_message(channel, embed={
"title": f"🎲 {name} lance un D{faces}",
"description": f"Résultat : **{result}**",
"color": "#f1c40f" if result == faces else "#5865f2"
})
elif clean.startswith('!8ball'):
reponses = ["✅ Oui, certainement", "❌ Non, pas du tout",
"🤔 Peut-être...", "🔮 Les étoiles disent oui"]
self.send_message(channel, f"🎱 **{name}** : {random.choice(reponses)}")Quiz interactif
Un vrai quiz avec timer, boutons exclusifs et score, utilisant toutes les fonctionnalités.
class QuizBot:
def on_ready(self):
self.scores = {}
self.questions = [
{"q": "Capitale de la France ?", "options": ["Lyon","Paris","Nice"], "answer": "Paris"},
{"q": "2 + 2 = ?", "options": ["3","4","5"], "answer": "4"},
{"q": "Couleur du ciel ?", "options": ["Rouge","Vert","Bleu"], "answer": "Bleu"},
]
self.current = None
def on_message(self, message):
import random
clean = message.get('content','').lower().strip()
ch = message.get('channel_id')
if message.get('author',{}).get('is_bot'): return
if clean == '!quiz':
q = random.choice(self.questions)
self.current = q
self.send_message(ch,
embed={"title": f"❓ {q['q']}", "color": "#9b59b6", "footer": "Tu as 15 secondes !"},
components=[
{"type": "timer", "duration": 15, "color": "#e74c3c", "custom_id": "quiz_end"},
{"type": "action_row", "components": [
{"type": "button", "label": o, "style": "secondary",
"custom_id": f"ans_{o}", "exclusive": True}
for o in q["options"]
]}
]
)
elif clean == '!scores':
lines = [f"• {uid} : {s} pts" for uid, s in self.scores.items()]
self.send_message(ch, embed={
"title": "🏆 Scores",
"description": "\n".join(lines) if lines else "Aucun score encore !",
"color": "#f1c40f"
})
def on_button_click(self, interaction):
itype = interaction.get('type')
cid = interaction.get('custom_id')
ch = interaction.get('channel_id')
user = interaction.get('user', {})
uid = user.get('username', '?')
if itype == 'timer_expired' and cid == 'quiz_end':
ans = self.current['answer'] if self.current else '?'
self.send_message(ch, embed={"title": "⏰ Temps écoulé !", "description": f"C'était **{ans}**", "color": "#ef4444"})
elif itype == 'button_click' and cid.startswith('ans_') and self.current:
chosen = cid[4:]
if chosen == self.current['answer']:
self.scores[uid] = self.scores.get(uid, 0) + 1
self.send_message(ch, f"🎉 {uid} a trouvé ! (+1 pt, total : {self.scores[uid]})")
else:
self.send_message(ch, f"❌ {uid}, c'était **{self.current['answer']}**")Messages riches complets
Toutes les fonctionnalités combinées : embed + barres + boutons + menu déroulant.
if content == '!profil':
user = author.get('username', 'Joueur')
self.send_message(channel,
embed={
"title": f"🎮 Profil de {user}",
"description": "Niveau 12 — Rang : **Vétéran**",
"color": "#7c6cf0",
"fields": [
{"name": "⭐ XP", "value": "1 250 / 2 000", "inline": True},
{"name": "🏆 Wins", "value": "47", "inline": True},
{"name": "💀 KDR", "value": "2.3", "inline": True},
],
"footer": "Dernière connexion : aujourd'hui"
},
components=[
{"type": "progress_bar", "label": "⭐ XP vers niveau 13", "value": 63, "color": "#7c6cf0"},
{"type": "progress_bar", "label": "❤️ Vie", "value": 80, "color": "#ef4444", "height": 5},
{"type": "action_row", "components": [
{"type": "button", "label": "⚔️ Combattre", "style": "danger", "custom_id": "fight"},
{"type": "button", "label": "🛒 Boutique", "style": "primary", "custom_id": "shop"},
{"type": "button", "label": "💤 Se reposer", "style": "success", "custom_id": "rest"},
]},
{"type": "action_row", "components": [{
"type": "select_menu",
"custom_id": "equip_select",
"placeholder": "Équiper un objet...",
"options": [
{"label": "Épée de feu", "value": "sword", "emoji": "🔥"},
{"label": "Bouclier d'or", "value": "shield", "emoji": "🛡️"},
{"label": "Arc enchanté", "value": "bow", "emoji": "🏹"},
]
}]},
]
)Sauvegarder & déployer
Le déploiement est automatique et instantané. Aucune configuration serveur requise.
Déboguer
Référence rapide pour résoudre les problèmes courants.
| Problème | Solution |
|---|---|
| Le bot ne répond pas du tout | Vérifier que le bot est EN LIGNE (bouton ▶️ Démarrer) et ajouté au serveur |
| Erreur de syntaxe Python | L'éditeur souligne les erreurs en rouge. Corriger avant de sauvegarder |
| La classe n'est pas détectée | Ta classe doit avoir une méthode on_message(self, message) |
| Boucle infinie | Ajouter if author.get('is_bot'): return en tout premier |
| Le code modifié ne s'applique pas | Vérifier que Ctrl+S a bien affiché le message de succès |
| Commande non reconnue | Utiliser .lower().strip() sur content pour normaliser |
| Bouton/menu ne déclenche rien | Vérifier que on_button_click est définie et que le custom_id correspond |
if message.get('author', {}).get('is_bot'): return au début de on_message. Sinon ton bot verra ses propres messages et créera une boucle infinie qui spammera le salon.Conseils & bonnes pratiques
Les meilleures habitudes pour écrire un bot propre, fiable et fun.
self.xxx et utilise random.choice() pour varier les réponses.if/elif/else et utilise return pour sortir après avoir répondu.author.get('username') pour inclure le nom de l'utilisateur dans les réponses.Mini-jeux embarqués
Envoie un jeu directement dans le chat. Le joueur joue sans quitter Le Navire. Récupère le score via on_button_click.
minigame est ajouté dans le tableau components d'un send_message(). Le front-end gère le rendu interactif, le bot reçoit l'événement on_button_click avec type: "minigame_end" quand la partie se termine.Événements reçus par le bot
| Champ | Valeur | Description |
|---|---|---|
type | "minigame_end" | Type de l'interaction |
custom_id | ton identifiant | L'id que tu as passé dans le composant |
values | ["score"] | Score final (liste de chaînes) |
channel_id | id salon | Salon où le jeu a été lancé |
class GameBot:
def on_message(self, message):
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
if message.get('author', {}).get('is_bot'): return
if content == '!snake':
self.send_message(channel, "🐍 À toi de jouer !", components=[{
"type": "minigame", "game": "snake",
"speed": 4, "custom_id": "snake_game"
}])
elif content == '!pfc':
self.send_message(channel, "❓ Qui va gagner ?", components=[{
"type": "minigame", "game": "pfc",
"rounds": 3, "custom_id": "pfc_game"
}])
elif content == '!quiz':
self.send_message(channel, "📝 Quiz !'", components=[{
"type": "minigame", "game": "quiz", "custom_id": "quiz_game"
}])
def on_button_click(self, interaction):
if interaction.get('type') == 'minigame_end':
score = interaction.get('values', ['?'])[0]
ch = interaction.get('channel_id')
self.send_message(ch, f"🎯 Score : **{score}**")🐍 Snake
Le serpent classique. Contrôle avec les flèches du clavier ou WASD. Supporte aussi le swipe sur mobile.
Démo live
Code Python
self.send_message(channel, "🐍 À toi de jouer !", components=[{
"type" : "minigame",
"game" : "snake",
"label" : "Snake", # Titre affiché
"width" : 20, # Colonnes (défaut: 20)
"height" : 14, # Lignes (défaut: 14)
"cell_size" : 18, # Pixels par cellule
"speed" : 4, # 1 (lent) → 8 (rapide)
"snake_color": "#00d9a3", # Couleur du serpent
"food_color" : "#f87171", # Couleur de la nourriture
"custom_id" : "snake_game" # ID pour on_button_click
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
game | str | — | "snake" |
label | str | "Snake" | Titre dans l'overlay |
width | int | 20 | Nombre de colonnes (max 30) |
height | int | 14 | Nombre de lignes (max 20) |
cell_size | int | 18 | Taille d'une cellule en px |
speed | int | 3 | Vitesse 1–8 (1=lent, 8=très rapide) |
snake_color | str | #00d9a3 | Couleur hex du serpent |
food_color | str | #f87171 | Couleur hex de la nourriture |
custom_id | str | — | ID envoyé dans on_button_click |
🃏 Memory
Retourne les cartes et trouve toutes les paires. Les emojis sont entièrement personnalisables.
Démo live
Code Python
self.send_message(channel, "🃏 Trouve les paires !", components=[{
"type" : "minigame",
"game" : "memory",
"label" : "Memory",
"emojis" : ["🎮","🐍","💎","⚡","🎯","🌊","🚀","🍕"], # max 8 paires
"cols" : 4, # colonnes de la grille
"custom_id": "memory_game"
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
game | str | — | "memory" |
label | str | "Memory" | Titre |
emojis | list | 8 emojis variés | Liste d'emojis (max 8). Chaque emoji forme une paire. |
cols | int | 4 | Nombre de colonnes (grille de cols × (2*len/cols)) |
custom_id | str | — | ID pour on_button_click |
⚡ Réaction rapide
Attend le signal vert et tape le plus vite possible. Le temps de réaction est mesuré en millisecondes.
Démo live
Code Python
self.send_message(channel, "⚡ Réaction rapide — sois le plus rapide !", components=[{
"type" : "minigame",
"game" : "reaction",
"label" : "Réaction rapide",
"custom_id": "react_game" # values = ["temps_ms"]
}])values[0] contient le temps en millisecondes (ex: "243"). Compare les scores pour faire un classement !🔢 Devinette de nombre
Le bot choisit un nombre secret. L'utilisateur a N essais pour le deviner, avec des indices chaud/froid.
Démo live
Code Python
self.send_message(channel, "🔢 Devine le nombre !", components=[{
"type" : "minigame",
"game" : "number_guess",
"label" : "Devinette",
"max" : 100, # Nombre max (1 à max)
"tries" : 7, # Essais maximum
"custom_id": "ng_game" # values = ["nb_essais"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
game | str | — | "number_guess" |
label | str | "Devinette" | Titre |
max | int | 100 | Borne supérieure (génère 1 à max) |
tries | int | 7 | Nombre max d'essais |
custom_id | str | — | ID pour on_button_click — values[0] = nb d'essais utilisés |
Mini-jeux avancés
Des jeux plus complexes avec logique de grille, alphabet, récursivité. Mêmes bases que les jeux simples — un composant, un on_button_click.
on_button_click pour récupérer le score et mettre à jour ton classement avec leaderboard ou score_card.🧵 Pendu
Trouve le mot caché lettre par lettre. Le dessin du pendu se complète à chaque erreur. Liste de mots entièrement personnalisable.
Démo live
Code Python
self.send_message(channel, "🧵 Pendu — trouve le mot !", components=[{
"type" : "minigame",
"game" : "hangman",
"label" : "Pendu",
"words" : ["PYTHON","DISCORD","NAVIRE","SERVEUR"],
"hint" : "Langage ou technologie", # indice optionnel
"max_errors" : 6, # erreurs avant perdu
"custom_id" : "hangman_game" # values = ["score"] (erreurs évitées)
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
game | str | — | "hangman" |
words | list | 5 mots tech | Tableau de mots en majuscules (un est choisi aléatoirement) |
hint | str | — | Indice affiché sous le titre |
max_errors | int | 6 | Nombre d'erreurs max avant Game Over |
custom_id | str | — | values[0] = erreurs évitées (0 si perdu) |
🔢 2048
Fusionne les tuiles pour atteindre 2048. Contrôle avec les flèches ou swipe. Grille de taille configurable.
Démo live
Code Python
self.send_message(channel, "🔢 Atteins 2048 !", components=[{
"type" : "minigame",
"game" : "2048",
"label" : "2048",
"size" : 4, # taille de la grille (3, 4 ou 5)
"custom_id" : "2048_game" # values = ["score_final"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
game | str | — | "2048" |
label | str | "2048" | Titre affiché |
size | int | 4 | Taille de la grille (3, 4 ou 5) |
custom_id | str | — | values[0] = score final |
🏆 Leaderboard
Affiche un classement complèt et visuel dans le chat. Totalement custom : titre, scores, badges, avatars, unité.
Démo live

Code Python
self.send_message(channel, components=[{
"type" : "leaderboard",
"title" : "🏆 Classement",
"color" : "#00d9a3", # couleur accent
"unit" : "pts", # unité affichée après le score
"max" : 10, # nb max de lignes affichées
"entries": [
{"name": "Alice", "score": 4280, "badge": "🔥 Streak"},
{"name": "Bob", "score": 3150},
{"name": "Charlie", "score": 1990, "avatar": "https://..."},
]
}])Paramètres — entrée
| Champ | Type | Description |
|---|---|---|
name | str | Nom du joueur |
score | int/str | Score affiché |
avatar | str | URL d'image de profil (optionnel) |
badge | str | Badge pill affiché après le nom (ex. "🔥 Streak") |
🃏 Carte de score
Affiche la carte perso d'un joueur : nom, rang, score principal et stats secondaires configurable.
Démo live

Code Python
self.send_message(channel, components=[{
"type" : "score_card",
"name" : author.get('username'),
"score" : 4280,
"rank" : 1, # 1=🥇 2=🥈 3=🥉 n=#n
"unit" : "pts",
"badge" : "🔥 Streak x7", # optionnel
"subtitle" : "Meilleur score cette semaine",
"color" : "#00d9a3",
"stats" : [ # stats secondaires (optionnel)
{"label": "Parties", "value": "42", "color": "#a78bfa"},
{"label": "Victoires","value": "31", "color": "#00d9a3"},
{"label": "Win Rate", "value": "74%", "color": "#fbbf24"},
]
}])🧠 Système de points complet
Crée un système de points persistant avec self. Points par joueur, classement global, commandes d'affichage custom.
selfself.scores (dict en mémoire). Les données persistent tant que le bot tourne. Pour une persistance totale, sauvegarde dans un fichier avec le module json.Bot complet avec scores + classement
import json, os
class ScoreBot:
def on_ready(self):
# Charge les scores depuis un fichier (persistance entre redémarrages)
try:
with open('scores.json') as f:
self.scores = json.load(f)
except:
self.scores = {} # {user_id: {"name":..., "points":..., "games":...}}
def _save(self):
with open('scores.json', 'w') as f:
json.dump(self.scores, f)
def _add_points(self, uid, name, pts):
if uid not in self.scores:
self.scores[uid] = {"name":name, "points":0, "games":0}
self.scores[uid]["points"] += pts
self.scores[uid]["games"] += 1
self._save()
def _get_leaderboard(self, limit=10):
# Tri décroissant par points
sorted_scores = sorted(
self.scores.values(),
key=lambda x: x["points"], reverse=True
)
return [{"name": s["name"], "score": s["points"]} for s in sorted_scores[:limit]]
def on_message(self, message):
content = message.get('content', '').lower().strip()
channel = message.get('channel_id')
author = message.get('author', {})
if author.get('is_bot'): return
uid = str(author.get('id',''))
name = author.get('username','')
if content == '!snake':
self.send_message(channel, "🐍 Joue et marque des points !",
components=[{"type":"minigame","game":"snake",
"speed":4,"custom_id":f"snake_{uid}"}])
elif content == '!score':
s = self.scores.get(uid, {"points":0,"games":0})
rank = sorted(self.scores.values(), key=lambdax:x["points"],reverse=True)
rank_n = next((i+1 for i,r in enumerate(rank) if r["name"]==name), None)
self.send_message(channel, components=[{
"type" : "score_card",
"name" : name,
"score" : s["points"],
"rank" : rank_n,
"unit" : "pts",
"subtitle" : f"{s['games']} parties jouées",
"color" : "#00d9a3",
"stats" : [
{"label":"Parties","value":str(s["games"]),"color":"#a78bfa"},
{"label":"Points", "value":str(s["points"]),"color":"#00d9a3"},
]
}])
elif content == '!top':
lb = self._get_leaderboard(10)
self.send_message(channel, components=[{
"type" : "leaderboard",
"title" : "🏆 Top joueurs",
"color" : "#00d9a3",
"unit" : "pts",
"entries": lb
}])
def on_button_click(self, interaction):
if interaction.get('type') == 'minigame_end':
cid = interaction.get('custom_id','')
score = int(interaction.get('values',['0'])[0] or 0)
ch = interaction.get('channel_id')
uid = cid.split('_')[-1] # custom_id = "snake_{uid}"
player= self.scores.get(uid,{}).get("name","Joueur")
self._add_points(uid, player, score)
self.send_message(ch, f"✅ +{score} pts pour **{player}** ! Total : {self.scores[uid]['points']}")Commandes disponibles
| Commande | Action |
|---|---|
!snake | Lance une partie de Snake, le score est ajouté automatiquement |
!score | Affiche la carte de score perso du joueur avec rang |
!top | Affiche le classement global des 10 meilleurs |
!+pts 100 @user, des rôles selon le score, des multiplicateurs, des saisons... tout est en Python pur. Utilise self.scores comme tu veux.✊ Pierre Feuille Ciseaux
Jeu en manches configurable contre le bot. Simple et rapide, parfait pour relancer l'activité dans un salon.
Démo live

Code Python
self.send_message(channel, "❓ Qui va gagner ?", components=[{
"type" : "minigame",
"game" : "pfc",
"rounds" : 3, # nombre de manches
"color" : "#8875f5",
"custom_id" : "pfc_game" # values = ["nb_manches_gagnées"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
rounds | int | 3 | Nombre de manches à jouer |
color | str | #8875f5 | Couleur accent |
custom_id | str | — | values[0] = manches gagnées par le joueur |
🖱️ Clic maniaque
Clique le plus vite possible pendant le compte à rebours. Score = nombre de clics. Durée personnalisable.
Démo live

Code Python
self.send_message(channel, "🖱️ Clic maniaque !", components=[{
"type" : "minigame",
"game" : "clicker",
"duration" : 10, # durée en secondes
"color" : "#f59e0b",
"custom_id" : "clicker_game" # values = ["nb_clics"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
duration | int | 10 | Durée du chrono en secondes |
color | str | #f59e0b | Couleur du bouton |
custom_id | str | — | values[0] = nombre de clics final |
📝 Quiz
Q&A à choix multiple entièrement personnalisable. Définis tes propres questions, réponses et bonne réponse.
Démo live

Code Python
self.send_message(channel, "📝 Quiz culture générale", components=[{
"type" : "minigame",
"game" : "quiz",
"color" : "#8875f5",
"questions" : [
{
"q" : "Quelle est la capitale de la France ?",
"answers": ["Lyon","Paris","Marseille","Bordeaux"],
"correct": 1 # index 0-based
},
{
"q" : "Combien de coulèures dans un arc-en-ciel ?",
"answers": ["5","6","7","8"],
"correct": 2
},
],
"custom_id" : "quiz_game" # values = ["score"]
}])Format d'une question
| Champ | Type | Description |
|---|---|---|
q | str | Texte de la question |
answers | list[str] | 2 à 4 réponses possibles |
correct | int | Index de la bonne réponse (base 0) |
💣 Démineur
Grille de mines à déminer. Clic gauche pour révéler, clic droit pour poser un drapeau. Taille et mines configurables.
Démo live
Code Python
self.send_message(channel, "💣 Déminage en cours...", components=[{
"type" : "minigame",
"game" : "minesweeper",
"cols" : 8,
"rows" : 8,
"mines" : 10,
"color" : "#f87171",
"custom_id" : "ms_game" # values = ["temps_en_secondes"] si gagné, ["0"] si perdu
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
cols | int | 8 | Nombre de colonnes |
rows | int | 8 | Nombre de lignes |
mines | int | 10 | Nombre de mines placées |
color | str | #f87171 | Couleur accent |
custom_id | str | — | values[0] = temps en s si gagné, "0" si perdu |
✖️ Morpion
Tic-tac-toe classique contre un bot IA basé sur minimax. Tu joues en X, le bot joue en O.
Démo live

Code Python
self.send_message(channel, "✖️ Morpion — bats le bot !", components=[{
"type" : "minigame",
"game" : "tictactoe",
"color" : "#8875f5",
"custom_id" : "tt_game" # values = ["1"] gagné, ["0"] perdu, ["draw"] égalité
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
color | str | #8875f5 | Couleur des X du joueur |
custom_id | str | — | values[0] = "1" gagné / "0" perdu / "draw" égalité |
🏓 Casse-brique
Breakout sur canvas HTML5. Déplace la raquette à la souris ou au tactile. Brise toutes les briques pour gagner.
Démo live
Code Python
self.send_message(channel, "🏓 Casse-brique !", components=[{
"type" : "minigame",
"game" : "breakout",
"width" : 320,
"height" : 260,
"color" : "#00d9a3",
"custom_id" : "br_game" # values = ["score_final"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
width | int | 320 | Largeur du canvas en pixels |
height | int | 260 | Hauteur du canvas en pixels |
color | str | #00d9a3 | Couleur accent (balle + première rangée) |
custom_id | str | — | values[0] = score final (10 pts par brique) |
🎨 Simon Mémorise
Le bot affiche une séquence de couleurs lumineuses. Le joueur doit la reproduire dans l'ordre exact. La séquence s'allonge à chaque niveau réussi.
Démo live

Code Python
self.send_message(channel, "🎨 Suis la séquence !", components=[{
"type" : "minigame",
"game" : "simon",
"max_level" : 10, # niveaux à réussir pour gagner
"speed" : 600, # durée du flash en ms
"color" : "#8875f5",
"custom_id" : "simon_game" # values = ["niveau_atteint"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
max_level | int | 10 | Nombre de niveaux pour gagner |
speed | int | 600 | Durée du flash en ms (plus bas = plus rapide) |
color | str | #8875f5 | Couleur accent |
custom_id | str | — | values[0] = niveau atteint avant l'erreur |
🐭 Tape-Taupe
Des taupes apparaissent aléatoirement dans une grille 3×3. Clique dessus avant qu'elles disparaissent ! Durée configurable.
Démo live

Code Python
self.send_message(channel, "🐭 Tape les taupes !", components=[{
"type" : "minigame",
"game" : "whack_mole",
"duration" : 20, # durée en secondes
"speed" : 900, # intervalle apparition ms
"color" : "#f59e0b",
"custom_id" : "wm_game" # values = ["nb_taupes"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
duration | int | 20 | Durée de la partie en secondes |
speed | int | 900 | Intervalle entre chaque taupe en ms |
color | str | #f59e0b | Couleur accent |
custom_id | str | — | values[0] = nombre de taupes tapées |
🎰 Machine à Sous
3 rouleaux d'émojis. Une paire rapporte 1 crédit, un triple rapporte 3 à 10 crédits selon le symbole. Parfait pour animer un salon détendu.
Démo live

Code Python
self.send_message(channel, "🎰 Tente ta chance !", components=[{
"type" : "minigame",
"game" : "slots",
"credits" : 10, # nombre de crédits de départ
"symbols" : ["🍒","🍋","🍊","🍇","⭐","💎","🔔"],
"color" : "#f59e0b",
"custom_id" : "slots_game" # values = ["dernier_gain"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
credits | int | 10 | Nombre de crédits de départ |
symbols | list | […] | 7 émojis utilisés pour les rouleaux |
color | str | #f59e0b | Couleur du cadre |
custom_id | str | — | values[0] = dernier gain obtenu |
🃏 Plus Haut / Plus Bas
Un jeu de cartes à 52 cartes mélangées. Devine si la prochaine carte est plus haute ou plus basse. Les series consécutives sont indiquées.
Démo live

Code Python
self.send_message(channel, "🃏 Plus haut ou plus bas ?", components=[{
"type" : "minigame",
"game" : "higher_lower",
"rounds" : 13, # nombre de cartes à deviner
"color" : "#06b6d4",
"custom_id" : "hl_game" # values = ["nb_bonnes_réponses"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
rounds | int | 13 | Nombre de cartes à deviner |
color | str | #06b6d4 | Couleur accent |
custom_id | str | — | values[0] = bonnes réponses sur le total |
🔴 Puissance 4
Grille 7×6. Le joueur joue contre un bot avec IA simple (gagne > bloque > centre). Premier à aligner 4 remporte la partie.
Démo live

Code Python
self.send_message(channel, "🔴 Puissance 4 !", components=[{
"type" : "minigame",
"game" : "connect4",
"color" : "#f87171", # couleur du joueur
"custom_id" : "c4_game" # values = ["1"] gagné / ["0"] perdu / ["draw"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
color | str | #f87171 | Couleur des jetons du joueur |
custom_id | str | — | "1" gagné, "0" perdu, "draw" égalité |
⌨️ Dactylographie
Tape la phrase affichée le plus rapidement possible. Plusieurs phrases enchâinées. Score = meilleur temps en secondes. Phrases entièrement configurables.
Démo live

Code Python
self.send_message(channel, "⌨️ Speed typing !", components=[{
"type" : "minigame",
"game" : "typing",
"phrases" : [
"Vite vite il faut taper !",
"Le chat dort sur le canapé",
"Bienvenue sur Le Navire"
],
"color" : "#10b981",
"custom_id" : "typing_game" # values = ["meilleur_temps_s"]
}])Paramètres
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
phrases | list | 5 phrases défaut | Liste de phrases à taper |
color | str | #10b981 | Couleur accent |
custom_id | str | — | values[0] = meilleur temps en secondes |
🔊 Voice & Musique
Fais rejoindre ton bot dans un salon vocal, lance une musique depuis YouTube / SoundCloud (via yt-dlp) ou depuis Spotify (via spotipy), et contrôle la lecture en temps réel.
Rejoindre / Quitter un vocal
# Rejoindre le salon vocal id=456
self.join_voice(456)
# Quitter le salon vocal
self.leave_voice(456)Jouer de la musique
if content.startswith('!play '):
url = content[6:].strip()
self.join_voice(456) # rejoindre d'abord
track = self.play_music(456, url, volume=0.8)
self.send_message(channel,
embed={
"title": "🎵 En lecture",
"description": track['title'],
"color": "#00d9a3",
"thumbnail": track.get('thumbnail'),
"footer": f"Durée : {track.get('duration','?')} s"
}
)if content == '!stop':
self.stop_music(456)
self.leave_voice(456)Recherche de pistes
if content.startswith('!search '):
query = content[8:].strip()
results = self.search_music(query, limit=5)
lines = [f"**{i+1}.** {r['title']} — {r.get('uploader','?')}"
for i, r in enumerate(results)]
self.send_message(channel, "\n".join(lines))Infos d'une piste
info = self.get_track_info("https://www.youtube.com/watch?v=...")
# → {'title': '...', 'uploader': '...', 'duration': 213, 'thumbnail': '...', 'source': 'youtube'}
# Spotify (nécessite spotipy + SPOTIPY_CLIENT_ID / SECRET en env)
info = self.get_track_info("https://open.spotify.com/track/...")
# → {'title': '...', 'artist': '...', 'album': '...', 'duration': 210, 'source': 'spotify'}Membres dans un vocal
members = self.get_voice_members(456)
for m in members:
print(m['username']) # liste des membres en ligne dans ce salonRéférence SDK
| Méthode | Paramètres | Retour |
|---|---|---|
join_voice(channel_id) | int | {'success': True} |
leave_voice(channel_id) | int | {'success': True} |
play_music(channel_id, url, volume=0.8) | int, str, float | Dict piste (title, duration, thumbnail…) |
stop_music(channel_id) | int | {'success': True} |
search_music(query, limit=5) | str, int | Liste de résultats |
get_track_info(url) | str | Dict infos piste |
get_voice_members(channel_id) | int | Liste membres en ligne |
•
yt-dlp — lecture YouTube / SoundCloud / 1 000+ sources (pip)•
spotipy — support Spotify (pip, optionnel)Installe-les depuis l'onglet Dépendances de ton bot ou via la commande
!installdep ci-dessous.
📦 Gestionnaire de dépendances
Installe des packages Python (pip) ou JavaScript (npm) directement depuis l'éditeur de ton bot. Chaque bot dispose de son propre espace isolé.
Installer une dépendance
Dans l'interface, onglet Dépendances du bot → tape le nom du package → clic sur Installer.
# Dans ton bot, après installation via l'UI :
import requests
response = requests.get("https://api.example.com/data")
data = response.json()// Dans ton bot, après installation via l'UI :
const axios = require('axios');
const res = await axios.get('https://api.example.com/data');
console.log(res.data);Quota de stockage
| Plan | Espace fichiers | Dépendances |
|---|---|---|
| 🆓 Gratuit | 100 MB | Illimitées dans le quota |
| ⭐ Premium | 500 MB | Illimitées dans le quota |
Les packages publiés depuis moins d'un an sont refusés automatiquement. Cette règle protège contre les packages malveillants récemment soumis sur PyPI / npm.
Commande bot — installer depuis le chat
import subprocess
if content.startswith('!installdep '):
pkg = content.split(' ', 1)[1].strip()
result = subprocess.run(['pip', 'install', pkg],
capture_output=True, text=True)
if result.returncode == 0:
self.send_message(channel, f"✅ `{pkg}` installé avec succès !")
else:
self.send_message(channel, f"❌ Erreur : {result.stderr[:200]}")Packages populaires
| Package | Type | Usage |
|---|---|---|
requests | pip | Requêtes HTTP |
yt-dlp | pip | Téléchargement / info audio-vidéo (requis pour Voice & Musique) |
spotipy | pip | API Spotify (optionnel pour Voice) |
Pillow | pip | Traitement d'images |
aiohttp | pip | Requêtes HTTP asynchrones |
axios | npm | Requêtes HTTP (JS) |
node-fetch | npm | Fetch API pour Node.js |