
在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 # ⑥
- 您的Telegram API密钥。
- 从电报接收消息(更新)的模式。 它可以是轮询或webhook。
- 如果“ webhook”指示接收更新的方法,则必须指定应用程序的路径。
- 如果愿意,您可以指定自己的端点路径。 如果未重新定义此参数,将生成以下格式的路径:
/telegram/${UUID}
。 在启动应用程序之前,将指定的地址设置为Web挂钩地址。 在工作结束时,Web挂钩地址将被覆盖,以便下次启动时可以切换到轮询。 - 如果需要,您可以更改处理程序脚本所在的文件夹。 默认情况下,这是
handlers
文件夹。 UnresolvedFilter
包含在“交付”中,并且默认情况下处于启用状态。 如果在用户的消息中UnresolvedFilter
找到处理程序, UnresolvedFilter
以“对不起,我不理解您:(”之类的内容进行响应。
现在该写脚本了!
处理程序
处理程序(脚本)是Telegraff的关键部分。 这是设置用户交互链的地方。 最重要的是,每个命令(例如“ / start”,“ / taxi”,“ / help”)都是单独的脚本/脚本/处理程序/处理程序。
脚本可以包含用户执行命令所需的一组步骤(问题)。 换句话说,用户必须填写表格。 而且由于Messenger是来自界面的,因此您需要交谈并询问用户。
我需要解释一下用户响应是否需要验证吗? 用户要做的第一件事是他的响应将不同于您的期望。
好吧,最后脚本可以分支了,即 每个问题的答案都会影响后续问题的顺序。
举个例子!
首先,将扩展名为.kts
的文件.kts
具有资源handlers
的文件夹中: src/main/resources/handlers/ExampleHandler.kts
。
计程车通话场景 enum class PaymentMethod { CARD, CASH } handler("/taxi", "") {
草原的关键是故意不考虑常数。 当然,在生产中最好避免这种情况。
让我们弄清楚:
- 我们声明脚本。 至少需要一个团队名称。 在这种情况下,有两个团队:“ /出租车”,“出租车”。 如果用户的消息以这些单词开头,则会调用相应的处理程序。
- 我们确定步骤(问题)。 唯一的步骤名称是必需的,因为 随后,可以通过此键(“ locationFrom”)精确访问用户的响应。
- 每个步骤包含三个部分,第一部分是问题本身。 问题是每个步骤都应包含的强制性部分。 毫无疑问,毫无意义。
- 您可以根据需要填写问题。 在这种情况下,将通过键盘提示用户选择以下选项之一:“卡”或“现金”。 调用此块的结果是,应该有一个
TelegramSendRequest
类型的对象。 抱歉,我无法提供比SendRequest
后缀更好的SendRequest
后缀在Telegram中将结构描述为外发请求。

- 第二个最重要的步骤部分是检查用户的响应。 每个步骤的类型都是参数化的(通用),因此,验证块必须准确返回对其步骤进行参数化的类型。
- 如果用户的响应不令人满意,则可以抛出带有澄清文本的
ValidationException
,但如果问题中已指出,则使用同一键盘。 - 最后一步部分是指示下一步的块。 默认情况下,步骤将按照其声明的顺序从上到下执行。 但是,可以通过覆盖相应的块来影响此过程。 执行此块的结果可以返回下一步的键(
String
)或“ null”,这表明没有更多的步骤了,现在该继续执行命令了。 - 生成用户请求时,需要对其进行处理。 lambda中的参数是State(这类似于会话)和用户响应。
- 请注意,失败的响应不再是用户的响应字符串,而是所需类型的已处理对象。
- 对命令的响应可以是任何,类似于第4段。如果不需要对命令的响应,则可以返回“ null”。
处理程序可能根本没有任何步骤。 在这种情况下,您只需要确定处理程序的行为即可调用命令。
欢迎脚本 handler("/start") { process { _, _ -> MarkdownMessage("!") } }
试一下
为了进行尝试,请分叉存储库 ,将其克隆到本地计算机,然后转到telegraff-sample
文件夹。 配置,启动,触摸!
通常, telegraff-sample
是一个故意独立的项目,与父项目无关,甚至有自己的Gradle Wrapper。 您只能保留此文件夹。 这是一种原型。
如何运作?
电报
与Telegram的集成非常简单,并在TelegramApi
实现。
由于多种情况,每种方法都是有意分别实现的:从使用Spring的RestTemplate(以及对其进行测试)开始到Telegram API的特殊性。
从配置中可以看到,Telegraff中此API有两种客户端: PollingClient , WebhookClient 。 根据配置,将声明特定的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,
我要感谢-在帖子上放一个星号,就像帖子并告诉你的朋友们!
项目资料库