3. Il codice
Un piccolo bot per note: /note buy milk salva una nota, /notes mostra le tue ultime cinque.
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()
Cosa fa: run_polling() chiede prima a Telegram chi è il bot, poi esegue post_init e solo allora inizia a ricevere i messaggi. post_init apre il pool, crea la tabella notes se manca, mette il pool in application.bot_data["pool"] e stampa Database ready e Logged in as @your_bot. Ogni handler riceve lo stesso dizionario come context.bot_data. Quando il bot si ferma, run_polling() esegue post_shutdown, che chiude il pool.
context.args è il testo dopo il comando, diviso in parole, quindi il codice lo riunisce con spazi singoli: gli a capo e i doppi spazi dentro una nota diventano uno spazio solo. Se ti serve la nota esattamente come è stata scritta, prendi update.effective_message.text e togli il comando da solo.
I segnaposto %s vengono riempiti dal driver, quindi una nota come '); DROP TABLE notes; -- viene salvata come semplice testo. Nel nostro test è stata salvata come riga 2 e la tabella è rimasta. Non costruire mai l'SQL con una f-string.
Salva gli ID di Telegram come BIGINT
La documentazione della Bot API di Telegram dice che un ID utente o chat «può avere più di 32 bit significativi» ma ne ha al massimo 52, quindi un intero a 64 bit è sicuro. La sua pagina sugli ID indica 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 (ad esempio -1001234567890). L'INT di MySQL si ferma a 2.147.483.647. Nel nostro test, con la stessa modalità SQL rigorosa usata dal 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 anche -1001234567890. Usa BIGINT semplice, 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 Logged in as @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 in poi, la console mostra anche Warning: Table 'notes' already exists, preceduto dal percorso del cursors.py di aiomysql. È la nota di MariaDB per CREATE TABLE IF NOT EXISTS, inoltrata dal driver. Non c'è niente che non va.
Nel nostro test il sostituto di Telegram ha inviato questi comandi e ha ottenuto 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, le più recenti per prime |
/notes da un secondo utente | You have no notes yet. |
Un aggiornamento alla volta
Con le impostazioni predefinite, python-telegram-bot gestisce un aggiornamento alla volta: il comando successivo aspetta che l'handler precedente abbia finito. Nel nostro test, 4 comandi arrivati insieme hanno ricevuto risposta nell'ordine di arrivo e, dopo il test, il bot teneva 1 connessione aperta. maxsize=5 conta comunque se attivi concurrent_updates in Application.builder(), o se job e handler interrogano il database nello stesso momento: il pool non apre mai più di 5 connessioni e una query in più aspetta che se ne liberi una.
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 su 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 inviato /notes. python-telegram-bot ha stampato No error handlers are registered, logging exception. e 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: appena la tabella è tornata, il /note successivo è stato salvato. Chi ha inviato il comando non vede nulla, quindi intercetta l'errore nell'handler, oppure registra un gestore con application.add_error_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 così tanto tempo, per esempio di notte, trova chiuse le connessioni del suo pool. aiomysql lo verifica quando un handler chiede una connessione e ne apre una nuova al posto di quella chiusa. L'abbiamo testato con il limite di inattività abbassato a 5 secondi: dopo 7 secondi di quiete, il /note successivo è stato salvato senza errori.
Limiti che contano per il tuo pool
- 20 connessioni contemporanee per utente del database. Conta ogni processo che usa lo stesso login. 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 richiedono millisecondi, quindi questo colpisce solo una query fuori controllo.
- 500 MB per database. La pagina Database mostra quanto è usato. A 500 MB le scritture vengono fermate finché non elimini delle righe.
- I backup del server non includono il database. Esportalo da phpMyAdmin di tanto in tanto.
Gli errori e 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 ..."): DB_USER o DB_PASSWORD sbagliato. Dopo Reimposta password nella 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, o 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 è INT. Cambiala in BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): la tabella manca. Il codice qui 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 il nome di quella mancante, perché il codice legge ogni valore con os.environ[...]. Aggiungila, poi riavvia il bot: le variabili si applicano al successivo avvio.
Un token sbagliato ferma il bot prima della parte database, perché post_init viene eseguito solo dopo che Telegram ha accettato il token. Con il nostro token finto contro il vero Telegram, il bot non ha mai stampato Database ready e si è fermato dopo circa 1 secondo con telegram.error.InvalidToken: The token `...` was rejected by the server. Quel messaggio contiene l'intero token, quindi non incollare da nessuna parte in pubblico l'output della console che lo contiene. 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 48,5 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. Nel nostro precedente test di hosting, python-telegram-bot da solo misurava 44 MB (quel bot si è fermato per un token rifiutato, prima di gestire qualsiasi messaggio), quindi la differenza, circa 4,5 MB, copre aiomysql, il pool e i comandi gestiti. Installare python-telegram-bot e aiomysql con pip da zero ha raggiunto un picco di 73 MB, cache dei file inclusa. Le librerie a confronto: 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 per bot Telegram.