Hao
Hi, I am Hao (👋): a coder, a woodworker, a blogger, and a father.
在iPhone上同时登录两个微信的方法
January 12, 2017

在iPhone上登录多个微信早年有一些类似“微信分身版”这样的奇淫技巧,但是随着腾讯和苹果以封闭为目标的不断耕耘这些方法已经纷纷失效。本文介绍另外一种办法,通过在服务器端架设破解了微信通讯协议的机器人,从而达到在手机端登录浏览器即可访问多个微信的方法。

这个方法主要使用了https://github.com/Urinx/WeixinBot

这是一个基于python的微信API。因此,思路主要是在云服务器上运行架设一个机器人,用手机微信扫码登录开始一个微信instance,该instance提供收取消息和发送消息的接口,同时在网页端架设简单聊天UI,每次手机只需要访问这个网页即可登录该微信账号。

服务器

使用最简单的Flask来作为web服务器。考虑是个人使用,因此只需要运行一个微信实例:

# app.py
if __name__ == '__main__':
    webwx = WebWeixin()
    listenProcess = multiprocessing.Process(target=webwx.start)
    listenProcess.start()

    port = int(os.environ.get("PORT", 5000))
    app.run(host='0.0.0.0', port=port)

因为webwx在启动之后会监听信息,因此另开一个新的进程。运行python app.py后,可在命令行中看到程序运行。

二维码

需要扫码,因为之后要部署在服务器上,所以在网页中显示二维码。首先调整WebWeixin类中源代码的生成二维码的路径,需要放在Flask的静态资源路径中:

self.saveFolder = os.path.join(os.getcwd(), 'static')

genQRCode()方法中,修改OS X的代码,把显示在终端改成保存为一个图片文件。

def genQRCode(self):
	self._showQRCodeImg()

在Flask中设置新controller,显示二维码图片:

# app.py
@app.route("/qrcode")
def qrcode():
    return render_template('qrcode.html')
# templates/qrcode.html
<img src="" />

然后就可以在浏览器中打开http://0.0.0.0:5000/qrcode来显示二维码了。

收取消息

由于WebWeixin和Flask运行在不同进程中,因此无法共享全局变量。考虑使用redis来沟通。但由于是个人使用,因此只需使用基本的文件结构即可。流程如下:WebWeixin实例监听新信息,一旦收取新信息,把该信息保存为<msgid>.json形式的文件,同时在一个名为status.json的文件中来添加该信息的ID,主要是记录未读信息。在网页端请求获取新信息,Flask服务器读取未读信息status.json列表,把符合该列表中的信息返回,网页端收取信息后把已读的信息ID发送给服务器,服务器接到收据后在status.json中删除信息。

在WebWeixin类中修改方法handleMsg(),意为每当处理信息时,如果信息类型不等于51(51为获取联系人信息信息,在这里属于无关信息),则保存该信息,同时把它的ID加入到未读列表中。

if msgType != 51:
    self._saveFile(msgid + '.json', json.dumps(msg), 'webwxgetmsg')
    self._appendFile('status.json', msgid, 'webwxgetstatus')

Flask中添加路由请求信息API:

@app.route("/msg", methods=['get'])
def getMsg():
    statusDirName = os.path.join(os.path.join(os.getcwd(), 'static'), 'status')
    msgDirName = os.path.join(os.path.join(os.getcwd(), 'static'), 'msg')
    fn = os.path.join(statusDirName, 'status.json')
    msgIds = []
    with open(fn) as f:
        msgIds = json.load(f)
        f.close()
    msgs = []
    for msgId in msgIds:
        msgfn = os.path.join(msgDirName, msgId + '.json')
        with open(msgfn) as f:
            msg = json.load(f)
            f.close()
            msgs.append(msg)
    return jsonify(results=msgs)

发送消息

同理,发送信息时,网页端把发送的信息详情请求给服务器端,服务器端在一个名叫send.json的文件中缓存队列,WebWexin在监听方法中添加新方法,该方法检查send.json,发送并删除信息。

修改listenMsgMode()方法:

# app.py
# ...
while True:
    self.checkAndHandleSend();

这个方法主要检查发送队列中是否有新信息,如果有的话发送该信息。简单的发送信息API如下:

@app.route("/send", methods=['get'])
def sendMsg():
    # /send?msg=testname:testmsg
    msg = request.args.get('msg', '')
    if not msg:
        return jsonify(results=False)
    dirName = os.path.join(os.path.join(os.getcwd(), 'static'), 'send')
    if not os.path.exists(dirName):
        os.makedirs(dirName)
    fn = os.path.join(dirName, 'send.json')
    msgs = []
    if os.path.isfile(fn):
        with open(fn) as f:
            msgs = json.load(f)
            f.close()
    msgs.append(msg)
    with open(fn, mode='w') as f:
        f.write(json.dumps(msgs))
        f.close()
    return jsonify(results=True)

前端UI

前端使用如下实例代码:

<div class="main">
        <div class="chat">
        </div>
    </div>
    <div class="bottom">
        <div class="contacts"></div>
        <div class="enter">
            <input type="text" class="input" />
            <button class="send">Send</button>
        </div>
    </div>
    <script type="text/javascript">
    var chat = document.querySelector('.chat');
    var input = document.querySelector('.input');
    var send = document.querySelector('.send');
    var contacts = document.querySelector('.contacts');

    function getMsg() {
        var msgids = [];
        $.getJSON('/msg', data => {
            var results = data.results;
            if (results.length > 0) {
                results.forEach(result => {
                    msgids.push(result.MsgId);
                    addChat(result.FromUserName2, result.Content, false);
                });
                sendReceipt(msgids.join(','));
            }
        });
        setTimeout(() => {
            getMsg();
        }, 5000);
    }

    function sendReceipt(msgids) {
        $.getJSON(`/receive?ids=${msgids}`, data => {});
    }

    function addChat(username, content, sendOut) {
    	if (!sendOut) sendOut = false;
    	var html = chat.innerHTML;
    	if (sendOut) {
    		html += `
	        	<div class="message out">
	        		[me] to [${username}] => ${content}
	    		</div>`;
    	} else {
    		html += `
	        	<div class="message in">
	        		[${username}] to [me] => ${content}
	    		</div>`;
    	}
        
        chat.innerHTML = html;
    }

    function getContacts() {
    	$.getJSON(`/contacts`, data => {
    		var results = data.results;
    		results.forEach(result => {
    			var html = contacts.innerHTML;
    			html += `
    				<div class="contact" data-nickname="${result.NickName}">
    					${result.NickName}
    				</div>
    			`;
    			contacts.innerHTML = html;
    		});
    	})
    }

    function handleDocumentClick(e) {
    	if (e.target.className === 'contact') {
    		input.value = `${e.target.dataset.nickname}:`;
    	}
    }

    function handleSendClick(e) {
    	var text = input.value;
    	var parts = text.split(':');
    	if (parts.length === 2) {
    		addChat(parts[0], parts[1], true);
    		$.getJSON(`/send?msg=${text}`, data => {});
    		input.value = `${parts[0]}:`;
    	} else {
    		alert("Must have a receiver.");
    		return;
    	}
    }

    function main() {
        getMsg();
        getContacts();
        document.addEventListener('click', handleDocumentClick);
        send.addEventListener('click', handleSendClick);
    }

    main();
    </script>

部署(Heroku)

部署在Heroku免费dyno中,配合每月1000小时免费配额,使用wakeupmydyno服务每隔30分钟激活程序。在Procfile中添加web: python app.py