Telegraff: Kotlin DSL pour Telegram

Le logo


Sur Habré des milliers d'articles sur la façon de créer un bot Telegram pour différents langages de programmation et plates-formes. Le sujet est loin d'être nouveau.


Mais Telegraff est le meilleur cadre pour implémenter les robots Telegram, et je vais le prouver sous la coupe.


Préambule


En 2015, le rouble russe avait de la fièvre. J'ai fait des économies en dollars et j'ai vérifié le taux toutes les cinq minutes pour vendre la monnaie au taux dont j'avais besoin. La fièvre a traîné, je me suis fatigué et j'ai écrit un bot Telegram ( @TinkoffRatesBot ), qui vous avertit si le taux de change atteint la valeur seuil (attendue).
J'ai été très touché par cette tâche. Botha a écrit assez rapidement, mais il n'a pas été satisfait.


L'intégration avec Telegram ne l'est pas et il n'y a eu aucun problème. Ce problème est résolu en quelques heures. Et je suis même surpris qu'il y ait des bibliothèques entières en Java (subjectivement, avec du code dégoûtant en qualité) pour l'intégration avec Telegrams, qui ont gagné plus de mille étoiles sur Github.


Le principal défi pour moi était le système de script: l'utilisateur appelle une commande, par exemple, "/ taxi", le bot lui pose une série de questions, chaque réponse est validée et peut affecter l'ordre des questions suivantes, le "formulaire" habituel est formé, donné à la méthode de traitement final pour former réponse.
Je l'ai fait, mais la structure des classes, les niveaux d'abstraction, tout était si hétérogène que c'était amer à regarder. J'ai été tourmenté par la question: comment cela peut-il être transposé succinctement et organiquement à un modèle orienté objet?


Je voulais avoir quelque chose de simple, de pratique et, surtout, de pouvoir décrire l'intégralité du script dans un fichier isolé afin de ne pas avoir besoin de visualiser la moitié du projet pour comprendre la chaîne d'interaction utilisateur.


Pour ne pas dire que la question était très aiguë, car la tâche a déjà été résolue. Au contraire, parfois je pensais à lui. L'idée était Groovy DSL, mais quand Kotlin est arrivé, le choix est devenu évident. Telegraff est donc apparu.


Oui, bien sûr, il n'y avait aucune compétition que Telegraff gagnerait. Et l'affirmation selon laquelle Telegraff est le meilleur ne doit pas être prise au pied de la lettre. Mais Telegraff est une nouvelle approche unique à ce défi. Il est facile d'être le meilleur, étant le seul.


Comment l'utiliser?


Dépendances


La première étape consiste à spécifier un référentiel supplémentaire pour les dépendances. Peut-être qu'à un moment donné, je publierai Telegraff dans Maven Central ou dans JCenter, mais pour l'instant.


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> 

Cela reste le cas pour les petits. Pour utiliser Telegraff, vous devez spécifier une seule dépendance Spring-Boot-Starter:


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> 

La configuration


La configuration du projet est simple et peut se limiter aux deux ou trois premiers paramètres:


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. Votre clé pour l'API Telegram.
  2. Mode de réception des messages (mises à jour) de Telegram. Il peut s'agir d'un sondage ou d'un webhook.
  3. Si la méthode de réception des mises à jour est indiquée par «webhook», vous devez spécifier le chemin d'accès à votre application.
  4. Si vous le souhaitez, vous pouvez spécifier votre propre chemin vers le point de terminaison. Si ce paramètre n'est pas redéfini, un chemin de la forme suivante sera généré: /telegram/${UUID} . Avant de démarrer l'application, l'adresse spécifiée est définie en tant qu'adresse de raccordement Web. À la fin du travail, l'adresse du hook Web est écrasée pour pouvoir basculer en interrogation au prochain démarrage.
  5. Si vous le souhaitez, vous pouvez modifier le dossier dans lequel les scripts des gestionnaires seront situés. Par défaut, il s'agit du dossier des handlers .
  6. UnresolvedFilter est inclus dans la «livraison» et il est activé par défaut. Dans le cas où aucun gestionnaire n'a été trouvé sur le message de l'utilisateur, UnresolvedFilter répond avec quelque chose comme "Désolé, je ne vous comprends pas :(".

