Crear un bot de Telegram con Micronaut

Compartir esta publicación

Telegram es una aplicación de mensajería instantánea muy conocida y utilizada en todo el mundo por millones de usuarios, por lo que no es necesario hacer una presentación. Hoy nos vamos a centrar en una característica especial: los bots, aplicaciones de terceros que se ejecutan dentro de Telegram. Los usuarios pueden interactuar con los bots a través de mensajes y comandos. Estas aplicaciones pueden tener múltiples usos: obtener información, comprar artículos, soporte de chat para comercio electrónico,etc. Y en este articulo vamos a crear un bot de Telegram con Micronaut

Creando un bot de Telegram con Micronaut

Para ver cómo funcionan, vamos a crear un bot de telegram con algunos comandos básicos por diversión. El proyecto será un bot que devuelva información sobre una población solicitada. Vamos a obtener estos datos de una API abierta de terceros. El servicio se creará utilizando el framework Micronaut para ver si se adapta a nuestras necesidades.

Para tener nuestro bot en funcionamiento necesitamos 2 piezas:

1. Una cuenta de tTlegram que será nuestro bot
2. Un webservice que interactúa con el bot

Veamos los pasos para crear todo el proyecto.

Crear el bot

Telegram ofrece un bot que crea nuevas cuentas conocido como BotFather

Si interactuamos con el botfather a través de vuestra cuenta de Telegram, escribiendo algunos comandos (/newbot para empezar), crearemos tu bot.

Screenshot 2021 01 28 at 10.37.41

BotFather creará un token de acceso que será necesario para interactuar con la API de Telegram.

Configurar el webservice

Para recibir las peticiones de nuestro bot, Telegram ofrece dos opciones:

1.- Long polling – nuestro webservice estará conectado a los servidores de Telegram todo el tiempo a la espera de una petición.

2.- Webhooks – Nuestro servicio tiene que proporcionar una API a la que Telegram llamará cada vez que haya un nuevo mensaje.

Vamos a utilizar webhooks, ya que es más sencillo y consume menos recursos (nuestro webservice está inactivo esperando nuevas conexiones, en lugar de tener una conexión abierta todo el tiempo).


curl -F "url=https://YOURDOMAIN.EXAMPLE/WEBHOOKLOCATION" https://api.telegram.org/bot/setWebhook

¡Atención!

  • La URL debe ser con https
  • El puerto sólo puede ser uno de estos: 443, 80, 88 or 8443
  • Por razones de seguridad, puede ser una buena idea permitir sólo las peticiones de las redes de Telegram.
  Empresas de desarrollo de software a medida: buenas prácticas

Nuestro bot ya está configurado y cualquier mensaje que le enviemos será recibido en nuestro webservice.

Cómo responder a las peticiones
Para contestar a nuestro bot con la respuesta, Telegram también nos da 2 opciones:

1-.  Contestar la respuesta en la misma petición

Screenshot 2021 01 28 at 10.41.04

2.- Responde a la petición con un HTTP 200 y envía una nueva petición a la API.

Screenshot 2021 01 28 at 11.33.18

Como nuestro webservice va a interactuar con terceros proveedores y no podemos saber cuánto tardará su respuesta, elegiremos la segunda opción: enviaremos un OK inmediato y enviaremos un POST a la API con la respuesta una vez tengamos los datos.

Crear el webservice

Para crear el webservice vamos a utilizar Micronaut. Es un framework JVM (puede estar escrito en Java, Kotlin o Groovy) que promete un bajo consumo de memoria y ofrece un servidor http no bloqueante lo que parece muy conveniente para nuestro caso de uso: necesitamos un framework ligero que responda el OK inmediatamente mientras se procesa la petición. Esta llamada asíncrona será manejada a través de RxJava ya que Micronaut soporta streams reactivos.

 

Con la herramienta de Micronaut Launch es muy fácil obtener un “esqueleto” para nuestra aplicación. Nos permitirá elegir el lenguaje, construir herramientas, testear el framework y los extras si son necesarios. 

Con el framework en marcha, creamos el controlador que gestionará las peticiones:


@Controller("/bot")
class BotController {
    @Post("/")
    fun index(@Body request: String): HttpResponse {
        processRequest(request)
        return HttpResponse.ok()
    }
}

* La anotación @Controller define el controlador con la ruta /bot
* @Post define el método para manejar Http POST como se espera.
* La petición se procesa de forma asíncrona y el 200 OK se responde inmediatamente.

  Entrevista con Paola Annis, experta en software ecológico

Para gestionar el protocolo de Telegram, vamos a utilizar una librería de código abierto que ya ha implementado todas las peticiones y respuestas de la API de Telegram. Muchas de ellas escritas en diferentes lenguajes están listadas por Telegram.

Implementar la lógica del bot

Para nuestras pruebas, vamos a utilizar los Datos Abiertos ofrecidos por el Instituto de Estadística de Cataluña para obtener información sobre ciudades, pueblos y poblaciones de Cataluña. Utilizando la API de búsqueda de población y la API del municipio en cifras podremos solicitar datos sobre una ciudad concreta y mostrarlos al usuario.

