geeeeeeeeek/electronic-wechat

网页版微信抓包+注入实现表情贴纸显示

geeeeeeeeek opened this issue · 18 comments

作者:Zhongyi Tong (geeeeeeeeek@github)

协议:知识共享-署名 (CC BY 2.5 AU)

提示: 这是最初的实现方式。#13@arrowrowe 提出了Angular注入的思路,取代了原先的DOM注入,极大地简化了逻辑并提升了性能。这份文档仅供参考。

由于微信协议变动,目前表情商店里的贴纸无法显示。

我们使用网页版微信登录时都会发现,聊天中的表情贴纸都无法显示,被替换成了一段[Sent a sticker. View on phone.]文本,提示你到手机上查看。原本以为微信在下发消息推送时根据终端做了手脚,但抓包的结果表明表情贴纸就在response中,只不过微信出于某些考虑,没有在前端解析出来。

受够了万年不更新bug一大堆的官方微信客户端,我最近用Electron封装了一个Mac和Linux下的WeChat客户端。做了很多本地化的工作,包括自适应窗口、docker上的消息计数,还有就是这个表情贴纸注入,效果如下图所示。由于还没有到production-ready的程度,所以先分享一篇技术文,介绍这个功能的实现。

qq20160224-0 2x

请求分析

为了知道收到的是不是贴纸,以及是什么贴纸,我们先来分析Web WeChat发起的网络请求。打开Chrome的DevTools,Network选项卡,只需要关注XHR请求即可。

qq20160224-1 2x

主要有下面两种请求:

  • GET https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck

    消息的同步请求,HTTP长连接,默认30s或需要同步时返回。

  • POST https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync

    上面synccheck请求返回时,也就是有新消息时,微信发起一个webwxsync请求。在这个请求的response中,包含了我们需要的新消息数量AddMsgCount、消息IdMsgId、消息内容Content等等有用的内容。

我们具体看看一个表情贴纸的消息Content是怎样的:

<msg><emoji fromusername = "wxid_********" tousername = "filehelper" type="2" idbuffer="media:0_0" md5="ead80e721af1faf2bb33054450c61a66" len = "80226" productid="com.tencent.xin.emoticon.bilibili" androidmd5="ead80e721af1faf2bb33054450c61a66" androidlen="80226" s60v3md5 = "ead80e721af1faf2bb33054450c61a66" s60v3len="80226" s60v5md5 = "ead80e721af1faf2bb33054450c61a66" s60v5len="80226" cdnurl = "http://mmbiz.qpic.cn/mmemoticon/dx4Y70y9XctRJf6tKsy7FibsxibBIEComianWNa3zMITfOiaztUNzrgjhg/0" designerid = "" thumburl = "http://mmbiz.qpic.cn/mmemoticon/dx4Y70y9XctRJf6tKsy7F01Cqy3ej686O49bu7YrDWyQ2VPADkeFMg/0" encrypturl = "http://emoji.qpic.cn/wx_emoji/CkibiaVE4Z5fdDEKPW3LEPhPBVFvmicEQhLDWMQNtO6yH7ReGPGtgT44g/" aeskey= "be4577207982f4d793f3586790930af8" ></emoji> </msg>

这就很清晰了,头部表明自己是个emoji,然后是一些CheckSum的数据,后面跟着的cdnurl是贴纸的gif资源,thumburl是贴纸的缩略图。理论上拿到这些资源之后,我们就可以在页面中显示贴纸了。

Electron中的抓包

先放一个无关的提醒,如果你想在Electron中嵌入第三方网页,直接用browserWindow.loadURL即可,不要使用官方提供的webview标签。webview标签带来的输入法错位、窗口resize性能问题对产品体验的影响非常严重。

