Wx::

番外篇:再也不生微信撤回消息的气

因为

实现微信防(好友)撤回的方法有很多,在电脑端可以通过微信客户端,在安卓手机可以通过额外的程序来监控微信消息,在其他端(比如苹果)呢呢呢???


所以

无聊之中我想用 Node.js 写一个脚本,通过(模拟)网页端微信,利用其接口,实现了登录微信并像手机一样接收所有消息并存入一个数组中,当发现某好友或群里的人撤回其消息时,在数组中找到他撤回的消息并提醒我。

这样,通过跑在服务器上的脚本,谁撤回了消息我都能收到通知:xxx 撤回了几点几点的什么内容!

嘿嘿😁


当然

对于具体实现,我主要借助了一个第三方库 wechat4u ,这个库是 node.js (其实就是 javascript 语言)封装的网页版微信的接口,类似的还有很多 python、java 封装的,原理大致相同,通过 http 请求来实现登录及所有的消息传输(简而言之,偷过来网页版微信与腾讯服务器的通信渠道)。

关于如何“偷过来”他们的通信渠道,在最后有简单的嗦一下。

其实,基础的 javascript 知识,基础的 node.js 知识,再到 Github 了解一下 wechat4u 或类似的库,你也可以写出来的...

下面是代码,仅供学习玩耍哦:

const Wechat = require('wechat4u')
const qrcode = require('qrcode-terminal')
const fs = require('fs')
const request = require('request')

let bot
let msgs = {}; // 用来存所有消息的对象(此处和数组作用一样)
let tipReceiver = 'filehelper'; // 如何通知我
let meID = ''; // 用来记录“我”的名字

try {
    bot = new Wechat(require('./wxrecall.json'))
}
catch (e) {
    bot = new Wechat()
}

// 设置心跳间隔:
bot.setPollingIntervalGetter(function () {
    return 8 * 60 * 60 * 1000; // 8 小时
    // return 20*1000; // 测试用 20 秒
});

// 启动机器人
if (bot.PROP.uin) {
    // 存在登录数据时,可以随时调用restart进行重启
    bot.restart()
} else {
    bot.start()
}


// uuid事件,参数为uuid,根据uuid生成二维码
bot.on('uuid', uuid => {
    // 在终端(命令行)生成二维码:
    qrcode.generate('https://login.weixin.qq.com/l/' + uuid, {
        small: true
    })

    // 同时将登录二维码图片保存到本地:
    console.log('https://login.weixin.qq.com/qrcode/'+uuid+' 已保存到本地');
    request('https://login.weixin.qq.com/qrcode/' + uuid).pipe(fs.createWriteStream('./wxlogin.png'));
})

// 登录成功事件
bot.on('login', () => {
    console.log('登录成功');
    // 保存数据,将数据序列化之后保存到任意位置
    fs.writeFileSync('./wxrecall.json', JSON.stringify(bot.botData))
})

// 登出成功事件
bot.on('logout', () => {
    console.log('登出成功')
    // 清除数据
    // fs.unlinkSync('./sync-data.json')
})

// 错误事件,参数一般为Error对象
bot.on('error', err => {
    console.error('错误:', err)
})


bot.on('login', () => {
    bot.sendMsg('bot start: ' + new Date().toLocaleString(), tipReceiver)
        .catch(err => {
            bot.emit('error', err)
        })

    meID = bot.user.UserName;
})

