最近因为某些原因需要做个Telegram的Bot帮我自动管理频道,于是寻思着用Node做一个bot,tg这么大的平台肯定是有Node专属的SDK的,于是在谷歌上找了一下,发现了两个star数量比较多的框架,第一个是 node-telegram-bot-api,第二个就是今天要介绍的 Telegraf

可是我在谷歌翻了半天也没找到比较易懂的中文的开发教程,官方的文档都是英文的,ytb上的介绍视频大多数也是英文的,虽然也能搜到零零星星的中文介绍,但都是写的含糊不清。在对官方的英文文档研究了一番之后,于是我决定自己写一篇比较浅层的入门介绍。

Bot的工作模式

Telegram提供了两种方式使Bot能够接收到最新的消息,第一种是长轮询(Long Polling),第二种是Webhook,下面简单介绍一下这两种方式。

  • 长轮询:这是Telegram官方比较推荐的一种方式,它通过客户端向Telegram的服务器建立一个长时间不会断开请求,服务器检测到更新后立即返回消息详情给客户端,客户端收到响应后断开链接然后重新建立一个新的长连接请求,如此往复。这种方式的好处就是效率高、回复即时,但需要占用连接资源。
  • WebHook:所谓WebHook,就是当Telegram检测的消息后立即向你的客户端(或者是服务器)发出Http请求来告诉内容,但是缺点显而易见,起码你需要一个公网环境下能够被Telegram服务器访问的IP或域名。但是相比较WebHook来说这种方式占用的资源要少一些。

Telegraf框架介绍

之所以选用这个框架,是因为它其中使用了中间件的设计模式,如果你用过像Koa、Express这样的web框架,你就可以像写Web后端一样来写Bot的后端。官方文档:https://telegraf.js.org/

基础使用

首先你需要以Bot的Token为参数来新建一个Telegraf对象,这个对象就像Koa里的app一样。

1
2
3
4
5
6
7
8
9
10
const { Telegraf } = require('telegraf')
const token = process.env.BOT_TOKEN; // Token放在了环境变量里

const bot = new Telegraf(token)

// 给Bot加上各种中间件

bot.launch() // 返回Promise<void>
// 或
bot.launch().then(() => console.log('Bot已经成功启动'))

添加中间件

就像koa-rounter一样,你可以使用bot.use()给bot添加中间件。大致写法如下:

1
2
3
4
5
bot.use(async (ctx, next) => (
// 一些逻辑

await next() // 使用next来跳转到下一个中间件
))

其中的next与koa的next作用一样,如果不需要的话也可以不加。比较复杂的就是ctx对象了,它里面包括了当前消息的上下文,包括发送方的信息以及telegram对象等,下面来简单介绍一下用法。

Context对象

获取消息内容

首先可以通过ctx.message拿到一个Message对象,不过这个Message其实是一个泛型接口,也就是说拿到的对象实际上都是继承于(或者说实现了)Messqge这个接口的。比如文字消息,就可以拿到一个TextMessage对象。然后我们就可以通过ctx.message.text获取文字信息的字符串内容,整个过程从原理上来讲比较复杂,其实Message又继承了Update,也就是一种包括了所有从Telegram收到的更新内容(因为使用Bot不仅限于发消息,还包括使用键盘,Inline查询等,这里就不多赘述了)

于是我们可以这样来把用户发送的消息返回给用户:

1
2
3
4
bot.use(async ctx => {
let text = ctx.message.text
await ctx.reply(text)
})

另外,如果你不知道什么是Inline消息的话,我认为最快的方式是去体验一下@like这个Bot。

回复消息

上面使用了ctx.reply来进行回复,它用来单纯的发送文本类型的消息。

如果要回复其他类型的消息,ctx中提供了一系列的replyWith…函数,这样就可以发送诸如图片、文件等其他消息。这些函数一般都可以接收多个参数,比如你可以使用replyWithPhoto一次性回复多张图片。

另外,reply以及相关的函数也返回一个Message对象,我们在获取这个对象之后可以先保存起来,之后需要编辑或删除消息的时候就可以从这个对象里获取消息的message_id和chat_id等信息。

获取发送方

部分情况下我们需要验证发送命令的一方是不是Bot的管理员,于是我们就可以对发送方的id进行验证:

