3. Kod
Mały bot z notatkami: /note buy milk zapisuje notatkę, /notes pokazuje twoje ostatnie pięć.
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()
Co robi: run_polling() najpierw pyta Telegram, kim jest bot, potem uruchamia post_init i dopiero wtedy zaczyna pobierać wiadomości. post_init otwiera pulę, tworzy tabelę notes, jeśli jej brakuje, umieszcza pulę w application.bot_data["pool"] i wypisuje Database ready oraz Logged in as @your_bot. Każdy handler dostaje ten sam słownik jako context.bot_data. Gdy bot się zatrzymuje, run_polling() uruchamia post_shutdown, która zamyka pulę.
context.args to tekst po poleceniu, podzielony na słowa, więc kod składa go z powrotem pojedynczymi spacjami: podziały wierszy i podwójne spacje wewnątrz notatki zamieniają się w jedną spację. Jeśli potrzebujesz notatki dokładnie tak, jak została wpisana, weź update.effective_message.text i odetnij polecenie samodzielnie.
Symbole zastępcze %s wypełnia sterownik, więc notatka taka jak '); DROP TABLE notes; -- zostaje zapisana jako zwykły tekst. W naszym teście zapisała się jako wiersz 2, a tabela została. Nigdy nie buduj SQL za pomocą f-stringa.
Przechowuj ID Telegrama jako BIGINT
Dokumentacja Bot API Telegrama mówi, że ID użytkownika lub czatu „może mieć więcej niż 32 znaczące bity”, ale ma ich najwyżej 52, więc 64-bitowa liczba całkowita jest bezpieczna. Strona o ID podaje zakresy: ID użytkowników sięgają 1.099.511.627.775, a ID grup, supergrup i kanałów są ujemne. Supergrupy i kanały mieszczą się od -1.000.000.000.001 do -1.997.852.516.352 (na przykład -1001234567890). INT w MySQL kończy się na 2.147.483.647. W naszym teście, przy tym samym ścisłym trybie SQL, którego używa nasz serwer bazy, zapis ID użytkownika 7.000.000.001 w kolumnie INT się nie udał. aiomysql zgłosił:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Kolumna BIGINT go zapisała, -1001234567890 również. Użyj zwykłego BIGINT, a nie BIGINT UNSIGNED, żeby mieściły się też ID czatów.
4. Uruchom
- Wgraj
bot.py i requirements.txt w Files.
- Naciśnij Uruchom i otwórz Console. Start instaluje pakiety z
requirements.txt, a potem pokazuje [PlayNHost] Starting your bot, Database ready i Logged in as @your_bot. Ta ostatnia linia zmienia status w Przegląd na Online w Telegramie.
- W Telegramie wyślij swojemu botowi
/note buy milk, a potem /notes. Żeby zobaczyć wiersze, otwórz stronę bazy i kliknij Otwórz w phpMyAdmin.
Od drugiego startu konsola pokazuje też Warning: Table 'notes' already exists, a przed tym ścieżkę do cursors.py z aiomysql. To uwaga MariaDB dla CREATE TABLE IF NOT EXISTS, przekazana przez sterownik. Nic złego się nie dzieje.
W naszym teście zamiennik Telegrama wysłał te polecenia i dostał te odpowiedzi:
| Wysłano | Odpowiedź |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note i 501 znaków | A note can be up to 500 characters. |
/notes | Zapisane notatki, od najnowszej |
/notes od drugiego użytkownika | You have no notes yet. |
Jedna aktualizacja naraz
Przy ustawieniach domyślnych python-telegram-bot obsługuje jedną aktualizację naraz: następne polecenie czeka, aż poprzedni handler skończy. W naszym teście 4 polecenia, które przyszły razem, dostały odpowiedź w kolejności nadejścia, a po teście bot trzymał 1 otwarte połączenie. maxsize=5 nadal ma znaczenie, jeśli włączysz concurrent_updates w Application.builder() albo jeśli zadania i handlery odpytują bazę jednocześnie: pula nigdy nie otwiera więcej niż 5 połączeń, a dodatkowe zapytanie czeka na wolne.
Pułapka autocommit
aiomysql otwiera połączenia z wyłączonym autocommit. INSERT zostaje wtedy w otwartej transakcji. Gdy połączenie wraca do puli z wciąż otwartą transakcją, aiomysql je zamyka, a MariaDB odrzuca niezatwierdzony wiersz. W naszym teście wiersz wstawiony na puli bez autocommit=True był widoczny dla 0 innych połączeń. Przekaż autocommit=True do create_pool, jak wyżej, albo wywołaj await conn.commit() po zapisach.
Nieudane zapytanie nie zatrzymuje bota
W naszym teście usunęliśmy tabelę, gdy bot działał, i wysłaliśmy /notes. python-telegram-bot wypisał No error handlers are registered, logging exception. oraz traceback kończący się na pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), nie wysłał odpowiedzi na to polecenie i działał dalej: gdy tabela wróciła, następne /note zostało zapisane. Osoba, która wysłała polecenie, nic nie widzi, więc przechwyć błąd w handlerze albo zarejestruj handler błędów przez application.add_error_handler() i odpowiedz krótką wiadomością.
Spokojna noc nie psuje puli
Nasz serwer bazy zamyka połączenie, które było bezczynne przez 8 godzin. Bot, który tak długo nie dostaje wiadomości, na przykład w nocy, zastaje swoje połączenia w puli zamknięte. aiomysql sprawdza to, gdy handler prosi o połączenie, i otwiera nowe w miejsce zamkniętego. Przetestowaliśmy to z limitem bezczynności obniżonym do 5 sekund: po 7 spokojnych sekundach następne /note zostało zapisane bez błędu.
Limity ważne dla twojej puli
- 20 połączeń naraz na użytkownika bazy. Liczy się każdy proces, który używa tego samego loginu. Połączenie 21 zostaje odrzucone z
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 trzyma bota znacznie poniżej tego limitu.
- Zapytanie trwające dłużej niż 120 sekund jest zatrzymywane. MariaDB kończy je błędem 1969,
Query execution was interrupted (max_statement_time exceeded). W naszym wcześniejszym teście, na koncie skonfigurowanym tak samo, stało się to po 120,03 sekundy. Zapytania bota zwykle trwają milisekundy, więc dotyczy to tylko zapytania, które wymknęło się spod kontroli.
- 500 MB na bazę. Strona Bazy danych pokazuje, ile jest zajęte. Przy 500 MB zapisy są zatrzymywane, dopóki nie usuniesz wierszy.
- Kopie zapasowe serwera nie obejmują bazy danych. Co jakiś czas eksportuj ją z phpMyAdmin.
Błędy i co oznaczają
To błędy, które aiomysql zgłosił w naszym teście, zapisane tak, jak kończą traceback w konsoli (pochodzą z PyMySQL, którego aiomysql używa pod spodem):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): zły DB_USER lub DB_PASSWORD. Po Zresetuj hasło na karcie Połączenie stare hasło przestaje działać od razu: zamień DB_PASSWORD i zrestartuj bota.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): zły host lub port albo połączenie spoza PlayNHost. Baza przyjmuje tylko połączenia z serwerów PlayNHost i z phpMyAdmin, więc to samo dostaniesz na własnym komputerze.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): za dużo otwartych połączeń, zobacz limity wyżej. Na przykład druga kopia bota albo pula otwierana dla każdej wiadomości.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): kolumna z ID jest typu INT. Zmień ją na BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): brakuje tabeli. Kod powyżej tworzy ją przy starcie; jeśli tabela powstała ręcznie, sprawdź jej nazwę.
Zmienna, której brakuje w Środowisko, zatrzymuje bota przed połączeniem, z KeyError: 'DB_HOST' lub nazwą brakującej zmiennej, bo kod czyta każdą wartość przez os.environ[...]. Dodaj ją i zrestartuj bota: zmienne obowiązują od następnego startu.
Zły token zatrzymuje bota przed częścią z bazą, bo post_init uruchamia się dopiero po tym, jak Telegram zaakceptuje token. Z naszym fałszywym tokenem wobec prawdziwego Telegrama bot nigdy nie wypisał Database ready i zatrzymał się po około 1 sekundzie z telegram.error.InvalidToken: The token `...` was rejected by the server. Ta wiadomość zawiera cały token, więc nie wklejaj nigdzie publicznie wyjścia konsoli, które go zawiera. Skopiuj token ponownie z @BotFather i wklej go na stronie Bot, w panelu Telegram. Więcej błędów Telegrama: Jak naprawić błąd 409 Conflict bota Telegram.
Ile pamięci zajmuje
W naszym teście działający bot zajmował 48,5 MB pamięci (rezydentnej) po odpowiedzi na 8 poleceń, z otwartą pulą, w kontenerze ograniczonym do 512 MB jak nasz najmniejszy plan bota. W naszym wcześniejszym teście hostingu sam python-telegram-bot zmierzył 44 MB (ten bot zatrzymał się na odrzuconym tokenie, zanim obsłużył jakąkolwiek wiadomość), więc różnica, około 4,5 MB, obejmuje aiomysql, pulę i obsłużone polecenia. Instalacja python-telegram-bot i aiomysql przez pip od zera osiągnęła szczyt 73 MB, razem z pamięcią podręczną plików. Biblioteki obok siebie: python-telegram-bot kontra aiogram w PlayNHost.
To samo z inną biblioteką
Potrzebujesz serwera dla swojego bota Telegram z wliczoną bazą danych? Zobacz plany dla botów Telegram.