✨ Messages riches — titres, tableaux, listes, médias & streaming

Depuis Telegram Bot API 10.1, un message peut être structuré : vrais paragraphes espacés, titres, tableaux, listes, citations, blocs de code, sections dépliables — et, depuis la 10.2, des photos, vidéos et audio posés à l'intérieur du message, chacun avec sa légende. BotStrike expose cela via trois méthodes disponibles en BSJS et en BSPy : Bot.sendRichMessage(), Bot.streamText() et Bot.sendRichMessageDraft() — et Api.sendRichMessage() pour la forme brute Telegram (§10).

1. Qu'est-ce qu'un message riche ?

Un message classique envoyé avec Bot.sendMessage() est un simple texte : pour le mettre en forme, il faut du HTML ou du Markdown, et le résultat reste une seule masse de texte. Un message riche est composé de blocs : chaque bloc (titre, paragraphe, tableau, liste…) est rendu nativement par Telegram, avec son propre espacement.

BesoinMéthode conseillée
Une phrase, une réponse courteBot.sendMessage()
Un message long, un récapitulatif, un tableau de prixBot.sendRichMessage()
Une photo seule avec une légendeBot.sendPhoto()
Une photo ou une vidéo dans un message mis en pageBloc média (§3.3)
Une réponse d'IA qui s'écrit progressivementBot.streamText()
Piloter soi-même l'animation d'écritureBot.sendRichMessageDraft()
Une option Telegram non exposée par le SDKApi.sendRichMessage() (§10)
💡 À retenir — Pour une phrase simple, Bot.sendMessage() reste le bon choix. Les messages riches servent quand la structure apporte quelque chose : un catalogue, une facture, un rapport, une FAQ.
⚠️ Client Telegram à jour requis — Le rendu structuré nécessite un client Telegram récent (Bot API 10.1). Sur une très vieille version, le message reste lisible mais perd une partie de sa mise en forme.

2. Démarrage rapide — Bot.sendRichMessage()

La méthode prend un tableau (JS) / une liste (Python) de blocs. Une simple chaîne de caractères devient automatiquement un paragraphe.

JavaScript
Python
// Commande : /boutique
Bot.sendRichMessage([
  { h1: "Bienvenue sur notre boutique" },
  "Voici nos offres du moment. Chaque paragraphe est bien séparé.",
  "Deuxième paragraphe, détaché du premier.",

  { h2: "Nos forfaits" },
  { table: [
      ["Forfait", "Prix", "Crédits"],   // 1re ligne = en-tête
      ["Free",    "0 $",   "20"],
      ["Pro",     "15 $",  "100"],
  ]},

  { h2: "Comment démarrer" },
  { list: ["Créer un compte", "Choisir un forfait", "Payer"], ordered: true },

  { quote: "Merci de votre confiance." },
  { code: "Bot.sendMessage('Salut');", lang: "javascript" },
  { details: "Voir les conditions", blocks: [
      "Ce contenu est replié par défaut.",
  ]},
  { divider: true },
  { footer: "Envoyé automatiquement." },
]);
# Commande : /boutique
Bot.sendRichMessage([
    {"h1": "Bienvenue sur notre boutique"},
    "Voici nos offres du moment. Chaque paragraphe est bien séparé.",
    "Deuxième paragraphe, détaché du premier.",

    {"h2": "Nos forfaits"},
    {"table": [
        ["Forfait", "Prix", "Crédits"],   # 1re ligne = en-tête
        ["Free",    "0 $",   "20"],
        ["Pro",     "15 $",  "100"],
    ]},

    {"h2": "Comment démarrer"},
    {"list": ["Créer un compte", "Choisir un forfait", "Payer"], "ordered": True},

    {"quote": "Merci de votre confiance."},
    {"code": "Bot.sendMessage('Salut')", "lang": "python"},
    {"details": "Voir les conditions", "blocks": [
        "Ce contenu est replié par défaut.",
    ]},
    {"divider": True},
    {"footer": "Envoyé automatiquement."},
])
✅ Coût — Un appel à sendRichMessage() est facturé comme un envoi normal : 1 itération, quel que soit le nombre de blocs. Voir Tarification Cloud Bot →

3. Tous les blocs disponibles

Chaque ligne du tableau ci-dessous est un bloc à placer dans la liste :

BSJS (JavaScript)BSPy (Python)Résultat
"texte""texte"Paragraphe
{ p: "texte" }{"p": "texte"}Paragraphe
{ h1: "…" } { h2: "…" } { h3: "…" }{"h1": "…"} {"h2": "…"} {"h3": "…"}Titre de niveau 1 à 3
{ table: [["A","B"], ["1","2"]] }{"table": [["A","B"], ["1","2"]]}Tableau bordé — la 1re ligne est l'en-tête
{ table: [...], striped: true, compact: true, caption: "…" }{"table": [...], "striped": True, "compact": True, "caption": "…"}Lignes alternées · cellules serrées · légende. bordered: false retire les traits, header: false l'en-tête
{ list: ["a","b"] }{"list": ["a","b"]}Liste à puces
{ list: ["a","b"], ordered: true }{"list": ["a","b"], "ordered": True}Liste numérotée
{ quote: "texte" }{"quote": "texte"}Citation
{ code: "x=1", lang: "python" }{"code": "x=1", "lang": "python"}Bloc de code coloré
{ divider: true }{"divider": True}Trait de séparation
{ footer: "texte" }{"footer": "texte"}Pied de message, en petit
{ details: "Titre", blocks: [...] }{"details": "Titre", "blocks": [...]}Section dépliable (repliée par défaut)
{ thinking: "…" }{"thinking": "…"}Réflexion — brouillons uniquement (voir §6)
Médias — détail en §3.3 et §3.4
{ photo: "url", caption: "…" }{"photo": "url", "caption": "…"}Photo avec légende
{ video: "url" } { gif: "url" }{"video": "url"} {"gif": "url"}Vidéo · GIF animé
{ audio: "url" } { voice: "url" }{"audio": "url"} {"voice": "url"}Musique · note vocale
{ collage: [...] } { slideshow: [...] }{"collage": [...]} {"slideshow": [...]}Mosaïque · diaporama
{ map: [48.85, 2.29] }{"map": [48.85, 2.29]}Carte
{ math: "E = mc^2" }{"math": "E = mc^2"}Formule LaTeX
{ pullquote: "…" }{"pullquote": "…"}Citation encadrée
{ anchor: "tarifs" }{"anchor": "tarifs"}Repère pour un lien interne
Boutons — détail en §3.6
{ buttons: [...], align: "center" }{"buttons": [...], "align": "center"}Rangée de boutons colorés, dans le message
💡 Alias acceptés — paragraph pour p, heading + size pour h1/h2/h3, separator pour divider, is_ordered pour ordered, language pour lang, animation pour gif, voice_note pour voice, formula / latex pour math.

