使用 Telegram Bot + Beancount 记账

由于最近听的捕蛇者说的这期播客节目,去了解了一下经常听到但是从来没去了解过的 Beancount,看了几篇网上的博文初步使用了几天。Beancount 的记账模式我非常的喜欢,但是美中不足的是基于文本很难能够进行随手记录。

发现了一款需要注册的 Beancount 应用,但和我想象中的不太一样,我希望的是能够通过 iCloud 读取 Beancount 文本并且进行快速的添加记录的应用。然后发现了一篇博文讲到通过 Telegram Bot 来进行记账的方式,看起来非常的靠谱。发现这种方式也不错,于是就开始了折腾起来。

首先,第一个问题是如何在 Telegram 里面方便的进行记录,如果是输入 beancount 语法的话就会非常的麻烦:

2021-03-13 * "Ahonn.me" "赞助 Ahonn 买一杯咖啡"
  Expenses:Blog:Donate 20 CNY
  Liabilities:CreditCard:CMB -20 CNY

原先是打算自己写一个 parser 来通过简化的语义命令来转换为 Beancount 的,偶然间发现了 Costflow Parser,简直就是我所需要的,节省了写 parser 的工作。虽然 Costflow 是可以 Self-Hosted 的,但是作为爱折腾的我来说,这种事情是我可以自己折腾的。

有了方便输入的文本格式转换之后,第二个问题就是怎么写 Telegram Bot,这里我通过 node-telegram-bot-api 来实现:

import TelegramBot from 'node-telegram-bot-api';

const token = 'YOUR_TELEGRAM_BOT_TOKEN';
const bot = new TelegramBot(token, { polling: true });

Telegram Bot 的 Token 可以很容易的通过 @BotFather 这个机器人来得到。然后这里又遇到了问题,Telegram Bot 是有两种实现的方式:一种是通过设置 polling 来进行轮训消息处理;另一种是通过 webhook 的方式让 Telegram Bot 有输入的时候进行触发。这里我选择了我更喜欢的 webhook 的方式,好处是说不定未来还可以通过其他的方式(IFTTT 之类的)来触发。

作为 Vercel 的无脑粉丝自然是希望能够将机器人部署在 Vercel 上了。搜索了许久终于找到 Build a serverless Telegram chatbot deployed using Vercel 这篇文章,无脑照做。于是就变成了下面这样:

import TelegramBot from 'node-telegram-bot-api';
import costflow from 'costflow';

const config = {
  mode: 'beancount',
  currency: 'CNY',
  timezone: 'Asia/Hong_Kong',
  account: {
    信用卡: 'Liabilities:CreditCard:CMB',
    捐赠: 'Expenses:Blog:Donate',
    // ...
  },
};

module.exports = async (req: NowRequest, res: NowResponse) => {
  const bot = new TelegramBot(BOT_TOKEN);

  const { message } = req.body;

  if (message) {
    const {
      chat: { id },
      text,
      message_id,
    } = message as TelegramBot.Message;

    try {
      const { output } = await costflow.parse(text, config);
      bot.sendMessage(id, output, { reply_to_message_id: message_id });
    } catch (e) {
      bot.sendMessage(id, e.message, {
        reply_to_message_id: message_id,
      });
    }
  }

  res.send('OK');
};

发布到 Vercel,设置对应的机器人的 webhook,大功告成!按照 Cosflow 的语法在 Telegram 的机器人上进行输入:给 ahonn.me 捐赠 20 CNY 信用卡 > 捐赠 就可以得到对应的记录文本了。

更新 beancount 文件

在 Telegram 的机器人入口搞定了,那么现在要解决的就是存储的问题。如果 beancount 文件存放在本地,机器人很难能够进行更新。所以最后决定把 beancount 文件放在 GitHub 上,这样就可以通过 webhook 来添加记录,本地 git pull 更新之后就可以愉快的使用 fava 进行可视化查看。

一切都非常的顺利,GitHub 对应的操作只需要生成一个 personal access tokens 配合 octokit 就可以了:

const response = await octokit.request(
  'GET /repos/{owner}/{repo}/contents/{path}',
  {
    owner: OWNER,
    repo: REPO,
    path: 'txs/2021.bean',
  },
);

const { content: encodeContent, encoding, sha, path } = response.data;
const content = Buffer.from(encodeContent, encoding).toString();

await octokit.request('PUT /repos/{owner}/{repo}/contents/{path}', {
  path,
  sha,
  owner: OWNER,
  repo: REPO,
  message: text!,
  content: Buffer.from(`${content}${output}\n\n`).toString('base64'),
});

在 Telegram 进行记录,同时 GitHub 上的 beancount 文件也有了一个新的提交,完工!

Made With BlogIt