前段時間一個多年的 QQ 群被和諧了,從大學就進入到這個群裡,我對這個群還是有些感情,平時有空就在群裡水兩句,難免感到有些惋惜。
想到能不能從 QQ 應用本身入手,通過 OCR 群友列表的方式,拿到群友 qq 號,然後使用 [email protected] 的形式群發郵件引導群友進入新群
但這個方案我這無法操作。
當時點擊了被封 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 來操作記錄群友列表了,代碼如下:
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 href="${href}" download="${name}"></a>`)
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 文件。
雖然本文和標題有些出入,並不是真正的 “恢復”,如果那天你的群突然被和諧了,不免為一種可行的解決方案,希望能幫你挽回一些損失。