3. Il codice
Un piccolo bot di note: /note buy milk salva una nota, /notes mostra le tue ultime cinque.
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())
Cosa fa: main() apre il pool, crea la tabella notes se manca e stampa Database ready prima che il bot chieda i messaggi a Telegram. dp.start_polling(bot, pool=pool) è il modo di aiogram di condividere un oggetto: ogni parola chiave in più che passi diventa un dato che aiogram consegna a ogni handler con un parametro di quel nome, quindi note() e notes() ricevono lo stesso pool. Quando il bot si ferma, il blocco finally chiude il pool.
I segnaposto %s vengono riempiti dal driver, quindi una nota come '); DROP TABLE notes; -- viene salvata come testo semplice. Nel nostro test è stata salvata come riga 2 e la tabella è rimasta. Non costruire mai l’SQL con una f-string.
Salva gli ID Telegram come BIGINT
La documentazione della Bot API di Telegram dice che un ID di utente o di chat “può avere più di 32 bit significativi” ma al massimo 52, quindi un intero a 64 bit è sicuro. La sua pagina sugli ID dà gli intervalli: gli ID utente arrivano fino a 1.099.511.627.775, mentre gli ID di gruppi, supergruppi e canali sono negativi. Supergruppi e canali vanno da -1.000.000.000.001 a -1.997.852.516.352 (per esempio -1001234567890). Il tipo INT di MySQL si ferma a 2.147.483.647. Nel nostro test, con la stessa modalità SQL rigorosa che usa il nostro server database, salvare l’ID utente 7.000.000.001 in una colonna INT è fallito. aiomysql ha sollevato:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Una colonna BIGINT lo ha salvato, e -1001234567890 anche. Usa un semplice BIGINT, non BIGINT UNSIGNED, così entrano anche gli ID delle chat.
4. Avvialo
- Carica
bot.py e requirements.txt in Files.
- Premi Avvia e apri Console. All’avvio vengono installati i pacchetti da
requirements.txt, poi compaiono [PlayNHost] Starting your bot, Database ready e Run polling for bot @your_bot. Quest’ultima riga porta lo stato in Panoramica su Online su Telegram.
- In Telegram, invia al tuo bot
/note buy milk, poi /notes. Per vedere le righe, apri la pagina del database e clicca su Apri in phpMyAdmin.
Dal secondo avvio, la console mostra anche Warning: Table 'notes' already exists, preceduto dal percorso del file cursors.py di aiomysql. È la nota di MariaDB per CREATE TABLE IF NOT EXISTS, inoltrata dal driver. Non c’è nessun problema.
Nel nostro test il sostituto di Telegram ha inviato questi comandi e ricevuto queste risposte:
| Inviato | Risposta |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note e 501 caratteri | A note can be up to 500 characters. |
/notes | Le note salvate, dalla più recente |
/notes da un secondo utente | You have no notes yet. |
Tanti messaggi insieme
aiogram gestisce ogni aggiornamento in arrivo come un task a sé, quindi più comandi possono eseguire le loro query nello stesso momento. Nel nostro test, 4 comandi arrivati insieme sono stati gestiti ciascuno in circa 50 ms. maxsize=5 limita il pool a 5 connessioni; una sesta query aspetta che se ne liberi una invece di aprirne un’altra. Dopo il test, il bot teneva aperte 2 connessioni.
La trappola dell’autocommit
aiomysql apre le connessioni con l’autocommit disattivato. Un INSERT resta quindi dentro una transazione aperta. Quando la connessione torna al pool con quella transazione ancora aperta, aiomysql la chiude e MariaDB scarta la riga non confermata. Nel nostro test, una riga inserita in un pool senza autocommit=True è stata vista da 0 altre connessioni. Passa autocommit=True a create_pool, come sopra, oppure chiama await conn.commit() dopo le tue scritture.
Una query fallita non ferma il bot
Nel nostro test abbiamo eliminato la tabella mentre il bot era in esecuzione e abbiamo inviato /notes. aiogram ha registrato l’errore con un traceback che finiva con pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), non ha inviato alcuna risposta a quel comando ed è rimasto in esecuzione: una volta tornata la tabella, il /note successivo è stato salvato. Chi ha inviato il comando non vede nulla, quindi se una query può fallire, intercetta l’errore nell’handler e rispondi con un breve messaggio.
Una notte tranquilla non rompe il pool
Il nostro server database chiude una connessione rimasta inattiva per 8 ore. Un bot che non riceve messaggi per tanto tempo, per esempio di notte, trova chiuse le connessioni del pool. aiomysql lo controlla quando un handler chiede una connessione e ne apre una nuova al posto di quella chiusa. L’abbiamo provato con il limite di inattività abbassato a 5 secondi: dopo 7 secondi di silenzio, il /note successivo è stato salvato senza errori.
I limiti che contano per il tuo pool
- 20 connessioni contemporanee per utente del database. Ogni processo che usa lo stesso login conta. La connessione 21 viene rifiutata con
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 tiene il bot ben al di sotto.
- Una query che dura più di 120 secondi viene fermata. MariaDB la termina con l’errore 1969,
Query execution was interrupted (max_statement_time exceeded). In un nostro test precedente, su un account configurato allo stesso modo, è successo dopo 120,03 secondi. Le query di un bot di solito durano millisecondi, quindi questo colpisce solo una query fuori controllo.
- 500 MB per database. La pagina Database mostra quanto spazio è usato. A 500 MB le scritture vengono fermate finché non elimini delle righe.
- I backup del server non includono il database. Esportalo ogni tanto da phpMyAdmin.
Errori e che cosa significano
Questi sono gli errori che aiomysql ha dato nel nostro test, scritti come terminano un traceback nella console (arrivano da PyMySQL, che aiomysql usa sotto):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): valore sbagliato di DB_USER o DB_PASSWORD. Dopo Reimposta password sulla scheda Connessione, la vecchia password smette subito di funzionare: sostituisci DB_PASSWORD e riavvia il bot.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): host o porta sbagliati, oppure una connessione da fuori PlayNHost. Il database accetta solo connessioni dai server PlayNHost e da phpMyAdmin, quindi è anche ciò che ottieni dal tuo computer.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): troppe connessioni aperte, vedi i limiti sopra. Per esempio una seconda copia del bot, o un pool aperto per ogni messaggio.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): una colonna ID è di tipo INT. Cambiala in BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): la tabella manca. Il codice sopra la crea all’avvio; se l’hai creata a mano, controlla il suo nome.
Una variabile mancante in Ambiente ferma il bot prima che si connetta, con KeyError: 'DB_HOST' o con il nome di quella mancante, perché il codice legge ogni valore con os.environ[...]. Aggiungila, poi riavvia il bot: le variabili si applicano al prossimo avvio.
Un token sbagliato si nota dopo la parte del database. Con il nostro token falso contro il vero Telegram, il bot ha stampato Database ready, poi si è fermato con aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Copia di nuovo il token da @BotFather e incollalo nella pagina Bot, riquadro Telegram. Altri errori di Telegram: Come risolvere l’errore 409 Conflict di un bot Telegram.
Quanta memoria usa
Nel nostro test il bot in esecuzione ha usato 174 MB di memoria (residente) dopo aver risposto agli 8 comandi, con il pool aperto, in un container limitato a 512 MB come il nostro piano bot più piccolo. Gran parte è aiogram stesso: nel nostro precedente test di hosting, aiogram da solo ha misurato 173 MB (quel bot si è fermato per un token rifiutato, prima di gestire un messaggio, quindi i due numeri sono vicini ma il confronto non è esatto). Installare aiogram e aiomysql con pip da zero ha raggiunto un picco di 163 MB, cache dei file inclusa. Resta spazio per il tuo codice sul piano più piccolo. La guida all’hosting confronta le librerie: python-telegram-bot vs aiogram su PlayNHost.
Lo stesso con un’altra libreria
Ti serve un server per il tuo bot Telegram con un database incluso? Scopri i piani bot per Telegram.