Il est temps d'écrire des scripts!


Gestionnaires


Les gestionnaires (scripts) sont un élément clé de Telegraff. C'est là que la chaîne d'interaction utilisateur est définie. En fin de compte, chaque commande, telle que «/ start», «/ taxi», «/ help», est un script / script / handler / handler distinct.


Un script peut contenir un ensemble d'étapes (questions) qu'un utilisateur doit parcourir pour exécuter une commande. En d'autres termes, l'utilisateur doit remplir le formulaire. Et puisque le messager provient de l'interface, vous devez parler et demander à l'utilisateur.


Dois-je expliquer que les réponses des utilisateurs doivent être validées? La première chose que l'utilisateur fera, c'est qu'il répondra différemment de ce que vous attendez.


Eh bien, au final, le script peut se ramifier, c'est-à-dire Chaque réponse à une question peut affecter l'ordre des questions suivantes.


Par exemple!


Pour commencer, placez le fichier avec l'extension .kts dans le dossier contenant les handlers ressources: src/main/resources/handlers/ExampleHandler.kts .


Scénario d'appel 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()) // ⑩ } } 

Les clés des steppes n'ont pas été délibérément prises en constantes. En production, bien sûr, il vaut mieux éviter cela.


Voyons cela:


  1. Nous déclarons le script. Au moins un nom d'équipe est requis. Dans ce cas, il y a deux équipes: «/ taxi», «taxi». Si le message de l'utilisateur commence par ces mots, le gestionnaire correspondant sera appelé.
  2. Nous déterminons les étapes (questions). Un nom d'étape unique est requis car par la suite, la réponse de l'utilisateur est accessible précisément par cette touche («locationFrom»).
  3. Chaque étape contient trois sections, dont la première est la question elle-même. La question est une section obligatoire qui doit être présente à chaque étape. Il n'y a aucun sens dans une étape sans question.
  4. Vous pouvez remplir la question comme vous le souhaitez. Dans ce cas, l'utilisateur sera invité par le clavier à sélectionner l'une des options: «Carte» ou «Argent». À la suite de l'appel de ce bloc, il devrait y avoir un objet de type TelegramSendRequest . Désolé, je n'ai pas pu trouver mieux que le suffixe SendRequest , qui décrit la structure comme une demande sortante dans Telegram.
    Structure des classes
  5. La deuxième section de l'étape la plus importante consiste à vérifier la réponse de l'utilisateur. Le type de chaque étape est paramétré (générique), et par conséquent, le bloc de validation doit retourner exactement le type par lequel son étape est paramétrée.
  6. Si la réponse de l'utilisateur n'est pas satisfaisante, vous pouvez lever une ValidationException avec un texte de clarification, mais le même clavier, si cela a été indiqué dans la question.
  7. La dernière étape est un bloc indiquant l'étape suivante. Par défaut, les étapes seront exécutées dans l'ordre de leur déclaration, de haut en bas. Mais ce processus peut être influencé en remplaçant le bloc correspondant. La clé de l'étape suivante ( String ) ou «null» peut être retournée à la suite de l'exécution de ce bloc, indiquant qu'il n'y a plus d'étapes et qu'il est temps de procéder à l'exécution de la commande.
  8. Lorsqu'une demande d'utilisateur est générée, son traitement est requis. Les arguments dans le lambda sont State (c'est quelque chose comme une session) et les réponses des utilisateurs.
  9. Notez que la réponse ayant échoué n'est plus la chaîne de réponse de l'utilisateur, mais un objet déjà traité du type souhaité.
  10. La réponse à la commande peut être quelconque, similaire au paragraphe 4. Si la réponse à la commande n'est pas requise, vous pouvez retourner "null".

Un gestionnaire peut ne pas avoir d'étapes du tout. Dans ce cas, il vous suffit de déterminer le comportement du gestionnaire pour appeler la commande.


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

Essayez


Pour essayer, forkez le référentiel , clonez-le sur la machine locale et accédez au dossier telegraff-sample . Configurez, lancez, touchez!


En général, telegraff-sample est un projet délibérément indépendant qui n'est pas lié au parent et a même son propre Gradle Wrapper. Vous ne pouvez laisser que ce dossier. C'est une sorte d'archétype.


Comment ça marche?


Télégramme


L'intégration avec Telegram est très simple et implémentée dans TelegramApi .


Chaque méthode a été délibérément implémentée individuellement en raison d'un certain nombre de circonstances: à partir de l'utilisation de Spring's RestTemplate (et des tests pour celui-ci), à la spécificité de l'API Telegram.


Comme vous pouvez le voir dans la configuration, il existe deux types de clients de cette API dans Telegraff: PollingClient , WebhookClient . Selon la configuration, un bac particulier sera déclaré.


Et bien que les méthodes de réception des mises à jour (nouveaux messages) diffèrent de Telegram, l'essence est inchangée et se résume à une chose - publier un événement ( TelegramUpdateEvent ) sur les nouveaux messages via EventPublisher de Spring ( EventPublisher «Observer»). Si vous le souhaitez, vous pouvez implémenter votre propre écouteur en vous abonnant à ce type d'événement. Une couche d'abstraction logique, me semble-t-il, car la façon dont le message a été reçu n'a absolument aucune importance.


Filtres


Dès qu'un nouveau message est reçu, il est nécessaire de le traiter et de répondre à l'utilisateur. Pour ce faire, le message doit passer par la chaîne de filtrage.


Ceci est similaire aux filtres Java EE familiers aux programmeurs Java. La seule différence est que les soi-disant gestionnaires (si nous établissons un parallèle avec Java EE, ce sont des servlets) ne sont pas indépendants des filtres, mais en font partie.


Chaîne de filtre


Ainsi, les filtres sont rationalisés et peuvent laisser les messages aller plus loin dans la chaîne, peut-être pas.


LoggingFilter est évidemment le premier (premier) filtre de priorité qui sera appelé dans le cadre du traitement d'un nouveau message. Enregistre les informations sur un message entrant et les envoie plus loin dans la chaîne. J'ai volontairement ajouté LoggingFilter comme exemple. En fait, cela peut ne pas avoir de sens, car Les messages entrants sont enregistrés au niveau du client.


Le filtre suivant est CancelFilter . Il fonctionne essentiellement en conjonction avec HandlersFilter et est un complément à celui-ci. Sa tâche est simple: si l'utilisateur veut abandonner le script en cours, il peut écrire «/ annuler» ou «annuler» et son statut (session) doit être effacé. Il peut commencer n'importe quel nouveau scénario sans terminer le précédent. Pour cette raison, CancelFilter «supérieur» (prioritaire).


HandlersFilter est le filtre principal du processus actuel. C'est ce filtre qui stocke l'état des conversations utilisateur, trouve et appelle le gestionnaire souhaité (script), applique les blocs de validation, détermine l'ordre des étapes et répond à l'utilisateur.


Si HandlersFilter n'a trouvé aucun gestionnaire approprié pour le message utilisateur, que ce soit dans la session ou dans le contenu, le message est envoyé plus bas dans la chaîne. Le filtre extrême est UnresolvedFilter . C'est un filtre qui sait que c'est le dernier, donc sa fonctionnalité est simple: s'ils m'ont atteint, comment répondre à un message n'est pas clair, je dirai que je n'ai rien compris. Il me semble qu'il vaut mieux recevoir au moins quelques messages du bot s'il ne sait pas répondre, que de ne rien recevoir du tout.


Pour ajouter votre filtre, vous devez déclarer un Bean de la classe TelegramFilter et spécifier l'annotation @TelegramFilterOrder(ORDER_NUMBER) .


Exemple de filtre
 @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) } } 

