3. De code
Een kleine notitiebot: /note buy milk slaat een notitie op, /notes toont je laatste vijf.
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())
Wat hij doet: main() opent de pool, maakt de tabel notes aan als die ontbreekt en print Database ready voordat de bot Telegram om berichten vraagt. dp.start_polling(bot, pool=pool) is de eigen manier van aiogram om een object te delen: elk extra sleutelwoord dat je meegeeft wordt data die aiogram doorgeeft aan elke handler met een parameter met die naam, dus note() en notes() krijgen dezelfde pool. Als de bot stopt, sluit het finally-blok de pool.
De %s-plaatshouders worden door de driver ingevuld, dus een notitie als '); DROP TABLE notes; -- wordt als gewone tekst opgeslagen. In onze test is hij opgeslagen als rij 2 en bleef de tabel bestaan. Bouw de SQL nooit op met een f-string.
Sla Telegram-ID’s op als BIGINT
In de Bot API-documentatie van Telegram staat dat een gebruikers- of chat-ID “meer dan 32 significante bits kan hebben”, maar hooguit 52, dus een 64-bits geheel getal is veilig. De pagina over ID’s geeft de bereiken: gebruikers-ID’s gaan tot 1.099.511.627.775, terwijl ID’s van groepen, supergroepen en kanalen negatief zijn. Supergroepen en kanalen lopen van -1.000.000.000.001 tot -1.997.852.516.352 (bijvoorbeeld -1001234567890). Het type INT van MySQL stopt bij 2.147.483.647. In onze test mislukte het opslaan van gebruikers-ID 7.000.000.001 in een INT-kolom, met dezelfde strikte SQL-modus als onze databaseserver. aiomysql gaf:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Een BIGINT-kolom sloeg hem op, en -1001234567890 ook. Gebruik gewone BIGINT, niet BIGINT UNSIGNED, zodat ook chat-ID’s passen.
4. Uitvoeren
- Upload
bot.py en requirements.txt onder Files.
- Druk op Starten en open Console. Bij het starten worden de pakketten uit
requirements.txt geïnstalleerd, daarna zie je [PlayNHost] Starting your bot, Database ready en Run polling for bot @your_bot. Die laatste regel zet de status onder Overzicht op Online in Telegram.
- Stuur je bot in Telegram
/note buy milk, daarna /notes. Om de rijen te bekijken open je de pagina van de database en klik je op Openen in phpMyAdmin.
Vanaf de tweede start toont de console ook Warning: Table 'notes' already exists, met ervoor het pad naar cursors.py van aiomysql. Dat is de melding van MariaDB voor CREATE TABLE IF NOT EXISTS, doorgegeven door de driver. Er is niets mis.
In onze test stuurde de stand-in voor Telegram deze commando’s en kreeg deze antwoorden:
| Verstuurd | Antwoord |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note en 501 tekens | A note can be up to 500 characters. |
/notes | De opgeslagen notities, nieuwste eerst |
/notes van een tweede gebruiker | You have no notes yet. |
Veel berichten tegelijk
aiogram verwerkt elke binnenkomende update als een eigen taak, dus meerdere commando’s kunnen hun queries tegelijk uitvoeren. In onze test werden 4 commando’s die tegelijk binnenkwamen elk in ongeveer 50 ms afgehandeld. maxsize=5 begrenst de pool op 5 verbindingen; een zesde query wacht op een vrije in plaats van er nog een te openen. Na de test had de bot 2 open verbindingen.
De autocommit-valkuil
aiomysql opent verbindingen met autocommit uit. Een INSERT blijft dan in een open transactie staan. Als de verbinding terug naar de pool gaat terwijl die transactie nog open is, sluit aiomysql haar en gooit MariaDB de niet-vastgelegde rij weg. In onze test werd een rij die in een pool zonder autocommit=True was ingevoegd, door 0 andere verbindingen gezien. Geef ofwel autocommit=True mee aan create_pool, zoals hierboven, of roep await conn.commit() aan na je schrijfacties.
Een mislukte query stopt de bot niet
In onze test hebben we de tabel verwijderd terwijl de bot draaide en stuurden /notes. aiogram logde de fout met een traceback die eindigt op pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), stuurde geen antwoord voor dat commando en draaide door: zodra de tabel terug was, werd de volgende /note opgeslagen. Wie het commando stuurt, ziet niets, dus als een query kan mislukken, vang de fout dan op in de handler en antwoord met een kort bericht.
Een rustige nacht breekt de pool niet
Onze databaseserver sluit een verbinding die 8 uur inactief is geweest. Een bot die zo lang geen berichten krijgt, bijvoorbeeld ’s nachts, vindt zijn gepoolde verbindingen gesloten. aiomysql controleert dat wanneer een handler een verbinding vraagt en opent een nieuwe in plaats van een gesloten. We testten het met de inactiviteitslimiet verlaagd naar 5 seconden: na 7 rustige seconden werd de volgende /note opgeslagen zonder fout.
Limieten die voor je pool tellen
- 20 verbindingen tegelijk per databasegebruiker. Elk proces dat dezelfde login gebruikt, telt mee. Verbinding 21 wordt geweigerd met
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 houdt de bot ruim eronder.
- Een query die langer dan 120 seconden duurt, wordt gestopt. MariaDB beëindigt hem met fout 1969,
Query execution was interrupted (max_statement_time exceeded). In een eerdere test van ons, op een account dat op dezelfde manier is ingesteld, gebeurde dat na 120,03 seconden. De queries van een bot duren normaal milliseconden, dus dit raakt alleen een op hol geslagen query.
- 500 MB per database. De pagina Databases toont hoeveel er wordt gebruikt. Bij 500 MB worden schrijfacties gestopt tot je rijen verwijdert.
- Serverbackups bevatten de database niet. Exporteer hem af en toe vanuit phpMyAdmin.
Fouten en wat ze betekenen
Dit zijn de fouten die aiomysql in onze test gaf, geschreven zoals ze een traceback in de console afsluiten (ze komen van PyMySQL, dat aiomysql eronder gebruikt):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): verkeerde DB_USER of verkeerde DB_PASSWORD. Na Wachtwoord resetten op de kaart Verbinding, werkt het oude wachtwoord meteen niet meer: vervang DB_PASSWORD en start de bot opnieuw.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): verkeerde host of poort, of een verbinding van buiten PlayNHost. De database accepteert alleen verbindingen van PlayNHost-servers en phpMyAdmin, dus dit krijg je ook op je eigen computer.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): te veel open verbindingen, zie de limieten hierboven. Bijvoorbeeld een tweede kopie van de bot, of een pool die voor elk bericht wordt geopend.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): een ID-kolom is INT. Verander hem naar BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): de tabel ontbreekt. De code hierboven maakt hem bij het starten aan; als je hem met de hand hebt gemaakt, controleer dan de naam.
Een variabele die ontbreekt in Omgeving stopt de bot voordat hij verbinding maakt, met KeyError: 'DB_HOST' of de naam van de ontbrekende, omdat de code elke waarde leest met os.environ[...]. Voeg hem toe en start de bot opnieuw: variabelen worden toegepast bij de volgende start.
Een verkeerde token valt pas na het databasegedeelte op. Met onze nep-token tegen het echte Telegram printte de bot Database ready, en stopte toen met aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Kopieer de token opnieuw van @BotFather en plak hem op de pagina Bot, deel Telegram. Meer Telegram-fouten: Telegram-bot 409 Conflict oplossen.
Hoeveel geheugen het gebruikt
In onze test gebruikte de draaiende bot 174 MB geheugen (resident) nadat hij de 8 commando’s had beantwoord, met zijn pool open, in een container begrensd op 512 MB zoals ons kleinste botabonnement. Het meeste daarvan is aiogram zelf: in onze eerdere hostingtest mat aiogram alleen 173 MB (die bot stopte bij een geweigerde token, voordat hij een bericht verwerkte, dus de twee getallen liggen dicht bij elkaar maar het is geen exacte vergelijking). Het installeren van aiogram en aiomysql met pip vanaf nul piekte op 163 MB, inclusief bestandscache. Dat laat ruimte voor je eigen code op het kleinste abonnement. De hostinggids vergelijkt de bibliotheken: python-telegram-bot vs aiogram op PlayNHost.
Hetzelfde met een andere bibliotheek
Heb je een server voor je Telegram-bot met database inbegrepen nodig? Bekijk de Telegram-botabonnementen.