2018年1月

微信自动发送消息接口开发(三)

上一篇已经能够成功监听微信的消息了,现在只剩最后一个功能发送消息了.
发消息我只做了两个功能:发送文字和发送图片,其他的不太用的到,就没有做.

首先是发送普通文字消息

发送文字

发送文字很简单,如图只有一个接口webwxsendmsg BaseRequest和之前的监听消息是一样的,另外一个Msg Type:1就是文本消息(Type就是监听消息的时候返回的Type类型)ClientMsgId和LocalID在源码中都是当前时间戳+4位随机数字,FromUserName当然是填自己的UserName了(监听里初始化的时候的自己的UserName),ToUserName当然是填发送方了. Content就是具体要发送的内容了.

如果发送中文出来乱码的话注意发送的data编码得是utf-8

当返回的消息BaseResponse.Ret == 0 的时候,就表示你发送成功了.

接下来是发送图片消息

发送图片通过抓包可以发现是两个接口

1.上传文件 webwxuploadmedia?f=json
上传图片

其他的根据抓到的包来填值都没问题
主要有一个参数是uploadmediarequest要观察一下源码来填内容.
request参数

参数比较多不一一说明,其中FileMd5其实没关系,不用管随便传个值都可以,我是对图片名称+图片大小进行md5加密后传送过去的.