3.1 Blocs imbriqués

Chaque puce d'une liste et chaque section dépliable peut contenir plusieurs blocs : il suffit d'y placer un tableau/une liste de blocs.

JavaScript
Python
Bot.sendRichMessage([
  { list: [
      "Une puce simple",
      // Une puce composée de plusieurs blocs
      [ { h3: "Une puce avec du code" },
        { code: "Bot.sendMessage('ok');", lang: "javascript" } ],
  ]},

  { details: "Détails techniques", blocks: [
      "Replié par défaut, l'utilisateur déplie s'il veut.",
      { table: [["Champ", "Valeur"], ["Version", "10.1"]] },
  ]},
]);
Bot.sendRichMessage([
    {"list": [
        "Une puce simple",
        # Une puce composée de plusieurs blocs
        [{"h3": "Une puce avec du code"},
         {"code": "Bot.sendMessage('ok')", "lang": "python"}],
    ]},

    {"details": "Détails techniques", "blocks": [
        "Replié par défaut, l'utilisateur déplie s'il veut.",
        {"table": [["Champ", "Valeur"], ["Version", "10.1"]]},
    ]},
])

3.2 Forme native Telegram

Les raccourcis ci-dessus couvrent l'essentiel du texte. Tous les autres blocs de Telegram — photo, video, audio, collage, slideshow, map, anchor — s'écrivent sous leur forme native { type: "…", … } : le bloc est alors transmis tel quel à Telegram, sans conversion. Les médias ont leur propre section : voir §3.3.

JavaScript
Python
Bot.sendRichMessage([
  { h2: "Notre boutique" },
  // forme native : le bloc part tel quel vers Telegram
  { type: "map",
    location: { latitude: 48.8584, longitude: 2.2945 },
    zoom: 15, width: 600, height: 400 },
]);
Bot.sendRichMessage([
    {"h2": "Notre boutique"},
    # forme native : le bloc part tel quel vers Telegram
    {"type": "map",
     "location": {"latitude": 48.8584, "longitude": 2.2945},
     "zoom": 15, "width": 600, "height": 400},
])
💡 Objet non reconnu — Si un bloc n'est ni un raccourci ni une forme native valide, il n'est pas perdu : il est affiché sous forme de paragraphe (JSON) plutôt que de faire échouer l'envoi.

3.3 Photos, vidéos, audio — les blocs média

Un message riche n'est pas réservé au texte. Depuis Bot API 10.2, une photo, une vidéo, un GIF, une piste audio ou une note vocale se posent à l'intérieur du message, entre deux paragraphes, chacun avec sa propre légende. Un raccourci existe pour chacun :

BSJS (JavaScript)BSPy (Python)Résultat
{ photo: "url" }{"photo": "url"}Photo
{ video: "url" }{"video": "url"}Vidéo
{ gif: "url" }{"gif": "url"}GIF animé
{ audio: "url" }{"audio": "url"}Musique / piste audio
{ voice: "url" }{"voice": "url"}Note vocale

Chacun accepte trois options, sur le même objet :

