前言
在上一篇文章中,我们已经完成了 Bot 项目的创建以及一些前期准备工作。本章内容,我将带领大家开始正式业务逻辑代码的编写,并将 Telegram 作为 OSS 对象存储服务,实现基于 Telegram 的图床。
本章需要实现的功能
在正式开始之前,我先介绍下,本章我们将具体实现的功能。
- 实现一个
/setup
endpoint,当我们请求/setup
时,对我们的 Telegram bot 进行一些初始化设置。具体为:- 为我们的 Telegram Bot 设置 Webhook endpoint
- 为我们的 Telegram Bot 设置一些可使用的命令(command)
- 实现我们的 Webhook endpoint (
/bot
),/bot
将接收来自 Telegram 服务器的消息,并对消息进行处理后,返回不同的响应结果。 - 实现基础 Bot 逻辑,让 Bot 能够响应
/start
,/help
命令,能够监听私聊中的图片以及文档消息,并返回图片以及文档的file_id
。 - 实现一个
/image
endpoint, 当请求这个 endpoint 时,将根据file_id
参数获取存储在 Telegram 服务器中的图片并返回。
看描述是不是很简单,那么让我们动起手来,实现它们吧!
启动项目
回顾下上篇文章的内容,我们的 package.json 文件中有三条 scripts。
npm run dev
: 本地开发时运行项目npm run deploy
: 将项目部署到 Cloudflare Workersnpm run tunnel
: 内网穿透,将通过npm run dev
运行的本地项目暴露到公网上
我们现在需要本地启动项目,所以需要运行 npm run dev
。运行之后,我们将在终端中看到如下信息:
1 | PS E:\Code\img-mom> npm run dev |
通过浏览器打开 [http://127.0.0.1:8787](http://127.0.0.1:8787)
, 我们将看到 Hello Hono!
接下来,让我们用 VSCode 打开src/index.ts
, 这个文件是我们项目的入口文件,项目代码将从这个文件开始执行。当我们打开这个文件时,你应该会看到如下代码:
1 | import { Hono } from 'hono' |
这个代码是由 Hono 脚手架生成的模板代码。我们需要进行一些小的修改,以方便我们之后的开发。
1 | import { Hono } from 'hono' |
- 我们将其中的
Hello Hono!
更改为Hello ImgMom!
。 - 将
export default app
删除,并添加app.fire()
。- Cloudflare Workers 有两种模式:ESModule Workers 以及 Service Workers。 Hono 对这两种模式进行了抽象封装,对应的启用方式为:
export default app
: 使用 ESModule Workersapp.fire()
: 使用 Service Workers
- 我们的 Bot 将使用 Service Workers 模式运行
- Cloudflare Workers 有两种模式:ESModule Workers 以及 Service Workers。 Hono 对这两种模式进行了抽象封装,对应的启用方式为:
重新运行下npm run dev
, 此时,如果你看到了Hello ImgMom!
这段文字,那么说明我们的项目更改已经生效。
我们接下来再启动下内网穿透服务,以便将我们的本地服务暴露到公网上去。打开新的终端窗口,运行: npm run tunnel
, 如果一切顺利,你将看到如下一些信息:
1 | PS E:\Code\img-mom> npm run tunnel |
其中 https://pregnant-mentor-reggae-answer.trycloudflare.com 便是 Cloudflared 提供给我们的临时外网 URL,每次运行npm run tunnel
, URL 都会不一样。
我们打开这个外网 URL,如果也显示 Hello ImgMom!
,那么说明我们的内网穿透服务也已经启动成功了。接下来,便可以正式开始我们实际代码的编写。
创建 Bot 模块并初始化一个 Bot 实例
src/index.ts
文件是项目的入口文件以及 Hono Web 相关的一些逻辑,而我们具体的 Bot 逻辑其实与 Hono Web 无关,所以我们将创建一个 Bot 模块, 将 Bot 相关的具体业务逻辑全部封装到这个模块里。在 TypeScript 项目中,一个文件就是一个模块,所以让我们在 src
目录中新建一个 bot 文件。然后输入如下代码:
1 | import { Bot } from 'grammy/web'; |
GrammY 框架为我们封装了一个 Bot 类,所有 Bot 相关的逻辑都封装在这个 Bot 类中,这个类的构造函数接收一个 bot token 参数, 然后创建对应的 bot 实例对象。这里我们使用了 self.TG_BOT_TOKEN
,这是 Cloudflare Workers ( Service Worker 模式) 使用环境变量的方式,这样可以将我们的 bot token 不硬编码在代码中,更加灵活,并且可以脱敏。
如何在我们的项目中使用环境变量
为了让 self.TG_BOT_TOKEN
以及之后我们将使用到的环境变量生效,我们需要对项目进行一些设置:
打开
wrangler.toml
文件,输入如下代码:1
2[vars]
TG_BOT_TOKEN = "<我们通过 botfather 获取到的bot token>"在
[vars]
表下面配置的键值对就是我们项目可使用的环境变量。我们可以在项目代码中通过self.<变量名>
访问具体的环境变量。类型提示可以方便我们代码的编写,也可以让我们的代码更加安全。当我们在
wrangler.toml
文件中配置完我们将使用的环境变量后,并不能让 TypeScript 知道它们的存在。我们还需要手动进行一些类型的定义才能让 TypeScript 感知到它们。让我们新建worker-configuration.d.ts
, 然后输入如下代码:1
2
3
4
5
6interface Env {
TG_BOT_TOKEN: string;
}
interface ServiceWorkerGlobalScope extends Env {
}我们尝试在我们的项目代码中输入
self.<变量名>
, VSCode 自动给出了所有可使用的环境变量的提示。
实现 /setup
/setup
是一个 GET 路由,当我们向 /setup
发送 GET 请求时,它会为我们的 Telegram Bot 设置 Webhook endpoint 以及一些命令。让我们在 src/index.ts
中添加一些代码来实现这个功能。
在文件头部导入我们所需要的依赖项
1
import bot from './bot';
在文件最底部输入如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14app.get('/setup', async (ctx) => {
const host = new URL(ctx.req.url).host;
const botUrl = `https://${host}/bot`;
await bot.api.setWebhook(botUrl, {
secret_token: self.TG_WEBHOOK_SECRET_TOKEN,
});
await bot.api.setMyCommands([{
command: '/settings',
description: 'Setting up the bot',
}]);
return ctx.text(`Webhook(${botUrl}) setup successful`);
}
return ctx.text('401 Unauthorized. Please visit ImgMom docs (https://github.com/beilunyang/img-mom)', 401)
});- Hono 受 Express, Koa 等 NodeJS Web 框架的影响,实现了与它们类似的 API。通过调用
app.get([path], handler|middleware...)
方法,就可以注册一个 GET 请求的路由,当服务接收到对应的 GET 请求时,handler 或者 middleware 就会执行。 - 这里我们的 handler 会获取当前请求的 host, 然后构造出我们完整的 Webhook endpoint URL, 这里我们默认我们的 Webhook endpoint 是
/bot
。 - 之前我们在
src/bot.ts
模块中构造了一个 bot 实例对象,通过这个 bot 对象的bot.api.setWebhook
方法,我们就可以将我们的 Webhook endpoint 告知给 Telegram 服务器。 - 这里我们还提供了一个可选的
secret_token
参数,这个参数主要作用是为了增强 Webhook 调用的安全性,之后 Telegram 调用我们的 Webhook 时,都会带上这个secret_token
。我们在实现我们的 Webhook endpoint 逻辑时,就可以进行访问控制,如果发送给我们的 Webhook endpoint 请求没有携带或者携带了错误的secret_token
, 那就证明这个请求不是来自 Telegram, 可能是有人在恶意调用我们的 Webhook endpoint, 我们可以直接返回错误信息。 - 通过调用
bot.api.setMyCommands
方法,可以向 Telegram 注册命令,这里我们注册一个/settings
命令,用来在之后对我们的 Bot 进行一些设置。 ctx.text
是 Hono 提供的 API,它会构造一个包含文本数据的 Response, 当 return Response 时,就会结束整个请求的处理流程并向前端返回响应结果。
- Hono 受 Express, Koa 等 NodeJS Web 框架的影响,实现了与它们类似的 API。通过调用
在
wrangler.toml
以及worker-configuration.d.ts
中配置我们的TG_WEBHOOK_SECRET_TOKEN
环境变量1
2
3[vars]
TG_BOT_TOKEN = "my-variable"
TG_WEBHOOK_SECRET_TOKEN = 'my-secret-token'1
2
3
4
5
6interface Env {
TG_BOT_TOKEN: string;
TG_WEBHOOK_SECRET_TOKEN: string
}
interface ServiceWorkerGlobalScope extends Env {
}
让我们测试下我们的实现,看看有没有异常,是否符合我们的预期:
打开浏览器,访问
https://pregnant-mentor-reggae-answer.trycloudflare.com/setup
。此时浏览器应该显示如下信息:Webhook(https://pregnant-mentor-reggae-answer.trycloudflare.com/bot) setup successful
在 Telegram 中打开我们的 Bot,我们的 Bot 应该会多一个 Menu 按钮,点击 Menu 按钮,会显示我们刚刚通过代码注册的
/settings
命令
OKay. 显然我这边代码运行是符合预期的。成功将我们的 Webhook endpoint 注册到了 Telegram。成功为 Bot 设置了一个/settings
命令。
下一步,让我们实现下我们的 Webhook endpoint /bot
实现 /bot
Telegram 会将 Message 通过 POST 请求发送给我们注册的 Webhook,所以我们/bot
需要是一个 POST 路由,让我们在src/index.ts
文件中添加如下代码:
1 | import { webhookCallback } from 'grammy/web'; |
1 | app.post('/bot', async (ctx, next) => { |
- 通过
app.post([path], handler|middleware...)
方法可以注册一个POST
路由 - 通过
self.host = new URL(ctx.req.url).host
, 将当前请求的 host 添加到全局变量中,可以方便之后获取 host 的值 return next()
, 执行下一个 middleware 中的代码。更多关于 Hono middleware 的内容,可以查询 Hono 官方文档 https://hono.dev/docs/guides/middlewarewebhookCallback
是 GrammY 提供的一个函数, 它会为不同的 Web 框架创建中间件,其中就包含 Hono。由于之前通过bot.api.setWebhook
方法注册Webhook
时,我们设置了secretToken
,每次 Telegram 发送请求到 Webhook 时都会携带设置的secretToken
。我们可以通过webhookCallback
函数实现对这个secretToken
正确性的校验,只需要在第三个参数中设置secretToken
,webhookCallback
函数内部会用设置的secretToken
与接收到的 Telegram 请求头中的secretToken
进行对比,如果不一致,则会报错。
由于我们新增了一个全局变量 self.host
, 为了让 TypeScript 不类型报错,还需要在worker-configuration.d.ts
中添加这个全局变量及其类型信息:
1 | interface ServiceWorkerGlobalScope extends Env { |
至此, /bot
已经实现完毕,我们已经成功将我们的 GrammY Bot 实例集成到了 Hono Web 服务上,Hono Web 服务将监听到来自 Telegram 发送过来的 Webhook 请求,并将消息内容传递给 Grammy Bot 实例。
下一步,我们将实现我们 Telegram Bot 的灵魂,Bot 实例的具体业务逻辑。
实现 Bot 业务逻辑
在之前,我们已经创建了 src/bot.ts
文件,我们继续往 /src/bot.ts
文件添加如下代码:
1 | bot.use((ctx, next) => { |
bot.use(middleware)
: 添加一个中间件,在这里我们将ctx.update
的内容打印出来,方便我们调试。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[wrangler:inf] POST /bot 200 OK (9870ms)
{
"update_id": 8787067,
"message": {
"message_id": 4,
"from": {
"id": 361756774,
"is_bot": false,
"first_name": "伊卡洛斯",
"username": "beilunyang",
"language_code": "zh-hans"
},
"chat": {
"id": 361756774,
"first_name": "伊卡洛斯",
"username": "beilunyang",
"type": "private"
},
"date": 1728791059,
"text": "/help",
"entities": [
{
"offset": 0,
"length": 5,
"type": "bot_command"
}
]
}bot.command('start', middleware)
: 监听 start 命令,当用户输入/start
命令时,将触发对应的中间件,在这里我们通过ctx.reply
方法,回复 Welcome to use ImgMom。bot.command('help', middleware)
: 监听 help 命令,当用户输入/help
命令时,将通过ctx.api.getMyComands
方法,获取当前 bot 的所有可用命令,然后将所有命令名以及命令描述,回复给用户。bot.on(['message:photo', 'message:document'], middleware)
: 监听 photo 以及 document 消息,当用户向 Bot 发送 photo 以及 document 时,将通过ctx.getFile
方法获取到用户发送过来的 photo 以及 document,即 file 对象,每个 file 对象都有一个唯一的 ID (file_id), 通过 file_id ,我们就可以通过 Telegram API 重新获取到对应的 File。在这里我们将 file_id 作为我们图片外链的一部分,然后回复给用户。
至此,我们 Bot 的逻辑也就基本完成了。我们向 Telegram Bot 发送一张图片,Bot 返回图片对应的 图片外链给我们,但是我们访问这个外链时,会返回 404 Not Found
,因为我们还没有实现这个外链对应的逻辑,即 /img
,下一步,我们将实现它。
实现 /img
我们切换回 src/index.ts
, 添加/img
路由以及对应的逻辑代码:
1 | import { fileTypeFromBuffer } from 'file-type'; |
1 | app.get('/img/:fileId', async (ctx) => { |
- 我们需要知道 file 的 mime type, 设置正确的
Content-Type
, 以便浏览器或其它用户代理能正确处理我们的外链。这里我们通过file-type
包来获取 mime type。- 运行
npm i file-type
安装file-type
包
- 运行
app.get('/img/:fileId', middleware)
: 注册我们的/img
路由,:fileId
是 hono 的路由参数匹配语法,当用户访问/img/123
路由时,123
将自动赋值给 fileId , 通过ctx.req.param('fileId')
方法,即可取到 fileId 的值。- 当获取到 fileId 后,通过调用
bot.api.getFile
方法,可以获取到 fileId 对应的 file 对象。file 对象有一个 file_path 属性,通过 file_path 以及我们之前获取的TG_BOT_TOKEN
, 即可通过请求 Telegram API 获取到存储在 Telegram 服务器中的文件。 - 由于是网络请求,存在一定的失败可能,我们可以判断下请求是否成功,如果失败,则返回错误信息,这里为了简单,统一返回文案:404 Not Found……
- 如果请求成功,我们可以通过调用
res.arrayBuffer
方法,获取文件的 Buffer 二进制数据。 file-type
包提供了一个fileTypeFromBuffer
函数,将我们获取的 Buffer 数据作为参数传给它,它便会解析我们 Buffer 中 magic number ,获取到文件的 mime type。- 最后,我们只需通过
ctx.body
方法,将正确的http status code (200)
, 正确的Content-Type
以及文件的 Buffer 数据,发送给用户代理即可。
再重新访问下我们之前获取到的图片外链,此时,你应该就能看到我们发送给 Telegram Bot 的图片的内容了。
结语
本篇是《使用 TypeScript 开发你的第一个 Telegram 机器人》 系列文章的第二篇,正式带大家开始编写我们的 Bot 逻辑,通过本篇文章的学习,你应该了解了如何设置 Webhook;如何将 Grammy Bot 实例集成到 Hono Web 服务上;如何监听命令,监听 photo, document 消息,并执行不同的自定义逻辑;如何通过 file_id 获取存储在 Telegram 中的文件。并且在最后,成功实现了将 Telegram 作为图片的 OSS 对象存储服务,实现了一个免费的图床。
在之后的篇章中,我们将继续扩展我们的 Telegram 图床,让我们的图片不仅仅只存储在 Telegram 中 ,还可以存储到其它 OSS 服务中~
最后,希望各位继续关注,你的评论,收藏,点赞,转发,将是我持续更新的动力~
本文首发于公众号 悖论的技术小屋,欢迎关注。🛠️ 分享大前端,Web3,信息安全方面的知识 ❓ 日常交流答疑?知无不言