Telegraff: Kotlin DSL para Telegram

Logotipo


En Habr茅, miles de art铆culos sobre c贸mo hacer un bot de Telegram para diferentes lenguajes y plataformas de programaci贸n. El tema est谩 lejos de ser nuevo.


Pero Telegraff es el mejor marco para implementar bots de Telegram, y lo probar茅 por debajo.


Pre谩mbulo


En 2015, el rublo ruso ten铆a fiebre. Ten铆a ahorros en d贸lares y revis茅 la tasa literalmente cada cinco minutos para vender la moneda a la tasa que necesitaba. La fiebre se prolong贸, me cans茅 y escrib铆 un bot de Telegram ( @TinkoffRatesBot ), que le notifica si el tipo de cambio alcanza el valor umbral (esperado).
Me conmovi贸 mucho esta tarea. Botha escribi贸 bastante r谩pido, pero no recibi贸 satisfacci贸n.


La integraci贸n con Telegram no es y no hubo problemas. Este problema se resuelve en un par de horas. Incluso me sorprende que haya bibliotecas enteras en Java (subjetivamente, con un c贸digo de calidad repugnante) para la integraci贸n con Telegrams, que han ganado m谩s de mil estrellas en Github.


El principal desaf铆o para m铆 fue el sistema de secuencias de comandos: el usuario llama a un comando, por ejemplo, "/ taxi", el bot le hace una serie de preguntas, cada respuesta se valida y puede afectar el orden de las preguntas posteriores, se forma el "formulario" habitual, dado el m茅todo de procesamiento final para la formaci贸n respuesta
Hice esto, pero la estructura de las clases, los niveles de abstracci贸n, todo era tan heterog茅neo que era amargo verlo. Me atorment贸 la pregunta: 驴c贸mo se puede transferir de manera sucinta y org谩nica a un modelo orientado a objetos?


Quer铆a tener algo simple, conveniente y lo m谩s importante: poder describir el script completo en un archivo aislado para no tener que ver la mitad del proyecto para comprender la cadena de interacci贸n del usuario.


No quiere decir que el problema fuera muy agudo, porque la tarea ya est谩 resuelta. M谩s bien, a veces pensaba en 茅l. La idea era Groovy DSL, pero cuando lleg贸 Kotlin, la elecci贸n se hizo evidente. Entonces apareci贸 Telegraff.


S铆, por supuesto, no hab铆a competencia que ganar铆a Telegraff. Y la afirmaci贸n de que Telegraff es el mejor no debe tomarse literalmente. Pero Telegraff es un enfoque nuevo y 煤nico para este desaf铆o. Es f谩cil ser el mejor, ser el 煤nico.


驴C贸mo usarlo?


Dependencias


El primer paso es especificar un repositorio adicional para las dependencias. Quiz谩s en alg煤n momento publique Telegraff en Maven Central o en JCenter, pero por ahora.


Gradle
repositories { maven { url "https://dl.bintray.com/ruslanys/maven" } } 

Maven
 <repositories> <repository> <snapshots> <enabled>false</enabled> </snapshots> <id>bintray-ruslanys-maven</id> <name>bintray</name> <url>https://dl.bintray.com/ruslanys/maven</url> </repository> </repositories> 

Sigue siendo el caso para los peque帽os. Para usar Telegraff, debe especificar solo una dependencia de arranque por arranque de resorte:


Gradle
 compile("me.ruslanys.telegraff:telegraff-starter:1.0.0") 

Maven
 <dependency> <groupId>me.ruslanys.telegraff</groupId> <artifactId>telegraff-starter</artifactId> <version>1.0.0</version> </dependency> 

Configuracion


La configuraci贸n del proyecto es simple y puede limitarse a los primeros dos o tres par谩metros:


application.properties
 telegram.access-key=123 # 鈶 telegram.mode=webhook # 鈶 telegram.webhook-base-url=https://ruslanys.me # 鈶 telegram.webhook-endpoint-url=/telegram # 鈶 telegram.handlers-path=handlers # 鈶 telegram.unresolved-filter.enabled=false # 鈶 

  1. Tu clave para la API de Telegram.
  2. El modo de recibir mensajes (actualizaciones) de Telegram. Puede ser sondeo o webhook.
  3. Si el m茅todo para recibir actualizaciones se indica mediante "webhook", debe especificar la ruta a su aplicaci贸n.
  4. Si lo desea, puede especificar su propia ruta al punto final. Si este par谩metro no se redefine, se generar谩 una ruta de la siguiente forma: /telegram/${UUID} . Antes de iniciar la aplicaci贸n, la direcci贸n especificada se establece como la direcci贸n de enlace web. Al final del trabajo, la direcci贸n de enlace web se sobrescribe para poder cambiar a sondeo la pr贸xima vez que comience.
  5. Si lo desea, puede cambiar la carpeta en la que se ubicar谩n los scripts de los controladores. Por defecto, esta es la carpeta de handlers .
  6. UnresolvedFilter est谩 incluido en la "entrega" y est谩 habilitado de forma predeterminada. En el caso de que no se encuentre ning煤n controlador en el mensaje del usuario, UnresolvedFilter responde con algo como "Lo siento, no te entiendo :(".

