✨ 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.
| Besoin | Méthode conseillée |
|---|---|
| Une phrase, une réponse courte | Bot.sendMessage() |
| Un message long, un récapitulatif, un tableau de prix | Bot.sendRichMessage() |
| Une photo seule avec une légende | Bot.sendPhoto() |
| Une photo ou une vidéo dans un message mis en page | Bloc média (§3.3) |
| Une réponse d'IA qui s'écrit progressivement | Bot.streamText() |
| Piloter soi-même l'animation d'écriture | Bot.sendRichMessageDraft() |
| Une option Telegram non exposée par le SDK | Api.sendRichMessage() (§10) |
Bot.sendMessage() reste le bon choix. Les messages riches servent
quand la structure apporte quelque chose : un catalogue, une
facture, un rapport, une FAQ.
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.
// 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."},
])
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 |
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.
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.
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},
])
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 :
| Option | Effet |
|---|---|
caption | La légende affichée sous le média |
credit | La source, en plus petit sous la légende |
spoiler: true | Média flouté, révélé au clic (photo, vidéo, GIF) |
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"},
]]},
})
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.
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.
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 |
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"},
])
{ collage: [{ photo: url1 }, { video: url2 }] }.
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: "…" }.
| Morceau | Ré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 |
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"},
]},
])
{ 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.
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: {}.
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,
})
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.
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.
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
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 :
| Option | Type | Effet |
|---|---|---|
silent | booléen | Envoi sans notification sonore |
reply_markup | objet | Clavier inline ou clavier personnalisé sous le message |
effect_id | chaîne | Effet d'animation Telegram à la réception |
buttons (+ align) | liste | Rangées de boutons ajoutées en bas du message, 8 par rangée — mêmes boutons que le bloc buttons (§3.6) |
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.
// 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
| Option | Défaut | Description |
|---|---|---|
thinking | — | Texte de « réflexion » affiché avant que la réponse ne commence à s'écrire |
chunk | 5 | Nombre de mots ajoutés à chaque étape |
delay | BSJS : 600 msBSPy : 0.6 s | Pause entre deux étapes. Minimum : 150 ms / 0,15 s |
maxSteps | 20 | Nombre maximum d'étapes d'animation (plafonné à 20) |
draftId | auto | Identifiant du brouillon, généré automatiquement |
blocks | — | Blocs riches ajoutés sous le texte dans le message final |
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.
streamText() envoie alors directement le
message final, sans animation. Aucun code particulier à écrire,
c'est géré automatiquement — la commande fonctionne partout.
5.2 Combiné avec BsAI
Le duo naturel : l'IA génère la réponse, streamText() l'écrit à l'écran.
// 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,
})
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.
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
- Le brouillon disparaît au bout de 30 secondes et ne laisse aucune trace dans le chat.
- Il faut toujours finir par
sendRichMessage()pour que le message existe réellement. - Le bloc
thinkingn'est accepté que dans un brouillon.
Bot.streamText() suffit :
il fait exactement cette séquence pour vous, finalisation comprise.
7. Facturation & limites
| Méthode | Facturation | Limites |
|---|---|---|
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 |
delay trop court ne rendra pas l'animation plus fluide : les
appels seront simplement mis en file d'attente.
7.1 Bonnes pratiques
- Un
delayentre 400 et 800 ms donne l'effet le plus naturel. - Gardez les textes de
thinkingcourts : ils ne sont visibles que quelques secondes. - Pour un tableau, envoyez des chaînes de caractères dans les cellules ; les nombres sont convertis automatiquement.
- Les tableaux larges sont tronqués sur mobile : préférez 2 à 4 colonnes.
8. Erreurs fréquentes
// ❌ É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})
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
// 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
// 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.
// 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."},
])
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.
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},
})
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ètre | Rôle |
|---|---|
chat_id | Destinataire — ajouté automatiquement par Api. |
rich_message | Le contenu (voir §10.2) — obligatoire |
reply_markup | Clavier inline sous le message |
reply_parameters | Répondre à un message : { message_id: … } |
disable_notification | Envoi silencieux |
protect_content | Interdit le transfert et la sauvegarde |
message_effect_id | Effet d'animation à la réception (conversations privées) |
message_thread_id | Sujet d'un groupe forum |
business_connection_id | Envoi depuis un compte business |
allow_paid_broadcast | Envoi payé au-delà des limites de débit |
direct_messages_topic_id | Sujet d'un canal à messages directs |
suggested_post_parameters | Publication 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 :
| Champ | Contenu |
|---|---|
blocks | La liste de blocs structurés — c'est ce qu'emploie Bot.sendRichMessage() |
html | Le message décrit en HTML enrichi (<h1>, <table>, <img>…) |
markdown | Le message décrit en Markdown enrichi |
media | Les médias référencés par les liens tg:// des styles HTML / Markdown |
is_rtl | Affichage de droite à gauche |
skip_entity_detection | N'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=…).
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 :
// 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é ✅"}]},
})
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).
// 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,
})
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.