3. Il codice
Un piccolo bot di note: /note buy milk salva una nota, /notes mostra le tue ultime cinque.
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);
});
Cosa fa: main() crea la tabella notes se manca e stampa Database ready prima che bot.start() chieda i messaggi a Telegram. Il pool viene creato una sola volta, in cima al file, e ogni comando lo usa: pool.execute() prende in prestito una connessione, esegue la query e la restituisce. ctx.match è il testo dopo il comando: grammY toglie gli spazi davanti e tiene il resto come scritto, a capo compresi. bot.catch() è importante qui: la documentazione di grammY dice che, senza un gestore degli errori, un errore in un handler ferma un bot avviato con bot.start(), e un errore del database è proprio quel tipo di errore.
I segnaposto ? mandano la nota al database separatamente dall'SQL, 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 stringa template.
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 riporta gli intervalli: gli ID utente arrivano fino a 1.099.511.627.775, mentre gli ID di gruppi, supergruppi e canali sono negativi. I supergruppi e i 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 del nostro server database, salvare l'ID utente 7.000.000.001 in una colonna INT è fallito. mysql2 ha dato il codice di errore ER_WARN_DATA_OUT_OF_RANGE (errno 1264) con questo messaggio:
Out of range value for column 'user_id' at row 1
Una colonna BIGINT l'ha salvato, e anche -1001234567890 . Usa BIGINTsemplice, non BIGINT UNSIGNED, così entrano anche gli ID delle chat.
4. Avvialo
- Carica
index.js e package.json in Files. Non caricare node_modules.
- Premi Avvia e apri Console. All'avvio vengono installati i pacchetti di
package.json, 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.
Nel nostro test il sostituto di Telegram ha inviato questi comandi e ha 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, le più recenti per prime |
/notes da un secondo utente | You have no notes yet. |
Molti messaggi insieme
La documentazione di grammY dice che bot.start() elabora tutti gli aggiornamenti in sequenza: un comando finisce prima che inizi il successivo. Nel nostro test il bot teneva 1 connessione aperta dopo aver risposto agli 8 comandi. connectionLimit: 5 conta quando il tuo codice esegue query in parallelo, o se passi a grammY runner, che elabora gli aggiornamenti in modo concorrente: il pool non apre mai più di 5 connessioni, e una query in più aspetta che ce ne sia una libera.
Una query fallita non ferma il bot
Nel nostro test abbiamo eliminato la tabella mentre il bot girava e abbiamo inviato /notes. bot.catch() ha stampato Error while handling update 500001: Error: Table '...notes' doesn't exist con code: 'ER_NO_SUCH_TABLE', il bot non ha mandato alcuna risposta per quel comando ed è rimasto attivo: quando la tabella è tornata, il /note successivo è stato salvato. Chi ha inviato il comando non vede nulla, quindi se una query può fallire, mettila in try/catch 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 così a lungo, per esempio di notte, trova chiuse le connessioni del suo pool. Il pool di mysql2 se la cava da solo. L'abbiamo provato con il limite di inattività abbassato a 5 secondi: dopo 7 secondi di silenzio, il /note successivo è stato salvato senza errori.
Limiti che contano per il tuo pool
- 20 connessioni alla volta per utente del database. Ogni processo che usa lo stesso login conta. La connessione 21 viene rifiutata con
ER_USER_LIMIT_REACHED. Nel nostro test un pool con connectionLimit: 30 è fallito con quell'errore quando 30 query giravano insieme. Un pool da 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 riguarda 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 ogni tanto.
Gli errori e cosa significano
Questi sono i codici di errore che mysql2 ha dato nel nostro test:
ER_ACCESS_DENIED_ERROR ... Access denied for user: DB_USER o DB_PASSWORDsbagliato. Dopo Reimposta password nella scheda Connessione la vecchia password smette di funzionare subito: sostituisci DB_PASSWORD e riavvia il bot.
ECONNREFUSED: nessuno ha accettato la connessione a quell'host e porta. Controlla DB_HOST e DB_PORT. Anche un DB_HOST mancante finisce qui, perché mysql2 si collega allora a localhost, dove non gira alcun database: la variabile manca in Ambiente, oppure il bot non è stato riavviato dopo averla aggiunta.
ENOTFOUND: DB_HOST è scritto male, quindi il nome non esiste.
ETIMEDOUT: nessuno ha risposto in tempo. Qualunque sia il messaggio, una connessione dal tuo computer fallisce: il database accetta solo connessioni dai server PlayNHost e da phpMyAdmin.
ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): troppe connessioni aperte, vedi i limiti sopra. Per esempio una seconda copia del bot, o un pool creato per ogni messaggio.
ER_WARN_DATA_OUT_OF_RANGE (errno 1264, Out of range value for column 'user_id' at row 1): una colonna ID è INT. Cambiala in BIGINT.
ER_NO_SUCH_TABLE (errno 1146): la tabella manca. Il codice qui sopra la crea all'avvio; se l'hai creata a mano, controlla il nome.
Un token sbagliato si nota dopo la parte del database. Con il nostro token finto contro il vero Telegram, il bot ha stampato Database ready, poi si è fermato dopo circa un quarto di secondo con GrammyError: Call to 'deleteWebhook' failed! (401: 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 67,6 MB di memoria (residente) dopo aver risposto agli 8 comandi, con il suo pool aperto, in un container limitato a 512 MB come il nostro piano bot più piccolo. Nel nostro test di hosting precedente, grammY da solo misurava 60 MB (quel bot si è fermato per un token rifiutato, prima di gestire qualsiasi messaggio), quindi la differenza, circa 8 MB, copre mysql2, il pool e i comandi gestiti. Installare grammY e mysql2 con npm da zero ha raggiunto un picco di 140 MB, cache dei file inclusa.
La stessa cosa con un'altra libreria
Ti serve un server per il tuo bot Telegram con un database incluso? Guarda i piani per bot Telegram.