C'est ainsi que @TinkoffRatesBot implémente une «calculatrice». Sans appeler de script et de commande, vous pouvez envoyer un nombre, par exemple, "1000", ou même une expression entière, par exemple, "4500 * 3 - 12000". Le bot calculera le résultat de l'expression, appliquera les taux de change actuels au résultat et affichera des informations à ce sujet. En fait, le résultat de ces actions est l'exécution du CalculationFilter , qui est dans la chaîne en dessous du HandlersFilter , mais au-dessus du UnresolvedFilter .


Gestionnaires


Le système de script Telegraff (gestionnaires) est basé sur le DSL Kotlin. En bref, il s'agit de lambdas et de constructeurs.


Je ne vois pas l'intérêt de visualiser séparément le DSL Kotlin, car c'est une conversation complètement différente. Il existe une excellente documentation de JetBrains et un rapport complet d' i_osipov .


Nuances


Cette section est consacrée aux fonctionnalités actuelles. Tous, à mon avis, ne sont pas critiques, certains peuvent être corrigés, d'autres non. Mais vous devez connaître ces aspects.


Si vous souhaitez participer ou savoir comment corriger l'un ou l'autre point de cette section, je vous en serai très reconnaissant.


Télégramme


La couche d'intégration avec Telegram n'est probablement pas entièrement décrite. Seules les méthodes dont j'avais besoin ont été mises en œuvre. S'il y a quelque chose qui vous manque personnellement, corrigez TelegramApi et envoyez des RP!


