molvqingtai

molvqingtai

JavaScript Developer and TypeScript Gymnast.

利用 Electron 版 QQ,可以使被封的 QQ 群复活。

最近一段时间,一个多年的 QQ 群被封了,我从大学时就加入了这个群,对它还是有些感情的,平时有空就在群里聊天,现在感到有些遗憾。

我想到能不能从 QQ 应用本身入手,通过 OCR 群友列表的方式,获取群友的 QQ 号码,然后使用 [email protected] 的形式群发邮件引导群友进入新群。

但是,我无法实施这个方案。

image

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

OCR 不起作用,所以只能从本地数据库入手,在网上搜索了一番,发现比想象中困难。

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

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

哈,既然是使用 Electron,那我们可以通过调试器的方式打开聊天窗口的开发工具,这样就可以获取到群友列表的 DOM 了。

思路有了,然后开始操作:

  1. 下载最新的 Electron 版 QQ

  2. 使用 debugtron 这个工具来启动 QQ

  3. 登录 QQ,在群列表中找到该群,右键打开单独的聊天窗口:
    image

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

image

  1. 有了开发工具,我们就可以使用 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)
    })
  }

  /**
   * 异步选择器
   * @param  {String} selector [选择器]
   * @param  {Number} timeout  [超时时间]
   * @return {Promise}         [目标]
   */
  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 文件。

虽然本文和标题有些出入,并不是真正的 “恢复”,但如果你的群突然被封了,这可能是一个可行的解决方案,希望能帮你挽回一些损失。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。