Recently, a QQ group that had been around for many years was disbanded. I joined this group back in college, so I have some feelings for it. I would often chat a bit in the group when I had free time, and I can't help but feel a bit regretful.
I thought about whether I could approach it from the QQ application itself, using OCR to extract the list of group members and obtain their QQ numbers, then sending mass emails in the format of [email protected] to guide them into a new group.
But I can't operate this plan.
At that time, I clicked the exit group chat button in the dialog prompt for the banned QQ group. The group no longer exists in the QQ group list, so I can't operate the UI. The only option is to find a device that has logged into that QQ group and hasn't clicked the "exit group chat" button in the banned group dialog prompt. Unfortunately, I don't have one...
Since OCR won't work, I can only start from the local database. I searched online, and it turned out to be more difficult than I imagined.
First, the local db file of the QQ application is encrypted. I finally found a post on 52pojie: [Debugging Reverse] Unlocking MacQQ's Local SQLite Database, but the operation difficulty was too high, so I gave up.
Fortunately, a group member reminded me that the new version of Electron QQ, after syncing data, will return to the initial state when the group was banned. By right-clicking on the group, I can open the group chat window.
Ha, since it uses Electron, we can open the chat window's devtools in debugger mode, which means we can get the DOM of the group member list, right?
With the idea in mind, I started the operation:
-
Download the latest Electron version of QQ.
-
Use debugtron to launch QQ.
-
Log into QQ, find the group in the group list, and right-click to open a separate chat window:
-
In the Sessions interface of the debugtron tool, find the address of the page just opened, click the respect button, and the familiar devtools panel will appear.
- With devtools, we can use JavaScript to manipulate the group member list records. The code is as follows:
void (async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
/**
* Frame timer
* @param {Funct} func [Callback method]
* @param {Number} timeout [Timeout duration]
* @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 asynchronous selector
* @param {String} selector [CSS selector]
* @param {Number} timeout [Timeout duration]
* @return {Promise} [Target]
*/
const asyncQuerySelector = (selector, timeout) => {
return asyncLoopTimer(() => {
return document.querySelector(selector)
}, timeout)
}
/**
* Create element from string template
* @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' // Group 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 simulates scrolling through the group member list, then clicks to open the information card one by one, records the information of the group members, and finally downloads it as a JSON file.
Although this article and the title are somewhat different, it is not truly a "recovery." If one day your group is suddenly disbanded, this may serve as a feasible solution, hoping to help you recover some losses.