Desarrollo de bots
Crear un bot
Crea un bot con @BotFather, recibe mensajes y respóndelos, y llega hasta los botones, los comandos, las fotos y los grupos: un tutorial paso a paso de principio a fin.
Al terminar este tutorial tendrás un bot que responde a los mensajes, lleva botones, maneja un menú de comandos y fotos, y también trabaja en grupos. Necesitas la aplicación Pabal, un ordenador donde ejecutar el programa del bot y Python 3.10 o posterior, o bien Node.js 18 o posterior.
Cómo funciona un bot
Un bot de Pabal es una cuenta controlada por un programa. Los mensajes que se envían al bot se acumulan en su buzón de mensajes; el programa del bot le pregunta al servidor «¿ha llegado algo nuevo?» (getUpdates), recoge lo que haya y envía la respuesta (sendMessage). El programa del bot funciona fuera del servidor, en tu ordenador.
Explicación del diagrama
- Tres columnas: a la izquierda, la persona que usa la aplicación; en el centro, el servidor Pabal; a la derecha, tu programa de bot. Las líneas verticales punteadas indican el paso del tiempo.
- Las flechas grises son peticiones que hace primero el bot, y las azules, los mensajes que circulan como resultado. El programa del bot solo envía peticiones; el servidor no se pone en contacto con el bot por iniciativa propia (salvo que uses un webhook).
- La espera de ① (long polling) es la clave: con
timeout=30, si no hay novedades el servidor no devuelve enseguida una respuesta vacía, sino que retiene la petición hasta 30 segundos y responde (③) en el momento en que llega un mensaje (②). Así, la reacción es rápida y el número de peticiones, bajo. - El offset de ⑥ es la marca de «recibido»: si envías el último
update_idprocesado más 1, el servidor borra todo lo anterior. Si no subes el offset, seguirás recibiendo la misma actualización. - ⑤ sigue el mismo camino que un mensaje enviado por una persona: la respuesta del bot llega como push a todos los dispositivos de la aplicación y también sube en la lista de chats.
1. Crear un bot con @BotFather
Los bots se crean conversando con @BotFather dentro de la aplicación Pabal. BotFather es un bot que viene incluido en el servidor Pabal.
- Escribe
BotFatheren el buscador de la aplicación y abre BotFather. Al pulsar Iniciar te llega la lista de comandos. - Envía
/newbot. - Envía el nombre del bot. Es el nombre que se ve en la lista de chats, así que puede llevar tildes, eñes u otros alfabetos.
- Envía el nombre de usuario del bot. Tiene entre 5 y 32 caracteres (letras latinas, números y guion bajo), empieza por una letra y tiene que terminar en
bot. - Cuando llegue la respuesta con el token, habrás terminado. Copia el token y guárdalo.
Por ahora, BotFather responde en coreano. En la conversación de arriba, primero te pide el nombre del bot (el que se verá en la lista de chats), después te pide que elijas su nombre de usuario y, por último, te confirma que el bot se ha creado, te entrega su token (la línea 100003:…) y te recuerda que lo guardes con el mismo cuidado que una contraseña.
| Comando de BotFather | Qué hace |
|---|---|
/newbot | Crear un bot nuevo (nombre → nombre de usuario → token) |
/mybots | Lista de los bots que has creado |
/token | Volver a ver el token de un bot |
/revoke | Emitir un token nuevo: el anterior deja de valer al instante y las conexiones abiertas con él también se cortan |
/setcommands | Configurar el menú de comandos (una línea comando - descripción por comando) |
/deletebot | Eliminar un bot: se confirma enviando 네, 삭제합니다 («Sí, elimínalo»). El nombre de usuario queda libre para volver a usarse |
/cancel | Cancelar la operación en curso |
Si añades el nombre de usuario, como en /token @hello_test_bot, te saltas el paso de «¿Qué bot?».
2. Manejar el token
El token tiene la forma <ID del bot>:<secreto>. El número de delante es el ID de usuario del bot, y lo que va detrás es el secreto. Con un token se controla el bot por completo, así que trátalo como una contraseña.
- No lo escribas en el código: guárdalo en una variable de entorno (
BOT_TOKEN) o en un gestor de secretos. No lo subas a repositorios públicos. - Si se ha filtrado, envía
/revokea BotFather. El token anterior se rechaza al instante (401 Unauthorized) y las sesiones del bot conectadas a MTProto con él también se cortan. - El token va dentro de la dirección (URL), así que evita que tu programa de bot registre en los logs las direcciones de las peticiones. El servidor Pabal tampoco registra las direcciones de la Bot API.
3. Primera petición: getMe
La dirección de todas las peticiones es https://pabal.me/bot<token>/<método>. Comprobemos con getMe que el token es correcto.
export BOT_TOKEN='100003:AbCdEf…'
curl -s "https://pabal.me/bot$BOT_TOKEN/getMe"# pip install requests
import os
import requests
r = requests.get(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/getMe", timeout=10)
print(r.json())// Node.js 18 o posterior: fetch viene incluido
const res = await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/getMe`);
console.log(await res.json());Si todo va bien, recibirás esto. Todas las respuestas son JSON con ok y result (en caso de éxito) o error_code y description (en caso de error).
{
"ok": true,
"result": {
"id": 100003,
"is_bot": true,
"first_name": "Hola Bot",
"username": "hello_test_bot",
"can_join_groups": true,
"can_read_all_group_messages": true,
"supports_inline_queries": false,
"can_connect_to_business": false,
"has_main_web_app": false
}
}
Si el token es incorrecto, recibirás un HTTP 401 con {"ok": false, "error_code": 401, "description": "Unauthorized"}.
4. Recibir mensajes: getUpdates
Abre el bot en la aplicación, pulsa Iniciar o envíale cualquier cosa, y después recoge las novedades.
curl -s "https://pabal.me/bot$BOT_TOKEN/getUpdates?timeout=30"
{
"ok": true,
"result": [
{
"update_id": 1,
"message": {
"message_id": 1,
"from": { "id": 100001, "is_bot": false, "first_name": "Ana" },
"chat": { "id": 100001, "first_name": "Ana", "type": "private" },
"date": 1789805661,
"text": "/start",
"entities": [ { "type": "bot_command", "offset": 0, "length": 6 } ]
}
}
]
}
- update_id: un número que aumenta en 1 con cada actualización. Después de procesarla, si en la siguiente petición pasas
offset=update_id+1, todo lo anterior se borra como «recibido». - timeout: los segundos que se espera cuando no hay novedades (0–50). Con 0 se devuelve enseguida una lista vacía. Se recomienda entre 25 y 30.
- chat.id: adónde enviar la respuesta. En un chat uno a uno es el ID de la persona (positivo); en un grupo, un número negativo.
- Las actualizaciones que puedes recibir son de tres tipos:
message(mensaje nuevo),edited_message(mensaje editado) ycallback_query(pulsación de un botón).
Las actualizaciones que no se recogen se acumulan en la memoria del servidor, hasta las 1.000 más recientes por bot. Si el servidor se reinicia, lo que no se haya recogido se pierde (los mensajes en sí siguen en el chat). No dejes el bot apagado mucho tiempo.
5. Enviar una respuesta: sendMessage
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" \
-H 'Content-Type: application/json' \
-d '{"chat_id": 100001, "text": "¡Hola!"}'requests.post(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/sendMessage",
json={"chat_id": 100001, "text": "¡Hola!"}, timeout=10)await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: 100001, text: '¡Hola!' }),
});Los parámetros se pueden enviar como prefieras: en un cuerpo JSON, como formulario (application/x-www-form-urlencoded), como multipart/form-data cuando subes archivos o en la cadena de consulta tras la dirección. El resultado es el mensaje enviado (Message).
Un bot solo puede escribir a una persona después de que esa persona le haya enviado al menos un mensaje. Si no, obtendrás 403 Forbidden: bot can't initiate conversation with a user. Es la misma regla que en Telegram.
6. Un bot que repite lo que le dicen
Repitiendo recibir y enviar ya tienes un bot. Este es el código completo, sin bibliotecas.
# echo.py — pip install requests
# Ejecución: BOT_TOKEN='100003:…' python3 echo.py
import os
import requests
API = f"https://pabal.me/bot{os.environ['BOT_TOKEN']}"
def call(method, **params):
r = requests.post(f"{API}/{method}", json=params, timeout=60)
data = r.json()
if not data["ok"]:
raise RuntimeError(f"{method}: {data['description']}")
return data["result"]
offset = 0
print("El bot está en marcha. Pulsa Ctrl+C para detenerlo")
while True:
for update in call("getUpdates", offset=offset, timeout=30):
offset = update["update_id"] + 1 # marca de «recibido»
message = update.get("message")
if message and "text" in message:
call("sendMessage", chat_id=message["chat"]["id"], text=message["text"])// echo.mjs — Node.js 18 o posterior, sin bibliotecas
// Ejecución: BOT_TOKEN='100003:…' node echo.mjs
const API = `https://pabal.me/bot${process.env.BOT_TOKEN}`;
async function call(method, params = {}) {
const res = await fetch(`${API}/${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
const data = await res.json();
if (!data.ok) throw new Error(`${method}: ${data.description}`);
return data.result;
}
let offset = 0;
console.log('El bot está en marcha. Pulsa Ctrl+C para detenerlo');
for (;;) {
const updates = await call('getUpdates', { offset, timeout: 30 });
for (const update of updates) {
offset = update.update_id + 1; // marca de «recibido»
const message = update.message;
if (message?.text) {
await call('sendMessage', { chat_id: message.chat.id, text: message.text });
}
}
}7. Crear el bot con una biblioteca
Las bibliotecas de bots para Telegram tienen una opción para cambiar la dirección del servidor. Con esa sola opción funcionan tal cual en Pabal. Para trasladar un bot hecho para Telegram, cambia solo eso y pide un token nuevo al BotFather de Pabal.
| Biblioteca | Qué cambiar | Versión comprobada |
|---|---|---|
| python-telegram-bot | .base_url("https://pabal.me/bot"), .base_file_url("https://pabal.me/file/bot") | 22.8 |
| aiogram | AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me")) | 3.31 |
| Sin biblioteca (HTTP) | El principio de la dirección: https://api.telegram.org → https://pabal.me | — |
# hello_bot.py — pip install python-telegram-bot
# Ejecución: BOT_TOKEN='100003:…' python3 hello_bot.py
import os
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler, ContextTypes,
MessageHandler, filters)
SERVER = "https://pabal.me"
def buttons():
return InlineKeyboardMarkup([[InlineKeyboardButton("👍 Me gusta", callback_data="like"),
InlineKeyboardButton("🔢 Sumar uno", callback_data="count")]])
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("¡Hola! Prueba a pulsar los botones.", reply_markup=buttons())
async def button(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
if query.data == "like":
await query.answer("¡Gracias!") # texto que aparece un momento en la pantalla de quien pulsó
else:
n = context.chat_data.get("n", 0) + 1
context.chat_data["n"] = n
await query.answer() # primero respondes
await query.edit_message_text(f"Número: {n}", reply_markup=buttons()) # y luego editas el mensaje
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(f"Has dicho «{update.message.text}».")
def main():
app = (Application.builder().token(os.environ["BOT_TOKEN"])
.base_url(f"{SERVER}/bot") # Pabal en lugar de api.telegram.org
.base_file_url(f"{SERVER}/file/bot")
.build())
app.add_handler(CommandHandler("start", start))
app.add_handler(CallbackQueryHandler(button))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
print("El bot está en marcha. Pulsa Ctrl+C para detenerlo")
app.run_polling()
if __name__ == "__main__":
main()# echo_aiogram.py — pip install aiogram
# Ejecución: BOT_TOKEN='100003:…' python3 echo_aiogram.py
import asyncio
import os
from aiogram import Bot, Dispatcher
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.filters import CommandStart
dp = Dispatcher()
@dp.message(CommandStart())
async def start(message):
await message.answer("¡Hola! Envíame lo que quieras.")
@dp.message()
async def echo(message):
if message.text:
await message.answer(message.text)
async def main():
session = AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me")) # Pabal
bot = Bot(os.environ["BOT_TOKEN"], session=session)
print("El bot está en marcha. Pulsa Ctrl+C para detenerlo")
await dp.start_polling(bot)
asyncio.run(main())8. Botones y callbacks
Para añadir botones inline a un mensaje, pasa en reply_markup un inline_keyboard (un array de filas; cada fila es un array de botones). Hay dos tipos de botón: los de callback_data, que avisan al bot cuando se pulsan, y los de url, que abren un enlace.
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -H 'Content-Type: application/json' -d '{
"chat_id": 100001,
"text": "Elige una opción",
"reply_markup": {
"inline_keyboard": [
[ {"text": "👍 Me gusta", "callback_data": "like"}, {"text": "👎 No mucho", "callback_data": "dislike"} ],
[ {"text": "Abrir la documentación de Pabal", "url": "https://pabal.me/docs/"} ]
]
}
}'
Cuando alguien pulsa un botón de callback_data, el bot recibe una actualización callback_query.
{
"update_id": 7,
"callback_query": {
"id": "5812039457730125441",
"from": { "id": 100001, "is_bot": false, "first_name": "Ana" },
"message": { "message_id": 4, "chat": { "id": 100001, "type": "private", "first_name": "Ana" }, "text": "Elige una opción", … },
"chat_instance": "8413962145072395171",
"data": "like"
}
}
El bot debe responder con answerCallbackQuery en menos de 10 segundos. Mientras tanto, la aplicación muestra un reloj en el botón y espera.
# Texto que aparece un momento sobre la pantalla (con show_alert: true, un cuadro de confirmación)
curl -s "https://pabal.me/bot$BOT_TOKEN/answerCallbackQuery" -H 'Content-Type: application/json' \
-d '{"callback_query_id": "5812039457730125441", "text": "¡Gracias!"}'
# Cambiar el texto y los botones del mensaje pulsado
curl -s "https://pabal.me/bot$BOT_TOKEN/editMessageText" -H 'Content-Type: application/json' \
-d '{"chat_id": 100001, "message_id": 4, "text": "Has pulsado Me gusta 👍"}'
callback_dataocupa entre 1 y 64 bytes. Un mensaje puede llevar como máximo 100 botones.- Si no respondes al callback, la aplicación deja de esperar a los 10 segundos. Si el bot está apagado, el servidor termina la espera enseguida.
- Solo vale la primera respuesta. Algunas bibliotecas envían primero una respuesta vacía al editar el mensaje, así que envía primero la respuesta con texto.
- Si editas con el mismo texto y los mismos botones, obtendrás
400 Bad Request: message is not modified(igual que en Telegram).
Teclado bajo el campo de texto
Si pasas keyboard, en lugar del campo de texto aparece un panel de botones, y al pulsar uno su texto se envía como mensaje. Con remove_keyboard se oculta y con force_reply se activa el modo de respuesta.
{
"chat_id": 100001,
"text": "¿Cuál prefieres?",
"reply_markup": {
"keyboard": [ [ {"text": "Sí"}, {"text": "No"} ], [ {"text": "Enviar mi ubicación", "request_location": true} ] ],
"resize_keyboard": true,
"one_time_keyboard": true
}
}
9. Menú de comandos
Es la lista que aparece al pulsar / en el chat o el botón Menú. Se define por código o con /setcommands de BotFather.
curl -s "https://pabal.me/bot$BOT_TOKEN/setMyCommands" -H 'Content-Type: application/json' -d '{
"commands": [
{"command": "start", "description": "Empezar"},
{"command": "help", "description": "Ayuda"}
]
}'
Un comando tiene entre 1 y 32 caracteres (letras latinas minúsculas, números y guion bajo), la descripción entre 1 y 256 caracteres, y puede haber como máximo 100 comandos. Si pasas language_code, se guarda una lista aparte para cada idioma, pero por ahora la aplicación solo muestra la lista predeterminada, la definida sin código de idioma. Los comandos que envía una persona, como /start, llegan marcados como bot_command en las entities del mensaje.
10. Enviar y recibir fotos
Enviar
Sube el archivo como multipart/form-data o reutiliza el file_id de una foto recibida antes. Las fotos pueden ocupar hasta 10 MB y el pie de foto (caption) hasta 1.024 caracteres. El envío mediante URL todavía no está disponible.
curl -s "https://pabal.me/bot$BOT_TOKEN/sendPhoto" \
-F chat_id=100001 -F caption='La foto de hoy' -F photo=@sunset.jpgwith open("sunset.jpg", "rb") as f:
requests.post(f"{API}/sendPhoto", data={"chat_id": 100001, "caption": "La foto de hoy"},
files={"photo": f}, timeout=60)Recibir
Las fotos que envía una persona llegan en el campo photo del mensaje (una lista por tamaños; en Pabal, un único elemento con el original). Obtén la ruta con getFile y descárgala.
# 1) file_id → file_path
curl -s "https://pabal.me/bot$BOT_TOKEN/getFile?file_id=AQAAAAAAAAB7…"
# {"ok":true,"result":{"file_id":"AQAA…","file_unique_id":"AQAA…","file_size":48213,"file_path":"photos/AQAA….jpg"}}
# 2) Descargar: la dirección lleva /file/
curl -s -o photo.jpg "https://pabal.me/file/bot$BOT_TOKEN/photos/AQAA….jpg"
11. En grupos
- Al crear un grupo en la aplicación, o desde la información del grupo → Añadir miembros, busca el nombre de usuario del bot y añádelo.
- Un bot que está en un grupo recibe todos los mensajes del grupo (como con el «modo de privacidad desactivado» de Telegram).
chat.typees"group"ychat.ides negativo. - Si haces
sendMessagecon esechat.id, el mensaje se envía al grupo. Los botones, las fotos y las ediciones funcionan igual que en un chat uno a uno. - Si el bot sale del grupo, los envíos a ese grupo devuelven
403 Forbidden: bot is not a member of the group chat.
12. Cambiar a webhook
Si tu bot funciona en un servidor con una dirección HTTPS pública, en lugar de preguntar con getUpdates puedes hacer que el servidor Pabal envíe las novedades a esa dirección.
curl -s "https://pabal.me/bot$BOT_TOKEN/setWebhook" -H 'Content-Type: application/json' \
-d '{"url": "https://bot.example.com/pabal-webhook", "secret_token": "cadena-larga-aleatoria"}'
La configuración, la verificación, los reintentos y cómo responder dentro de la propia respuesta se explican en el documento Webhooks.
Bots que se conectan por MTProto (Telethon)
Un bot también puede conectarse por MTProto, como la aplicación. Es cómodo si ya usas herramientas para cuentas de persona (Telethon, por ejemplo). Como es la misma cuenta de bot, puedes combinarlo con HTTP.
# pip install telethon==1.42.0 ← usa la 1.42 (explicación más abajo)
# Clave pública del servidor: descarga https://pabal.me/docs/server-key.pem en la misma carpeta
import asyncio
import os
from telethon import TelegramClient, events
from telethon.crypto import rsa
from telethon.sessions import StringSession
rsa.add_key(open("server-key.pem").read(), old=False) # clave pública del servidor Pabal
client = TelegramClient(StringSession(), api_id=1, api_hash="0" * 32)
client.session.set_dc(2, "122.34.175.215", 8443)
@client.on(events.NewMessage(incoming=True))
async def echo(event):
await event.reply(event.raw_text)
async def main():
await client.start(bot_token=os.environ["BOT_TOKEN"]) # auth.importBotAuthorization
print("El bot está en marcha. Pulsa Ctrl+C para detenerlo")
await client.run_until_disconnected()
asyncio.run(main())
- Usa Telethon 1.42. Pabal habla la capa 216, y las versiones más recientes de Telethon intentan leer las respuestas con una capa superior, así que fallan ya en el inicio de sesión (
TypeNotFoundError). - Pabal no comprueba
api_idniapi_hash, así que sirve cualquier valor. - A los bots MTProto el servidor les envía los mensajes nuevos en tiempo real (no hacen falta getUpdates ni webhooks). Para responder a un callback, llama a
event.answer("…")antes que aevent.edit(…).
Reglas y límites
| Concepto | Valor |
|---|---|
| Caracteres por mensaje / pie de foto | 4.096 / 1.024 caracteres |
| Tamaño de foto (subida con sendPhoto) | 10 MB |
| Tamaño del cuerpo de la petición | 12 MB |
timeout · limit de getUpdates | 0–50 segundos · 1–100 elementos |
| Conservación de actualizaciones no recogidas | Las 1.000 más recientes por bot, en la memoria del servidor (se pierden al reiniciar) |
| Espera de la respuesta a un callback | 10 segundos |
callback_data · número de botones | 1–64 bytes · 100 por mensaje |
| Comandos | 1–32 caracteres (letras latinas minúsculas, números y guion bajo), descripción de 1–256 caracteres, 100 como máximo |
| Iniciar la conversación | No se puede: la persona tiene que escribir antes al bot |
Solución de problemas
| Síntoma | Causa y solución |
|---|---|
401 Unauthorized | El token es incorrecto o se cambió con /revoke. Envía /token a BotFather para comprobarlo. |
404 Not Found: method not found | Es un método que Pabal todavía no admite. Consulta la lista de métodos. |
403 Forbidden: bot can't initiate conversation with a user | Esa persona todavía no ha escrito nunca al bot. Pídele que abra el bot en la aplicación y pulse Iniciar. |
409 Conflict: can't use getUpdates method while webhook is active | Hay un webhook configurado. Llama a deleteWebhook o recibe por webhook. |
| No pasa nada al pulsar un botón | El bot está apagado o no ha llamado a answerCallbackQuery. |
| Recibo el mismo mensaje una y otra vez | No has subido el offset. Pasa en la siguiente petición el update_id + 1 que procesaste. |
| La negrita o los enlaces con formato no funcionan | parse_mode todavía no está disponible, así que el texto se envía tal cual. Los comandos, las @menciones, las URL y las #etiquetas se marcan automáticamente como pulsables. |
| El bot no dice nada en el grupo | El bot no es miembro del grupo. Añádelo desde la información del grupo → Añadir miembros. |