diff --git a/caller.go b/caller.go index 704f617..170f6e3 100644 --- a/caller.go +++ b/caller.go @@ -3,6 +3,8 @@ package openwechat import ( "bytes" "context" + "crypto/md5" + "encoding/hex" "encoding/json" "encoding/xml" "errors" @@ -325,12 +327,63 @@ type CallerUploadMediaOptions struct { func (c *Caller) UploadMedia(ctx context.Context, file *os.File, opt *CallerUploadMediaOptions) (*UploadResponse, error) { // 首先尝试上传图片 + h := md5.New() + if _, err := io.Copy(h, file); err != nil { + return nil, err + } + fileMd5 := hex.EncodeToString(h.Sum(nil)) + stat, err := file.Stat() + if err != nil { + return nil, err + } + filename := file.Name() + filesize := stat.Size() + clientWebWxUploadMediaByChunkOpt := &ClientWebWxUploadMediaByChunkOptions{ - FromUserName: opt.FromUserName, - ToUserName: opt.ToUserName, - BaseRequest: opt.BaseRequest, - LoginInfo: opt.LoginInfo, + FromUserName: opt.FromUserName, + ToUserName: opt.ToUserName, + BaseRequest: opt.BaseRequest, + LoginInfo: opt.LoginInfo, + Filename: filename, + FileMD5: fileMd5, + FileSize: filesize, + LastModifiedDate: stat.ModTime(), + } + + if filesize > needCheckSize { + checkUploadRequest := webWxCheckUploadRequest{ + BaseRequest: opt.BaseRequest, + FileMd5: fileMd5, + FileName: filename, + FileSize: filesize, + FileType: 7, + FromUserName: opt.FromUserName, + ToUserName: opt.ToUserName, + } + resp, err := c.Client.webWxCheckUploadRequest(ctx, checkUploadRequest) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + var checkUploadResponse webWxCheckUploadResponse + if err = json.NewDecoder(resp.Body).Decode(&checkUploadResponse); err != nil { + return nil, err + } + if err = checkUploadResponse.BaseResponse.Err(); err != nil { + return nil, err + } + // 如果已经上传过了,直接返回 + if checkUploadResponse.MediaId != "" { + var item UploadResponse + item.MediaId = checkUploadResponse.MediaId + item.Signature = checkUploadResponse.Signature + item.BaseResponse = checkUploadResponse.BaseResponse + return &item, nil + } + clientWebWxUploadMediaByChunkOpt.AESKey = checkUploadResponse.AESKey + clientWebWxUploadMediaByChunkOpt.Signature = checkUploadResponse.Signature } + resp, err := c.Client.WebWxUploadMediaByChunk(ctx, file, clientWebWxUploadMediaByChunkOpt) // 无错误上传成功之后获取请求结果,判断结果是否正常 if err != nil { @@ -347,6 +400,7 @@ func (c *Caller) UploadMedia(ctx context.Context, file *os.File, opt *CallerUplo if len(item.MediaId) == 0 { return &item, errors.New("upload failed") } + item.Signature = clientWebWxUploadMediaByChunkOpt.Signature return &item, nil } @@ -418,13 +472,17 @@ func (c *Caller) WebWxSendFile(ctx context.Context, reader io.Reader, opt *Calle return nil, err } // 构造新的文件类型的信息 - stat, _ := file.Stat() + stat, err := file.Stat() + if err != nil { + return nil, err + } appMsg := newFileAppMessage(stat, resp.MediaId) content, err := appMsg.XmlByte() if err != nil { return nil, err } msg := NewSendMessage(AppMessage, string(content), opt.FromUserName, opt.ToUserName, "") + msg.Signature = resp.Signature return c.WebWxSendAppMsg(ctx, msg, opt.BaseRequest) } diff --git a/client.go b/client.go index 5c2fd33..755bf00 100644 --- a/client.go +++ b/client.go @@ -3,8 +3,6 @@ package openwechat import ( "bytes" "context" - "crypto/md5" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -479,37 +477,87 @@ func (c *Client) WebWxGetHeadImg(ctx context.Context, user *User) (*http.Respons return c.Do(req) } -type ClientWebWxUploadMediaByChunkOptions struct { - FromUserName string - ToUserName string - BaseRequest *BaseRequest - LoginInfo *LoginInfo +type webWxCheckUploadRequest struct { + BaseRequest *BaseRequest `json:"BaseRequest"` + FileMd5 string `json:"FileMd5"` + FileName string `json:"FileName"` + FileSize int64 `json:"FileSize"` + FileType uint8 `json:"FileType"` + FromUserName string `json:"FromUserName"` + ToUserName string `json:"ToUserName"` } -// WebWxUploadMediaByChunk 分块上传文件 -// TODO 优化掉这个函数 -func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt *ClientWebWxUploadMediaByChunkOptions) (*http.Response, error) { - // 获取文件上传的类型 - contentType, err := GetFileContentType(file) +type webWxCheckUploadResponse struct { + BaseResponse BaseResponse `json:"BaseResponse"` + MediaId string `json:"MediaId"` + AESKey string `json:"AESKey"` + Signature string `json:"Signature"` + EntryFileName string `json:"EncryFileName"` +} + +func (c *Client) webWxCheckUploadRequest(ctx context.Context, req webWxCheckUploadRequest) (*http.Response, error) { + path, err := url.Parse(c.Domain.BaseHost() + webwxcheckupload) if err != nil { return nil, err } - if _, err = file.Seek(0, io.SeekStart); err != nil { + body, err := jsonEncode(req) + if err != nil { return nil, err } - - // 获取文件的md5 - h := md5.New() - if _, err = io.Copy(h, file); err != nil { + reqs, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body) + if err != nil { return nil, err } - fileMd5 := hex.EncodeToString(h.Sum(nil)) + reqs.Header.Add("Content-Type", jsonContentType) + return c.Do(reqs) +} + +type uploadMediaRequest struct { + UploadType uint8 `json:"UploadType"` + BaseRequest *BaseRequest `json:"BaseRequest"` + ClientMediaId int64 `json:"ClientMediaId"` + TotalLen int64 `json:"TotalLen"` + StartPos int `json:"StartPos"` + DataLen int64 `json:"DataLen"` + MediaType uint8 `json:"MediaType"` + FromUserName string `json:"FromUserName"` + ToUserName string `json:"ToUserName"` + FileMd5 string `json:"FileMd5"` + AESKey string `json:"AESKey,omitempty"` + Signature string `json:"Signature,omitempty"` +} + +type ClientWebWxUploadMediaByChunkOptions struct { + FromUserName string + ToUserName string + BaseRequest *BaseRequest + LoginInfo *LoginInfo + Filename string + FileMD5 string + FileSize int64 + LastModifiedDate time.Time + AESKey string + Signature string +} - sate, err := file.Stat() +type UploadFile interface { + io.ReaderAt + io.ReadSeeker +} + +// WebWxUploadMediaByChunk 分块上传文件 +// TODO 优化掉这个函数 +func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file UploadFile, opt *ClientWebWxUploadMediaByChunkOptions) (*http.Response, error) { + // 获取文件上传的类型 + if _, err := file.Seek(0, io.SeekStart); err != nil { + return nil, err + } + contentType, err := GetFileContentType(file) if err != nil { return nil, err } - filename := sate.Name() + + filename := opt.Filename if ext := filepath.Ext(filename); ext == "" { names := strings.Split(contentType, "/") @@ -530,22 +578,24 @@ func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt cookies := c.Jar().Cookies(path) - webWxDataTicket, err := getWebWxDataTicket(cookies) + webWxDataTicket, err := wxDataTicket(cookies) if err != nil { return nil, err } - uploadMediaRequest := map[string]interface{}{ - "UploadType": 2, - "BaseRequest": opt.BaseRequest, - "ClientMediaId": time.Now().Unix() * 1e4, - "TotalLen": sate.Size(), - "StartPos": 0, - "DataLen": sate.Size(), - "MediaType": 4, - "FromUserName": opt.FromUserName, - "ToUserName": opt.ToUserName, - "FileMd5": fileMd5, + uploadMediaRequest := &uploadMediaRequest{ + UploadType: 2, + BaseRequest: opt.BaseRequest, + ClientMediaId: time.Now().Unix() * 1e4, + TotalLen: opt.FileSize, + StartPos: 0, + DataLen: opt.FileSize, + MediaType: 4, + FromUserName: opt.FromUserName, + ToUserName: opt.ToUserName, + FileMd5: opt.FileMD5, + AESKey: opt.AESKey, + Signature: opt.Signature, } uploadMediaRequestByte, err := json.Marshal(uploadMediaRequest) @@ -554,14 +604,14 @@ func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt } // 计算上传文件的次数 - chunks := int((sate.Size() + chunkSize - 1) / chunkSize) + chunks := int((opt.FileSize + chunkSize - 1) / chunkSize) content := map[string]string{ "id": "WU_FILE_0", "name": filename, "type": contentType, - "lastModifiedDate": sate.ModTime().Format(TimeFormat), - "size": strconv.FormatInt(sate.Size(), 10), + "lastModifiedDate": opt.LastModifiedDate.Format(TimeFormat), + "size": strconv.FormatInt(opt.FileSize, 10), "mediatype": mediaType, "webwx_data_ticket": webWxDataTicket, "pass_ticket": opt.LoginInfo.PassTicket, @@ -596,7 +646,7 @@ func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt } // create form file - fileWriter, err := writer.CreateFormFile("filename", file.Name()) + fileWriter, err := writer.CreateFormFile("filename", filename) if err != nil { return err } @@ -671,6 +721,7 @@ func (c *Client) WebWxSendAppMsg(ctx context.Context, msg *SendMessage, request params := url.Values{} params.Add("fun", "async") params.Add("f", "json") + params.Add("lang", "zh_CN") path.RawQuery = params.Encode() return c.sendMessage(ctx, request, path.String(), msg) } @@ -815,7 +866,7 @@ func (c *Client) WebWxGetMedia(ctx context.Context, msg *Message, info *LoginInf return nil, err } cookies := c.Jar().Cookies(path) - webWxDataTicket, err := getWebWxDataTicket(cookies) + webWxDataTicket, err := wxDataTicket(cookies) if err != nil { return nil, err } diff --git a/cookiejar.go b/cookiejar.go index 3726697..60a1f86 100644 --- a/cookiejar.go +++ b/cookiejar.go @@ -77,7 +77,7 @@ func (c CookieGroup) GetByName(cookieName string) (cookie *http.Cookie, exist bo return nil, false } -func getWebWxDataTicket(cookies []*http.Cookie) (string, error) { +func wxDataTicket(cookies []*http.Cookie) (string, error) { cookieGroup := CookieGroup(cookies) cookie, exist := cookieGroup.GetByName("webwx_data_ticket") if !exist { diff --git a/entity.go b/entity.go index d573029..9cb68cb 100644 --- a/entity.go +++ b/entity.go @@ -173,8 +173,9 @@ type MessageResponse struct { } type UploadResponse struct { - BaseResponse BaseResponse - MediaId string + BaseResponse BaseResponse `json:"BaseResponse"` + MediaId string `json:"MediaId"` + Signature string `json:"Signature"` } type PushLoginResponse struct { diff --git a/message.go b/message.go index c4013d6..c88c8f4 100644 --- a/message.go +++ b/message.go @@ -506,6 +506,7 @@ type SendMessage struct { MediaId string `json:"MediaId,omitempty"` EmojiFlag int `json:"EmojiFlag,omitempty"` EMoticonMd5 string `json:"EMoticonMd5,omitempty"` + Signature string `json:"Signature,omitempty"` } // NewSendMessage SendMessage的构造方法