上传图片需要用multipart/form-data形式发送,这里附上我自己封装的上传方法供大家参考

    //上传图片消息
    private HttpResult WebWXUpLoadPicture(Contact model, string filePath)
    {
        string boundary = "----WebKitFormBoundary" + DateTime.Now.Ticks.ToString("x");

        var fileInfo = new FileInfo(filePath);

        List<FormItemModel> data = new List<FormItemModel>();
        //id
        data.Add(new FormItemModel("id", "WU_FILE_" + _fileId++));
        //name
        data.Add(new FormItemModel("name", fileInfo.Name));
        //type
        data.Add(new FormItemModel("type", System.Web.MimeMapping.GetMimeMapping(filePath)));
        //lastModifiedDate
        data.Add(new FormItemModel("lastModifiedDate", fileInfo.LastWriteTime.ToString("yyyy-MM-dd tt hh:mm:ss")));
        //size
        data.Add(new FormItemModel("size", fileInfo.Length.ToString()));
        //mediatype
        data.Add(new FormItemModel("mediatype", "pic"));
        //uploadmediarequest
        string md5 = Utils.EncryptToMD5(fileInfo.Name + fileInfo.Length);
        PicModel pic = new PicModel()
        {
            UploadType = 2,
            BaseRequest = new BaseRequest(this._wxModel),
            TotalLen = fileInfo.Length,
            FromUserName = this.User.UserName,
            ToUserName = model.UserName,
            FileMd5 = md5
        };
        data.Add(new FormItemModel("uploadmediarequest", JsonHelper.SerializeObject(pic)));
        //webwx_data_ticket
        string ticket = this.GetCookie("webwx_data_ticket");
        data.Add(new FormItemModel("webwx_data_ticket", ticket));
        //pass_ticket
        data.Add(new FormItemModel("pass_ticket", this._wxModel.pass_ticket));
        //file
        data.Add(new FormItemModel("filename", fileInfo.Name, filePath));

        var bytes = CreateFileByte(data, boundary);

        HttpItem item = new HttpItem()
        {
            URL = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json",
            Method = "POST",
            PostDataType = PostDataType.Byte,
            ContentType = "multipart/form-data; boundary=" + boundary,
            PostEncoding = Encoding.UTF8,
            PostdataByte = bytes
        };
        HttpHelper http = new HttpHelper();

        var rst = http.GetHtml(item);

        return rst;
    }
    //实体类模型
    public class FormItemModel
    {
    public FormItemModel(string key, string value)
    {
        this.Key = key;
        this.Value = value;
    }

    public FormItemModel(string key, string name, string path)
    {
        this.Key = key;
        this.Value = name;
        this.FilePath = path;
    }

    /// <summary>  
    /// 表单键,request["key"]  
    /// </summary>  
    public string Key { set; get; }
    /// <summary>  
    /// 表单值,上传文件时为文件名
    /// </summary>  
    public string Value { set; get; }
    /// <summary>  
    /// 是否是文件  
    /// </summary>  
    public bool IsFile
    {
        get
        {
            if (string.IsNullOrEmpty(FilePath) && FileContent == null) return false;

            return true;
        }
    }

    public byte[] GetFileBytes(Encoding encoding)
    {
        if (encoding == null) encoding = Encoding.UTF8;
        if (IsFile)
        {
            try
            {
                if (!string.IsNullOrEmpty(FilePath))
                {
                    using (FileStream fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        byte[] bytes = new byte[fileStream.Length];
                        fileStream.Read(bytes, 0, bytes.Length);
                        return bytes;
                    }
                }
                else
                {
                    byte[] bytes = new byte[FileContent.Length];
                    FileContent.Read(bytes, 0, bytes.Length);
                    FileContent.Seek(0, SeekOrigin.Begin);
                    return bytes;
                }
            }
            catch (Exception)
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }

    //如果是文件,下面两个只要填一个即可.

    /// <summary>  
    /// 上传的文件路径 
    /// </summary>  
    public string FilePath { set; get; }

    /// <summary>
    /// 文件流
    /// </summary>
    public Stream FileContent { get; set; }
}

//核心postdata 字节创建方法
private byte[] CreateFileByte(List<FormItemModel> formItems, string boundary, Encoding encoding = null)
    {
        if (encoding == null) encoding = Encoding.UTF8;
        Stream postStream = new MemoryStream();
        //文件模板
        string fileFormdataTemplate =
        "\r\n--" + boundary +
        "\r\nContent-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"" +
        "\r\nContent-Type: application/octet-stream" +
        "\r\n\r\n";
        //文本数据模板  
        string dataFormdataTemplate =
            "\r\n--" + boundary +
            "\r\nContent-Disposition: form-data; name=\"{0}\"" +
            "\r\n\r\n{1}";

        foreach (var item in formItems)
        {
            string formdata = null;
            if (item.IsFile)
            {   //文件
                formdata = string.Format(
                fileFormdataTemplate,
                item.Key, //表单键  
                item.Value);
            }
            else
            {   //文本
                formdata = string.Format(
                dataFormdataTemplate,
                item.Key,
                item.Value);
            }

            byte[] formdataBytes = null;

            if (postStream.Length == 0)
                formdataBytes = encoding.GetBytes(formdata.Substring(2));
            else
                formdataBytes = encoding.GetBytes(formdata);

            //写入流
            postStream.Write(formdataBytes, 0, formdataBytes.Length);

            if (item.IsFile)
            {
                var bytes = item.GetFileBytes(encoding);
                if (bytes == null)
                {
                    this.PrintErrlog("读取文件错误:" + JsonHelper.SerializeObject(item));
                    throw new Exception("读取文件错误");
                }
                postStream.Write(bytes, 0, bytes.Length);
            }
        }

        var footer = encoding.GetBytes("\r\n--" + boundary + "--\r\n");
        postStream.Write(footer, 0, footer.Length);

        byte[] buffer = StreamToBytes(postStream);

        return buffer;

    }





如果上传图片成功,服务器会返回一个MediaId,相当于图片已经成功上传到微信服务器了,这个就是图片的key

文件上传返回

2.发送图片消息 webwxsendmsgimg

发送图片消息

发送图片接口就比较简单了,和文字消息差不多,要改的就是Msg中的Type换成3,再加上刚才得到的MediaId,其他的都基本差不多,当返回的消息BaseResponse.Ret == 0 的时候,你就成功了

接下来那些发送小视频,声音等等的由于本人实际需求用不到,就没有继续写了.本来还想写个自动抢红包的,发现网页版根本不支持抢红包,就无从写起了...

微信自动发送消息接口开发(二)

上一篇讲到OAuth鉴权成功,接下去就是监听微信消息的接口了.

成功登陆后,通过抓包和源码查看分析,主要有用到的监听新消息的接口有以下几个:

1. 微信登录初始化列表: webwxinit

微信登录后会立即调用的一个接口,用来初始化一些列表数据,比方说常用联系人列表,广告推送等

微信初始化

这个接口是一个POST接口,传参是一个json对象,注意BaseRequest这个对象存的是oauth鉴权返回的一些值,很多地方都会用到,下面是我做的封装,可以参考一下:

public class BaseRequest
{
    WXModel _model;
    public BaseRequest(WXModel model)
    {
        this._model = model;
    }

    public string DeviceID { get { return "e" + DateTime.Now.Ticks; } }

    public string Sid { get { return _model.wxsid; } }

    public string Skey { get { return _model.skey; } }

    public string Uin { get { return _model.wxuin; } }

}

注意这个接口返回的Content-Type不是很标准,是text/plain 并且没有跟charset字符集,微信整套接口用的全部都是UTF-8,和我一样用HttpHelper的人可能要手动设置一下获取编码,否则获取下来的会和谷歌开发环境的数据一样是乱码.

微信乱码

微信几个接口都存在这样的问题,不再一一叙述

2. 微信获取联系人列表详情: webwxgetcontact

webwxgetcontact是一个Get接口,用来获取联系人内容,没有什么好多说的,参数中r取随机值,其他的在之前鉴权内容中都有返回.

微信联系人列表

3. 微信获取群组列表详情: webwxbatchgetcontact

POST请求,除了有BaseRequest外,再给一个List是要请求的UserName,在一开始的webwxinit接口返回的ChatSet中可以得到,源码是对ChatSet过滤了已经存在的常用个联系人,请求剩下的所有群联系人

微信群列表

4. 微信检查是否有新的数据: synccheck

GET请求 里面其他没什么好说的,值得注意的是synckey 这个参数在init的时候会返回外,每次更新数据的时候也会有,要记得每次请求接口对应更新synckey里面的值.
请求当返回的是window.synccheck={retcode:"0",selector:"0"}则表示没有新的数据更新,其他都是要更新新的消息.
下面是我对synckey的封装.

public class KeyModel
{
    public int Key { get; set; }

    public long Val { get; set; }

    public override string ToString()
    {
        return Key + "_" + Val;
    }
}

public class SyncKey
{
    public List<KeyModel> List { get; set; }

    public int Count { get { return List?.Count ?? 0; } }

    public override string ToString()
    {
        if (List == null || List.Count == 0) return "";
        return string.Join("|", List);
    }
}

5. 微信同步新的消息 webwxsync

POST请求 最重要的一个请求,用来同步更新新的消息.
请求参数其他也没什么好说的,主要通过SyncKey来确定这一次需要更新的内容.rr是一个随机数.

同步消息

里面返回的json中 AddMsgList是新的消息列表,剩下的ModContactList,DelContactList,ModChatRoomMemberList我没有仔细研究.应该就是字面的意思:群人员变动,群删除,群基本信息变动等.
注意这里会返回SyncKey ,需要更新最新的SyncKey.

成功监听到微信消息:(接口中还有不少值,我的监听程序并没有去解析,具体可去看对应接口)

微信监听消息

源码中消息返回type展示:


 /*
        MSGTYPE_TEXT: 1,
        MSGTYPE_IMAGE: 3,
        MSGTYPE_VOICE: 34,
        MSGTYPE_VIDEO: 43,
        MSGTYPE_MICROVIDEO: 62,
        MSGTYPE_EMOTICON: 47,
        MSGTYPE_APP: 49,
        MSGTYPE_VOIPMSG: 50,
        MSGTYPE_VOIPNOTIFY: 52,
        MSGTYPE_VOIPINVITE: 53,
        MSGTYPE_LOCATION: 48,
        MSGTYPE_STATUSNOTIFY: 51,
        MSGTYPE_SYSNOTICE: 9999,
        MSGTYPE_POSSIBLEFRIEND_MSG: 40,
        MSGTYPE_VERIFYMSG: 37,
        MSGTYPE_SHARECARD: 42,
        MSGTYPE_SYS: 1e4,
        MSGTYPE_RECALLED: 10002,  // 撤销消息*/


微信自动发送消息接口开发(一)

一直有需求想做一个微信自动发送消息的功能,然而官方没有提供公开的接口,趁着下午有空简单的研究了一下.
大概有以下几种方法可以实现:
1:通过电脑版微信去模拟发送消息
这种方式相当于服务器开启一个微信客户端,通过程序获取客户端的句柄去模拟手动操作各种功能,这种方法优势是程序开发完后维护较少,缺点是可能存在技术难点,比方说如何定位到要发送的群,发送图片是否存在难题等.
2:通过网页版微信抓包获取接口,调用接口开发功能.
这种方式占用资源小,效率较高,延迟低,缺点是人家万一重构连接口都改了,就的重新开发了.
3:通过桌面程序内嵌一个web容器,通过web容器模拟操作各种动作.这个相对第一个简单一点,但是也存在服务器开销大,延迟高等问题.
4:通过电脑版微信抓包获取接口,模拟调用接口开发功能.
这个有点是性能达到最高,但是对我而言比较难,TCP协议抓包功夫不到家,万一中途碰到坑爬都爬不出来.

综合考虑觉得还是通过网页版微信抓包先观察观察具体情况再说.
(本人C#网抓喜欢用苏飞的HttpHelper,的确挺好用的.网址:http://www.sufeinet.com/thread-3-1-1.html)

首先访问微信网页版网址: https://wx.qq.com/

观察了一下数据包请求.
登录页面

发现微信并没有用webSocket连接进行通讯,而是采用长轮询的方式进行通讯,感觉有戏.第一次赞美IE8等浏览器的存在.决定就通过网页版微信开发自己想要的接口吧!

首先发现微信会post提交一个 https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatreport?fun=new 的接口,
然而一直没有什么返回消息,感觉像是等待消息的接口,先挖一个坑,放在这里.以后再来看.

接下去又会Get请求一个https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1515506233792
的接口.
里面有一个appid参数,不知道是怎么获取来的,通过我不懈的努力,发现...是....js里面写死的....
appiid

好吧,你牛B,姑且认为你写死吧,就当写死的用,大不了以后改版了再去你js里面取.

private HttpResult GetLoginHtml()
    {
        Item.URL = "https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=" + DateTime.Now.Ticks;
        var rst = Http.GetHtml(Item);
        return rst;
    }

恩,不需要任何cookie,直接得到uuid
window.QRLogin.code = 200; window.QRLogin.uuid = "QY1SL_Lv_Q==";

public string Get_uuid()
    {
        try
        {
            var loginRst = GetLoginHtml();
            string html = loginRst.Html;
            var uuid = Regex.Match(html, @"""[^""]+").Value.Substring(1);
            return uuid;
        }
        catch (Exception)
        {
            return null;
        }
    }

紧接着后面又是一个接口就用到uuid了.
https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=QY1SL_Lv_Q==&tip=1&r=616617253&_=1515506837549

这个接口是一个长轮询,应该就是在等待我们用手机扫描二维码了.赶紧看一看这图片是怎么生成的.

微信二维码

太棒了,这图片后面跟着的就是一个uuid(微信扫码登录有时间限制,超过多少时间就会重置,所以没法取到上面的uuid了,其实都一样,就是uuid)

OK,写一个登录自动发送图片到邮箱的方法.(邮箱发送不是本次重点,略过不谈)

public static bool Login(WXService wx, string sendMail)
    {
        string uuid = wx.Get_uuid();
        wx.uuid = uuid;
        if (string.IsNullOrEmpty(uuid))
        {
            Console.WriteLine("获取uuid失败");
            return false;
        }
        //二维码链接
        string url = "https://login.weixin.qq.com/qrcode/" + uuid;
        //发送邮件.
        MailHelper mail = new MailHelper();
        MailItem item = new MailItem();

        item.SendMailNumbers.Add(sendMail);     //发送方邮箱
        item.MailSubJect = "微信登录服务程序";
        item.IsBodyHtml = true;
        item.MailBody = $"<img src='{url}' />";

        return mail.Send(item);

    }

邮箱二维码
完美收到需要扫描的二维码,为将来自动化登录做好基础.

接下来就是长轮询请求等待扫码登录了.
上面已经说过,请求接口是https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=QY1SL_Lv_Q==&tip=1&r=616617253&_=1515506837549

值得注意的是这里的tip参数,第一次请求是1,但是如果超时后,接下来的请求都会提交0
tip

虽然说不知道具体是为了干嘛,不过做网抓的,必须要心细,任何疑点都不要放过,先记录一下留一个坑位,将来有问题了好回来思考.

private HttpResult GetTicket(string uuid)
    {
        var obj = new
        {
            loginicon = true,
            uuid = uuid,
            tip = 0,
            r = DateTime.Now.Ticks,
            _ = DateTime.Now.Ticks
        };
        string url = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?" + HttpTool.UrlEncodeObj(obj);
        Item.URL = url;

        var rst = Http.GetHtml(Item);

        return rst;
    }

HttpTool是我封装的一些网抓小功能,UrlEncodeObj就是把object对象解析成url的字符串参数.辅助函数不做详细讲解,有疑问的话可以联系我的邮箱.
如果一直没有扫描,最终接口会返回window.code=408;同时再次发出一个相同的请求.
手机扫描二维码后,接口会返回一个window.code=201;同时会将微信的头像以base64的形式返回回来.(通过测试发现,loginicon参数给false就没有头像返回回来,我的服务程序不需要图像,在后面完善时改成了false)
成功登陆后,该接口会返回

window.code=200;
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=ASDBS3liXbgbLZiGetuv9Y6a@qrticket_0&uuid=wYcsrOLhog==&lang=zh_CN&scan=1515508420";

通过验证后.这个地方又得到一个非常重要的值:ticket
根据redirect_uri接口,微信网页版在后面又加上了&fun=new&version=v2,再次发起请求
https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=ASDBS3liXbgbLZiGetuv9Y6a@qrticket_0&uuid=wYcsrOLhog==&lang=zh_CN&scan=1515508420&fun=new&version=v2
可以拿到一个xml数据.里面有用的参数应该有:skey,wxsid,wxuin,pass_ticket,isgrayscale
当然还有很多才cookice
cookice

public WXModel GetOAuthWXModel(string uuid)
    {
        bool authFlag = false;
        WXModel result = new WXModel();
        int c = 0;
        while (!authFlag)
        {
            var rst = this.GetTicket(uuid);
            if (Regex.IsMatch(rst.Html, @"window\.code=200"))
            {   //成功.
                var d = HttpTool.GetUrlCode(rst.Html);
                result.uuid = d["uuid"];
                result.ticket = d["ticket"];

                var oResult = this.GetOAuth(result.uuid, result.ticket, d["scan"]);
                string xml = oResult.Html;
                result.skey = GetXMLValue(xml, "skey");
                result.wxsid = GetXMLValue(xml, "wxsid");
                result.wxuin = GetXMLValue(xml, "wxuin");
                result.pass_ticket = GetXMLValue(xml, "pass_ticket");
                result.isgrayscale = GetXMLValue(xml, "isgrayscale");

                return result;
            }
            c++;

            if (c > 10) break;      //超时.
        }
        return null;
    }

xml

完美拿到xml数据,登录也差不多结束了,接下去要做的应该就是获取人员列表以及发送消息了.
写到这里觉得网页版微信接口抓取还算顺利,后面的数据读取希望也如现在这样一帆风顺!^_^