
Auf Habré Tausende von Artikeln darüber, wie man einen Telegramm-Bot für verschiedene Programmiersprachen und Plattformen erstellt. Das Thema ist alles andere als neu.
Aber Telegraff ist der beste Rahmen für die Implementierung von Telegramm-Bots, und ich werde es unter dem Strich beweisen.
Präambel
Im Jahr 2015 hatte der russische Rubel Fieber. Ich hatte Einsparungen in Dollar und überprüfte den Kurs buchstäblich alle fünf Minuten, um die Währung zu dem Kurs zu verkaufen, den ich brauchte. Das Fieber zog sich hin, ich wurde müde und schrieb an den Telegramm-Bot ( @TinkoffRatesBot ), der Sie benachrichtigt, wenn der Wechselkurs den (erwarteten) Schwellenwert erreicht.
Diese Aufgabe hat mich sehr berührt. Botha schrieb ziemlich schnell, aber er erhielt keine Befriedigung.
Die Integration mit Telegramm ist nicht und es gab keine Probleme. Dieses Problem ist in wenigen Stunden behoben. Und ich bin sogar überrascht, dass es in Java ganze Bibliotheken (subjektiv mit ekelhaftem Code) für die Integration in Telegramme gibt, die auf Github mehr als tausend Sterne verdient haben.
Die größte Herausforderung für mich war das Skriptsystem: Der Benutzer ruft einen Befehl auf, zum Beispiel "/ Taxi", der Bot stellt ihm eine Reihe von Fragen, jede Antwort wird validiert und kann die Reihenfolge der nachfolgenden Fragen beeinflussen. Das übliche "Formular" wird gebildet, das der endgültigen Verarbeitungsmethode für das Formen gegeben wird Antwort.
Ich habe das getan, aber die Struktur der Klassen, die Abstraktionsebenen, alles war so heterogen, dass es bitter anzusehen war. Die Frage quälte mich: Wie kann dies prägnant und organisch auf ein objektorientiertes Modell übertragen werden?
Ich wollte etwas Einfaches, Bequemes und vor allem - um das gesamte Skript in einer isolierten Datei beschreiben zu können, sodass ich nicht die Hälfte des Projekts anzeigen musste, um die Benutzerinteraktionskette zu verstehen.
Um nicht zu sagen, dass das Problem sehr akut war, weil die Aufgabe bereits gelöst wurde. Eher dachte ich manchmal an ihn. Der Gedanke war Groovy DSL, aber als Kotlin ankam, wurde die Wahl offensichtlich. Also erschien Telegraff.
Ja, natürlich gab es keinen Wettbewerb, den Telegraff gewinnen würde. Und die Behauptung, Telegraff sei das Beste, sollte nicht wörtlich genommen werden. Aber Telegraff ist ein neuer, einzigartiger Ansatz für diese Herausforderung. Es ist leicht, der Beste zu sein, der einzige zu sein.
Wie benutzt man es?
Abhängigkeiten
Der erste Schritt besteht darin, ein zusätzliches Repository für Abhängigkeiten anzugeben. Vielleicht werde ich irgendwann Telegraff in Maven Central oder im JCenter veröffentlichen, aber vorerst.
Gradlerepositories { 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>
Es bleibt der Fall für kleine. Um Telegraff verwenden zu können, müssen Sie nur eine Spring-Boot-Starter-Abhängigkeit angeben:
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>
Konfiguration
Die Projektkonfiguration ist einfach und kann auf die ersten zwei oder drei Parameter beschränkt werden:
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 # ⑥
- Ihr Schlüssel zur Telegramm-API.
- Der Modus zum Empfangen von Nachrichten (Aktualisierungen) vom Telegramm. Es kann entweder Polling oder Webhook sein.
- Wenn die Methode zum Empfangen von Updates durch "Webhook" angegeben ist, müssen Sie den Pfad zu Ihrer Anwendung angeben.
- Wenn Sie möchten, können Sie Ihren eigenen Pfad zum Endpunkt angeben. Wenn dieser Parameter nicht neu definiert wird, wird ein Pfad der folgenden Form generiert:
/telegram/${UUID}
. Vor dem Starten der Anwendung wird die angegebene Adresse als Web-Hook-Adresse festgelegt. Am Ende der Arbeit wird die Web-Hook-Adresse überschrieben, um beim nächsten Start zur Abfrage wechseln zu können. - Bei Bedarf können Sie den Ordner ändern, in dem sich die Skripte der Handler befinden. Standardmäßig ist dies der
handlers
. UnresolvedFilter
ist in der "Lieferung" enthalten und standardmäßig aktiviert. Für den Fall, dass in der Nachricht des Benutzers kein Handler gefunden wurde, antwortet UnresolvedFilter
mit etwas wie "Entschuldigung, ich verstehe Sie nicht :(".
Es ist Zeit, Skripte zu schreiben!
Handler
Handler (Skripte) sind ein wichtiger Bestandteil von Telegraff. Hier wird die Benutzerinteraktionskette festgelegt. Unter dem Strich ist jeder Befehl wie "/ start", "/ Taxi", "/ help" ein separates Skript / script / handler / handler.
Ein Skript kann eine Reihe von Schritten (Fragen) enthalten, die ein Benutzer ausführen muss, um einen Befehl auszuführen. Mit anderen Worten, der Benutzer muss das Formular ausfüllen. Und da der Messenger von der Benutzeroberfläche stammt, müssen Sie sprechen und den Benutzer fragen.
Muss ich erklären, dass Benutzerantworten validiert werden müssen? Das erste, was der Benutzer tun wird, ist, dass er anders reagiert als erwartet.
Nun, am Ende kann das Skript verzweigen, d.h. Jede Antwort auf eine Frage kann sich auf die Reihenfolge der nachfolgenden auswirken.
Z.B!
.kts
die Datei mit der Erweiterung .kts
in den Ordner mit den Ressourcenhandlern: src/main/resources/handlers/ExampleHandler.kts
.
Taxi-Anrufszenario enum class PaymentMethod { CARD, CASH } handler("/taxi", "") {
Die Schlüssel der Steppen wurden bewusst nicht in Konstanten aufgenommen. In der Produktion wird dies natürlich am besten vermieden.
Lassen Sie es uns herausfinden:
- Wir deklarieren das Skript. Es ist mindestens ein Teamname erforderlich. In diesem Fall gibt es zwei Teams: "/ Taxi", "Taxi". Wenn die Nachricht des Benutzers mit diesen Wörtern beginnt, wird der entsprechende Handler aufgerufen.
- Wir bestimmen die Schritte (Fragen). Da ist ein eindeutiger Schrittname erforderlich Anschließend kann über diesen Schlüssel („locationFrom“) genau auf die Antwort des Benutzers zugegriffen werden.
- Jeder Schritt enthält drei Abschnitte, von denen der erste die Frage selbst ist. Die Frage ist ein obligatorischer Abschnitt, der bei jedem Schritt vorhanden sein sollte. Ein Schritt ohne Frage macht keinen Sinn.
- Sie können die Frage nach Belieben ausfüllen. In diesem Fall wird der Benutzer über die Tastatur aufgefordert, eine der folgenden Optionen auszuwählen: "Karte" oder "Bargeld". Als Ergebnis des Aufrufs dieses Blocks sollte ein Objekt vom Typ
TelegramSendRequest
. Leider konnte ich mir nichts Besseres als das SendRequest
Suffix SendRequest
, das die Struktur als ausgehende Anforderung in Telegram beschreibt.

- Der zweitwichtigste Schrittabschnitt besteht darin, die Antwort des Benutzers zu überprüfen. Der Typ jedes Schritts ist parametrisiert (generisch), und daher muss der Validierungsblock genau den Typ zurückgeben, mit dem sein Schritt parametrisiert wird.
- Wenn die Antwort des Benutzers nicht zufriedenstellend ist, können Sie eine
ValidationException
mit klarstellendem Text, aber derselben Tastatur auslösen, wenn dies in der Frage angegeben wurde. - Der letzte Schrittabschnitt ist ein Block, der den nächsten Schritt angibt. Standardmäßig werden Schritte in der Reihenfolge ihrer Deklaration von oben nach unten ausgeführt. Dieser Prozess kann jedoch durch Überschreiben des entsprechenden Blocks beeinflusst werden. Als Ergebnis der Ausführung dieses Blocks kann entweder der Schlüssel des nächsten Schritts (
String
) oder "null" zurückgegeben werden. Dies zeigt an, dass keine weiteren Schritte vorhanden sind und es Zeit ist, mit der Ausführung des Befehls fortzufahren. - Wenn eine Benutzeranforderung generiert wird, ist deren Verarbeitung erforderlich. Die Argumente im Lambda sind Status (dies ist so etwas wie eine Sitzung) und Benutzerantworten.
- Beachten Sie, dass die fehlgeschlagene Antwort nicht mehr die Antwortzeichenfolge des Benutzers ist, sondern ein bereits verarbeitetes Objekt des gewünschten Typs.
- Die Antwort auf den Befehl kann beliebig sein, ähnlich wie in Absatz 4. Wenn die Antwort auf den Befehl nicht erforderlich ist, können Sie "null" zurückgeben.
Ein Handler hat möglicherweise überhaupt keine Schritte. In diesem Fall müssen Sie nur das Verhalten des Handlers bestimmen, um den Befehl aufzurufen.
Begrüßungsskript handler("/start") { process { _, _ -> MarkdownMessage("!") } }
Versuchen Sie es
Um dies zu versuchen, gabeln Sie das Repository , klonen Sie es auf den lokalen Computer und wechseln Sie in den Ordner telegraff-sample
. Konfigurieren, starten, berühren!
Im Allgemeinen ist telegraff-sample
ein bewusst unabhängiges Projekt, das nicht mit dem Elternteil zusammenhängt und sogar einen eigenen Gradle Wrapper hat. Sie können nur diesen Ordner verlassen. Dies ist eine Art Archetyp.
Wie funktioniert es
Telegramm
Die Integration mit Telegram ist sehr einfach und in TelegramApi
implementiert.
Jede Methode wurde aufgrund einer Reihe von Umständen absichtlich einzeln implementiert: angefangen von der Verwendung von Spring's RestTemplate (und Tests dafür) bis hin zur Spezifität der Telegramm-API.
Wie Sie der Konfiguration entnehmen können, gibt es in Telegraff zwei Arten von Clients dieser API: PollingClient , WebhookClient . Abhängig von der Konfiguration wird ein bestimmter Behälter deklariert.
Und obwohl sich die Methoden zum Empfangen von Updates (neue Nachrichten) von Telegramm unterscheiden, bleibt das Wesentliche unverändert und läuft auf eine Sache hinaus: das Veröffentlichen eines Ereignisses ( TelegramUpdateEvent
) über neue Nachrichten über Spring's EventPublisher
("Observer" -Muster). Wenn Sie möchten, können Sie Ihren eigenen Listener implementieren, indem Sie diese Art von Ereignis abonnieren. Eine logische Abstraktionsebene, wie es mir scheint, denn es spielt absolut keine Rolle, wie die Nachricht empfangen wurde.
Filter
Sobald eine neue Nachricht empfangen wurde, muss sie verarbeitet und dem Benutzer geantwortet werden. Dazu muss die Nachricht die Filterkette durchlaufen.
Dies ähnelt Java EE-Filtern, die Java-Programmierern bekannt sind. Der einzige Unterschied besteht darin, dass die sogenannten Handler (wenn wir eine Parallele zu Java EE ziehen, sind dies Servlets) nicht unabhängig von Filtern sind, sondern Teil davon sind.

Die Filter sind also optimiert und können Nachrichten weiter unten in der Kette erscheinen lassen, vielleicht auch nicht.
LoggingFilter
ist offensichtlich der Filter mit der höchsten Priorität (erster Filter), der im Rahmen der Verarbeitung einer neuen Nachricht aufgerufen wird. Protokolliert Informationen zu einer eingehenden Nachricht und sendet sie weiter unten in der Kette. Ich habe absichtlich LoggingFilter
als Beispiel hinzugefügt. In der Tat kann es nicht sinnvoll sein, weil Eingehende Nachrichten werden auf Client-Ebene protokolliert.
Der nächste Filter ist CancelFilter
. Es funktioniert im Wesentlichen in Verbindung mit HandlersFilter
und ist eine Ergänzung dazu. Die Aufgabe ist einfach: Wenn der Benutzer das aktuelle Skript verlassen möchte, kann er "/ cancel" oder "cancel" schreiben und sein Status (Sitzung) sollte gelöscht werden. Er kann jedes neue Szenario starten, ohne das vorherige abzuschließen. Aus diesem Grund " CancelFilter
" höher (Priorität) HandlersFilter
.
HandlersFilter
ist der Hauptfilter im aktuellen Prozess. Dieser Filter speichert den Status von Benutzerchats, findet und ruft den gewünschten Handler (Skript) auf, wendet Validierungsblöcke an, bestimmt die Reihenfolge der Schritte und antwortet dem Benutzer.
Wenn der HandlersFilter
weder in der Sitzung noch im Inhalt geeignete Handler für die Benutzernachricht gefunden hat, wird die Nachricht weiter unten in der Kette gesendet. Der extreme Filter ist UnresolvedFilter
. Dies ist ein Filter, der weiß, dass es der letzte ist, daher ist seine Funktionalität einfach: Wenn sie mich erreicht haben, ist es nicht klar, wie sie auf eine Nachricht reagieren sollen. Ich werde sagen, dass ich nichts verstanden habe. Es scheint mir, dass es besser ist, zumindest einige Nachrichten vom Bot zu erhalten, wenn er nicht weiß, wie er antworten soll, als überhaupt nichts zu erhalten.
Um Ihren Filter hinzuzufügen, müssen Sie eine Bean der TelegramFilter
Klasse deklarieren und die Annotation @TelegramFilterOrder(ORDER_NUMBER)
angeben.
Filterbeispiel @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) } }
So implementiert @TinkoffRatesBot einen „Taschenrechner“. Ohne ein Skript oder einen Befehl aufzurufen, können Sie eine Nummer senden, z. B. "1000", oder sogar einen ganzen Ausdruck, z. B. "4500 * 3 - 12000". Der Bot berechnet das Ergebnis des Ausdrucks, wendet die aktuellen Wechselkurse auf das Ergebnis an und zeigt Informationen dazu an. Tatsächlich ist das Ergebnis solcher Aktionen die Ausführung des CalculationFilter
, der sich in der Kette unter dem HandlersFilter
, aber über dem HandlersFilter
befindet.
Handler
Das Telegraff-Skriptsystem (Handler) basiert auf dem Kotlin DSL. Kurz gesagt, es geht um Lambdas und um Bauherren.
Ich sehe keinen Sinn darin, das Kotlin DSL separat zu betrachten, weil Das ist ein ganz anderes Gespräch. Es gibt eine großartige Dokumentation von JetBrains und einen umfassenden Bericht von i_osipov .
Nuancen
Dieser Abschnitt ist den vorliegenden Funktionen gewidmet. Alle sind meiner Meinung nach nicht kritisch, einige können repariert werden, andere nicht. Aber Sie müssen über diese Aspekte Bescheid wissen.
Wenn Sie den Wunsch haben, teilzunehmen, oder wissen, wie Sie den einen oder anderen Punkt in diesem Abschnitt korrigieren können, bin ich Ihnen sehr dankbar.
Telegramm
Die Integrationsschicht mit Telegramm ist wahrscheinlich nicht vollständig beschrieben. Es wurden nur die Methoden implementiert, die ich brauchte. Wenn Ihnen persönlich etwas fehlt, korrigieren Sie TelegramApi
und senden Sie PR!
Im Moment ist der Mangel an Inline-Tastaturunterstützung wichtig (dies ist der Fall, wenn sich die Tastatur direkt unter der Meldung in der Multifunktionsleiste befindet). Die Aufgabe wird durch die Tatsache erschwert, dass Inline-Tastaturen korrekt in die vorhandene Struktur „eingegeben“ werden müssen, damit sie einfach, bequem und isoliert bleibt. Es gibt bereits eine gute Idee für die Implementierung dieser Funktionalität, sie wurde jedoch noch nicht in irgendeiner Form implementiert und getestet.
Fettglas
Leider können einige Bibliotheken wie JRuby
und wahrscheinlich der Kotlin Embedded Compiler
(der zum Kompilieren von Skripten benötigt wird) Probleme als Teil der Fat JAR
. Fat JAR
ist, wenn Ihr Code und alle Ihre Abhängigkeiten in einer Datei ( *.jar
) gepackt sind.
Um dieses Problem zu lösen, können Sie Abhängigkeiten zur Laufzeit entpacken. Das heißt, wenn die Anwendung gestartet wird, wird die Abhängigkeits-JAR vom Hauptpaket irgendwo auf der Festplatte bereitgestellt und der Klassenpfad davor angegeben. Dies ist über die bootJar
Konfiguration ziemlich einfach:
Plugin Konfiguration bootJar { requiresUnpack "**/**kotlin**.jar" requiresUnpack "**/**telegraff**.jar" }
Um jedoch von Handlern (Skripten) auf Ihre Beans (z. B. Dienste) verweisen zu können, müssen diese auch entpackt werden. Dies eliminiert im Prinzip den Nutzen dieses Ansatzes.
Aus meiner Sicht bleibt die Verwendung des Gradle- application
Plugins die zuverlässigste, einfachste und bequemste Methode. Wenn Sie Ihre Anwendung containerisieren, gibt es außerdem keinen Unterschied zum Ergebnis.
Über all das habe ich hier ausführlich geschrieben.
Initialisierungsreihenfolge
Hier möchte ich zwei Umstände erwähnen.
Wenn Sie sich das Taxi- enum
ansehen, sehen Sie zunächst, dass die enum
über dem Aufruf an den handler(...)
. Diese Notwendigkeit wird durch die Tatsache auferlegt, dass der handler
tatsächlich ein Funktionsaufruf ist. Ein Funktionsaufruf, dessen Ergebnis eine Struktur sein sollte, die Telegraff später verwenden wird. Wenn die Factory gemäß dem Ergebnis der Ausführung Ihres Skripts das Ergebnis nicht auf den gewünschten Typ bringen kann, tritt in der Initialisierungsphase ein Fehler auf.
Zweitens müssen Sie bedenken, dass Ihre Skripte früher als Ihre gesamte Anwendung und Ihre Beans initialisiert werden können. Wenn wir beispielsweise einen Link zum Kontext in eine statische Variable einfügen und versuchen, in der ersten Zeile der Skriptdatei einen Dienst abzurufen, stellt sich möglicherweise heraus, dass der Kontext diesen nicht hat, weil es wurde noch nicht initialisiert. Verwenden Sie diese Telegraff-Methode, um solche Probleme zu vermeiden. Es stellt sicher, dass der Kontext initialisiert wird und alle erforderlichen Beans verfügbar sind. Ein Beispiel ist hier zu sehen.
Fazit
Ich wollte versuchen - Gabel,
Ich wollte es reparieren - PR senden,
Ich wollte mich bedanken - setzen Sie ein Sternchen in Github, wie die Post und sagen Sie es Ihren Freunden!
Projekt-Repository