Electron的WebContents提供了did-get-response-details事件,但回调中只能获取到Response Headers,无法获取到Response Body。因此我们需要寻找一些tricky的方法来解决这个问题。我采用的方案是Chrome Debugging Protocol,这其实就是我们日常使用的DevTools为Chrome Apps暴露的一套API。只不过我们平时使用它的UI来调试,现在使用文本命令。

在Electron中我们可以这样连接debugger [docs]

 try {
   browserWindow.webContents.debugger.attach("1.1");
 } catch (err) {
   console.log("Debugger attach failed : ", err);
 }

 browserWindow.webContents.debugger.on('detach', (event, reason) => {
   console.log("Debugger detached due to : ", reason);
 });

 browserWindow.webContents.debugger.on('message', (event, method, params) => {
   if (method == "Network.responseReceived" && params.type == "XHR") {
     // Code here.
   }
 });

 browserWindow.webContents.debugger.sendCommand("Network.enable");

首先注册debugger,然后通过Network.enable开启记录Network日志,接收Network.responseReceived事件,类似于我们在DevTools Network选项卡中看到的请求。查看请求详情需要额外发送一个指令:

debug.sendCommand("Network.getResponseBody", {
        "requestId": requestId
      }, (error, response) => {
        // Code here.
      });

剩余的工作就是用正则表达式和一些逻辑判断从请求中获取到cdnurl,这里不再赘述,可以参考我的代码

Electron中的页面注入

如何在Electron中向WebContents注入代码请参考[文档]。大致思路是在dom-ready和有新的贴纸消息到达时分别注入下面两行代码:

browserWindow.webContents.executeJavaScript(`injectBundle.initEmojiListJS()`);
browserWindow.webContents.executeJavaScript(`injectBundle.updateEmojiListJS('${JSON.stringify(emojiList)}')`);

我们在浏览器环境中维护一个window.emojiList对象,emojiList[msgId] = imageUrl。在DOM中找到div.js_message_bubbledata-cm标签中包含相应msgId的组件,注入background等CSS使其显示贴纸。

我们要对页面上所有可见的贴纸消息气泡执行上述逻辑,因此注入的时机非常重要。Web WeChat是一个由Angular构建的应用,聊天气泡的加载是一个动态的过程,每次切换聊天,甚至是滚动聊天消息时,都会不断有节点的移除和加载。如果新加入的节点有贴纸消息的话,我们就需要及时注入。最后,我采用了一个对性能有些影响的解决方案:

  • 新的贴纸消息到达时替换这个贴纸消息
  • 切换聊天时替换所有贴纸消息
  • 滚动聊天消息时替换所有贴纸消息

下面是注入部分的代码:

injectBundle.replaceEmojiMessageJS = (msgId, imageUrl, delay) => {
  setTimeout(()=> {
    let $bubble = $(`div.js_message_bubble:regex("${msgId}")`)
        .css('background', `url('${imageUrl}') no-repeat`)
        .css('background-size', '120px')
        .css('height', '120px')
        .css('width', '120px');
    $bubble.addClass('no_arrow');
    $bubble.find('pre').text('')
        .css('width', '120px');
  }, delay);
};

injectBundle.updateEmojiListJS = (newList)=> {
  newList = JSON.parse(newList);
  window.emojiList = $.extend(window.emojiList, newList);
  for (let msgId in newList) {
    injectBundle.replaceEmojiMessageJS(msgId, window.emojiList[msgId], 0);
  }
};

injectBundle.initEmojiListJS = ()=> {
  $.expr[':'].regex = (elem, index, match) => {
    var regex = new RegExp(match[3]),
        $elem = $(elem);
    return regex.test($elem.attr('data-cm'));
  };

  window.emojiList = {};
  $('a.title_name').on('DOMSubtreeModified', () => {
    for (let msgId in window.emojiList) {
      injectBundle.replaceEmojiMessageJS(msgId, window.emojiList[msgId], 0);
    }
  });
  $('.chat_bd.scroll-content').on('DOMNodeInserted', (ev) => {
    if (ev.timeStamp - injectBundle.timestamp > 50) {
      injectBundle.timestamp = ev.timeStamp;
      for (let msgId in window.emojiList) {
        injectBundle.replaceEmojiMessageJS(msgId, window.emojiList[msgId], 0);
      }
    }
  })
};

