添加文件发送接口

This commit is contained in:
eatMoreApple 2021-04-30 00:04:45 +08:00
parent 3c7fec07f3
commit e751e13afd
9 changed files with 1046 additions and 985 deletions

View File

@ -291,3 +291,29 @@ func TestRevokeMessage(t *testing.T) {
t.Error(err) 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)
}

View File

@ -200,47 +200,64 @@ func (c *Caller) WebWxOplog(request *BaseRequest, remarkName, toUserName string)
return parseBaseResponseError(resp) return parseBaseResponseError(resp)
} }
// 发送图片消息接口 func (c *Caller) UploadMedia(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName, mediaType string) (*UploadResponse, error) {
func (c *Caller) WebWxSendImageMsg(file *os.File, request *BaseRequest, info *LoginInfo, fromUserName, toUserName string) (*SentMessage, error) {
// 首先尝试上传图片 // 首先尝试上传图片
sate, err := file.Stat() resp := NewReturnResponse(c.Client.WebWxUploadMediaByChunk(file, request, info, fromUserName, toUserName, mediaType))
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"))
}
// 无错误上传成功之后获取请求结果,判断结果是否正常 // 无错误上传成功之后获取请求结果,判断结果是否正常
if resp.Err() != nil { if resp.Err() != nil {
return nil, resp.Err() return nil, resp.Err()
} }
defer resp.Body.Close() defer resp.Body.Close()
var item struct { var item UploadResponse
BaseResponse BaseResponse
MediaId string
}
if err = resp.ScanJSON(&item); err != nil { if err := resp.ScanJSON(&item); err != nil {
return nil, err return &item, err
} }
if !item.BaseResponse.Ok() { if !item.BaseResponse.Ok() {
return nil, item.BaseResponse return &item, item.BaseResponse
} }
if len(item.MediaId) == 0 { 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} sendSuccessMsg := &SentMessage{SendMessage: msg}
err = parseMessageResponseError(resp, sendSuccessMsg) err := parseMessageResponseError(resp, sendSuccessMsg)
return sendSuccessMsg, err return sendSuccessMsg, err
} }
@ -312,7 +329,7 @@ func parseMessageResponseError(resp *ReturnResponse, msg *SentMessage) error {
if !messageResp.BaseResponse.Ok() { if !messageResp.BaseResponse.Ok() {
return messageResp.BaseResponse return messageResp.BaseResponse
} }
//// 发送成功之后将msgId赋值给SendMessage // 发送成功之后将msgId赋值给SendMessage
msg.MsgId = messageResp.MsgID msg.MsgId = messageResp.MsgID
return nil return nil
} }

168
client.go
View File

@ -251,104 +251,12 @@ func (c *Client) WebWxGetHeadImg(headImageUrl string) (*http.Response, error) {
return c.Do(req) 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) { func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, info *LoginInfo, forUserName, toUserName, mediaType string) (*http.Response, error) {
// 获取文件上传的类型 // 获取文件上传的类型
contentType, err := GetFileContentType(file) contentType, err := GetFileContentType(file)
if err != nil { if err != nil {
return nil, err 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个字节 // 上面获取文件的类型的时候已经读取了512个字节
@ -363,6 +271,20 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
data := buffer.Bytes() data := buffer.Bytes()
fileMd5 := fmt.Sprintf("%x", md5.Sum(data)) 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{}{ uploadMediaRequest := map[string]interface{}{
"UploadType": 2, "UploadType": 2,
"BaseRequest": request, "BaseRequest": request,
@ -375,18 +297,40 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
"ToUserName": toUserName, "ToUserName": toUserName,
"FileMd5": fileMd5, "FileMd5": fileMd5,
} }
uploadMediaRequestByte, err := json.Marshal(uploadMediaRequest) uploadMediaRequestByte, err := json.Marshal(uploadMediaRequest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
chunks := sate.Size() / chunkSize var chunks int64
if chunks*chunkSize < sate.Size() {
chunks++ if sate.Size() > chunkSize {
chunks = sate.Size() / chunkSize
if chunks*chunkSize < sate.Size() {
chunks++
}
} else {
chunks = 1
} }
var resp *http.Response 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++ { for chunk := 0; int64(chunk) < chunks; chunk++ {
var isLastTime bool var isLastTime bool
@ -394,18 +338,10 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
isLastTime = true isLastTime = true
} }
content := map[string]string{ if chunks > 1 {
"id": "WU_FILE_0", content["chunk"] = strconv.Itoa(chunk)
"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),
} }
var formBuffer bytes.Buffer var formBuffer bytes.Buffer
writer := multipart.NewWriter(&formBuffer) writer := multipart.NewWriter(&formBuffer)
@ -450,7 +386,7 @@ func (c *Client) WebWxUploadMediaByChunk(file *os.File, request *BaseRequest, in
} }
} }
} }
// 将最后一次携带文件信息的response返回
return resp, err return resp, err
} }
@ -645,3 +581,21 @@ func (c *Client) WebWxRevokeMsg(msg *SentMessage, request *BaseRequest) (*http.R
req.Header.Set("Content-Type", jsonContentType) req.Header.Set("Content-Type", jsonContentType)
return c.Do(req) 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)
}

147
global.go
View File

