花了半个多月实现了一个 收集游戏活动档线,绘制成排名图片,并发布到QQ群上
的机器人,简单聊一聊开发的动机、遇到的难点和架构的变迁,细致末节的东西就不赘述了,太多了
Github仓库:https://github.com/fun4wut/mltd-zh-functions
开发动机
我所接触的游戏是《偶像大师 百万现场》的繁中服,一款音游,判定宽松,难度不高,人少,活动牌子比较好拿。但是冲活动经常需要知道当前的活动档线,以便决定是继续冲分还是稍微歇一会。这样的BOT不少,Twitter上就有专门的活动档线BOT,但是毕竟上Twitter要科学上网,同时也不方便。那时候我比较闲,便想着把BOT做到QQ群上,满足大家和自己的需求,漫长的踩坑开发便由此开始了。
日服的活动档线图如下所示
最初与最大的难点:数据获取
这是我最先遇到的坑。日版普罗丢瑟比较积极,制作了专门的公式网站,同时还有现成的API可供调用,拿到当前活动的档线。同时也提供了繁中版的接口,但当我尝试之后发现,繁中版并没有提供档线,只提供了活动的基本信息(何时开始,何时结束)。
转换思路,没有轮子,那就造个轮子,直接对游戏抓包。由于现在大部分app都用了 SSL Pining
,得找个越狱的iOS或者root的安卓来操作,由于我的电脑装了WSL2,模拟器没法用,我不得已祭出了被尘封半年的root过的一加5。装上 xposed
和 justtrustme
,配好 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,反正主要功能都实现了,剩下的就看我有没有时间吧(逃
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!