3. Kod
Küçük bir not botu: /note buy milk bir not kaydeder, /notes son beş notunuzu gösterir.
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())
Ne yapar: main() havuzu açar, notes tablosu yoksa oluşturur ve bot Telegram’dan mesaj istemeden önce Database ready yazdırır. dp.start_polling(bot, pool=pool) aiogram’ın kendi nesne paylaşma yöntemidir: ilettiğiniz her ek anahtar sözcük, o adda parametresi olan her handler’a aiogram’ın verdiği veri olur, bu yüzden note() ve notes() aynı havuzu alır. Bot durduğunda finally bloğu havuzu kapatır.
%s yer tutucularını sürücü doldurur, 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 f-string ile oluşturmayın.
Telegram kimliklerini BIGINT olarak saklayın
Telegram’ın Bot API belgeleri, bir kullanıcı veya sohbet kimliğinin “32’den fazla anlamlı bite sahip olabileceğini” ama en fazla 52 bit olduğunu söylüyor; bu yüzden 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üpergrup ve kanal kimlikleri ise negatiftir. Süpergruplar ve kanallar -1.000.000.000.001 ile -1.997.852.516.352 arasındadır (örneğin -1001234567890). MySQL’in INT türü 2.147.483.647’de durur. Bizim testimizde, veritabanı sunucumuzun kullandığı aynı katı SQL modunda, 7.000.000.001 kullanıcı kimliğini bir INT sütununa kaydetmek başarısız oldu. aiomysql şunu verdi:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Bir BIGINT sütunu onu sakladı, -1001234567890’ı da. Düz BIGINT kullanın, BIGINT UNSIGNED değil; böylece sohbet kimlikleri de sığar.
4. Çalıştırın
bot.py ve requirements.txt dosyalarını Dosyalar bölümünden yükleyin.
- Başlat düğmesine basın ve Konsol bölümünü açın. Başlatma,
requirements.txt içindeki paketleri kurar, ardından [PlayNHost] Starting your bot, Database ready ve Run polling for bot @your_bot satırlarını gösterir. Bu son satır, Genel bakış altındaki durumu Telegram’da çevrimiçi olarak değiştirir.
- Telegram’da botunuza
/note buy milk gönderin, sonra /notes. Satırlara bakmak için veritabanının sayfasını açın ve phpMyAdmin’de aç düğmesine tıklayın.
İkinci başlatmadan itibaren konsol ayrıca Warning: Table 'notes' already exists satırını da gösterir; önünde aiomysql’in cursors.py dosyasının yolu bulunur. Bu, MariaDB’nin CREATE TABLE IF NOT EXISTS için verdiği ve sürücünün ilettiği nottur. Sorun yok.
Testimizde Telegram taklidi ş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 yeniden başlayarak |
/notes ikinci bir kullanıcıdan | You have no notes yet. |
Aynı anda çok mesaj
aiogram her gelen güncellemeyi ayrı bir görev olarak işler, bu yüzden birkaç komut sorgularını aynı anda çalıştırabilir. Testimizde birlikte gelen 4 komutun her biri yaklaşık 50 ms içinde işlendi. maxsize=5 havuzu 5 bağlantıyla sınırlar; altıncı sorgu yenisini açmak yerine boş bir bağlantıyı bekler. Testten sonra bot 2 açık bağlantı tutuyordu.
Autocommit tuzağı
aiomysql bağlantıları autocommit kapalı olarak açar. Bu durumda bir INSERT açık bir işlemin içinde kalır. Bağlantı o işlem hâlâ açıkken havuza döndüğünde aiomysql onu kapatır ve MariaDB onaylanmamış satırı atar. Testimizde autocommit=True olmayan bir havuza eklenen satırı diğer 0 bağlantı gördü. Ya autocommit=True değerini create_pool’a yukarıdaki gibi verin ya da yazma işlemlerinizden sonra await conn.commit() çağırın.
Başarısız bir sorgu botu durdurmaz
Testimizde bot çalışırken tabloyu sildik ve /notes gönderdik. aiogram hatayı, şununla biten bir traceback ile kaydetti: pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"). Bu komut için yanıt göndermedi ve çalışmaya devam etti: tablo geri geldiğinde sonraki /note kaydedildi. Komutu gönderen kişi hiçbir şey görmez, bu yüzden bir sorgu başarısız olabiliyorsa hatayı handler içinde yakalayı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. aiomysql bir handler bağlantı istediğinde bunu kontrol eder ve kapalı olanın yerine yenisini açar. Boşta kalma sınırını 5 saniyeye düşürerek test ettik: 7 sessiz saniyeden 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ı şununla reddedilir:
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 botu bu sınırın çok altında tutar.
- 120 saniyeden uzun süren bir sorgu durdurulur. MariaDB onu 1969 numaralı hatayla sonlandırır:
Query execution was interrupted (max_statement_time exceeded). Bizim daha önceki bir testimizde, aynı şekilde ayarlanmış bir hesapta bu, 120,03 saniye sonra oldu. Bir botun sorguları normalde milisaniye sürer, bu yüzden bu yalnızca kontrolden çıkmış bir sorguyu etkiler.
- Veritabanı başına 500 MB. Ne kadarının kullanıldığını Veritabanları sayfası gösterir. 500 MB’a ulaşınca, siz satır silene kadar yazma işlemleri durdurulur.
- Sunucu yedekleri veritabanını içermez. Ara sıra phpMyAdmin’den dışa aktarın.
Hatalar ve anlamları
Bunlar aiomysql’in testimizde verdiği hatalardır; konsolda bir traceback’i bitirdikleri biçimde yazılmıştır (aiomysql’in altta kullandığı PyMySQL’den gelirler):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): yanlış DB_USER veya DB_PASSWORD. Şifreyi sıfırla işleminden sonra (Bağlantı kartında) eski şifre hemen geçersiz olur: DB_PASSWORD değerini değiştirin ve botu yeniden başlatın.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): yanlış sunucu adresi veya port ya da PlayNHost dışından bir bağlantı. Veritabanı yalnızca PlayNHost sunucularından ve phpMyAdmin’den gelen bağlantıları kabul eder, bu yüzden kendi bilgisayarınızda da bunu alırsınız.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): çok fazla açık bağlantı, yukarıdaki sınırlara bakın. Örneğin botun ikinci bir kopyası ya da her mesaj için açılan bir havuz.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): bir kimlik sütunu INT. Onu BIGINT yapın.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): tablo yok. Yukarıdaki kod onu başlangıçta oluşturur; elle oluşturduysanız adını kontrol edin.
Ortam içinde eksik bir değişken, botu bağlanmadan önce şu hatayla durdurur: KeyError: 'DB_HOST' ya da eksik olanın adıyla. Çünkü kod her değeri os.environ[...] ile okur. Değişkeni ekleyin, sonra botu yeniden başlatın: değişkenler bir sonraki başlatmada uygulanır.
Yanlış token, veritabanı kısmından sonra ortaya çıkar. Sahte token’ımızla gerçek Telegram’a bağlandığımızda bot Database ready yazdırdı, sonra şununla durdu: aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. 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 174 MB bellek (resident) kullandı. Bunun çoğu aiogram’ın kendisi: önceki barındırma testimizde yalnızca aiogram 173 MB ölçüldü (o bot, hiçbir mesajı işlemeden önce reddedilen bir token’da durdu, yani iki sayı birbirine yakın ama tam bir karşılaştırma değil). aiogram ve aiomysql’i pip ile sıfırdan kurmak, dosya önbelleği dahil, 163 MB’ta zirve yaptı. Bu, en küçük planda kendi kodunuza yer bırakır. Barındırma kılavuzu kütüphaneleri karşılaştırır: PlayNHost’ta python-telegram-bot ve aiogram karşılaştırması.
Başka bir kütüphaneyle aynısı
Telegram botunuz için veritabanı dahil bir sunucu mu arıyorsunuz? Telegram bot planlarına göz atın.