利用 Electron 版 QQ 复活被封的 QQ 群

前段时间一个多年的 QQ 群被和谐了，从大学就进入到这个群里，我对这个群还是有些感情，平时有空就在群里水两句，难免感到有些惋惜。

想到能不能从 QQ 应用本身入手，通过 OCR 群友列表的方式，拿到群友 qq 号，然后使用 xxx@qq.com 的形式群发邮件引导群友进入新群

但这个方案我这无法操作。

当时点击了被封 QQ 群封群 Dialog 提示中的退出群聊按钮，该群在 QQ 群列表中已不存在了，也就无法操作 UI，那么只有找一个登过该 QQ 群的设备，并未点击过封群 Dialog 提示中的 “退出群聊” 按钮。可惜我没有...

OCR 不行，那么只能从本地数据库入手，在网上搜索了一番，比想象中难度高。

首先 QQ 应用的本地 db 文件是加密的，好不容易在吾爱破解上找到一篇帖子： [调试逆向] 撬开 MacQQ 的本地 SQLite 数据库，奈何操作难度太高，遂放弃。

幸好，经群友提醒，新版的 Electron QQ，同步数据之后，会回到封群时的初始状态，右键该群，可以打开群聊窗口。

哈，既然是使用的 Electron，那我们以 debugger 的方式打开聊天窗口的 devtools，这样不就可以拿到了群友列表的 dom 吗？

思路有了，然后开始操作：

下载最新的 Electron 版 QQ

使用 debugtron 这个工具来启动 QQ

登录 QQ，在群列表中找到该群，右键打开单独的聊天窗口：

在 debugtron 工具的 Sessions 界面中找到刚才打开的页面地址，点击 respect 按钮，然后就会出现熟悉的 devtools 面板。

有了 devtools 我们就可以使用 JavaScript 来操作记录群友列表了，代码如下：

Copy
void (async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
/**
* 帧定时器
* @param {Funct} func [回调方法]
* @param {Number} timeout [超时时间]
* @return {Promise}
*/
const asyncLoopTimer = (func, timeout = Infinity) => {
const startTime = performance.now()
return new Promise(resolve => {
const timer = async nowTime => {
cancelAnimationFrame(requestID)
const data = await func()
if (data || nowTime - startTime > timeout) {
resolve(data)
} else {
requestID = requestAnimationFrame(timer)
}
}
let requestID = requestAnimationFrame(timer)
})
}

/**
* css 异步选择器
* @param {String} selector [CSS选择器]
* @param {Number} timeout [超时时间]
* @return {Promise} [Target]
*/
const asyncQuerySelector = (selector, timeout) => {
return asyncLoopTimer(() => {
return document.querySelector(selector)
}, timeout)
}

/**
* 字符串模板创建元素
* @param {String} template [元素模板]
* @return {Element} 元素对象
*/
const createElement = template => {
return new Range().createContextualFragment(template).firstElementChild
}

/** 下载 */
const download = (data, name, options) => {
const href = URL.createObjectURL(new Blob(data), options)
const a = createElement(``)
a.click()
}

const LIST_REF_CLASS = '.viewport-list__inner' // 群员列表 dom
const USER_CARD_REF_CLASS = '.buddy-profile' // 群员信息卡片
const USER_NAME_REF_CLASS = '.buddy-profile__header-name' // 群员名称
const USER_QQ_REF_CLASS = '.buddy-profile__header-uid' // 群员 qq

const autopilot = (delay = 300) => {
let userRef = document.querySelector(LIST_REF_CLASS).firstElementChild
const userList = []
return async () => {
userRef.scrollIntoView()
userRef.firstElementChild.click()
const cardRef = await asyncQuerySelector(USER_CARD_REF_CLASS, 1000)
await sleep(delay)
userList.push({
name: cardRef.querySelector(USER_NAME_REF_CLASS)?.textContent,
qq: cardRef.querySelector(USER_QQ_REF_CLASS)?.textContent?.split(' ')[1]
})

document.body.click()

userRef = userRef.nextElementSibling
console.log('----userList----', userList)
return userRef ? false : userList
}
}

const userList = await asyncLoopTimer(autopilot(100))

download([JSON.stringify(userList)], 'users.json', { type: 'application/json' })
})().catch(error => {
console.error(error)
})

以上代码大概流程：模拟滚动群友列表，然后依次点击打开信息卡片，记录群友的信息，最后下载为 JSON 文件。

虽然本文和标题有些出入，并不是真正的 “恢复”，如果那天你的群突然被和谐了，不免为一种可行的解决方案，希望能帮你挽回一些损失。