花了半个多月实现了一个 收集游戏活动档线,绘制成排名图片,并发布到QQ群上 的机器人,简单聊一聊开发的动机、遇到的难点和架构的变迁,细致末节的东西就不赘述了,太多了

Github仓库:https://github.com/fun4wut/mltd-zh-functions

开发动机

我所接触的游戏是《偶像大师 百万现场》的繁中服,一款音游,判定宽松,难度不高,人少,活动牌子比较好拿。但是冲活动经常需要知道当前的活动档线,以便决定是继续冲分还是稍微歇一会。这样的BOT不少,Twitter上就有专门的活动档线BOT,但是毕竟上Twitter要科学上网,同时也不方便。那时候我比较闲,便想着把BOT做到QQ群上,满足大家和自己的需求,漫长的踩坑开发便由此开始了。

日服的活动档线图如下所示

最初与最大的难点:数据获取

这是我最先遇到的坑。日版普罗丢瑟比较积极,制作了专门的公式网站,同时还有现成的API可供调用,拿到当前活动的档线。同时也提供了繁中版的接口,但当我尝试之后发现,繁中版并没有提供档线,只提供了活动的基本信息(何时开始,何时结束)。

转换思路,没有轮子,那就造个轮子,直接对游戏抓包。由于现在大部分app都用了 SSL Pining,得找个越狱的iOS或者root的安卓来操作,由于我的电脑装了WSL2,模拟器没法用,我不得已祭出了被尘封半年的root过的一加5。装上 xposedjusttrustme,配好 charles 证书,准备开冲。却遇到了游戏都无法打开的问题,这下确实难住了,万幸的是在Google的帮助下,我找到了这篇博客,发现了问题所在,原来是流量没有全部走 Charles ,官方API的流量依然走了手机本地,而因为这个游戏需要fq才能玩,所以导致了游戏无法加载的问题,找到了问题所在,那也就好解决了,安卓下可以使用 Drony 来把一个应用的所有流量打到代理服务器上,因为 Drony 的实现是VPN,可以劫持全部流量;而直接在Wifi上配置的代理服务器只是配的系统代理,应用完全可以不遵守。

★ Root NOT needed ★
Proxy that can operate with proxy authentications.
Android OS has just proxy with no authentication.
So this app can help you with your corporate/university/school network environment.

第二个问题就在于抓来的数据是经过加密的,但上面这篇博客也同样给出了解密方法,照着它的指示做即可。至此,这个项目最大的难点其实也就被攻克了。

Ver 0.1:lazy抓取

最开始的打算是档线数据懒抓取,即,只有我去主动请求这个活动的档线,才会去抓取,并保存至数据库,同时因为游戏的档线半小时更新一次,需要给数据库的记录设置过期时间。

export async function searchDB<T extends MLTDBase>(
  db: Db,
  name: ColletionName,
  evtId: number
): Promise<IResult<T, MLTDBase>> {
  const collection = db.collection(name)
  const obj = await collection.findOne<WithUpdTime<T>>({
    evtId: { $eq: evtId },
  })
  return result(
    () =>
      !!obj && new Date().getTime() - obj.updateTime.getTime() < 1000 * 30 * 60,
    obj!,
    {
      evtId,
      evtName: Dict.get(evtId)!.evtName,
      evtType: Dict.get(evtId)!.evtType,
    } // 过期了或者还没写入db,进入failed
  )
}

Ver 0.2:采用ORM

自带的Mongo API比较底层,操作起来不方便,最后发现 Mongoose 很好用,但是配合 Typescript,需要写两遍类型定义。

// schema定义
const blogSchema = new Schema({
    title:  String, // String is shorthand for {type: String}
    author: String,
    body:   String,
    comments: [{ body: String, date: Date }],
});

// 接口定义

interface BlogSchema {
    title: string
    author: string
    body: string
    comments: Array<{
        body: string
        date: Date
    }>
}

幸好有 Typegoose,可以完美满足我的需求:

class Blog {
    @prop()
    public title: string

    @prop()
    public author: string

    @prop()
    public body: string

    @prop()
    public comments: Array<{
        body: string
        date: Date
    }>
}
const BlogSchema = getModelForClass(Blog)

Ver 0.25 定时任务,错误处理

初版的lazy模式看起来很酷,但是有致命劣势,那就是数据信息不全,而且历史数据会被清掉,非常僵硬。最后决定还是返璞归真,每半小时定时抓取比较好。

随着架构的发展,很多问题也开始浮现,最典型的就是你抓取的档线可能不是最新的,或者你的登录信息过期了,这些该如何解决?最简单的方法就是,打Log,多试几次

wrappedFetch(url: string, data: any) {
    return promiseRetry(
      (retry, times) =>
        axios
          .post(url, data)
          .then(async res => {
            const json = await decRes(res.data)
            if (!!json.error) {
              throw new Error('token gg')
            }
            return json
          })
          .catch(async err => {
            if (err?.message === 'token gg' || err?.response?.status === 401) {
              this.logger.warn('土豆身份过期,重新登录')
              await this.login()
            } else {
              this.logger.warn(`抓包失败,重新尝试,尝试次数${times}`)
              await this.login()
            }
            return retry(err)
          }),
      {
        retries: 3,
      }
    )

Ver 0.5 前后端分离

游戏数据的抓取和QQ Bot的应答都是放在一起的,这样也会有一个大问题,两者太过于耦合了。也不利于后期的维护和发展,所以决定把数据的抓取做成API,放到云上,供QQ Bot调用,这里我是用的是Azure Function。开发方便,文档齐全,维护也比较方便。

Ver 1.0 文字版done

架构分离完,我便把QQ BOT放到了群上,效果图如下,基本的展示已经不成问题了。同时还要注意一下过往档线可能为null的问题,做判空处理

Ver 2.0 图片版

饭一口一口吃,文字版已经做出来,那必然要往图片版去做,回去翻开头的图,一个图片版档线,如何制作,最讨巧的方法其实是用HTML绘制,然后用 puppeteer 等无头浏览器截图。

知道了大致的思路,那么就可以考虑具体实现了。由于Azure Function倡导的是分解功能,把绘制HTML,截图分到另一个Function里应该是一个更优雅的办法。消息的传递可以依靠 Storage Queue 来传递,64kb对于一个简单的HTML来说,足矣。

还有一个是使用 Serverless 方案才会有的问题,那就是不支持中文字体,你也不可能通过 apt 来安装,不过,感谢浏览器技术,我们可以通过 webfont 来导入中文字体,而且因为网页只是在本地跑,所以字体文件的大小也不care,多大都行。

@font-face {
    font-family: "NotoSansCJK";
    src: url("./NotoSansCJK-Regular-1.otf");
}
* {
    font-family: "NotoSansCJK";
}

最后的效果展示:

The End?

这个项目应该还会继续加feature,反正主要功能都实现了,剩下的就看我有没有时间吧(逃



经验分享      Serverless 抓包 BOT

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!