3. Kod
Küçük bir not botu: /note buy milk bir not kaydeder, /notes son beş notunuzu gösterir.
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);
});
Ne yapar: main() eksikse notes tablosunu oluşturur ve Database ready yazdırır; bu, bot.start() Telegram'dan mesaj istemeden önce gerçekleşir. Havuz dosyanın en üstünde bir kez oluşturulur ve her komut onu kullanır: pool.execute() bir bağlantı ödünç alır, sorguyu çalıştırır ve bağlantıyı geri verir. ctx.match komuttan sonraki metindir: grammY önündeki boşlukları kaldırır, geri kalanı satır sonları dahil yazıldığı gibi tutar. bot.catch() burada önemlidir: grammY belgelerine göre, hata işleyicisi yoksa bir işleyicideki hata bot.start() ile başlatılmış botu durdurur ve bir veritabanı hatası tam olarak bu tür bir hatadır.
? yer tutucuları notu SQL'den ayrı olarak veritabanına gönderir; bu yüzden '); DROP TABLE notes; -- gibi bir not düz metin olarak saklanır. Testimizde 2. satır olarak kaydedildi ve tablo yerinde kaldı. SQL'i asla şablon dizgesiyle oluşturmayın.
Telegram kimliklerini BIGINT olarak saklayın
Telegram'ın Bot API belgeleri, bir kullanıcı veya sohbet kimliğinin “32 bitten fazla anlamlı bit içerebileceğini” ama en fazla 52 bit olduğunu söyler, dolayısıyla 64 bitlik bir tam sayı güvenlidir. Kimlikler sayfası aralıkları verir: kullanıcı kimlikleri 1.099.511.627.775'e kadar çıkar, grup, süper grup ve kanal kimlikleri ise negatiftir. Süper gruplar ve kanallar -1.000.000.000.001 ile -1.997.852.516.352 arasında yer alır (örneğin -1001234567890). MySQL'de INT 2.147.483.647'de durur. Testimizde, veritabanı sunucumuzun kullandığı aynı katı SQL kipiyle, 7.000.000.001 kullanıcı kimliğini bir INT sütununa kaydetmek başarısız oldu. mysql2 şu hata kodunu verdi: ER_WARN_DATA_OUT_OF_RANGE (errno 1264), ileti şuydu:
Out of range value for column 'user_id' at row 1
Bir BIGINT sütunu bunu sakladı, -1001234567890 değerini de. Düz BIGINT kullanın, BIGINT UNSIGNED değil; böylece sohbet kimlikleri de sığar.
4. Çalıştırın
index.js ve package.json dosyalarını Dosyalar bölümünden yükleyin. node_modules klasörünü yüklemeyin.
- Başlat düğmesine basın ve Konsol bölümünü açın. Başlatınca
package.json içindeki paketler kurulur, ardından [PlayNHost] Starting your bot, Database ready ve Logged in as @your_bot satırları görünür. Bu son satır, Genel bakış bölümündeki durumu Telegram’da çevrimiçi yapar.
- Telegram'da botunuza
/note buy milk ve ardından /notes gönderin. Satırlara bakmak için veritabanının sayfasını açın ve phpMyAdmin’de aç düğmesine tıklayın.
Testimizde Telegram'ın yerine geçen vekil şu komutları gönderdi ve şu yanıtları aldı:
| Gönderilen | Yanıt |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note ve 501 karakter | A note can be up to 500 characters. |
/notes | Kaydedilen notlar, en yeniler başta |
/notes ikinci bir kullanıcıdan | You have no notes yet. |
Aynı anda çok mesaj
grammY belgeleri bot.start() çağrısının tüm güncellemeleri sırayla işlediğini söyler: bir komut, sonrakinden önce biter. Testimizde bot, 8 komutu yanıtladıktan sonra 1 açık bağlantı tuttu. connectionLimit: 5 ayarı, kodunuz sorguları paralel çalıştırdığında ya da güncellemeleri eşzamanlı işleyen grammY runner'a geçtiğinizde önem kazanır: havuz hiçbir zaman 5'ten fazla bağlantı açmaz, fazladan bir sorgu boş bir bağlantıyı bekler.
Başarısız bir sorgu botu durdurmaz
Testimizde bot çalışırken tabloyu sildik ve /notes gönderdik. bot.catch() şunu yazdırdı: Error while handling update 500001: Error: Table '...notes' doesn't exist; ayrıca code: 'ER_NO_SUCH_TABLE' vardı. Bot o komuta yanıt göndermedi ve çalışmaya devam etti: tablo geri gelince sonraki /note kaydedildi. Komutu gönderen kişi hiçbir şey görmez; bu yüzden bir sorgu başarısız olabiliyorsa, işleyicide onu try/catch içine alın ve kısa bir mesajla yanıt verin.
Sessiz bir gece havuzu bozmaz
Veritabanı sunucumuz 8 saat boşta kalan bir bağlantıyı kapatır. O kadar uzun süre mesaj almayan bir bot, örneğin gece boyunca, havuzdaki bağlantılarını kapalı bulur. mysql2'nin havuzu bunu kendi başına halleder. Boşta kalma sınırını 5 saniyeye indirerek denedik: 7 sessiz saniye sonra sonraki /note hatasız kaydedildi.
Havuzunuz için önemli sınırlar
- Veritabanı kullanıcısı başına aynı anda 20 bağlantı. Aynı girişi kullanan her işlem sayılır. 21. bağlantı
ER_USER_LIMIT_REACHED ile reddedilir. Testimizde connectionLimit: 30 olan bir havuz, 30 sorgu aynı anda çalışınca bu hatayla başarısız oldu. 5'lik bir havuz botu sınırın çok altında tutar.
- 120 saniyeden uzun süren bir sorgu durdurulur. MariaDB bunu 1969 hatasıyla sonlandırır:
Query execution was interrupted (max_statement_time exceeded). Daha önceki bir testimizde, aynı şekilde kurulmuş bir hesapta bu 120,03 saniye sonra oldu. Bir botun sorguları normalde milisaniyeler sürer, bu yüzden bu yalnızca kontrolden çıkmış bir sorguyu etkiler.
- Veritabanı başına 500 MB. Veritabanları sayfası ne kadarının kullanıldığını gösterir. 500 MB'a ulaşınca, siz satır silene kadar yazma işlemleri durdurulur.
- Sunucu yedekleri veritabanını içermez. Onu zaman zaman phpMyAdmin'den dışa aktarın.
Hatalar ve ne anlama geldikleri
mysql2'nin testimizde verdiği hata kodları şunlar:
ER_ACCESS_DENIED_ERROR ... Access denied for user: DB_USER veya DB_PASSWORD yanlış. Şifreyi sıfırla düğmesine (Bağlantı kartı) bastıktan sonra eski şifre hemen geçersiz olur: DB_PASSWORD değerini değiştirin ve botu yeniden başlatın.
ECONNREFUSED: o host ve portta hiçbir şey bağlantıyı kabul etmedi. DB_HOST ve DB_PORT değerlerini kontrol edin. DB_HOST eksikse de buraya düşersiniz, çünkü mysql2 o zaman localhost adresine bağlanır, orada veritabanı çalışmaz: değişken Ortam bölümünde eksiktir ya da eklendikten sonra bot yeniden başlatılmamıştır.
ENOTFOUND: DB_HOST yanlış yazılmış, bu yüzden böyle bir ad yok.
ETIMEDOUT: zamanında hiçbir yanıt gelmedi. İleti ne olursa olsun, kendi bilgisayarınızdan bağlantı başarısız olur: veritabanı yalnızca PlayNHost sunucularından ve phpMyAdmin'den gelen bağlantıları kabul eder.
ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): çok fazla bağlantı açık, yukarıdaki sınırlara bakın. Örneğin botun ikinci bir kopyası ya da her mesaj için oluşturulan bir havuz.
ER_WARN_DATA_OUT_OF_RANGE (errno 1264, Out of range value for column 'user_id' at row 1): bir kimlik sütunu INT türünde. Bunu BIGINT yapın.
ER_NO_SUCH_TABLE (errno 1146): tablo eksik. Yukarıdaki kod onu başlangıçta oluşturur; elle yaptıysanız adını kontrol edin.
Yanlış bir token, veritabanı bölümünden sonra ortaya çıkar. Sahte tokenimizle gerçek Telegram'a bağlanınca bot Database ready yazdırdı, ardından yaklaşık çeyrek saniye sonra GrammyError: Call to 'deleteWebhook' failed! (401: Unauthorized) hatasıyla durdu. Token'ı @BotFather'dan yeniden kopyalayın ve Bot sayfasında Telegram bölmesine yapıştırın. Daha fazla Telegram hatası: Telegram botu 409 Conflict hatasını düzeltin.
Ne kadar bellek kullanır
Testimizde çalışan bot, 8 komutu yanıtladıktan sonra havuzu açıkken, en küçük bot planımız gibi 512 MB ile sınırlı bir konteynerde 67,6 MB bellek (resident) kullandı. Daha önceki barındırma testimizde grammY tek başına 60 MB ölçüldü (o bot reddedilen bir token'da durdu, hiçbir mesajı işlemeden), bu yüzden yaklaşık 8 MB'lık fark mysql2'yi, havuzu ve işlenen komutları kapsar. grammY ve mysql2'yi npm ile sıfırdan kurmak, dosya önbelleği dahil 140 MB'ta zirve yaptı.
Başka bir kütüphaneyle aynı şey
Telegram botunuz için veritabanı dahil bir sunucuya mı ihtiyacınız var? Telegram bot planlarına göz atın.