OptionEffet
captionLa légende affichée sous le média
creditLa source, en plus petit sous la légende
spoiler: trueMédia flouté, révélé au clic (photo, vidéo, GIF)
JavaScript
Python
Bot.sendRichMessage([
  { h1: "Notre nouveau modèle" },
  "Disponible en boutique dès aujourd'hui.",

  { photo: "https://exemple.com/produit.jpg",
    caption: "Le modèle 2026, coloris nuit",
    credit: "Photo : Studio BotStrike" },

  { h2: "En mouvement" },
  { video: "https://exemple.com/demo.mp4", caption: "30 secondes de démo" },

  { table: [["Modèle", "Prix"], ["Nuit", "129 $"], ["Jour", "119 $"]] },
], {
  reply_markup: { inline_keyboard: [[
    { text: "🛒 Commander", callback_data: "buy" },
  ]] },
});
Bot.sendRichMessage([
    {"h1": "Notre nouveau modèle"},
    "Disponible en boutique dès aujourd'hui.",

    {"photo": "https://exemple.com/produit.jpg",
     "caption": "Le modèle 2026, coloris nuit",
     "credit": "Photo : Studio BotStrike"},

    {"h2": "En mouvement"},
    {"video": "https://exemple.com/demo.mp4", "caption": "30 secondes de démo"},

    {"table": [["Modèle", "Prix"], ["Nuit", "129 $"], ["Jour", "119 $"]]},
], {
    "reply_markup": {"inline_keyboard": [[
        {"text": "🛒 Commander", "callback_data": "buy"},
    ]]},
})
⚠️ URL ou file_id — jamais un fichier local — La valeur est une adresse HTTPS publique ou un file_id déjà présent sur les serveurs Telegram (celui d'une photo reçue, par exemple). Un chemin de fichier du bot (/tmp/facture.png) ne fonctionne pas : le téléversement direct n'existe pas dans un message riche. Pour un fichier que le bot vient de créer, passez par Bot.sendPhoto() / Bot.sendDocument(), ou téléversez-le une première fois et réutilisez son file_id.
💡 Une seule photo et une légende ? sendPhoto suffit — Bot.sendPhoto(url, "Ma légende") reste le bon outil pour un envoi média classique : il accepte le Markdown dans la légende et sait téléverser un fichier local. Le bloc média sert quand la photo doit vivre dans un message structuré, au milieu des titres, tableaux et listes. Voir Commandes BSJS → · Commandes BSPy →

Forme native équivalente

Le raccourci écrit pour vous la forme native de Telegram, qui imbrique trois niveaux. Vous pouvez l'écrire à la main pour utiliser une option non exposée (durée d'un audio, miniature d'une vidéo…) :

// { photo: "url", caption: "légende", credit: "source" }  ⇢  devient :
{ type: "photo",
  photo: { type: "photo", media: "url" },
  caption: { text: "légende", credit: "source" } }

Le champ média porte toujours le même nom que le type : photo, video, animation, audio, voice_note. Une valeur d'objet est également acceptée par le raccourci — { photo: { media: id, has_spoiler: true } } — et complétée automatiquement.

⚠️ La légende se met sur le bloc, pas sur le média — Dans la forme native, le champ caption à l'intérieur de l'objet média est ignoré par Telegram. Seule la caption du bloc s'affiche — c'est ce que fait le raccourci.

3.4 Collage, diaporama, carte, formule

Les mêmes médias se regroupent en collage (une mosaïque) ou en diaporama (des vignettes que l'on fait défiler) : il suffit de donner la liste des URL. Trois autres blocs complètent la panoplie.

BSJS (JavaScript)BSPy (Python)Résultat
{ collage: ["url1", "url2"], caption: "…" } {"collage": ["url1", "url2"], "caption": "…"} Mosaïque de plusieurs médias
{ slideshow: ["url1", "url2"] } {"slideshow": ["url1", "url2"]} Médias défilables sur une seule vignette
{ map: [48.8584, 2.2945] } {"map": [48.8584, 2.2945]} Carte centrée sur un point
{ math: "E = mc^2" } {"math": "E = mc^2"} Formule LaTeX rendue par Telegram
{ pullquote: "…", credit: "…" } {"pullquote": "…", "credit": "…"} Citation centrée, mise en avant
{ anchor: "tarifs" } {"anchor": "tarifs"} Repère invisible, cible d'un lien interne
JavaScript
Python
Bot.sendRichMessage([
  { h2: "L'appartement" },

  { collage: [urlSalon, urlCuisine, urlChambre],
    caption: "3 pièces, 62 m² — la visite en images" },

  { map: [48.8584, 2.2945], zoom: 15, caption: "À 400 m du métro" },

  { pullquote: "Un coup de cœur immédiat.", credit: "Marie, locataire" },
]);
Bot.sendRichMessage([
    {"h2": "L'appartement"},

    {"collage": [url_salon, url_cuisine, url_chambre],
     "caption": "3 pièces, 62 m² — la visite en images"},

    {"map": [48.8584, 2.2945], "zoom": 15, "caption": "À 400 m du métro"},

    {"pullquote": "Un coup de cœur immédiat.", "credit": "Marie, locataire"},
])
💡 Un collage n'est pas limité aux photos — Une URL dans la liste vaut « une photo » ; pour mélanger photos et vidéos, mettez des blocs complets : { collage: [{ photo: url1 }, { video: url2 }] }.
📐 Options de la carte — zoom (0 à 24, défaut 15), width (défaut 600) et height (défaut 400), au plus 10000 chacun, avec une somme qui ne dépasse pas 10000 et un rapport qui ne dépasse pas 20. Les coordonnées s'écrivent aussi { map: { latitude: 48.8584, longitude: 2.2945 } }.

3.5 Gras, lien, spoiler à l'intérieur d'un bloc

Le Markdown ne fonctionne pas dans un message riche — mais le formatage, lui, existe. Partout où un bloc attend un texte (le text d'un paragraphe, d'un titre, d'une légende, d'une cellule…), on peut mettre une liste de morceaux : chaque morceau est soit une chaîne, soit un objet { type: "…", text: "…" }.

MorceauRésultat
{ type: "bold", text: "…" }Gras
{ type: "italic", text: "…" }Italique
{ type: "underline", text: "…" }Souligné
{ type: "strikethrough", text: "…" }Barré
{ type: "spoiler", text: "…" }Masqué, révélé au clic
{ type: "code", text: "…" }Police à chasse fixe
{ type: "marked", text: "…" }Surligné
{ type: "url", text: "…", url: "https://…" }Lien cliquable
{ type: "subscript", text: "…" } · { type: "superscript", text: "…" }Indice · exposant
{ type: "mathematical_expression", text: "…" }Formule LaTeX dans la ligne
JavaScript
Python
Bot.sendRichMessage([
  { type: "paragraph", text: [
      "Votre commande ",
      { type: "bold", text: "#1042" },
      " est ",
      { type: "spoiler", text: "en préparation" },
      ". Suivi : ",
      { type: "url", text: "ici", url: "https://exemple.com/suivi/1042" },
  ]},
]);
Bot.sendRichMessage([
    {"type": "paragraph", "text": [
        "Votre commande ",
        {"type": "bold", "text": "#1042"},
        " est ",
        {"type": "spoiler", "text": "en préparation"},
        ". Suivi : ",
        {"type": "url", "text": "ici", "url": "https://exemple.com/suivi/1042"},
    ]},
])
💡 Raccourci ou forme native — Dès qu'un texte est formaté, il faut écrire la forme native du bloc : { p: "…" } n'accepte qu'une chaîne, { type: "paragraph", text: [...] } accepte la liste.

3.6 Boutons dans le message

Une rangée de boutons se place entre deux blocs, à l'endroit exact où elle doit apparaître — et plus seulement sous le message, comme un clavier classique. Chaque rangée compte 1 à 8 boutons, alignés à gauche (par défaut), au centre ou à droite, et chaque bouton peut prendre une couleur.

BSJS (JavaScript)BSPy (Python)Effet au clic
"/menu""/menu"Lance la commande /menu (le texte sert de libellé)
{ text: "Menu", command: "/menu" }{"text": "Menu", "command": "/menu"}Lance la commande /menu
{ text: "Site", url: "https://…" }{"text": "Site", "url": "https://…"}Ouvre le lien
{ text: "App", webapp: "https://…" }{"text": "App", "webapp": "https://…"}Ouvre une Mini App (conversation privée uniquement)
{ text: "Code", copy: "PROMO" }{"text": "Code", "copy": "PROMO"}Copie le texte dans le presse-papiers
{ text: "Profil", profile: 123456789 }{"text": "Profil", "profile": 123456789}Ouvre le profil Telegram de cette personne
{ text: "Épuisé", disabled: true }{"text": "Épuisé", "disabled": True}Bouton grisé, ne fait rien

Couleur, facultative : style: "primary" (bleu), "success" (vert), "danger" (rouge) ou "link" (simple lien sans bordure, seulement avec command). Sans style, le bouton reste neutre.

JavaScript
Python
Bot.sendRichMessage([
  { h2: "Ta commande #1042" },
  "Tout est prêt. Que veux-tu faire ?",
  { buttons: [
      { text: "✅ Confirmer", style: "success", command: "/confirmer" },
      { text: "✖ Annuler",    style: "danger",  command: "/annuler" },
  ], align: "center" },

  { h3: "Liens utiles" },
  { buttons: [
      { text: "Suivre le colis", url: "https://exemple.com/suivi/1042" },
      { text: "Code promo", copy: "MERCI10" },
  ]},
  { buttons: [{ text: "Épuisé", disabled: true }], align: "right" },
]);
Bot.sendRichMessage([
    {"h2": "Ta commande #1042"},
    "Tout est prêt. Que veux-tu faire ?",
    {"buttons": [
        {"text": "✅ Confirmer", "style": "success", "command": "/confirmer"},
        {"text": "✖ Annuler",    "style": "danger",  "command": "/annuler"},
    ], "align": "center"},

    {"h3": "Liens utiles"},
    {"buttons": [
        {"text": "Suivre le colis", "url": "https://exemple.com/suivi/1042"},
        {"text": "Code promo", "copy": "MERCI10"},
    ]},
    {"buttons": [{"text": "Épuisé", "disabled": True}], "align": "right"},
])

Forme brute avec Api.sendRichMessage — aucun raccourci n'y est converti : command s'écrit callback_data, webapp devient web_app: { url }, copy devient copy_text: { text }, profile devient url: "tg://user?id=…" et disabled: true devient disabled: {}.

JavaScript
Python
Api.sendRichMessage({
  rich_message: { blocks: [
    { type: "heading", text: "Ta commande #1042", size: 2 },
    { type: "paragraph", text: "Tout est prêt. Que veux-tu faire ?" },
    { type: "buttons", align: "center", buttons: [
        { text: "✅ Confirmer", style: "success", callback_data: "/confirmer" },
        { text: "✖ Annuler", style: "danger", callback_data: "/annuler" },
    ] },
    { type: "buttons", buttons: [
        { text: "Mini App", web_app: { url: "https://exemple.com/app" } },
        { text: "Code promo", copy_text: { text: "MERCI10" } },
        { text: "Mon profil", url: "tg://user?id=123456789" },
        { text: "Épuisé", disabled: {} },
    ] },
  ] },
  protect_content: true,
});
Api.sendRichMessage({
    "rich_message": {"blocks": [
        {"type": "heading", "text": "Ta commande #1042", "size": 2},
        {"type": "paragraph", "text": "Tout est prêt. Que veux-tu faire ?"},
        {"type": "buttons", "align": "center", "buttons": [
            {"text": "✅ Confirmer", "style": "success", "callback_data": "/confirmer"},
            {"text": "✖ Annuler", "style": "danger", "callback_data": "/annuler"},
        ]},
        {"type": "buttons", "buttons": [
            {"text": "Mini App", "web_app": {"url": "https://exemple.com/app"}},
            {"text": "Code promo", "copy_text": {"text": "MERCI10"}},
            {"text": "Mon profil", "url": "tg://user?id=123456789"},
            {"text": "Épuisé", "disabled": {}},
        ]},
    ]},
    "protect_content": True,
})
⚠️ À savoir — Une seule action par bouton. command fait au plus 64 octets, et la commande lancée par le clic reçoit un params vide, comme avec un bouton de clavier classique. Au-delà de 8 boutons, faites deux rangées. Le libellé est du texte simple (emoji compris). Un message riche demande une version récente de Telegram.
💡 Avec retenue — Une couleur sur le bouton qui décide, les autres neutres. Une rangée entièrement coloriée ne met plus rien en valeur.

3.7 Un écran = un seul message

Ce que la personne voit ensemble s'envoie ensemble : une image, un résultat et un bouton « Rejouer » forment un message riche — pas trois bulles (sendPhoto, puis sendRichMessage, puis sendButtons) qui s'empilent à chaque tour. Et quand un bouton rafraîchit l'écran (« Rejouer », « Page suivante », « Actualiser »), Bot.editRichMessage() modifie ce message au lieu d'en envoyer un nouveau.

JavaScript
Python
const multi = Math.round((1 + Math.random() * 9) * 100) / 100;
const blocs = [
  { photo: Props.get("media_crash", "") || "https://exemple.com/crash.png" },
  { h2: "🚀 Lucky Jet" },
  { table: [["", "Multiplicateur"], ["Ce tour", "x" + multi.toFixed(2)]] },
  { buttons: [
      { text: "🔄 Rejouer", style: "primary", command: "/luckyjet" },
  ], align: "center" },
];

if (request && request.callback_query) {
  await Bot.editRichMessage(blocs);          // appui sur « Rejouer »
} else {
  const r = await Bot.sendRichMessage(blocs); // commande tapée
  const p = r?.result?.rich_message?.blocks[0]?.photo;
  if (p && !Props.get("media_crash", "")) {
    Props.set("media_crash", p[p.length - 1].file_id);  // plus de retéléchargement
  }
}
multi = round(1 + random.random() * 9, 2)
blocs = [
    {"photo": Props.get("media_crash", "") or "https://exemple.com/crash.png"},
    {"h2": "🚀 Lucky Jet"},
    {"table": [["", "Multiplicateur"], ["Ce tour", f"x{multi:.2f}"]]},
    {"buttons": [
        {"text": "🔄 Rejouer", "style": "primary", "command": "/luckyjet"},
    ], "align": "center"},
]

if request and request.get("callback_query"):
    Bot.editRichMessage(blocs)               # appui sur « Rejouer »
else:
    r = Bot.sendRichMessage(blocs)           # commande tapée
    p = (((r or {}).get("result") or {}).get("rich_message") or {}).get("blocks", [{}])[0].get("photo")
    if p and not Props.get("media_crash", ""):
        Props.set("media_crash", p[-1]["file_id"])   # plus de retéléchargement
⚠️ Le test sur request.callback_query est obligatoire — sur un clic, message_id est déjà celui du message qui porte le bouton ; si la commande a été tapée, il n'y a aucun message du bot à modifier. Pour viser un autre message : Bot.editRichMessage(blocs, { message_id: id }).

4. Options d'envoi

Le deuxième argument accepte les mêmes options que les autres envois :

OptionTypeEffet
silentbooléenEnvoi sans notification sonore
reply_markupobjetClavier inline ou clavier personnalisé sous le message
effect_idchaîneEffet d'animation Telegram à la réception
buttons (+ align)listeRangées de boutons ajoutées en bas du message, 8 par rangée — mêmes boutons que le bloc buttons (§3.6)
JavaScript
Python
Bot.sendRichMessage([
  { h2: "Votre commande #1042" },
  { table: [["Article", "Qté", "Prix"], ["T-shirt", "2", "30 $"]] },
], {
  silent: true,
  reply_markup: {
    inline_keyboard: [[
      { text: "✅ Confirmer", callback_data: "confirm_1042" },
      { text: "❌ Annuler",   callback_data: "cancel_1042"  },
    ]],
  },
});
Bot.sendRichMessage([
    {"h2": "Votre commande #1042"},
    {"table": [["Article", "Qté", "Prix"], ["T-shirt", "2", "30 $"]]},
], {
    "silent": True,
    "reply_markup": {
        "inline_keyboard": [[
            {"text": "✅ Confirmer", "callback_data": "confirm_1042"},
            {"text": "❌ Annuler",   "callback_data": "cancel_1042"},
        ]],
    },
})

5. Bot.streamText() — l'effet « IA qui écrit »

Le texte s'affiche progressivement, mot après mot, puis devient un message définitif : c'est le comportement d'un assistant IA dans Telegram. Une seule ligne de code suffit, toute l'animation est gérée pour vous.

JavaScript
Python
// Simple
await Bot.streamText("Voici une réponse longue qui va s'afficher mot à mot…");

// Avec une phase de réflexion affichée avant la réponse
await Bot.streamText(reponse, {
  thinking: "Je consulte les données…",
  chunk: 4,        // mots ajoutés à chaque étape (défaut 5)
  delay: 500,      // MILLISECONDES entre deux étapes (défaut 600)
  blocks: [        // blocs ajoutés sous le texte, dans le message final
    { code: "resultat = 42", lang: "python" },
  ],
});
# Simple
Bot.streamText("Voici une réponse longue qui va s'afficher mot à mot…")

# Avec une phase de réflexion affichée avant la réponse
Bot.streamText(reponse, {
    "thinking": "Je consulte les données…",
    "chunk": 4,        # mots ajoutés à chaque étape (défaut 5)
    "delay": 0.5,      # SECONDES entre deux étapes (défaut 0.6)
    "blocks": [        # blocs ajoutés sous le texte, dans le message final
        {"code": "resultat = 42", "lang": "python"},
    ],
})

5.1 Options

OptionDéfautDescription
thinking—Texte de « réflexion » affiché avant que la réponse ne commence à s'écrire
chunk5Nombre de mots ajoutés à chaque étape
delayBSJS : 600 ms
BSPy : 0.6 s
Pause entre deux étapes. Minimum : 150 ms / 0,15 s
maxSteps20Nombre maximum d'étapes d'animation (plafonné à 20)
draftIdautoIdentifiant du brouillon, généré automatiquement
blocks—Blocs riches ajoutés sous le texte dans le message final
⚠️ BSJS : pensez à await — streamText() est asynchrone en JavaScript : sans await, la commande se termine avant la fin de l'animation. En BSPy l'appel est bloquant, il n'y a rien à ajouter.
⚠️ Uniquement en conversation privée — En groupe, Telegram refuse les brouillons : streamText() envoie alors directement le message final, sans animation. Aucun code particulier à écrire, c'est géré automatiquement — la commande fonctionne partout.
✅ L'animation est gratuite — Les étapes intermédiaires ne sont pas facturées. Seul le message final compte comme un envoi (1 itération).

5.2 Combiné avec BsAI

Le duo naturel : l'IA génère la réponse, streamText() l'écrit à l'écran.

JavaScript
Python
// Commande : /chat ou tout message texte
const reply = await BsAI.chat(message, { history: true });

await Bot.streamText(reply, {
  thinking: "Je réfléchis…",
  chunk: 6,
});
# Commande : /chat ou tout message texte
reply = BsAI.chat(message, history=True)

Bot.streamText(reply, {
    "thinking": "Je réfléchis…",
    "chunk": 6,
})
📖 Voir aussi — BsAI — IA conversationnelle →

6. Bot.sendRichMessageDraft() — contrôle manuel

Pour piloter l'animation soi-même (barre de progression, étapes d'un traitement, résultat qui se précise…). Réutiliser le même identifiant de brouillon d'un appel à l'autre est ce qui produit la transition animée.

JavaScript
Python
var id = 12345;                                   // entier non nul, stable

// await : l'ordre des étapes doit être respecté
await Bot.sendRichMessageDraft([{ thinking: "Je cherche…" }], id);
await Bot.sendRichMessageDraft([{ p: "Voici le" }], id);
await Bot.sendRichMessageDraft([{ p: "Voici le résultat :" }], id);

// Obligatoire : sans ceci, rien ne reste dans le chat.
Bot.sendRichMessage([{ p: "Voici le résultat : 42" }]);
did = 12345                                       # entier non nul, stable

Bot.sendRichMessageDraft([{"thinking": "Je cherche…"}], did)
Bot.sendRichMessageDraft([{"p": "Voici le"}], did)
Bot.sendRichMessageDraft([{"p": "Voici le résultat :"}], did)

# Obligatoire : sans ceci, rien ne reste dans le chat.
Bot.sendRichMessage([{"p": "Voici le résultat : 42"}])

Trois règles à connaître

  1. Le brouillon disparaît au bout de 30 secondes et ne laisse aucune trace dans le chat.
  2. Il faut toujours finir par sendRichMessage() pour que le message existe réellement.
  3. Le bloc thinking n'est accepté que dans un brouillon.
💡 Dans 90 % des cas, Bot.streamText() suffit : il fait exactement cette séquence pour vous, finalisation comprise.

7. Facturation & limites

MéthodeFacturationLimites
sendRichMessage() 1 itération par appel Fonctionne partout : privé, groupe, canal
sendRichMessageDraft() Gratuit (non facturé) Conversation privée uniquement · brouillon valable 30 s · débit plafonné
streamText() 1 itération (le message final) 20 étapes d'animation maximum · animation ignorée en groupe
Blocs média (§3.3) Compris dans l'itération du message URL publique ou file_id · pas de téléversement · Telegram plafonne à 5 Mo une photo récupérée par URL, 20 Mo pour les autres fichiers
⚠️ Débit plafonné — Les brouillons sont gratuits mais restent limités en débit (~20 envois/seconde par bot, comme tous les autres appels). Un delay trop court ne rendra pas l'animation plus fluide : les appels seront simplement mis en file d'attente.

7.1 Bonnes pratiques

8. Erreurs fréquentes

JavaScript
Python
// ❌ Écrire le format Telegram brut — inutilement verbeux
Bot.sendRichMessage([{ type: "table", cells: [[{ text: "A" }]] }]);
// ✅ Le raccourci fait la conversion
Bot.sendRichMessage([{ table: [["A"]] }]);

// ❌ thinking dans un message définitif → refusé par Telegram
Bot.sendRichMessage([{ thinking: "…" }]);
// ✅ seulement dans un brouillon
Bot.sendRichMessageDraft([{ thinking: "…" }], 1);

// ❌ Oublier la finalisation → l'utilisateur ne voit rien après 30 s
Bot.sendRichMessageDraft([{ p: "fini" }], 1);
// ✅ Ou plus simplement :
Bot.streamText("fini");

// ❌ Markdown à l'intérieur d'un bloc riche
Bot.sendRichMessage(["*gras*"]);      // affiche les astérisques
// ✅ La structure remplace le formatage : titres, citations, listes
Bot.sendRichMessage([{ h2: "Gras" }]);
// ✅ Ou la mise en forme en ligne (§3.5)
Bot.sendRichMessage([
  { type: "paragraph", text: [{ type: "bold", text: "gras" }] },
]);

// ❌ Un fichier local dans un bloc média → jamais téléversé
Bot.sendRichMessage([{ photo: "/tmp/recu.png" }]);
// ✅ URL publique, file_id… ou l'envoi média classique
Bot.sendPhoto("/tmp/recu.png", "Votre reçu");

// ❌ En forme native, une légende posée sur l'objet média → ignorée
Bot.sendRichMessage([
  { type: "photo", photo: { type: "photo", media: url, caption: "Perdue" } },
]);
// ✅ La légende se met sur le bloc — ou passez par le raccourci
Bot.sendRichMessage([{ photo: url, caption: "Affichée" }]);
# ❌ Écrire le format Telegram brut — inutilement verbeux
Bot.sendRichMessage([{"type": "table", "cells": [[{"text": "A"}]]}])
# ✅ Le raccourci fait la conversion
Bot.sendRichMessage([{"table": [["A"]]}])

# ❌ thinking dans un message définitif → refusé par Telegram
Bot.sendRichMessage([{"thinking": "…"}])
# ✅ seulement dans un brouillon
Bot.sendRichMessageDraft([{"thinking": "…"}], 1)

# ❌ Oublier la finalisation → l'utilisateur ne voit rien après 30 s
Bot.sendRichMessageDraft([{"p": "fini"}], 1)
# ✅ Ou plus simplement :
Bot.streamText("fini")

# ❌ Markdown à l'intérieur d'un bloc riche
Bot.sendRichMessage(["*gras*"])       # affiche les astérisques
# ✅ La structure remplace le formatage : titres, citations, listes
Bot.sendRichMessage([{"h2": "Gras"}])
# ✅ Ou la mise en forme en ligne (§3.5)
Bot.sendRichMessage([
    {"type": "paragraph", "text": [{"type": "bold", "text": "gras"}]},
])

# ❌ Un fichier local dans un bloc média → jamais téléversé
Bot.sendRichMessage([{"photo": "/tmp/recu.png"}])
# ✅ URL publique, file_id… ou l'envoi média classique
Bot.sendPhoto("/tmp/recu.png", "Votre reçu")

# ❌ En forme native, une légende posée sur l'objet média → ignorée
Bot.sendRichMessage([
    {"type": "photo", "photo": {"type": "photo", "media": url,
                                "caption": "Perdue"}},
])
# ✅ La légende se met sur le bloc — ou passez par le raccourci
Bot.sendRichMessage([{"photo": url, "caption": "Affichée"}])

# ❌ delay en millisecondes (c'est du JavaScript, pas du Python)
Bot.streamText(txt, {"delay": 500})   # 500 SECONDES d'attente !
# ✅ en secondes
Bot.streamText(txt, {"delay": 0.5})
⚠️ Piège classique en BSPy — delay s'exprime en secondes en Python et en millisecondes en JavaScript. Copier un exemple JS tel quel dans une commande Python fige l'animation pendant plusieurs minutes.

9. Exemples prêts à l'emploi

9.1 Grille tarifaire

JavaScript
Python
// Commande : /prix
Bot.sendRichMessage([
  { h1: "💰 Nos tarifs" },
  { table: [
      ["Forfait",  "Prix",   "Inclus"],
      ["Découverte", "0 $",  "20 crédits"],
      ["Standard",  "15 $",  "100 crédits"],
      ["Illimité",  "49 $",  "Sans limite"],
  ]},
  { quote: "Changement de forfait possible à tout moment." },
  { footer: "Tarifs mis à jour le 01/01." },
], {
  reply_markup: {
    inline_keyboard: [[{ text: "🛒 Souscrire", callback_data: "buy" }]],
  },
});
# Commande : /prix
Bot.sendRichMessage([
    {"h1": "💰 Nos tarifs"},
    {"table": [
        ["Forfait",    "Prix",  "Inclus"],
        ["Découverte", "0 $",   "20 crédits"],
        ["Standard",   "15 $",  "100 crédits"],
        ["Illimité",   "49 $",  "Sans limite"],
    ]},
    {"quote": "Changement de forfait possible à tout moment."},
    {"footer": "Tarifs mis à jour le 01/01."},
], {
    "reply_markup": {
        "inline_keyboard": [[{"text": "🛒 Souscrire", "callback_data": "buy"}]],
    },
})

9.2 Récapitulatif de commande

JavaScript
Python
// Commande : /macommande
var panier = [["T-shirt", "2", "30 $"], ["Casquette", "1", "12 $"]];

Bot.sendRichMessage([
  { h2: "🧾 Commande #1042" },
  { table: [["Article", "Qté", "Prix"]].concat(panier) },
  { p: "Total : 42 $" },
  { details: "Conditions de livraison", blocks: [
      "Livraison sous 3 à 5 jours ouvrés.",
      { list: ["Retour gratuit sous 14 jours", "Suivi par e-mail"] },
  ]},
  { divider: true },
  { footer: "Merci pour votre achat !" },
]);
# Commande : /macommande
panier = [["T-shirt", "2", "30 $"], ["Casquette", "1", "12 $"]]

Bot.sendRichMessage([
    {"h2": "🧾 Commande #1042"},
    {"table": [["Article", "Qté", "Prix"]] + panier},
    {"p": "Total : 42 $"},
    {"details": "Conditions de livraison", "blocks": [
        "Livraison sous 3 à 5 jours ouvrés.",
        {"list": ["Retour gratuit sous 14 jours", "Suivi par e-mail"]},
    ]},
    {"divider": True},
    {"footer": "Merci pour votre achat !"},
])

9.3 Traitement en plusieurs étapes

Le brouillon affiche l'avancement, le message final garde la trace du résultat.

JavaScript
Python
// Commande : /rapport
var id = 777;

await Bot.sendRichMessageDraft([{ thinking: "Récupération des données…" }], id);
var data = await HTTP.get("https://api.exemple.com/stats");

await Bot.sendRichMessageDraft([{ p: "Calcul en cours…" }], id);

Bot.sendRichMessage([
  { h2: "📊 Rapport du jour" },
  { table: [["Indicateur", "Valeur"], ["Utilisateurs", String(data.users)]] },
  { footer: "Généré automatiquement." },
]);
# Commande : /rapport
did = 777

Bot.sendRichMessageDraft([{"thinking": "Récupération des données…"}], did)
data = HTTP.get("https://api.exemple.com/stats")

Bot.sendRichMessageDraft([{"p": "Calcul en cours…"}], did)

Bot.sendRichMessage([
    {"h2": "📊 Rapport du jour"},
    {"table": [["Indicateur", "Valeur"], ["Utilisateurs", str(data["users"])]]},
    {"footer": "Généré automatiquement."},
])
📖 Documentation liée — Commandes BSJS → · Commandes BSPy → · BsAI → · Requêtes HTTP →

10. La forme brute Telegram — Api.*

Bot.sendRichMessage() est une enveloppe autour d'une vraie méthode Telegram : sendRichMessage. Tout ce que l'enveloppe n'expose pas (réponse à un message, contenu protégé, compte business, sujet de forum…) reste accessible avec Api. — l'objet qui appelle l'API Telegram directement. Le chat_id est ajouté automatiquement s'il manque.

JavaScript
Python
Api.sendRichMessage({
  rich_message: {
    blocks: [
      { type: "heading", text: "Facture #1042", size: 2 },
      { type: "photo",
        photo: { type: "photo", media: "https://exemple.com/produit.jpg" },
        caption: { text: "Article commandé" } },
      { type: "paragraph", text: "Merci de votre achat." },
    ],
  },
  protect_content: true,          // pas de transfert ni de copie
  message_effect_id: "5104841245755180586",
  reply_parameters: { message_id: ctx.message_id },
});
Api.sendRichMessage({
    "rich_message": {
        "blocks": [
            {"type": "heading", "text": "Facture #1042", "size": 2},
            {"type": "photo",
             "photo": {"type": "photo", "media": "https://exemple.com/produit.jpg"},
             "caption": {"text": "Article commandé"}},
            {"type": "paragraph", "text": "Merci de votre achat."},
        ],
    },
    "protect_content": True,          # pas de transfert ni de copie
    "message_effect_id": "5104841245755180586",
    "reply_parameters": {"message_id": message_id},
})
⚠️ En brut, plus de raccourcis — Api. ne convertit rien : { h2: "…" } ou { table: [[…]] } n'y ont aucun sens, il faut écrire { type: "heading", text: "…", size: 2 } et { type: "table", cells: [[{ text: "…" }]] }. Les raccourcis du §3 n'existent que dans Bot.sendRichMessage().

10.1 Paramètres de sendRichMessage

ParamètreRôle
chat_idDestinataire — ajouté automatiquement par Api.
rich_messageLe contenu (voir §10.2) — obligatoire
reply_markupClavier inline sous le message
reply_parametersRépondre à un message : { message_id: … }
disable_notificationEnvoi silencieux
protect_contentInterdit le transfert et la sauvegarde
message_effect_idEffet d'animation à la réception (conversations privées)
message_thread_idSujet d'un groupe forum
business_connection_idEnvoi depuis un compte business
allow_paid_broadcastEnvoi payé au-delà des limites de débit
direct_messages_topic_idSujet d'un canal à messages directs
suggested_post_parametersPublication proposée dans un canal

10.2 L'objet rich_message

Il porte le contenu, sous une seule de ces trois écritures — jamais deux à la fois :

ChampContenu
blocksLa liste de blocs structurés — c'est ce qu'emploie Bot.sendRichMessage()
htmlLe message décrit en HTML enrichi (<h1>, <table>, <img>…)
markdownLe message décrit en Markdown enrichi
mediaLes médias référencés par les liens tg:// des styles HTML / Markdown
is_rtlAffichage de droite à gauche
skip_entity_detectionN'auto-détecte pas les URL, @pseudos, #hashtags, numéros…

En style HTML ou Markdown, un média ne s'écrit pas dans le texte : on le déclare dans media avec un identifiant libre, puis on le cite par un lien tg://photo?id=… (ou tg://video?id=…, tg://audio?id=…).

JavaScript
Python
Api.sendRichMessage({
  rich_message: {
    html: '<h1>Nouveau modèle</h1>'
        + '<p>Disponible <b>aujourd\'hui</b>.</p>'
        + '<img src="tg://photo?id=produit">',
    media: [
      { id: "produit",
        media: { type: "photo", media: "https://exemple.com/produit.jpg" } },
    ],
  },
});
Api.sendRichMessage({
    "rich_message": {
        "html": ("<h1>Nouveau modèle</h1>"
                 "<p>Disponible <b>aujourd'hui</b>.</p>"
                 "<img src='tg://photo?id=produit'>"),
        "media": [
            {"id": "produit",
             "media": {"type": "photo",
                       "media": "https://exemple.com/produit.jpg"}},
        ],
    },
})
💡 id du média — 1 à 64 caractères parmi A-Z a-z 0-9 _ -. Il ne sert qu'à relier le lien tg://…?id= à l'entrée de media.

10.3 Brouillons et modification

Les deux autres méthodes riches de Telegram s'appellent de la même façon :

JavaScript
Python
// Brouillon animé — conversations privées, draft_id non nul
Api.sendRichMessageDraft({
  draft_id: 777,
  rich_message: { blocks: [{ type: "thinking", text: "Je cherche…" }] },
});

// Transformer un message déjà envoyé en message riche
Api.editMessageText({
  message_id: msgId,
  rich_message: { blocks: [{ type: "paragraph", text: "Terminé ✅" }] },
});
# Brouillon animé — conversations privées, draft_id non nul
Api.sendRichMessageDraft({
    "draft_id": 777,
    "rich_message": {"blocks": [{"type": "thinking", "text": "Je cherche…"}]},
})

# Transformer un message déjà envoyé en message riche
Api.editMessageText({
    "message_id": msg_id,
    "rich_message": {"blocks": [{"type": "paragraph", "text": "Terminé ✅"}]},
})
⚠️ Le brouillon n'accepte ni clavier ni média téléversé — sendRichMessageDraft ne prend que chat_id, draft_id, rich_message et message_thread_id. Le clavier n'apparaît donc qu'au message final, et les médias d'un brouillon doivent être des URL ou des file_id.

10.4 Et la légende d'un sendPhoto ?

Les envois média classiques — sendPhoto, sendVideo, sendDocument, sendAnimation, sendAudio, sendMediaGroup — gardent leur légende habituelle : caption en texte simple, 1024 caractères, mis en forme par parse_mode. Il n'existe pas de légende riche sur ces méthodes. Pour un média accompagné de titres, d'un tableau ou d'une longue mise en page, la réponse est l'inverse : mettre le média dans un message riche, avec un bloc média (§3.3).

JavaScript
Python
// Légende simple : la voie classique, Markdown accepté
Bot.sendPhoto("https://exemple.com/produit.jpg", "*Nouveau* modèle — 129 $");

// En brut, même chose avec toutes les options Telegram
Api.sendPhoto({
  photo: "https://exemple.com/produit.jpg",
  caption: "*Nouveau* modèle — 129 $",
  parse_mode: "Markdown",
  show_caption_above_media: true,
  has_spoiler: false,
});
# Légende simple : la voie classique, Markdown accepté
Bot.sendPhoto("https://exemple.com/produit.jpg", "*Nouveau* modèle — 129 $")

# En brut, même chose avec toutes les options Telegram
Api.sendPhoto({
    "photo": "https://exemple.com/produit.jpg",
    "caption": "*Nouveau* modèle — 129 $",
    "parse_mode": "Markdown",
    "show_caption_above_media": True,
    "has_spoiler": False,
})
✅ Même facturation — Un Api.sendRichMessage() coûte exactement la même chose qu'un Bot.sendRichMessage() : 1 itération, quel que soit le nombre de blocs ou de médias qu'il contient.