我的动态时间线聚合

之前一段时间偶然发现了 DIYGod 创建的一个个人 Channel,内容是关于他的一些动态,包括其发布的推特内容,以及豆瓣的书影音动态等内容。这种形式的 Channel 对我来说很有意思,一方面也是一个对外界分享自身情况的平台(虽然我的内容也没什么人关注),另一个方面也是可以有个地方将自己的相关信息数据持久化地保存下来,拿到自己手上。于是我也写了点代码,整出了一个自己的时间线聚合 YeungYeah's Timeline

程序大体的工作流程是这样的

  1. Vercel 上面部署一个自己的 RSSHub,通过 rss 的方式来订阅相关的个人信息。
    • RSSHub 是一个让所有事情都可以 rss 订阅的服务,支持许多网站源,而且 文档详细,基本都可以查得到。
    • 一些网站的信息生成订阅需要相关的访问权限或者有一些访问次数的限制,比如 YouTube 保存的视频,和 Spotify 点赞的音乐,因此自己搭建最佳,搭建以后根据 文档 在环境变量进行配置即可。
    • 一般地 Vercel 部署后提供的域名在大陆访问都不太方便,需要的话可以绑一个自己的域名。
  2. 运行一个程序定时访问 RSSHub 相关接口,获取 rss 订阅内容并保存
  3. 缓存获取的数据的链接,如果发现新数据,则以一定的格式发送到 Telegram Channel 当中。
    • 要往 Telegram Channel 自动发送消息,虽然首先申请一个自己的 Channel,获取 Channel 的 ID
    • 然后在 @BotFather 处创建一个自己的 bot,并获取这个 bot 对应的访问 token。
    • 将 bot 添加为自己 Channel 的管理员,确保 bot 发送的信息可以传给 Channel
    • 通过官方提供的 API,附带访问 token,调用相应的接口,发送消息

程序的工作流程很简单,实现起来也不难。一开始我用 typescript 写了一个版本,使用 sqlite 存储数据,sequelize 作为 ORM 库,node-telegram-bot-api 作为 telegram 接口的封装库,两三天就写完上线。写起来没什么难点,只有在开发过程中一些坑点:

  1. Vercel 部署的 RSSHub 提供的访问域名和 Telegram 的接口,在大陆可能会出现不能访问的问题。在本地开发时,可以走代理,但是因为要 24 小时定期几分钟就跑一次,所以还是得放在服务器上面,服务器就不太方便走代理了,于是我放在了 CloudCone 位于美国的 VPS 上面,可以直连。
  2. 往 Channel 发送消息时不好调试,需要真实往 Channel 发送消息才能看到效果。但有时候调试或者出了点啥问题后,一下子给自己的 Channel 发个几十条信息,还得自己手动删除,幸好也没有其他人关注,只影响到了自己。
  3. Telegram 接口发送文本消息支持 Markdown 格式信息,但是如果 Markdown 内容解析异常,会直接发送失败,而且因为内容是从 rss 订阅里抽出来的,也难判断哪里会有解析的问题,只能够出一次问题,就修复一次,重新上线一次。

这也好像是我第一次用 typescript 来开发项目,整体而言还是挺爽的,即使没有学过 ts,凭着 js 的基础和 ide 的帮助,也可以爽写 ts(当然原因可能代码只是写给自己的,很多时候都不太需要复杂的类型标注)。不过刚好当时也在学习 Rust,再加上可以打包成单个可执行文件的诱惑,于是我就使用 Rust 重新实现了一个版本,并编译部署到我的服务器上面。运行起来确实好像要快了点,而且最主要的好处是,内存占用从 ts 版本的 100+M,下降到了 10+M,内存占用只有原来的十分之一,而且开发效率也还不错。

虽然写起来没什么问题,但是在打包发布上面,Rust 还是有很多槽点的。虽然 Rust 支持多个系统平台,但是很多的 crate 在系统的兼容性上都存在一定的问题,尤其是传递依赖,导致打包出来的产物还会动态依赖系统的库,或者 crate 的编译需要系统工具链的支持。我是在 Windows 上面开发调试,完成后编译一个 linux 版本的 release 一样,传到服务器上面运行。听起来很简单,但是实际上跨平台编译会出现各种问题,一些传递引入的 crate 在编译时需要 gnu 的工具链和一些在 Windows 没有的链接库,而其中的一些 crate 明确就是不支持 Windows 系统的,在解决了一个 crate 的编译问题后,又来一个,完全没有办法,只能够在 Windows Subsystem for Linux (WSL) 上面的 Ubuntu 来编译。

然而使用 WSL 也没有那么简单,代码放在 WSL 能够正常编译得到二进制产物,传到服务器上面就不能运行,提示找不到某个版本 libssl.so 的链接库。一检查发现是因为我 WSL 上的 Ubuntu 是 18.04,而服务器在跑的已经是 22.04 的,两者通过 apt 安装的 ssl 库版本不一致,导致 WSL 编译出来的产物没有办法在服务器运行。也尝试过使用基于容器的方式来进行编译,比如 cross-rs,但在编译的过程中还是会出现各种的问题导致编译不成功。

于是只能够根据依赖进行进一步的具体排查,找到跟这个动态 so 库相关的依赖,发现许多需要和使用到网络连接的依赖库,都可能间接或直接依赖于 rust-native-tls,这个库会根据系统自动选择相应的 TLS 实现 crate,来调用系统的 SSL bindings,比如在 Linux 上就会调用系统的 openssl 这个依赖库,包括常用的 http 请求库 reqwest,支持连接远程数据库的 orm 库 sea-orm,以及 telegram api 的封装库。这种通过 binding 方式依赖于具体的某个系统环境和依赖库,就使得构建过程和程序运行的可移植性大大降低。于是考虑使用 Rust 版本实现的 tls 依赖库 rustls 来全部替代掉。比较好的一点是,很多人都在 native-tls 的跨平台编译上面踩过不少坑,于是上面提到的这些库都提供了相关的 feature 配置选项,供用户使用 rustls 替换掉 native-tls,具体的设置方式可以到依赖库自身的文档和提供的 feature 查询。

1
2
3
4
[dependencies]
reqwest = {version = "0.11.16", default-features = false, features = ["rustls-tls"]}
teloxide = { version = "0.12", default-features = false, features = ["macros", "rustls"] }
sea-orm = { version = "0.11.2", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }

最后 Rust 版本的程序通过 WSL 成功编译,并能够在服务器上面运行,运行的性能和资源占用也挺不错。但是实际上,我也并不缺这一点性能,跑得快一点意义不大,服务器的内存其实也暂时充足。而程序其实上线后也常常会出现一些意料之外的小问题,或者是需要添加一些新的订阅资源,这些都需要快速迭代,快速修改,因此使用 Rust 每次修改重新编译发布,可能都要比修改代码的时间长了。于是只能又用回 ts 版本。

PS:另外偶然发现,Rust 开发过程中生成的中间产物,居然这么吃硬盘空间, 又劝退多一点了