Telegraff: DSL Kotlin para telegrama

Logomarca


No Habré, milhares de artigos sobre como fazer um bot do Telegram para diferentes linguagens e plataformas de programação. O tópico está longe de ser novo.


Mas o Telegraff é a melhor estrutura para implementar os bots do Telegram, e vou provar isso por baixo.


Preâmbulo


Em 2015, o rublo russo estava com febre. Economizei dólares e verifiquei a taxa literalmente a cada cinco minutos para vender a moeda na taxa que eu precisava. A febre se arrastou, eu me cansei e escrevi para o bot do Telegram ( @TinkoffRatesBot ), que o notifica se a taxa de câmbio atingir o valor limite (esperado).
Fiquei muito emocionado com esta tarefa. Botha escreveu muito rapidamente, mas ele não recebeu satisfação.


A integração com o Telegram não é e não houve problemas. Esse problema foi resolvido em algumas horas. E até me surpreendo que existam bibliotecas inteiras em Java (subjetivamente, com código nojento em qualidade) para integração com os Telegrams, que ganharam mais de mil estrelas no Github.


O principal desafio para mim foi o sistema de script: o usuário chama um comando, por exemplo, "/ taxi", o bot faz uma série de perguntas, cada resposta é validada e pode afetar a ordem das perguntas subsequentes, a "forma" usual é formada, dada ao método de processamento final para a formação resposta.
Eu fiz isso, mas a estrutura das classes, os níveis de abstração, era tudo tão heterogêneo que era amargo de se olhar. Fiquei atormentado com a pergunta: como isso pode ser sucinta e organicamente transferido para um modelo orientado a objetos?


Eu queria ter algo simples, conveniente e mais importante - para poder descrever o script inteiro em um arquivo isolado, para não precisar visualizar metade do projeto para entender a cadeia de interação do usuário.


Para não dizer que o problema foi muito agudo, porque a tarefa já foi resolvida. Em vez disso, às vezes eu pensava nele. O pensamento era Groovy DSL, mas quando Kotlin chegou, a escolha se tornou óbvia. Então Telegraff apareceu.


Sim, claro, não havia competição que a Telegraff vencesse. E a alegação de que Telegraff é o melhor não deve ser tomada literalmente. Mas o Telegraff é uma abordagem nova e única para esse desafio. É fácil ser o melhor, sendo o único.


Como usá-lo?


Dependências


A primeira etapa é especificar um repositório adicional para dependências. Talvez em algum momento eu publiquei o Telegraff no Maven Central ou no JCenter, mas por enquanto.


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> 

Continua sendo o caso dos pequenos. Para usar o Telegraff, é necessário especificar apenas uma dependência do iniciador de inicialização por mola:


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> 

Configuração


A configuração do projeto é simples e pode ser limitada aos dois ou três primeiros 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. Sua chave para a API do Telegram.
  2. O modo de receber mensagens (atualizações) do Telegram. Pode ser polling ou webhook.
  3. Se o método para receber atualizações for indicado por "webhook", você deverá especificar o caminho para o seu aplicativo.
  4. Se desejar, você pode especificar seu próprio caminho para o terminal. Se esse parâmetro não for redefinido, será gerado um caminho com o seguinte formulário: /telegram/${UUID} . Antes de iniciar o aplicativo, o endereço especificado é definido como o endereço do gancho da web. No final do trabalho, o endereço do gancho da Web é substituído para poder mudar para a pesquisa na próxima vez em que for iniciado.
  5. Se desejar, você pode alterar a pasta na qual os scripts dos manipuladores serão localizados. Por padrão, esta é a pasta de handlers .
  6. UnresolvedFilter está incluído na "entrega" e está ativado por padrão. Caso nenhum manipulador tenha sido encontrado na mensagem do usuário, o UnresolvedFilter responde com algo como "Desculpe, eu não entendo você :(".

É hora de escrever scripts!