// 如何处理会话消息
bot.on('message', msg => {
    // 消息类型:

    switch (msg.MsgType) {
        case bot.CONF.MSGTYPE_TEXT:
            // 文本消息
            msgs[msg.MsgId] = {
                type: msg.MsgType,
                from: bot.contacts[msg.FromUserName].getDisplayName(),
                time: msg.getDisplayTime(),
                originalTime: msg.CreateTime,
                fromId: msg.FromUserName,
                toId: msg.ToUserName,
                content: msg.Content,
                originalContent: msg.OriginalContent,
            };
            break
        case bot.CONF.MSGTYPE_IMAGE:
            // 图片消息

            msgs[msg.MsgId] = {
                type: msg.MsgType,
                from: bot.contacts[msg.FromUserName].getDisplayName(),
                time: msg.getDisplayTime(),
                originalTime: msg.CreateTime,
            };
            break
        case bot.CONF.MSGTYPE_VOICE:
            // 语音消息

            msgs[msg.MsgId] = {
                type: msg.MsgType,
                from: bot.contacts[msg.FromUserName].getDisplayName(),
                time: msg.getDisplayTime(),
                originalTime: msg.CreateTime,
            };
            break
        case bot.CONF.MSGTYPE_EMOTICON:
            // 表情消息

            msgs[msg.MsgId] = {
                type: msg.MsgType,
                from: bot.contacts[msg.FromUserName].getDisplayName(),
                time: msg.getDisplayTime(),
                originalTime: msg.CreateTime,
            };
            break
        case bot.CONF.MSGTYPE_VIDEO:
        case bot.CONF.MSGTYPE_MICROVIDEO:
            // 视频消息

            msgs[msg.MsgId] = {
                type: msg.MsgType,
                from: bot.contacts[msg.FromUserName].getDisplayName(),
                time: msg.getDisplayTime(),
                originalTime: msg.CreateTime,
            };
            break
        case bot.CONF.MSGTYPE_APP:
            if (msg.AppMsgType == 6) {
                // 文件消息
                let fileName = msg.FileName;
                fileName = fileName.replace(/ /g, "");
                fileName = fileName.replace(/:/g, "-");
                bot.getDoc(msg.FromUserName, msg.MediaId, msg.FileName).then(res => {
                    fs.writeFileSync(`./wechat files/${fileName}`, res.data)
                }).catch(err => {
                    bot.emit('error', err)
                })

                msgs[msg.MsgId] = {
                    type: msg.MsgType,
                    from: bot.contacts[msg.FromUserName].getDisplayName(),
                    time: msg.getDisplayTime(),
                    originalTime: msg.CreateTime,
                    postfix: fileName.lastIndexOf('.')==-1?".unknown":fileName.slice(fileName.lastIndexOf('.')),
                    AppMsgType: msg.AppMsgType,
                    FromUserName: msg.FromUserName,
                    MediaId: msg.MediaId,
                    FileName: msg.FileName
                };
            }
            else{
                msgs[msg.MsgId] = {
                    type: bot.CONF.MSGTYPE_TEXT, // 以文本处理应用分享链接
                    from: bot.contacts[msg.FromUserName].getDisplayName(),
                    time: msg.getDisplayTime(),
                    originalTime: msg.CreateTime,
                    fromId: msg.FromUserName,
                    toId: msg.ToUserName,
                    content: msg.Url || "分享链接获取失败",
                    originalContent: msg.OriginalContent,
                };
            }
            break
        default:
            break
    }
})

