3. Kod
Mały bot z notatkami: /note buy milk zapisuje notatkę, /notes pokazuje twoje ostatnie pięć.
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())
Co robi: main() otwiera pulę, tworzy tabelę notes jeśli jej brakuje, i wypisuje Database ready zanim bot zacznie pytać Telegram o wiadomości. dp.start_polling(bot, pool=pool) to własny sposób aiogram na współdzielenie obiektu: każde dodatkowe słowo kluczowe, które przekażesz, staje się danymi, które aiogram podaje każdemu handlerowi z parametrem o tej nazwie, więc note() i notes() dostają tę samą pulę. Gdy bot się zatrzymuje, blok finally zamyka pulę.
Symbole zastępcze %s wypełnia sterownik, więc notatka taka jak '); DROP TABLE notes; -- zostanie 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 najwyżej 52, więc 64-bitowa liczba całkowita jest bezpieczna. Jej 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 biegną od -1.000.000.000.001 do -1.997.852.516.352 (na przykład -1001234567890). Typ 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 danych, zapis ID użytkownika 7.000.000.001 w kolumnie INT nie powiódł się. aiomysql zgłosił:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Kolumna BIGINT zapisała go, tak samo jak -1001234567890. Używaj zwykłego BIGINT, a nie BIGINT UNSIGNED, żeby mieściły się też ID czatów.
4. Uruchom
- Prześlij
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 Run polling for bot @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. Aby zobaczyć wiersze, otwórz stronę bazy i kliknij Otwórz w phpMyAdmin.
Od drugiego startu konsola pokazuje też Warning: Table 'notes' already exists, poprzedzone ścieżką do pliku cursors.py z aiomysql. To uwaga MariaDB dla CREATE TABLE IF NOT EXISTS, przekazana przez sterownik. Nic nie jest zepsute.
W naszym teście atrapa Telegrama wysłała te polecenia i dostała 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, najnowsze pierwsze |
/notes od drugiego użytkownika | You have no notes yet. |
Wiele wiadomości naraz
aiogram obsługuje każdą przychodzącą aktualizację jako osobne zadanie, więc kilka poleceń może wykonywać zapytania w tym samym czasie. W naszym teście 4 polecenia, które przyszły razem, zostały obsłużone w około 50 ms każde. maxsize=5 ogranicza pulę do 5 połączeń; szóste zapytanie czeka na wolne zamiast otwierać kolejne. Po teście bot trzymał 2 otwarte połączenia.
Pułapka autocommit
aiomysql otwiera połączenia z wyłączonym autocommit. Wtedy INSERT zostaje 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 w puli bez autocommit=True został zobaczony przez 0 innych połączeń. Albo przekaż autocommit=True do create_pool, jak wyżej, albo wywołaj await conn.commit() po swoich zapisach.
Nieudane zapytanie nie zatrzymuje bota
W naszym teście usunęliśmy tabelę, gdy bot działał, i wysłaliśmy /notes. aiogram zapisał błąd w logu z tracebackiem kończącym 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, kolejne /note zostało zapisane. Osoba, która wysłała polecenie, nic nie widzi, więc jeśli zapytanie może się nie udać, złap błąd w handlerze i odpowiedz krótką wiadomością.
Spokojna noc nie psuje puli
Nasz serwer bazy danych 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 z puli zamknięte. aiomysql sprawdza to, gdy handler prosi o połączenie, i otwiera nowe zamiast zamkniętego. Przetestowaliśmy to z limitem bezczynności obniżonym do 5 sekund: po 7 sekundach ciszy kolejne /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 używający tego samego loginu. Połączenie 21 zostanie odrzucone z
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 trzyma bota znacznie poniżej tego.
- Zapytanie trwające dłużej niż 120 sekund zostaje zatrzymane. MariaDB kończy je błędem 1969,
Query execution was interrupted (max_statement_time exceeded). W naszym wcześniejszym teście, na koncie ustawionym 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. Od czasu do czasu 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 zły 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 połączenia tylko z serwerów PlayNHost i 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 powyż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 ID ma typ INT. Zmień ją na BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): brakuje tabeli. Kod powyżej tworzy ją przy starcie; jeśli została utworzona ręcznie, sprawdź jej nazwę.
Zmienna brakująca w Środowisko zatrzymuje bota przed połączeniem, z komunikatem KeyError: 'DB_HOST' lub nazwą brakującej, bo kod czyta każdą wartość przez os.environ[...]. Dodaj ją, potem zrestartuj bota: zmienne działają od następnego startu.
Zły token wychodzi na jaw po części z bazą. Z naszym fałszywym tokenem wobec prawdziwego Telegrama bot wypisał Database ready, a potem zatrzymał się z aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Skopiuj token jeszcze raz od @BotFather i wklej go na stronie Bot, sekcja 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 zajął 174 MB pamięci (rezydentnej) po odpowiedzi na 8 poleceń, z otwartą pulą, w kontenerze ograniczonym do 512 MB jak nasz najmniejszy plan bota. Większość to sam aiogram: w naszym wcześniejszym teście hostingu sam aiogram wyniósł 173 MB (ten bot zatrzymał się na odrzuconym tokenie, zanim obsłużył jakąkolwiek wiadomość, więc obie liczby są bliskie, ale to nie jest dokładne porównanie). Instalacja aiogram i aiomysql przez pip od zera osiągnęła szczyt 163 MB, razem z pamięcią podręczną plików. To zostawia miejsce na twój własny kod w najmniejszym planie. Przewodnik po hostingu porównuje biblioteki: python-telegram-bot kontra aiogram w PlayNHost.
To samo z inną biblioteką
Potrzebujesz serwera dla swojego bota Telegram z wliczoną bazą danych? Zobacz plany botów Telegram.