隆Es hora de escribir guiones!


Manipuladores


Los controladores (scripts) son una parte clave de Telegraff. Aqu铆 es donde se establece la cadena de interacci贸n del usuario. La conclusi贸n es que cada comando, como "/ start", "/ taxi", "/ help", es un script / script / handler / handler separado.


Un script puede contener un conjunto de pasos (preguntas) que un usuario debe seguir para ejecutar un comando. En otras palabras, el usuario debe completar el formulario. Y dado que el messenger es de la interfaz, debe hablar y preguntarle al usuario.


驴Debo explicar que las respuestas de los usuarios deben validarse? Lo primero que har谩 el usuario es que responder谩 de manera diferente a lo que usted espera.


Bueno, al final, el script puede ramificarse, es decir Cada respuesta a una pregunta puede afectar el orden de las siguientes.


Por ejemplo!


Para comenzar, coloque el archivo con la extensi贸n .kts en la carpeta con handlers recursos: src/main/resources/handlers/ExampleHandler.kts .


Escenario de llamada de taxi
 enum class PaymentMethod { CARD, CASH } handler("/taxi", "") { // 鈶 step<String>("locationFrom") { // 鈶 question { // 鈶 MarkdownMessage(" ?") } } step<String>("locationTo") { question { MarkdownMessage(" ?") } } step<PaymentMethod>("paymentMethod") { question { state -> MarkdownMessage("   ?", "", "") // 鈶 } validation { // 鈶 when (it.toLowerCase()) { "" -> PaymentMethod.CARD "" -> PaymentMethod.CASH else -> throw ValidationException(",    ") // 鈶 } } next { state -> null // 鈶 } } process { state, answers -> // 鈶 val from = answers["locationFrom"] as String val to = answers["locationTo"] as String val paymentMethod = answers["paymentMethod"] as PaymentMethod // 鈶 // Business logic MarkdownMessage("""     #${state.chat.id}.   $from  $to.  $paymentMethod. """.trimIndent()) // 鈶 } } 

Las llaves de las estepas no fueron deliberadamente tomadas en constantes. En producci贸n, por supuesto, es mejor evitarlo.


Vamos a resolverlo:


  1. Declaramos el gui贸n. Se requiere al menos un nombre de equipo. En este caso, hay dos equipos: "/ taxi", "taxi". Si el mensaje del usuario comienza con estas palabras, se llamar谩 al controlador correspondiente.
  2. Determinamos los pasos (preguntas). Se requiere un nombre de paso 煤nico porque posteriormente, se puede acceder a la respuesta del usuario precisamente por esta clave ("locationFrom").
  3. Cada paso contiene tres secciones, la primera de las cuales es la pregunta en s铆. La pregunta es una secci贸n obligatoria que debe estar presente en cada paso. No tiene sentido en un paso sin una pregunta.
  4. Puede completar la pregunta como desee. En este caso, se le pedir谩 al usuario a trav茅s del teclado que seleccione una de las opciones: "Tarjeta" o "Efectivo". Como resultado de llamar a este bloque, debe haber un objeto de tipo TelegramSendRequest . Lo sentimos, no se me ocurri贸 nada mejor que el sufijo SendRequest , que describe la estructura como una solicitud saliente en Telegram.
    Estructura de clase
  5. El segundo paso m谩s importante es verificar la respuesta del usuario. El tipo de cada paso est谩 parametrizado (gen茅rico) y, por lo tanto, el bloque de validaci贸n debe devolver exactamente el tipo por el cual se parametriza su paso.
  6. Si la respuesta del usuario no es satisfactoria, puede lanzar una ValidationException con texto aclaratorio, pero el mismo teclado, si se indic贸 en la pregunta.
  7. La secci贸n del paso final es un bloque que indica el siguiente paso. Por defecto, los pasos se ejecutar谩n en el orden de su declaraci贸n, de arriba a abajo. Pero este proceso puede verse influenciado al anular el bloque correspondiente. La clave del siguiente paso ( String ) o "nulo" se puede devolver como resultado de la ejecuci贸n de este bloque, lo que indica que no hay m谩s pasos y que es hora de proceder a la ejecuci贸n del comando.
  8. Cuando se genera una solicitud de usuario, se requiere su procesamiento. Los argumentos en el lambda son State (esto es algo as铆 como una sesi贸n) y las respuestas del usuario.
  9. Tenga en cuenta que la respuesta fallida ya no es la cadena de respuesta del usuario, sino un objeto ya procesado del tipo deseado.
  10. La respuesta al comando puede ser cualquiera, similar al p谩rrafo 4. Si no se requiere la respuesta al comando, puede devolver "nulo".

Un controlador puede no tener pasos en absoluto. En este caso, solo necesita determinar el comportamiento del controlador para invocar el comando.


Gui贸n de bienvenida
 handler("/start") { process { _, _ -> MarkdownMessage("!") } } 

Prueba


Para intentarlo, bifurca el repositorio , cl贸nalo en la m谩quina local y ve a la carpeta de telegraff-sample . Configurar, iniciar, tocar!


En general, telegraff-sample es un proyecto deliberadamente independiente que no est谩 relacionado con el padre e incluso tiene su propio Gradle Wrapper. Solo puede dejar esta carpeta. Este es un tipo de arquetipo.


Como funciona


Telegrama


La integraci贸n con Telegram es muy simple e implementada en TelegramApi .


Cada m茅todo se implement贸 deliberadamente individualmente debido a una serie de circunstancias: comenzando por el uso de Spring's RestTemplate (y las pruebas para ello), hasta la especificidad de la API de Telegram.


Como puede ver en la configuraci贸n, hay dos tipos de clientes de esta API en Telegraff: PollingClient , WebhookClient . Dependiendo de la configuraci贸n, se declarar谩 un bin particular.


Y aunque los m茅todos para recibir actualizaciones (mensajes nuevos) difieren de Telegram, la esencia no cambia y se reduce a una sola cosa: publicar un evento ( TelegramUpdateEvent ) sobre mensajes nuevos a trav茅s de EventPublisher de Spring (patr贸n "Observador"). Si lo desea, puede implementar su propio oyente suscribi茅ndose a este tipo de evento. Una l贸gica, como me parece, una capa de abstracci贸n, porque no importa en absoluto c贸mo se recibi贸 el mensaje.


Filtros


Tan pronto como se reciba un nuevo mensaje, es necesario procesarlo y responder al usuario. Para hacer esto, el mensaje debe pasar por la cadena de filtros.


Esto es similar a los filtros Java EE familiares para los programadores Java. La 煤nica diferencia es que los llamados controladores (si dibujamos un paralelo con Java EE, estos son Servlets) no son independientes de los filtros, sino que son parte de ellos.


Cadena de filtro


Entonces, los filtros est谩n optimizados y pueden permitir que los mensajes vayan m谩s abajo en la cadena, tal vez no.


LoggingFilter es obviamente el filtro de prioridad m谩s alta (primero) que se llamar谩 como parte del procesamiento de un nuevo mensaje. Registra informaci贸n en un mensaje entrante y lo env铆a m谩s abajo en la cadena. A prop贸sito agregu茅 LoggingFilter como ejemplo. De hecho, puede no tener sentido, porque Los mensajes entrantes se registran a nivel del cliente.


El siguiente filtro es CancelFilter . B谩sicamente funciona en conjunto con HandlersFilter y es un complemento para 茅l. Su tarea es simple: si el usuario desea abandonar la secuencia de comandos actual, puede escribir "/ cancelar" o "cancelar" y su estado (sesi贸n) debe borrarse. Puede comenzar cualquier nuevo escenario sin completar el anterior. Por esta raz贸n, CancelFilter "mayor" (prioridad) HandlersFilter .


HandlersFilter es el filtro principal en el proceso actual. Es este filtro el que almacena el estado de los chats del usuario, encuentra y llama al controlador (script) deseado, aplica bloques de validaci贸n, determina el orden de los pasos y responde al usuario.


Si HandlersFilter no encontr贸 ning煤n controlador adecuado para el mensaje del usuario, ya sea en la sesi贸n o en el contenido, el mensaje se env铆a m谩s abajo en la cadena. El filtro extremo es UnresolvedFilter . Este es un filtro que sabe que es el 煤ltimo, por lo tanto, su funcionalidad es simple: si me contactaron, la forma de responder a un mensaje no est谩 clara, dir茅 que no entend铆 nada. Me parece que es mejor recibir al menos algunos mensajes del bot si no sabe c贸mo responder, que no recibir nada en absoluto.


Para agregar su filtro, debe declarar un Bean de la clase TelegramFilter y especificar la anotaci贸n @TelegramFilterOrder(ORDER_NUMBER) .


Ejemplo de filtro
 @Component @TelegramFilterOrder(Integer.MIN_VALUE) class LoggingFilter : TelegramFilter { override fun handleMessage(message: TelegramMessage, chain: TelegramFilterChain) { log.info("New message from #{}: {}", message.chat.id, message.text) chain.doFilter(message) } companion object { private val log = LoggerFactory.getLogger(LoggingFilter::class.java) } } 

As铆 es como @TinkoffRatesBot implementa una "calculadora". Sin llamar a ning煤n script y comando, puede enviar un n煤mero, por ejemplo, "1000", o incluso una expresi贸n completa, por ejemplo, "4500 * 3 - 12000". El bot calcular谩 el resultado de la expresi贸n, aplicar谩 los tipos de cambio actuales al resultado y mostrar谩 informaci贸n al respecto. De hecho, el resultado de tales acciones es la ejecuci贸n de CalculationFilter , que se encuentra en la cadena debajo de HandlersFilter , pero por encima de UnresolvedFilter .


Manipuladores


El sistema de secuencias de comandos de Telegraff (controladores) se basa en el DSL de Kotlin. En resumen, se trata de lambdas y de constructores.


No veo el punto de ver por separado el DSL Kotlin, porque Esta es una conversaci贸n completamente diferente. Hay una gran documentaci贸n de JetBrains y un informe completo de i_osipov .


Matices


Esta secci贸n est谩 dedicada a las caracter铆sticas actuales. Todos ellos, en mi opini贸n, no son cr铆ticos, algunos pueden ser reparados, otros no. Pero necesitas saber sobre estos aspectos.


Si desea participar o sabe c贸mo corregir uno u otro punto de esta secci贸n, se lo agradecer茅.


Telegrama


La capa de integraci贸n con Telegram probablemente no se describe completamente. Solo se implementaron los m茅todos que necesitaba. Si hay algo que personalmente le falta, corrija TelegramApi y env铆e PR.


Una de las partes importantes en este momento es la falta de soporte de teclado en l铆nea (esto es cuando el teclado est谩 directamente debajo del mensaje en la cinta de opciones). La tarea se ve agravada por el hecho de que los teclados en l铆nea deben ser "ingresados" correctamente en la estructura existente para que siga siendo simple, conveniente, aislada. Ya existe una buena idea para implementar esta funcionalidad, pero a煤n no se ha implementado y probado de ninguna forma.


Tarro de grasa


Desafortunadamente, algunas bibliotecas, como JRuby y probablemente el Kotlin Embedded Compiler (necesario para compilar scripts) pueden tener problemas como parte del Fat JAR . Fat JAR es cuando su c贸digo y todas sus dependencias se empaquetan en un archivo ( *.jar ).


Para resolver este problema, puede desempaquetar dependencias en tiempo de ejecuci贸n. Es decir, cuando se inicia la aplicaci贸n, el JAR de dependencia del paquete principal se implementa en alg煤n lugar del disco y la ruta de clase se indica antes. Esto es bastante f谩cil de hacer a trav茅s de la configuraci贸n de bootJar :


Configuraci贸n del complemento
 bootJar { requiresUnpack "**/**kotlin**.jar" requiresUnpack "**/**telegraff**.jar" } 

Sin embargo, para referirse de los controladores (scripts) a sus beans (servicios, por ejemplo), tambi茅n deben estar desempaquetados. Lo cual, en principio, elimina el beneficio de este enfoque.


Tal como lo veo, el uso del complemento de application Gradle sigue siendo el m茅todo m谩s confiable, simple y conveniente. Adem谩s, si est谩 conteniendo su aplicaci贸n en contenedores, no hay diferencia por el resultado.


Sobre todo esto escrib铆 en detalle aqu铆 .


Orden de inicializaci贸n


Aqu铆 me gustar铆a se帽alar dos circunstancias.


En primer lugar, si observa el escenario de la llamada de taxi, puede ver que la clase de enum se define sobre la llamada al handler(...) . Esta necesidad se impone por el hecho de que, de hecho, el handler es una llamada a la funci贸n. Una llamada a funci贸n, cuyo resultado deber铆a ser alguna estructura, que Telegraff usar谩 m谩s adelante. Si, de acuerdo con el resultado de la ejecuci贸n de su script, la f谩brica no puede llevar el resultado al tipo deseado, se producir谩 un error en la etapa de inicializaci贸n.


En segundo lugar, debe recordar que sus scripts se pueden inicializar antes que toda su aplicaci贸n y beans. Si, por ejemplo, coloca un enlace a un contexto en una variable est谩tica e intenta obtener alg煤n servicio en la primera l铆nea del archivo de script, puede resultar que el contexto no lo tenga, porque a煤n no se ha inicializado. Para evitar tales problemas, use este m茅todo de Telegraff. Asegura que el contexto se inicialice y que todos los beans necesarios est茅n disponibles. Un ejemplo se puede ver aqu铆 .


Conclusi贸n


Quer铆a probar - tenedor,
Quer铆a arreglarlo: enviar relaciones p煤blicas,
Quer铆a agradecer: 隆pon un asterisco en Github, como la publicaci贸n y cu茅ntaselo a tus amigos!


Repositorio de proyectos

Source: https://habr.com/ru/post/445072/


All Articles