Telegram

Escopo: JavaScript/Node.js. Guia baseado em Telegraf v4.16 com Node.js 22. Conceitos fundamentais (webhook, polling, command routing, rate limit): shared/platform/bots.md. Primitivas Telegram (BotFather, Inline Keyboard, tipos de chat): shared/platform/bots-advanced.md.

Telegraf é o framework Node.js para a Bot API (Interface de Programação para Bots) do Telegram. Usa arquitetura de middleware encadeado: cada update passa por uma pilha de funções antes de chegar ao Handler (tratador) final. A biblioteca chama o parâmetro de contexto de ctx por convenção, mas este guia usa context para seguir o code style do projeto.

Conceitos fundamentais

ConceitoO que é
Telegraf (framework de bot)Instância principal do bot; gerencia polling, middleware e roteamento
Context (contexto da requisição)Objeto com o evento atual e todos os métodos de resposta; passado a cada Handler
Middleware (componente de pipeline)Função (context, next) => void encadeada antes ou depois dos Handlers
message filter (filtro de mensagem)Função importada de telegraf/filters para filtrar eventos por tipo de conteúdo
InlineKeyboard (teclado embutido)Objeto Markup.inlineKeyboard para construir botões renderizados abaixo de uma mensagem
callback_query (consulta de retorno)Evento disparado quando o usuário clica em um botão inline; deve ser respondido com answerCbQuery

Instalação

npm install telegraf

Setup do Bot

import { Telegraf } from 'telegraf';

const bot = new Telegraf(process.env.TELEGRAM_TOKEN);

bot.start((context) => context.reply('Olá! Use /help para ver os comandos disponíveis.'));

bot.launch();

process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));

bot.launch() inicia polling por padrão. Para webhook, use bot.createWebhook() (ver seção Webhook em Produção).

Command Router

❌ Ruim: ctx abreviado; lógica de negócio dentro do handler; sem separação
bot.command('order', async (ctx) => {
  const parts = ctx.message.text.split(' ');
  const orderId = parts[1];
  const order = await db.findOrder(orderId);
  await ctx.reply(`Pedido ${order.id}: ${order.status}`);
});
✅ Bom: context sem abreviação; handlers importados; router só delega
import { helpCommand } from './commands/help.js';
import { orderCommand } from './commands/order.js';
import { statusCommand } from './commands/status.js';

bot.command('help', helpCommand);
bot.command('order', orderCommand);
bot.command('status', statusCommand);

Filtrando por Tipo de Mensagem

Use o message filter de telegraf/filters para reagir a tipos específicos de conteúdo sem verificações manuais.

❌ Ruim: ctx abreviado; verificação manual de tipo; compute e format misturados no argumento
bot.on('message', (ctx) => {
  if (ctx.message.sticker) {
    ctx.reply('Sticker recebido!');
  }
  if (ctx.message.photo) {
    ctx.reply('Foto recebida!');
  }
  if (ctx.message.text) {
    ctx.reply(`Texto: ${ctx.message.text}`);
  }
});
✅ Bom: message filter declarativo; compute extraído; format separado do argumento
import { message } from 'telegraf/filters';

bot.on(message('sticker'), (context) => context.reply('Sticker recebido!'));
bot.on(message('photo'), (context) => context.reply('Foto recebida!'));
bot.on(message('text'), (context) => {
  const userText = context.message.text;
  const echoText = `Texto: ${userText}`;
  context.reply(echoText);
});

Implementando um Command

❌ Ruim: ctx abreviado; property access direto no argumento; format inline na chamada
export async function orderCommand(ctx) {
  const orderId = ctx.message.text.split(' ')[1];
  const order = await db.findOrder(orderId);
  await ctx.reply(`Pedido #${order.id}, Status: ${order.status}, Cliente: ${order.customerName}`);
}
✅ Bom: compute extraído antes do argumento; guard clause; format em função separada
export async function orderCommand(context) {
  const messageText = context.message.text;
  const orderId = extractOrderId(messageText);

  if (!orderId) {
    await context.reply('Informe o ID do pedido. Exemplo: /order 12345');
    return;
  }

  const order = await fetchOrder(orderId);
  const summary = buildOrderSummary(order);
  await context.reply(summary);
}

function extractOrderId(messageText) {
  const parts = messageText.split(' ');
  const orderId = parts[1] ?? null;
  return orderId;
}

async function fetchOrder(orderId) {
  const order = await orderRepository.findById(orderId);
  return order;
}

function buildOrderSummary(order) {
  const summary = `Pedido #${order.id}\nStatus: ${order.status}\nCliente: ${order.customerName}`;
  return summary;
}

Inline Keyboard e Callbacks

Botões inline enviam um callback_query silencioso ao bot. Sempre chame answerCbQuery ao final. Sem isso, o Telegram exibe o indicador de carregamento no botão indefinidamente.

❌ Ruim: ctx abreviado; format inline no argumento; sem answerCbQuery
export async function orderCommand(ctx) {
  const orderId = ctx.message.text.split(' ')[1];
  const keyboard = Markup.inlineKeyboard([
    Markup.button.callback('Cancelar pedido', `cancel_${orderId}`),
  ]);
  await ctx.reply(`Pedido #${orderId} encontrado.`, keyboard);
}

bot.action(/^cancel_(.+)$/, async (ctx) => {
  const orderId = ctx.match[1];
  await cancelOrder(orderId);
  await ctx.editMessageText(`Pedido #${orderId} cancelado.`);
});
✅ Bom: compute extraído; format nomeado antes do reply; answerCbQuery antes do editMessageText
import { Markup } from 'telegraf';

export async function orderCommand(context) {
  const messageText = context.message.text;
  const orderId = extractOrderId(messageText);

  if (!orderId) {
    await context.reply('Informe o ID do pedido. Exemplo: /order 12345');
    return;
  }

  const keyboard = Markup.inlineKeyboard([
    Markup.button.callback('Cancelar pedido', `cancel_${orderId}`),
    Markup.button.callback('Ver detalhes', `details_${orderId}`),
  ]);
  const replyText = `Pedido #${orderId} encontrado.`;

  await context.reply(replyText, keyboard);
}

bot.action(/^cancel_(.+)$/, async (context) => {
  const orderId = context.match[1];

  await cancelOrder(orderId);

  const cancelMessage = `Pedido #${orderId} cancelado.`;

  await context.answerCbQuery('Pedido cancelado.');
  await context.editMessageText(cancelMessage);
});

Webhook em Produção

Use bot.createWebhook() com secretToken para validar que os updates vêm do Telegram.

❌ Ruim: webhookCallback deprecado no v4.16; sem secretToken; sem shutdown limpo
import express from 'express';

const app = express();

app.use(bot.webhookCallback('/webhook'));
bot.telegram.setWebhook(`${process.env.WEBHOOK_URL}/webhook`);

app.listen(3000);
✅ Bom: bot.createWebhook() com secretToken; await extraído do argumento; shutdown limpo
import { createServer } from 'http';

const webhookOptions = {
  domain: process.env.WEBHOOK_DOMAIN,
  path: '/webhook',
  secretToken: process.env.WEBHOOK_SECRET,
};

const webhookHandler = await bot.createWebhook(webhookOptions);
const server = createServer(webhookHandler);

server.listen(3000);

process.once('SIGINT', () => {
  server.close();
  bot.stop('SIGINT');
});
process.once('SIGTERM', () => {
  server.close();
  bot.stop('SIGTERM');
});

Veja também

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.