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,那我們以 debugger 的方式打開聊天窗口的 devtools,這樣不就可以拿到了群友列表的 dom 嗎?

思路有了,然後開始操作:

  1. 下載最新的 Electron 版 QQ

  2. 使用 debugtron 這個工具來啟動 QQ

  3. 登錄 QQ,在群列表中找到該群,右鍵打開單獨的聊天窗口:
    image

  4. 在 debugtron 工具的 Sessions 界面中找到剛才打開的頁面地址,點擊 respect 按鈕,然後就會出現熟悉的 devtools 面板。

image

  1. 有了 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 文件。

雖然本文和標題有些出入,並不是真正的 “恢復”,如果那天你的群突然被和諧了,不免為一種可行的解決方案,希望能幫你挽回一些損失。

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