Workers部署Telegraf 框架的Telegram Bot

发布时间: 2024-02-06 热度: 1422

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

 

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

迁移思路

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

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

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

@cfworker/web

这是一个类 Koa 框架,并包含了 Router,但 cfworker 的RequestResponse的 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 中间件使用

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

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_TOKENSECRET_PATH这类不宜写入代码的变量可以直接设置成 secret

 

2. webpack

要在 cfworker 上运行需要 webpack 一下,把依赖全打进一个文件中,然后……粗暴的复制粘贴到 cfworker 编辑器中保存就行

当然你也可以用 wrangler,可通过配置实现自动 webpack 并部署

下面是一份示例配置

 

// webpack.config.jsmodule.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);

请在下方留下您的评论.加入TG吹水群