使用 Telegram Bot + Beancount 记账

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

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

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

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

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

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

import TelegramBot from 'node-telegram-bot-api';
 
const token = 'YOUR_TELEGRAM_BOT_TOKEN';
const bot = new TelegramBot(token, { polling: true });

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

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

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

更新 beancount 文件

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

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

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 文件也有了一个新的提交,完工!

Telegram Beancount Bot

Footnotes

  1. [特别篇 04] - 四位主播的无主题闲聊: https://pythonhunter.org/episodes/sp04 (opens in a new tab)

  2. Beancount —— 命令行复式簿记: https://wzyboy.im/post/1063.html (opens in a new tab)

  3. Beancount 复式记账(一):为什么: https://byvoid.com/zhs/blog/beancount-bookkeeping-1/ (opens in a new tab)

  4. How do I create bot: https://core.telegram.org/bots#3-how-do-i-create-a-bot (opens in a new tab) 2

CC BY-NC 4.0 © Yuexun Jiang
GitHubTwitterRSS