3. Le code
Un petit bot de notes : /note buy milk enregistre une note, /notes affiche vos cinq dernières.
package.json
{
"name": "my-grammy-bot",
"private": true,
"main": "index.js",
"dependencies": {
"grammy": "^1.46.0",
"mysql2": "^3.24.6"
}
}
index.js
const { Bot } = require("grammy");
const mysql = require("mysql2/promise");
// One pool for the whole bot. The values come from the Environment pane.
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
connectionLimit: 5,
});
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN);
bot.command("note", async (ctx) => {
const text = ctx.match.trim();
if (!text) return ctx.reply("Write your note after the command, for example: /note buy milk");
if (text.length > 500) return ctx.reply("A note can be up to 500 characters.");
await pool.execute("INSERT INTO notes (user_id, text) VALUES (?, ?)", [ctx.from.id, text]);
await ctx.reply("Saved. Send /notes to see your last five.");
});
bot.command("notes", async (ctx) => {
const [rows] = await pool.execute(
"SELECT text FROM notes WHERE user_id = ? ORDER BY id DESC LIMIT 5",
[ctx.from.id],
);
if (rows.length === 0) return ctx.reply("You have no notes yet.");
await ctx.reply(rows.map((row) => `- ${row.text}`).join("\n"));
});
// Without this, an error in one command stops the whole bot.
bot.catch((err) => {
console.error(`Error while handling update ${err.ctx.update.update_id}:`, err.error);
});
async function main() {
await pool.query(
"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))",
);
console.log("Database ready");
// bot.start() only returns when the bot stops, so the ready line goes in onStart.
await bot.start({ onStart: (me) => console.log(`Logged in as @${me.username}`) });
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Ce qu’il fait : main() crée la table notes si elle n’existe pas et affiche Database ready avant que bot.start() ne demande les messages à Telegram. Le pool est créé une seule fois, en haut du fichier, et chaque commande l’utilise : pool.execute() emprunte une connexion, exécute la requête et rend la connexion. ctx.match est le texte après la commande : grammY retire les espaces qui le précèdent et garde le reste tel qu’il a été saisi, retours à la ligne compris. bot.catch() est important ici : selon la documentation de grammY, sans gestionnaire d’erreurs, une erreur dans un gestionnaire arrête un bot démarré avec bot.start(), et une erreur de base de données est exactement ce genre d’erreur.
Les paramètres ? envoient la note à la base de données séparément du SQL, donc une note comme '); DROP TABLE notes; -- est stockée comme du texte brut. Dans notre test, elle a été enregistrée à la ligne 2 et la table est restée. Ne construisez jamais le SQL avec une chaîne de gabarit.
Stocker les ID Telegram en BIGINT
La documentation de la Bot API de Telegram indique qu’un ID d’utilisateur ou de discussion « peut avoir plus de 32 bits significatifs », mais au plus 52, donc un entier de 64 bits suffit. Sa page sur les ID donne les plages : les ID d’utilisateur vont jusqu’à 1.099.511.627.775, alors que les ID de groupes, de supergroupes et de 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 notre serveur de base de données, l’enregistrement de l’ID d’utilisateur 7.000.000.001 dans une colonne INT a échoué. mysql2 a renvoyé le code d’erreur ER_WARN_DATA_OUT_OF_RANGE (errno 1264) avec ce message :
Out of range value for column 'user_id' at row 1
Une colonne BIGINT l’a enregistré, ainsi que -1001234567890 . Utilisez BIGINTsimple, pas BIGINT UNSIGNED, pour que les ID de discussion tiennent aussi.
4. Le lancer
- Envoyez
index.js et package.json dans Fichiers. N’envoyez pas node_modules.
- Appuyez sur Démarrer et ouvrez Console. Au démarrage, les paquets de
package.jsonsont installés, puis s’affichent [PlayNHost] Starting your bot, Database ready et Logged in as @your_bot. Cette dernière ligne fait passer l’état sous 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.
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
La documentation de grammY indique que bot.start() traite toutes les mises à jour séquentiellement : une commande se termine avant que la suivante ne commence. Dans notre test, le bot avait 1 connexion ouverte après avoir répondu aux 8 commandes. connectionLimit: 5 compte dès que votre code exécute des requêtes en parallèle, ou si vous passez à grammY runner, qui traite les mises à jour en parallèle : le pool n’ouvre jamais plus de 5 connexions, et une requête supplémentaire attend qu’une connexion se libère.
Une requête qui échoue n’arrête pas le bot
Dans notre test, nous avons supprimé la table pendant que le bot tournait et envoyé /notes. bot.catch() a affiché Error while handling update 500001: Error: Table '...notes' doesn't exist avec code: 'ER_NO_SUCH_TABLE', le bot n’a envoyé aucune réponse pour cette commande et a continué de tourner : une fois la table revenue, le /note suivant a été enregistré. La personne qui a envoyé la commande ne voit rien, donc si une requête peut échouer, entourez-la de try/catch dans le gestionnaire 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. Le pool de mysql2 s’en charge tout seul. 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.
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
ER_USER_LIMIT_REACHED. Dans notre test, un pool avec connectionLimit: 30 a échoué avec cette erreur quand 30 requêtes se sont exécutées en même temps. Un pool de 5 garde le bot bien en dessous.
- Une requête qui dure plus de 120 secondes est arrêtée. MariaDB l’interrompt 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é après 120,03 secondes. Les requêtes d’un bot prennent normalement quelques millisecondes, donc cela ne concerne qu’une requête qui s’emballe.
- 500 Mo par base de données. La page Bases de données montre l’espace utilisé. À 500 Mo, 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 signification
Voici les codes d’erreur renvoyés par mysql2 dans notre test :
ER_ACCESS_DENIED_ERROR ... Access denied for user : DB_USER ou DB_PASSWORDincorrect. 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.
ECONNREFUSED : rien n’a accepté la connexion à cet hôte et ce port. Vérifiez DB_HOST et DB_PORT. Un DB_HOST absent mène aussi ici, car mysql2 se connecte alors à localhost, où aucune base de données ne tourne : la variable manque dans Environnement, ou le bot n’a pas été redémarré après son ajout.
ENOTFOUND : DB_HOST est mal orthographié, donc le nom n’existe pas.
ETIMEDOUT : rien n’a répondu à temps. Quel que soit le message, une connexion depuis votre propre ordinateur échoue : la base de données n’accepte que les connexions des serveurs PlayNHost et de phpMyAdmin.
ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20) : trop de connexions ouvertes, voir les limites ci-dessus. Par exemple une deuxième copie du bot, ou un pool créé pour chaque message.
ER_WARN_DATA_OUT_OF_RANGE (errno 1264, Out of range value for column 'user_id' at row 1) : une colonne d’ID est en INT. Changez-la en BIGINT.
ER_NO_SUCH_TABLE (errno 1146) : 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.
Un mauvais token se voit 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é au bout d’environ un quart de seconde avec GrammyError: Call to 'deleteWebhook' failed! (401: 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.
Combien de mémoire il utilise
Dans notre test, le bot en marche a utilisé 67,6 Mo de mémoire (résidente) après avoir répondu aux 8 commandes, avec son pool ouvert, dans un conteneur plafonné à 512 Mo comme notre plus petite offre de bot. Dans notre test d’hébergement précédent, grammY seul mesurait 60 Mo (ce bot s’était arrêté sur un token refusé, avant de traiter le moindre message), donc la différence, environ 8 Mo, couvre mysql2, le pool et les commandes traitées. L’installation de grammY et mysql2 avec npm à partir de zéro a atteint un pic de 140 Mo, cache de fichiers compris.
La même chose avec une autre bibliothèque
Besoin d’un serveur pour votre bot Telegram avec une base de données incluse ? Voir les offres de bots Telegram.