3. Kod
Küçük bir not botu: /note buy milk bir not kaydeder, /notes son beş notunuzu gösterir.
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()
Ne yapar: run_polling() önce Telegram'a botun kim olduğunu sorar, sonra post_init işlevini çalıştırır ve ancak ondan sonra mesaj almaya başlar. post_init havuzu açar, yoksa notes tablosunu oluşturur, havuzu application.bot_data["pool"] içine koyar ve Database ready ile Logged in as @your_bot satırlarını yazdırır. Her handler aynı sözlüğü context.bot_data olarak alır. Bot durduğunda run_polling(), havuzu kapatan post_shutdown işlevini çalıştırır.
context.args, komuttan sonraki metindir ve kelimelere bölünmüştür; bu yüzden kod onu tek boşluklarla yeniden birleştirir: bir notun içindeki satır sonları ve çift boşluklar tek boşluğa dönüşür. Notu tam olarak yazıldığı gibi istiyorsanız update.effective_message.text değerini alın ve komutu kendiniz kesin.
%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 ID'lerini BIGINT olarak saklayın
Telegram'ın Bot API belgeleri, bir kullanıcı veya sohbet ID'sinin “32'den fazla anlamlı bit içerebileceğini” ama en fazla 52 bit olduğunu söyler; yani 64 bitlik bir tamsayı güvenlidir. ID'ler sayfası aralıkları verir: kullanıcı ID'leri 1.099.511.627.775'e kadar çıkar, grup, süper grup ve kanal ID'leri ise negatiftir. Süper gruplar 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. Testimizde, veritabanı sunucumuzun kullandığı aynı katı SQL kipiyle, 7.000.000.001 kullanıcı ID'sini 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 bunu sakladı, -1001234567890 değerini de. Sohbet ID'leri de sığsın diye düz BIGINT kullanın, BIGINT UNSIGNED değil.
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şlangıçta
requirements.txt içindeki paketler kurulur, ardından [PlayNHost] Starting your bot, Database ready ve Logged in as @your_bot görünür. Bu son satır, Genel bakış içindeki durumu Telegram’da çevrimiçi yapar.
- Telegram'da botunuza
/note buy milk, ardından /notes gönderin. Satırlara bakmak için veritabanının sayfasını açın ve phpMyAdmin’de aç öğesine tıklayın.
İkinci başlatmadan itibaren konsol ayrıca Warning: Table 'notes' already exists satırını gösterir, önünde aiomysql'in cursors.py dosyasının yolu bulunur. Bu, CREATE TABLE IF NOT EXISTS için MariaDB'nin notudur ve sürücü tarafından iletilir. Bir sorun yok.
Testimizde Telegram'ın benzeri ş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 yeni önce |
/notes ikinci bir kullanıcıdan | You have no notes yet. |
Her seferinde bir güncelleme
Varsayılan ayarlarla python-telegram-bot her seferinde bir güncellemeyi işler: sonraki komut, önceki handler bitene kadar bekler. Testimizde birlikte gelen 4 komut geliş sırasıyla yanıtlandı ve testten sonra botun 1 açık bağlantısı vardı. maxsize=5, concurrent_updates seçeneğini Application.builder() içinde açarsanız ya da işler ve handler'lar aynı anda sorgu yaparsa yine de önemlidir: havuz asla 5'ten fazla bağlantı açmaz ve fazladan bir sorgu boş bir bağlantıyı bekler.
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ı, bu 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 havuzda eklenen satırı 0 başka bağlantı gördü. Ya yukarıdaki gibi autocommit=True değerini create_pool işlevine 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. python-telegram-bot No error handlers are registered, logging exception. yazdırdı ve pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist") ile biten bir geri izleme verdi, o komuta yanıt göndermedi ve çalışmaya devam etti: tablo geri gelince bir sonraki /note kaydedildi. Komutu gönderen kişi hiçbir şey görmez, bu yüzden hatayı handler içinde yakalayın ya da application.add_error_handler() ile bir hata handler'ı kaydedin ve kısa bir mesajla yanıt verin.
Sakin 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ın kapandığını görür. 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 indirerek test ettik: 7 sakin saniyeden sonra bir 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ı şu hatayla reddedilir:
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 botu bunun çok altında tutar.
- 120 saniyeden uzun süren bir sorgu durdurulur. MariaDB onu 1969 hatasıyla sonlandırır,
Query execution was interrupted (max_statement_time exceeded). Daha önceki bir testimizde, aynı şekilde ayarlanmış bir hesapta bu, 120,03 saniye sonra oldu. Bir botun sorguları normalde milisaniyeler sürer, bu yüzden bu yalnızca başıboş kalmış 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ırları silene kadar yazma işlemleri durdurulur.
- Sunucu yedekleri veritabanını içermez. Zaman zaman phpMyAdmin'den dışa aktarın.
Hatalar ve anlamları
Bunlar, aiomysql'in testimizde verdiği hatalardır ve konsolda bir geri izlemeyi 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. Bağlantı kartında Şifreyi sıfırla işleminden sonra eski şifre hemen çalışmaz: 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 gelen 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 ID sütunu INT türünde. 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 bölmesinde eksik bir değişken, botu bağlanmadan önce durdurur; hata KeyError: 'DB_HOST' ya da eksik olanın adıdır, çü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ış bir token botu veritabanı kısmından önce durdurur, çünkü post_init yalnızca Telegram token'ı kabul ettikten sonra çalışır. Sahte token'ımızla gerçek Telegram'a karşı bot hiçbir zaman Database ready yazdırmadı ve yaklaşık 1 saniye sonra şu hatayla durdu: telegram.error.InvalidToken: The token `...` was rejected by the server. Bu mesaj token'ın tamamını içerir, bu yüzden onu içeren konsol çıktısını herkese açık hiçbir yere yapıştırmayın. Token'ı @BotFather'dan yeniden kopyalayın ve Bot sayfasında, Telegram bölmesinde 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 48,5 MB bellek (resident) kullandı. Daha önceki barındırma testimizde python-telegram-bot tek başına 44 MB ölçüldü (o bot, hiçbir mesajı işlemeden reddedilen bir token'da durdu), dolayısıyla yaklaşık 4,5 MB'lık fark aiomysql'i, havuzu ve işlenen komutları kapsar. python-telegram-bot ve aiomysql'i pip ile sıfırdan kurmak, dosya önbelleği dahil 73 MB'a çıktı. Kütüphaneler yan yana: PlayNHost’ta python-telegram-bot ve aiogram karşılaştırması.
Başka bir kütüphaneyle aynısı
Veritabanı dahil, Telegram botunuz için bir sunucuya mı ihtiyacınız var? Telegram bot planlarına göz atın.