在 Cloudflare Workers 上部署基于 Telegraf 框架的 Telegram Bot

1639 字
8 分钟
在 Cloudflare Workers 上部署基于 Telegraf 框架的 Telegram Bot
在 Cloudflare Workers 上部署基于 Telegraf 框架的 Telegram Bot

用 serverless 解放你的服务器

Head Pic: #アークナイツ timeout - Alcxome - pixiv

Cloudflare Workers#

官网:https://workers.cloudflare.com/

我也不做过多介绍了,可以从官网上了解,简单来说它是个运行 javascript 的 serverless 平台,虽然还说支持 Rust 或者 C 等等,但实际上得将它们编译为 Wasm

有官方客户端 wrangler,但这客户端 bug 是真的多,请做好各种搜 issue 的准备(

以下描述针对免费版计划

为什么值得用:

  1. 每天免费调用十万次,超出只会终止而不扣费
  2. 最多创建 30 个 worker,管够
  3. 赠送一个专门用于调用 worker 的 workers.dev 子域,子域名自定义
  4. 可以自定义路由来使用你自己的域名
  5. 没有流量限制
  6. 可以使用 KV(cf 提供的键值数据库,2020年11月起对免费套餐开放使用了)

局限性:

  1. Serverless 的特性所决定的局限性
  2. 每个请求限制 10ms CPU 时间(网络 IO 时间不计),无法完成某些太耗算力的任务,例如较复杂的图片处理或者使用 cheerio 这类效率不高的 HTML 解析库
  3. (目前)不允许使用 websocket

关于 KV#

由于免费版套餐也可以白嫖 KV 了,这意味着你可以编写有状态的 bot 逻辑了,但 KV 的写入生效可能存在延迟,并且存在每秒一次(目前)的频率限制,不能与内存相提并论,因此请根据实际情况合理使用

Telegram Bot#

实现 Telegram Bot 的处理消息有两种方式,长轮询和 webhook,想依靠 serverless 处理则选用 webhook 方式

注意,webhook 每一次被 tg 服务器调用都只能返回一次,因此没办法做到“等待某条消息发送后再xxxx”,只能一次性发送完所有操作

我一般用 Telegraf 框架开发,所以这里主要说下怎么将使用该框架开发的 bot 迁移至 cfworker

Warning

下面的内容是对于 Telegraf 3 的,目前已经支持 Telegraf 4,具体使用方法请以 github 项目为准

迁移思路#

如果只想知道最终该怎么做可以直接跳过这一节

首先 Telegraf 的文档有说明如何处理 webhook

我一般使用 cf 官方的框架来开发 cfworker:

@cfworker/web

这是一个类 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做的适配有以下几点:

  1. 实现set()函数,用于设置 header
  2. 有header属性且为Object,由于没有实际使用所以随便糊弄也可以
  3. 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 中间件使用

Tsuk1ko
/
cfworker-middware-telegraf
Make telegraf (a telegram bot framework) useable in Cloudflare Workers
—
—
MIT
JavaScript

在你的新项目中安装这些依赖:

Terminal window
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);

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

在 Cloudflare Workers 上部署基于 Telegraf 框架的 Telegram Bot
https://moe.best/posts/cfworker-telegraf-tgbot/
作者
神代綺凛
发布于
2020-04-19
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
神代綺凛
专业咕咕
分类
标签
站点统计
文章
107
分类
15
标签
269
总字数
140,468
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录