3. De code
Een kleine notitiebot: /note buy milk slaat een notitie op, /notes toont je laatste vijf.
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()
Wat hij doet: run_polling() vraagt eerst aan Telegram wie de bot is, voert dan post_init uit en begint pas daarna berichten op te halen. post_init opent de pool, maakt de tabel notes aan als die ontbreekt, zet de pool in application.bot_data["pool"] en print Database ready en Logged in as @your_bot. Elke handler krijgt hetzelfde dictionary als context.bot_data. Als de bot stopt, voert run_polling() de functie post_shutdown uit, die de pool sluit.
context.args is de tekst na het commando, in woorden gesplitst, dus de code voegt hem weer samen met enkele spaties: regeleinden en dubbele spaties in een notitie worden één spatie. Heb je de notitie precies zoals getypt nodig, neem dan update.effective_message.text en knip het commando er zelf af.
De %s-placeholders worden door de driver ingevuld, dus een notitie als '); DROP TABLE notes; -- wordt als gewone tekst opgeslagen. In onze test werd ze opgeslagen als rij 2 en bleef de tabel bestaan. Bouw de SQL nooit op met een f-string.
Telegram-ID's opslaan als BIGINT
In de Bot API-documentatie van Telegram staat dat een gebruikers- of chat-ID „meer dan 32 significante bits kan hebben” maar er hooguit 52 heeft, dus een 64-bits integer 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). De INT van MySQL stopt bij 2.147.483.647. In onze test, met dezelfde strikte SQL-modus die onze databaseserver gebruikt, mislukte het opslaan van gebruikers-ID 7.000.000.001 in een INT-kolom. aiomysql gaf:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Een BIGINT-kolom sloeg hem wel op, en -1001234567890 ook. Gebruik gewoon BIGINT, niet BIGINT UNSIGNED, zodat ook chat-ID's passen.
4. Starten maar
- Upload
bot.py en requirements.txt bij 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 Logged in as @your_bot. Die laatste regel zet de status op Overzicht op Online in Telegram.
- Stuur je bot in Telegram
/note buy milk en 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 het pad van cursors.py van aiomysql ervoor. 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 vervanger van Telegram deze commando's en kreeg hij 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. |
Eén update tegelijk
Met de standaardinstellingen verwerkt python-telegram-bot één update tegelijk: het volgende commando wacht tot de vorige handler klaar is. In onze test kregen 4 commando's die tegelijk binnenkwamen antwoord in de volgorde waarin ze binnenkwamen, en na de test had de bot 1 open verbinding. maxsize=5 blijft belangrijk als je concurrent_updates aanzet in Application.builder(), of als jobs en handlers tegelijk querien: de pool opent nooit meer dan 5 verbindingen en een extra query wacht op een vrije.
De autocommit-valkuil
aiomysql opent verbindingen met autocommit uit. Een INSERT blijft dan in een open transactie staan. Als de verbinding met die nog open transactie terug naar de pool gaat, sluit aiomysql hem en gooit MariaDB de niet-bevestigde rij weg. In onze test werd een rij die op een pool zonder autocommit=True was ingevoegd door 0 andere verbindingen gezien. Geef 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 verwijderden we de tabel terwijl de bot draaide en stuurden we /notes. python-telegram-bot printte No error handlers are registered, logging exception. en een traceback die eindigde op pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), stuurde geen antwoord op dat commando en bleef draaien: zodra de tabel terug was, werd de volgende /note opgeslagen. Wie het commando stuurde, ziet niets, dus vang de fout op in de handler, of registreer een handler met application.add_error_handler(), en antwoord met een kort bericht.
Een rustige nacht breekt de pool niet
Onze databaseserver sluit een verbinding die 8 uur ongebruikt is geweest. Een bot die zo lang geen berichten krijgt, bijvoorbeeld 's nachts, treft zijn gepoolde verbindingen gesloten aan. aiomysql controleert dat als een handler een verbinding vraagt en opent een nieuwe in plaats van een gesloten verbinding. We testten het met de inactiviteitslimiet verlaagd naar 5 seconden: na 7 rustige seconden werd de volgende /note zonder fout opgeslagen.
Limieten die voor je pool belangrijk zijn
- 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 gebruikt is. Bij 500 MB worden schrijfacties gestopt tot je rijen verwijdert.
- Serverback-ups bevatten de database niet. Exporteer hem af en toe vanuit phpMyAdmin.
Foutmeldingen 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 DB_PASSWORD. Na Wachtwoord resetten op de kaart Verbinding werkt het oude wachtwoord meteen niet meer: vervang DB_PASSWORD en herstart de bot.
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 in BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): de tabel ontbreekt. De code hierboven maakt hem aan bij het starten; heb je hem met de hand gemaakt, controleer dan de naam.
Een variabele die ontbreekt in Omgeving stopt de bot voordat hij verbindt, met KeyError: 'DB_HOST' of de naam van de ontbrekende, omdat de code elke waarde leest met os.environ[...]. Voeg hem toe en herstart de bot: variabelen gelden bij de volgende start.
Een verkeerde token stopt de bot vóór het databasedeel, omdat post_init pas draait nadat Telegram de token heeft geaccepteerd. Met onze nepttoken tegen het echte Telegram printte de bot nooit Database ready en stopte hij na ongeveer 1 seconde met telegram.error.InvalidToken: The token `...` was rejected by the server. Dat bericht bevat de hele token, dus plak consoleoutput daarmee nergens openbaar. Kopieer de token opnieuw van @BotFather en plak hem op de pagina Bot, paneel Telegram. Meer Telegram-fouten: Telegram-bot 409 Conflict oplossen.
Hoeveel geheugen hij gebruikt
In onze test gebruikte de draaiende bot 48,5 MB geheugen (resident) na het beantwoorden van de 8 commando's, met open pool, in een container met een limiet van 512 MB zoals ons kleinste botplan. In onze eerdere hostingtest mat python-telegram-bot alleen 44 MB (die bot stopte bij een geweigerde token, voordat hij een bericht verwerkte), dus het verschil, ongeveer 4,5 MB, dekt aiomysql, de pool en de verwerkte commando's. Het installeren van python-telegram-bot en aiomysql met pip vanaf nul piekte op 73 MB, bestandscache inbegrepen. De bibliotheken naast elkaar: python-telegram-bot vs aiogram op PlayNHost.
Hetzelfde met een andere bibliotheek
Een server voor je Telegram-bot met database inbegrepen nodig? Bekijk de Telegram-botplannen.