Manipuladores


Manipuladores (scripts) são uma parte essencial do Telegraff. É aqui que a cadeia de interação do usuário é definida. A linha inferior é que cada comando, como “/ start”, “/ taxi”, “/ help”, é um script / script / manipulador / manipulador separado.


Um script pode conter um conjunto de etapas (perguntas) pelas quais um usuário precisa executar para executar um comando. Em outras palavras, o usuário deve preencher o formulário. E como o messenger é da interface, você precisa conversar e perguntar ao usuário.


Preciso explicar que as respostas do usuário precisam ser validadas? A primeira coisa que o usuário fará é que ele responda de maneira diferente do que você espera.


Bem, no final, o script pode ramificar, ou seja, Cada resposta a uma pergunta pode afetar a ordem das seguintes.


Por exemplo!


Para iniciar, coloque o arquivo com a extensão .kts na pasta com handlers recursos: src/main/resources/handlers/ExampleHandler.kts .


Cenário de chamada de táxi
 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()) // ⑩ } } 

As chaves das estepes não foram deliberadamente tomadas em constantes. Na produção, é claro, é melhor evitar isso.


Vamos descobrir isso:


  1. Declaramos o script. É necessário pelo menos um nome de equipe. Nesse caso, existem duas equipes: "/ taxi", "taxi". Se a mensagem do usuário começar com essas palavras, o manipulador correspondente será chamado.
  2. Nós determinamos as etapas (perguntas). Um nome de etapa exclusivo é necessário porque posteriormente, a resposta do usuário pode ser acessada com precisão por essa chave ("locationFrom").
  3. Cada etapa contém três seções, a primeira das quais é a própria pergunta. A questão é uma seção obrigatória que deve estar presente em todas as etapas. Não há sentido em uma etapa sem uma pergunta.
  4. Você pode preencher a pergunta como desejar. Nesse caso, o usuário será solicitado, através do teclado, a selecionar uma das opções: "Cartão" ou "Dinheiro". Como resultado da chamada desse bloco, deve haver um objeto do tipo TelegramSendRequest . Desculpe, não consegui encontrar nada melhor que o sufixo SendRequest , que descreve a estrutura como uma solicitação de saída no Telegram.
    Estrutura de classe
  5. A segunda seção da etapa mais importante é verificar a resposta do usuário. O tipo de cada etapa é parametrizado (genérico) e, portanto, o bloco de validação deve retornar exatamente o tipo pelo qual sua etapa é parametrizada.
  6. Se a resposta do usuário for insatisfatória, você poderá lançar uma ValidationException com texto esclarecedor, mas com o mesmo teclado, se indicado na pergunta.
  7. A seção da etapa final é um bloco que indica a próxima etapa. Por padrão, as etapas serão executadas na ordem de sua declaração, de cima para baixo. Mas esse processo pode ser influenciado substituindo o bloco correspondente. A chave da próxima etapa ( String ) ou “null” pode ser retornada como resultado da execução deste bloco, indicando que não há mais etapas e é hora de prosseguir com a execução do comando.
  8. Quando uma solicitação do usuário é gerada, seu processamento é necessário. Os argumentos no lambda são State (isso é algo como uma sessão) e respostas do usuário.
  9. Observe que a resposta com falha não é mais a sequência de respostas do usuário, mas um objeto já processado do tipo desejado.
  10. A resposta ao comando pode ser qualquer uma, semelhante ao parágrafo 4. Se a resposta ao comando não for necessária, você poderá retornar "nulo".

Um manipulador pode não ter etapas. Nesse caso, você só precisa determinar o comportamento do manipulador para chamar o comando.


Script de boas-vindas
 handler("/start") { process { _, _ -> MarkdownMessage("!") } } 

Experimente


Para tentar, bifurque o repositório , clone-o na máquina local e vá para a pasta telegraff-sample . Configure, inicie, toque!