1
2
3
4
5
6
7
8
bot.use(async (ctx, next) => {
if(ctx.from.id !== 753861233)
{
await ctx.reply("你没有权限操作")
return
}
await next()
})

其中ctx.from中就包含了所有发送方的信息,包括ID,用户名等。如果是在群组中使用,还会包含群组信息。

构造中间件

往往直接使用bot.use()来添加中间件很麻烦,但是直接使用外部模块作为中间件时又没有提示信息可以参考,于是Telegraf专门提供了一个构造中间件的函数:Telegraf.fork

于是我们可以像这样来构造中间件:

1
2
3
4
5
6
const { Telegraf } = require('telegraf')
const fork = Telegraf.fork;

module.exports = fork(async (ctx,next) => {
// 中间件逻辑
})

然后可以在中间件管理的程序里统一导入,然后再使用bot.use()来使用所构造的中间件。

专属中间件加载器

Telegraf为一部分的消息类型提供了专属的中间件加载器(中间件加载器这个词是我编的,因为不知道怎么翻译比较合适),比如命令类型的消息、Inline类型消息等。这里注意介绍一下命令类型的。

使用方法比较简单,直接使用bot.command('命令',中间件)这样的格式就行。

比如接收add命令:

1
2
3
bot.command('add',async (ctx,next) => {
// 中间件逻辑
})

如果使用上面的fork构造器话,第二个参数也就可以写成中间件对象。

如果要获取命令的参数,我们在中间件里任然可以使用ctx.message.text来获取消息文本,进而获取命令参数。

此外Telegraf还提供了其他的加载器来处理特定类型的消息,比如bot.hears()用来处理特定内容的文本消息,bot.textLink()只处理内容为一个链接的消息,bot.textMetion()只处理内容为Metion类型的消息(也就是@xxx)等等,这里不做过多介绍。

主动发送消息

Bot不仅可以在收到更新时执行相应的动作,也可以主动执行某个动作。

Telegraf在bot.telegram中提供了一系列函数供我们调用,下面会挑几个简单介绍一下。

下面的几个函数均有返回值,同上面的reply()函数,不再赘述了。

发送文本消息 - sendMessage

此函数主要接收2个参数:chat_id,text。

要注意Telegram中的chat_id既可以是一串数字,也可以是以@开头的一个字符串,比如 @zh_CN 就可以是一个chat_id。但区别在于@形式的id随时都可以被用户修改,而数字id一旦创建则不可修改。

发送图片 - sendPhoto

此函数的第一个参数也是chat_id,但第二个参数既可以是字符串又可以是一个文件流对象。其中字符串参数又分为两种情况:file_id或file_url,也就是可以传一个在Telegram已经存在的文件的id作为要发送的文件,也可以直接传一个图片链接让Telegram去获取这个图片。

其中有一个小坑,如果传一个图片链接给Telegram的话,有可能会返回链接不是图片的错误。解决方法很简单,在链接的query里加一个随机参数即可,比如时间戳参数。

编辑消息 - editMessageText

这个函数主要接收4个参数,分别是chat_id,message_id,inline_message_id,text这四个参数。

其中message_id,inline_message_id只需要一个有值,另一个为undefined就行。

对于其他的函数比如editMessageMedia,使用方法类似,详情可参考官方文档。

下面是一个发送消息然后编辑的示例:

1
2
3
4
5
6
async ctx => {
let message = await bot.telegram.sendMessage("@Test","开始处理数据...")
// 执行其他异步操作

await ctx.telegram.editMessageText(message.chat.id, message.message_id, undefined, "数据处理完成")
}

需要注意的是message的chat_id是使用message.chat.id来获取的,而message_id是直接通过message_id字段来获取的。

Extra参数与Markup

其实上面提到的reply系列函数与send系列函数都有一个最后面的extra参数,它是可选参数,代表着这条消息的附属信息,比如文本信息等。

如果我们想在发送图片的同时携带一段文本,就可以使用extra中的caption字段:

1
2
3
ctx.telegram.sendPhoto('chat_id', 'picture', {
caption: '这是一段文本介绍'
});

