{
"title": "利用 Electron 版 QQ 复活被封的 QQ 群",
"tags": [
"post",
"QQ群",
"解封",
"Electron QQ"
],
"summary": "利用 Electron 版 QQ 复活被封的 QQ 群",
"sources": [
"xlog"
],
"external_urls": [
"https://molvqingtai-2886.xlog.app/li-yong-Electron-ban-QQ-fu-huo-bei-feng-de-QQ-qun"
],
"date_published": "2023-03-21T03:13:05.822Z",
"content": "前段时间一个多年的 QQ 群被和谐了,从大学就进入到这个群里,我对这个群还是有些感情,平时有空就在群里水两句,难免感到有些惋惜。\n\n想到能不能从 QQ 应用本身入手,通过 OCR 群友列表的方式,拿到群友 qq 号,然后使用 [email protected] 的形式群发邮件引导群友进入新群\n\n但这个方案我这无法操作。\n\n![image](ipfs://bafkreigwhlzgb4lr7aym56fqr7maitlh727w3mvjhmjyei5secwmy7gr5a)\n\n当时点击了被封 QQ 群封群 Dialog 提示中的退出群聊按钮,该群在 QQ 群列表中已不存在了,也就无法操作 UI,那么只有找一个登过该 QQ 群的设备,并未点击过封群 Dialog 提示中的 “退出群聊” 按钮。可惜我没有...\n\nOCR 不行,那么只能从本地数据库入手,在网上搜索了一番,比想象中难度高。\n\n首先 QQ 应用的本地 db 文件是加密的,好不容易在吾爱破解上找到一篇帖子: [[调试逆向] 撬开MacQQ的本地SQLite数据库](https://www.52pojie.cn/thread-1335657-1-1.html),奈何操作难度太高,遂放弃。\n\n幸好,经群友提醒,新版的 Electron QQ,同步数据之后,会回到封群时的初始状态,右键该群,可以打开群聊窗口。\n\n哈,既然是使用的 Electron,那我们以 debugger 的方式打开聊天窗口的 devtools,这样不就可以拿到了群友列表的 dom 吗?\n\n思路有了,然后开始操作:\n\n1. 下载最新的 Electron 版 QQ\n2. 使用 [debugtron](https://github.com/pd4d10/debugtron) 这个工具来启动 QQ\n3. 登录 QQ,在群列表中找到该群,右键打开单独的聊天窗口:\n![image](ipfs://bafkreidnmk4gf2unym4e6fkuyzsjqngopy64rlzpplrykqpvua56hnfmle)\n\n4. 在 debugtron 工具的 Sessions 界面中找到刚才打开的页面地址,点击 respect 按钮,然后就会出现熟悉的 devtools 面板。\n\n![image](ipfs://bafkreiaw5otwbbnvcobvf5ms4jwb6qkaxm2366axuxbks3yiibplaed5ke)\n\n5. 有了 devtools 我们就可以使用 JavaScript 来操作记录群友列表了,代码如下:\n```javascript\nvoid (async () => {\n const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))\n /**\n * 帧定时器\n * @param {Funct} func [回调方法]\n * @param {Number} timeout [超时时间]\n * @return {Promise}\n */\n const asyncLoopTimer = (func, timeout = Infinity) => {\n const startTime = performance.now()\n return new Promise(resolve => {\n const timer = async nowTime => {\n cancelAnimationFrame(requestID)\n const data = await func()\n if (data || nowTime - startTime > timeout) {\n resolve(data)\n } else {\n requestID = requestAnimationFrame(timer)\n }\n }\n let requestID = requestAnimationFrame(timer)\n })\n }\n\n /**\n * css 异步选择器\n * @param {String} selector [CSS选择器]\n * @param {Number} timeout [超时时间]\n * @return {Promise} [Target]\n */\n const asyncQuerySelector = (selector, timeout) => {\n return asyncLoopTimer(() => {\n return document.querySelector(selector)\n }, timeout)\n }\n\n /**\n * 字符串模板创建元素\n * @param {String} template [元素模板]\n * @return {Element} 元素对象\n */\n const createElement = template => {\n return new Range().createContextualFragment(template).firstElementChild\n }\n\n /** 下载 */\n const download = (data, name, options) => {\n const href = URL.createObjectURL(new Blob(data), options)\n const a = createElement(`<a href=\"${href}\" download=\"${name}\"></a>`)\n a.click()\n }\n\n const LIST_REF_CLASS = '.viewport-list__inner' // 群员列表 dom\n const USER_CARD_REF_CLASS = '.buddy-profile' // 群员信息卡片\n const USER_NAME_REF_CLASS = '.buddy-profile__header-name' // 群员名称\n const USER_QQ_REF_CLASS = '.buddy-profile__header-uid' // 群员 qq\n\n const autopilot = (delay = 300) => {\n let userRef = document.querySelector(LIST_REF_CLASS).firstElementChild\n const userList = []\n return async () => {\n userRef.scrollIntoView()\n userRef.firstElementChild.click()\n const cardRef = await asyncQuerySelector(USER_CARD_REF_CLASS, 1000)\n await sleep(delay)\n userList.push({\n name: cardRef.querySelector(USER_NAME_REF_CLASS)?.textContent,\n qq: cardRef.querySelector(USER_QQ_REF_CLASS)?.textContent?.split(' ')[1]\n })\n\n document.body.click()\n\n userRef = userRef.nextElementSibling\n console.log('----userList----', userList)\n return userRef ? false : userList\n }\n }\n\n const userList = await asyncLoopTimer(autopilot(100))\n\n download([JSON.stringify(userList)], 'users.json', { type: 'application/json' })\n})().catch(error => {\n console.error(error)\n})\n\n```\n以上代码大概流程:模拟滚动群友列表,然后依次点击打开信息卡片,记录群友的信息,最后下载为 JSON 文件。\n\n虽然本文和标题有些出入,并不是真正的“恢复”,如果那天你的群突然被和谐了,不免为一种可行的解决方案,希望能帮你挽回一些损失。",
"attributes": [
{
"value": "li-yong-Electron-ban-QQ-fu-huo-bei-feng-de-QQ-qun",
"trait_type": "xlog_slug"
}
]
}