3. Der Code
Ein kleiner Notiz-Bot: /note buy milk speichert eine Notiz, /notes zeigt deine letzten fünf.
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);
});
Was er tut: main() legt die Tabelle notes an, falls sie fehlt, und gibt Database ready aus, bevor bot.start() bei Telegram Nachrichten abholt. Der Pool wird einmal am Anfang der Datei erstellt und jeder Befehl nutzt ihn: pool.execute() leiht sich eine Verbindung, führt die Abfrage aus und gibt die Verbindung zurück. ctx.match ist der Text nach dem Befehl: grammY entfernt die Leerzeichen davor und behält den Rest wie getippt, Zeilenumbrüche eingeschlossen. bot.catch() ist hier wichtig: Laut der Dokumentation von grammY stoppt ein Fehler in einem Handler einen mit bot.start()gestarteten Bot, wenn es keinen Fehlerhandler gibt, und ein Datenbankfehler ist genau so ein Fehler.
Die ? -Platzhalter schicken die Notiz getrennt vom SQL an die Datenbank, deshalb wird eine Notiz wie '); DROP TABLE notes; -- als normaler Text gespeichert. In unserem Test wurde sie als Zeile 2 gespeichert und die Tabelle blieb erhalten. Baue das SQL nie mit einem Template-String zusammen.
Telegram-IDs als BIGINT speichern
In der Bot API-Dokumentation von Telegram steht, eine Nutzer- oder Chat-ID „kann mehr als 32 signifikante Bits haben“, aber höchstens 52, deshalb ist eine 64-Bit-Ganzzahl sicher. Die Seite zu den IDs nennt die Bereiche: Nutzer-IDs gehen bis 1.099.511.627.775, Gruppen-, Supergruppen- und Kanal-IDs sind negativ. Supergruppen und Kanäle reichen von -1.000.000.000.001 bis -1.997.852.516.352 (zum Beispiel -1001234567890). Das INT von MySQL endet bei 2.147.483.647. In unserem Test, mit demselben strikten SQL-Modus wie auf unserem Datenbankserver, schlug das Speichern der Nutzer-ID 7.000.000.001 in einer INT -Spalte fehl. mysql2 lieferte den Fehlercode ER_WARN_DATA_OUT_OF_RANGE (errno 1264) mit dieser Meldung:
Out of range value for column 'user_id' at row 1
Eine BIGINT -Spalte hat sie gespeichert, und -1001234567890 auch. Nimm einfach BIGINT, nicht BIGINT UNSIGNED, damit auch Chat-IDs hineinpassen.
4. Starten
- Lade
index.js und package.json unter Dateienhoch. Lade nicht node_moduleshoch.
- Drücke Starten und öffne Konsole. Beim Start werden die Pakete aus
package.jsoninstalliert, dann erscheinen [PlayNHost] Starting your bot, Database ready und Logged in as @your_bot. Diese letzte Zeile schaltet den Status unter Übersicht auf Online in Telegram.
- Schick deinem Bot in Telegram
/note buy milk, dann /notes. Um dir die Zeilen anzusehen, öffne die Seite der Datenbank und klicke auf In phpMyAdmin öffnen.
In unserem Test hat der Telegram-Ersatz diese Befehle gesendet und diese Antworten bekommen:
| Gesendet | Antwort |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note und 501 Zeichen | A note can be up to 500 characters. |
/notes | Die gespeicherten Notizen, neueste zuerst |
/notes von einem zweiten Nutzer | You have no notes yet. |
Viele Nachrichten gleichzeitig
Laut der Dokumentation von grammY verarbeitet bot.start() alle Updates nacheinander: Ein Befehl ist fertig, bevor der nächste beginnt. In unserem Test hielt der Bot nach den Antworten auf die 8 Befehle 1 offene Verbindung. connectionLimit: 5 wird wichtig, sobald dein Code Abfragen parallel ausführt oder du auf grammY runner umsteigst, das Updates gleichzeitig verarbeitet: Der Pool öffnet nie mehr als 5 Verbindungen, und eine weitere Abfrage wartet auf eine freie.
Eine fehlgeschlagene Abfrage stoppt den Bot nicht
In unserem Test haben wir die Tabelle gelöscht, während der Bot lief, und /notesgesendet. bot.catch() gab Error while handling update 500001: Error: Table '...notes' doesn't exist aus, mit code: 'ER_NO_SUCH_TABLE', der Bot schickte für diesen Befehl keine Antwort und lief weiter: Sobald die Tabelle wieder da war, wurde das nächste /note gespeichert. Wer den Befehl geschickt hat, sieht nichts. Wenn eine Abfrage fehlschlagen kann, pack sie im Handler in try/catch und antworte mit einer kurzen Nachricht.
Eine ruhige Nacht bringt den Pool nicht kaputt
Unser Datenbankserver schließt eine Verbindung, die 8 Stunden lang ungenutzt war. Ein Bot, der so lange keine Nachrichten bekommt, zum Beispiel über Nacht, findet seine gepoolten Verbindungen geschlossen vor. Der Pool von mysql2 regelt das von selbst. Wir haben es mit auf 5 Sekunden gesenktem Leerlauflimit getestet: Nach 7 ruhigen Sekunden wurde das nächste /note ohne Fehler gespeichert.
Limits, die für deinen Pool wichtig sind
- 20 Verbindungen gleichzeitig pro Datenbankbenutzer. Jeder Prozess mit demselben Login zählt mit. Verbindung 21 wird mit
ER_USER_LIMIT_REACHEDabgelehnt. In unserem Test schlug ein Pool mit connectionLimit: 30 mit diesem Fehler fehl, als 30 Abfragen gleichzeitig liefen. Ein Pool mit 5 hält den Bot weit darunter.
- Eine Abfrage, die länger als 120 Sekunden läuft, wird gestoppt. MariaDB beendet sie mit Fehler 1969,
Query execution was interrupted (max_statement_time exceeded). In einem früheren Test von uns, auf einem genauso eingerichteten Konto, kam das nach 120,03 Sekunden. Die Abfragen eines Bots dauern normalerweise Millisekunden, deshalb trifft das nur eine außer Kontrolle geratene Abfrage.
- 500 MB pro Datenbank. Die Seite Datenbanken zeigt, wie viel belegt ist. Bei 500 MB werden Schreibzugriffe gestoppt, bis du Zeilen löschst.
- Server-Backups enthalten die Datenbank nicht. Exportiere sie ab und zu aus phpMyAdmin.
Fehler und was sie bedeuten
Das sind die Fehlercodes, die mysql2 in unserem Test geliefert hat:
ER_ACCESS_DENIED_ERROR ... Access denied for user: falscher DB_USER oder DB_PASSWORD. Nach Passwort zurücksetzen auf der Karte Verbindung funktioniert das alte Passwort sofort nicht mehr: Ersetze DB_PASSWORD und starte den Bot neu.
ECONNREFUSED: Unter diesem Host und Port hat nichts die Verbindung angenommen. Prüfe DB_HOST und DB_PORT. Auch ein fehlendes DB_HOST landet hier, weil mysql2 sich dann mit localhostverbindet, wo keine Datenbank läuft: Die Variable fehlt unter Umgebung, oder der Bot wurde nach dem Hinzufügen nicht neu gestartet.
ENOTFOUND: DB_HOST ist falsch geschrieben, der Name existiert also nicht.
ETIMEDOUT: Nichts hat rechtzeitig geantwortet. Egal, was die Meldung sagt: Eine Verbindung von deinem eigenen Computer schlägt fehl, denn die Datenbank akzeptiert nur Verbindungen von PlayNHost-Servern und phpMyAdmin.
ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): zu viele offene Verbindungen, siehe die Limits oben. Zum Beispiel eine zweite Kopie des Bots oder ein Pool, der für jede Nachricht neu erstellt wird.
ER_WARN_DATA_OUT_OF_RANGE (errno 1264, Out of range value for column 'user_id' at row 1): Eine ID-Spalte ist INT. Ändere sie auf BIGINT.
ER_NO_SUCH_TABLE (errno 1146): Die Tabelle fehlt. Der Code oben legt sie beim Start an; wenn du sie von Hand erstellt hast, prüfe ihren Namen.
Ein falsches Token fällt erst nach dem Datenbankteil auf. Mit unserem falschen Token gegen das echte Telegram gab der Bot Database readyaus und stoppte dann nach etwa einer Viertelsekunde mit GrammyError: Call to 'deleteWebhook' failed! (401: Unauthorized). Kopiere das Token erneut von @BotFather und füge es auf der Seite Bot im Bereich Telegramein. Mehr Telegram-Fehler: Telegram-Bot-Fehler 409 Conflict beheben.
Wie viel Speicher er braucht
In unserem Test belegte der laufende Bot nach den Antworten auf die 8 Befehle 67,6 MB Speicher (resident), mit offenem Pool, in einem auf 512 MB begrenzten Container wie bei unserem kleinsten Bot-Tarif. In unserem früheren Hosting-Test maß grammY allein 60 MB (dieser Bot stoppte bei einem abgelehnten Token, bevor er eine Nachricht verarbeitet hatte), der Unterschied von etwa 8 MB deckt also mysql2, den Pool und die verarbeiteten Befehle ab. Die Installation von grammY und mysql2 mit npm von null auf erreichte 140 MB Spitze, Dateicache eingeschlossen.
Dasselbe mit einer anderen Bibliothek
Brauchst du einen Server für deinen Telegram-Bot mit Datenbank? Die Telegram-Bot-Tarife ansehen.