3. Le code
Un petit bot de notes : /note buy milk enregistre une note, /notes affiche vos cinq dernières.
requirements.txt
aiogram>=3,<4
aiomysql
bot.py
import asyncio
import logging
import os
import sys
import aiomysql
from aiogram import Bot, Dispatcher
from aiogram.filters import Command, CommandObject
from aiogram.types import Message
dp = Dispatcher()
@dp.message(Command("note"))
async def note(message: Message, command: CommandObject, pool: aiomysql.Pool) -> None:
text = (command.args or "").strip()
if not text:
await message.answer("Write your note after the command, for example: /note buy milk")
return
if len(text) > 500:
await message.answer("A note can be up to 500 characters.")
return
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO notes (user_id, text) VALUES (%s, %s)",
(message.from_user.id, text),
)
await message.answer("Saved. Send /notes to see your last five.")
@dp.message(Command("notes"))
async def notes(message: Message, pool: aiomysql.Pool) -> None:
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"SELECT text FROM notes WHERE user_id = %s ORDER BY id DESC LIMIT 5",
(message.from_user.id,),
)
rows = await cur.fetchall()
if not rows:
await message.answer("You have no notes yet.")
return
await message.answer("\n".join(f"- {text}" for (text,) in rows))
async def main() -> None:
# One pool for the whole bot. The values come from the Environment pane.
pool = await aiomysql.create_pool(
host=os.environ["DB_HOST"],
port=int(os.environ["DB_PORT"]),
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
db=os.environ["DB_NAME"],
autocommit=True,
maxsize=5,
)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"CREATE TABLE IF NOT EXISTS notes ("
"id INT AUTO_INCREMENT PRIMARY KEY, "
"user_id BIGINT NOT NULL, "
"text VARCHAR(500) NOT NULL, "
"created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
"KEY user_id (user_id))"
)
print("Database ready")
bot = Bot(os.environ["BOT_TOKEN"])
try:
# Every keyword passed here reaches the handlers that ask for it by name.
await dp.start_polling(bot, pool=pool)
finally:
pool.close()
await pool.wait_closed()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
asyncio.run(main())
Ce qu’il fait : main() ouvre le pool, crée la table notes si elle manque et affiche Database ready avant que le bot demande des messages à Telegram. dp.start_polling(bot, pool=pool) est la façon propre à aiogram de partager un objet : chaque mot-clé supplémentaire que vous passez devient une donnée qu’aiogram transmet à tout handler ayant un paramètre de ce nom, donc note() et notes() reçoivent le même pool. Quand le bot s’arrête, le bloc finally ferme le pool.
Les marqueurs %s sont remplis par le driver, donc une note comme '); DROP TABLE notes; -- est stockée comme du texte brut. Dans notre test, elle a été enregistrée en ligne 2 et la table est restée. Ne construisez jamais le SQL avec une f-string.
Stockez les IDs Telegram en BIGINT
La documentation de la Bot API de Telegram indique qu’un ID d’utilisateur ou de chat « peut avoir plus de 32 bits significatifs » mais au plus 52, donc un entier de 64 bits suffit. Sa page sur les IDs donne les plages : les IDs d’utilisateur montent jusqu’à 1.099.511.627.775, tandis que les IDs de groupes, supergroupes et canaux sont négatifs. Les supergroupes et canaux vont de -1.000.000.000.001 à -1.997.852.516.352 (par exemple -1001234567890). Le type INT de MySQL s’arrête à 2.147.483.647. Dans notre test, avec le même mode SQL strict que celui de notre serveur de base de données, l’enregistrement de l’ID d’utilisateur 7.000.000.001 dans une colonne INT a échoué. aiomysql a levé :
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Une colonne BIGINT l’a enregistré, et -1001234567890 aussi. Utilisez un simple BIGINT, pas BIGINT UNSIGNED, pour que les IDs de chat tiennent aussi.
4. Lancez-le
- Téléversez
bot.py et requirements.txt dans Fichiers.
- Appuyez sur Démarrer et ouvrez Console. Le démarrage installe les paquets de
requirements.txt, puis affiche [PlayNHost] Starting your bot, Database ready et Run polling for bot @your_bot. Cette dernière ligne fait passer l’état dans Aperçu à En ligne sur Telegram.
- Dans Telegram, envoyez à votre bot
/note buy milk, puis /notes. Pour voir les lignes, ouvrez la page de la base de données et cliquez sur Ouvrir dans phpMyAdmin.
À partir du deuxième démarrage, la console affiche aussi Warning: Table 'notes' already exists, avec devant le chemin du fichier cursors.py d’aiomysql. C’est la remarque de MariaDB pour CREATE TABLE IF NOT EXISTS, relayée par le driver. Rien n’est cassé.
Dans notre test, le substitut de Telegram a envoyé ces commandes et reçu ces réponses :
| Envoyé | Réponse |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note et 501 caractères | A note can be up to 500 characters. |
/notes | Les notes enregistrées, les plus récentes d’abord |
/notes d’un deuxième utilisateur | You have no notes yet. |
Beaucoup de messages à la fois
aiogram traite chaque mise à jour entrante comme sa propre tâche, donc plusieurs commandes peuvent exécuter leurs requêtes en même temps. Dans notre test, 4 commandes arrivées ensemble ont chacune été traitées en environ 50 ms. maxsize=5 limite le pool à 5 connexions ; une sixième requête attend qu’une connexion se libère au lieu d’en ouvrir une autre. Après le test, le bot gardait 2 connexions ouvertes.
Le piège de l’autocommit
aiomysql ouvre les connexions avec l’autocommit désactivé. Un INSERT reste alors dans une transaction ouverte. Quand la connexion retourne au pool avec cette transaction encore ouverte, aiomysql la ferme et MariaDB jette la ligne non validée. Dans notre test, une ligne insérée sur un pool sans autocommit=True a été vue par 0 autre connexion. Soit passez autocommit=True à create_pool, comme ci-dessus, soit appelez await conn.commit() après vos écritures.
Une requête en échec n’arrête pas le bot
Dans notre test, nous avons supprimé la table pendant que le bot tournait et envoyé /notes. aiogram a journalisé l’erreur avec une trace se terminant par pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), n’a envoyé aucune réponse pour cette commande et a continué de tourner : une fois la table de retour, le /note suivant a été enregistré. La personne qui a envoyé la commande ne voit rien, donc si une requête peut échouer, interceptez l’erreur dans le handler et répondez par un court message.
Une nuit calme ne casse pas le pool
Notre serveur de base de données ferme une connexion restée inactive pendant 8 heures. Un bot qui ne reçoit aucun message aussi longtemps, pendant la nuit par exemple, trouve ses connexions du pool fermées. aiomysql le vérifie quand un handler demande une connexion et en ouvre une nouvelle à la place de celle qui est fermée. Nous l’avons testé avec la limite d’inactivité abaissée à 5 secondes : après 7 secondes de calme, le /note suivant a été enregistré sans erreur.
Les limites qui comptent pour votre pool
- 20 connexions à la fois par utilisateur de base de données. Chaque processus qui utilise le même identifiant compte. La connexion 21 est refusée avec
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 garde le bot bien en dessous.
- Une requête qui dure plus de 120 secondes est arrêtée. MariaDB y met fin avec l’erreur 1969,
Query execution was interrupted (max_statement_time exceeded). Dans un test précédent, sur un compte configuré de la même façon, cela est arrivé au bout de 120,03 secondes. Les requêtes d’un bot prennent normalement quelques millisecondes, donc cela ne touche qu’une requête qui s’emballe.
- 500 MB par base de données. La page Bases de données indique l’espace utilisé. À 500 MB, les écritures sont bloquées jusqu’à ce que vous supprimiez des lignes.
- Les sauvegardes du serveur n’incluent pas la base de données. Exportez-la de temps en temps depuis phpMyAdmin.
Les erreurs et leur sens
Voici les erreurs qu’aiomysql a données dans notre test, écrites comme elles terminent une trace dans la console (elles viennent de PyMySQL, qu’aiomysql utilise en dessous) :
pymysql.err.OperationalError: (1045, "Access denied for user ...") : mauvais DB_USER ou mauvais DB_PASSWORD. Après Réinitialiser le mot de passe sur la carte Connexion, l’ancien mot de passe cesse de fonctionner immédiatement : remplacez DB_PASSWORD et redémarrez le bot.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ...") : hôte ou port incorrect, ou connexion depuis l’extérieur de PlayNHost. La base n’accepte que les connexions des serveurs PlayNHost et de phpMyAdmin, c’est donc aussi ce que vous obtenez depuis votre propre ordinateur.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...) : trop de connexions ouvertes, voir les limites ci-dessus. Par exemple une deuxième copie du bot, ou un pool ouvert pour chaque message.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1") : une colonne d’ID est de type INT. Passez-la en BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist") : la table est absente. Le code ci-dessus la crée au démarrage ; si vous l’avez créée à la main, vérifiez son nom.
Une variable manquante dans Environnement arrête le bot avant sa connexion, avec KeyError: 'DB_HOST' ou le nom de la variable manquante, car le code lit chaque valeur avec os.environ[...]. Ajoutez-la, puis redémarrez le bot : les variables s’appliquent au prochain démarrage.
Un mauvais token apparaît après la partie base de données. Avec notre faux token contre le vrai Telegram, le bot a affiché Database ready, puis s’est arrêté avec aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Copiez à nouveau le token depuis @BotFather et collez-le sur la page Bot, volet Telegram. Plus d’erreurs Telegram : Corriger l’erreur 409 Conflict d’un bot Telegram.
La mémoire utilisée
Dans notre test, le bot en marche a utilisé 174 MB de mémoire (résidente) après avoir répondu aux 8 commandes, avec son pool ouvert, dans un conteneur limité à 512 MB comme notre plus petite offre de bot. L’essentiel vient d’aiogram lui-même : lors de notre précédent test d’hébergement, aiogram seul a mesuré 173 MB (ce bot s’est arrêté sur un token refusé, avant de traiter un message, donc les deux chiffres sont proches mais la comparaison n’est pas exacte). L’installation d’aiogram et d’aiomysql avec pip à partir de zéro a atteint 163 MB, cache de fichiers compris. Cela laisse de la place pour votre propre code sur la plus petite offre. Le guide d’hébergement compare les bibliothèques : python-telegram-bot vs aiogram sur PlayNHost.
La même chose avec une autre bibliothèque
Vous voulez un serveur pour votre bot Telegram avec une base de données incluse ? Voir les offres de bot Telegram.