3. O código
Um pequeno bot de notas: /note buy milk salva uma nota, /notes mostra as suas últimas cinco.
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())
O que ele faz: main() abre o pool, cria a tabela notes se ela não existir e imprime Database ready antes de o bot pedir mensagens ao Telegram. dp.start_polling(bot, pool=pool) é o jeito do próprio aiogram de compartilhar um objeto: toda palavra-chave extra que você passa vira um dado que o aiogram entrega a qualquer handler com um parâmetro desse nome, então note() e notes() recebem o mesmo pool. Quando o bot para, o bloco finally fecha o pool.
Os marcadores %s são preenchidos pelo driver, então uma nota como '); DROP TABLE notes; -- é salva como texto simples. No nosso teste, ela foi salva como a linha 2 e a tabela continuou lá. Nunca monte o SQL com uma f-string.
Guarde os IDs do Telegram como BIGINT
A documentação da Bot API do Telegram diz que um ID de usuário ou de chat “pode ter mais de 32 bits significativos”, mas no máximo 52, então um inteiro de 64 bits é seguro. A página sobre IDs dá os intervalos: IDs de usuário vão até 1.099.511.627.775, enquanto IDs de grupos, supergrupos e canais são negativos. Supergrupos e canais vão de -1.000.000.000.001 a -1.997.852.516.352 (por exemplo -1001234567890). O tipo INT do MySQL para em 2.147.483.647. No nosso teste, com o mesmo modo SQL estrito que o nosso servidor de banco usa, salvar o ID de usuário 7.000.000.001 em uma coluna INT falhou. O aiomysql lançou:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Uma coluna BIGINT armazenou o valor, e -1001234567890 também. Use um BIGINT simples, não BIGINT UNSIGNED, para que os IDs de chat também caibam.
4. Execute
- Envie
bot.py e requirements.txt em Files.
- Pressione Iniciar e abra Console. A inicialização instala os pacotes de
requirements.txt, depois mostra [PlayNHost] Starting your bot, Database ready e Run polling for bot @your_bot. Essa última linha muda o status em Visão geral para Online no Telegram.
- No Telegram, envie ao seu bot
/note buy milk, depois /notes. Para ver as linhas, abra a página do banco e clique em Abrir no phpMyAdmin.
A partir da segunda inicialização, o console também mostra Warning: Table 'notes' already exists, com o caminho do arquivo cursors.py do aiomysql na frente. É o aviso do MariaDB para CREATE TABLE IF NOT EXISTS, repassado pelo driver. Nada está errado.
No nosso teste, o substituto do Telegram enviou estes comandos e recebeu estas respostas:
| Enviado | Resposta |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note e 501 caracteres | A note can be up to 500 characters. |
/notes | As notas salvas, da mais recente para a mais antiga |
/notes de um segundo usuário | You have no notes yet. |
Muitas mensagens ao mesmo tempo
O aiogram trata cada atualização recebida como uma tarefa própria, então vários comandos podem executar suas consultas ao mesmo tempo. No nosso teste, 4 comandos que chegaram juntos foram tratados em cerca de 50 ms cada um. maxsize=5 limita o pool a 5 conexões; uma sexta consulta espera por uma livre em vez de abrir outra. Depois do teste, o bot mantinha 2 conexões abertas.
A armadilha do autocommit
O aiomysql abre as conexões com o autocommit desligado. Um INSERT fica então dentro de uma transação aberta. Quando a conexão volta ao pool com essa transação ainda aberta, o aiomysql a fecha, e o MariaDB descarta a linha não confirmada. No nosso teste, uma linha inserida em um pool sem autocommit=True foi vista por 0 outras conexões. Ou passe autocommit=True para create_pool, como acima, ou chame await conn.commit() depois das suas gravações.
No nosso teste, apagamos a tabela com o bot rodando e enviamos /notes. O aiogram registrou o erro com um traceback terminando em pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), não enviou resposta a esse comando e continuou rodando: quando a tabela voltou, o /note seguinte foi salvo. Quem enviou o comando não vê nada, então, se uma consulta puder falhar, capture o erro no handler e responda com uma mensagem curta.
Uma noite tranquila não quebra o pool
Nosso servidor de banco fecha uma conexão ociosa por 8 horas. Um bot que fica sem mensagens por tanto tempo, de madrugada, por exemplo, encontra as conexões do pool fechadas. O aiomysql verifica isso quando um handler pede uma conexão e abre uma nova no lugar da fechada. Testamos com o limite de ociosidade reduzido para 5 segundos: após 7 segundos de silêncio, o /note seguinte foi salvo sem nenhum erro.
Limites que importam para o seu pool
- 20 conexões ao mesmo tempo por usuário do banco. Todo processo que usa o mesmo login conta. A conexão 21 é recusada com
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 mantém o bot bem abaixo disso.
- Uma consulta que roda por mais de 120 segundos é interrompida. O MariaDB a encerra com o erro 1969,
Query execution was interrupted (max_statement_time exceeded). Em um teste anterior nosso, em uma conta configurada do mesmo jeito, isso aconteceu após 120,03 segundos. As consultas de um bot normalmente levam milissegundos, então isso só atinge uma consulta fora de controle.
- 500 MB por banco. A página Bancos de dados mostra quanto está em uso. Com 500 MB, as gravações são interrompidas até você apagar linhas.
- Os backups do servidor não incluem o banco de dados. Exporte-o do phpMyAdmin de vez em quando.
Erros e o que significam
Estes são os erros que o aiomysql deu no nosso teste, escritos do jeito que terminam um traceback no console (eles vêm do PyMySQL, que o aiomysql usa por baixo):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): valor errado de DB_USER ou DB_PASSWORD. Depois de Redefinir senha no cartão Conexão, a senha antiga para de funcionar na hora: troque DB_PASSWORD e reinicie o bot.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): host ou porta errados, ou uma conexão de fora do PlayNHost. O banco só aceita conexões dos servidores PlayNHost e do phpMyAdmin, então é isso também que você recebe no seu computador.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): conexões abertas demais, veja os limites acima. Por exemplo, uma segunda cópia do bot ou um pool aberto para cada mensagem.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): uma coluna de ID é INT. Mude-a para BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): a tabela não existe. O código acima a cria ao iniciar; se você a criou à mão, confira o nome dela.
Uma variável ausente em Ambiente interrompe o bot antes de ele conectar, com KeyError: 'DB_HOST' ou o nome da que faltar, porque o código lê cada valor com os.environ[...]. Adicione-a e reinicie o bot: as variáveis valem a partir da próxima inicialização.
Um token errado aparece depois da parte do banco. Com o nosso token falso contra o Telegram de verdade, o bot imprimiu Database ready, depois parou com aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Copie o token de novo do @BotFather e cole-o na página Bot, seção Telegram. Mais erros do Telegram: Como corrigir o erro 409 Conflict do bot do Telegram.
Quanta memória ele usa
No nosso teste, o bot em execução usou 174 MB de memória (residente) depois de responder aos 8 comandos, com o pool aberto, em um contêiner limitado a 512 MB como o nosso menor plano de bot. Quase tudo isso é o próprio aiogram: no nosso teste de hospedagem anterior, só o aiogram mediu 173 MB (aquele bot parou em um token recusado, antes de tratar qualquer mensagem, então os dois números são próximos, mas não é uma comparação exata). Instalar aiogram e aiomysql com pip do zero chegou ao pico de 163 MB, incluindo o cache de arquivos. Isso deixa espaço para o seu código no menor plano. O guia de hospedagem compara as bibliotecas: python-telegram-bot vs aiogram na PlayNHost.
Precisa de um servidor para o seu bot do Telegram com banco de dados incluído? Veja os planos de bot para Telegram.