Introducción
En este tutorial, crearemos un Bot de Telegram utilizando Spring Boot. Un bot de Telegram es un programa automatizado que opera dentro de la plataforma de mensajería de Telegram. Utiliza la API de Bot de Telegram para interactuar con los usuarios y realizar diversas tareas. En lugar de interactuar directamente con la API, utilizaremos una librería de Java. Los bots nos ayudan a responder a los comandos de los usuarios, proporcionar información y realizar acciones automatizadas.
Creando un Bot de Telegram
Para empezar, necesitamos crear un nuevo bot en la plataforma de Telegram. Esto lo hacemos directamente utilizando la aplicación de mensajería de Telegram y buscando a BotFather en la barra de búsqueda. Una vez abierto, escribiremos el comando /newbot
para crear el bot y seguiremos las instrucciones de BotFather. Nos pedirá el nombre de usuario que queremos asignar al bot, que debe terminar con bot
según la política de Telegram.
Una vez creado el bot, BotFather generará un token que debemos guardar en un lugar seguro, ya que lo usaremos más adelante para configurar nuestra aplicación.
Configurando la Aplicación
Lo siguiente que necesitamos es un proyecto de Spring Boot donde queremos integrar el bot de Telegram. Vamos a modificar el archivo pom.xml
e incluir la librería telegrambots-spring-boot-starter y telegrambots-abilities:
<dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots-spring-boot-starter</artifactId>
<version>6.7.0</version>
</dependency>
<dependency>
<groupId>org.telegram</groupId>
<artifactId>telegrambots-abilities</artifactId>
<version>6.7.0</version>
</dependency>
En el fondo, el AbilityBot utiliza webhooks para comunicarse con las APIs de Telegram, pero no necesitamos preocuparnos por eso. De hecho, la librería implementa todas las interfaces proporcionadas por la API de Bot de Telegram.
Ahora, podemos implementar nuestro bot.
Explicando el PizzaBot
Implementaremos un bot simple que simula una pizzería para demostrar el uso de la librería con Spring Boot. Tendremos un conjunto de interacciones predefinidas con el bot.
En resumen, comenzaremos pidiendo al usuario su nombre. Luego, le pediremos que seleccione una pizza o una bebida. En el caso de las bebidas, mostraremos un mensaje diciendo que no vendemos bebidas. De lo contrario, preguntaremos por los ingredientes de la pizza. Después de seleccionar los ingredientes disponibles, confirmaremos si el usuario quiere hacer un pedido nuevamente. En este caso, repetiremos el flujo. Alternativamente, les agradeceremos y cerraremos el chat con un mensaje de despedida.
Configurando y Registrando el PizzaBot
Empecemos configurando un AbilityBot para nuestra nueva Pizzería:
@Component
public class PizzaBot extends AbilityBot {
private final ResponseHandler responseHandler;
@Autowired
public PizzaBot(Environment env) {
super(env.getProperty("botToken"), "baeldungbot");
responseHandler = new ResponseHandler(silent, db);
}
@Override
public long creatorId() {
return 1L;
}
}
Leemos la propiedad botToken
inyectada como una variable de entorno dentro del constructor. Debemos mantener el token seguro y no subirlo en el código base. En este ejemplo, lo exportamos a nuestro entorno antes de ejecutar la aplicación. Alternativamente, podríamos definirlo dentro del archivo de propiedades. Además, debemos proporcionar un creatorId
único que describe nuestro bot.
También extendemos la clase AbilityBot
, que reduce el código redundante y ofrece herramientas comunes como máquinas de estado a través de ReplyFlow
. Sin embargo, solo utilizaremos la base de datos embebida y gestionaremos el estado explícitamente dentro de ResponseHandler
:
public class ResponseHandler {
private final SilentSender sender;
private final Map<Long, UserState> chatStates;
public ResponseHandler(SilentSender sender, DBContext db) {
this.sender = sender;
chatStates = db.getMap(Constants.CHAT_STATES);
}
}
Problema de Compatibilidad con Spring Boot 3
Al utilizar la versión 3 de Spring Boot, la librería no configura automáticamente el bot cuando se declara como @Component
. Por lo tanto, debemos inicializarlo manualmente dentro de nuestra clase de aplicación principal:
TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class);
botsApi.registerBot(ctx.getBean("pizzaBot", TelegramLongPollingBot.class));
Creamos una nueva instancia de TelegramBotsApi
y luego proporcionamos la instancia del componente pizzaBot
desde el contexto de la aplicación de Spring.
Implementando el PizzaBot
La API de Telegram es enorme y puede volverse repetitiva al implementar nuevos comandos. Por lo tanto, utilizamos habilidades para simplificar el proceso de desarrollo de nuevas capacidades. Debemos considerar al usuario final, las condiciones necesarias y el proceso de ejecución al diseñar el flujo de interacción.
En nuestro PizzaBot, utilizaremos una sola habilidad que activará el /start
de la conversación con el bot:
public Ability startBot() {
return Ability
.builder()
.name("start")
.info(Constants.START_DESCRIPTION)
.locality(USER)
.privacy(PUBLIC)
.action(ctx -> responseHandler.replyToStart(ctx.chatId()))
.build();
}
Utilizamos el patrón de diseño builder para construir la Ability
. Primero, definimos el nombre de la habilidad, que es el mismo que el comando de la habilidad. Segundo, proporcionamos una cadena de descripción de esta nueva habilidad. Esto será útil cuando ejecutemos el comando /commands
para obtener las habilidades de nuestro bot. Tercero, definimos la localidad y la privacidad del bot. Finalmente, definimos qué acción debemos tomar al recibir el comando. Para este ejemplo, avanzaremos el ID del chat a la clase ResponseHandler
. Siguiendo el diseño del diagrama anterior, preguntaremos por el nombre del usuario y lo guardaremos dentro de un mapa con su estado inicial:
public void replyToStart(long chatId) {
SendMessage message = new SendMessage();
message.setChatId(chatId);
message.setText(START_TEXT);
sender.execute(message);
chatStates.put(chatId, AWAITING_NAME);
}
En este método, creamos un comando de SendMessage
y lo ejecutamos utilizando el sender
. Luego, establecemos el estado del chat en AWAITING_NAME
, señalando que estamos esperando el nombre del usuario:
private void replyToName(long chatId, Message message) {
promptWithKeyboardForState(chatId, "Hola " + message.getText() + ". ¿Qué te gustaría pedir?",
KeyboardFactory.getPizzaOrDrinkKeyboard(),
UserState.FOOD_DRINK_SELECTION);
}
Después de que el usuario ingrese su nombre, le enviamos un ReplyKeyboardMarkup
que le presenta dos opciones:
public static ReplyKeyboard getPizzaToppingsKeyboard() {
KeyboardRow row = new KeyboardRow();
row.add("Margherita");
row.add("Pepperoni");
return new ReplyKeyboardMarkup(List.of(row));
}
Esto ocultará el teclado y mostrará al usuario una interfaz con dos botones.
Ahora, el usuario puede seleccionar una pizza o una bebida que nuestra pizzería no ofrece. Telegram enviará un mensaje de texto como respuesta al seleccionar cualquiera de las dos opciones.
Para todos los elementos verdes en forma de diamante en el diagrama anterior, seguimos un proceso similar. Por lo tanto, no repetiremos aquí. En su lugar, enfoquémonos en manejar la respuesta a los botones.
Manejo de las Respuestas del Usuario
Para todos los mensajes entrantes y el estado actual del chat, manejamos la respuesta de manera diferente dentro de nuestra clase PizzaBot
:
public Reply replyToButtons() {
BiConsumer<BaseAbilityBot, Update> action = (abilityBot, upd) -> responseHandler.replyToButtons(getChatId(upd), upd.getMessage());
return Reply.of(action, Flag.TEXT, upd -> responseHandler.userIsActive(getChatId(upd)));
}
El método .replyToButtons()
recibe todas las respuestas de tipo TEXT
y las reenvía a la ResponseHandler
junto con el chatId
y el objeto Message
entrante. Luego, dentro de ResponseHandler
, el método .replyToButtons()
decide cómo procesar el mensaje:
public void replyToButtons(long chatId, Message message) {
if (message.getText().equalsIgnoreCase("/stop")) {
stopChat(chatId);
}
switch (chatStates.get(chatId)) {
case AWAITING_NAME -> replyToName(chatId, message);
case FOOD_DRINK_SELECTION -> replyToFoodDrinkSelection(chatId, message);
case PIZZA_TOPPINGS -> replyToPizzaToppings(chatId, message);
case AWAITING_CONFIRMATION -> replyToOrder(chatId, message);
default -> unexpectedMessage(chatId);
}
}
Dentro del switch
, comprobamos el estado actual del chat y respondemos al usuario en consecuencia. Por ejemplo, cuando el estado actual para el usuario es FOOD_DRINK_SELECTION
, procesamos la respuesta y avanzamos al siguiente estado cuando el usuario toca la opción pizza
:
private void replyToFoodDrinkSelection(long chatId, Message message) {
SendMessage sendMessage = new SendMessage();
sendMessage.setChatId(chatId);
if ("drink".equalsIgnoreCase(message.getText())) {
sendMessage.setText("No vendemos bebidas.\n¡Trae tu propia bebida! :)");
sendMessage.setReplyMarkup(KeyboardFactory.getPizzaOrDrinkKeyboard());
sender.execute(sendMessage);
} else if ("pizza".equalsIgnoreCase(message.getText())) {
sendMessage.setText("Nos encanta la pizza aquí.\n¡Selecciona los ingredientes!");
sendMessage.setReplyMarkup(KeyboardFactory.getPizzaToppingsKeyboard());
sender.execute(sendMessage);
chatStates.put(chatId, UserState.PIZZA_TOPPINGS);
} else {
sendMessage.setText("No vendemos " + message.getText() + ". Por favor selecciona de las opciones a continuación.");
sendMessage.setReplyMarkup(KeyboardFactory.getPizzaOrDrinkKeyboard());
sender.execute(sendMessage);
}
}
Adicionalmente, dentro de .replyToButtons()
, comprobamos de inmediato si el usuario envió el comando /stop
. En ese caso, detenemos el chat y eliminamos el chatId
del mapa chatStates
:
private void stopChat(long chatId) {
SendMessage sendMessage = new SendMessage();
sendMessage.setChatId(chatId);
sendMessage.setText("Gracias por tu pedido. ¡Hasta la próxima!\nPresiona /start para pedir nuevamente.");
chatStates.remove(chatId);
sendMessage.setReplyMarkup(new ReplyKeyboardRemove(true));
sender.execute(sendMessage);
}
Esto elimina el estado del usuario de la base de datos. Para interactuar nuevamente, el usuario debe escribir el comando /start
.
Conclusión
En este tutorial, discutimos cómo implementar un bot de Telegram utilizando Spring Boot.
Primero, creamos un nuevo bot en la plataforma de Telegram usando BotFather. Segundo, configuramos nuestro proyecto de Spring Boot y explicamos la funcionalidad de nuestro PizzaBot y cómo interactúa con los usuarios. Luego, implementamos el PizzaBot usando habilidades para simplificar el desarrollo de nuevos comandos. Finalmente, manejamos las respuestas de los usuarios y proporcionamos respuestas adecuadas según el estado del chat.
Consejos Prácticos para Programadores
- Mantén siempre seguro tu botToken: Nunca lo incluyas en tu código fuente. Usa variables de entorno para mantenerlo seguro.
- Organiza tu código: Mantén una buena estructura de tu proyecto y utiliza patrones de diseño cuando sea apropiado.
- Haz uso de las habilidades: Las habilidades te ayudarán a simplificar la implementación de comandos y a mantener tu código limpio.
- Prueba tu bot: Realiza pruebas exhaustivas para asegurarte de que los usuarios tengan una experiencia fluida y sin errores.
Espero que este tutorial haya sido útil para que inicies tu viaje en la creación de bots con Java y Spring Boot. ¡Feliz codificación!