Pour le moment, le manque de prise en charge du clavier en ligne est important (c'est lorsque le clavier est directement en dessous du message dans le ruban). La tâche est aggravée par le fait que les claviers en ligne doivent être correctement «introduits» dans la structure existante afin qu'elle reste simple, pratique et isolée. Il existe déjà une bonne idée pour implémenter cette fonctionnalité, mais elle n'a pas encore été implémentée et testée sous aucune forme.


Pot de graisse


Malheureusement, certaines bibliothèques, telles que JRuby et probablement le Kotlin Embedded Compiler (nécessaire pour la compilation de scripts) peuvent avoir des problèmes dans le cadre du Fat JAR . Fat JAR c'est quand votre code et toutes vos dépendances sont regroupés dans un seul fichier ( *.jar ).


Afin de résoudre ce problème, vous pouvez décompresser les dépendances lors de l'exécution. En d'autres termes, lorsque l'application démarre, le fichier JAR de dépendance du package principal est déployé quelque part sur le disque et le chemin de classe est indiqué avant celui-ci. C'est assez facile à faire grâce à la configuration bootJar :


Configuration du plugin
 bootJar { requiresUnpack "**/**kotlin**.jar" requiresUnpack "**/**telegraff**.jar" } 

Cependant, afin de se référer des gestionnaires (scripts) à vos beans (services, par exemple), ils doivent également être décompressés. Ce qui, en principe, élimine le bénéfice de cette approche.


Selon moi, l'utilisation du plugin d' application Gradle reste la méthode la plus fiable, simple et pratique. De plus, si vous conteneurisez votre application, il n'y a pas de différence par le résultat.


À propos de tout cela, j'ai écrit en détail ici .


Ordre d'initialisation


Ici, je voudrais noter deux circonstances.


Premièrement, si vous regardez le scénario d'appel de taxi, vous pouvez voir que la classe enum est définie au-dessus de l'appel au handler(...) . Cette nécessité est imposée par le fait qu'en fait, handler est un appel de fonction. Un appel de fonction, dont le résultat devrait être une structure, que Telegraff utilisera plus tard. Si, selon le résultat de l'exécution de votre script, la fabrique ne peut pas amener le résultat au type souhaité, une erreur va se produire lors de l'initialisation.


Deuxièmement, vous devez vous rappeler que vos scripts peuvent être initialisés plus tôt que l'ensemble de votre application et de vos beans. Si, par exemple, nous mettons un lien vers le contexte dans une variable statique et essayons d'obtenir un service sur la première ligne du fichier de script, il se peut que le contexte ne l'ait pas, car il n'a pas encore été initialisé. Afin d'éviter de tels problèmes, utilisez cette méthode Telegraff. Il garantit que le contexte est initialisé et que tous les beans nécessaires sont disponibles. Un exemple peut être vu ici .


Conclusion


Je voulais essayer - fourchette,
Je voulais le réparer - envoyer des relations publiques,
Je voulais remercier - mettre un astérisque dans Github, comme le message et le dire à vos amis!


Dépôt de projets

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


All Articles