🌐 WebApp — Pages Web
Créez et hébergez des pages web directement depuis votre bot — idéal pour les formulaires, tableaux de bord, mini-apps et landing pages
S1 — Introduction
WebApp est une bibliothèque BotStrike qui permet à votre bot de :
- 🌍 Héberger des pages web (HTML, CSS, JS, JSON…) sur l'infrastructure BotStrike
- 🔗 Générer des URLs publiques pour vos pages
- 📝 Rendre du contenu dynamique avec un système de templates
- ⚡ Combiner avec Webhook pour créer des mini-apps interactives
/dashboard, le bot génère une page HTML personnalisée avec ses statistiques et envoie un bouton qui l'ouvre directement dans Telegram — la page est servie instantanément par BotStrike.
web_app : elle s'ouvre par-dessus la conversation, comme une application, au lieu d'un long lien qui sort vers le navigateur. Bot.sendButtons ne sait pas poser ce bouton : passez par Bot.sendMessage avec inline_keyboard. C'est même obligatoire : voir « Les pages s'ouvrent uniquement dans Telegram ».
Comment ça fonctionne
WebApp stocke vos fichiers sur l'infrastructure BotStrike et les rend accessibles via une URL publique sur pages.botstrike.net.
| Méthode | Rôle |
|---|---|
WebApp.render() | Publie le contenu comme page web accessible en ligne |
WebApp.getUrl() | Génère l'URL publique de la page |
S2 — Installation
Pour utiliser WebApp, vous devez l'installer depuis la bibliothèque :
- Ouvrez votre bot dans l'application BotStrike
- Allez dans l'onglet Bibliothèques (icône 📚)
- Cherchez WebApp dans la liste
- Appuyez sur Installer
WebApp.getUrl() et WebApp.render() sont disponibles dans vos commandes JavaScript et Python.
WebApp sans avoir installé la bibliothèque, une erreur sera levée : "WebApp BotStrike n'est pas installée. Allez dans Bibliothèques → WebApp → Installer."
🔒 Les pages s'ouvrent uniquement dans Telegram
Une page BotStrike ne s'affiche que si elle est ouverte depuis Telegram, par un bouton WebApp envoyé par votre bot (web_app: { url: … }). Telegram signe alors l'ouverture, et BotStrike vérifie cette signature avant de montrer la page.
Ouverte ailleurs — lien collé dans le texte, bouton « lien » (url), navigateur externe, WhatsApp… — la page affiche à la place, dans la langue du visiteur : « Cette page s'ouvre uniquement dans Telegram », avec un bouton qui ouvre votre bot et la mention Powered by BotStrike. Le visiteur sait quoi faire, et votre code ne reçoit aucune erreur : c'est l'ouverture qui est refusée, pas l'envoi du message.
// JavaScript
Bot.sendMessage("Votre page :", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});
# Python
Bot.sendMessage("Votre page :", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
Bot.sendMessage("Votre page : " + result.url) — le lien part, mais n'affiche que le message « ouvrir dans Telegram ». Même chose pour un bouton url:, qui ouvre le navigateur sans signature.
t.me/votrebot/app.⏱️ Une page ouverte reste valable 24 heures ; au-delà, le visiteur la rouvre depuis le bot.
Les fichiers annexes (CSS, JS, images, JSON) ne sont pas concernés : seules les pages HTML sont protégées.
S3 — WebApp.getUrl()
Génère l'URL publique d'une page hébergée par BotStrike. Utile pour créer des liens à envoyer aux utilisateurs.
getUrl() ne crée pas la page. Elle calcule seulement l'adresse (une simple chaîne de caractères) où la page se trouvera. Tant que WebApp.render() n'a pas été appelé pour publier le contenu à cette adresse, ouvrir le lien affiche « Page introuvable » (404).
Toute commande qui envoie un lien WebApp doit appeler
render() avant — le plus simple étant d'utiliser directement l'URL que render() renvoie. getUrl() ne sert que dans deux cas : construire le lien d'une page publiée par un autre appel à render(), ou référencer depuis une page HTML un fichier annexe (CSS, JS) déjà publié.
Paramètres
| Paramètre | Type | Requis | Description |
|---|---|---|---|
command | string | ❌ | Nom de la page (par défaut : nom de la commande en cours) |
options | object | ❌ | Paramètres query string ajoutés à l'URL |
Retour
Une string contenant l'URL complète, par exemple :https://pages.botstrike.net/abc123/................
https://pages.botstrike.net/eaac4e6d0f/f153ebdea3b94ff/3410a555bfb17a971c943fd7
Auparavant l'adresse finissait par
/clic : toute personne recevant le lien savait que le bot répond à /clic et pouvait envoyer la commande elle-même. Ce n'est plus le cas — l'empreinte ne se devine pas, et ne se recalcule pas depuis le lien.
- L'adresse est stable : la même commande donne toujours la même empreinte.
WebApp.getUrl()est le seul moyen de l'obtenir. Un chemin écrit à la main ne peut plus fonctionner.getUrletrenderdoivent recevoir la même chaîne : deux écritures du même nom donnent deux pages différentes, dont l'une sera vide. Le nom est nettoyé avant d'être haché — espaces en tiret, accents retirés, seulsa-z 0-9 _ - .survivent — doncma pageetma-pagedésignent bien la même page.
Exemple simple
// URL pour la commande en cours
const url = WebApp.getUrl();
Bot.sendMessage("Ouvrez votre page :", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: url } }]]
});
// URL pour une page spécifique
const dashUrl = WebApp.getUrl({ command: "dashboard" });
Bot.sendMessage("Votre tableau de bord :", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: dashUrl } }]]
});
# URL pour la commande en cours
url = WebApp.getUrl()
Bot.sendMessage("Ouvrez votre page :", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": url}}]]
})
# URL pour une page spécifique
dash_url = WebApp.getUrl({"command": "dashboard"})
Bot.sendMessage("Votre tableau de bord :", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": dash_url}}]]
})
Exemple avec query string
// Passer des données via l'URL
const url = WebApp.getUrl({
command: "profil",
options: {
user_id: user.telegramid,
theme: "dark",
lang: "fr"
}
});
// → https://pages.botstrike.net/abc123/................
Bot.sendMessage("Votre profil :", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: url } }]]
});
# Passer des données via l'URL
url = WebApp.getUrl({
"command": "profil",
"options": {
"user_id": user.telegramid,
"theme": "dark",
"lang": "fr"
}
})
# → https://pages.botstrike.net/abc123/................
Bot.sendMessage("Votre profil :", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": url}}]]
})
S4 — WebApp.render()
Rend (upload) du contenu comme une page web. Le contenu est immédiatement accessible via l'URL générée.
Paramètres
| Paramètre | Type | Requis | Description |
|---|---|---|---|
content | string | ✅* | Contenu HTML/CSS/JS à publier |
template | string | ✅* | Nom d'une commande à utiliser comme template (alternative à content) |
command | string | ❌ | Nom de la page (par défaut : commande en cours) |
mime_type | string | ❌ | Type MIME (par défaut : text/html). Obligatoire dès que le contenu n'est pas du HTML : text/css pour une feuille de style, application/javascript pour un script. Sans lui, le navigateur refuse le fichier. |
options | object | ❌ | Variables passées au template (pour <% %>) |
* Au moins un des deux est requis : content ou template
Retour
| Clé | Type | Description |
|---|---|---|
success | boolean | true si l'upload a réussi |
url | string | URL publique de la page |
WebApp.render() est asynchrone. Utilisez toujours await devant l'appel pour obtenir le résultat. Sans await, vous recevrez un objet Promise au lieu du résultat réel.
Exemple avec contenu direct
// Publier une page HTML
const result = await WebApp.render({
command: "bienvenue",
content: `<!DOCTYPE html>
<html>
<head><title>Bienvenue</title></head>
<body>
<h1>Bienvenue sur mon bot !</h1>
<p>Merci de nous rejoindre.</p>
</body>
</html>`
});
if (result.success) {
Bot.sendMessage("Page publiée :", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});
}
# Publier une page HTML
result = WebApp.render({
"command": "bienvenue",
"content": """<!DOCTYPE html>
<html>
<head><title>Bienvenue</title></head>
<body>
<h1>Bienvenue sur mon bot !</h1>
<p>Merci de nous rejoindre.</p>
</body>
</html>"""
})
if result.get("success"):
Bot.sendMessage("Page publiée :", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
Publier d'autres types de fichiers
// Publier du CSS
await WebApp.render({
command: "styles.css",
content: "body { font-family: Arial; background: #1a1a2e; color: #fff; }",
mime_type: "text/css"
});
// Publier du JSON (API statique)
await WebApp.render({
command: "config.json",
content: JSON.stringify({ version: "2.0", features: ["chat", "shop"] }),
mime_type: "application/json"
});
// Publier du JavaScript
await WebApp.render({
command: "app.js",
content: "document.addEventListener('DOMContentLoaded', () => { console.log('Chargé !'); });",
mime_type: "application/javascript"
});
# Publier du CSS
WebApp.render({
"command": "styles.css",
"content": "body { font-family: Arial; background: #1a1a2e; color: #fff; }",
"mime_type": "text/css"
})
# Publier du JSON (API statique)
WebApp.render({
"command": "config.json",
"content": json.dumps({"version": "2.0", "features": ["chat", "shop"]}),
"mime_type": "application/json"
})
# Publier du JavaScript
WebApp.render({
"command": "app.js",
"content": "document.addEventListener('DOMContentLoaded', () => { console.log('Chargé !'); });",
"mime_type": "application/javascript"
})
text/html, text/css, application/javascript, application/json, text/plain, image/svg+xml, text/xml, text/csv.
S5 — Système de Templates
WebApp dispose d'un système de templates puissant qui permet de générer du contenu dynamique. Créez une commande contenant votre template HTML, puis utilisez WebApp.render() avec le paramètre template.
Principe
- Créez une commande (ex:
tpl_dashboard) contenant du HTML avec des balises<% %> - À l'intérieur de
<% %>, écrivez des expressions qui seront évaluées - Passez des variables via le paramètre
options - Le résultat est publié comme une page web statique
Syntaxe : <% expression %>
Les balises sont remplacées par le résultat de l'expression. Deux variables sont disponibles dans le contexte :
| Variable | Description |
|---|---|
bot | L'objet Bot courant (accès à bot.name, etc.) |
options | Les options passées à render() |
Exemple de template
Étape 1 — Créez la commande tpl_profil dans votre bot :
<!DOCTYPE html>
<html>
<head>
<title>Profil de <% options.username %></title>
<style>
body { font-family: Arial; background: #0f0f23; color: #e0e0e0; padding: 40px; }
.card { background: #1a1a2e; border-radius: 12px; padding: 24px; max-width: 400px; margin: auto; }
h1 { color: #2AABEE; }
.stat { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #2a2a4a; }
</style>
</head>
<body>
<div class="card">
<h1>👤 <% options.username %></h1>
<div class="stat"><span>Points</span><span><% options.points %></span></div>
<div class="stat"><span>Niveau</span><span><% options.level %></span></div>
<div class="stat"><span>Rang</span><span><% options.rank %></span></div>
</div>
</body>
</html>
Étape 2 — Dans votre commande /profil, rendez le template :
// Commande /profil — génère une page profil personnalisée
const points = Bot.getProp("points_" + user.telegramid) || 0;
const level = Bot.getProp("level_" + user.telegramid) || 1;
const result = await WebApp.render({
template: "tpl_profil",
command: "profil_" + user.telegramid,
options: {
username: user.first_name,
points: points,
level: level,
rank: points > 1000 ? "⭐ Expert" : "🔰 Débutant"
}
});
if (result.success) {
Bot.sendMessage("📊 Votre profil :\n", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});
}
# Commande /profil — génère une page profil personnalisée
points = Bot.getProp("points_" + str(user.telegramid)) or 0
level = Bot.getProp("level_" + str(user.telegramid)) or 1
result = WebApp.render({
"template": "tpl_profil",
"command": "profil_" + str(user.telegramid),
"options": {
"username": user.first_name,
"points": points,
"level": level,
"rank": "⭐ Expert" if int(points) > 1000 else "🔰 Débutant"
}
})
if result.get("success"):
Bot.sendMessage("📊 Votre profil :\n", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
.js ; en Python, un fichier .py. Le contenu est traité comme du texte brut avec remplacement des balises <% %>, pas comme du code exécutable.
<% %>. Chaque clé passée dans options est disponible sous son nom, et aussi via l'objet options — les deux écritures ci-dessous sont équivalentes :
<% titre %> · <% options.titre %> (JavaScript) ou <% options['titre'] %> (Python)
L'objet
bot est également accessible. Toute autre chose — une variable de la commande appelante, une fonction du SDK — n'existe pas dans un template : tout ce dont la page a besoin doit passer par options.
href="", un lien manquant. La page s'affiche, mais amputée.
Ces échecs sont désormais écrits dans le journal d'erreurs du bot (écran « Erreurs »), avec le nom du gabarit et l'expression fautive. Si votre page s'affiche sans style ou avec des trous, regardez-y en premier.
S6 — Webhook + WebApp
La combinaison Webhook + WebApp permet de créer des mini-applications web interactives. La page web (WebApp) communique avec votre bot via l'API Webhook pour envoyer et recevoir des données.
Architecture
- WebApp.render() publie la page HTML
- La page contient du JavaScript qui fait des appels
fetch()vers le Webhook du bot - Le bot reçoit les requêtes webhook et répond avec des données JSON
- La page met à jour son contenu dynamiquement
Exemple : Formulaire de contact
Étape 1 — Commande tpl_contact (template de la page) :
<!DOCTYPE html>
<html>
<head>
<title>Contact — <% options.bot_name %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px 20px; }
.container { max-width: 480px; margin: auto; }
h1 { color: #2AABEE; margin-bottom: 20px; }
input, textarea { width: 100%; padding: 12px; background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px; color: #e0e0e0; font-size: 14px; margin-bottom: 12px; }
button { background: #2AABEE; color: #fff; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-size: 14px; width: 100%; }
button:hover { background: #1D95D2; }
.msg { margin-top: 12px; padding: 12px; border-radius: 8px; display: none; }
.msg.ok { display: block; background: rgba(40,167,69,0.15); color: #28A745; }
.msg.err { display: block; background: rgba(220,53,69,0.15); color: #dc3545; }
</style>
</head>
<body>
<div class="container">
<h1>📬 Contactez-nous</h1>
<form id="contactForm">
<input type="text" id="name" placeholder="Votre nom" required>
<input type="email" id="email" placeholder="Votre e-mail" required>
<textarea id="message" rows="5" placeholder="Votre message..." required></textarea>
<button type="submit">Envoyer</button>
</form>
<div class="msg" id="feedback"></div>
</div>
<script>
const WEBHOOK_URL = "<% options.webhook_url %>";
document.getElementById("contactForm").addEventListener("submit", async (e) => {
e.preventDefault();
const fb = document.getElementById("feedback");
try {
const resp = await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "contact",
name: document.getElementById("name").value,
email: document.getElementById("email").value,
message: document.getElementById("message").value
})
});
const data = await resp.json();
fb.className = "msg ok";
fb.textContent = data.message || "Message envoyé !";
} catch (err) {
fb.className = "msg err";
fb.textContent = "Erreur de connexion.";
}
});
</script>
</body>
</html>
Étape 2 — Commande /contact (publie la page) :
// Publier la page de contact avec l'URL webhook intégrée
const webhookUrl = Webhook.getUrlFor({ command: "contact_submit" });
const result = await WebApp.render({
template: "tpl_contact",
command: "contact",
options: {
bot_name: Bot.name,
webhook_url: webhookUrl
}
});
if (result.success) {
Bot.sendMessage(
"📬 Page de contact créée !\n\n" +
"Ouvrez-la avec le bouton ci-dessous.", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});
}
# Publier la page de contact avec l'URL webhook intégrée
webhook_url = Webhook.get_url_for({"command": "contact_submit"})
result = WebApp.render({
"template": "tpl_contact",
"command": "contact",
"options": {
"bot_name": Bot.name,
"webhook_url": webhook_url
}
})
if result.get("success"):
Bot.sendMessage(
"📬 Page de contact créée !\n\n"
"Ouvrez-la avec le bouton ci-dessous.", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
Étape 3 — Commande /contact_submit (traite les requêtes webhook) :
// Commande /contact_submit — reçoit les données du formulaire via webhook
const data = JSON.parse(content || "{}");
if (data.action === "contact") {
// Sauvegarder le message
const contacts = JSON.parse(Bot.getProp("contacts") || "[]");
contacts.push({
name: data.name,
email: data.email,
message: data.message,
date: new Date().toISOString()
});
Bot.setProp("contacts", JSON.stringify(contacts), "string");
// Notifier l'admin du bot
Bot.sendMessageToChatId(
Bot.getProp("admin_chat_id"),
"📩 Nouveau message de " + data.name + " (" + data.email + ") :\n" + data.message
);
}
# Commande /contact_submit — reçoit les données du formulaire via webhook
data = json.loads(content or "{}")
if data.get("action") == "contact":
# Sauvegarder le message
contacts = json.loads(Bot.getProp("contacts") or "[]")
contacts.append({
"name": data["name"],
"email": data["email"],
"message": data["message"],
"date": str(datetime.datetime.now())
})
Bot.setProp("contacts", json.dumps(contacts), "string")
# Notifier l'admin du bot
Bot.sendMessageToChatId(
Bot.getProp("admin_chat_id"),
f"📩 Nouveau message de {data['name']} ({data['email']}) :\n{data['message']}"
)
S7 — Exemples complets
Exemple 1 : Tableau de bord de statistiques
Un bot qui génère un tableau de bord avec les statistiques de l'utilisateur, mis à jour en temps réel via webhook.
// Commande /stats — génère un tableau de bord
const totalUsers = Bot.getProp("total_users") || 0;
const totalMessages = Bot.getProp("total_messages") || 0;
const activeToday = Bot.getProp("active_today") || 0;
const html = `<!DOCTYPE html>
<html><head><title>Stats</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; max-width: 700px; margin: auto; }
.stat-card { background: #1a1a2e; border-radius: 12px; padding: 24px; text-align: center; }
.stat-card .num { font-size: 36px; font-weight: 700; color: #2AABEE; }
.stat-card .label { color: #8888aa; margin-top: 4px; }
h1 { text-align: center; color: #2AABEE; margin-bottom: 24px; }
</style></head><body>
<h1>📊 ${Bot.name} — Statistiques</h1>
<div class="grid">
<div class="stat-card"><div class="num">${totalUsers}</div><div class="label">Utilisateurs</div></div>
<div class="stat-card"><div class="num">${totalMessages}</div><div class="label">Messages</div></div>
<div class="stat-card"><div class="num">${activeToday}</div><div class="label">Actifs aujourd'hui</div></div>
</div>
</body></html>`;
const result = await WebApp.render({ command: "stats", content: html });
if (result.success) {
Bot.sendMessage("📊 Votre tableau de bord :\n", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});
}
# Commande /stats — génère un tableau de bord
total_users = Bot.getProp("total_users") or 0
total_messages = Bot.getProp("total_messages") or 0
active_today = Bot.getProp("active_today") or 0
html = f"""<!DOCTYPE html>
<html><head><title>Stats</title>
<style>
body {{ font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; max-width: 700px; margin: auto; }}
.stat-card {{ background: #1a1a2e; border-radius: 12px; padding: 24px; text-align: center; }}
.stat-card .num {{ font-size: 36px; font-weight: 700; color: #2AABEE; }}
.stat-card .label {{ color: #8888aa; margin-top: 4px; }}
h1 {{ text-align: center; color: #2AABEE; margin-bottom: 24px; }}
</style></head><body>
<h1>📊 {Bot.name} — Statistiques</h1>
<div class="grid">
<div class="stat-card"><div class="num">{total_users}</div><div class="label">Utilisateurs</div></div>
<div class="stat-card"><div class="num">{total_messages}</div><div class="label">Messages</div></div>
<div class="stat-card"><div class="num">{active_today}</div><div class="label">Actifs aujourd'hui</div></div>
</div>
</body></html>"""
result = WebApp.render({"command": "stats", "content": html})
if result.get("success"):
Bot.sendMessage("📊 Votre tableau de bord :\n", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
Exemple 2 : Mini-app de sondage
Créez un sondage web interactif. Ce projet utilise deux commandes :
/sondage— Crée et publie la page HTML du sondage/sondage_vote— Reçoit les votes via webhook et met à jour les résultats
// ═══ Commande /sondage ═══
// Crée et publie la page HTML du sondage
const question = "Quel framework préférez-vous ?";
const choix = ["React", "Vue", "Angular", "Svelte"];
// Le webhook pointe vers /sondage_vote (pas /sondage !)
const webhookUrl = Webhook.getUrlFor({ command: "sondage_vote" });
const html = `<!DOCTYPE html>
<html><head><title>Sondage</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
.container { max-width: 500px; margin: auto; }
h1 { color: #2AABEE; }
.choice { background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px; padding: 14px 20px;
margin: 8px 0; cursor: pointer; transition: all 0.2s; }
.choice:hover { border-color: #2AABEE; background: rgba(42,171,238,0.1); }
.result { margin-top: 16px; display: none; }
.bar { background: #1a1a2e; border-radius: 6px; height: 32px; margin: 6px 0; overflow: hidden; position: relative; }
.bar-fill { background: #2AABEE; height: 100%; border-radius: 6px; transition: width 0.5s; }
.bar-label { position: absolute; left: 12px; top: 6px; font-size: 13px; }
</style></head><body>
<div class="container">
<h1>📊 ${question}</h1>
<div id="choices">
${choix.map(c => '<div class="choice" onclick="vote(\'' + c + '\')">' + c + '</div>').join("")}
</div>
<div class="result" id="result"></div>
</div>
<script>
async function vote(choice) {
document.getElementById("choices").innerHTML = "<p>⏳ Envoi du vote...</p>";
try {
const resp = await fetch("${webhookUrl}", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ action: "vote", choice: choice })
});
document.getElementById("choices").style.display = "none";
document.getElementById("result").style.display = "block";
document.getElementById("result").innerHTML = "<h2>✅ Merci !</h2><p>Votre vote pour <strong>" + choice + "</strong> a été enregistré.</p>";
} catch(e) {
document.getElementById("choices").innerHTML = "<p>❌ Erreur, réessayez.</p>";
}
}
</script>
</body></html>`;
const result = await WebApp.render({ command: "sondage", content: html });
if (result.success) {
Bot.sendMessage("🗳 Sondage créé !\n", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});
}
// ═══ Commande /sondage_vote ═══
// Reçoit les votes via webhook et les enregistre
// Lire le contenu envoyé par la page web
const data = JSON.parse(content || "{}");
const choice = data.choice || "";
if (!choice) {
Bot.sendMessage("⚠️ Vote invalide reçu.");
} else {
// Charger les votes existants
let votes = Bot.getProperty("sondage_votes") || {};
votes[choice] = (votes[choice] || 0) + 1;
Bot.setProperty("sondage_votes", votes);
// Envoyer une notification au propriétaire
Bot.sendMessage("📥 Nouveau vote : " + choice + "\n📊 Total: " + JSON.stringify(votes));
}
# ═══ Commande /sondage ═══
# Crée et publie la page HTML du sondage
question = "Quel framework préférez-vous ?"
choix = ["React", "Vue", "Angular", "Svelte"]
# Le webhook pointe vers /sondage_vote (pas /sondage !)
webhook_url = Webhook.get_url_for({"command": "sondage_vote"})
choices_html = ""
for c in choix:
choices_html += f'<div class="choice" onclick="vote(\'{c}\')">{c}</div>\n'
html = f"""<!DOCTYPE html>
<html><head><title>Sondage</title>
<style>
body {{ font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }}
.container {{ max-width: 500px; margin: auto; }}
h1 {{ color: #2AABEE; }}
.choice {{ background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px; padding: 14px 20px;
margin: 8px 0; cursor: pointer; transition: all 0.2s; }}
.choice:hover {{ border-color: #2AABEE; background: rgba(42,171,238,0.1); }}
.result {{ margin-top: 16px; display: none; }}
</style></head><body>
<div class="container">
<h1>📊 {question}</h1>
<div id="choices">{choices_html}</div>
<div class="result" id="result"></div>
</div>
<script>
async function vote(choice) {{
document.getElementById("choices").innerHTML = "<p>⏳ Envoi du vote...</p>";
try {{
const resp = await fetch("{webhook_url}", {{
method: "POST",
headers: {{"Content-Type": "application/json"}},
body: JSON.stringify({{ action: "vote", choice: choice }})
}});
document.getElementById("choices").style.display = "none";
document.getElementById("result").style.display = "block";
document.getElementById("result").innerHTML = "<h2>✅ Merci !</h2><p>Votre vote pour <strong>" + choice + "</strong> a été enregistré.</p>";
}} catch(e) {{
document.getElementById("choices").innerHTML = "<p>❌ Erreur, réessayez.</p>";
}}
}}
</script>
</body></html>"""
result = WebApp.render({{"command": "sondage", "content": html}})
if result.get("success"):
Bot.sendMessage("🗳 Sondage créé !\n", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
# ═══ Commande /sondage_vote ═══
# Reçoit les votes via webhook et les enregistre
# Lire le contenu envoyé par la page web
data = json.loads(content or "{}")
choice = data.get("choice", "")
if not choice:
Bot.sendMessage("⚠️ Vote invalide reçu.")
else:
# Charger les votes existants
votes = Bot.getProperty("sondage_votes") or {}
votes[choice] = votes.get(choice, 0) + 1
Bot.setProperty("sondage_votes", votes)
# Envoyer une notification au propriétaire
Bot.sendMessage(f"📥 Nouveau vote : {choice}\n📊 Total: {json_module.dumps(votes)}")
Exemple 3 : Décomposer une page en CSS, HTML et JS
Plutôt que de mettre tout le CSS, le HTML et le JavaScript dans un seul fichier, vous pouvez les publier en fichiers séparés. Chaque appel à WebApp.render() publie un fichier, et la page principale les relie avec <link> et <script src>.
WebApp.render() a tourné pour lui. Écrire le CSS dans une commande /sondage_styles ne publie rien tant que personne ne tape cette commande dans Telegram : l'adresse du style ne mène nulle part, la page s'affiche toute nue, et aucun message d'erreur n'apparaît — ni dans le bot, ni dans le navigateur.
C'est pourquoi l'exemple ci-dessous publie les trois fichiers depuis une seule commande, le style et le script avant le HTML. Une seule commande à lancer, et la page est complète.
mime_typeest obligatoire pour le CSS (text/css) et le JS (application/javascript). Sans lui le fichier est servi comme une page HTML, et le navigateur refuse de l'appliquer ou de l'exécuter.- Les adresses viennent de
WebApp.getUrl(), jamais d'un chemin écrit à la main : chaque page vit dans un dossier propre à l'utilisateur qui l'ouvre.
// ═══ Commande /sondage ═══
// Publie les trois fichiers, puis renvoie le lien de la page.
// ── 1. Le style, dans son propre fichier ──
const css = `
body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
.container { max-width: 500px; margin: auto; }
h1 { color: #2AABEE; }
.choice { background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px;
padding: 14px 20px; margin: 8px 0; cursor: pointer; transition: all 0.2s; }
.choice:hover { border-color: #2AABEE; background: rgba(42,171,238,0.1); }
.result { margin-top: 16px; display: none; }
`;
await WebApp.render({
command: "sondage_styles",
content: css,
mime_type: "text/css" // ← sans ça, le style ne s'appliquera pas
});
// ── 2. Le script, dans son propre fichier ──
const js = `
async function vote(choice) {
const webhookUrl = document.body.dataset.webhook;
document.getElementById("choices").innerHTML = "<p>⏳ Envoi du vote...</p>";
try {
await fetch(webhookUrl, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ action: "vote", choice: choice })
});
document.getElementById("choices").style.display = "none";
document.getElementById("result").style.display = "block";
document.getElementById("result").innerHTML = "<h2>✅ Merci !</h2>";
} catch(e) {
document.getElementById("choices").innerHTML = "<p>❌ Erreur, réessayez.</p>";
}
}
`;
await WebApp.render({
command: "sondage_script",
content: js,
mime_type: "application/javascript" // ← sans ça, le script ne s'exécutera pas
});
// ── 3. La page, qui relie les deux ──
const question = "Quel framework préférez-vous ?";
const choix = ["React", "Vue", "Angular", "Svelte"];
const webhookUrl = Webhook.getUrlFor({ command: "sondage_vote" });
// Les adresses des deux fichiers publiés ci-dessus. Le nom doit être
// EXACTEMENT celui passé à render() — un caractère de plus, et la page
// pointe vers du vide.
const cssUrl = WebApp.getUrl({ command: "sondage_styles" });
const jsUrl = WebApp.getUrl({ command: "sondage_script" });
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sondage</title>
<link rel="stylesheet" href="${cssUrl}">
</head>
<body data-webhook="${webhookUrl}">
<div class="container">
<h1>📊 ${question}</h1>
<div id="choices">
${choix.map(c => '<div class="choice" onclick="vote(\''+c+'\')">'+c+'</div>').join("")}
</div>
<div class="result" id="result"></div>
</div>
<script src="${jsUrl}"></script>
</body>
</html>`;
const result = await WebApp.render({ command: "sondage", content: html });
if (result.success) {
Bot.sendMessage("🗳 Sondage complet créé !\n", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});
}
# ═══ Commande /sondage ═══
# Publie les trois fichiers, puis renvoie le lien de la page.
# ── 1. Le style, dans son propre fichier ──
css = """
body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
.container { max-width: 500px; margin: auto; }
h1 { color: #2AABEE; }
.choice { background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 8px;
padding: 14px 20px; margin: 8px 0; cursor: pointer; transition: all 0.2s; }
.choice:hover { border-color: #2AABEE; background: rgba(42,171,238,0.1); }
.result { margin-top: 16px; display: none; }
"""
WebApp.render({
"command": "sondage_styles",
"content": css,
"mime_type": "text/css" # ← sans ça, le style ne s'appliquera pas
})
# ── 2. Le script, dans son propre fichier ──
js = """
async function vote(choice) {
const webhookUrl = document.body.dataset.webhook;
document.getElementById("choices").innerHTML = "⏳ Envoi du vote...
";
try {
await fetch(webhookUrl, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ action: "vote", choice: choice })
});
document.getElementById("choices").style.display = "none";
document.getElementById("result").style.display = "block";
document.getElementById("result").innerHTML = "✅ Merci !
";
} catch(e) {
document.getElementById("choices").innerHTML = "❌ Erreur, réessayez.
";
}
}
"""
WebApp.render({
"command": "sondage_script",
"content": js,
"mime_type": "application/javascript" # ← sans ça, le script ne s'exécutera pas
})
# ── 3. La page, qui relie les deux ──
question = "Quel framework préférez-vous ?"
choix = ["React", "Vue", "Angular", "Svelte"]
webhook_url = Webhook.get_url_for({"command": "sondage_vote"})
# Les adresses des deux fichiers publiés ci-dessus. Le nom doit être
# EXACTEMENT celui passé à render() — un caractère de plus, et la page
# pointe vers du vide.
css_url = WebApp.getUrl({"command": "sondage_styles"})
js_url = WebApp.getUrl({"command": "sondage_script"})
choices_html = ""
for c in choix:
choices_html += f'<div class="choice" onclick="vote(\'{c}\')">{c}</div>\n'
html = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sondage</title>
<link rel="stylesheet" href="{css_url}">
</head>
<body data-webhook="{webhook_url}">
<div class="container">
<h1>📊 {question}</h1>
<div id="choices">{choices_html}</div>
<div class="result" id="result"></div>
</div>
<script src="{js_url}"></script>
</body>
</html>"""
result = WebApp.render({"command": "sondage", "content": html})
if result.get("success"):
Bot.sendMessage("🗳 Sondage complet créé !\n", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
WebApp.getUrl). Trois réponses possibles :
- « Page introuvable » → le fichier n'a jamais été publié, ou le nom passé à
getUrlne correspond pas exactement à celui passé àrender. - le CSS s'affiche comme une page web → le
mime_typea été oublié. - le CSS s'affiche en texte brut → tout va bien, le problème est ailleurs (nom de classe, balise
<link>hors du<head>).
Exemple 4 : Page de statut du service
Publiez une page de statut accessible publiquement, mise à jour automatiquement par le bot.
// Commande /update_status — met à jour la page de statut
const services = [
{ name: "API", status: "ok" },
{ name: "Base de données", status: "ok" },
{ name: "Paiements", status: "maintenance" }
];
const statusColors = { ok: "#28A745", degraded: "#F59E0B", down: "#dc3545", maintenance: "#6b7280" };
const statusLabels = { ok: "Opérationnel", degraded: "Dégradé", down: "Hors service", maintenance: "Maintenance" };
const rows = services.map(s =>
`<tr><td>${s.name}</td><td style="color:${statusColors[s.status]}">● ${statusLabels[s.status]}</td></tr>`
).join("");
const html = `<!DOCTYPE html>
<html><head><title>Statut</title>
<style>
body { font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }
table { width: 100%; max-width: 600px; margin: auto; border-collapse: collapse; }
td { padding: 12px; border-bottom: 1px solid #2a2a4a; }
h1 { text-align: center; color: #2AABEE; margin-bottom: 24px; }
.updated { text-align: center; color: #8888aa; font-size: 12px; margin-top: 16px; }
</style></head><body>
<h1>🟢 Statut des services</h1>
<table>${rows}</table>
<p class="updated">Mis à jour : ${new Date().toLocaleString("fr-FR")}</p>
</body></html>`;
const result = await WebApp.render({ command: "status", content: html });
if (result.success) {
Bot.sendMessage("✅ Page de statut mise à jour :\n", {
inline_keyboard: [[{ text: "Ouvrir", web_app: { url: result.url } }]]
});
}
# Commande /update_status — met à jour la page de statut
services = [
{"name": "API", "status": "ok"},
{"name": "Base de données", "status": "ok"},
{"name": "Paiements", "status": "maintenance"}
]
status_colors = {"ok": "#28A745", "degraded": "#F59E0B", "down": "#dc3545", "maintenance": "#6b7280"}
status_labels = {"ok": "Opérationnel", "degraded": "Dégradé", "down": "Hors service", "maintenance": "Maintenance"}
rows = ""
for s in services:
color = status_colors[s["status"]]
label = status_labels[s["status"]]
rows += f'<tr><td>{s["name"]}</td><td style="color:{color}">● {label}</td></tr>\n'
html = f"""<!DOCTYPE html>
<html><head><title>Statut</title>
<style>
body {{ font-family: 'Segoe UI', sans-serif; background: #0f0f23; color: #e0e0e0; padding: 40px; }}
table {{ width: 100%; max-width: 600px; margin: auto; border-collapse: collapse; }}
td {{ padding: 12px; border-bottom: 1px solid #2a2a4a; }}
h1 {{ text-align: center; color: #2AABEE; margin-bottom: 24px; }}
.updated {{ text-align: center; color: #8888aa; font-size: 12px; margin-top: 16px; }}
</style></head><body>
<h1>🟢 Statut des services</h1>
<table>{rows}</table>
<p class="updated">Mis à jour : {datetime.datetime.now().strftime('%d/%m/%Y %H:%M')}</p>
</body></html>"""
result = WebApp.render({{"command": "status", "content": html}})
if result.get("success"):
Bot.sendMessage("✅ Page de statut mise à jour :\n", {
"inline_keyboard": [[{"text": "Ouvrir", "web_app": {"url": result["url"]}}]]
})
Paiement en étoiles Telegram
Un bouton de votre page peut ouvrir la fenêtre de paiement Telegram et faire payer en étoiles (devise XTR), sans prestataire de paiement. Trois morceaux :
- 1. Le lien —
Api.createInvoiceLink, créé dans la commande qui publie la page. Le lien se trouve dansresult. - 2. Le paiement — la page appelle
Telegram.WebApp.openInvoice(lien, callback). - 3. La réception — la commande
!paymentest lancée à chaque paiement réussi, avec la variablepayment.
Publier une page payante
// Commande /jeu
const r = await Api.createInvoiceLink({
title: "Prédiction",
description: "Le meilleur coup pour ta partie",
payload: "prediction",
currency: "XTR",
prices: [{ label: "Prédiction", amount: 1 }]
});
const html = `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>
<button id="pred">🔮 Prédiction (1 ⭐)</button>
<div id="resultat"></div>
<script>
const LIEN = ${JSON.stringify(r.result)};
document.getElementById("pred").onclick = function () {
Telegram.WebApp.openInvoice(LIEN, function (statut) {
if (statut === "paid") {
document.getElementById("resultat").textContent = "Joue au centre !";
}
});
};
</script>
</body>
</html>`;
const page = await WebApp.render({ command: "jeu", content: html });
Bot.sendMessage("🎮 Ta partie est prête :", {
inline_keyboard: [[{ text: "🎮 Jouer", web_app: { url: page.url } }]]
});
# Commande /jeu (json est déjà disponible, sans import)
r = Api.createInvoiceLink({
"title": "Prédiction",
"description": "Le meilleur coup pour ta partie",
"payload": "prediction",
"currency": "XTR",
"prices": [{"label": "Prédiction", "amount": 1}]
})
html = """<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>
<button id="pred">🔮 Prédiction (1 ⭐)</button>
<div id="resultat"></div>
<script>
const LIEN = __LIEN__;
document.getElementById("pred").onclick = function () {
Telegram.WebApp.openInvoice(LIEN, function (statut) {
if (statut === "paid") {
document.getElementById("resultat").textContent = "Joue au centre !";
}
});
};
</script>
</body>
</html>""".replace("__LIEN__", json.dumps(r.get("result")))
page = WebApp.render({"command": "jeu", "content": html})
Bot.sendMessage("🎮 Ta partie est prête :", {
"inline_keyboard": [[{"text": "🎮 Jouer", "web_app": {"url": page["url"]}}]]
})
telegram-web-app.js dans le <head>, une page ouverte par un bouton web_app (pas un lien), et un lien écrit avec JSON.stringify / json.dumps. Sinon openInvoice n'existe pas.
La commande !payment
Créez une commande nommée exactement !payment. Elle est lancée dans la conversation de l'acheteur à chaque paiement réussi. La variable payment contient currency, total_amount (nombre d'étoiles), invoice_payload et telegram_payment_charge_id (utile pour rembourser avec Api.refundStarPayment).
// Commande !payment
if (payment.invoice_payload === "prediction") {
const total = (User.getProp("etoiles_payees") || 0) + payment.total_amount;
User.setProp("etoiles_payees", total);
Bot.sendMessage("Merci pour ton étoile ⭐ !");
}
# Commande !payment
if payment["invoice_payload"] == "prediction":
total = (User.getProp("etoiles_payees") or 0) + payment["total_amount"]
User.setProp("etoiles_payees", total)
Bot.sendMessage("Merci pour ton étoile ⭐ !")
"paid" est reçu par le navigateur et peut être simulé. Pour une petite récompense affichée dans la page, c'est acceptable. Pour quelque chose de valeur (crédits, accès), livrez-le dans !payment, côté bot.
S8 — Sécurité
1. Données sensibles : Ne stockez jamais de tokens, mots de passe ou clés API directement dans le contenu HTML. Utilisez les
bot_properties côté serveur et passez uniquement les données nécessaires côté client.
2. Pages ouvertes dans Telegram : une page ne s'affiche que depuis un bouton WebApp de votre bot (voir plus haut). Ce n'est pas un contrôle d'accès par personne : tout utilisateur de votre bot qui reçoit le bouton voit la page. N'y publiez pas d'informations confidentielles.
3. Validation webhook : Si votre page envoie des données au webhook, validez toujours les données côté bot avant de les traiter.
4. XSS : Si vous injectez des données utilisateur dans le HTML (via templates ou contenu direct), échappez-les correctement pour éviter les attaques XSS.
Bonnes pratiques
- ✅ Utilisez des noms de commandes uniques par utilisateur (ex:
profil_12345) pour éviter les collisions - ✅ Validez toutes les entrées côté webhook avant traitement
- ✅ Limitez les données passées via
optionsdans les templates à ce qui est strictement nécessaire - ✅ Utilisez HTTPS (automatique avec BotStrike)
- ❌ Ne publiez pas de tokens ou secrets dans le HTML
- ❌ Ne faites pas confiance aux données envoyées depuis la page web sans validation
Échapper les données utilisateur
// Fonction d'échappement HTML
function escapeHtml(text) {
return String(text)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// Utilisation dans le contenu
const safeName = escapeHtml(user.first_name);
const html = `<h1>Bonjour ${safeName}</h1>`;
await WebApp.render({ command: "page", content: html });
# Fonction d'échappement HTML
safe_name = html.escape(user.first_name)
content = f"<h1>Bonjour {safe_name}</h1>"
WebApp.render({"command": "page", "content": content})
S9 — Récapitulatif
| Méthode | Description | Retour |
|---|---|---|
WebApp.getUrl() |
Génère l'URL publique d'une page | string — URL complète |
WebApp.render() |
Upload du contenu comme page web | { success, url } |
Paramètres render()
| Paramètre | Requis | Description |
|---|---|---|
content | ✅* | Contenu à publier (HTML, CSS, JS, JSON…) |
template | ✅* | Nom de la commande template |
command | ❌ | Nom de la page (défaut: commande en cours) |
mime_type | ❌ | Type MIME (défaut: text/html) |
options | ❌ | Variables pour les templates <% %> |
* content ou template — au moins un des deux
• La bibliothèque WebApp doit être installée depuis l'onglet Bibliothèques
• Les pages sont hébergées sur
pages.botstrike.net par BotStrike
• Combinez avec Webhook pour créer des mini-apps interactives
• Les templates utilisent
<% expression %> pour le contenu dynamique
• Toutes les pages sont publiques — ne publiez pas d'informations sensibles
• L'URL générée est unique et sécurisée pour votre bot