完整的项目请见electronic-wechat

先点赞. 然后, 似乎也可以有另一个解决方案, 增加 transformResponse, 收到消息时就把表情类消息锁定为图片类消息. 求意见~

@arrowrowe 如果可以直接替换那是最吼的!微信把ng-inspect也干了就没去细想注入angular……你可以发一个Pull Request,我需要先test一下……

re @geeeeeeeeek: See #13. 谢先~

请问如果想扩展公众号文章右侧顶部的按钮是否可行,比如想把公众号的文章直接分享到twitter 或是 保存到 evernote

@oblank 应该可行,你可以做个拓展。

xream commented

刚刚发现 emoji 的图片是 20x20 的很模糊...如果能替换成高清版的就好了...

<img class="emoji emoji1f639" text="_web" src="/zh_CN/htmledition/v2/images/spacer.gif">

貌似现在有些表情能显示,有些显示不了,这是一个没显示的表情 sync 的返回的 Content:

"Content": "&lt;msg&gt;&lt;emoji 
fromusername = \"ihciah\" 
tousername = \"wxid_******\" 
type=\"1\" 
idbuffer=\"media:0_0\" 
md5=\"5f323f6de9b33feadfcebfb8a9aaf912\"
len = \"8790\" 
productid=\"\" 
androidmd5=\"5f323f6de9b33feadfcebfb8a9aaf912\" 
androidlen=\"8790\" 
s60v3md5 = \"5f323f6de9b33feadfcebfb8a9aaf912\" 
s60v3len=\"8790\" 
s60v5md5 = \"5f323f6de9b33feadfcebfb8a9aaf912\" 
s60v5len=\"8790\" 
cdnurl = \"http://emoji.qpic.cn/wx_emoji/bnweqIcxewJscYfHjM8P73FOkwfYIMMicsHsib6Dgia4uYb6zGkqhFNXA/\" 
designerid = \"\" 
thumburl = \"\" 
encrypturl = \"http://emoji.qpic.cn/wx_emoji/bnweqIcxewJscYfHjM8P73FOkwfYIMMicUwm37cbTvstwxBdMQdzCPw/\" 
aeskey= \"b918b9c8eb07d751daa16fdcf9dad8dd\" 
width= \"200\" 
height= \"57\" 
&gt;&lt;/emoji&gt; &lt;gameext 
type=\"0\" 
content=\"0\" 
&gt;&lt;/gameext&gt;&lt;/msg&gt;",
Qusic commented

看起来 /webwxgetmsgimg 接口现在对于购买的表情都返回空的200了。。只有自定义表情能显示了

@ripples-alive @Qusic 没错

微信为此还修改了服务端真是不容易……

@geeeeeeeeek 我这个没显示是自定义的吧,那个 cdnurl 对应的图片我看了是和手机上看到的一样啊,不能直接显示么?

Qusic commented

@ripples-alive web微信的api和手机的不一样

@ripples-alive 现在什么情况,我发现官方的Web WeChat有些自定义表情能显示,有些不能。

image

😂 完全不知道什么情况。。。我只是用的时候发现有些表情总是显示不出来就来吱一声 😳
原来是官方的就已经有问题了么。。。

但是商店里的贴纸真的无法显示了,准备撤离这个功能……

我刚好也想做一个第三方的客户端,偶然看到你的项目,有点好奇的时,你是怎么处理消息的收发的,因为我没想过要在electron里面加载网页版的微信,其实我的想法是,抓取所有网页版微信的api借口,但是我发现这好麻烦的

现在想实现显示自定义表情 可以么?

今天抓了一下数据,现在后端返回里面直接把content都给干掉了,真是233333