@ -1,94 +1,96 @@
package openwechat package openwechat
import ( import (
"errors" "errors"
"regexp" "regexp"
) )
var ( var (
uuidRegexp = regexp.MustCompile(`uuid = "(.*?)";`) uuidRegexp = regexp.MustCompile(`uuid = "(.*?)";`)
statusCodeRegexp = regexp.MustCompile(`window.code=(\d+);`) statusCodeRegexp = regexp.MustCompile(`window.code=(\d+);`)
syncCheckRegexp = regexp.MustCompile(`window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}`) syncCheckRegexp = regexp.MustCompile(`window.synccheck=\{retcode:"(\d+)",selector:"(\d+)"\}`)
redirectUriRegexp = regexp.MustCompile(`window.redirect_uri="(.*?)"`) redirectUriRegexp = regexp.MustCompile(`window.redirect_uri="(.*?)"`)
) )
const ( const (
appId = "wx782c26e4c19acffb" appId = "wx782c26e4c19acffb"
jsLoginUrl = "https://login.wx.qq.com/jslogin" jsLoginUrl = "https://login.wx.qq.com/jslogin"
qrcodeUrl = "https://login.weixin.qq.com/qrcode/" qrcodeUrl = "https://login.weixin.qq.com/qrcode/"
loginUrl = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login" loginUrl = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login"
// Normal urls // Normal urls
baseNormalUrl = "https://wx2.qq.com" baseNormalUrl = "https://wx2.qq.com"
webWxNewLoginPageNormalUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage" webWxNewLoginPageNormalUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage"
webWxInitNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit" webWxInitNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit"
webWxStatusNotifyNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify" webWxStatusNotifyNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify"
webWxSyncNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsync" webWxSyncNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsync"
webWxSendMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg" webWxSendMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg"
webWxGetContactNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact" webWxGetContactNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact"
webWxSendMsgImgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg" webWxSendMsgImgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg"
webWxSendAppMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg" webWxSendAppMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg"
webWxBatchGetContactNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact" webWxBatchGetContactNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact"
webWxOplogNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxoplog" webWxOplogNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxoplog"
webWxVerifyUserNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser" webWxVerifyUserNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser"
syncCheckNormalUrl = "https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck" syncCheckNormalUrl = "https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck"
webWxUpLoadMediaNormalUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia" webWxUpLoadMediaNormalUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia"
webWxGetMsgImgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg" webWxGetMsgImgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg"
webWxGetVoiceNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice" webWxGetVoiceNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice"
webWxGetVideoNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo" webWxGetVideoNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo"
webWxLogoutNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxlogout" webWxLogoutNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxlogout"
webWxGetMediaNormalUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia" webWxGetMediaNormalUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia"
webWxUpdateChatRoomNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom" webWxUpdateChatRoomNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom"
webWxRevokeMsgNormalUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg" 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" baseDesktopUrl = "https://wx.qq.com"
webWxNewLoginPageDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop" webWxNewLoginPageDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop"
webWxInitDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit" webWxInitDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit"
webWxStatusNotifyDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify" webWxStatusNotifyDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify"
webWxSyncDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync" webWxSyncDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync"
webWxSendMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg" webWxSendMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg"
webWxGetContactDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact" webWxGetContactDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact"
webWxSendMsgImgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg" webWxSendMsgImgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg"
webWxSendAppMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg" webWxSendAppMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg"
webWxBatchGetContactDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact" webWxBatchGetContactDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact"
webWxOplogDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxoplog" webWxOplogDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxoplog"
webWxVerifyUserDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser" webWxVerifyUserDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser"
syncCheckDesktopUrl = "https://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck" syncCheckDesktopUrl = "https://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck"
webWxUpLoadMediaDesktopUrl = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia" webWxUpLoadMediaDesktopUrl = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia"
webWxGetMsgImgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg" webWxGetMsgImgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg"
webWxGetVoiceDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice" webWxGetVoiceDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvoice"
webWxGetVideoDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo" webWxGetVideoDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetvideo"
webWxLogoutDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxlogout" webWxLogoutDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxlogout"
webWxGetMediaDesktopUrl = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia" webWxGetMediaDesktopUrl = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia"
webWxUpdateChatRoomDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom" webWxUpdateChatRoomDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxupdatechatroom"
webWxRevokeMsgDesktopUrl = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxrevokemsg" 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" jsonContentType = "application/json; charset=utf-8"
uosPatchClientVersion = "2.0.0" 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=" 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 ( const (
TextMessage = 1 TextMessage = 1
ImageMessage = 3 ImageMessage = 3
AppMessage = 6 AppMessage = 6
) )
// 登录状态 // 登录状态
const ( const (
statusSuccess = "200" statusSuccess = "200"
statusScanned = "201" statusScanned = "201"
statusTimeout = "400" statusTimeout = "400"
statusWait = "408" statusWait = "408"
) )
// errors // errors
var ( var (
noSuchUserFoundError = errors.New("no such user found") noSuchUserFoundError = errors.New("no such user found")
) )
// ALL跟search函数搭配 // ALL跟search函数搭配
@ -97,10 +99,19 @@ const ALL = 0
// 性别 // 性别
const ( const (
MALE = 1 MALE = 1
FEMALE = 2 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 (中国标准时间)" const TimeFormat = "Mon Jan 02 2006 15:04:05 GMT+0800 (中国标准时间)"

247
items.go
View File

@ -1,8 +1,8 @@
package openwechat package openwechat
import ( import (
"errors" "errors"
"fmt" "fmt"
) )
/* /*
@ -11,194 +11,199 @@ import (
// 登录信息 // 登录信息
type LoginInfo struct { type LoginInfo struct {
Ret int `xml:"ret"` Ret int `xml:"ret"`
WxUin int `xml:"wxuin"` WxUin int `xml:"wxuin"`
IsGrayScale int `xml:"isgrayscale"` IsGrayScale int `xml:"isgrayscale"`
Message string `xml:"message"` Message string `xml:"message"`
SKey string `xml:"skey"` SKey string `xml:"skey"`
WxSid string `xml:"wxsid"` WxSid string `xml:"wxsid"`
PassTicket string `xml:"pass_ticket"` PassTicket string `xml:"pass_ticket"`
} }
func (l LoginInfo) Ok() bool { func (l LoginInfo) Ok() bool {
return l.Ret == 0 return l.Ret == 0
} }
func (l LoginInfo) Error() string { func (l LoginInfo) Error() string {
return l.Message return l.Message
} }
// 初始的请求信息 // 初始的请求信息
// 几乎所有的请求都要携带该参数 // 几乎所有的请求都要携带该参数
type BaseRequest struct { type BaseRequest struct {
Uin int Uin int
Sid, Skey, DeviceID string Sid, Skey, DeviceID string
} }
// 大部分返回对象都携带该信息 // 大部分返回对象都携带该信息
type BaseResponse struct { type BaseResponse struct {
Ret int Ret int
ErrMsg string ErrMsg string
} }
func (b BaseResponse) Ok() bool { func (b BaseResponse) Ok() bool {
return b.Ret == 0 return b.Ret == 0
} }
func (b BaseResponse) Error() string { func (b BaseResponse) Error() string {
if err := getResponseErrorWithRetCode(b.Ret); err != nil { if err := getResponseErrorWithRetCode(b.Ret); err != nil {
return err.Error() return err.Error()
} }
return "" return ""
} }
func getResponseErrorWithRetCode(code int) error { func getResponseErrorWithRetCode(code int) error {
switch code { switch code {
case 0: case 0:
return nil return nil
case 1: case 1:
return errors.New("param error") return errors.New("param error")
case -14: case -14:
return errors.New("ticket error") return errors.New("ticket error")
case 1100: case 1100:
return errors.New("not login warn") return errors.New("not login warn")
case 1101: case 1101:
return errors.New("not login check") return errors.New("not login check")
case 1102: case 1102:
return errors.New("cookie invalid error") return errors.New("cookie invalid error")
case 1203: case 1203:
return errors.New("login env error") return errors.New("login env error")
case 1205: case 1205:
return errors.New("opt too often") return errors.New("opt too often")
default: default:
return fmt.Errorf("base response ret code %d", code) return fmt.Errorf("base response ret code %d", code)
} }
} }
type SyncKey struct { type SyncKey struct {
Count int Count int
List []struct{ Key, Val int64 } List []struct{ Key, Val int64 }
} }
// 初始化的相应信息 // 初始化的相应信息
type WebInitResponse struct { type WebInitResponse struct {
Count int Count int
ClientVersion int ClientVersion int
GrayScale int GrayScale int
InviteStartCount int InviteStartCount int
MPSubscribeMsgCount int MPSubscribeMsgCount int
ClickReportInterval int ClickReportInterval int
SystemTime int64 SystemTime int64
ChatSet string ChatSet string
SKey string SKey string
BaseResponse BaseResponse BaseResponse BaseResponse
SyncKey SyncKey SyncKey SyncKey
User User User User
MPSubscribeMsgList []MPSubscribeMsg MPSubscribeMsgList []MPSubscribeMsg
ContactList []User ContactList []User
} }
// 公众号的订阅信息 // 公众号的订阅信息
type MPSubscribeMsg struct { type MPSubscribeMsg struct {
MPArticleCount int MPArticleCount int
Time int64 Time int64
UserName string UserName string
NickName string NickName string
MPArticleList []struct { MPArticleList []struct {
Title string Title string
Cover string Cover string
Digest string Digest string
Url string Url string
} }
} }
type UserDetailItem struct { type UserDetailItem struct {
UserName string UserName string
EncryChatRoomId string EncryChatRoomId string
} }
type UserDetailItemList []UserDetailItem type UserDetailItemList []UserDetailItem
func NewUserDetailItemList(members Members) UserDetailItemList { func NewUserDetailItemList(members Members) UserDetailItemList {
var list UserDetailItemList var list UserDetailItemList
for _, member := range members { for _, member := range members {
item := UserDetailItem{UserName: member.UserName, EncryChatRoomId: member.EncryChatRoomId} item := UserDetailItem{UserName: member.UserName, EncryChatRoomId: member.EncryChatRoomId}
list = append(list, item) list = append(list, item)
} }
return list return list
} }
type SyncCheckResponse struct { type SyncCheckResponse struct {
RetCode string RetCode string
Selector string Selector string
} }
func (s *SyncCheckResponse) Success() bool { func (s *SyncCheckResponse) Success() bool {
return s.RetCode == "0" return s.RetCode == "0"
} }
func (s *SyncCheckResponse) NorMal() bool { func (s *SyncCheckResponse) NorMal() bool {
return s.Success() && s.Selector == "0" return s.Success() && s.Selector == "0"
} }
// 实现error接口 // 实现error接口
func (s *SyncCheckResponse) Error() string { func (s *SyncCheckResponse) Error() string {
switch s.RetCode { switch s.RetCode {
case "0": case "0":
return "" return ""
case "1": case "1":
return "param error" return "param error"
case "-14": case "-14":
return "ticker error" return "ticker error"
case "1100": case "1100":
return "not login warn" return "not login warn"
case "1101": case "1101":
return "not login check" return "not login check"
case "1102": case "1102":
return "cookie invalid error" return "cookie invalid error"
case "1203": case "1203":
return "login env error" return "login env error"
case "1205": case "1205":
return "opt too often" return "opt too often"
default: default:
return fmt.Sprintf("sync check response error code %s", s.RetCode) return fmt.Sprintf("sync check response error code %s", s.RetCode)
} }
} }
type WebWxSyncResponse struct { type WebWxSyncResponse struct {
AddMsgCount int AddMsgCount int
ContinueFlag int ContinueFlag int
DelContactCount int DelContactCount int
ModChatRoomMemberCount int ModChatRoomMemberCount int
ModContactCount int ModContactCount int
Skey string Skey string
SyncCheckKey SyncKey SyncCheckKey SyncKey
SyncKey SyncKey SyncKey SyncKey
BaseResponse BaseResponse BaseResponse BaseResponse
ModChatRoomMemberList Members ModChatRoomMemberList Members
AddMsgList []*Message AddMsgList []*Message
} }
type WebWxContactResponse struct { type WebWxContactResponse struct {
MemberCount int MemberCount int
Seq int Seq int
BaseResponse BaseResponse BaseResponse BaseResponse
MemberList []*User MemberList []*User
} }
type WebWxBatchContactResponse struct { type WebWxBatchContactResponse struct {
Count int Count int
BaseResponse BaseResponse BaseResponse BaseResponse
ContactList []*User ContactList []*User
} }
type CheckLoginResponse struct { type CheckLoginResponse struct {
Code string Code string
Raw []byte Raw []byte
} }
type MessageResponse struct { type MessageResponse struct {
BaseResponse BaseResponse BaseResponse BaseResponse
LocalID string LocalID string
MsgID string MsgID string
}
type UploadResponse struct {
BaseResponse BaseResponse
MediaId string
} }

View File

@ -1,473 +1,504 @@
package openwechat package openwechat
import ( import (
"context" "context"
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode" "unicode"
) )
type Message struct { type Message struct {
IsAt bool IsAt bool
AppInfo struct { AppInfo struct {
Type int Type int
AppID string AppID string
} }
AppMsgType int AppMsgType int
HasProductId int HasProductId int
ImgHeight int ImgHeight int
ImgStatus int ImgStatus int
ImgWidth int ImgWidth int
ForwardFlag int ForwardFlag int
MsgType int MsgType int
Status int Status int
StatusNotifyCode int StatusNotifyCode int
SubMsgType int SubMsgType int
VoiceLength int VoiceLength int
CreateTime int64 CreateTime int64
NewMsgId int64 NewMsgId int64
PlayLength int64 PlayLength int64
MediaId string MediaId string
MsgId string MsgId string
EncryFileName string EncryFileName string
FileName string FileName string
FileSize string FileSize string
Content string Content string
FromUserName string FromUserName string
OriContent string OriContent string
StatusNotifyUserName string StatusNotifyUserName string
Ticket string Ticket string
ToUserName string ToUserName string
Url string Url string
senderInGroupUserName string senderInGroupUserName string
RecommendInfo RecommendInfo RecommendInfo RecommendInfo
Bot *Bot Bot *Bot
mu sync.RWMutex mu sync.RWMutex
Context context.Context Context context.Context
item map[string]interface{} item map[string]interface{}
} }
// 获取消息的发送者 // 获取消息的发送者
func (m *Message) Sender() (*User, error) { func (m *Message) Sender() (*User, error) {
members, err := m.Bot.self.Members(true) members, err := m.Bot.self.Members(true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if m.FromUserName == m.Bot.self.User.UserName { if m.FromUserName == m.Bot.self.User.UserName {
return m.Bot.self.User, nil return m.Bot.self.User, nil
} }
user := members.SearchByUserName(1, m.FromUserName) user := members.SearchByUserName(1, m.FromUserName)
if user == nil { if user == nil {
return nil, noSuchUserFoundError return nil, noSuchUserFoundError
} }
return user.First().Detail() return user.First().Detail()
} }
// 获取消息在群里面的发送者 // 获取消息在群里面的发送者
func (m *Message) SenderInGroup() (*User, error) { func (m *Message) SenderInGroup() (*User, error) {
if !m.IsSendByGroup() { if !m.IsSendByGroup() {
return nil, errors.New("message is not from group") return nil, errors.New("message is not from group")
} }
group, err := m.Sender() group, err := m.Sender()
if err != nil { if err != nil {
return nil, err return nil, err
} }
group, err = group.Detail() group, err = group.Detail()
if err != nil { if err != nil {
return nil, err return nil, err
} }
users := group.MemberList.SearchByUserName(1, m.senderInGroupUserName) users := group.MemberList.SearchByUserName(1, m.senderInGroupUserName)
if users == nil { if users == nil {
return nil, noSuchUserFoundError return nil, noSuchUserFoundError
} }
return users.First(), nil return users.First(), nil
} }
// 获取消息的接收者 // 获取消息的接收者
func (m *Message) Receiver() (*User, error) { func (m *Message) Receiver() (*User, error) {
if m.IsSendByGroup() { if m.IsSendByGroup() {
if sender, err := m.Sender(); err != nil { if sender, err := m.Sender(); err != nil {
return nil, err return nil, err
} else { } else {
users := sender.MemberList.SearchByUserName(1, m.ToUserName) users := sender.MemberList.SearchByUserName(1, m.ToUserName)
if users == nil { if users == nil {
return nil, noSuchUserFoundError return nil, noSuchUserFoundError
} }
return users.First(), nil return users.First(), nil
} }
} else { } else {
users := m.Bot.self.MemberList.SearchByUserName(1, m.ToUserName) users := m.Bot.self.MemberList.SearchByUserName(1, m.ToUserName)
if users == nil { if users == nil {
return nil, noSuchUserFoundError return nil, noSuchUserFoundError
} }
return users.First(), nil return users.First(), nil
} }
} }
// 判断消息是否由自己发送 // 判断消息是否由自己发送
func (m *Message) IsSendBySelf() bool { 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 { func (m *Message) IsSendByFriend() bool {
return !m.IsSendByGroup() && strings.HasPrefix(m.FromUserName, "@") return !m.IsSendByGroup() && strings.HasPrefix(m.FromUserName, "@")
} }
// 判断消息是否由群组发送 // 判断消息是否由群组发送
func (m *Message) IsSendByGroup() bool { 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) { func (m *Message) Reply(msgType int, content, mediaId string) (*SentMessage, error) {
msg := NewSendMessage(msgType, content, m.Bot.self.User.UserName, m.FromUserName, mediaId) msg := NewSendMessage(msgType, content, m.Bot.self.User.UserName, m.FromUserName, mediaId)
info := m.Bot.storage.LoginInfo info := m.Bot.storage.LoginInfo
request := m.Bot.storage.Request request := m.Bot.storage.Request
return m.Bot.Caller.WebWxSendMsg(msg, info, request) return m.Bot.Caller.WebWxSendMsg(msg, info, request)
} }
// 回复文本消息 // 回复文本消息
func (m *Message) ReplyText(content string) (*SentMessage, error) { 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) { func (m *Message) ReplyImage(file *os.File) (*SentMessage, error) {
info := m.Bot.storage.LoginInfo info := m.Bot.storage.LoginInfo
request := m.Bot.storage.Request request := m.Bot.storage.Request
return m.Bot.Caller.WebWxSendImageMsg(file, request, info, m.Bot.self.UserName, m.FromUserName) return m.Bot.Caller.WebWxSendImageMsg(file, request, info, m.Bot.self.UserName, m.FromUserName)
} }
func (m *Message) IsText() bool { func (m *Message) IsText() bool {
return m.MsgType == 1 && m.Url == "" return m.MsgType == 1 && m.Url == ""
} }
func (m *Message) IsMap() bool { func (m *Message) IsMap() bool {
return m.MsgType == 1 && m.Url != "" return m.MsgType == 1 && m.Url != ""
} }
func (m *Message) IsPicture() bool { func (m *Message) IsPicture() bool {
return m.MsgType == 3 || m.MsgType == 47 return m.MsgType == 3 || m.MsgType == 47
} }
func (m *Message) IsVoice() bool { func (m *Message) IsVoice() bool {
return m.MsgType == 34 return m.MsgType == 34
} }
func (m *Message) IsFriendAdd() bool { func (m *Message) IsFriendAdd() bool {
return m.MsgType == 37 && m.FromUserName == "fmessage" return m.MsgType == 37 && m.FromUserName == "fmessage"
} }
func (m *Message) IsCard() bool { func (m *Message) IsCard() bool {
return m.MsgType == 42 return m.MsgType == 42
} }
func (m *Message) IsVideo() bool { func (m *Message) IsVideo() bool {
return m.MsgType == 43 || m.MsgType == 62 return m.MsgType == 43 || m.MsgType == 62
} }
func (m *Message) IsMedia() bool { func (m *Message) IsMedia() bool {
return m.MsgType == 49 return m.MsgType == 49
} }
// 判断是否撤回 // 判断是否撤回
func (m *Message) IsRecalled() bool { func (m *Message) IsRecalled() bool {
return m.MsgType == 10002 return m.MsgType == 10002
} }
func (m *Message) IsSystem() bool { func (m *Message) IsSystem() bool {
return m.MsgType == 10000 return m.MsgType == 10000
} }
func (m *Message) IsNotify() bool { func (m *Message) IsNotify() bool {
return m.MsgType == 51 && m.StatusNotifyCode != 0 return m.MsgType == 51 && m.StatusNotifyCode != 0
} }
// 判断当前的消息是不是微信转账 // 判断当前的消息是不是微信转账
func (m *Message) IsTransferAccounts() bool { func (m *Message) IsTransferAccounts() bool {
return m.IsMedia() && m.FileName == "微信转账" return m.IsMedia() && m.FileName == "微信转账"
} }
// 判断当前是否发出红包 // 判断当前是否发出红包
func (m *Message) IsSendRedPacket() bool { func (m *Message) IsSendRedPacket() bool {
return m.IsSystem() && m.Content == "发出红包,请在手机上查看" return m.IsSystem() && m.Content == "发出红包,请在手机上查看"
} }
// 判断当前是否收到红包 // 判断当前是否收到红包
func (m *Message) IsReceiveRedPacket() bool { func (m *Message) IsReceiveRedPacket() bool {
return m.IsSystem() && m.Content == "收到红包,请在手机上查看" return m.IsSystem() && m.Content == "收到红包,请在手机上查看"
} }
func (m *Message) IsSysNotice() bool { func (m *Message) IsSysNotice() bool {
return m.MsgType == 9999 return m.MsgType == 9999
} }
// 判断是否为操作通知消息 // 判断是否为操作通知消息
func (m *Message) StatusNotify() bool { func (m *Message) StatusNotify() bool {
return m.MsgType == 51 return m.MsgType == 51
} }
// 判断消息是否为文件类型的消息 // 判断消息是否为文件类型的消息
func (m *Message) HasFile() bool { 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) { func (m *Message) GetFile() (*http.Response, error) {
if !m.HasFile() { if !m.HasFile() {
return nil, errors.New("invalid message type") return nil, errors.New("invalid message type")
} }
if m.IsPicture() { if m.IsPicture() {
return m.Bot.Caller.Client.WebWxGetMsgImg(m, m.Bot.storage.LoginInfo) return m.Bot.Caller.Client.WebWxGetMsgImg(m, m.Bot.storage.LoginInfo)
} }
if m.IsVoice() { if m.IsVoice() {
return m.Bot.Caller.Client.WebWxGetVoice(m, m.Bot.storage.LoginInfo) return m.Bot.Caller.Client.WebWxGetVoice(m, m.Bot.storage.LoginInfo)
} }
if m.IsVideo() { if m.IsVideo() {
return m.Bot.Caller.Client.WebWxGetVideo(m, m.Bot.storage.LoginInfo) return m.Bot.Caller.Client.WebWxGetVideo(m, m.Bot.storage.LoginInfo)
} }
if m.IsMedia() { if m.IsMedia() {
return m.Bot.Caller.Client.WebWxGetMedia(m, m.Bot.storage.LoginInfo) return m.Bot.Caller.Client.WebWxGetMedia(m, m.Bot.storage.LoginInfo)
} }
return nil, errors.New("unsupported type") return nil, errors.New("unsupported type")
} }
// 获取card类型 // 获取card类型
func (m *Message) Card() (*Card, error) { func (m *Message) Card() (*Card, error) {
if !m.IsCard() { if !m.IsCard() {
return nil, errors.New("card message required") return nil, errors.New("card message required")
} }
var card Card var card Card
content := XmlFormString(m.Content) content := XmlFormString(m.Content)
err := xml.Unmarshal([]byte(content), &card) err := xml.Unmarshal([]byte(content), &card)
return &card, err return &card, err
} }
// 获取FriendAddMessageContent内容 // 获取FriendAddMessageContent内容
func (m *Message) FriendAddMessageContent() (*FriendAddMessage, error) { func (m *Message) FriendAddMessageContent() (*FriendAddMessage, error) {
if !m.IsFriendAdd() { if !m.IsFriendAdd() {
return nil, errors.New("friend add message required") return nil, errors.New("friend add message required")
} }
var f FriendAddMessage var f FriendAddMessage
content := XmlFormString(m.Content) content := XmlFormString(m.Content)
err := xml.Unmarshal([]byte(content), &f) err := xml.Unmarshal([]byte(content), &f)
return &f, err return &f, err
} }
// 获取撤回消息的内容 // 获取撤回消息的内容
func (m *Message) RevokeMsg() (*RevokeMsg, error) { func (m *Message) RevokeMsg() (*RevokeMsg, error) {
if !m.IsRecalled() { if !m.IsRecalled() {
return nil, errors.New("recalled message required") return nil, errors.New("recalled message required")
} }
var r RevokeMsg var r RevokeMsg
content := XmlFormString(m.Content) content := XmlFormString(m.Content)
err := xml.Unmarshal([]byte(content), &r) err := xml.Unmarshal([]byte(content), &r)
return &r, err return &r, err
} }
// 同意好友的请求 // 同意好友的请求
func (m *Message) Agree(verifyContents ...string) error { func (m *Message) Agree(verifyContents ...string) error {
if !m.IsFriendAdd() { if !m.IsFriendAdd() {
return fmt.Errorf("friend add message required") return fmt.Errorf("friend add message required")
} }
var builder strings.Builder var builder strings.Builder
for _, v := range verifyContents { for _, v := range verifyContents {
builder.WriteString(v) builder.WriteString(v)
} }
return m.Bot.Caller.WebWxVerifyUser(m.Bot.storage, m.RecommendInfo, builder.String()) return m.Bot.Caller.WebWxVerifyUser(m.Bot.storage, m.RecommendInfo, builder.String())
} }
// 往消息上下文中设置值 // 往消息上下文中设置值
// goroutine safe // goroutine safe
func (m *Message) Set(key string, value interface{}) { func (m *Message) Set(key string, value interface{}) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.item == nil { if m.item == nil {
m.item = make(map[string]interface{}) m.item = make(map[string]interface{})
} }
m.item[key] = value m.item[key] = value
} }
// 从消息上下文中获取值 // 从消息上下文中获取值
// goroutine safe // goroutine safe
func (m *Message) Get(key string) (value interface{}, exist bool) { func (m *Message) Get(key string) (value interface{}, exist bool) {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
value, exist = m.item[key] value, exist = m.item[key]
return return
} }
// 消息初始化,根据不同的消息作出不同的处理 // 消息初始化,根据不同的消息作出不同的处理
func (m *Message) init(bot *Bot) { func (m *Message) init(bot *Bot) {
m.Bot = bot m.Bot = bot
if m.IsSendByGroup() { if m.IsSendByGroup() {
data := strings.Split(m.Content, ":<br/>") data := strings.Split(m.Content, ":<br/>")
m.Content = strings.Join(data[1:], "") m.Content = strings.Join(data[1:], "")
m.senderInGroupUserName = data[0] m.senderInGroupUserName = data[0]
receiver, err := m.Receiver() receiver, err := m.Receiver()
if err == nil { if err == nil {
displayName := receiver.DisplayName displayName := receiver.DisplayName
if displayName == "" { if displayName == "" {
displayName = receiver.NickName displayName = receiver.NickName
} }
atFlag := "@" + displayName atFlag := "@" + displayName
index := len(atFlag) + 1 + 1 index := len(atFlag) + 1 + 1
if strings.HasPrefix(m.Content, atFlag) && unicode.IsSpace(rune(m.Content[index])) { if strings.HasPrefix(m.Content, atFlag) && unicode.IsSpace(rune(m.Content[index])) {
m.IsAt = true m.IsAt = true
m.Content = m.Content[index+1:] m.Content = m.Content[index+1:]
} }
} }
} }
} }
// 发送消息的结构体 // 发送消息的结构体
type SendMessage struct { type SendMessage struct {
Type int Type int
Content string Content string
FromUserName string FromUserName string
ToUserName string ToUserName string
LocalID string LocalID string
ClientMsgId string ClientMsgId string
MediaId string MediaId string
} }
// SendMessage的构造方法 // SendMessage的构造方法
func NewSendMessage(msgType int, content, fromUserName, toUserName, mediaId string) *SendMessage { func NewSendMessage(msgType int, content, fromUserName, toUserName, mediaId string) *SendMessage {
id := strconv.FormatInt(time.Now().UnixNano()/1e2, 10) id := strconv.FormatInt(time.Now().UnixNano()/1e2, 10)
return &SendMessage{ return &SendMessage{
Type: msgType, Type: msgType,
Content: content, Content: content,
FromUserName: fromUserName, FromUserName: fromUserName,
ToUserName: toUserName, ToUserName: toUserName,
LocalID: id, LocalID: id,
ClientMsgId: id, ClientMsgId: id,
MediaId: mediaId, MediaId: mediaId,
} }
} }
// 文本消息的构造方法 // 文本消息的构造方法
func NewTextSendMessage(content, fromUserName, toUserName string) *SendMessage { 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 { func NewMediaSendMessage(msgType int, fromUserName, toUserName, mediaId string) *SendMessage {
return NewSendMessage(msgType, "", fromUserName, toUserName, mediaId) return NewSendMessage(msgType, "", fromUserName, toUserName, mediaId)
} }
// 一些特殊类型的消息会携带该结构体信息 // 一些特殊类型的消息会携带该结构体信息
type RecommendInfo struct { type RecommendInfo struct {
OpCode int OpCode int
Scene int Scene int
Sex int Sex int
VerifyFlag int VerifyFlag int
AttrStatus int64 AttrStatus int64
QQNum int64 QQNum int64
Alias string Alias string
City string City string
Content string Content string
NickName string NickName string
Province string Province string
Signature string Signature string
Ticket string Ticket string
UserName string UserName string
} }
// 名片消息内容 // 名片消息内容
type Card struct { type Card struct {
XMLName xml.Name `xml:"msg"` XMLName xml.Name `xml:"msg"`
ImageStatus int `xml:"imagestatus,attr"` ImageStatus int `xml:"imagestatus,attr"`
Scene int `xml:"scene,attr"` Scene int `xml:"scene,attr"`
Sex int `xml:"sex,attr"` Sex int `xml:"sex,attr"`
Certflag int `xml:"certflag,attr"` Certflag int `xml:"certflag,attr"`
BigHeadImgUrl string `xml:"bigheadimgurl,attr"` BigHeadImgUrl string `xml:"bigheadimgurl,attr"`
SmallHeadImgUrl string `xml:"smallheadimgurl,attr"` SmallHeadImgUrl string `xml:"smallheadimgurl,attr"`
UserName string `xml:"username,attr"` UserName string `xml:"username,attr"`
NickName string `xml:"nickname,attr"` NickName string `xml:"nickname,attr"`
ShortPy string `xml:"shortpy,attr"` ShortPy string `xml:"shortpy,attr"`
Alias string `xml:"alias,attr"` // Note: 这个是名片用户的微信号 Alias string `xml:"alias,attr"` // Note: 这个是名片用户的微信号
Province string `xml:"province,attr"` Province string `xml:"province,attr"`
City string `xml:"city,attr"` City string `xml:"city,attr"`
Sign string `xml:"sign,attr"` Sign string `xml:"sign,attr"`
Certinfo string `xml:"certinfo,attr"` Certinfo string `xml:"certinfo,attr"`
BrandIconUrl string `xml:"brandIconUrl,attr"` BrandIconUrl string `xml:"brandIconUrl,attr"`
BrandHomeUr string `xml:"brandHomeUr,attr"` BrandHomeUr string `xml:"brandHomeUr,attr"`
BrandSubscriptConfigUrl string `xml:"brandSubscriptConfigUrl,attr"` BrandSubscriptConfigUrl string `xml:"brandSubscriptConfigUrl,attr"`
BrandFlags string `xml:"brandFlags,attr"` BrandFlags string `xml:"brandFlags,attr"`
RegionCode string `xml:"regionCode,attr"` RegionCode string `xml:"regionCode,attr"`
} }
// 好友添加消息信息内容 // 好友添加消息信息内容
type FriendAddMessage struct { type FriendAddMessage struct {
XMLName xml.Name `xml:"msg"` XMLName xml.Name `xml:"msg"`
Shortpy int `xml:"shortpy,attr"` Shortpy int `xml:"shortpy,attr"`
ImageStatus int `xml:"imagestatus,attr"` ImageStatus int `xml:"imagestatus,attr"`
Scene int `xml:"scene,attr"` Scene int `xml:"scene,attr"`
PerCard int `xml:"percard,attr"` PerCard int `xml:"percard,attr"`
Sex int `xml:"sex,attr"` Sex int `xml:"sex,attr"`
AlbumFlag int `xml:"albumflag,attr"` AlbumFlag int `xml:"albumflag,attr"`
AlbumStyle int `xml:"albumstyle,attr"` AlbumStyle int `xml:"albumstyle,attr"`
SnsFlag int `xml:"snsflag,attr"` SnsFlag int `xml:"snsflag,attr"`
Opcode int `xml:"opcode,attr"` Opcode int `xml:"opcode,attr"`
FromUserName string `xml:"fromusername,attr"` FromUserName string `xml:"fromusername,attr"`
EncryptUserName string `xml:"encryptusername,attr"` EncryptUserName string `xml:"encryptusername,attr"`
FromNickName string `xml:"fromnickname,attr"` FromNickName string `xml:"fromnickname,attr"`
Content string `xml:"content,attr"` Content string `xml:"content,attr"`
Country string `xml:"country,attr"` Country string `xml:"country,attr"`
Province string `xml:"province,attr"` Province string `xml:"province,attr"`
City string `xml:"city,attr"` City string `xml:"city,attr"`
Sign string `xml:"sign,attr"` Sign string `xml:"sign,attr"`
Alias string `xml:"alias,attr"` Alias string `xml:"alias,attr"`
WeiBo string `xml:"weibo,attr"` WeiBo string `xml:"weibo,attr"`
AlbumBgImgId string `xml:"albumbgimgid,attr"` AlbumBgImgId string `xml:"albumbgimgid,attr"`
SnsBgImgId string `xml:"snsbgimgid,attr"` SnsBgImgId string `xml:"snsbgimgid,attr"`
SnsBgObjectId string `xml:"snsbgobjectid,attr"` SnsBgObjectId string `xml:"snsbgobjectid,attr"`
MHash string `xml:"mhash,attr"` MHash string `xml:"mhash,attr"`
MFullHash string `xml:"mfullhash,attr"` MFullHash string `xml:"mfullhash,attr"`
BigHeadImgUrl string `xml:"bigheadimgurl,attr"` BigHeadImgUrl string `xml:"bigheadimgurl,attr"`
SmallHeadImgUrl string `xml:"smallheadimgurl,attr"` SmallHeadImgUrl string `xml:"smallheadimgurl,attr"`
Ticket string `xml:"ticket,attr"` Ticket string `xml:"ticket,attr"`
GoogleContact string `xml:"googlecontact,attr"` GoogleContact string `xml:"googlecontact,attr"`
QrTicket string `xml:"qrticket,attr"` QrTicket string `xml:"qrticket,attr"`
ChatRoomUserName string `xml:"chatroomusername,attr"` ChatRoomUserName string `xml:"chatroomusername,attr"`
SourceUserName string `xml:"sourceusername,attr"` SourceUserName string `xml:"sourceusername,attr"`
ShareCardUserName string `xml:"sharecardusername,attr"` ShareCardUserName string `xml:"sharecardusername,attr"`
ShareCardNickName string `xml:"sharecardnickname,attr"` ShareCardNickName string `xml:"sharecardnickname,attr"`
CardVersion string `xml:"cardversion,attr"` CardVersion string `xml:"cardversion,attr"`
BrandList struct { BrandList struct {
Count int `xml:"count,attr"` Count int `xml:"count,attr"`
Ver int64 `xml:"ver,attr"` Ver int64 `xml:"ver,attr"`
} `xml:"brandlist"` } `xml:"brandlist"`
} }
// 撤回消息Content // 撤回消息Content
type RevokeMsg struct { type RevokeMsg struct {
SysMsg xml.Name `xml:"sysmsg"` SysMsg xml.Name `xml:"sysmsg"`
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
RevokeMsg struct { RevokeMsg struct {
OldMsgId int64 `xml:"oldmsgid"` OldMsgId int64 `xml:"oldmsgid"`
MsgId int64 `xml:"msgid"` MsgId int64 `xml:"msgid"`
Session string `xml:"session"` Session string `xml:"session"`
ReplaceMsg string `xml:"replacemsg"` ReplaceMsg string `xml:"replacemsg"`
} `xml:"revokemsg"` } `xml:"revokemsg"`
} }
// 已发送的信息 // 已发送的信息
type SentMessage struct { type SentMessage struct {
*SendMessage *SendMessage
Self *Self Self *Self
MsgId string MsgId string
} }
// 撤回该消息 // 撤回该消息
func (s *SentMessage) Revoke() error { 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
} }

View File

@ -1,66 +1,74 @@
package openwechat package openwechat
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"math/rand" "math/rand"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
func ToBuffer(v interface{}) (*bytes.Buffer, error) { func ToBuffer(v interface{}) (*bytes.Buffer, error) {
buf, err := json.Marshal(v) buf, err := json.Marshal(v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return bytes.NewBuffer(buf), nil return bytes.NewBuffer(buf), nil
} }
// 获取随机设备id // 获取随机设备id
func GetRandomDeviceId() string { func GetRandomDeviceId() string {
rand.Seed(time.Now().Unix()) rand.Seed(time.Now().Unix())
var builder strings.Builder var builder strings.Builder
builder.WriteString("e") builder.WriteString("e")
for i := 0; i < 15; i++ { for i := 0; i < 15; i++ {
r := rand.Intn(9) r := rand.Intn(9)
builder.WriteString(strconv.Itoa(r)) builder.WriteString(strconv.Itoa(r))
} }
return builder.String() return builder.String()
} }
func getWebWxDataTicket(cookies []*http.Cookie) string { func getWebWxDataTicket(cookies []*http.Cookie) string {
for _, cookie := range cookies { for _, cookie := range cookies {
if cookie.Name == "webwx_data_ticket" { if cookie.Name == "webwx_data_ticket" {
return cookie.Value return cookie.Value
} }
} }
return "" return ""
} }
// Form Xml 格式化 // Form Xml 格式化
func XmlFormString(text string) string { func XmlFormString(text string) string {
lt := strings.ReplaceAll(text, "&lt;", "<") lt := strings.ReplaceAll(text, "&lt;", "<")
gt := strings.ReplaceAll(lt, "&gt;", ">") gt := strings.ReplaceAll(lt, "&gt;", ">")
br := strings.ReplaceAll(gt, "<br/>", "\n") br := strings.ReplaceAll(gt, "<br/>", "\n")
return strings.ReplaceAll(br, "&amp;amp;", "&") return strings.ReplaceAll(br, "&amp;amp;", "&")
} }
func getTotalDuration(delay ...time.Duration) time.Duration { func getTotalDuration(delay ...time.Duration) time.Duration {
var total time.Duration var total time.Duration
for _, d := range delay { for _, d := range delay {
total += d total += d
} }
return total return total
} }
// 获取文件上传的类型 // 获取文件上传的类型
func GetFileContentType(file multipart.File) (string, error) { func GetFileContentType(file multipart.File) (string, error) {
data := make([]byte, 512) data := make([]byte, 512)
if _, err := file.Read(data); err != nil { if _, err := file.Read(data); err != nil {
return "", err return "", err
} }
return http.DetectContentType(data), nil 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]
} }

3
url.go
View File

@ -23,6 +23,7 @@ type UrlManager struct {
webWxGetMediaUrl string webWxGetMediaUrl string
webWxUpdateChatRoomUrl string webWxUpdateChatRoomUrl string
webWxRevokeMsg string webWxRevokeMsg string
webWxCheckUploadUrl string
} }
var ( var (
@ -49,6 +50,7 @@ var (
webWxGetMediaUrl: webWxGetMediaDesktopUrl, webWxGetMediaUrl: webWxGetMediaDesktopUrl,
webWxUpdateChatRoomUrl: webWxUpdateChatRoomDesktopUrl, webWxUpdateChatRoomUrl: webWxUpdateChatRoomDesktopUrl,
webWxRevokeMsg: webWxRevokeMsgDesktopUrl, webWxRevokeMsg: webWxRevokeMsgDesktopUrl,
webWxCheckUploadUrl: webWxCheckUploadDesktopUrl,
} }
// 网页版 // 网页版
@ -74,6 +76,7 @@ var (
webWxGetMediaUrl: webWxGetMediaNormalUrl, webWxGetMediaUrl: webWxGetMediaNormalUrl,
webWxUpdateChatRoomUrl: webWxUpdateChatRoomNormalUrl, webWxUpdateChatRoomUrl: webWxUpdateChatRoomNormalUrl,
webWxRevokeMsg: webWxRevokeMsgNormalUrl, webWxRevokeMsg: webWxRevokeMsgNormalUrl,
webWxCheckUploadUrl: webWxCheckUploadNormalUrl,
} }
) )

654
user.go
View File

@ -1,336 +1,342 @@
package openwechat package openwechat
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strings" "strings"
) )
// 抽象的用户结构: 好友 群组 公众号 // 抽象的用户结构: 好友 群组 公众号
type User struct { type User struct {
Uin int Uin int
HideInputBarFlag int HideInputBarFlag int
StarFriend int StarFriend int
Sex int Sex int
AppAccountFlag int AppAccountFlag int
VerifyFlag int VerifyFlag int
ContactFlag int ContactFlag int
WebWxPluginSwitch int WebWxPluginSwitch int
HeadImgFlag int HeadImgFlag int
SnsFlag int SnsFlag int
IsOwner int IsOwner int
MemberCount int MemberCount int
ChatRoomId int ChatRoomId int
UniFriend int UniFriend int
OwnerUin int OwnerUin int
Statues int Statues int
AttrStatus int AttrStatus int
Province string Province string
City string City string
Alias string Alias string
DisplayName string DisplayName string
KeyWord string KeyWord string
EncryChatRoomId string EncryChatRoomId string
UserName string UserName string
NickName string NickName string
HeadImgUrl string HeadImgUrl string
RemarkName string RemarkName string
PYInitial string PYInitial string
PYQuanPin string PYQuanPin string
RemarkPYInitial string RemarkPYInitial string
RemarkPYQuanPin string RemarkPYQuanPin string
Signature string Signature string
MemberList Members MemberList Members
Self *Self Self *Self
} }
// implement fmt.Stringer // implement fmt.Stringer
func (u *User) String() string { func (u *User) String() string {
return fmt.Sprintf("<User:%s>", u.NickName) return fmt.Sprintf("<User:%s>", u.NickName)
} }
// 获取用户头像 // 获取用户头像
func (u *User) GetAvatarResponse() (*http.Response, error) { 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 { func (u *User) SaveAvatar(filename string) error {
resp, err := u.GetAvatarResponse() resp, err := u.GetAvatarResponse()
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
buffer := bytes.Buffer{} buffer := bytes.Buffer{}
if _, err := buffer.ReadFrom(resp.Body); err != nil { if _, err := buffer.ReadFrom(resp.Body); err != nil {
return err return err
} }
file, err := os.Create(filename) file, err := os.Create(filename)
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
_, err = file.Write(buffer.Bytes()) _, err = file.Write(buffer.Bytes())
return err return err
} }
// 获取用户的详情 // 获取用户的详情
func (u *User) Detail() (*User, error) { func (u *User) Detail() (*User, error) {
members := Members{u} members := Members{u}
request := u.Self.Bot.storage.Request request := u.Self.Bot.storage.Request
newMembers, err := u.Self.Bot.Caller.WebWxBatchGetContact(members, request) newMembers, err := u.Self.Bot.Caller.WebWxBatchGetContact(members, request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
user := newMembers.First() user := newMembers.First()
user.Self = u.Self user.Self = u.Self
return user, nil return user, nil
} }
// 判断是否为好友 // 判断是否为好友
func (u *User) IsFriend() bool { 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 { 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 { 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 { type Self struct {
*User *User
Bot *Bot Bot *Bot
fileHelper *Friend fileHelper *Friend
members Members members Members
friends Friends friends Friends
groups Groups groups Groups
mps Mps mps Mps
} }
// 获取所有的好友、群组、公众号信息 // 获取所有的好友、群组、公众号信息
func (s *Self) Members(update ...bool) (Members, error) { func (s *Self) Members(update ...bool) (Members, error) {
// 首先判断缓存里有没有,如果没有则去更新缓存 // 首先判断缓存里有没有,如果没有则去更新缓存
if s.members == nil { if s.members == nil {
if err := s.updateMembers(); err != nil { if err := s.updateMembers(); err != nil {
return nil, err return nil, err
} }
return s.members, nil return s.members, nil
} }
// 判断是否需要更新,如果传入的参数不为nil,则取最后一个 // 判断是否需要更新,如果传入的参数不为nil,则取最后一个
if len(update) > 0 && update[0] { if len(update) > 0 && update[0] {
if err := s.updateMembers(); err != nil { if err := s.updateMembers(); err != nil {
return nil, err return nil, err
} }
} }
return s.members, nil return s.members, nil
} }
// 更新联系人处理 // 更新联系人处理
func (s *Self) updateMembers() error { func (s *Self) updateMembers() error {
info := s.Bot.storage.LoginInfo info := s.Bot.storage.LoginInfo
members, err := s.Bot.Caller.WebWxGetContact(info) members, err := s.Bot.Caller.WebWxGetContact(info)
if err != nil { if err != nil {
return err return err
} }
members.SetOwner(s) members.SetOwner(s)
s.members = members s.members = members
return nil return nil
} }
// 获取文件传输助手对象封装成Friend返回 // 获取文件传输助手对象封装成Friend返回
// fh, err := self.FileHelper() // or fh := openwechat.NewFriendHelper(self) // fh, err := self.FileHelper() // or fh := openwechat.NewFriendHelper(self)
func (s *Self) FileHelper() (*Friend, error) { func (s *Self) FileHelper() (*Friend, error) {
// 如果缓存里有,直接返回,否则去联系人里面找 // 如果缓存里有,直接返回,否则去联系人里面找
if s.fileHelper != nil { if s.fileHelper != nil {
return s.fileHelper, nil return s.fileHelper, nil
} }
members, err := s.Members() members, err := s.Members()
if err != nil { if err != nil {
return nil, err return nil, err
} }
users := members.SearchByUserName(1, "filehelper") users := members.SearchByUserName(1, "filehelper")
if users == nil { if users == nil {
return NewFriendHelper(s), nil return NewFriendHelper(s), nil
} }
s.fileHelper = &Friend{users.First()} s.fileHelper = &Friend{users.First()}
return s.fileHelper, nil return s.fileHelper, nil
} }
// 获取所有的好友 // 获取所有的好友
func (s *Self) Friends(update ...bool) (Friends, error) { func (s *Self) Friends(update ...bool) (Friends, error) {
if s.friends == nil || (len(update) > 0 && update[0]) { if s.friends == nil || (len(update) > 0 && update[0]) {
if _, err := s.Members(true); err != nil { if _, err := s.Members(true); err != nil {
return nil, err return nil, err
} }
s.friends = s.members.Friends() s.friends = s.members.Friends()
} }
return s.friends, nil return s.friends, nil
} }
// 获取所有的群组 // 获取所有的群组
func (s *Self) Groups(update ...bool) (Groups, error) { func (s *Self) Groups(update ...bool) (Groups, error) {
if s.groups == nil || (len(update) > 0 && update[0]) { if s.groups == nil || (len(update) > 0 && update[0]) {
if _, err := s.Members(true); err != nil { if _, err := s.Members(true); err != nil {
return nil, err return nil, err
} }
s.groups = s.members.Groups() s.groups = s.members.Groups()
} }
return s.groups, nil return s.groups, nil
} }
// 获取所有的公众号 // 获取所有的公众号
func (s *Self) Mps(update ...bool) (Mps, error) { func (s *Self) Mps(update ...bool) (Mps, error) {
if s.mps == nil || (len(update) > 0 && update[0]) { if s.mps == nil || (len(update) > 0 && update[0]) {
if _, err := s.Members(true); err != nil { if _, err := s.Members(true); err != nil {
return nil, err return nil, err
} }
s.mps = s.members.MPs() s.mps = s.members.MPs()
} }
return s.mps, nil return s.mps, nil
} }
// 更新所有的联系人信息 // 更新所有的联系人信息
func (s *Self) UpdateMembersDetail() error { func (s *Self) UpdateMembersDetail() error {
// 先获取所有的联系人 // 先获取所有的联系人
members, err := s.Members() members, err := s.Members()
if err != nil { if err != nil {
return err return err
} }
return members.detail(s) return members.detail(s)
} }
// 抽象发送消息接口 // 抽象发送消息接口
func (s *Self) sendMessageToUser(user *User, msg *SendMessage) (*SentMessage, error) { func (s *Self) sendMessageToUser(user *User, msg *SendMessage) (*SentMessage, error) {
msg.FromUserName = s.UserName msg.FromUserName = s.UserName
msg.ToUserName = user.UserName msg.ToUserName = user.UserName
info := s.Bot.storage.LoginInfo info := s.Bot.storage.LoginInfo
request := s.Bot.storage.Request request := s.Bot.storage.Request
successSendMessage, err := s.Bot.Caller.WebWxSendMsg(msg, info, request) successSendMessage, err := s.Bot.Caller.WebWxSendMsg(msg, info, request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
successSendMessage.Self = s successSendMessage.Self = s
return successSendMessage, nil return successSendMessage, nil
} }
// 发送消息给好友 // 发送消息给好友
func (s *Self) SendMessageToFriend(friend *Friend, msg *SendMessage) (*SentMessage, error) { 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) { func (s *Self) SendTextToFriend(friend *Friend, text string) (*SentMessage, error) {
msg := NewTextSendMessage(text, s.UserName, friend.UserName) msg := NewTextSendMessage(text, s.UserName, friend.UserName)
return s.SendMessageToFriend(friend, msg) return s.SendMessageToFriend(friend, msg)
} }
// 发送图片消息给好友 // 发送图片消息给好友
func (s *Self) SendImageToFriend(friend *Friend, file *os.File) (*SentMessage, error) { func (s *Self) SendImageToFriend(friend *Friend, file *os.File) (*SentMessage, error) {
req := s.Bot.storage.Request req := s.Bot.storage.Request
info := s.Bot.storage.LoginInfo info := s.Bot.storage.LoginInfo
return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, friend.UserName) 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") // self.SetRemarkNameToFriend(friend, "remark") // or friend.SetRemarkName("remark")
func (s *Self) SetRemarkNameToFriend(friend *Friend, remarkName string) error { func (s *Self) SetRemarkNameToFriend(friend *Friend, remarkName string) error {
req := s.Bot.storage.Request req := s.Bot.storage.Request
return s.Bot.Caller.WebWxOplog(req, remarkName, friend.UserName) return s.Bot.Caller.WebWxOplog(req, remarkName, friend.UserName)
} }
// 拉多名好友进群 // 拉多名好友进群
// 最好自己是群主,成功率高一点,因为有的群允许非群组拉人,而有的群不允许 // 最好自己是群主,成功率高一点,因为有的群允许非群组拉人,而有的群不允许
func (s *Self) AddFriendsIntoGroup(group *Group, friends ...*Friend) error { func (s *Self) AddFriendsIntoGroup(group *Group, friends ...*Friend) error {
if len(friends) == 0 { if len(friends) == 0 {
return nil return nil
} }
// 获取群的所有的群员 // 获取群的所有的群员
groupMembers, err := group.Members() groupMembers, err := group.Members()
if err != nil { if err != nil {
return err return err
} }
// 判断当前的成员在不在群里面 // 判断当前的成员在不在群里面
for _, friend := range friends { for _, friend := range friends {
for _, member := range groupMembers { for _, member := range groupMembers {
if member.UserName == friend.UserName { if member.UserName == friend.UserName {
return fmt.Errorf("user %s has alreay in this group", friend.String()) return fmt.Errorf("user %s has alreay in this group", friend.String())
} }
} }
} }
req := s.Bot.storage.Request req := s.Bot.storage.Request
info := s.Bot.storage.LoginInfo info := s.Bot.storage.LoginInfo
return s.Bot.Caller.AddFriendIntoChatRoom(req, info, group, friends...) return s.Bot.Caller.AddFriendIntoChatRoom(req, info, group, friends...)
} }
// 从群聊中移除用户 // 从群聊中移除用户
// Deprecated // Deprecated
// 无论是网页版,还是程序上都不起作用 // 无论是网页版,还是程序上都不起作用
func (s *Self) RemoveMemberFromGroup(group *Group, members Members) error { func (s *Self) RemoveMemberFromGroup(group *Group, members Members) error {
if len(members) == 0 { if len(members) == 0 {
return nil return nil
} }
if group.IsOwner == 0 { if group.IsOwner == 0 {
return errors.New("group owner required") return errors.New("group owner required")
} }
groupMembers, err := group.Members() groupMembers, err := group.Members()
if err != nil { if err != nil {
return err return err
} }
// 判断用户是否在群聊中 // 判断用户是否在群聊中
var count int var count int
for _, member := range members { for _, member := range members {
for _, gm := range groupMembers { for _, gm := range groupMembers {
if gm.UserName == member.UserName { if gm.UserName == member.UserName {
count++ count++
} }
} }
} }
if count != len(members) { if count != len(members) {
return errors.New("invalid members") return errors.New("invalid members")
} }
req := s.Bot.storage.Request req := s.Bot.storage.Request
info := s.Bot.storage.LoginInfo info := s.Bot.storage.LoginInfo
return s.Bot.Caller.RemoveFriendFromChatRoom(req, info, group, members...) return s.Bot.Caller.RemoveFriendFromChatRoom(req, info, group, members...)
} }
// 拉好友进多个群聊 // 拉好友进多个群聊
// AddFriendIntoGroups, 名字和上面的有点像 // AddFriendIntoGroups, 名字和上面的有点像
func (s *Self) AddFriendIntoManyGroups(friend *Friend, groups ...*Group) error { func (s *Self) AddFriendIntoManyGroups(friend *Friend, groups ...*Group) error {
for _, group := range groups { for _, group := range groups {
if err := s.AddFriendsIntoGroup(group, friend); err != nil { if err := s.AddFriendsIntoGroup(group, friend); err != nil {
return err return err
} }
} }
return nil return nil
} }
// 发送消息给群组 // 发送消息给群组
func (s *Self) SendMessageToGroup(group *Group, msg *SendMessage) (*SentMessage, error) { 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) { func (s *Self) SendTextToGroup(group *Group, text string) (*SentMessage, error) {
msg := NewTextSendMessage(text, s.UserName, group.UserName) msg := NewTextSendMessage(text, s.UserName, group.UserName)
return s.SendMessageToGroup(group, msg) return s.SendMessageToGroup(group, msg)
} }
// 发送图片消息给群组 // 发送图片消息给群组
func (s *Self) SendImageToGroup(group *Group, file *os.File) (*SentMessage, error) { func (s *Self) SendImageToGroup(group *Group, file *os.File) (*SentMessage, error) {
req := s.Bot.storage.Request req := s.Bot.storage.Request
info := s.Bot.storage.LoginInfo info := s.Bot.storage.LoginInfo
return s.Bot.Caller.WebWxSendImageMsg(file, req, info, s.UserName, group.UserName) 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() // self.RevokeMessage(sentMessage) // or sentMessage.Revoke()
// } // }
func (s *Self) RevokeMessage(msg *SentMessage) error { 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 { func (m Members) Count() int {
return len(m) return len(m)
} }
// 获取第一个 // 获取第一个
func (m Members) First() *User { func (m Members) First() *User {
if m.Count() > 0 { if m.Count() > 0 {
u := m[0] u := m[0]
return u return u
} }
return nil return nil
} }
// 获取最后一个 // 获取最后一个
func (m Members) Last() *User { func (m Members) Last() *User {
if m.Count() > 0 { if m.Count() > 0 {
u := m[m.Count()-1] u := m[m.Count()-1]
return u return u
} }
return nil return nil
} }
// 设置owner // 设置owner
// 请不要随意设置 // 请不要随意设置
func (m Members) SetOwner(s *Self) { func (m Members) SetOwner(s *Self) {
for _, member := range m { for _, member := range m {
member.Self = s member.Self = s
} }
} }
// 根据用户名查找 // 根据用户名查找
func (m Members) SearchByUserName(limit int, username string) (results Members) { 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) { 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) { 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) { func (m Members) Search(limit int, condFuncList ...func(user *User) bool) (results Members) {
if condFuncList == nil { if condFuncList == nil {
return m return m
} }
if limit <= 0 { if limit <= 0 {
limit = m.Count() limit = m.Count()
} }
for _, member := range m { for _, member := range m {
if count := len(results); count == limit { if count := len(results); count == limit {
break break
} }
var passCount int var passCount int
for _, condFunc := range condFuncList { for _, condFunc := range condFuncList {
if condFunc(member) { if condFunc(member) {
passCount++ passCount++
} else { } else {
break break
} }
} }
if passCount == len(condFuncList) { if passCount == len(condFuncList) {
results = append(results, member) results = append(results, member)
} }
} }
return return
} }
func (m Members) Friends() Friends { func (m Members) Friends() Friends {
friends := make(Friends, 0) friends := make(Friends, 0)
for _, mb := range m { for _, mb := range m {
if mb.IsFriend() { if mb.IsFriend() {
friend := &Friend{mb} friend := &Friend{mb}
friends = append(friends, friend) friends = append(friends, friend)
} }
} }
return friends return friends
} }
func (m Members) Groups() Groups { func (m Members) Groups() Groups {
groups := make(Groups, 0) groups := make(Groups, 0)
for _, mb := range m { for _, mb := range m {
if mb.IsGroup() { if mb.IsGroup() {
group := &Group{mb} group := &Group{mb}
groups = append(groups, group) groups = append(groups, group)
} }
} }
return groups return groups
} }
func (m Members) MPs() Mps { func (m Members) MPs() Mps {
mps := make(Mps, 0) mps := make(Mps, 0)
for _, mb := range m { for _, mb := range m {
if mb.IsMP() { if mb.IsMP() {
mp := &Mp{mb} mp := &Mp{mb}
mps = append(mps, mp) mps = append(mps, mp)
} }
} }
return mps return mps
} }
// 获取当前Members的详情 // 获取当前Members的详情
func (m Members) detail(self *Self) error { func (m Members) detail(self *Self) error {
// 获取他们的数量 // 获取他们的数量
members := m members := m
count := members.Count() count := members.Count()
// 一次更新50个,分情况讨论 // 一次更新50个,分情况讨论
// 获取总的需要更新的次数 // 获取总的需要更新的次数
var times int var times int
if count < 50 { if count < 50 {
times = 1 times = 1
} else { } else {
times = count / 50 times = count / 50
} }
var newMembers Members var newMembers Members
request := self.Bot.storage.Request request := self.Bot.storage.Request
var pMembers Members var pMembers Members
// 分情况依次更新 // 分情况依次更新
for i := 1; i <= times; i++ { for i := 1; i <= times; i++ {
if times == 1 { if times == 1 {
pMembers = members pMembers = members
} else { } else {
pMembers = members[(i-1)*50 : i*50] pMembers = members[(i-1)*50 : i*50]
} }
nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request) nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request)
if err != nil { if err != nil {
return err return err
} }
newMembers = append(newMembers, nMembers...) newMembers = append(newMembers, nMembers...)
} }
// 最后判断是否全部更新完毕 // 最后判断是否全部更新完毕
total := times * 50 total := times * 50
if total < count { if total < count {
// 将全部剩余的更新完毕 // 将全部剩余的更新完毕
left := count - total left := count - total
pMembers = members[total : total+left] pMembers = members[total : total+left]
nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request) nMembers, err := self.Bot.Caller.WebWxBatchGetContact(pMembers, request)
if err != nil { if err != nil {
return err return err
} }
newMembers = append(newMembers, nMembers...) newMembers = append(newMembers, nMembers...)
} }
if len(newMembers) > 0 { if len(newMembers) > 0 {
newMembers.SetOwner(self) newMembers.SetOwner(self)
self.members = newMembers self.members = newMembers
} }
return nil return nil
} }
// 这里为了兼容Desktop版本找不到文件传输助手的问题 // 这里为了兼容Desktop版本找不到文件传输助手的问题
@ -506,5 +512,5 @@ func (m Members) detail(self *Self) error {
// 这种形式的对象可能缺少一些其他属性 // 这种形式的对象可能缺少一些其他属性
// 但是不影响发送信息的功能 // 但是不影响发送信息的功能
func NewFriendHelper(self *Self) *Friend { func NewFriendHelper(self *Self) *Friend {
return &Friend{&User{UserName: "filehelper", Self: self}} return &Friend{&User{UserName: "filehelper", Self: self}}
} }