// 发生撤回消息:
bot.on('message', msg => {
    if (msg.MsgType == bot.CONF.MSGTYPE_RECALLED) {
        // msg.Content是一个xml,关键信息是MsgId

        let MsgId = msg.Content.match(/<msgid>(.*?)<\/msgid>/)[0]
        MsgId = MsgId.slice(7, MsgId.indexOf("</msgid>"));
        console.log("撤回:"+MsgId);

        if(msgs[MsgId]){
            let thism = msgs[MsgId];
            if(thism.type == bot.CONF.MSGTYPE_TEXT){
                bot.sendMsg(
                        (thism.from==meID?'我':thism.from)
                        + " 撤回 "
                        + thism.time
                        + " 的 "
                        + thism.content, tipReceiver
                    )
                    .catch(err => {
                        bot.emit('error', err)
                    })
            }
            else{
                switch (thism.type) {
                    case bot.CONF.MSGTYPE_IMAGE:
                        // 图片消息
                        bot.sendMsg(
                                (thism.from==meID?'我':thism.from)
                                + " 撤回 "
                                + thism.time
                                + " 的 图片", tipReceiver
                            )
                            .catch(err => {
                                bot.emit('error', err)
                            })

                        bot.getMsgImg(MsgId).then(res => {
                            bot.sendMsg({
                                file: res.data,
                                filename: 'recall.jpg'
                            }, tipReceiver)
                                .catch(err => {
                                    bot.emit('error', err)
                                })
                        }).catch(err => {
                            bot.emit('error', err)
                        })

                        break
                    case bot.CONF.MSGTYPE_VOICE:
                        // 语音消息
                        bot.sendMsg(
                                (thism.from==meID?'我':thism.from)
                                + " 撤回 "
                                + thism.time
                                + " 的 语音", tipReceiver
                            )
                            .catch(err => {
                                bot.emit('error', err)
                            })

                        bot.getVoice(MsgId).then(res => {
                            console.log("获取撤回语音成功");
                            bot.sendMsg({
                                file: res.data,
                                filename: 'recall.mp3'
                            }, tipReceiver)
                                .catch(err => {
                                    console.log("发送撤回语音失败");
                                    bot.emit('error', err)
                                })
                        }).catch(err => {
                            bot.emit('error', err)
                        })

                        break
                    case bot.CONF.MSGTYPE_EMOTICON:
                        // 表情消息
                        bot.sendMsg(
                                (thism.from==meID?'我':thism.from)
                                + " 撤回 "
                                + thism.time
                                + " 的 表情", tipReceiver)
                            .catch(err => {
                                bot.emit('error', err)
                            })

                        bot.getMsgImg(MsgId).then(res => {
                            bot.sendMsg({
                                file: res.data,
                                filename: 'recall.gif'
                            }, tipReceiver)
                                .catch(err => {
                                    bot.emit('error', err)
                                })
                        }).catch(err => {
                            bot.emit('error', err)
                        })

                        break
                    case bot.CONF.MSGTYPE_VIDEO:
                    case bot.CONF.MSGTYPE_MICROVIDEO:
                        // 视频消息
                        bot.sendMsg(
                                (thism.from==meID?'我':thism.from)
                                + " 撤回 "
                                + thism.time
                                + " 的 视频", tipReceiver
                            )
                            .catch(err => {
                                bot.emit('error', err)
                            })

                        bot.getVideo(MsgId).then(res => {
                            bot.sendMsg({
                                file: res.data,
                                filename: 'recall.mp4'
                            }, tipReceiver)
                                .catch(err => {
                                    bot.emit('error', err)
                                })
                        }).catch(err => {
                            bot.emit('error', err)
                        })

                        break
                    case bot.CONF.MSGTYPE_APP:
                        if (thism.AppMsgType == 6) {
                            // 文件消息
                            bot.getDoc(thism.FromUserName, thism.MediaId, thism.FileName).then(res => {
                                bot.sendMsg(
                                        (thism.from==meID?'我':thism.from)
                                        +" 撤回 "
                                        +thism.time
                                        +" 的 "
                                        +thism.postfix.slice(1)
                                        +" 文件,原文件已保存至服务器", tipReceiver
                                    )
                                    .catch(err => {
                                        bot.emit('error', err)
                                    })

                                bot.sendMsg({
                                    file: res.data,
                                    filename: 'recall'+thism.postfix
                                }, tipReceiver)
                                    .catch(err => {
                                        bot.emit('error', err)
                                    })
                            }).catch(err => {
                                bot.emit('error', err)
                            })
                        }
                        break
                    default:
                        break
                }
            }

            delete msgs[MsgId];
        }
    }
})

// 定时清理已存的超过 2 分钟(不可能撤回)的消息:
let checkTime = 1000*60*2+10; // 2 分钟 + 10 秒的容差时间
const disTime = 2*60+5; // 2 分钟 + 5 s 的容差时间
let s = setInterval(function () {
    let nw = Math.floor(Date.now()/1000);
    for(let id in msgs){
        if(parseInt(nw) - parseInt(msgs[id].originalTime) > disTime){
            delete msgs[id];
        }
    }
}, checkTime);

划重点

这里多说一下网站的接口的获取,除了本身公开的 api 接口,如:某些网站提供的天气查询之类的,其他接口一般是通过抓包找到的,虽然我没有太深入的学习过,但简单来看我认为的抓包方式其中一种便是通过浏览器(如 Chrome)的开发者功能中的 Network 版块,查看该网站与其后端服务器的通信记录,所谓通信,简单来讲也就是将特定的数据发送到一个地址(服务器也就是这个地址的持有者)来像服务器发请求,服务器会发送数据到你的浏览器,这样便能实现前后端的通信。

具体对于网页版微信,我不太清楚是其公开了 api 接口的协议还是人为抓包,网上有很多公开的大神的微信协议分析和封装好的库。我只是简单调用并写了个防撤回的逻辑而已


最后啊

其实我也是写着玩的,呵呵呵😀


348

评论(0

评论 取消
验证码:
搜索