前言
在上一篇文章中,我们已经完成了 Bot 项目的创建以及一些前期准备工作。本章内容,我将带领大家开始正式业务逻辑代码的编写,并将 Telegram 作为 OSS 对象存储服务,实现基于 Telegram 的图床。
本章需要实现的功能
在正式开始之前,我先介绍下,本章我们将具体实现的功能。
- 实现一个
/setupendpoint,当我们请求/setup时,对我们的 Telegram bot 进行一些初始化设置。具体为:- 为我们的 Telegram Bot 设置 Webhook endpoint
- 为我们的 Telegram Bot 设置一些可使用的命令(command)
- 实现我们的 Webhook endpoint (
/bot),/bot将接收来自 Telegram 服务器的消息,并对消息进行处理后,返回不同的响应结果。 - 实现基础 Bot 逻辑,让 Bot 能够响应
/start,/help命令,能够监听私聊中的图片以及文档消息,并返回图片以及文档的file_id。 - 实现一个
/imageendpoint, 当请求这个 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,信息安全方面的知识 ❓ 日常交流答疑?知无不言