Docs développeurs
Français

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.

Utilisateur Serveur Pabal Votre bot ① getUpdates?offset=0&timeout=30 Attend au plus 30 s qu'une nouveauté arrive ② envoie « Salut » ③ [{update_id: 1, message: "Salut"}] ④ sendMessage(chat_id, "Bonjour !") ⑤ Réponse dans l'app (temps réel) ⑥ getUpdates?offset=2 — 1 reçue, la suite
L'enchaînement de base : recevoir avec getUpdates, répondre avec sendMessage

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_id traité 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.

  1. Tapez BotFather dans le champ de recherche de l'application et ouvrez BotFather. Appuyez sur Démarrer : la liste des commandes s'affiche.
  2. Envoyez /newbot.
  3. 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.
  4. 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.
  5. Quand la réponse contenant le jeton arrive, c'est terminé. Copiez le jeton et conservez-le.
/newbot
BotFather새 봇을 만듭니다. 봇의 이름을 알려 주세요. (대화 목록에 보이는 이름이에요)
Bonjour Bot
BotFather좋아요. 이제 봇의 사용자명을 정해 주세요. …
hello_test_bot
BotFather완료! 새 봇 @hello_test_bot 를 만들었어요. 검색해서 대화를 시작할 수 있어요. 봇 토큰: 100003:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789 토큰은 비밀번호처럼 안전하게 보관하세요. …

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 BotFatherCe qu'elle fait
/newbotCréer un nouveau bot (nom → nom d'utilisateur → jeton)
/mybotsListe des bots que vous avez créés
/tokenAfficher 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
/setcommandsDéfinir le menu des commandes (une ligne commande - description par commande)
/deletebotSupprimer un bot — confirmez en envoyant 네, 삭제합니다 (« Oui, supprimez-le »). Le nom d'utilisateur redevient disponible
/cancelAnnuler 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+1 dans 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é) et callback_query (appui sur un bouton).
La file d'attente est dans la mémoire du serveur

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 pas engager la conversation

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èqueRéglage à changerVersion vérifiée
python-telegram-bot.base_url("https://pabal.me/bot"), .base_file_url("https://pabal.me/file/bot")22.8
aiogramAiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me"))3.31
Sans bibliothèque (HTTP)Début de l'adresse : https://api.telegram.orghttps://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_data fait 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.jpg
with 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.type vaut "group" et chat.id est négatif.
  • Un sendMessage vers ce chat.id envoie 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_id et api_hash ne 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("…") avant event.edit(…).

Règles et limites

ÉlémentValeur
Longueur d'un message / d'une légende de photo4 096 caractères / 1 024 caractères
Taille d'une photo (envoi avec sendPhoto)10 Mo
Taille du corps de la requête12 Mo
getUpdates timeout · limit0 à 50 s · 1 à 100
Conservation des mises à jour non récupéréesLes 1 000 plus récentes par bot, dans la mémoire du serveur (perdues au redémarrage)
Attente de la réponse à un callback10 secondes
callback_data · nombre de boutons1 à 64 octets · 100 par message
Commandes1 à 32 caractères (minuscules latines, chiffres, tiret bas), description de 1 à 256 caractères, 100 au maximum
Engager la conversationImpossible — la personne doit écrire au bot en premier

Dépannage

SymptômeCause et solution
401 UnauthorizedLe jeton est faux ou a été changé avec /revoke. Envoyez /token à BotFather pour vérifier.
404 Not Found: method not foundMéthode que Pabal ne prend pas encore en charge. Consultez la liste des méthodes.
403 Forbidden: bot can't initiate conversation with a userCette 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 activeUn webhook est configuré. Appelez deleteWebhook ou recevez les mises à jour par webhook.
Appuyer sur un bouton ne produit rienLe bot est éteint ou n'appelle pas answerCallbackQuery.
Je reçois sans cesse le même messageVous n'augmentez pas offset. Passez l'update_id + 1 traité dans la requête suivante.
Le gras ou les liens ne s'appliquent pasparse_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 groupeLe bot n'est pas membre du groupe. Ajoutez-le via infos du groupe → Ajouter des membres.