3. El código
Un pequeño bot de notas: /note buy milk guarda una nota, /notes muestra tus últimas cinco.
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())
Qué hace: main() abre el pool, crea la tabla notes si no existe e imprime Database ready antes de que el bot pida mensajes a Telegram. dp.start_polling(bot, pool=pool) es la forma propia de aiogram de compartir un objeto: cada argumento extra que pasas se convierte en datos que aiogram entrega a cualquier handler con un parámetro de ese nombre, así que note() y notes() reciben el mismo pool. Cuando el bot se detiene, el bloque finally cierra el pool.
Los marcadores %s los rellena el driver, así que una nota como '); DROP TABLE notes; -- se guarda como texto normal. En nuestra prueba se guardó como fila 2 y la tabla siguió ahí. No construyas nunca el SQL con un f-string.
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 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 tipo INT de MySQL llega solo hasta 2.147.483.647. En nuestra prueba, con el mismo modo SQL estricto que usa nuestro servidor de base de datos, guardar el ID de usuario 7.000.000.001 en una columna INT falló. aiomysql lanzó:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Una columna BIGINT lo guardó, y -1001234567890 también. Usa un BIGINT normal, no BIGINT UNSIGNED, para que quepan también los IDs de chat.
4. Ejecútalo
- Sube
bot.py y requirements.txt en Archivos.
- Pulsa Iniciar y abre Consola. Al iniciar se instalan los paquetes de
requirements.txt, y luego aparecen [PlayNHost] Starting your bot, Database ready y Run polling for bot @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 milk, y luego /notes. Para ver las filas, abre la página de la base de datos y haz clic en Abrir en phpMyAdmin.
A partir del segundo inicio, la consola también muestra Warning: Table 'notes' already exists, con la ruta del archivo cursors.py de aiomysql delante. Es el aviso de MariaDB para CREATE TABLE IF NOT EXISTS, transmitido por el driver. No pasa nada.
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, la más reciente primero |
/notes de un segundo usuario | You have no notes yet. |
Muchos mensajes a la vez
aiogram trata cada actualización entrante como su propia tarea, así que varios comandos pueden ejecutar sus consultas a la vez. En nuestra prueba, 4 comandos que llegaron juntos se atendieron cada uno en unos 50 ms. maxsize=5 limita el pool a 5 conexiones; una sexta consulta espera a que haya una libre en lugar de abrir otra. Tras la prueba, el bot mantenía 2 conexiones abiertas.
La trampa del autocommit
aiomysql abre las conexiones con autocommit desactivado. Un INSERT se queda entonces dentro de una transacción abierta. Cuando la conexión vuelve al pool con esa transacción aún abierta, aiomysql la cierra y MariaDB descarta la fila sin confirmar. En nuestra prueba, una fila insertada en un pool sin autocommit=True fue vista por 0 conexiones más. O bien pasa autocommit=True a create_pool, como arriba, o llama a await conn.commit() después de tus escrituras.
Una consulta fallida no detiene el bot
En nuestra prueba borramos la tabla mientras el bot funcionaba y enviamos /notes. aiogram registró el error con un traceback que acababa en pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), no envió ninguna respuesta a ese comando y siguió funcionando: cuando la tabla volvió, el siguiente /note se guardó. Quien envía el comando no ve nada, así que, si una consulta puede fallar, captura el error en el handler y responde con un mensaje corto.
Una noche tranquila no rompe el pool
Nuestro servidor de base de datos cierra una conexión que lleva 8 horas inactiva. Un bot que no recibe mensajes durante tanto tiempo, por ejemplo por la noche, se encuentra con sus conexiones del pool cerradas. aiomysql lo comprueba cuando un handler pide una conexión y abre una nueva en lugar de la cerrada. Lo probamos con el límite de inactividad bajado a 5 segundos: tras 7 segundos de silencio, el siguiente /note se guardó sin ningún error.
Límites que importan para tu pool
- 20 conexiones a la vez por usuario de base de datos. Cada proceso que use el mismo login cuenta. La conexión 21 se rechaza con
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 mantiene al bot muy por debajo.
- Una consulta que tarda 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, en una cuenta configurada igual, eso ocurrió a los 120,03 segundos. Las consultas de un bot suelen tardar milisegundos, así que 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, las escrituras se detienen hasta que borres filas.
- Las copias de seguridad del servidor no incluyen la base de datos. Expórtala desde phpMyAdmin de vez en cuando.
Errores y qué significan
Estos son los errores que dio aiomysql en nuestra prueba, escritos tal como terminan un traceback en la consola (vienen de PyMySQL, que aiomysql usa por debajo):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): valor incorrecto de DB_USER o DB_PASSWORD. Tras Restablecer contraseña en la tarjeta Conexión, la contraseña antigua deja de funcionar al instante: sustituye DB_PASSWORD y reinicia el bot.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): host o puerto incorrectos, o una conexión desde fuera de PlayNHost. La base de datos solo acepta conexiones desde servidores de PlayNHost y phpMyAdmin, así que esto es también lo que obtienes desde tu propio ordenador.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): demasiadas conexiones abiertas, mira los límites de arriba. Por ejemplo, una segunda copia del bot o un pool abierto para cada mensaje.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): una columna de ID es INT. Cámbiala a BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): falta la tabla. El código de arriba la crea al arrancar; si la hiciste a mano, revisa su nombre.
Una variable que falta en Entorno detiene el bot antes de que se conecte, con KeyError: 'DB_HOST' o el nombre de la que falte, porque el código lee cada valor con os.environ[...]. Añádela y reinicia el bot: las variables se aplican en el siguiente inicio.
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 ready, y luego se detuvo con aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Copia de nuevo el token desde @BotFather y pégalo en la página Bot, sección 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ó 174 MB de memoria (residente) después de responder a los 8 comandos, con su pool abierto, en un contenedor limitado a 512 MB como nuestro plan de bots más pequeño. Casi todo es aiogram en sí: en nuestra prueba de hosting anterior, aiogram solo midió 173 MB (ese bot se detuvo por un token rechazado, antes de procesar ningún mensaje, así que las dos cifras son parecidas pero no una comparación exacta). Instalar aiogram y aiomysql con pip desde cero llegó a un pico de 163 MB, caché de archivos incluida. Eso deja margen para tu propio código en el plan más pequeño. La guía de hosting compara las librerías: python-telegram-bot vs aiogram en PlayNHost.
Lo mismo con otra librería
¿Necesitas un servidor para tu bot de Telegram con base de datos incluida? Consulta los planes de bots de Telegram.