Em geral, telegraff-sample é um projeto deliberadamente independente, que não está relacionado ao pai e tem seu próprio Gradle Wrapper. Você pode deixar apenas esta pasta. Este é um tipo de arquétipo.


Como isso funciona?


Telegram


A integração com o Telegram é muito simples e implementada no TelegramApi .


Cada método foi deliberadamente implementado individualmente devido a várias circunstâncias: a partir do uso do RestTemplate da Spring (e testes para ele), até a especificidade da API do Telegram.


Como você pode ver na configuração, existem dois tipos de clientes dessa API no Telegraff: PollingClient , WebhookClient . Dependendo da configuração, uma posição específica será declarada.


E embora os métodos para receber atualizações (novas mensagens) sejam diferentes do Telegram, a essência permanece inalterada e resume-se a uma coisa: publicar um evento ( TelegramUpdateEvent ) sobre novas mensagens no EventPublisher da Spring (padrão "Observer"). Se desejar, você pode implementar seu próprio ouvinte assinando esse tipo de evento. Uma camada lógica, como me parece, de abstração, porque absolutamente não importa como a mensagem foi recebida.


Filtros


Assim que uma nova mensagem for recebida, é necessário processá-la e responder ao usuário. Para fazer isso, a mensagem precisa passar pela cadeia de filtros.


Isso é semelhante aos filtros Java EE familiares aos programadores Java. A única diferença é que os chamados Manipuladores (se traçarmos um paralelo com o Java EE, estes são Servlets) não são independentes dos filtros, mas fazem parte deles.


Cadeia de filtro


Portanto, os filtros são otimizados e podem deixar as mensagens irem mais longe na cadeia, talvez não.


LoggingFilter é obviamente o filtro de prioridade mais alta (primeiro) que será chamado como parte do processamento de uma nova mensagem. Registra as informações em uma mensagem recebida e as envia mais adiante na cadeia. Eu propositadamente adicionei o LoggingFilter como exemplo. De fato, pode não fazer sentido, porque As mensagens recebidas são registradas no nível do cliente.


O próximo filtro é CancelFilter . Funciona essencialmente em conjunto com o HandlersFilter e é um complemento para ele. Sua tarefa é simples: se o usuário deseja abandonar o script atual, ele pode escrever "/ cancel" ou "cancel" e seu Status (sessão) deve ser limpo. Ele pode iniciar qualquer novo cenário sem concluir o anterior. Por esse motivo, o CancelFilter " CancelFilter " mais alto (prioritário).


HandlersFilter é o principal filtro no processo atual. É esse filtro que armazena o estado dos bate-papos do usuário, encontra e chama o manipulador (script) desejado, aplica blocos de validação, determina a ordem das etapas e responde ao usuário.


Se o HandlersFilter não encontrou nenhum manipulador adequado para a mensagem do usuário, na sessão ou no conteúdo, a mensagem é enviada mais adiante na cadeia. O filtro extremo é UnresolvedFilter . Este é um filtro que sabe que é o último, portanto, sua funcionalidade é simples: se eles chegaram até mim, como responder a uma mensagem não está claro, direi que não entendi nada. Parece-me que é melhor receber pelo menos algumas mensagens do bot se não souber responder, do que não receber nada.


Para adicionar seu filtro, você precisa declarar um Bean da classe TelegramFilter e especificar a anotação @TelegramFilterOrder(ORDER_NUMBER) .


Exemplo 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) } } 

É assim que o @TinkoffRatesBot implementa uma “calculadora”. Sem chamar nenhum script e comando, você pode enviar um número, por exemplo, "1000" ou até uma expressão inteira, por exemplo, "4500 * 3 - 12000". O bot calculará o resultado da expressão, aplicará as taxas de câmbio atuais ao resultado e exibirá informações sobre ele. De fato, o resultado de tais ações é a execução do CalculationFilter , que está na cadeia abaixo do HandlersFilter , mas acima do UnresolvedFilter .


