Telegraff:用于电报的Kotlin DSL

商标


在Habré上,有数千篇文章介绍了如何为不同的编程语言和平台制作Telegram机器人。 这个话题远非新鲜。


但是Telegraff是实现Telegram机器人的最佳框架,我将在削减范围内对其进行证明。


前言


2015年,俄罗斯卢布发烧。 我有美元储蓄,我每五分钟检查一次汇率,以我需要的汇率卖出货币。 发烧继续,我很累,写了一个Telegram机器人( @TinkoffRatesBot ),该机器人会通知您汇率是否达到阈值(预期)。
这个任务让我很感动。 博塔写得很快,但他没有得到满意的结果。


没有与Telegram集成,也没有问题。 这个问题将在几个小时内解决。 而且令我惊讶的是,整个Java库都与Telegrams集成在一起(主观地说,代码的质量令人恶心),在Github上已经赢得了上千个星。


对我而言,主要挑战是脚本系统:用户调用命令,例如“ /出租车”,机器人向他询问一系列问题,每个答案均经过验证,并且可以影响后续问题的顺序,形成通常的“表格”,并赋予最终的形成表格的处理方法回应。
我是这样做的,但是类的结构,抽象的级别是如此的异构,以至于很难看。 我被一个问题折磨了:如何将其简洁而有机地转换为面向对象的模型?


我希望有一个简单,方便且最重要的东西-能够在一个隔离的文件中描述整个脚本,这样我就不需要查看项目的一半就可以了解用户交互链。


并不是说这个问题非常严重,因为任务已经解决了。 相反,有时我想到了他。 当时的想法是Groovy DSL,但是当Kotlin到达时,选择就显而易见了。 因此,Telegraff出现了。


是的,当然,Telegraff不会赢得任何竞争。 而且,Telegraff是最好的主张不应从字面上接受。 但是Telegraff是应对这一挑战的新颖独特方法。 成为唯一的最好的是容易的。


如何使用?


依存关系


第一步是为依赖项指定其他存储库。 也许到现在,我会在Maven Central或JCenter中发布Telegraff。


