在 Cloudflare Workers 上部署基于 Telegraf 框架的 Telegram Bot
当前页面是本站的「Baidu MIP」版。查看和发表评论请点击:完整版 »
用 serverless 解放你的服务器
Head Pic: #アークナイツ timeout - Alcxome - pixiv
Cloudflare Workers
官网:https://workers.cloudflare.com/
我也不做过多介绍了,可以从官网上了解,简单来说它是个运行 javascript 的 serverless 平台,虽然还说支持 Rust 或者 C 等等,但实际上得将它们编译为 Wasm
有官方客户端 wrangler,但这客户端 bug 是真的多,请做好各种搜 issue 的准备(
以下描述针对免费版计划
为什么值得用:
- 每天免费调用十万次,超出只会终止而不扣费
- 最多创建 30 个 worker,管够
- 赠送一个专门用于调用 worker 的 workers.dev 子域,子域名自定义
- 可以自定义路由来使用你自己的域名
- 没有流量限制
- 可以使用 KV(cf 提供的键值数据库,2020年11月起对免费套餐开放使用了)
局限性:
- Serverless 的特性所决定的局限性
- 每个请求限制 10ms CPU 时间(网络 IO 时间不计),无法完成某些太耗算力的任务,例如较复杂的图片处理或者使用 cheerio 这类效率不高的 HTML 解析库
- (目前)不允许使用 websocket
关于 KV
由于免费版套餐也可以白嫖 KV 了,这意味着你可以编写有状态的 bot 逻辑了,但 KV 的写入生效可能存在延迟,并且存在每秒一次(目前)的频率限制,不能与内存相提并论,因此请根据实际情况合理使用
Telegram Bot
实现 Telegram Bot 的处理消息有两种方式,长轮询和 webhook,想依靠 serverless 处理则选用 webhook 方式
注意,webhook 每一次被 tg 服务器调用都只能返回一次,因此没办法做到“等待某条消息发送后再xxxx”,只能一次性发送完所有操作
我一般用 Telegraf 框架开发,所以这里主要说下怎么将使用该框架开发的 bot 迁移至 cfworker
!> 下面的内容是对于 Telegraf 3 的,目前已经支持 Telegraf 4,具体使用方法请以 github 项目为准
迁移思路
如果只想知道最终该怎么做可以直接跳过这一节
首先 Telegraf 的文档有说明如何处理 webhook
我一般使用 cf 官方的框架来开发 cfworker:
这是一个类 Koa 框架,并包含了 Router,但 cfworker 的Request
和Response
的 API 和 Koa 略有不同,具体可以见官方文档
综合考虑决定参考 Telegraf 文档中 Koa 的示例,使用handleUpdate
方法来处理 webhook
这个函数接受两个参数,第一个是 webhook 请求的 payload,也就是JSON.parse
body 所得到的内容,这个只要await req.json()
就可以得到
而第二个参数是一个 http.ServerResponse,但 web-router 的ResponseBuilder
由于 API 不同所以不能直接拿来用,需要改造一下
Telegraf 框架的这段代码作用就是产生 webhook 的应答,因此我从这里入手进行分析
function isKoaResponse (response) {
return typeof response.set === 'function' && typeof response.header === 'object'
}
function answerToWebhook (response, payload = {}, options) {
if (!includesMedia(payload)) {
if (isKoaResponse(response)) {
response.body = payload
return Promise.resolve(WEBHOOK_REPLY_STUB)
}
if (!response.headersSent) {
response.setHeader('content-type', 'application/json')
}
return new Promise((resolve) => {
if (response.end.length === 2) {
response.end(JSON.stringify(payload), 'utf-8')
return resolve(WEBHOOK_REPLY_STUB)
}
response.end(JSON.stringify(payload), 'utf-8', () => resolve(WEBHOOK_REPLY_STUB))
})
}
return buildFormDataConfig(payload, options.agent)
.then(({ headers, body }) => {
if (isKoaResponse(response)) {
Object.keys(headers).forEach(key => response.set(key, headers[key]))
response.body = body
return Promise.resolve(WEBHOOK_REPLY_STUB)
}
if (!response.headersSent) {
Object.keys(headers).forEach(key => response.setHeader(key, headers[key]))
}
return new Promise((resolve) => {
response.on('finish', () => resolve(WEBHOOK_REPLY_STUB))
body.pipe(response)
})
})
}
根据 cfworker 中Response
的 API,其body
是支持被赋值Stream
的,因此向 KoaResponse 靠拢会比较好处理,不需要考虑 pipe 的问题
那么就需要使isKoaResponse(response)
为true
,根据这段代码我们需要对 web-router 的ResponseBuilder
做的适配有以下几点:
- 实现
set()
函数,用于设置 header - 有
header
属性且为Object
,由于没有实际使用所以随便糊弄也可以 body
支持被赋值一个Object
(但Array
最好也考虑进去,与 Koa API 一致),相当于构造一个 JSON Response
考虑到第三点,做一个 Proxy 就可以实现
function getKoaLikeResponse(res) {
return new Proxy(
Object.assign(res, {
set: (...args) => res.headers.set(...args),
header: res.headers,
}),
{
set(obj, prop, value) {
if (prop === 'body' && ['Object', 'Array'].includes(Object.getPrototypeOf(value).constructor.name)) {
obj.body = JSON.stringify(value);
obj.headers.set('content-type', 'application/json');
return true;
}
return Reflect.set(...arguments);
},
}
);
}
迁移流程
你可以选择直接使用我写好的模板 Tsuk1ko/cfworker-telegraf-template 来建仓库,或者是根据下面的步骤自己动手
0. 安装依赖
我将以上解决方案写了个 npm 包,可以直接作为 cfworker 中间件使用
[button color="dark" icon="fa fa-github" url="https:\/\/github.com\/Tsuk1ko\/cfworker-middware-telegraf"]Tsuk1ko/cfworker-middware-telegraf[/button]
在你的新项目中安装这些依赖:
npm i @cfworker/web cfworker-middware-telegraf telegraf
npm i -D webpack webpack-cli
1. 写代码
主入口应该为这样的结构
const { Telegraf } = require('telegraf');
const { Application, Router } = require('@cfworker/web');
const createTelegrafMiddware = require('cfworker-middware-telegraf');
const bot = new Telegraf('BOT_TOKEN');
// 写 bot 逻辑,但不要 bot.launch()
const router = new Router();
// `/SECRET_PATH` 指的是一个不容易猜到的路径,以防止他人访问你的 webhook
// 可以滚键盘或者用 UUID 之类的生成,例如 '/d4507ff0-08d1-4160-bad8-1addf587374a'
router.post('/SECRET_PATH', createTelegrafMiddware(bot));
new Application().use(router.middleware).listen();
所以很简单,把这段代码抄走然后补上你的 bot 逻辑即可
顺带一提,cfworker 支持设置 secret,它们会成为代码中的全局变量,所以像TG_BOT_TOKEN
和SECRET_PATH
这类不宜写入代码的变量可以直接设置成 secret
2. webpack
要在 cfworker 上运行需要 webpack 一下,把依赖全打进一个文件中,然后……粗暴的复制粘贴到 cfworker 编辑器中保存就行
当然你也可以用 wrangler,可通过配置实现自动 webpack 并部署
下面是一份示例配置
// webpack.config.js
module.exports = {
target: 'webworker',
entry: './index.js',
mode: 'production',
node: {
fs: 'empty',
},
module: {
rules: [
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
],
},
};
至于其中的 node 配置是干嘛的,你可以看 webpack 文档
如果在打包时出现找不到模块错误,就说明你的依赖中用到了 webworker 不支持的 node 模块或全局变量,需要配置上面所说的 node
如果代码实际运行时不会用到这些模块或全局变量,则 empty 就可以解决;如果实际用到则需要尝试 webpack 提供的 polyfill 或 mock,如果仍无法正常执行那就只能放弃使用该依赖了
3. 设置 webhook
接着我们设置 bot 的 webhook,另起一个 js 在本地执行一下即可
只用执行一次即可,这段不需要放进 bot 代码中
const Telegraf = require('telegraf');
const bot = new Telegraf(TG_BOT_TOKEN);
// 设置 webhook,请改成你自己的回调地址
bot.telegram.setWebhook('https://name.subdomain.workers.dev/SECRET_PATH');
// 删除 webhook
// bot.telegram.deleteWebhook();
// 查看当前 webhook
// bot.telegram.getWebhookInfo().then(console.log);