3. O código
Um pequeno bot de notas: /note buy milk salva uma nota, /notes mostra suas últimas cinco.
package.json
{
"name": "my-grammy-bot",
"private": true,
"main": "index.js",
"dependencies": {
"grammy": "^1.46.0",
"mysql2": "^3.24.6"
}
}
index.js
const { Bot } = require("grammy");
const mysql = require("mysql2/promise");
// One pool for the whole bot. The values come from the Environment pane.
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
connectionLimit: 5,
});
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN);
bot.command("note", async (ctx) => {
const text = ctx.match.trim();
if (!text) return ctx.reply("Write your note after the command, for example: /note buy milk");
if (text.length > 500) return ctx.reply("A note can be up to 500 characters.");
await pool.execute("INSERT INTO notes (user_id, text) VALUES (?, ?)", [ctx.from.id, text]);
await ctx.reply("Saved. Send /notes to see your last five.");
});
bot.command("notes", async (ctx) => {
const [rows] = await pool.execute(
"SELECT text FROM notes WHERE user_id = ? ORDER BY id DESC LIMIT 5",
[ctx.from.id],
);
if (rows.length === 0) return ctx.reply("You have no notes yet.");
await ctx.reply(rows.map((row) => `- ${row.text}`).join("\n"));
});
// Without this, an error in one command stops the whole bot.
bot.catch((err) => {
console.error(`Error while handling update ${err.ctx.update.update_id}:`, err.error);
});
async function main() {
await pool.query(
"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))",
);
console.log("Database ready");
// bot.start() only returns when the bot stops, so the ready line goes in onStart.
await bot.start({ onStart: (me) => console.log(`Logged in as @${me.username}`) });
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
O que ele faz: main() cria a tabela notes se ela não existir e imprime Database ready antes de bot.start() pedir mensagens ao Telegram. O pool é criado uma vez, no topo do arquivo, e todo comando o usa: pool.execute() pega uma conexão emprestada, executa a consulta e devolve a conexão. ctx.match é o texto depois do comando: o grammY remove os espaços na frente dele e mantém o resto como foi digitado, quebras de linha incluídas. bot.catch() importa aqui: a documentação do grammY diz que, sem um tratador de erros, um erro em um handler interrompe um bot iniciado com bot.start(), e um erro de banco de dados é exatamente esse tipo de erro.
Os marcadores ? enviam a nota ao banco separadamente do SQL, então uma nota como '); DROP TABLE notes; -- é guardada como texto simples. No nosso teste ela foi salva como a linha 2 e a tabela continuou lá. Nunca monte o SQL com uma string de template.
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 traz as faixas: 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 mysql2 deu o código de erro ER_WARN_DATA_OUT_OF_RANGE (errno 1264) com esta mensagem:
Out of range value for column 'user_id' at row 1
Uma coluna BIGINT o guardou, e -1001234567890 também. Use BIGINTsimples, não BIGINT UNSIGNED, para que os IDs de chat também caibam.
4. Execute
- Envie
index.js e package.json em Files. Não envie node_modules.
- Pressione Iniciar e abra Console. Ao iniciar, os pacotes de
package.jsonsão instalados e depois aparecem [PlayNHost] Starting your bot, Database ready e Logged in as @your_bot. Essa última linha muda o status em Visão geral para Online no Telegram.
- No Telegram, envie ao seu bot
/note buy milke depois /notes. Para ver as linhas, abra a página do banco e clique em Abrir no phpMyAdmin.
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, as mais recentes primeiro |
/notes de um segundo usuário | You have no notes yet. |
Muitas mensagens ao mesmo tempo
A documentação do grammY diz que bot.start() processa todas as atualizações em sequência: um comando termina antes de o próximo começar. No nosso teste, o bot manteve 1 conexão aberta depois de responder aos 8 comandos. connectionLimit: 5 importa quando o seu código executa consultas em paralelo, ou se você passar para o grammY runner, que processa as atualizações de forma concorrente: o pool nunca abre mais de 5 conexões, e uma consulta extra espera por uma livre.
Uma consulta que falha não derruba o bot
No nosso teste, apagamos a tabela com o bot rodando e enviamos /notes. bot.catch() imprimiu Error while handling update 500001: Error: Table '...notes' doesn't exist com code: 'ER_NO_SUCH_TABLE', o bot não enviou resposta a esse comando e continuou rodando: quando a tabela voltou, o próximo /note foi salvo. Quem enviou o comando não vê nada, então, se uma consulta pode falhar, envolva-a em try/catch no handler e responda com uma mensagem curta.
Uma noite calma não quebra o pool
Nosso servidor de banco fecha uma conexão que ficou ociosa por 8 horas. Um bot que não recebe mensagens por tanto tempo, de madrugada por exemplo, encontra as conexões do pool fechadas. O pool do mysql2 resolve isso sozinho. Testamos com o limite de ociosidade reduzido para 5 segundos: após 7 segundos de silêncio, o próximo /note foi salvo sem 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
ER_USER_LIMIT_REACHED. No nosso teste, um pool com connectionLimit: 30 falhou com esse erro quando 30 consultas rodaram ao mesmo tempo. Um pool de 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 depois de 120,03 segundos. As consultas de um bot normalmente levam milissegundos, então isso só atinge uma consulta descontrolada.
- 500 MB por banco de dados. A página Bancos de dados mostra quanto está em uso. Em 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 códigos de erro que o mysql2 deu no nosso teste:
ER_ACCESS_DENIED_ERROR ... Access denied for user: DB_USER ou DB_PASSWORDincorretos. Depois de Redefinir senha no cartão Conexão a senha antiga para de funcionar na hora: troque DB_PASSWORD e reinicie o bot.
ECONNREFUSED: nada aceitou a conexão nesse host e porta. Confira DB_HOST e DB_PORT. Um DB_HOST ausente também termina aqui, porque o mysql2 então se conecta a localhost, onde nenhum banco roda: a variável está faltando em Ambiente, ou o bot não foi reiniciado depois de adicioná-la.
ENOTFOUND: DB_HOST está escrito errado, então o nome não existe.
ETIMEDOUT: nada respondeu a tempo. Seja qual for a mensagem, uma conexão do seu próprio computador falha: o banco só aceita conexões de servidores da PlayNHost e do phpMyAdmin.
ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): conexões abertas demais, veja os limites acima. Por exemplo, uma segunda cópia do bot ou um pool criado para cada mensagem.
ER_WARN_DATA_OUT_OF_RANGE (errno 1264, Out of range value for column 'user_id' at row 1): uma coluna de ID é INT. Mude-a para BIGINT.
ER_NO_SUCH_TABLE (errno 1146): a tabela não existe. O código acima a cria na inicialização; se você a fez à mão, confira o nome.
Um token errado aparece depois da parte do banco. Com o nosso token falso contra o Telegram real, o bot imprimiu Database readye parou depois de cerca de um quarto de segundo com GrammyError: Call to 'deleteWebhook' failed! (401: Unauthorized). Copie o token de novo do @BotFather e cole na página Bot , painel 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 67,6 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. No nosso teste de hospedagem anterior, o grammY sozinho media 60 MB (aquele bot parou em um token recusado, antes de tratar qualquer mensagem), então a diferença, cerca de 8 MB, cobre o mysql2, o pool e os comandos tratados. Instalar grammY e mysql2 com npm do zero chegou a um pico de 140 MB, incluindo o cache de arquivos.
Precisa de um servidor para o seu bot do Telegram com banco de dados incluído? Veja os planos de bots do Telegram.