摇篮
repositories { maven { url "https://dl.bintray.com/ruslanys/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> 

小型情况仍然如此。 要使用Telegraff,您只需指定一个spring-boot-starter依赖项:


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

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

构型


项目配置很简单,可以限制在前两个或三个参数中:


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. 您的Telegram API密钥。
  2. 从电报接收消息(更新)的模式。 它可以是轮询或webhook。
  3. 如果“ webhook”指示接收更新的方法,则必须指定应用程序的路径。
  4. 如果愿意,您可以指定自己的端点路径。 如果未重新定义此参数,将生成以下格式的路径: /telegram/${UUID} 。 在启动应用程序之前,将指定的地址设置为Web挂钩地址。 在工作结束时,Web挂钩地址将被覆盖,以便下次启动时可以切换到轮询。
  5. 如果需要,您可以更改处理程序脚本所在的文件夹。 默认情况下,这是handlers文件夹。
  6. UnresolvedFilter包含在“交付”中,并且默认情况下处于启用状态。 如果在用户的消息中UnresolvedFilter找到处理程序, UnresolvedFilter以“对不起,我不理解您:(”之类的内容进行响应。

现在该写脚本了!


处理程序


处理程序(脚本)是Telegraff的关键部分。 这是设置用户交互链的地方。 最重要的是,每个命令(例如“ / start”,“ / taxi”,“ / help”)都是单独的脚本/脚本/处理程序/处理程序。


脚本可以包含用户执行命令所需的一组步骤(问题)。 换句话说,用户必须填写表格。 而且由于Messenger是来自界面的,因此您需要交谈并询问用户。


我需要解释一下用户响应是否需要验证吗? 用户要做的第一件事是他的响应将不同于您的期望。


好吧,最后脚本可以分支了,即 每个问题的答案都会影响后续问题的顺序。


举个例子!


首先,将扩展名为.kts的文件.kts具有资源handlers的文件夹中: src/main/resources/handlers/ExampleHandler.kts


计程车通话场景
 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()) // ⑩ } } 

草原的关键是故意不考虑常数。 当然,在生产中最好避免这种情况。


让我们弄清楚:


  1. 我们声明脚本。 至少需要一个团队名称。 在这种情况下,有两个团队:“ /出租车”,“出租车”。 如果用户的消息以这些单词开头,则会调用相应的处理程序。
  2. 我们确定步骤(问题)。 唯一的步骤名称是必需的,因为 随后,可以通过此键(“ locationFrom”)精确访问用户的响应。
  3. 每个步骤包含三个部分,第一部分是问题本身。 问题是每个步骤都应包含的强制性部分。 毫无疑问,毫无意义。
  4. 您可以根据需要填写问题。 在这种情况下,将通过键盘提示用户选择以下选项之一:“卡”或“现金”。 调用此块的结果是,应该有一个TelegramSendRequest类型的对象。 抱歉,我无法提供比SendRequest后缀更好的SendRequest后缀在Telegram中将结构描述为外发请求。
    类结构
  5. 第二个最重要的步骤部分是检查用户的响应。 每个步骤的类型都是参数化的(通用),因此,验证块必须准确返回对其步骤进行参数化的类型。
  6. 如果用户的响应不令人满意,则可以抛出带有澄清文本的ValidationException ,但如果问题中已指出,则使用同一键盘。
  7. 最后一步部分是指示下一步的块。 默认情况下,步骤将按照其声明的顺序从上到下执行。 但是,可以通过覆盖相应的块来影响此过程。 执行此块的结果可以返回下一步的键( String )或“ null”,这表明没有更多的步骤了,现在该继续执行命令了。
  8. 生成用户请求时,需要对其进行处理。 lambda中的参数是State(这类似于会话)和用户响应。
  9. 请注意,失败的响应不再是用户的响应字符串,而是所需类型的已处理对象。
  10. 对命令的响应可以是任何,类似于第4段。如果不需要对命令的响应,则可以返回“ null”。

处理程序可能根本没有任何步骤。 在这种情况下,您只需要确定处理程序的行为即可调用命令。


欢迎脚本
 handler("/start") { process { _, _ -> MarkdownMessage("!") } } 

试一下


为了进行尝试,请分叉存储库 ,将其克隆到本地计算机,然后转到telegraff-sample文件夹。 配置,启动,触摸!


通常, telegraff-sample是一个故意独立的项目,与父项目无关,甚至有自己的Gradle Wrapper。 您只能保留此文件夹。 这是一种原型。


如何运作?


电报


与Telegram的集成非常简单,并在TelegramApi实现。


由于多种情况,每种方法都是有意分别实现的:从使用Spring的RestTemplate(以及对其进行测试)开始到Telegram API的特殊性。


从配置中可以看到,Telegraff中此API有两种客户端: PollingClientWebhookClient 。 根据配置,将声明特定的bin。


而且,尽管接收更新(新消息)的方法与Telegram不同,但本质没有改变,归结为一件事-通过Spring的EventPublisher (“观察者”模式)发布有关新消息的事件( TelegramUpdateEvent )。 如果愿意,可以通过订阅此类事件来实现自己的侦听器。 在我看来,逻辑层是抽象层,因为它与消息的接收方式绝对无关紧要。


筛选器


一旦收到新消息,就需要对其进行处理并响应用户。 为此,该消息需要经过过滤器链。


这类似于Java程序员熟悉的Java EE过滤器。 唯一的区别是所谓的Handlers(如果我们与Java EE并行,则是Servlet)不是独立于过滤器,而是它们的一部分。


过滤链


因此,筛选器得到了简化,可以使消息更进一步,也许不是。


LoggingFilter显然是最高优先级(第一)的过滤器,它将作为新消息处理的一部分被调用。 将信息记录在传入消息中,并将其发送到链的更深处。 我特意添加了LoggingFilter作为示例。 实际上,这可能没有意义,因为 传入消息在客户端级别记录。


下一个过滤器是CancelFilter 。 它实际上与HandlersFilter结合使用,并且是对它的补充。 它的任务很简单:如果用户要放弃当前脚本,则可以写“ / cancel”(取消)或“ cancel”(取消),然后清除其状态(会话)。 他可以启动任何新方案而无需完成前一个方案。 由于这个原因, CancelFilter “更高”(优先级)的HandlersFilter


HandlersFilter是当前进程中的主要过滤器。 该过滤器存储用户聊天的状态,查找并调用所需的处理程序(脚本),应用验证块,确定步骤顺序并响应用户。


如果HandlersFilter在会话或内容中都没有为用户消息找到任何合适的处理程序,则消息将在链中进一步发送。 极端过滤器是UnresolvedFilter 。 这是一个过滤器,知道它是最后一个过滤器,因此它的功能很简单:如果他们到达了我,如何响应消息还不清楚,我会说我什么都不懂。 在我看来,如果僵尸程序不知道如何响应,最好至少从它那里接收一些消息,而不是什么都不接收。


为了添加过滤器,您需要声明TelegramFilter类的Bean并指定注释@TelegramFilterOrder(ORDER_NUMBER)


筛选范例
 @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) } } 

