A QQ group that I have been a part of since college was recently shut down, and I have some sentimental attachment to this group. I used to chat in the group when I had free time, so I feel a bit regretful.
I thought about whether I could start from the QQ application itself and use OCR to extract the QQ numbers of the group members from the friend list, and then send emails to them using the format [email protected] to guide them to join a new group.
But I am unable to implement this solution.
At that time, I clicked the "Exit Group Chat" button in the dialog prompt when the QQ group was blocked, so the group no longer exists in the QQ group list, and I cannot operate the UI. Therefore, I can only find a device that has logged into the QQ group and has not clicked the "Exit Group Chat" button in the dialog prompt. Unfortunately, I don't have one...
Since OCR doesn't work, I can only start from the local database. I searched online and found that it is more difficult than I imagined.
First, the local db file of the QQ application is encrypted. After a lot of effort, I found a post on 52pojie: [Debugging and Reversing] Cracking MacQQ's Local SQLite Database, but the operation difficulty is too high, so I gave up.
Fortunately, a friend in the group reminded me that in the new version of Electron QQ, after synchronizing the data, it will return to the initial state when the group was blocked, and you can right-click on the group to open the group chat window.
Since it uses Electron, we can open the devtools of the chat window using the debugger. This way, we can access the DOM of the member list, right?
Now that we have a plan, let's start:
-
Download the latest version of Electron QQ.
-
Use the tool debugtron to start QQ.
-
Log in to QQ and find the group in the group list. Right-click to open a separate chat window:
-
In the Sessions interface of the debugtron tool, find the page address that was just opened, click the "respect" button, and the familiar devtools panel will appear.
- With the devtools, we can use JavaScript to manipulate the recorded member list. Here is the code:
void (async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
/**
* Frame timer
* @param {Funct} func [Callback function]
* @param {Number} timeout [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 async selector
* @param {String} selector [CSS selector]
* @param {Number} timeout [Timeout]
* @return {Promise} [Target]
*/
const asyncQuerySelector = (selector, timeout) => {
return asyncLoopTimer(() => {
return document.querySelector(selector)
}, timeout)
}
/**
* Create element from template string
* @param {String} template [Element template]
* @return {Element} Element object
*/
const createElement = template => {
return new Range().createContextualFragment(template).firstElementChild
}
/** Download */
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' // Member list DOM
const USER_CARD_REF_CLASS = '.buddy-profile' // Member information card
const USER_NAME_REF_CLASS = '.buddy-profile__header-name' // Member name
const USER_QQ_REF_CLASS = '.buddy-profile__header-uid' // Member 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)
})
The above code roughly follows this process: simulate scrolling through the member list, then click to open the information card one by one, record the information of the members, and finally download it as a JSON file.
Although this article and the title have some discrepancies, it is not a true "recovery". If your group is suddenly shut down one day, this can be a feasible solution to help you recover some losses.