Los pasos serían que primero el usuario busca un nombre con /población XXX y el bot responderá con una lista de posibles nombres de ciudades con un enlace al lado. Si el usuario hace clic en cualquier enlace que confirme el nombre del lugar, se mostrará la información de la población.

Hasta aquí, ya tenemos que manejar 2 posibles peticiones, vamos a escribirlas.

1.- Obtener la lista de ciudades


{...}
    private fun processRequest(request: String) {
        val message: Message = BotUtils.parseUpdate(request).message()
        val chatId: Long = message.chat().id()
        val messageReceived = message.text()
        when (messageReceived.split(" ")[0]) {
            "/start" -> sendResponse(chatId, "This is the very beginning")
            "/town" -> getCityNamesList(messageReceived, chatId)
            else -> sendResponse(chatId, messageReceived.reversed())
        }
    }
{...}
  private fun getCityNamesList(messageReceived: String, chatId: Long) {
        val search = messageReceived.split(" ").drop(1).joinToString("%20")
        val body: Flowable
        try {
            val client = RxHttpClient.create(URL("https://api.idescat.cat"))
            val towns: HttpRequest = HttpRequest.GET("/pob/v1/sug.txt?p=q/$search;tipus/mun")
            body = client.retrieve(towns).doOnError { sendResponse(chatId, "Error. Please try another name") }
        } catch (e: Exception) {
            sendResponse(chatId, "Nothing found. Please try another name")
            return
        }
        body.subscribe {
            val split = it.split("\n")
            val eq = "These are the cities found:\n" + split.map { "${cleanName(it)} /${it.base64encode()}" }
                .joinToString("\n")
            sendResponse(chatId, eq)
        }
    }
  private fun sendResponse(chatId: Long, reply: String): SendResponse? {
        val bot = TelegramBot(TELEGRAM_TOKEN)
        return bot.execute(SendMessage(chatId, reply))
    }

El comando /town XX provocará una petición a la API externa para obtener la lista de ciudades. RxHttpClient creará un Flowable que eventualmente devolverá la respuesta de la llamada externa. Nosotros capturaremos ese evento porque nos hemos suscrito a él (body.subscribe). Haciendo esto, el proceso no esperará su respuesta y no bloqueará toda la petición. 

  Explaining BEMIT: ITCSS + BEM

La respuesta asíncrona se envía a través de la librería de Telegram que sólo necesita como configuración el token de acceso que BotFather nos ha creado. 

2.- Obtener la información de una ciudad determinada


private fun getCityInfo(chatId: Long, messageReceived: String) {
        val cityDecoded: String
        try {
            cityDecoded = messageReceived.replace("/", "").base64decode()
        } catch (e: Exception) {
            sendResponse(chatId, "invalid text")
            return
        }
        val client = RxHttpClient.create(URL("https://api.idescat.cat"))
        val towns: HttpRequest = HttpRequest.GET("/emex/v1/dades.json?i=f171,f36,f42&lang=en&id=${cityDecoded}")
        client.retrieve(towns).subscribe {
            val data = Gson().fromJson(it, Fitxes::class.java)
            val texts = data.fitxes.indicadors.i.map { "${it.c}: ${it.v.split(",").first()}" }
                .joinToString("\n")
            sendResponse(chatId, "Information about ${cleanName(cityDecoded)} \n${texts}")
        }
    }

/XXXX donde XXX es el nombre codificado en base64 provocará una petición a la API externa para obtener información de esa ciudad concreta. De nuevo creamos un flowable y nos suscribimos a él para obtener los resultados de forma asíncrona y enviarlos de vuelta a Telegram.

El bot está  funcionando

Aquí podemos ver una captura de pantalla del bot, buscando información sobre Barcelona. Como pista extra, cualquier comando no existente devolverá la cadena invertida.

bot is working

Conclusión

Ha sido un proyecto divertido para conocer el funcionamiento de los bots de Telegram y descubrir que existen infinitas posibilidades para este tipo de bots. El uso del framework Micronaut se adapta muy bien a este caso concreto, pero seguro que puede ser una herramienta a tener en cuenta para otros casos especialmente cuando se necesiten manejar procesos asíncronos.

Author

  • Oriol Saludes

    Experienced Full Stack Engineer with a demonstrated history of working in the information technology and services industry. Skilled in PHP, Spring Boot, Java, Kotlin, Domain-Driven Design (DDD), TDD and Front-end Development. Strong engineering professional with a Engineer's degree focused in Computer Engineering from Universitat Oberta de Catalunya (UOC).

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Suscríbete a nuestro boletín de noticias

Recibe actualizaciones de los últimos descubrimientos tecnológicos

¿Tienes un proyecto desafiante?

Podemos trabajar juntos

apiumhub software development projects barcelona
Secured By miniOrange