3. El código
Un pequeño bot de notas: /note buy milk guarda una nota, /notes muestra tus últimas cinco.
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);
});
Qué hace: main() crea la tabla notes si no existe e imprime Database ready antes de que bot.start() pida mensajes a Telegram. El pool se crea una sola vez, al principio del archivo, y todos los comandos lo usan: pool.execute() toma prestada una conexión, ejecuta la consulta y la devuelve. ctx.match es el texto que va después del comando: grammY quita los espacios de delante y conserva el resto tal como se escribió, saltos de línea incluidos. bot.catch() importa aquí: la documentación de grammY dice que, sin un manejador de errores, un error en un manejador detiene un bot iniciado con bot.start(), y un error de base de datos es justo ese tipo de error.
Los marcadores ? envían la nota a la base de datos separada del SQL, así que una nota como '); DROP TABLE notes; -- se guarda como texto normal. En nuestra prueba se guardó como la fila 2 y la tabla siguió ahí. Nunca construyas el SQL con una plantilla de texto.
Guarda los IDs de Telegram como BIGINT
La documentación de la Bot API de Telegram dice que un ID de usuario o de chat «puede tener más de 32 bits significativos», pero como máximo 52, así que un entero de 64 bits es seguro. Su página sobre los IDs da los rangos: los IDs de usuario llegan hasta 1.099.511.627.775, mientras que los IDs de grupos, supergrupos y canales son negativos. Los supergrupos y canales van de -1.000.000.000.001 a -1.997.852.516.352 (por ejemplo -1001234567890). El INT de MySQL llega hasta 2.147.483.647. En nuestra prueba, con el mismo modo SQL estricto que usa nuestro servidor de bases de datos, guardar el ID de usuario 7.000.000.001 en una columna INT falló. mysql2 dio el código de error ER_WARN_DATA_OUT_OF_RANGE (errno 1264) con este mensaje:
Out of range value for column 'user_id' at row 1
Una columna BIGINT lo guardó, y también -1001234567890 . Usa BIGINTa secas, no BIGINT UNSIGNED, para que también quepan los IDs de chat.
4. Ejecútalo
- Sube
index.js y package.json en Archivos. No subas node_modules.
- Pulsa Iniciar y abre Consola. Al iniciar se instalan los paquetes de
package.json, y después se muestran [PlayNHost] Starting your bot, Database ready y Logged in as @your_bot. Esa última línea cambia el estado en Resumen a En línea en Telegram.
- En Telegram, envía a tu bot
/note buy milky después /notes. Para ver las filas, abre la página de la base de datos y haz clic en Abrir en phpMyAdmin.
En nuestra prueba, el sustituto de Telegram envió estos comandos y recibió estas respuestas:
| Enviado | Respuesta |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note y 501 caracteres | A note can be up to 500 characters. |
/notes | Las notas guardadas, las más recientes primero |
/notes de un segundo usuario | You have no notes yet. |
Muchos mensajes a la vez
La documentación de grammY dice que bot.start() procesa todas las actualizaciones de forma secuencial: un comando termina antes de que empiece el siguiente. En nuestra prueba el bot mantuvo 1 conexión abierta tras responder a los 8 comandos. connectionLimit: 5 importa cuando tu código ejecuta consultas en paralelo, o si pasas a grammY runner, que procesa las actualizaciones de forma concurrente: el pool nunca abre más de 5 conexiones, y una consulta adicional espera a que haya una libre.
Una consulta fallida no detiene el bot
En nuestra prueba borramos la tabla mientras el bot estaba en marcha y enviamos /notes. bot.catch() imprimió Error while handling update 500001: Error: Table '...notes' doesn't exist con code: 'ER_NO_SUCH_TABLE', el bot no envió ninguna respuesta a ese comando y siguió funcionando: cuando la tabla volvió, el siguiente /note se guardó. Quien envió el comando no ve nada, así que si una consulta puede fallar, envuélvela en try/catch dentro del manejador y responde con un mensaje corto.
Una noche tranquila no rompe el pool
Nuestro servidor de bases de datos cierra una conexión que ha estado inactiva durante 8 horas. Un bot que no recibe mensajes durante tanto tiempo, por ejemplo por la noche, encuentra cerradas sus conexiones del pool. El pool de mysql2 lo gestiona solo. Lo probamos con el límite de inactividad reducido a 5 segundos: tras 7 segundos de calma, el siguiente /note se guardó sin errores.
Límites que importan para tu pool
- 20 conexiones a la vez por usuario de base de datos. Cuenta cada proceso que use el mismo acceso. La conexión 21 se rechaza con
ER_USER_LIMIT_REACHED. En nuestra prueba, un pool con connectionLimit: 30 falló con ese error cuando se ejecutaron 30 consultas a la vez. Un pool de 5 mantiene al bot muy por debajo.
- Una consulta que se ejecuta durante más de 120 segundos se detiene. MariaDB la termina con el error 1969,
Query execution was interrupted (max_statement_time exceeded). En una prueba anterior nuestra, con una cuenta configurada igual, eso llegó a los 120,03 segundos. Las consultas de un bot normalmente tardan milisegundos, así que esto solo afecta a una consulta descontrolada.
- 500 MB por base de datos. La página Bases de datos muestra cuánto se usa. Al llegar a 500 MB se detienen las escrituras hasta que borres filas.
- Las copias de seguridad del servidor no incluyen la base de datos. Expórtala de vez en cuando desde phpMyAdmin.
Errores y qué significan
Estos son los códigos de error que dio mysql2 en nuestra prueba:
ER_ACCESS_DENIED_ERROR ... Access denied for user: DB_USER o DB_PASSWORDincorrectos. Después de Restablecer contraseña en la tarjeta Conexión la contraseña antigua deja de funcionar al instante: sustituye DB_PASSWORD y reinicia el bot.
ECONNREFUSED: nada aceptó la conexión en ese host y puerto. Comprueba DB_HOST y DB_PORT. Si falta DB_HOST también acabas aquí, porque mysql2 se conecta entonces a localhost, donde no hay ninguna base de datos: falta la variable en Entorno, o no se reinició el bot después de añadirla.
ENOTFOUND: DB_HOST está mal escrito, así que el nombre no existe.
ETIMEDOUT: nada respondió a tiempo. Sea cual sea el mensaje, una conexión desde tu propio ordenador falla: la base de datos solo acepta conexiones desde servidores de PlayNHost y phpMyAdmin.
ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): hay demasiadas conexiones abiertas, mira los límites de arriba. Por ejemplo, una segunda copia del bot o un pool creado para cada mensaje.
ER_WARN_DATA_OUT_OF_RANGE (errno 1264, Out of range value for column 'user_id' at row 1): una columna de ID es INT. Cámbiala a BIGINT.
ER_NO_SUCH_TABLE (errno 1146): falta la tabla. El código de arriba la crea al arrancar; si la hiciste a mano, revisa su nombre.
Un token incorrecto aparece después de la parte de la base de datos. Con nuestro token falso contra el Telegram real, el bot imprimió Database readyy se detuvo tras un cuarto de segundo aproximadamente con GrammyError: Call to 'deleteWebhook' failed! (401: Unauthorized). Copia de nuevo el token desde @BotFather y pégalo en la página Bot , panel Telegram. Más errores de Telegram: Solucionar el error 409 Conflict de un bot de Telegram.
Cuánta memoria usa
En nuestra prueba, el bot en marcha usó 67,6 MB de memoria (residente) tras responder a los 8 comandos, con su pool abierto, en un contenedor limitado a 512 MB como nuestro plan de bot más pequeño. En nuestra prueba de alojamiento anterior, grammY solo midió 60 MB (ese bot se detuvo por un token rechazado, antes de procesar ningún mensaje), así que la diferencia, unos 8 MB, cubre mysql2, el pool y los comandos procesados. Instalar grammY y mysql2 con npm desde cero alcanzó un máximo de 140 MB, caché de archivos incluida.
Lo mismo con otra biblioteca
¿Necesitas un servidor para tu bot de Telegram con base de datos incluida? Mira los planes de bots de Telegram.