Développement de bots
Créer un bot
Créez un bot avec @BotFather, recevez des messages et répondez-y, puis passez aux boutons, commandes, photos et groupes : un tutoriel à suivre du début à la fin.
À la fin de ce tutoriel, vous aurez un bot qui répond aux messages, ajoute des boutons, gère un menu de commandes et des photos, et travaille aussi dans les groupes. Il vous faut l'application Pabal, un ordinateur pour faire tourner le programme du bot, et au choix Python 3.10 ou plus récent, ou Node.js 18 ou plus récent.
Comment fonctionne un bot
Un bot Pabal est un compte piloté par un programme. Les messages envoyés au bot s'accumulent dans sa boîte de messages ; le programme du bot demande au serveur « Quoi de neuf ? » (getUpdates), récupère ces messages, puis envoie une réponse (sendMessage). Le programme du bot tourne hors du serveur, sur votre ordinateur.
Explication du diagramme
- Trois colonnes : à gauche la personne qui utilise l'application, au milieu le serveur Pabal, à droite votre programme de bot. Les pointillés verticaux indiquent l'écoulement du temps.
- Les flèches grises sont les requêtes que fait le bot de lui-même, les flèches bleues les messages qui circulent en conséquence. Le programme du bot ne fait qu'envoyer des requêtes : le serveur ne contacte jamais le bot de sa propre initiative (sauf si vous utilisez un webhook).
- L'attente en ① (long polling) est l'élément clé : avec
timeout=30, quand il n'y a rien de nouveau, le serveur ne renvoie pas tout de suite une réponse vide ; il garde la requête jusqu'à 30 secondes et répond (③) dès qu'un message arrive (②). Le bot réagit ainsi vite, avec peu de requêtes. - L'offset en ⑥ signifie « reçu » : si vous envoyez le dernier
update_idtraité plus 1, le serveur efface tout ce qui précède. Si vous n'augmentez pas l'offset, vous recevez sans cesse les mêmes mises à jour. - ⑤ suit le même chemin qu'un message envoyé par une personne : la réponse du bot est poussée vers tous les appareils de l'application et remonte dans la liste des discussions.
1. Créer un bot avec @BotFather
On crée un bot en discutant avec @BotFather dans l'application Pabal. BotFather est un bot intégré au serveur Pabal.
- Tapez
BotFatherdans le champ de recherche de l'application et ouvrez BotFather. Appuyez sur Démarrer : la liste des commandes s'affiche. - Envoyez
/newbot. - Envoyez le nom du bot. C'est le nom affiché dans la liste des discussions : il peut contenir des accents ou n'importe quel autre caractère.
- Envoyez le nom d'utilisateur du bot : de 5 à 32 caractères (lettres latines, chiffres, tiret bas), commençant par une lettre et se terminant obligatoirement par
bot. - Quand la réponse contenant le jeton arrive, c'est terminé. Copiez le jeton et conservez-le.
Pour l'instant, BotFather répond en coréen. Dans cet échange, il demande d'abord le nom du bot (celui qui s'affiche dans la liste des discussions), puis son nom d'utilisateur ; enfin, il annonce que le bot @hello_test_bot est créé, remet son jeton et rappelle de le garder en lieu sûr, comme un mot de passe.
| Commande BotFather | Ce qu'elle fait |
|---|---|
/newbot | Créer un nouveau bot (nom → nom d'utilisateur → jeton) |
/mybots | Liste des bots que vous avez créés |
/token | Afficher de nouveau le jeton d'un bot |
/revoke | Émettre un nouveau jeton — l'ancien devient invalide immédiatement et les connexions ouvertes avec lui sont coupées |
/setcommands | Définir le menu des commandes (une ligne commande - description par commande) |
/deletebot | Supprimer un bot — confirmez en envoyant 네, 삭제합니다 (« Oui, supprimez-le »). Le nom d'utilisateur redevient disponible |
/cancel | Annuler l'opération en cours |
En ajoutant le nom d'utilisateur, par exemple /token @hello_test_bot, vous sautez l'étape « Quel bot ? ».
2. Gérer le jeton
Un jeton a la forme <ID du bot>:<secret>. Le nombre du début est l'identifiant utilisateur du bot, la suite est le secret. Un jeton suffit pour contrôler entièrement un bot : traitez-le comme un mot de passe.
- Ne l'écrivez pas dans le code : placez-le dans une variable d'environnement (
BOT_TOKEN) ou dans un coffre à secrets. Ne le publiez pas dans un dépôt public. - S'il a fuité, envoyez
/revokeà BotFather. L'ancien jeton est aussitôt refusé (401 Unauthorized), et les sessions de bot connectées en MTProto avec l'ancien jeton sont coupées elles aussi. - Le jeton figure dans l'adresse (URL) : veillez à ce que votre programme de bot n'enregistre pas les adresses de ses requêtes dans ses journaux. Le serveur Pabal non plus n'enregistre pas les adresses de la Bot API dans ses journaux.
3. Première requête — getMe
Toutes les requêtes ont pour adresse https://pabal.me/bot<jeton>/<méthode>. Vérifions avec getMe que le jeton est correct.
export BOT_TOKEN='100003:AbCdEf…'
curl -s "https://pabal.me/bot$BOT_TOKEN/getMe"# pip install requests
import os
import requests
r = requests.get(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/getMe", timeout=10)
print(r.json())// Node.js 18 ou plus récent — fetch est intégré
const res = await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/getMe`);
console.log(await res.json());En cas de succès, vous obtenez ceci. Toutes les réponses sont du JSON contenant ok et result (succès) ou error_code et description (échec).
{
"ok": true,
"result": {
"id": 100003,
"is_bot": true,
"first_name": "Bonjour Bot",
"username": "hello_test_bot",
"can_join_groups": true,
"can_read_all_group_messages": true,
"supports_inline_queries": false,
"can_connect_to_business": false,
"has_main_web_app": false
}
}
Si le jeton est faux, vous recevez HTTP 401 avec {"ok": false, "error_code": 401, "description": "Unauthorized"}.
4. Recevoir des messages — getUpdates
Ouvrez le bot dans l'application et appuyez sur Démarrer ou envoyez-lui n'importe quel message, puis récupérez les nouveautés.
curl -s "https://pabal.me/bot$BOT_TOKEN/getUpdates?timeout=30"
{
"ok": true,
"result": [
{
"update_id": 1,
"message": {
"message_id": 1,
"from": { "id": 100001, "is_bot": false, "first_name": "Léa" },
"chat": { "id": 100001, "first_name": "Léa", "type": "private" },
"date": 1789805661,
"text": "/start",
"entities": [ { "type": "bot_command", "offset": 0, "length": 6 } ]
}
}
]
}
- update_id : numéro qui augmente de 1 à chaque mise à jour. Après traitement, passez
offset=update_id+1dans la requête suivante : tout ce qui précède est effacé comme « reçu ». - timeout : nombre de secondes d'attente quand il n'y a rien de nouveau (0 à 50). Avec 0, une liste vide est renvoyée immédiatement. Une valeur de 25 à 30 est recommandée.
- chat.id : là où envoyer la réponse. Pour une conversation 1:1, c'est l'ID de la personne (positif) ; pour un groupe, un nombre négatif.
- Trois types de mises à jour peuvent être reçus :
message(nouveau message),edited_message(message modifié) etcallback_query(appui sur un bouton).
Les mises à jour non récupérées s'accumulent dans la mémoire du serveur, jusqu'aux 1 000 plus récentes par bot. Au redémarrage du serveur, celles qui n'ont pas été récupérées sont perdues (les messages eux-mêmes restent dans les conversations). Ne laissez pas votre bot éteint trop longtemps.
5. Envoyer une réponse — sendMessage
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" \
-H 'Content-Type: application/json' \
-d '{"chat_id": 100001, "text": "Bonjour !"}'requests.post(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/sendMessage",
json={"chat_id": 100001, "text": "Bonjour !"}, timeout=10)await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: 100001, text: 'Bonjour !' }),
});Envoyez les paramètres sous la forme qui vous arrange : corps JSON, formulaire (application/x-www-form-urlencoded), multipart/form-data pour envoyer des fichiers, ou chaîne de requête après l'adresse. Le résultat est le message envoyé (Message).
Un bot ne peut écrire à une personne qu'après que celle-ci lui a envoyé au moins un message. Sinon, vous obtenez 403 Forbidden: bot can't initiate conversation with a user. C'est la même règle que chez Telegram.
6. Un bot perroquet complet
Recevez et envoyez en boucle, et vous avez un bot. Voici le code complet, sans bibliothèque.
# echo.py — pip install requests
# Lancement : BOT_TOKEN='100003:…' python3 echo.py
import os
import requests
API = f"https://pabal.me/bot{os.environ['BOT_TOKEN']}"
def call(method, **params):
r = requests.post(f"{API}/{method}", json=params, timeout=60)
data = r.json()
if not data["ok"]:
raise RuntimeError(f"{method}: {data['description']}")
return data["result"]
offset = 0
print("Bot démarré. Ctrl+C pour arrêter")
while True:
for update in call("getUpdates", offset=offset, timeout=30):
offset = update["update_id"] + 1 # marquer comme reçu
message = update.get("message")
if message and "text" in message:
call("sendMessage", chat_id=message["chat"]["id"], text=message["text"])// echo.mjs — Node.js 18 ou plus récent, sans bibliothèque
// Lancement : BOT_TOKEN='100003:…' node echo.mjs
const API = `https://pabal.me/bot${process.env.BOT_TOKEN}`;
async function call(method, params = {}) {
const res = await fetch(`${API}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await res.json();
if (!data.ok) throw new Error(`${method}: ${data.description}`);
return data.result;
}
let offset = 0;
console.log('Bot démarré. Ctrl+C pour arrêter');
for (;;) {
const updates = await call('getUpdates', { offset, timeout: 30 });
for (const update of updates) {
offset = update.update_id + 1; // marquer comme reçu
const message = update.message;
if (message?.text) {
await call('sendMessage', { chat_id: message.chat.id, text: message.text });
}
}
}7. Avec une bibliothèque
Les bibliothèques de bots pour Telegram ont un réglage qui change l'adresse du serveur. Ce seul réglage suffit pour qu'elles fonctionnent telles quelles avec Pabal. Pour migrer un bot conçu pour Telegram, changez uniquement ce réglage et obtenez un nouveau jeton auprès du BotFather de Pabal.
| Bibliothèque | Réglage à changer | Version vérifiée |
|---|---|---|
| python-telegram-bot | .base_url("https://pabal.me/bot"), .base_file_url("https://pabal.me/file/bot") | 22.8 |
| aiogram | AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me")) | 3.31 |
| Sans bibliothèque (HTTP) | Début de l'adresse : https://api.telegram.org → https://pabal.me | — |
# hello_bot.py — pip install python-telegram-bot
# Lancement : BOT_TOKEN='100003:…' python3 hello_bot.py
import os
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler, ContextTypes,
MessageHandler, filters)
SERVER = "https://pabal.me"
def buttons():
return InlineKeyboardMarkup([[InlineKeyboardButton("👍 J’aime", callback_data="like"),
InlineKeyboardButton("🔢 Compter", callback_data="count")]])
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Bonjour ! Appuyez sur un bouton.", reply_markup=buttons())
async def button(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
if query.data == "like":
await query.answer("Merci !") # texte affiché brièvement chez la personne qui a appuyé
else:
n = context.chat_data.get("n", 0) + 1
context.chat_data["n"] = n
await query.answer() # d'abord répondre
await query.edit_message_text(f"Compteur : {n}", reply_markup=buttons()) # puis modifier le message
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(f"Vous avez dit « {update.message.text} ».")
def main():
app = (Application.builder().token(os.environ["BOT_TOKEN"])
.base_url(f"{SERVER}/bot") # Pabal au lieu de api.telegram.org
.base_file_url(f"{SERVER}/file/bot")
.build())
app.add_handler(CommandHandler("start", start))
app.add_handler(CallbackQueryHandler(button))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
print("Bot démarré. Ctrl+C pour arrêter")
app.run_polling()
if __name__ == "__main__":
main()# echo_aiogram.py — pip install aiogram
# Lancement : BOT_TOKEN='100003:…' python3 echo_aiogram.py
import asyncio
import os
from aiogram import Bot, Dispatcher
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.filters import CommandStart
dp = Dispatcher()
@dp.message(CommandStart())
async def start(message):
await message.answer("Bonjour ! Envoyez-moi un message.")
@dp.message()
async def echo(message):
if message.text:
await message.answer(message.text)
async def main():
session = AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me")) # Pabal
bot = Bot(os.environ["BOT_TOKEN"], session=session)
print("Bot démarré. Ctrl+C pour arrêter")
await dp.start_polling(bot)
asyncio.run(main())8. Boutons et callbacks
Pour ajouter des boutons inline à un message, passez dans reply_markup un inline_keyboard (un tableau de rangées, chaque rangée étant un tableau de boutons). Il existe deux types de boutons : les boutons callback_data, qui préviennent le bot quand on appuie dessus, et les boutons url, qui ouvrent un lien.
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -H 'Content-Type: application/json' -d '{
"chat_id": 100001,
"text": "Faites votre choix",
"reply_markup": {
"inline_keyboard": [
[ {"text": "👍 J’aime", "callback_data": "like"}, {"text": "👎 Bof", "callback_data": "dislike"} ],
[ {"text": "Ouvrir la doc Pabal", "url": "https://pabal.me/docs/"} ]
]
}
}'
Quand une personne appuie sur un bouton callback_data, le bot reçoit une mise à jour callback_query.
{
"update_id": 7,
"callback_query": {
"id": "5812039457730125441",
"from": { "id": 100001, "is_bot": false, "first_name": "Léa" },
"message": { "message_id": 4, "chat": { "id": 100001, "type": "private", "first_name": "Léa" }, "text": "Faites votre choix", … },
"chat_instance": "8413962145072395171",
"data": "like"
}
}
Le bot doit répondre dans les 10 secondes avec answerCallbackQuery. Pendant ce temps, l'application affiche une horloge sur le bouton et attend.
# Texte affiché brièvement en haut de l'écran (avec show_alert: true, une fenêtre de confirmation)
curl -s "https://pabal.me/bot$BOT_TOKEN/answerCallbackQuery" -H 'Content-Type: application/json' \
-d '{"callback_query_id": "5812039457730125441", "text": "Merci !"}'
# Changer le texte et les boutons du message concerné
curl -s "https://pabal.me/bot$BOT_TOKEN/editMessageText" -H 'Content-Type: application/json' \
-d '{"chat_id": 100001, "message_id": 4, "text": "Vous avez aimé 👍"}'
callback_datafait de 1 à 64 octets. Un message peut porter jusqu'à 100 boutons.- Si le bot ne répond pas au callback, l'application cesse d'attendre au bout de 10 secondes. Si le bot est éteint, le serveur met fin à l'attente immédiatement.
- Seule la première réponse compte. Certaines bibliothèques envoient d'abord une réponse vide quand elles modifient le message : envoyez donc d'abord la réponse qui contient un texte.
- Modifier un message avec le même texte et les mêmes boutons donne
400 Bad Request: message is not modified(comme chez Telegram).
Clavier sous le champ de saisie
Avec keyboard, un pavé de boutons s'affiche à la place du clavier, et appuyer sur un bouton envoie son texte comme message. remove_keyboard le masque, force_reply active le mode réponse.
{
"chat_id": 100001,
"text": "Lequel choisissez-vous ?",
"reply_markup": {
"keyboard": [ [ {"text": "Oui"}, {"text": "Non"} ], [ {"text": "Envoyer ma position", "request_location": true} ] ],
"resize_keyboard": true,
"one_time_keyboard": true
}
}
9. Menu des commandes
C'est la liste qui s'affiche quand on tape / dans une conversation ou qu'on appuie sur le bouton Menu. Définissez-la par le code ou avec la commande /setcommands de BotFather.
curl -s "https://pabal.me/bot$BOT_TOKEN/setMyCommands" -H 'Content-Type: application/json' -d '{
"commands": [
{"command": "start", "description": "Commencer"},
{"command": "help", "description": "Aide"}
]
}'
Une commande fait de 1 à 32 caractères (minuscules latines, chiffres, tiret bas), une description de 1 à 256 caractères, et il peut y avoir au plus 100 commandes. Si vous passez language_code, une liste distincte est enregistrée pour cette langue, mais l'application n'affiche pour l'instant que la liste par défaut, définie sans code de langue. Les commandes envoyées par une personne, comme /start, arrivent marquées bot_command dans les entities du message.
10. Envoyer et recevoir des photos
Envoyer
Envoyez le fichier en multipart/form-data, ou réutilisez le file_id d'une photo reçue auparavant. Une photo peut peser jusqu'à 10 Mo et la légende (caption) compter jusqu'à 1 024 caractères. L'envoi par URL n'est pas encore pris en charge.
curl -s "https://pabal.me/bot$BOT_TOKEN/sendPhoto" \
-F chat_id=100001 -F caption='Photo du jour' -F photo=@sunset.jpgwith open("sunset.jpg", "rb") as f:
requests.post(f"{API}/sendPhoto", data={"chat_id": 100001, "caption": "Photo du jour"},
files={"photo": f}, timeout=60)Recevoir
Une photo envoyée par une personne arrive dans le champ photo du message (une liste par taille ; chez Pabal, l'original seul). Obtenez son chemin avec getFile, puis téléchargez-la.
# 1) file_id → file_path
curl -s "https://pabal.me/bot$BOT_TOKEN/getFile?file_id=AQAAAAAAAAB7…"
# {"ok":true,"result":{"file_id":"AQAA…","file_unique_id":"AQAA…","file_size":48213,"file_path":"photos/AQAA….jpg"}}
# 2) Téléchargement — l'adresse contient /file/
curl -s -o photo.jpg "https://pabal.me/file/bot$BOT_TOKEN/photos/AQAA….jpg"
11. Dans un groupe
- Ajoutez le bot en recherchant son nom d'utilisateur, soit à la création du groupe dans l'application, soit via infos du groupe → Ajouter des membres.
- Un bot membre d'un groupe reçoit tous les messages du groupe (comme avec le « mode confidentialité désactivé » de Telegram).
chat.typevaut"group"etchat.idest négatif. - Un
sendMessagevers cechat.idenvoie le message dans le groupe. Boutons, photos et modifications fonctionnent exactement comme en 1:1. - Si le bot quitte le groupe, les envois vers ce groupe renvoient
403 Forbidden: bot is not a member of the group chat.
12. Passer aux webhooks
Si votre bot tourne sur un serveur doté d'une adresse HTTPS publique, vous pouvez, au lieu d'interroger avec getUpdates, demander au serveur Pabal d'envoyer les nouveautés à cette adresse.
curl -s "https://pabal.me/bot$BOT_TOKEN/setWebhook" -H 'Content-Type: application/json' \
-d '{"url": "https://bot.example.com/pabal-webhook", "secret_token": "longue-chaîne-aléatoire"}'
Configuration, vérification, nouvelles tentatives et réponse dans la réponse HTTP : tout est dans la page Webhooks.
Bot connecté en MTProto (Telethon)
Un bot peut aussi se connecter en MTProto, comme l'application. C'est pratique si vous utilisez déjà un outil conçu pour les comptes utilisateur (Telethon, etc.). Comme il s'agit du même compte de bot, vous pouvez le combiner avec le HTTP.
# pip install telethon==1.42.0 ← utilisez la 1.42 (voir plus bas)
# Clé publique du serveur : téléchargez https://pabal.me/docs/server-key.pem dans le même dossier
import asyncio
import os
from telethon import TelegramClient, events
from telethon.crypto import rsa
from telethon.sessions import StringSession
rsa.add_key(open("server-key.pem").read(), old=False) # clé publique du serveur Pabal
client = TelegramClient(StringSession(), api_id=1, api_hash="0" * 32)
client.session.set_dc(2, "122.34.175.215", 8443)
@client.on(events.NewMessage(incoming=True))
async def echo(event):
await event.reply(event.raw_text)
async def main():
await client.start(bot_token=os.environ["BOT_TOKEN"]) # auth.importBotAuthorization
print("Bot démarré. Ctrl+C pour arrêter")
await client.run_until_disconnected()
asyncio.run(main())
- Utilisez Telethon 1.42. Pabal parle la layer 216 ; les versions plus récentes de Telethon tentent de lire les réponses avec une layer plus élevée et échouent dès la connexion (
TypeNotFoundError). api_idetapi_hashne sont pas vérifiés par Pabal : n'importe quelle valeur convient.- Un bot MTProto reçoit les nouveaux messages poussés en temps réel par le serveur (ni getUpdates ni webhook ne sont nécessaires). Pour répondre à un callback, appelez
event.answer("…")avantevent.edit(…).
Règles et limites
| Élément | Valeur |
|---|---|
| Longueur d'un message / d'une légende de photo | 4 096 caractères / 1 024 caractères |
| Taille d'une photo (envoi avec sendPhoto) | 10 Mo |
| Taille du corps de la requête | 12 Mo |
getUpdates timeout · limit | 0 à 50 s · 1 à 100 |
| Conservation des mises à jour non récupérées | Les 1 000 plus récentes par bot, dans la mémoire du serveur (perdues au redémarrage) |
| Attente de la réponse à un callback | 10 secondes |
callback_data · nombre de boutons | 1 à 64 octets · 100 par message |
| Commandes | 1 à 32 caractères (minuscules latines, chiffres, tiret bas), description de 1 à 256 caractères, 100 au maximum |
| Engager la conversation | Impossible — la personne doit écrire au bot en premier |
Dépannage
| Symptôme | Cause et solution |
|---|---|
401 Unauthorized | Le jeton est faux ou a été changé avec /revoke. Envoyez /token à BotFather pour vérifier. |
404 Not Found: method not found | Méthode que Pabal ne prend pas encore en charge. Consultez la liste des méthodes. |
403 Forbidden: bot can't initiate conversation with a user | Cette personne n'a encore jamais écrit au bot. Demandez-lui d'ouvrir le bot dans l'application et d'appuyer sur Démarrer. |
409 Conflict: can't use getUpdates method while webhook is active | Un webhook est configuré. Appelez deleteWebhook ou recevez les mises à jour par webhook. |
| Appuyer sur un bouton ne produit rien | Le bot est éteint ou n'appelle pas answerCallbackQuery. |
| Je reçois sans cesse le même message | Vous n'augmentez pas offset. Passez l'update_id + 1 traité dans la requête suivante. |
| Le gras ou les liens ne s'appliquent pas | parse_mode n'est pas encore pris en charge : le texte est envoyé tel quel. Commandes, @mentions, URL et #hashtags sont automatiquement rendus cliquables. |
| Le bot reste muet dans un groupe | Le bot n'est pas membre du groupe. Ajoutez-le via infos du groupe → Ajouter des membres. |