神代綺凜

在 Cloudflare Workers 上部署基于 Telegraf 框架的 Telegram Bot
用 serverless 解放你的服务器
扫描右侧二维码阅读全文
19
2020/04

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

用 serverless 解放你的服务器

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

20200507 更新:修正先前给出的解决方案中getKoaLikeRes()的一处错误,set()需要返回一个boolean来代表是否成功

Cloudflare Workers

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

我也不做过多介绍了,可以从官网上了解,简单来说它是个主要支持 javascript 的 serverless 平台

虽然还说支持 rust c c++,但毫无感觉,官方示例清一色 javascript,可能执行其他语言需要用官方客户端 wrangler,但这客户端 bug 是真的多,我 Win10 完全无法正常使用(

以下描述针对免费版计划

为什么值得用:

  1. 每天免费调用十万次,超出只会终止而不扣费
  2. 最多创建 30 个 worker,管够
  3. 赠送一个专门用于调用 worker 的 workers.dev 子域,子域名自定义
  4. 可以自定义路由来使用你自己的域名
  5. 没有流量限制

局限性:

  1. Serverless 的特性所决定的局限性
  2. 每个请求限制 10ms CPU 时间(网络 IO 时间不计),无法完成某些耗时长的任务,例如使用 cheerio 解析比较大的 DOM
  3. (目前)不允许使用 websocket

Telegram Bot

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

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

迁移思路

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

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

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

这是一个类 Koa 框架,但 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;
        } else Reflect.set(...arguments);
      },
    }
  );
}

迁移流程

我为以上解决方案写了个 npm 包,可以直接安装使用

npm i cfworker-response-for-telegraf

1. 写代码

主入口应该为这样的结构

const Telegraf = require('telegraf');
const { Application } = require('@cfworker/web');
const { Router } = require('@cfworker/web-router');
const crft = require('cfworker-response-for-telegraf');

const bot = new Telegraf(TG_BOT_TOKEN);

// 写 bot 逻辑,但不要 bot.launch()
// ...

const router = new Router();

// SECRET_PATH 指的是一个不容易猜到的路径,以防止他人访问你的 webhook
// 可以滚键盘或者用 UUID 之类的生成,例如 '/d4507ff0-08d1-4160-bad8-1addf587374a'
router.post(SECRET_PATH, async ({ req, res }) => {
  await bot.handleUpdate(await req.json(), crft(res));
  res.status = 200;
});

new Application().use(router.middleware).listen();

所以很简单,把这段代码抄走然后补上你的 bot 逻辑即可

顺带一提,cfworker 支持设置 secret,它们会成为代码中的全局变量,所以像TG_BOT_TOKENSECRET_PATH这类不宜写入代码的变量可以直接设置成 secret

2. webpack

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

下面是一份示例配置

// 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/d4507ff0-08d1-4160-bad8-1addf587374a');

// 删除 webhook
// bot.telegram.deleteWebhook();

// 查看当前 webhook
// bot.telegram.getWebhookInfo().then(console.log);
搬瓦工VPS优惠套餐,建站稳如狗,支持支付宝,循环出账94折优惠码BWH3HYATVBJW
年付$47CN2线路,1核/1G内存/20G硬盘/1T@1Gbps【点击购买】($28套餐已经不再销售)
年付$47CN2 GIA线路,1核/512MB内存/10G硬盘/500G@1Gbps【点击购买】(可能已售罄)
Last modification:May 14th, 2020 at 11:42 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment

5 comments

  1. LYM  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 81.0.4044.138(Google Chrome 81.0.4044.138)
    (๑•̀ㅁ•́ฅ)
  2. jelwell  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 79.0.3945.88(Google Chrome 79.0.3945.88)
    技术大牛。厉害啊。
  3. unlsycn  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 84.0.4115.0(Google Chrome 84.0.4115.0)
    之前是部署在Google Apps上的,没有框架可用写得我蛋疼,终于可以解脱了(
  4. 夏目  Mac OS X 10.12.6(Mac OS X 10.12.6) / Google Chrome 80.0.3987.163(Google Chrome 80.0.3987.163)
    相比部署在VPS上,在CloudFlare Worker上部署Bot稳定性和可用性应该都更高一些.
    1. 神代綺凜  Mac OS X(Mac OS X) / Safari(Safari)
      @夏目 没错,而且不再需要花精力去维护 VPS