这就是@TinkoffRatesBot实现“计算器”的方式。 无需调用任何脚本和命令,您就可以发送数字,例如“ 1000”,甚至可以发送整个表达式,例如“ 4500 * 3-12000”。 机器人将计算表达式的结果,将当前汇率应用于结果并显示有关该信息的信息。 实际上,执行此类操作的结果是CalculationFilter的执行,它位于HandlersFilter下方但UnresolvedFilter上方的链中。


处理程序


Telegraff脚本系统(处理程序)基于Kotlin DSL构建。 简而言之,它与lambda和构建器有关。


我看不到单独查看Kotlin DSL的意义,因为 这是完全不同的对话。 JetBrains提供了出色的文档i_osipov提供了全面的报告


细微差别


本节专门介绍当前功能。 在我看来,所有这些都不是至关重要的,其中一些可以固定,而有些则可以。 但是您需要了解这些方面。


如果您希望参与本节或了解如何纠正本节中的一个或另一点,我将不胜感激。


电报


与Telegram的集成层可能未完整描述。 仅实现了我需要的那些方法。 如果您个人缺少某些东西,请更正TelegramApi并发送PR!


当前重要的部分是缺少嵌入式键盘支持(这是键盘位于功能区中消息的正下方)。 嵌入式键盘需要正确地“输入”到现有结构中,以使其保持简单,方便,隔离的事实使任务更加艰巨。 实现此功能已经有了一个好主意,但是尚未以任何形式实现和测试它。


胖子


不幸的是,作为Fat JAR一部分,某些库(例如JRuby和可能的Kotlin Embedded Compiler (需要编译脚本))可能会遇到问题。 Fat JAR是将您的代码和所有依赖项打包到一个文件( *.jar )中的时候。


为了解决此问题,您可以在运行时中解压缩依赖项。 也就是说,当应用程序启动时,来自主程序包的依赖项JAR会部署在磁盘上的某个位置,并在其之前指示类路径。 通过bootJar 配置,这很容易做到:


插件配置
 bootJar { requiresUnpack "**/**kotlin**.jar" requiresUnpack "**/**telegraff**.jar" } 

但是,为了从处理程序(脚本)引用到您的bean(例如服务),还必须将它们解压缩。 原则上,这消除了此方法的好处。


如我所见,使用Gradle application插件仍然是最可靠,最简单和方便的方法。 此外,如果您要对应用程序进行容器化,则结果不会有任何差异。


关于这一切,我在这里详细介绍。


初始化顺序


在这里,我想指出两种情况。


首先,如果您看出租车的情况,可以看到enum类是在对handler(...)的调用之上定义的。 实际上, handler是一个函数调用,这一事实强加了这种必要性。 一个函数调用,其结果应为某些结构,Telegraff稍后将使用该结构。 如果根据脚本执行的结果,工厂无法将结果带到所需的类型,则在初始化阶段将出错。


其次,您需要记住,可以比整个应用程序和Bean更早地初始化脚本。 例如,如果我们将指向上下文的链接放入静态变量中,并尝试在脚本文件的第一行中获得某些服务,则可能会发现上下文将没有它,因为 尚未初始化。 为了避免此类问题,请使用 Telegraff方法。 它确保上下文已初始化并且所有必需的bean都可用。 一个例子可以在这里看到。


结论


我想尝试-叉子,
我想修复它-发送PR,
我要感谢-在帖子上放一个星号,就像帖子并告诉你的朋友们!


项目资料库

Source: https://habr.com/ru/post/zh-CN445072/


All Articles