diff --git a/bot_test.go b/bot_test.go index fc302b2..623f0df 100644 --- a/bot_test.go +++ b/bot_test.go @@ -291,3 +291,29 @@ func TestRevokeMessage(t *testing.T) { t.Error(err) } } + +func TestSendFile(t *testing.T) { + bot := defaultBot(Desktop) + if err := bot.HotLogin(NewJsonFileHotReloadStorage("2.json"), true); err != nil { + t.Error(err) + return + } + self, err := bot.GetCurrentUser() + if err != nil { + t.Error(err) + return + } + fh, err := self.FileHelper() + if err != nil { + t.Error(err) + return + } + f, _ := os.Open("Taylor+Swift+-+Red") + defer f.Close() + msg, err := self.SendFileToFriend(fh, f) + if err != nil { + t.Error(err) + return + } + t.Log(msg.MsgId) +} diff --git a/caller.go b/caller.go index d7f6a0f..0a08f07 100644 --- a/caller.go +++ b/caller.go @@ -200,47 +200,64 @@ func (c *Caller) WebWxOplog(request *BaseRequest, remarkName, toUserName string) return parseBaseResponseError(resp) } -// 发送图片消息接口 -func (c *Caller) WebWxSendImageMsg(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) { +func (c *Caller) UploadMedia(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName, mediaType string) (*UploadResponse, error) { // 首先尝试上传图片 - sate, err := file.Stat() - if err != nil { - return nil, err - } - - var resp *ReturnResponse - if sate.Size() <= chunkSize { - resp = NewReturnResponse(c.Client.WebWxUploadMedia(file, request, info, fromUserName, toUserName, "pic")) - } else { - resp = NewReturnResponse(c.Client.WebWxUploadMediaByChunk(file, request, info, fromUserName, toUserName, "pic")) - } + resp := NewReturnResponse(c.Client.WebWxUploadMediaByChunk(file, request, info, fromUserName, toUserName, mediaType)) // 无错误上传成功之后获取请求结果,判断结果是否正常 if resp.Err() != nil { return nil, resp.Err() } defer resp.Body.Close() - var item struct { - BaseResponse BaseResponse - MediaId string - } + var item UploadResponse - if err = resp.ScanJSON(&item); err != nil { - return nil, err + if err := resp.ScanJSON(&item); err != nil { + return &item, err } if !item.BaseResponse.Ok() { - return nil, item.BaseResponse + return &item, item.BaseResponse } if len(item.MediaId) == 0 { - return nil, errors.New("upload failed") + return &item, errors.New("upload failed") + } + return &item, nil +} + +// 发送图片消息接口 +func (c *Caller) WebWxSendImageMsg(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) { + // 首先尝试上传图片 + resp, err := c.UploadMedia(file, request, info, fromUserName, toUserName, "pic") + if err != nil { + return nil, err } // 构造新的图片类型的信息 - msg := NewMediaSendMessage(ImageMessage, fromUserName, toUserName, item.MediaId) + msg := NewMediaSendMessage(ImageMessage, fromUserName, toUserName, resp.MediaId) // 发送图片信息 - resp = NewReturnResponse(c.Client.WebWxSendMsgImg(msg, request, info)) + return c.WebWxSendAppMsg(msg, request, info) +} +func (c *Caller) WebWxSendFile(file *os.File, req *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) { + resp, err := c.UploadMedia(file, req, info, fromUserName, toUserName, "doc") + if err != nil { + return nil, err + } + // 构造新的图片类型的信息 + msg := NewMediaSendMessage(ImageMessage, fromUserName, toUserName, resp.MediaId) + stat, _ := file.Stat() + appMsg := NewFileAppMessage(stat, resp.MediaId) + content, err := appMsg.XmlByte() + if err != nil { + return nil, err + } + msg.Content = string(content) + return c.WebWxSendAppMsg(msg, req, info) +} + +// 发送媒体消息 +func (c *Caller) WebWxSendAppMsg(msg *SendMessage, req *BaseRequest, info *LoginInfo) (*SentMessage, error) { + resp := NewReturnResponse(c.Client.WebWxSendMsgImg(msg, req, info)) sendSuccessMsg := &SentMessage{SendMessage: msg} - err = parseMessageResponseError(resp, sendSuccessMsg) + err := parseMessageResponseError(resp, sendSuccessMsg) return sendSuccessMsg, err } @@ -312,7 +329,7 @@ func parseMessageResponseError(resp *ReturnResponse, msg *SentMessage) error { if !messageResp.BaseResponse.Ok() { return messageResp.BaseResponse } - //// 发送成功之后将msgId赋值给SendMessage + // 发送成功之后将msgId赋值给SendMessage msg.MsgId = messageResp.MsgID return nil } diff --git a/client.go b/client.go index d2e41ee..2eef103 100644 --- a/client.go +++ b/client.go @@ -251,104 +251,12 @@ func (c *Client) WebWxGetHeadImg(headImageUrl string) (*http.Response, error) { return c.Do(req) } -// 上传文件 -func (c *Client) WebWxUploadMedia(file *os.File, request *BaseRequest, info *LoginInfo, forUserName, toUserName, mediaType string) (*http.Response, error) { - // 获取文件上传的类型 - contentType, err := GetFileContentType(file) - if err != nil { - return nil, err - } - path, _ := url.Parse(c.webWxUpLoadMediaUrl) - params := url.Values{} - params.Add("f", "json") - path.RawQuery = params.Encode() - sate, err := file.Stat() - if err != nil { - return nil, err - } - cookies := c.Jar.Cookies(path) - webWxDataTicket := getWebWxDataTicket(cookies) - - // 文件复位 - if _, err = file.Seek(0, 0); err != nil { - return nil, err - } - - buffer := bytes.Buffer{} - if _, err := buffer.ReadFrom(file); err != nil { - return nil, err - } - data := buffer.Bytes() - fileMd5 := fmt.Sprintf("%x", md5.Sum(data)) - - uploadMediaRequest := map[string]interface{}{ - "UploadType": 2, - "BaseRequest": request, - "ClientMediaId": time.Now().Unix() * 1e4, - "TotalLen": sate.Size(), - "StartPos": 0, - "DataLen": sate.Size(), - "MediaType": 4, - "FromUserName": forUserName, - "ToUserName": toUserName, - "FileMd5": fileMd5, - } - uploadMediaRequestByte, err := json.Marshal(uploadMediaRequest) - if err != nil { - return nil, err - } - - content := map[string]interface{}{ - "id": "WU_FILE_0", - "name": file.Name(), - "type": contentType, - "lastModifiedDate": sate.ModTime().Format(TimeFormat), - "size": sate.Size(), - "mediatype": mediaType, - "webwx_data_ticket": webWxDataTicket, - "pass_ticket": info.PassTicket, - } - body, err := ToBuffer(content) - if err != nil { - return nil, err - } - writer := multipart.NewWriter(body) - if err = writer.WriteField("uploadmediarequest", string(uploadMediaRequestByte)); err != nil { - return nil, err - } - if w, err := writer.CreateFormFile("filename", file.Name()); err != nil { - return nil, err - } else { - if _, err = w.Write(data); err != nil { - return nil, err - } - } - ct := writer.FormDataContentType() - if err = writer.Close(); err != nil { - return nil, err - } - req, _ := http.NewRequest(http.MethodPost, path.String(), body) - - req.Header.Set("Content-Type", ct) - return c.Do(req) -} - func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, info *LoginInfo, forUserName, toUserName, mediaType string) (*http.Response, error) { // 获取文件上传的类型 contentType, err := GetFileContentType(file) if err != nil { return nil, err } - path, _ := url.Parse(c.webWxUpLoadMediaUrl) - params := url.Values{} - params.Add("f", "json") - path.RawQuery = params.Encode() - sate, err := file.Stat() - if err != nil { - return nil, err - } - cookies := c.Jar.Cookies(path) - webWxDataTicket := getWebWxDataTicket(cookies) // 将文件的游标复原到原点 // 上面获取文件的类型的时候已经读取了512个字节 @@ -363,6 +271,20 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in data := buffer.Bytes() fileMd5 := fmt.Sprintf("%x", md5.Sum(data)) + sate, err := file.Stat() + if err != nil { + return nil, err + } + + path, _ := url.Parse(c.webWxUpLoadMediaUrl) + params := url.Values{} + params.Add("f", "json") + + path.RawQuery = params.Encode() + + cookies := c.Jar.Cookies(path) + webWxDataTicket := getWebWxDataTicket(cookies) + uploadMediaRequest := map[string]interface{}{ "UploadType": 2, "BaseRequest": request, @@ -375,18 +297,40 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in "ToUserName": toUserName, "FileMd5": fileMd5, } + uploadMediaRequestByte, err := json.Marshal(uploadMediaRequest) if err != nil { return nil, err } - chunks := sate.Size() / chunkSize - if chunks*chunkSize < sate.Size() { - chunks++ + var chunks int64 + + if sate.Size() > chunkSize { + chunks = sate.Size() / chunkSize + if chunks*chunkSize < sate.Size() { + chunks++ + } + } else { + chunks = 1 } var resp *http.Response + content := map[string]string{ + "id": "WU_FILE_0", + "name": file.Name(), + "type": contentType, + "lastModifiedDate": sate.ModTime().Format(TimeFormat), + "size": strconv.FormatInt(sate.Size(), 10), + "mediatype": mediaType, + "webwx_data_ticket": webWxDataTicket, + "pass_ticket": info.PassTicket, + } + + if chunks > 1 { + content["chunks"] = strconv.FormatInt(chunks, 10) + } + // 分块上传 for chunk := 0; int64(chunk) < chunks; chunk++ { var isLastTime bool @@ -394,18 +338,10 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in isLastTime = true } - content := map[string]string{ - "id": "WU_FILE_0", - "name": file.Name(), - "type": contentType, - "lastModifiedDate": sate.ModTime().Format(TimeFormat), - "size": strconv.FormatInt(sate.Size(), 10), - "mediatype": mediaType, - "webwx_data_ticket": webWxDataTicket, - "pass_ticket": info.PassTicket, - "chunks": strconv.FormatInt(chunks, 10), - "chunk": strconv.Itoa(chunk), + if chunks > 1 { + content["chunk"] = strconv.Itoa(chunk) } + var formBuffer bytes.Buffer writer := multipart.NewWriter(&formBuffer) @@ -450,7 +386,7 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in } } } - + // 将最后一次携带文件信息的response返回 return resp, err } @@ -645,3 +581,21 @@ func (c *Client) WebWxRevokeMsg(msg *SentMessage, request *BaseRequest) (*http.R req.Header.Set("Content-Type", jsonContentType) return c.Do(req) } + +// 校验上传文件 +func (c *Client) webWxCheckUpload(stat os.FileInfo, request *BaseRequest, fileMd5, fromUserName, toUserName string) (*http.Response, error) { + path, _ := url.Parse(c.webWxCheckUploadUrl) + content := map[string]interface{}{ + "BaseRequest": request, + "FileMd5": fileMd5, + "FileName": stat.Name(), + "FileSize": stat.Size(), + "FileType": 7, + "FromUserName": fromUserName, + "ToUserName": toUserName, + } + body, _ := ToBuffer(content) + req, _ := http.NewRequest(http.MethodPost, path.String(), body) + req.Header.Add("Content-Type", jsonContentType) + return c.Do(req) +} diff --git a/global.go b/global.go index 765d136..1fc90bd 100644 --- a/global.go +++ b/global.go @@ -1,94 +1,96 @@ package openwechat import ( - "errors" - "regexp" + "errors" + "regexp" ) var ( - uuidRegexp = regexp.MustCompile(`uuid = "(.*?)";`) - statusCodeRegexp = regexp.MustCompile(`window.code=(\d+);`) - syncCheckRegexp = regexp.MustCompile(`window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}`) - redirectUriRegexp = regexp.MustCompile(`window.redirect_uri="(.*?)"`) + uuidRegexp = regexp.MustCompile(`uuid = "(.*?)";`) + statusCodeRegexp = regexp.MustCompile(`window.code=(\d+);`) + syncCheckRegexp = regexp.MustCompile(`window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}`) + redirectUriRegexp = regexp.MustCompile(`window.redirect_uri="(.*?)"`) ) const ( - appId = "wx782c26e4c19acffb" - jsLoginUrl = "https://login.wx.qq.com/jslogin" - qrcodeUrl = "https://login.weixin.qq.com/qrcode/" - loginUrl = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login" + appId = "wx782c26e4c19acffb" + jsLoginUrl = "https://login.wx.qq.com/jslogin" + qrcodeUrl = "https://login.weixin.qq.com/qrcode/" + loginUrl = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login" - // Normal urls + // Normal urls - baseNormalUrl = "https://wx2.qq.com" - webWxNewLoginPageNormalUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage" - webWxInitNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit" - webWxStatusNotifyNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify" - webWxSyncNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsync" - webWxSendMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg" - webWxGetContactNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact" - webWxSendMsgImgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg" - webWxSendAppMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg" - webWxBatchGetContactNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact" - webWxOplogNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxoplog" - webWxVerifyUserNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser" - syncCheckNormalUrl = "https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck" - webWxUpLoadMediaNormalUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia" - webWxGetMsgImgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg" - webWxGetVoiceNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice" - webWxGetVideoNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo" - webWxLogoutNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxlogout" - webWxGetMediaNormalUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia" - webWxUpdateChatRoomNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom" - webWxRevokeMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg" + baseNormalUrl = "https://wx2.qq.com" + webWxNewLoginPageNormalUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage" + webWxInitNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit" + webWxStatusNotifyNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify" + webWxSyncNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsync" + webWxSendMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg" + webWxGetContactNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact" + webWxSendMsgImgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg" + webWxSendAppMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg" + webWxBatchGetContactNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact" + webWxOplogNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxoplog" + webWxVerifyUserNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser" + syncCheckNormalUrl = "https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck" + webWxUpLoadMediaNormalUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia" + webWxGetMsgImgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg" + webWxGetVoiceNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice" + webWxGetVideoNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo" + webWxLogoutNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxlogout" + webWxGetMediaNormalUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia" + webWxUpdateChatRoomNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom" + webWxRevokeMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg" + webWxCheckUploadNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxcheckupload" - // Desktop urls + // Desktop urls - baseDesktopUrl = "https://wx.qq.com" - webWxNewLoginPageDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop" - webWxInitDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit" - webWxStatusNotifyDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify" - webWxSyncDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync" - webWxSendMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg" - webWxGetContactDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact" - webWxSendMsgImgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg" - webWxSendAppMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg" - webWxBatchGetContactDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact" - webWxOplogDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxoplog" - webWxVerifyUserDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser" - syncCheckDesktopUrl = "https://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck" - webWxUpLoadMediaDesktopUrl = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia" - webWxGetMsgImgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg" - webWxGetVoiceDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice" - webWxGetVideoDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo" - webWxLogoutDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxlogout" - webWxGetMediaDesktopUrl = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia" - webWxUpdateChatRoomDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom" - webWxRevokeMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg" + baseDesktopUrl = "https://wx.qq.com" + webWxNewLoginPageDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop" + webWxInitDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit" + webWxStatusNotifyDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify" + webWxSyncDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync" + webWxSendMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg" + webWxGetContactDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact" + webWxSendMsgImgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg" + webWxSendAppMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg" + webWxBatchGetContactDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact" + webWxOplogDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxoplog" + webWxVerifyUserDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser" + syncCheckDesktopUrl = "https://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck" + webWxUpLoadMediaDesktopUrl = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia" + webWxGetMsgImgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg" + webWxGetVoiceDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice" + webWxGetVideoDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo" + webWxLogoutDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxlogout" + webWxGetMediaDesktopUrl = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia" + webWxUpdateChatRoomDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom" + webWxRevokeMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg" + webWxCheckUploadDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxcheckupload" - jsonContentType = "application/json; charset=utf-8" - uosPatchClientVersion = "2.0.0" - uosPatchExtspam = "Gp8ICJkIEpkICggwMDAwMDAwMRAGGoAI1GiJSIpeO1RZTq9QBKsRbPJdi84ropi16EYI10WB6g74sGmRwSNXjPQnYUKYotKkvLGpshucCaeWZMOylnc6o2AgDX9grhQQx7fm2DJRTyuNhUlwmEoWhjoG3F0ySAWUsEbH3bJMsEBwoB//0qmFJob74ffdaslqL+IrSy7LJ76/G5TkvNC+J0VQkpH1u3iJJs0uUYyLDzdBIQ6Ogd8LDQ3VKnJLm4g/uDLe+G7zzzkOPzCjXL+70naaQ9medzqmh+/SmaQ6uFWLDQLcRln++wBwoEibNpG4uOJvqXy+ql50DjlNchSuqLmeadFoo9/mDT0q3G7o/80P15ostktjb7h9bfNc+nZVSnUEJXbCjTeqS5UYuxn+HTS5nZsPVxJA2O5GdKCYK4x8lTTKShRstqPfbQpplfllx2fwXcSljuYi3YipPyS3GCAqf5A7aYYwJ7AvGqUiR2SsVQ9Nbp8MGHET1GxhifC692APj6SJxZD3i1drSYZPMMsS9rKAJTGz2FEupohtpf2tgXm6c16nDk/cw+C7K7me5j5PLHv55DFCS84b06AytZPdkFZLj7FHOkcFGJXitHkX5cgww7vuf6F3p0yM/W73SoXTx6GX4G6Hg2rYx3O/9VU2Uq8lvURB4qIbD9XQpzmyiFMaytMnqxcZJcoXCtfkTJ6pI7a92JpRUvdSitg967VUDUAQnCXCM/m0snRkR9LtoXAO1FUGpwlp1EfIdCZFPKNnXMeqev0j9W9ZrkEs9ZWcUEexSj5z+dKYQBhIICviYUQHVqBTZSNy22PlUIeDeIs11j7q4t8rD8LPvzAKWVqXE+5lS1JPZkjg4y5hfX1Dod3t96clFfwsvDP6xBSe1NBcoKbkyGxYK0UvPGtKQEE0Se2zAymYDv41klYE9s+rxp8e94/H8XhrL9oGm8KWb2RmYnAE7ry9gd6e8ZuBRIsISlJAE/e8y8xFmP031S6Lnaet6YXPsFpuFsdQs535IjcFd75hh6DNMBYhSfjv456cvhsb99+fRw/KVZLC3yzNSCbLSyo9d9BI45Plma6V8akURQA/qsaAzU0VyTIqZJkPDTzhuCl92vD2AD/QOhx6iwRSVPAxcRFZcWjgc2wCKh+uCYkTVbNQpB9B90YlNmI3fWTuUOUjwOzQRxJZj11NsimjOJ50qQwTTFj6qQvQ1a/I+MkTx5UO+yNHl718JWcR3AXGmv/aa9rD1eNP8ioTGlOZwPgmr2sor2iBpKTOrB83QgZXP+xRYkb4zVC+LoAXEoIa1+zArywlgREer7DLePukkU6wHTkuSaF+ge5Of1bXuU4i938WJHj0t3D8uQxkJvoFi/EYN/7u2P1zGRLV4dHVUsZMGCCtnO6BBigFMAA=" + jsonContentType = "application/json; charset=utf-8" + uosPatchClientVersion = "2.0.0" + uosPatchExtspam = "Gp8ICJkIEpkICggwMDAwMDAwMRAGGoAI1GiJSIpeO1RZTq9QBKsRbPJdi84ropi16EYI10WB6g74sGmRwSNXjPQnYUKYotKkvLGpshucCaeWZMOylnc6o2AgDX9grhQQx7fm2DJRTyuNhUlwmEoWhjoG3F0ySAWUsEbH3bJMsEBwoB//0qmFJob74ffdaslqL+IrSy7LJ76/G5TkvNC+J0VQkpH1u3iJJs0uUYyLDzdBIQ6Ogd8LDQ3VKnJLm4g/uDLe+G7zzzkOPzCjXL+70naaQ9medzqmh+/SmaQ6uFWLDQLcRln++wBwoEibNpG4uOJvqXy+ql50DjlNchSuqLmeadFoo9/mDT0q3G7o/80P15ostktjb7h9bfNc+nZVSnUEJXbCjTeqS5UYuxn+HTS5nZsPVxJA2O5GdKCYK4x8lTTKShRstqPfbQpplfllx2fwXcSljuYi3YipPyS3GCAqf5A7aYYwJ7AvGqUiR2SsVQ9Nbp8MGHET1GxhifC692APj6SJxZD3i1drSYZPMMsS9rKAJTGz2FEupohtpf2tgXm6c16nDk/cw+C7K7me5j5PLHv55DFCS84b06AytZPdkFZLj7FHOkcFGJXitHkX5cgww7vuf6F3p0yM/W73SoXTx6GX4G6Hg2rYx3O/9VU2Uq8lvURB4qIbD9XQpzmyiFMaytMnqxcZJcoXCtfkTJ6pI7a92JpRUvdSitg967VUDUAQnCXCM/m0snRkR9LtoXAO1FUGpwlp1EfIdCZFPKNnXMeqev0j9W9ZrkEs9ZWcUEexSj5z+dKYQBhIICviYUQHVqBTZSNy22PlUIeDeIs11j7q4t8rD8LPvzAKWVqXE+5lS1JPZkjg4y5hfX1Dod3t96clFfwsvDP6xBSe1NBcoKbkyGxYK0UvPGtKQEE0Se2zAymYDv41klYE9s+rxp8e94/H8XhrL9oGm8KWb2RmYnAE7ry9gd6e8ZuBRIsISlJAE/e8y8xFmP031S6Lnaet6YXPsFpuFsdQs535IjcFd75hh6DNMBYhSfjv456cvhsb99+fRw/KVZLC3yzNSCbLSyo9d9BI45Plma6V8akURQA/qsaAzU0VyTIqZJkPDTzhuCl92vD2AD/QOhx6iwRSVPAxcRFZcWjgc2wCKh+uCYkTVbNQpB9B90YlNmI3fWTuUOUjwOzQRxJZj11NsimjOJ50qQwTTFj6qQvQ1a/I+MkTx5UO+yNHl718JWcR3AXGmv/aa9rD1eNP8ioTGlOZwPgmr2sor2iBpKTOrB83QgZXP+xRYkb4zVC+LoAXEoIa1+zArywlgREer7DLePukkU6wHTkuSaF+ge5Of1bXuU4i938WJHj0t3D8uQxkJvoFi/EYN/7u2P1zGRLV4dHVUsZMGCCtnO6BBigFMAA=" ) // 消息类型 const ( - TextMessage = 1 - ImageMessage = 3 - AppMessage = 6 + TextMessage = 1 + ImageMessage = 3 + AppMessage = 6 ) // 登录状态 const ( - statusSuccess = "200" - statusScanned = "201" - statusTimeout = "400" - statusWait = "408" + statusSuccess = "200" + statusScanned = "201" + statusTimeout = "400" + statusWait = "408" ) // errors var ( - noSuchUserFoundError = errors.New("no such user found") + noSuchUserFoundError = errors.New("no such user found") ) // ALL跟search函数搭配 @@ -97,10 +99,19 @@ const ALL = 0 // 性别 const ( - MALE = 1 - FEMALE = 2 + MALE = 1 + FEMALE = 2 ) -const chunkSize int64 = 512 * 1024 +const ( + // 分块上传时每次上传的文件的大小 + chunkSize int64 = (1 << 20) / 2 // 0.5m + // 需要检测的文件大小 + needCheckSize int64 = 25 << 20 // 20m + // 最大文件上传大小 + maxFileUploadSize int64 = 50 << 20 // 50m + // 最大图片上传大小 + maxImageUploadSize int64 = 20 << 20 // 20m +) const TimeFormat = "Mon Jan 02 2006 15:04:05 GMT+0800 (中国标准时间)" diff --git a/items.go b/items.go index d5f8c07..a1f4d41 100644 --- a/items.go +++ b/items.go @@ -1,8 +1,8 @@ package openwechat import ( - "errors" - "fmt" + "errors" + "fmt" ) /* @@ -11,194 +11,199 @@ import ( // 登录信息 type LoginInfo struct { - Ret int `xml:"ret"` - WxUin int `xml:"wxuin"` - IsGrayScale int `xml:"isgrayscale"` - Message string `xml:"message"` - SKey string `xml:"skey"` - WxSid string `xml:"wxsid"` - PassTicket string `xml:"pass_ticket"` + Ret int `xml:"ret"` + WxUin int `xml:"wxuin"` + IsGrayScale int `xml:"isgrayscale"` + Message string `xml:"message"` + SKey string `xml:"skey"` + WxSid string `xml:"wxsid"` + PassTicket string `xml:"pass_ticket"` } func (l LoginInfo) Ok() bool { - return l.Ret == 0 + return l.Ret == 0 } func (l LoginInfo) Error() string { - return l.Message + return l.Message } // 初始的请求信息 // 几乎所有的请求都要携带该参数 type BaseRequest struct { - Uin int - Sid, Skey, DeviceID string + Uin int + Sid, Skey, DeviceID string } // 大部分返回对象都携带该信息 type BaseResponse struct { - Ret int - ErrMsg string + Ret int + ErrMsg string } func (b BaseResponse) Ok() bool { - return b.Ret == 0 + return b.Ret == 0 } func (b BaseResponse) Error() string { - if err := getResponseErrorWithRetCode(b.Ret); err != nil { - return err.Error() - } - return "" + if err := getResponseErrorWithRetCode(b.Ret); err != nil { + return err.Error() + } + return "" } func getResponseErrorWithRetCode(code int) error { - switch code { - case 0: - return nil - case 1: - return errors.New("param error") - case -14: - return errors.New("ticket error") - case 1100: - return errors.New("not login warn") - case 1101: - return errors.New("not login check") - case 1102: - return errors.New("cookie invalid error") - case 1203: - return errors.New("login env error") - case 1205: - return errors.New("opt too often") - default: - return fmt.Errorf("base response ret code %d", code) - } + switch code { + case 0: + return nil + case 1: + return errors.New("param error") + case -14: + return errors.New("ticket error") + case 1100: + return errors.New("not login warn") + case 1101: + return errors.New("not login check") + case 1102: + return errors.New("cookie invalid error") + case 1203: + return errors.New("login env error") + case 1205: + return errors.New("opt too often") + default: + return fmt.Errorf("base response ret code %d", code) + } } type SyncKey struct { - Count int - List []struct{ Key, Val int64 } + Count int + List []struct{ Key, Val int64 } } // 初始化的相应信息 type WebInitResponse struct { - Count int - ClientVersion int - GrayScale int - InviteStartCount int - MPSubscribeMsgCount int - ClickReportInterval int - SystemTime int64 - ChatSet string - SKey string - BaseResponse BaseResponse - SyncKey SyncKey - User User - MPSubscribeMsgList []MPSubscribeMsg - ContactList []User + Count int + ClientVersion int + GrayScale int + InviteStartCount int + MPSubscribeMsgCount int + ClickReportInterval int + SystemTime int64 + ChatSet string + SKey string + BaseResponse BaseResponse + SyncKey SyncKey + User User + MPSubscribeMsgList []MPSubscribeMsg + ContactList []User } // 公众号的订阅信息 type MPSubscribeMsg struct { - MPArticleCount int - Time int64 - UserName string - NickName string - MPArticleList []struct { - Title string - Cover string - Digest string - Url string - } + MPArticleCount int + Time int64 + UserName string + NickName string + MPArticleList []struct { + Title string + Cover string + Digest string + Url string + } } type UserDetailItem struct { - UserName string - EncryChatRoomId string + UserName string + EncryChatRoomId string } type UserDetailItemList []UserDetailItem func NewUserDetailItemList(members Members) UserDetailItemList { - var list UserDetailItemList - for _, member := range members { - item := UserDetailItem{UserName: member.UserName, EncryChatRoomId: member.EncryChatRoomId} - list = append(list, item) - } - return list + var list UserDetailItemList + for _, member := range members { + item := UserDetailItem{UserName: member.UserName, EncryChatRoomId: member.EncryChatRoomId} + list = append(list, item) + } + return list } type SyncCheckResponse struct { - RetCode string - Selector string + RetCode string + Selector string } func (s *SyncCheckResponse) Success() bool { - return s.RetCode == "0" + return s.RetCode == "0" } func (s *SyncCheckResponse) NorMal() bool { - return s.Success() && s.Selector == "0" + return s.Success() && s.Selector == "0" } // 实现error接口 func (s *SyncCheckResponse) Error() string { - switch s.RetCode { - case "0": - return "" - case "1": - return "param error" - case "-14": - return "ticker error" - case "1100": - return "not login warn" - case "1101": - return "not login check" - case "1102": - return "cookie invalid error" - case "1203": - return "login env error" - case "1205": - return "opt too often" - default: - return fmt.Sprintf("sync check response error code %s", s.RetCode) - } + switch s.RetCode { + case "0": + return "" + case "1": + return "param error" + case "-14": + return "ticker error" + case "1100": + return "not login warn" + case "1101": + return "not login check" + case "1102": + return "cookie invalid error" + case "1203": + return "login env error" + case "1205": + return "opt too often" + default: + return fmt.Sprintf("sync check response error code %s", s.RetCode) + } } type WebWxSyncResponse struct { - AddMsgCount int - ContinueFlag int - DelContactCount int - ModChatRoomMemberCount int - ModContactCount int - Skey string - SyncCheckKey SyncKey - SyncKey SyncKey - BaseResponse BaseResponse - ModChatRoomMemberList Members - AddMsgList []*Message + AddMsgCount int + ContinueFlag int + DelContactCount int + ModChatRoomMemberCount int + ModContactCount int + Skey string + SyncCheckKey SyncKey + SyncKey SyncKey + BaseResponse BaseResponse + ModChatRoomMemberList Members + AddMsgList []*Message } type WebWxContactResponse struct { - MemberCount int - Seq int - BaseResponse BaseResponse - MemberList []*User + MemberCount int + Seq int + BaseResponse BaseResponse + MemberList []*User } type WebWxBatchContactResponse struct { - Count int - BaseResponse BaseResponse - ContactList []*User + Count int + BaseResponse BaseResponse + ContactList []*User } type CheckLoginResponse struct { - Code string - Raw []byte + Code string + Raw []byte } type MessageResponse struct { - BaseResponse BaseResponse - LocalID string - MsgID string + BaseResponse BaseResponse + LocalID string + MsgID string +} + +type UploadResponse struct { + BaseResponse BaseResponse + MediaId string } diff --git a/message.go b/message.go index 91b98cd..105ddf8 100644 --- a/message.go +++ b/message.go @@ -1,473 +1,504 @@ package openwechat import ( - "context" - "encoding/xml" - "errors" - "fmt" - "net/http" - "os" - "strconv" - "strings" - "sync" - "time" - "unicode" + "context" + "encoding/xml" + "errors" + "fmt" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" + "unicode" ) type Message struct { - IsAt bool - AppInfo struct { - Type int - AppID string - } - AppMsgType int - HasProductId int - ImgHeight int - ImgStatus int - ImgWidth int - ForwardFlag int - MsgType int - Status int - StatusNotifyCode int - SubMsgType int - VoiceLength int - CreateTime int64 - NewMsgId int64 - PlayLength int64 - MediaId string - MsgId string - EncryFileName string - FileName string - FileSize string - Content string - FromUserName string - OriContent string - StatusNotifyUserName string - Ticket string - ToUserName string - Url string - senderInGroupUserName string - RecommendInfo RecommendInfo - Bot *Bot - mu sync.RWMutex - Context context.Context - item map[string]interface{} + IsAt bool + AppInfo struct { + Type int + AppID string + } + AppMsgType int + HasProductId int + ImgHeight int + ImgStatus int + ImgWidth int + ForwardFlag int + MsgType int + Status int + StatusNotifyCode int + SubMsgType int + VoiceLength int + CreateTime int64 + NewMsgId int64 + PlayLength int64 + MediaId string + MsgId string + EncryFileName string + FileName string + FileSize string + Content string + FromUserName string + OriContent string + StatusNotifyUserName string + Ticket string + ToUserName string + Url string + senderInGroupUserName string + RecommendInfo RecommendInfo + Bot *Bot + mu sync.RWMutex + Context context.Context + item map[string]interface{} } // 获取消息的发送者 func (m *Message) Sender() (*User, error) { - members, err := m.Bot.self.Members(true) - if err != nil { - return nil, err - } - if m.FromUserName == m.Bot.self.User.UserName { - return m.Bot.self.User, nil - } - user := members.SearchByUserName(1, m.FromUserName) - if user == nil { - return nil, noSuchUserFoundError - } - return user.First().Detail() + members, err := m.Bot.self.Members(true) + if err != nil { + return nil, err + } + if m.FromUserName == m.Bot.self.User.UserName { + return m.Bot.self.User, nil + } + user := members.SearchByUserName(1, m.FromUserName) + if user == nil { + return nil, noSuchUserFoundError + } + return user.First().Detail() } // 获取消息在群里面的发送者 func (m *Message) SenderInGroup() (*User, error) { - if !m.IsSendByGroup() { - return nil, errors.New("message is not from group") - } - group, err := m.Sender() - if err != nil { - return nil, err - } - group, err = group.Detail() - if err != nil { - return nil, err - } - users := group.MemberList.SearchByUserName(1, m.senderInGroupUserName) - if users == nil { - return nil, noSuchUserFoundError - } - return users.First(), nil + if !m.IsSendByGroup() { + return nil, errors.New("message is not from group") + } + group, err := m.Sender() + if err != nil { + return nil, err + } + group, err = group.Detail() + if err != nil { + return nil, err + } + users := group.MemberList.SearchByUserName(1, m.senderInGroupUserName) + if users == nil { + return nil, noSuchUserFoundError + } + return users.First(), nil } // 获取消息的接收者 func (m *Message) Receiver() (*User, error) { - if m.IsSendByGroup() { - if sender, err := m.Sender(); err != nil { - return nil, err - } else { - users := sender.MemberList.SearchByUserName(1, m.ToUserName) - if users == nil { - return nil, noSuchUserFoundError - } - return users.First(), nil - } - } else { - users := m.Bot.self.MemberList.SearchByUserName(1, m.ToUserName) - if users == nil { - return nil, noSuchUserFoundError - } - return users.First(), nil - } + if m.IsSendByGroup() { + if sender, err := m.Sender(); err != nil { + return nil, err + } else { + users := sender.MemberList.SearchByUserName(1, m.ToUserName) + if users == nil { + return nil, noSuchUserFoundError + } + return users.First(), nil + } + } else { + users := m.Bot.self.MemberList.SearchByUserName(1, m.ToUserName) + if users == nil { + return nil, noSuchUserFoundError + } + return users.First(), nil + } } // 判断消息是否由自己发送 func (m *Message) IsSendBySelf() bool { - return m.FromUserName == m.Bot.self.User.UserName + return m.FromUserName == m.Bot.self.User.UserName } // 判断消息是否由好友发送 func (m *Message) IsSendByFriend() bool { - return !m.IsSendByGroup() && strings.HasPrefix(m.FromUserName, "@") + return !m.IsSendByGroup() && strings.HasPrefix(m.FromUserName, "@") } // 判断消息是否由群组发送 func (m *Message) IsSendByGroup() bool { - return strings.HasPrefix(m.FromUserName, "@@") + return strings.HasPrefix(m.FromUserName, "@@") } // 回复消息 func (m *Message) Reply(msgType int, content, mediaId string) (*SentMessage, error) { - msg := NewSendMessage(msgType, content, m.Bot.self.User.UserName, m.FromUserName, mediaId) - info := m.Bot.storage.LoginInfo - request := m.Bot.storage.Request - return m.Bot.Caller.WebWxSendMsg(msg, info, request) + msg := NewSendMessage(msgType, content, m.Bot.self.User.UserName, m.FromUserName, mediaId) + info := m.Bot.storage.LoginInfo + request := m.Bot.storage.Request + return m.Bot.Caller.WebWxSendMsg(msg, info, request) } // 回复文本消息 func (m *Message) ReplyText(content string) (*SentMessage, error) { - return m.Reply(TextMessage, content, "") + return m.Reply(TextMessage, content, "") } // 回复图片消息 func (m *Message) ReplyImage(file *os.File) (*SentMessage, error) { - info := m.Bot.storage.LoginInfo - request := m.Bot.storage.Request - return m.Bot.Caller.WebWxSendImageMsg(file, request, info, m.Bot.self.UserName, m.FromUserName) + info := m.Bot.storage.LoginInfo + request := m.Bot.storage.Request + return m.Bot.Caller.WebWxSendImageMsg(file, request, info, m.Bot.self.UserName, m.FromUserName) } func (m *Message) IsText() bool { - return m.MsgType == 1 && m.Url == "" + return m.MsgType == 1 && m.Url == "" } func (m *Message) IsMap() bool { - return m.MsgType == 1 && m.Url != "" + return m.MsgType == 1 && m.Url != "" } func (m *Message) IsPicture() bool { - return m.MsgType == 3 || m.MsgType == 47 + return m.MsgType == 3 || m.MsgType == 47 } func (m *Message) IsVoice() bool { - return m.MsgType == 34 + return m.MsgType == 34 } func (m *Message) IsFriendAdd() bool { - return m.MsgType == 37 && m.FromUserName == "fmessage" + return m.MsgType == 37 && m.FromUserName == "fmessage" } func (m *Message) IsCard() bool { - return m.MsgType == 42 + return m.MsgType == 42 } func (m *Message) IsVideo() bool { - return m.MsgType == 43 || m.MsgType == 62 + return m.MsgType == 43 || m.MsgType == 62 } func (m *Message) IsMedia() bool { - return m.MsgType == 49 + return m.MsgType == 49 } // 判断是否撤回 func (m *Message) IsRecalled() bool { - return m.MsgType == 10002 + return m.MsgType == 10002 } func (m *Message) IsSystem() bool { - return m.MsgType == 10000 + return m.MsgType == 10000 } func (m *Message) IsNotify() bool { - return m.MsgType == 51 && m.StatusNotifyCode != 0 + return m.MsgType == 51 && m.StatusNotifyCode != 0 } // 判断当前的消息是不是微信转账 func (m *Message) IsTransferAccounts() bool { - return m.IsMedia() && m.FileName == "微信转账" + return m.IsMedia() && m.FileName == "微信转账" } // 判断当前是否发出红包 func (m *Message) IsSendRedPacket() bool { - return m.IsSystem() && m.Content == "发出红包,请在手机上查看" + return m.IsSystem() && m.Content == "发出红包,请在手机上查看" } // 判断当前是否收到红包 func (m *Message) IsReceiveRedPacket() bool { - return m.IsSystem() && m.Content == "收到红包,请在手机上查看" + return m.IsSystem() && m.Content == "收到红包,请在手机上查看" } func (m *Message) IsSysNotice() bool { - return m.MsgType == 9999 + return m.MsgType == 9999 } // 判断是否为操作通知消息 func (m *Message) StatusNotify() bool { - return m.MsgType == 51 + return m.MsgType == 51 } // 判断消息是否为文件类型的消息 func (m *Message) HasFile() bool { - return m.IsPicture() || m.IsVoice() || m.IsVideo() || m.IsMedia() + return m.IsPicture() || m.IsVoice() || m.IsVideo() || m.IsMedia() } // 获取文件消息的文件 func (m *Message) GetFile() (*http.Response, error) { - if !m.HasFile() { - return nil, errors.New("invalid message type") - } - if m.IsPicture() { - return m.Bot.Caller.Client.WebWxGetMsgImg(m, m.Bot.storage.LoginInfo) - } - if m.IsVoice() { - return m.Bot.Caller.Client.WebWxGetVoice(m, m.Bot.storage.LoginInfo) - } - if m.IsVideo() { - return m.Bot.Caller.Client.WebWxGetVideo(m, m.Bot.storage.LoginInfo) - } - if m.IsMedia() { - return m.Bot.Caller.Client.WebWxGetMedia(m, m.Bot.storage.LoginInfo) - } - return nil, errors.New("unsupported type") + if !m.HasFile() { + return nil, errors.New("invalid message type") + } + if m.IsPicture() { + return m.Bot.Caller.Client.WebWxGetMsgImg(m, m.Bot.storage.LoginInfo) + } + if m.IsVoice() { + return m.Bot.Caller.Client.WebWxGetVoice(m, m.Bot.storage.LoginInfo) + } + if m.IsVideo() { + return m.Bot.Caller.Client.WebWxGetVideo(m, m.Bot.storage.LoginInfo) + } + if m.IsMedia() { + return m.Bot.Caller.Client.WebWxGetMedia(m, m.Bot.storage.LoginInfo) + } + return nil, errors.New("unsupported type") } // 获取card类型 func (m *Message) Card() (*Card, error) { - if !m.IsCard() { - return nil, errors.New("card message required") - } - var card Card - content := XmlFormString(m.Content) - err := xml.Unmarshal([]byte(content), &card) - return &card, err + if !m.IsCard() { + return nil, errors.New("card message required") + } + var card Card + content := XmlFormString(m.Content) + err := xml.Unmarshal([]byte(content), &card) + return &card, err } // 获取FriendAddMessageContent内容 func (m *Message) FriendAddMessageContent() (*FriendAddMessage, error) { - if !m.IsFriendAdd() { - return nil, errors.New("friend add message required") - } - var f FriendAddMessage - content := XmlFormString(m.Content) - err := xml.Unmarshal([]byte(content), &f) - return &f, err + if !m.IsFriendAdd() { + return nil, errors.New("friend add message required") + } + var f FriendAddMessage + content := XmlFormString(m.Content) + err := xml.Unmarshal([]byte(content), &f) + return &f, err } // 获取撤回消息的内容 func (m *Message) RevokeMsg() (*RevokeMsg, error) { - if !m.IsRecalled() { - return nil, errors.New("recalled message required") - } - var r RevokeMsg - content := XmlFormString(m.Content) - err := xml.Unmarshal([]byte(content), &r) - return &r, err + if !m.IsRecalled() { + return nil, errors.New("recalled message required") + } + var r RevokeMsg + content := XmlFormString(m.Content) + err := xml.Unmarshal([]byte(content), &r) + return &r, err } // 同意好友的请求 func (m *Message) Agree(verifyContents ...string) error { - if !m.IsFriendAdd() { - return fmt.Errorf("friend add message required") - } - var builder strings.Builder - for _, v := range verifyContents { - builder.WriteString(v) - } - return m.Bot.Caller.WebWxVerifyUser(m.Bot.storage, m.RecommendInfo, builder.String()) + if !m.IsFriendAdd() { + return fmt.Errorf("friend add message required") + } + var builder strings.Builder + for _, v := range verifyContents { + builder.WriteString(v) + } + return m.Bot.Caller.WebWxVerifyUser(m.Bot.storage, m.RecommendInfo, builder.String()) } // 往消息上下文中设置值 // goroutine safe func (m *Message) Set(key string, value interface{}) { - m.mu.Lock() - defer m.mu.Unlock() - if m.item == nil { - m.item = make(map[string]interface{}) - } - m.item[key] = value + m.mu.Lock() + defer m.mu.Unlock() + if m.item == nil { + m.item = make(map[string]interface{}) + } + m.item[key] = value } // 从消息上下文中获取值 // goroutine safe func (m *Message) Get(key string) (value interface{}, exist bool) { - m.mu.RLock() - defer m.mu.RUnlock() - value, exist = m.item[key] - return + m.mu.RLock() + defer m.mu.RUnlock() + value, exist = m.item[key] + return } // 消息初始化,根据不同的消息作出不同的处理 func (m *Message) init(bot *Bot) { - m.Bot = bot - if m.IsSendByGroup() { - data := strings.Split(m.Content, ":
") - m.Content = strings.Join(data[1:], "") - m.senderInGroupUserName = data[0] - receiver, err := m.Receiver() - if err == nil { - displayName := receiver.DisplayName - if displayName == "" { - displayName = receiver.NickName - } - atFlag := "@" + displayName - index := len(atFlag) + 1 + 1 - if strings.HasPrefix(m.Content, atFlag) && unicode.IsSpace(rune(m.Content[index])) { - m.IsAt = true - m.Content = m.Content[index+1:] - } - } - } + m.Bot = bot + if m.IsSendByGroup() { + data := strings.Split(m.Content, ":
") + m.Content = strings.Join(data[1:], "") + m.senderInGroupUserName = data[0] + receiver, err := m.Receiver() + if err == nil { + displayName := receiver.DisplayName + if displayName == "" { + displayName = receiver.NickName + } + atFlag := "@" + displayName + index := len(atFlag) + 1 + 1 + if strings.HasPrefix(m.Content, atFlag) && unicode.IsSpace(rune(m.Content[index])) { + m.IsAt = true + m.Content = m.Content[index+1:] + } + } + } } // 发送消息的结构体 type SendMessage struct { - Type int - Content string - FromUserName string - ToUserName string - LocalID string - ClientMsgId string - MediaId string + Type int + Content string + FromUserName string + ToUserName string + LocalID string + ClientMsgId string + MediaId string } // SendMessage的构造方法 func NewSendMessage(msgType int, content, fromUserName, toUserName, mediaId string) *SendMessage { - id := strconv.FormatInt(time.Now().UnixNano()/1e2, 10) - return &SendMessage{ - Type: msgType, - Content: content, - FromUserName: fromUserName, - ToUserName: toUserName, - LocalID: id, - ClientMsgId: id, - MediaId: mediaId, - } + id := strconv.FormatInt(time.Now().UnixNano()/1e2, 10) + return &SendMessage{ + Type: msgType, + Content: content, + FromUserName: fromUserName, + ToUserName: toUserName, + LocalID: id, + ClientMsgId: id, + MediaId: mediaId, + } } // 文本消息的构造方法 func NewTextSendMessage(content, fromUserName, toUserName string) *SendMessage { - return NewSendMessage(TextMessage, content, fromUserName, toUserName, "") + return NewSendMessage(TextMessage, content, fromUserName, toUserName, "") } // 媒体消息的构造方法 func NewMediaSendMessage(msgType int, fromUserName, toUserName, mediaId string) *SendMessage { - return NewSendMessage(msgType, "", fromUserName, toUserName, mediaId) + return NewSendMessage(msgType, "", fromUserName, toUserName, mediaId) } // 一些特殊类型的消息会携带该结构体信息 type RecommendInfo struct { - OpCode int - Scene int - Sex int - VerifyFlag int - AttrStatus int64 - QQNum int64 - Alias string - City string - Content string - NickName string - Province string - Signature string - Ticket string - UserName string + OpCode int + Scene int + Sex int + VerifyFlag int + AttrStatus int64 + QQNum int64 + Alias string + City string + Content string + NickName string + Province string + Signature string + Ticket string + UserName string } // 名片消息内容 type Card struct { - XMLName xml.Name `xml:"msg"` - ImageStatus int `xml:"imagestatus,attr"` - Scene int `xml:"scene,attr"` - Sex int `xml:"sex,attr"` - Certflag int `xml:"certflag,attr"` - BigHeadImgUrl string `xml:"bigheadimgurl,attr"` - SmallHeadImgUrl string `xml:"smallheadimgurl,attr"` - UserName string `xml:"username,attr"` - NickName string `xml:"nickname,attr"` - ShortPy string `xml:"shortpy,attr"` - Alias string `xml:"alias,attr"` // Note: 这个是名片用户的微信号 - Province string `xml:"province,attr"` - City string `xml:"city,attr"` - Sign string `xml:"sign,attr"` - Certinfo string `xml:"certinfo,attr"` - BrandIconUrl string `xml:"brandIconUrl,attr"` - BrandHomeUr string `xml:"brandHomeUr,attr"` - BrandSubscriptConfigUrl string `xml:"brandSubscriptConfigUrl,attr"` - BrandFlags string `xml:"brandFlags,attr"` - RegionCode string `xml:"regionCode,attr"` + XMLName xml.Name `xml:"msg"` + ImageStatus int `xml:"imagestatus,attr"` + Scene int `xml:"scene,attr"` + Sex int `xml:"sex,attr"` + Certflag int `xml:"certflag,attr"` + BigHeadImgUrl string `xml:"bigheadimgurl,attr"` + SmallHeadImgUrl string `xml:"smallheadimgurl,attr"` + UserName string `xml:"username,attr"` + NickName string `xml:"nickname,attr"` + ShortPy string `xml:"shortpy,attr"` + Alias string `xml:"alias,attr"` // Note: 这个是名片用户的微信号 + Province string `xml:"province,attr"` + City string `xml:"city,attr"` + Sign string `xml:"sign,attr"` + Certinfo string `xml:"certinfo,attr"` + BrandIconUrl string `xml:"brandIconUrl,attr"` + BrandHomeUr string `xml:"brandHomeUr,attr"` + BrandSubscriptConfigUrl string `xml:"brandSubscriptConfigUrl,attr"` + BrandFlags string `xml:"brandFlags,attr"` + RegionCode string `xml:"regionCode,attr"` } // 好友添加消息信息内容 type FriendAddMessage struct { - XMLName xml.Name `xml:"msg"` - Shortpy int `xml:"shortpy,attr"` - ImageStatus int `xml:"imagestatus,attr"` - Scene int `xml:"scene,attr"` - PerCard int `xml:"percard,attr"` - Sex int `xml:"sex,attr"` - AlbumFlag int `xml:"albumflag,attr"` - AlbumStyle int `xml:"albumstyle,attr"` - SnsFlag int `xml:"snsflag,attr"` - Opcode int `xml:"opcode,attr"` - FromUserName string `xml:"fromusername,attr"` - EncryptUserName string `xml:"encryptusername,attr"` - FromNickName string `xml:"fromnickname,attr"` - Content string `xml:"content,attr"` - Country string `xml:"country,attr"` - Province string `xml:"province,attr"` - City string `xml:"city,attr"` - Sign string `xml:"sign,attr"` - Alias string `xml:"alias,attr"` - WeiBo string `xml:"weibo,attr"` - AlbumBgImgId string `xml:"albumbgimgid,attr"` - SnsBgImgId string `xml:"snsbgimgid,attr"` - SnsBgObjectId string `xml:"snsbgobjectid,attr"` - MHash string `xml:"mhash,attr"` - MFullHash string `xml:"mfullhash,attr"` - BigHeadImgUrl string `xml:"bigheadimgurl,attr"` - SmallHeadImgUrl string `xml:"smallheadimgurl,attr"` - Ticket string `xml:"ticket,attr"` - GoogleContact string `xml:"googlecontact,attr"` - QrTicket string `xml:"qrticket,attr"` - ChatRoomUserName string `xml:"chatroomusername,attr"` - SourceUserName string `xml:"sourceusername,attr"` - ShareCardUserName string `xml:"sharecardusername,attr"` - ShareCardNickName string `xml:"sharecardnickname,attr"` - CardVersion string `xml:"cardversion,attr"` - BrandList struct { - Count int `xml:"count,attr"` - Ver int64 `xml:"ver,attr"` - } `xml:"brandlist"` + XMLName xml.Name `xml:"msg"` + Shortpy int `xml:"shortpy,attr"` + ImageStatus int `xml:"imagestatus,attr"` + Scene int `xml:"scene,attr"` + PerCard int `xml:"percard,attr"` + Sex int `xml:"sex,attr"` + AlbumFlag int `xml:"albumflag,attr"` + AlbumStyle int `xml:"albumstyle,attr"` + SnsFlag int `xml:"snsflag,attr"` + Opcode int `xml:"opcode,attr"` + FromUserName string `xml:"fromusername,attr"` + EncryptUserName string `xml:"encryptusername,attr"` + FromNickName string `xml:"fromnickname,attr"` + Content string `xml:"content,attr"` + Country string `xml:"country,attr"` + Province string `xml:"province,attr"` + City string `xml:"city,attr"` + Sign string `xml:"sign,attr"` + Alias string `xml:"alias,attr"` + WeiBo string `xml:"weibo,attr"` + AlbumBgImgId string `xml:"albumbgimgid,attr"` + SnsBgImgId string `xml:"snsbgimgid,attr"` + SnsBgObjectId string `xml:"snsbgobjectid,attr"` + MHash string `xml:"mhash,attr"` + MFullHash string `xml:"mfullhash,attr"` + BigHeadImgUrl string `xml:"bigheadimgurl,attr"` + SmallHeadImgUrl string `xml:"smallheadimgurl,attr"` + Ticket string `xml:"ticket,attr"` + GoogleContact string `xml:"googlecontact,attr"` + QrTicket string `xml:"qrticket,attr"` + ChatRoomUserName string `xml:"chatroomusername,attr"` + SourceUserName string `xml:"sourceusername,attr"` + ShareCardUserName string `xml:"sharecardusername,attr"` + ShareCardNickName string `xml:"sharecardnickname,attr"` + CardVersion string `xml:"cardversion,attr"` + BrandList struct { + Count int `xml:"count,attr"` + Ver int64 `xml:"ver,attr"` + } `xml:"brandlist"` } // 撤回消息Content type RevokeMsg struct { - SysMsg xml.Name `xml:"sysmsg"` - Type string `xml:"type,attr"` - RevokeMsg struct { - OldMsgId int64 `xml:"oldmsgid"` - MsgId int64 `xml:"msgid"` - Session string `xml:"session"` - ReplaceMsg string `xml:"replacemsg"` - } `xml:"revokemsg"` + SysMsg xml.Name `xml:"sysmsg"` + Type string `xml:"type,attr"` + RevokeMsg struct { + OldMsgId int64 `xml:"oldmsgid"` + MsgId int64 `xml:"msgid"` + Session string `xml:"session"` + ReplaceMsg string `xml:"replacemsg"` + } `xml:"revokemsg"` } // 已发送的信息 type SentMessage struct { - *SendMessage - Self *Self - MsgId string + *SendMessage + Self *Self + MsgId string } // 撤回该消息 func (s *SentMessage) Revoke() error { - return s.Self.RevokeMessage(s) + return s.Self.RevokeMessage(s) +} + +type FileAppMessage struct { + AppMsg xml.Name `xml:"appmsg"` + Type int `xml:"type"` + AppId string `xml:"appid,attr"` // wxeb7ec651dd0aefa9 + SdkVer string `xml:"sdkver,attr"` + Title string `xml:"title"` + Des string `xml:"des"` + Action string `xml:"action"` + Content string `xml:"content"` + Url string `xml:"url"` + LowUrl string `xml:"lowurl"` + ExtInfo string `xml:"extinfo"` + AppAttach struct { + TotalLen int64 `xml:"totallen"` + AttachId string `xml:"attachid"` + FileExt string `xml:"fileext"` + } `xml:"appattach"` +} + +func (f FileAppMessage) XmlByte() ([]byte, error) { + return xml.Marshal(f) +} + +func NewFileAppMessage(stat os.FileInfo, attachId string) *FileAppMessage { + m := &FileAppMessage{AppId: "wxeb7ec651dd0aefa9", Title: stat.Name()} + m.AppAttach.AttachId = attachId + m.AppAttach.TotalLen = stat.Size() + m.AppAttach.FileExt = getFileExt(stat.Name()) + return m } diff --git a/parser.go b/parser.go index a2c3e3d..1fb625c 100644 --- a/parser.go +++ b/parser.go @@ -1,66 +1,74 @@ package openwechat import ( - "bytes" - "encoding/json" - "math/rand" - "mime/multipart" - "net/http" - "strconv" - "strings" - "time" + "bytes" + "encoding/json" + "math/rand" + "mime/multipart" + "net/http" + "strconv" + "strings" + "time" ) func ToBuffer(v interface{}) (*bytes.Buffer, error) { - buf, err := json.Marshal(v) - if err != nil { - return nil, err - } - return bytes.NewBuffer(buf), nil + buf, err := json.Marshal(v) + if err != nil { + return nil, err + } + return bytes.NewBuffer(buf), nil } // 获取随机设备id func GetRandomDeviceId() string { - rand.Seed(time.Now().Unix()) - var builder strings.Builder - builder.WriteString("e") - for i := 0; i < 15; i++ { - r := rand.Intn(9) - builder.WriteString(strconv.Itoa(r)) - } - return builder.String() + rand.Seed(time.Now().Unix()) + var builder strings.Builder + builder.WriteString("e") + for i := 0; i < 15; i++ { + r := rand.Intn(9) + builder.WriteString(strconv.Itoa(r)) + } + return builder.String() } func getWebWxDataTicket(cookies []*http.Cookie) string { - for _, cookie := range cookies { - if cookie.Name == "webwx_data_ticket" { - return cookie.Value - } - } - return "" + for _, cookie := range cookies { + if cookie.Name == "webwx_data_ticket" { + return cookie.Value + } + } + return "" } // Form Xml 格式化 func XmlFormString(text string) string { - lt := strings.ReplaceAll(text, "<", "<") - gt := strings.ReplaceAll(lt, ">", ">") - br := strings.ReplaceAll(gt, "
", "\n") - return strings.ReplaceAll(br, "&amp;", "&") + lt := strings.ReplaceAll(text, "<", "<") + gt := strings.ReplaceAll(lt, ">", ">") + br := strings.ReplaceAll(gt, "
", "\n") + return strings.ReplaceAll(br, "&amp;", "&") } func getTotalDuration(delay ...time.Duration) time.Duration { - var total time.Duration - for _, d := range delay { - total += d - } - return total + var total time.Duration + for _, d := range delay { + total += d + } + return total } // 获取文件上传的类型 func GetFileContentType(file multipart.File) (string, error) { - data := make([]byte, 512) - if _, err := file.Read(data); err != nil { - return "", err - } - return http.DetectContentType(data), nil + data := make([]byte, 512) + if _, err := file.Read(data); err != nil { + return "", err + } + return http.DetectContentType(data), nil +} + +func getFileExt(name string) string { + results := strings.Split(name, ".") + if len(results) == 0 { + return "undefined" + } + return results[len(results)-1] } diff --git a/url.go b/url.go index 8f11727..42a539e 100644 --- a/url.go +++ b/url.go @@ -23,6 +23,7 @@ type UrlManager struct { webWxGetMediaUrl string webWxUpdateChatRoomUrl string webWxRevokeMsg string + webWxCheckUploadUrl string } var ( @@ -49,6 +50,7 @@ var ( webWxGetMediaUrl: webWxGetMediaDesktopUrl, webWxUpdateChatRoomUrl: webWxUpdateChatRoomDesktopUrl, webWxRevokeMsg: webWxRevokeMsgDesktopUrl, + webWxCheckUploadUrl: webWxCheckUploadDesktopUrl, } // 网页版 @@ -74,6 +76,7 @@ var ( webWxGetMediaUrl: webWxGetMediaNormalUrl, webWxUpdateChatRoomUrl: webWxUpdateChatRoomNormalUrl, webWxRevokeMsg: webWxRevokeMsgNormalUrl, + webWxCheckUploadUrl: webWxCheckUploadNormalUrl, } ) diff --git a/user.go b/user.go index 5e60b9d..a181f96 100644 --- a/user.go +++ b/user.go @@ -1,336 +1,342 @@ package openwechat import ( - "bytes" - "errors" - "fmt" - "net/http" - "os" - "strings" + "bytes" + "errors" + "fmt" + "net/http" + "os" + "strings" ) // 抽象的用户结构: 好友 群组 公众号 type User struct { - Uin int - HideInputBarFlag int - StarFriend int - Sex int - AppAccountFlag int - VerifyFlag int - ContactFlag int - WebWxPluginSwitch int - HeadImgFlag int - SnsFlag int - IsOwner int - MemberCount int - ChatRoomId int - UniFriend int - OwnerUin int - Statues int - AttrStatus int - Province string - City string - Alias string - DisplayName string - KeyWord string - EncryChatRoomId string - UserName string - NickName string - HeadImgUrl string - RemarkName string - PYInitial string - PYQuanPin string - RemarkPYInitial string - RemarkPYQuanPin string - Signature string + Uin int + HideInputBarFlag int + StarFriend int + Sex int + AppAccountFlag int + VerifyFlag int + ContactFlag int + WebWxPluginSwitch int + HeadImgFlag int + SnsFlag int + IsOwner int + MemberCount int + ChatRoomId int + UniFriend int + OwnerUin int + Statues int + AttrStatus int + Province string + City string + Alias string + DisplayName string + KeyWord string + EncryChatRoomId string + UserName string + NickName string + HeadImgUrl string + RemarkName string + PYInitial string + PYQuanPin string + RemarkPYInitial string + RemarkPYQuanPin string + Signature string - MemberList Members + MemberList Members - Self *Self + Self *Self } // implement fmt.Stringer func (u *User) String() string { - return fmt.Sprintf("", u.NickName) + return fmt.Sprintf("", u.NickName) } // 获取用户头像 func (u *User) GetAvatarResponse() (*http.Response, error) { - return u.Self.Bot.Caller.Client.WebWxGetHeadImg(u.HeadImgUrl) + return u.Self.Bot.Caller.Client.WebWxGetHeadImg(u.HeadImgUrl) } // 下载用户头像 func (u *User) SaveAvatar(filename string) error { - resp, err := u.GetAvatarResponse() - if err != nil { - return err - } - defer resp.Body.Close() - buffer := bytes.Buffer{} - if _, err := buffer.ReadFrom(resp.Body); err != nil { - return err - } - file, err := os.Create(filename) - if err != nil { - return err - } - defer file.Close() - _, err = file.Write(buffer.Bytes()) - return err + resp, err := u.GetAvatarResponse() + if err != nil { + return err + } + defer resp.Body.Close() + buffer := bytes.Buffer{} + if _, err := buffer.ReadFrom(resp.Body); err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.Write(buffer.Bytes()) + return err } // 获取用户的详情 func (u *User) Detail() (*User, error) { - members := Members{u} - request := u.Self.Bot.storage.Request - newMembers, err := u.Self.Bot.Caller.WebWxBatchGetContact(members, request) - if err != nil { - return nil, err - } - user := newMembers.First() - user.Self = u.Self - return user, nil + members := Members{u} + request := u.Self.Bot.storage.Request + newMembers, err := u.Self.Bot.Caller.WebWxBatchGetContact(members, request) + if err != nil { + return nil, err + } + user := newMembers.First() + user.Self = u.Self + return user, nil } // 判断是否为好友 func (u *User) IsFriend() bool { - return !u.IsGroup() && strings.HasPrefix(u.UserName, "@") && u.VerifyFlag == 0 + return !u.IsGroup() && strings.HasPrefix(u.UserName, "@") && u.VerifyFlag == 0 } // 判断是否为群组 func (u *User) IsGroup() bool { - return strings.HasPrefix(u.UserName, "@@") && u.VerifyFlag == 0 + return strings.HasPrefix(u.UserName, "@@") && u.VerifyFlag == 0 } // 判断是否为公众号 func (u *User) IsMP() bool { - return u.VerifyFlag == 8 || u.VerifyFlag == 24 || u.VerifyFlag == 136 + return u.VerifyFlag == 8 || u.VerifyFlag == 24 || u.VerifyFlag == 136 } // 自己,当前登录用户对象 type Self struct { - *User - Bot *Bot - fileHelper *Friend - members Members - friends Friends - groups Groups - mps Mps + *User + Bot *Bot + fileHelper *Friend + members Members + friends Friends + groups Groups + mps Mps } // 获取所有的好友、群组、公众号信息 func (s *Self) Members(update ...bool) (Members, error) { - // 首先判断缓存里有没有,如果没有则去更新缓存 - if s.members == nil { - if err := s.updateMembers(); err != nil { - return nil, err - } - return s.members, nil - } - // 判断是否需要更新,如果传入的参数不为nil,则取最后一个 - if len(update) > 0 && update[0] { - if err := s.updateMembers(); err != nil { - return nil, err - } - } - return s.members, nil + // 首先判断缓存里有没有,如果没有则去更新缓存 + if s.members == nil { + if err := s.updateMembers(); err != nil { + return nil, err + } + return s.members, nil + } + // 判断是否需要更新,如果传入的参数不为nil,则取最后一个 + if len(update) > 0 && update[0] { + if err := s.updateMembers(); err != nil { + return nil, err + } + } + return s.members, nil } // 更新联系人处理 func (s *Self) updateMembers() error { - info := s.Bot.storage.LoginInfo - members, err := s.Bot.Caller.WebWxGetContact(info) - if err != nil { - return err - } - members.SetOwner(s) - s.members = members - return nil + info := s.Bot.storage.LoginInfo + members, err := s.Bot.Caller.WebWxGetContact(info) + if err != nil { + return err + } + members.SetOwner(s) + s.members = members + return nil } // 获取文件传输助手对象,封装成Friend返回 // fh, err := self.FileHelper() // or fh := openwechat.NewFriendHelper(self) func (s *Self) FileHelper() (*Friend, error) { - // 如果缓存里有,直接返回,否则去联系人里面找 - if s.fileHelper != nil { - return s.fileHelper, nil - } - members, err := s.Members() - if err != nil { - return nil, err - } - users := members.SearchByUserName(1, "filehelper") - if users == nil { - return NewFriendHelper(s), nil - } - s.fileHelper = &Friend{users.First()} - return s.fileHelper, nil + // 如果缓存里有,直接返回,否则去联系人里面找 + if s.fileHelper != nil { + return s.fileHelper, nil + } + members, err := s.Members() + if err != nil { + return nil, err + } + users := members.SearchByUserName(1, "filehelper") + if users == nil { + return NewFriendHelper(s), nil + } + s.fileHelper = &Friend{users.First()} + return s.fileHelper, nil } // 获取所有的好友 func (s *Self) Friends(update ...bool) (Friends, error) { - if s.friends == nil || (len(update) > 0 && update[0]) { - if _, err := s.Members(true); err != nil { - return nil, err - } - s.friends = s.members.Friends() - } - return s.friends, nil + if s.friends == nil || (len(update) > 0 && update[0]) { + if _, err := s.Members(true); err != nil { + return nil, err + } + s.friends = s.members.Friends() + } + return s.friends, nil } // 获取所有的群组 func (s *Self) Groups(update ...bool) (Groups, error) { - if s.groups == nil || (len(update) > 0 && update[0]) { - if _, err := s.Members(true); err != nil { - return nil, err - } - s.groups = s.members.Groups() - } - return s.groups, nil + if s.groups == nil || (len(update) > 0 && update[0]) { + if _, err := s.Members(true); err != nil { + return nil, err + } + s.groups = s.members.Groups() + } + return s.groups, nil } // 获取所有的公众号 func (s *Self) Mps(update ...bool) (Mps, error) { - if s.mps == nil || (len(update) > 0 && update[0]) { - if _, err := s.Members(true); err != nil { - return nil, err - } - s.mps = s.members.MPs() - } - return s.mps, nil + if s.mps == nil || (len(update) > 0 && update[0]) { + if _, err := s.Members(true); err != nil { + return nil, err + } + s.mps = s.members.MPs() + } + return s.mps, nil } // 更新所有的联系人信息 func (s *Self) UpdateMembersDetail() error { - // 先获取所有的联系人 - members, err := s.Members() - if err != nil { - return err - } - return members.detail(s) + // 先获取所有的联系人 + members, err := s.Members() + if err != nil { + return err + } + return members.detail(s) } // 抽象发送消息接口 func (s *Self) sendMessageToUser(user *User, msg *SendMessage) (*SentMessage, error) { - msg.FromUserName = s.UserName - msg.ToUserName = user.UserName - info := s.Bot.storage.LoginInfo - request := s.Bot.storage.Request - successSendMessage, err := s.Bot.Caller.WebWxSendMsg(msg, info, request) - if err != nil { - return nil, err - } - successSendMessage.Self = s - return successSendMessage, nil + msg.FromUserName = s.UserName + msg.ToUserName = user.UserName + info := s.Bot.storage.LoginInfo + request := s.Bot.storage.Request + successSendMessage, err := s.Bot.Caller.WebWxSendMsg(msg, info, request) + if err != nil { + return nil, err + } + successSendMessage.Self = s + return successSendMessage, nil } // 发送消息给好友 func (s *Self) SendMessageToFriend(friend *Friend, msg *SendMessage) (*SentMessage, error) { - return s.sendMessageToUser(friend.User, msg) + return s.sendMessageToUser(friend.User, msg) } // 发送文本消息给好友 func (s *Self) SendTextToFriend(friend *Friend, text string) (*SentMessage, error) { - msg := NewTextSendMessage(text, s.UserName, friend.UserName) - return s.SendMessageToFriend(friend, msg) + msg := NewTextSendMessage(text, s.UserName, friend.UserName) + return s.SendMessageToFriend(friend, msg) } // 发送图片消息给好友 func (s *Self) SendImageToFriend(friend *Friend, file *os.File) (*SentMessage, error) { - req := s.Bot.storage.Request - info := s.Bot.storage.LoginInfo - return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, friend.UserName) + req := s.Bot.storage.Request + info := s.Bot.storage.LoginInfo + return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, friend.UserName) +} + +func (s *Self) SendFileToFriend(friend *Friend, file *os.File) (*SentMessage, error) { + req := s.Bot.storage.Request + info := s.Bot.storage.LoginInfo + return s.Bot.Caller.WebWxSendFile(file, req, info, s.UserName, friend.UserName) } // 设置好友备注 // self.SetRemarkNameToFriend(friend, "remark") // or friend.SetRemarkName("remark") func (s *Self) SetRemarkNameToFriend(friend *Friend, remarkName string) error { - req := s.Bot.storage.Request - return s.Bot.Caller.WebWxOplog(req, remarkName, friend.UserName) + req := s.Bot.storage.Request + return s.Bot.Caller.WebWxOplog(req, remarkName, friend.UserName) } // 拉多名好友进群 // 最好自己是群主,成功率高一点,因为有的群允许非群组拉人,而有的群不允许 func (s *Self) AddFriendsIntoGroup(group *Group, friends ...*Friend) error { - if len(friends) == 0 { - return nil - } - // 获取群的所有的群员 - groupMembers, err := group.Members() - if err != nil { - return err - } - // 判断当前的成员在不在群里面 - for _, friend := range friends { - for _, member := range groupMembers { - if member.UserName == friend.UserName { - return fmt.Errorf("user %s has alreay in this group", friend.String()) - } - } - } - req := s.Bot.storage.Request - info := s.Bot.storage.LoginInfo - return s.Bot.Caller.AddFriendIntoChatRoom(req, info, group, friends...) + if len(friends) == 0 { + return nil + } + // 获取群的所有的群员 + groupMembers, err := group.Members() + if err != nil { + return err + } + // 判断当前的成员在不在群里面 + for _, friend := range friends { + for _, member := range groupMembers { + if member.UserName == friend.UserName { + return fmt.Errorf("user %s has alreay in this group", friend.String()) + } + } + } + req := s.Bot.storage.Request + info := s.Bot.storage.LoginInfo + return s.Bot.Caller.AddFriendIntoChatRoom(req, info, group, friends...) } // 从群聊中移除用户 // Deprecated // 无论是网页版,还是程序上都不起作用 func (s *Self) RemoveMemberFromGroup(group *Group, members Members) error { - if len(members) == 0 { - return nil - } - if group.IsOwner == 0 { - return errors.New("group owner required") - } - groupMembers, err := group.Members() - if err != nil { - return err - } - // 判断用户是否在群聊中 - var count int - for _, member := range members { - for _, gm := range groupMembers { - if gm.UserName == member.UserName { - count++ - } - } - } - if count != len(members) { - return errors.New("invalid members") - } - req := s.Bot.storage.Request - info := s.Bot.storage.LoginInfo - return s.Bot.Caller.RemoveFriendFromChatRoom(req, info, group, members...) + if len(members) == 0 { + return nil + } + if group.IsOwner == 0 { + return errors.New("group owner required") + } + groupMembers, err := group.Members() + if err != nil { + return err + } + // 判断用户是否在群聊中 + var count int + for _, member := range members { + for _, gm := range groupMembers { + if gm.UserName == member.UserName { + count++ + } + } + } + if count != len(members) { + return errors.New("invalid members") + } + req := s.Bot.storage.Request + info := s.Bot.storage.LoginInfo + return s.Bot.Caller.RemoveFriendFromChatRoom(req, info, group, members...) } // 拉好友进多个群聊 // AddFriendIntoGroups, 名字和上面的有点像 func (s *Self) AddFriendIntoManyGroups(friend *Friend, groups ...*Group) error { - for _, group := range groups { - if err := s.AddFriendsIntoGroup(group, friend); err != nil { - return err - } - } - return nil + for _, group := range groups { + if err := s.AddFriendsIntoGroup(group, friend); err != nil { + return err + } + } + return nil } // 发送消息给群组 func (s *Self) SendMessageToGroup(group *Group, msg *SendMessage) (*SentMessage, error) { - return s.sendMessageToUser(group.User, msg) + return s.sendMessageToUser(group.User, msg) } // 发送文本消息给群组 func (s *Self) SendTextToGroup(group *Group, text string) (*SentMessage, error) { - msg := NewTextSendMessage(text, s.UserName, group.UserName) - return s.SendMessageToGroup(group, msg) + msg := NewTextSendMessage(text, s.UserName, group.UserName) + return s.SendMessageToGroup(group, msg) } // 发送图片消息给群组 func (s *Self) SendImageToGroup(group *Group, file *os.File) (*SentMessage, error) { - req := s.Bot.storage.Request - info := s.Bot.storage.LoginInfo - return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, group.UserName) + req := s.Bot.storage.Request + info := s.Bot.storage.LoginInfo + return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, group.UserName) } // 撤回消息 @@ -339,7 +345,7 @@ func (s *Self) SendImageToGroup(group *Group, file *os.File) (*SentMessage, erro // self.RevokeMessage(sentMessage) // or sentMessage.Revoke() // } func (s *Self) RevokeMessage(msg *SentMessage) error { - return s.Bot.Caller.WebWxRevokeMsg(msg, s.Bot.storage.Request) + return s.Bot.Caller.WebWxRevokeMsg(msg, s.Bot.storage.Request) } // 抽象的用户组 @@ -347,158 +353,158 @@ type Members []*User // 统计数量 func (m Members) Count() int { - return len(m) + return len(m) } // 获取第一个 func (m Members) First() *User { - if m.Count() > 0 { - u := m[0] - return u - } - return nil + if m.Count() > 0 { + u := m[0] + return u + } + return nil } // 获取最后一个 func (m Members) Last() *User { - if m.Count() > 0 { - u := m[m.Count()-1] - return u - } - return nil + if m.Count() > 0 { + u := m[m.Count()-1] + return u + } + return nil } // 设置owner // 请不要随意设置 func (m Members) SetOwner(s *Self) { - for _, member := range m { - member.Self = s - } + for _, member := range m { + member.Self = s + } } // 根据用户名查找 func (m Members) SearchByUserName(limit int, username string) (results Members) { - return m.Search(limit, func(user *User) bool { return user.UserName == username }) + return m.Search(limit, func(user *User) bool { return user.UserName == username }) } // 根据昵称查找 func (m Members) SearchByNickName(limit int, nickName string) (results Members) { - return m.Search(limit, func(user *User) bool { return user.NickName == nickName }) + return m.Search(limit, func(user *User) bool { return user.NickName == nickName }) } // 根据备注查找 func (m Members) SearchByRemarkName(limit int, remarkName string) (results Members) { - return m.Search(limit, func(user *User) bool { return user.RemarkName == remarkName }) + return m.Search(limit, func(user *User) bool { return user.RemarkName == remarkName }) } // 根据自定义条件查找 func (m Members) Search(limit int, condFuncList ...func(user *User) bool) (results Members) { - if condFuncList == nil { - return m - } - if limit <= 0 { - limit = m.Count() - } - for _, member := range m { - if count := len(results); count == limit { - break - } - var passCount int - for _, condFunc := range condFuncList { - if condFunc(member) { - passCount++ - } else { - break - } - } - if passCount == len(condFuncList) { - results = append(results, member) - } - } - return + if condFuncList == nil { + return m + } + if limit <= 0 { + limit = m.Count() + } + for _, member := range m { + if count := len(results); count == limit { + break + } + var passCount int + for _, condFunc := range condFuncList { + if condFunc(member) { + passCount++ + } else { + break + } + } + if passCount == len(condFuncList) { + results = append(results, member) + } + } + return } func (m Members) Friends() Friends { - friends := make(Friends, 0) - for _, mb := range m { - if mb.IsFriend() { - friend := &Friend{mb} - friends = append(friends, friend) - } - } - return friends + friends := make(Friends, 0) + for _, mb := range m { + if mb.IsFriend() { + friend := &Friend{mb} + friends = append(friends, friend) + } + } + return friends } func (m Members) Groups() Groups { - groups := make(Groups, 0) - for _, mb := range m { - if mb.IsGroup() { - group := &Group{mb} - groups = append(groups, group) - } - } - return groups + groups := make(Groups, 0) + for _, mb := range m { + if mb.IsGroup() { + group := &Group{mb} + groups = append(groups, group) + } + } + return groups } func (m Members) MPs() Mps { - mps := make(Mps, 0) - for _, mb := range m { - if mb.IsMP() { - mp := &Mp{mb} - mps = append(mps, mp) - } - } - return mps + mps := make(Mps, 0) + for _, mb := range m { + if mb.IsMP() { + mp := &Mp{mb} + mps = append(mps, mp) + } + } + return mps } // 获取当前Members的详情 func (m Members) detail(self *Self) error { - // 获取他们的数量 - members := m + // 获取他们的数量 + members := m - count := members.Count() - // 一次更新50个,分情况讨论 + count := members.Count() + // 一次更新50个,分情况讨论 - // 获取总的需要更新的次数 - var times int - if count < 50 { - times = 1 - } else { - times = count / 50 - } - var newMembers Members - request := self.Bot.storage.Request - var pMembers Members - // 分情况依次更新 - for i := 1; i <= times; i++ { - if times == 1 { - pMembers = members - } else { - pMembers = members[(i-1)*50 : i*50] - } - nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request) - if err != nil { - return err - } - newMembers = append(newMembers, nMembers...) - } - // 最后判断是否全部更新完毕 - total := times * 50 - if total < count { - // 将全部剩余的更新完毕 - left := count - total - pMembers = members[total : total+left] - nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request) - if err != nil { - return err - } - newMembers = append(newMembers, nMembers...) - } - if len(newMembers) > 0 { - newMembers.SetOwner(self) - self.members = newMembers - } - return nil + // 获取总的需要更新的次数 + var times int + if count < 50 { + times = 1 + } else { + times = count / 50 + } + var newMembers Members + request := self.Bot.storage.Request + var pMembers Members + // 分情况依次更新 + for i := 1; i <= times; i++ { + if times == 1 { + pMembers = members + } else { + pMembers = members[(i-1)*50 : i*50] + } + nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request) + if err != nil { + return err + } + newMembers = append(newMembers, nMembers...) + } + // 最后判断是否全部更新完毕 + total := times * 50 + if total < count { + // 将全部剩余的更新完毕 + left := count - total + pMembers = members[total : total+left] + nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request) + if err != nil { + return err + } + newMembers = append(newMembers, nMembers...) + } + if len(newMembers) > 0 { + newMembers.SetOwner(self) + self.members = newMembers + } + return nil } // 这里为了兼容Desktop版本找不到文件传输助手的问题 @@ -506,5 +512,5 @@ func (m Members) detail(self *Self) error { // 这种形式的对象可能缺少一些其他属性 // 但是不影响发送信息的功能 func NewFriendHelper(self *Self) *Friend { - return &Friend{&User{UserName: "filehelper", Self: self}} + return &Friend{&User{UserName: "filehelper", Self: self}} }