xDroid's Blog

React js、ChatGPT 和打卡 bot(二):第一份活

上一篇里我们提到了想要做一个 Telegram bot 的想法,那么这一篇里我们来开始写第一个打卡工具吧!

那这一个 Leetcode 打卡工具的使用场景大概是这样的:

  1. 我们向 bot 转发当天写完题目的网址,它会把这个网址和当天的日期保存下来
  2. 通过某种 query 命令,我们可以查询已经存了的打卡有哪些
    就两个要求,应该很简单吧!(大概……瑟瑟发抖

Leetcode 网址长什么样子呢?

还真的问倒我了,让我先去做一道题看看(

今天的是一道 medium,提交完成之后会跳转道这样一个网址:

https://leetcode.com/problems/minimum-falling-path-sum/submissions/1151151707/?envType=daily-question&envId=2024-01-19

可以看到有时间、提交 id 和题目的名字,那么我们用一个正则表达式提取一下(ChatGPT 真聪明):

const url = "https://leetcode.com/problems/minimum-falling-path-sum/submissions/1151151707/?envType=daily-question&envId=2024-01-19";
const regex = /\/problems\/([^\/]+)\/submissions\/(\d+)\/\?.*envId=(\d{4})-(\d{2})-(\d{2})/;
const match = url.match(regex);

单元测试!

怎么能忘了单元测试呢!让我们看看怎么搞的:

  • 入口是 bun test
  • 会递归执行 *.test.ts 或者 *_test.ts 名字的文件,
  • 测试套件函数有 import { expect, test } from "bun:test";

数据库怎么搞?

之前写 mongodb 感觉还可以,但是无奈体积实在是太大了,所以这次想换个库用用看。

那么这次我用的是 prisma,有 ORM 还有 typescript 支持,算是不错啦。

这里遇到一个问题是怎么把 sqlite 的初始化和 docker compose 结合起来。结果折腾一晚上还是没看懂怎么搞,所以第二天早上转投 postgres(画风一转)。

目前大概的解决方案是这样的:

  1. postgres 有两个 compose 文件,一个负责 dev(由 docker 长期跑,这样 bun 只需要启动 index.ts),另一个做本地的测试,同时复用为服务端的配置文件。
  2. 本地和服务器的 database directory 由 production 和 test 两个文件夹里的 .env 文件指定,然后在各自的 compose.yaml 文件里读取出来。
  3. prisma 有一个生成 client 的 migrate 工具,应该是每次修改完 schema 之后都要重新跑一遍的,所以我就放在 package.json 的一个脚本里了。

嗯……嗯嗯?

然后我在这里卡了好几个小时……等到回过神来的时候天都已经黑了(

大概是遇到这么问题了呢,就是 ts application 所在的容器死活连不上 postgres。我一开始以为是端口穿透一类的问题,还使劲问 ChatGPT,还去看 Github 上有人做了一个 MVP 出来可以跑。然后我就使劲想为什么我的不能跑,最后在一次不起眼的比较中我发现它写的数据库地址

DATABASE_URL="postgresql://...@postgres:5432/..."

居然不是用 localhost!真的是好坑。

还有一个问题是 bun 和 prisma 的相性也不太好,至少官方文档

Note - At the moment Prisma needs Node.js to be installed to run certain generation code. Make sure Node.js is installed in the environment where you’re running bunx prisma commands.

能连上数据库之后我就尝试从 Telegram 那边发个消息过来,结果乐极生悲

Database schema is up to date!
up and running
Segmentation fault (core dumped)
error: script "start:migrate:prod" exited with code 139

呃……我们继续 debug 吧。

宿主 distro

后来我发现可能是和宿主的 distro 有关系,毕竟上一节里面我们也提到说还是要安装 node 的。目前我是这么解决的:

FROM oven/bun:debian as base
RUN apt-get -y update; apt-get -y install curl
ARG NODE_VERSION=21
RUN curl -L https://raw.githubusercontent.com/tj/n/master/bin/n -o n \
  && bash n $NODE_VERSION \
  && rm n \
  && npm install -g n

好文明

  • Typescript 可以很方便地写 data bean 了(见 Object Types)。
  • mock 是个好东西,可以方便地覆盖一些函数的行为,然后就可以在单元测试里短路了(比如跳过从外界读入文件内容)。更黑魔法的东西是 .spyOn,居然可以窥探到没有 export 过的函数(谁叫 ts 的访问模型做的这么粗暴呢)(虽然最后还是因为不好用所以暂时放弃了)(哦最后还是用了更多的黑魔法 work 了)。
  • 函数重载终于看起来好一点了,虽然还是要手写 undefined 的判断。
  • 本来以为 bun test runner 可以测试改时间的,结果
    94 | function setSystemTime(arg0: Date) {
    95 |   throw new Error("Function not implemented.");
                 ^
    error: Function not implemented.
    (咚咚咚)查了一下官方文档说

    Timers — Note that we have not implemented builtin support for mocking timers yet, but this is on the roadmap.

哪天做的题?

现在遇到的一个问题是 Leon 告诉我说,他那边做完题之后网址是不含日期的,所以我可能得自己想办法找到哪一天对应哪一题。

我问了问 ChatGPT,ta 给我仙人指路到了这个网页 which used to be working 但是现在他们把这个网页移除了。emmm 看起来不太好办的样子。

BTW 现在 leetcode 的每日任务放到了 problems 页面的右上角(而且是用非常愚蠢的 UTC 时区计时);拿开发者工具看了一下,似乎 leetcode 现在在用 GraphQL。我试着把请求复制到 ts 里,去掉 cookies 一类的东西运行了一下,看起来居然能正常请求到!也是少见。

不过我还是遇到了类型系统的问题,毕竟原生 fetch 函数的签名是

export declare class Response implements BodyMixin {
  // ...
  readonly text: () => Promise<string>
  // ...
}

我看了一下 graphQL 返回的 json 长这样:

{data: {
    dailyCodingChallengeV2: {
      challenges: [[Object ...], ...],
      weeklyChallenges: [[Object ...], ...],
    },
},}

搞得我只好写出这种代码:

const data = await response.json() as { data: { dailyCodingChallengeV2: { challenges: ChallengeBean[] } } };
const challenges = data.data.dailyCodingChallengeV2.challenges;

Unit test, again

现在写代码总算有点眉目了:如果遇到没有日期的网址,那就先查本地缓存,如果也没有的话就到 leetcode 上查找最近 12 个月里面有没有这个题。

Prisma 似乎有自己一套 mock test 的办法,可惜我并看不懂,所以还是老老实实用 beforeAll() 一类的方法真的写到数据库里算了。

(又睡了一觉)

想了一下还是把单元测试加回到 Dockerfile 里面去好了,但是试了一下发现跑 test 的时候并没有 prisma,所以有些数据库的东西跑不通(听起来很头疼的样子)。所以 prisma 还是得用 mock 的方式在 test 里跑。

这里查插一个怎么检查 .dockerignore 文件是否起效果的命令

$ rsync -avn . /dev/shm --exclude-from .dockerignore

看起来我的 .env 文件也没有打包进去啊……奇怪……

emmm 又看了一会儿现在的解决方案,好像 prisma 的单元测试用的 mock 又是一些 jest 的黑魔法,和 bun 不是很兼容。索性我把关于数据库的测试在这里短路掉算了。

CI

As a closing remark,我们来看看怎么搞 CI。

因为不会写 workflow file,所以直接问了 ChatGPT(然后发现 workflows 文件夹少了一个 s 导致 Github 识别不出来)。现在他们已经把 CI 做的相当好了,刚才的一个 commit 也自动 build 成功了,yeah!

下期预告

TBA