在上一篇文章中,我们已经完成了 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 Workers
npm run tunnel : 内网穿透,将通过 npm run dev运行的本地项目暴露到公网上
我们现在需要本地启动项目,所以需要运行 npm run dev 。运行之后,我们将在终端中看到如下信息:

import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
export default app
这个代码是由 Hono 脚手架生成的模板代码。我们需要进行一些小的修改,以方便我们之后的开发。
1
2
3
4
5
6
7
8
9
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello ImgMom!')
})
app.fire()
我们将其中的 Hello Hono! 更改为 Hello ImgMom! 。
将 export default app 删除,并添加 app.fire() 。
Cloudflare Workers 有两种模式:ESModule Workers 以及 Service Workers。 Hono 对这两种模式进行了抽象封装,对应的启用方式为:
export default app : 使用 ESModule Workers
app.fire() : 使用 Service Workers
我们的 Bot 将使用 Service Workers 模式运行
重新运行下npm run dev , 此时,如果你看到了Hello ImgMom! 这段文字,那么说明我们的项目更改已经生效。
我们接下来再启
其中 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 文件。然后输入如下代码:
import { Bot } from 'grammy/web';
const bot = new Bot(self.TG_BOT_TOKEN)
export default bot;
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 , 然后输入如下代码:
}
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
app.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 时,就会结束整个请求的处理流程并向前端返回响应结果。
在 wrangler.toml以及 worker-configuration.d.ts 中配置我们的 TG_WEBHOOK_SECRET_TOKEN环境变量
[vars]
TG_BOT_TOKEN = "my-variable"
TG_WEBHOOK_SECRET_TOKEN = 'my-secret-token'
interface 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 文件中添加如下代码:
import { webhookCallback } from 'grammy/web';
app.post('/bot', async (ctx, next) => {
self.host = new URL(ctx.req.url).host;
return next();
}, webhookCallback(bot, 'hono', {
secretToken: self.TG_WEBHOOK_SECRET_TOKEN
}));
通过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/middleware
webhookCallback 是 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 中添加这个全局变量及其类型信息:
interface ServiceWorkerGlobalScope extends Env {
host: string;
}
至此, /bot 已经实现完毕,我们已经成功将我们的 GrammY Bot 实例集成到了 Hono Web 服务上,Hono Web 服务将监听到来自 Telegram 发送过来的 Webhook 请求,并将消息内容传递给 Grammy Bot 实例。
下一步,我们将实现我们 Telegram Bot 的灵魂,Bot 实例的具体业务逻辑。
实现 Bot 业务逻辑
在之前,我们已经创建了 src/bot.ts 文件,我们继续往 /src/bot.ts 文件添加如下代码:
bot.use((ctx, next) => {
console.log(JSON.stringify(ctx.update, null, 2));
return next();
});
bot.command('start', (ctx) => ctx.reply('Welcome to use ImgMom'));
bot.command('help', async (ctx) => {
const commands = await ctx.api.getMyCommands();
const info = commands.reduce((acc, val) => `${acc}/${val.command} - ${val.description}\n`, '');
return ctx.reply(info);
});
bot.on(['message:photo', 'message:document'], async (ctx) => {
const file = await ctx.getFile();
const tgImgUrl = `https://${self.host}/img/${file.file_id}`;
return ctx.reply(
`Successfully uploaded image!\nTelegram:\n${tgImgUrl}`
);
});
bot.use(middleware) : 添加一个中间件,在这里我们将ctx.update 的内容打印出来,方便我们调试。
[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 路由以及对应的逻辑代码:
import { fileTypeFromBuffer } from 'file-type';
app.get('/img/:fileId', async (ctx) => {
const fileId = ctx.req.param('fileId');
const file = await bot.api.getFile(fileId)
const res = await fetch(`https://api.telegram.org/file/bot${self.TG_BOT_TOKEN}/${file.file_path}`);
if (!res.ok) {
return ctx.text('404 Not Found. Please visit ImgMom docs (https://github.com/beilunyang/img-mom)', 404);
}
const bf = await res.arrayBuffer()
const fileType = await fileTypeFromBuffer(bf)
return ctx.body(bf, 200, {
'Content-Type': fileType?.mime ?? '',
});
});
我们需要知道 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 服务中~

发表评论 取消回复