Manipuladores


O sistema de script Telegraff (manipuladores) é construído no DSL do Kotlin. Em resumo, trata-se de lambdas e construtores.


Não vejo o objetivo de visualizar separadamente o DSL Kotlin, porque essa é uma conversa completamente diferente. Há uma excelente documentação do JetBrains e um relatório abrangente do i_osipov .


Nuances


Esta seção é dedicada aos recursos atuais. Todos eles, na minha opinião, não são críticos, alguns podem ser corrigidos, outros não. Mas você precisa saber sobre esses aspectos.


Se você deseja participar ou conhece como corrigir um ou outro ponto desta seção, ficarei muito agradecido.


Telegram


A camada de integração com o Telegram provavelmente não está totalmente descrita. Apenas os métodos que eu precisava foram implementados. Se houver algo que lhe falte pessoalmente, corrija o TelegramApi e envie o PR!


Uma das partes importantes no momento é a falta de suporte ao teclado embutido (é quando o teclado está diretamente abaixo da mensagem na faixa de opções). A tarefa é agravada pelo fato de que os teclados em linha precisam ser corretamente "inseridos" na estrutura existente, para que ela permaneça simples, conveniente e isolada. Já existe uma boa idéia para implementar essa funcionalidade, mas ainda não foi implementada e testada de nenhuma forma.


Frasco de gordura


Infelizmente, algumas bibliotecas, como JRuby e provavelmente o Kotlin Embedded Compiler (necessário para compilar scripts), podem ter problemas como parte do Fat JAR . Fat JAR é quando seu código e todas as suas dependências são compactados em um arquivo ( *.jar ).


Para resolver esse problema, você pode descompactar dependências em tempo de execução. Ou seja, quando o aplicativo é iniciado, a dependência JAR do pacote principal é implementada em algum lugar do disco e o caminho de classe é indicado antes dele. Isso é muito fácil de fazer através da configuração bootJar :


Configuração de plugins
 bootJar { requiresUnpack "**/**kotlin**.jar" requiresUnpack "**/**telegraff**.jar" } 

No entanto, para se referir dos manipuladores (scripts) aos seus beans (serviços, por exemplo), eles também devem ser descompactados. O que, em princípio, elimina os benefícios dessa abordagem.


A meu ver, o uso do plugin do application Gradle continua sendo o método mais confiável, simples e conveniente. Além disso, se você estiver continhando seu aplicativo, não há diferença no resultado.


Sobre tudo isso, escrevi com alguns detalhes aqui .


Ordem de inicialização


Aqui eu gostaria de observar duas circunstâncias.


Primeiro, se você observar o cenário de chamada de táxi, poderá ver que a classe enum está definida acima da chamada para o handler(...) . Essa necessidade é imposta pelo fato de que, de fato, handler é uma chamada de função. Uma chamada de função, cujo resultado deve ser alguma estrutura, que o Telegraff usará mais tarde. Se, de acordo com o resultado da execução do seu script, a fábrica não puder trazer o resultado para o tipo desejado, ocorrerá um erro no estágio de inicialização.


Em segundo lugar, é necessário lembrar que seus scripts podem ser inicializados antes de todo o aplicativo e beans. Se, por exemplo, colocarmos um link para o contexto em uma variável estática e tentarmos obter algum serviço na primeira linha do arquivo de script, pode acontecer que o contexto não o tenha, porque ainda não foi inicializado. Para evitar esses problemas, use este método Telegraff. Ele garante que o contexto seja inicializado e que todos os beans necessários estejam disponíveis. Um exemplo pode ser visto aqui .


Conclusão


Eu queria tentar - garfo,
Eu queria corrigi-lo - enviar PR,
Queria agradecer - coloque um asterisco no Github, curta o post e conte para seus amigos!


Repositório do projeto

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


All Articles