此外,extra参数中还有几个比较常用的字段,比如parse_mode字段可以指定内容的解析格式为Markdown或者HTML等,再比如如果这条发送的消息是要回复另一条消息的,可以在其中指定reply_to_message_id为要回复的消息ID即可。

除此之外,Telegram中还有一个有趣的功能:Keyboard。也就是说,你可以在消息的下方插入几个按钮来供用户点击,是不是很有意思?

要实现这个功能,就需要用到Telegraf提供的Markup。

Markup简单介绍

Telegram中的键盘(Keyboard)分为两种:Inline Keyboard和普通的Keyboard。其中Inline Keyboard是出现在消息下方的键盘,比较常用。而普通的KeyBoard则是出现在输入框下方的键盘,一般用于标签检索等场景。

对此,Markup类里分别提供了inlineKeyboard和keyboard供我们创建键盘对象。这里我们直接通过官方的几个的示例来看一下它是怎么用的:

1
2
3
4
5
6
7
bot.command('onetime', (ctx) =>
ctx.reply('One time keyboard', Markup
.keyboard(['/simple', '/inline', '/pyramid'])
.oneTime()
.resize()
)
)

这个示例中的extra参数就是由由Markup创建的普通键盘对象,键盘中的数组代表了三个按钮。这个键盘会展示在输入框的下面,oneTime代表这个键盘被点击一次会自动收起,resize代表这个键盘会根据高度自动调整尺寸。

另外,如果直接使用数组的方式来创建键盘,那么键盘上的按钮点击后会是用户发送该文本,一般会同时使用bot.hears()来判断用户点击了哪个按钮。

1
2
3
4
5
6
7
8
9
bot.command('special', (ctx) => {
return ctx.reply(
'Special buttons keyboard',
Markup.keyboard([
Markup.button.contactRequest('Send contact'),
Markup.button.locationRequest('Send location')
]).resize()
)
})

这个则复杂一些,通过Markup.button来调用Telegram的其他功能,比如contactRequest则会请求用户发送一个联系人信息,locationRequest则会请求用户发送位置信息。参数中的字符串仍代表按钮文本。

1
2
3
4
5
6
7
8
9
bot.command('inline', (ctx) => {
return ctx.reply('<b>Coke</b> or <i>Pepsi?</i>', {
parse_mode: 'HTML',
...Markup.inlineKeyboard([
Markup.button.callback('Coke', 'Coke'),
Markup.button.callback('Pepsi', 'Pepsi')
])
})
})

这个就是创建Inline Keybord了,它会展示在消息的下面,button.callback函数接收2个参数:text和data,text仍然是按钮文本,data则是回调数据。用这个搭配bot.action()就可以实现点击选项然后执行回调的过程。下面是官方的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bot.command('caption', (ctx) => {
return ctx.replyWithPhoto({ url: 'https://picsum.photos/200/300/?random' },
{
caption: 'Caption',
parse_mode: 'Markdown',
...Markup.inlineKeyboard([
Markup.button.callback('Plain', 'plain'),
Markup.button.callback('Italic', 'italic')
])
}
)
})

bot.action('plain', async (ctx) => {
await ctx.answerCbQuery()
await ctx.editMessageCaption('Caption', Markup.inlineKeyboard([
Markup.button.callback('Plain', 'plain'),
Markup.button.callback('Italic', 'italic')
]))
})

bot.action('italic', async (ctx) => {
await ctx.answerCbQuery()
await ctx.editMessageCaption('_Caption_', {
parse_mode: 'Markdown',
...Markup.inlineKeyboard([
Markup.button.callback('Plain', 'plain'),
Markup.button.callback('* Italic *', 'italic')
])
})
})

这个例子中,用户点击了Italic按钮后,就会执行bot.action('italic', ...)中的中间件,这里首先调用了answerCbQuery()向用户展示操作正在执行,然后调用调用editMessageCaption来编辑这个消息中的文本,并指定解析模式为Markdown,然后消息中的文本就被编辑为了斜体(下划线在Markdown中表示斜体)。

本文章参考来源

Telegraf官方文档:https://telegraf.js.org/classes/telegraf.html

Telegraf官方示例:https://github.com/telegraf/telegraf/tree/develop/docs/examples

Telegram Bot API文档:https://core.telegram.org/bots/api#sendphoto