3. El código
Un pequeño bot de notas: /note buy milk guarda una nota y /notes muestra tus últimas cinco.
requirements.txt
python-telegram-bot>=22,<23
aiomysql
bot.py
import os
import aiomysql
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
async def post_init(application: Application) -> 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))"
)
application.bot_data["pool"] = pool
print("Database ready")
print(f"Logged in as @{application.bot.username}")
async def post_shutdown(application: Application) -> None:
pool = application.bot_data.get("pool")
if pool is not None:
pool.close()
await pool.wait_closed()
async def note(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
text = " ".join(context.args).strip()
if not text:
await update.effective_message.reply_text("Write your note after the command, for example: /note buy milk")
return
if len(text) > 500:
await update.effective_message.reply_text("A note can be up to 500 characters.")
return
pool = context.bot_data["pool"]
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO notes (user_id, text) VALUES (%s, %s)",
(update.effective_user.id, text),
)
await update.effective_message.reply_text("Saved. Send /notes to see your last five.")
async def notes(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
pool = context.bot_data["pool"]
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",
(update.effective_user.id,),
)
rows = await cur.fetchall()
if not rows:
await update.effective_message.reply_text("You have no notes yet.")
return
await update.effective_message.reply_text("\n".join(f"- {text}" for (text,) in rows))
def main() -> None:
application = (
Application.builder()
.token(os.environ["TELEGRAM_BOT_TOKEN"])
.post_init(post_init)
.post_shutdown(post_shutdown)
.build()
)
application.add_handler(CommandHandler("note", note))
application.add_handler(CommandHandler("notes", notes))
application.run_polling()
if __name__ == "__main__":
main()
Qué hace: run_polling() primero pregunta a Telegram quién es el bot, luego ejecuta post_init y solo después empieza a recibir mensajes. post_init abre el pool, crea la tabla notes si no existe, guarda el pool en application.bot_data["pool"] e imprime Database ready y Logged in as @your_bot. Todos los handlers reciben el mismo diccionario como context.bot_data. Cuando el bot se detiene, run_polling() ejecuta post_shutdown, que cierra el pool.
context.args es el texto que sigue al comando, dividido en palabras, así que el código lo vuelve a unir con espacios simples: los saltos de línea y los espacios dobles dentro de una nota se convierten en un solo espacio. Si necesitas la nota exactamente como se escribió, toma update.effective_message.text y recorta el comando tú mismo.
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 la 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 tiene 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 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 BIGINT a secas, no BIGINT UNSIGNED, para que también quepan los IDs de chat.
4. Ejecútalo
- Sube
bot.py y requirements.txt en Archivos.
- Pulsa Iniciar y abre Consola. El inicio instala los paquetes de
requirements.txt y luego muestra [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 milk y después /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 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 obtuvo 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. |
Una actualización cada vez
Con su configuración por defecto, python-telegram-bot procesa una actualización cada vez: el siguiente comando espera a que el handler anterior termine. En nuestra prueba, 4 comandos que llegaron juntos se respondieron en el orden en que llegaron, y después de la prueba el bot tenía 1 conexión abierta. maxsize=5 sigue importando si activas concurrent_updates en Application.builder(), o si los jobs y los handlers consultan a la vez: el pool nunca abre más de 5 conexiones y una consulta extra espera a que haya una libre.
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 todavía 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 otras conexiones. O pasas autocommit=True a create_pool, como arriba, o llamas a await conn.commit() después de tus escrituras.
Una consulta fallida no detiene el bot
En nuestra prueba borramos la tabla con el bot en marcha y enviamos /notes. python-telegram-bot imprimió No error handlers are registered, logging exception. y un traceback que terminaba en pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), no envió 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 captura el error en el handler, o registra un handler con application.add_error_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 de madrugada, encuentra cerradas las conexiones de su pool. 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 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 login. La conexión 21 se rechaza con
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 mantiene el bot muy por debajo.
- Una consulta que dura 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, ocurrió a los 120,03 segundos. Las consultas de un bot suelen tardar 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. A los 500 MB se detienen las escrituras 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 como terminan un traceback en la consola (vienen de PyMySQL, que aiomysql usa por debajo):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): DB_USER o DB_PASSWORD incorrectos. 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 por 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 iniciar; si la hiciste a mano, revisa su nombre.
Una variable que falta en Entorno detiene el bot antes de conectar, 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 la próxima vez que arranca.
Un token incorrecto detiene el bot antes de la parte de la base de datos, porque post_init solo se ejecuta después de que Telegram haya aceptado el token. Con nuestro token falso contra el Telegram real, el bot nunca imprimió Database ready y se detuvo tras aproximadamente 1 segundo con telegram.error.InvalidToken: The token `...` was rejected by the server. Ese mensaje contiene el token entero, así que no pegues en ningún sitio público la salida de la consola que lo incluya. 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ó 48,5 MB de memoria (residente) tras responder 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, python-telegram-bot solo midió 44 MB (ese bot se detuvo ante un token rechazado, antes de procesar ningún mensaje), así que la diferencia, unos 4,5 MB, cubre aiomysql, el pool y los comandos procesados. Instalar python-telegram-bot y aiomysql con pip desde cero alcanzó un pico de 73 MB, caché de archivos incluida. Las bibliotecas lado a lado: python-telegram-bot vs aiogram en PlayNHost.
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.