diff --git a/bot.go b/bot.go new file mode 100644 index 0000000..2120f96 --- /dev/null +++ b/bot.go @@ -0,0 +1,177 @@ +package openwechat + +import ( + "errors" + "fmt" +) + +type Bot struct { + Caller *Caller + self *Self + storage WechatStorage + ScanCallBack func(body []byte) + LoginCallBack func(body []byte) + UUIDCallback func(uuid string) + messageHandlerGroups *MessageHandlerGroup + err error + exit chan bool +} + +func (b *Bot) Alive() bool { + if b.self == nil { + return false + } + select { + case <-b.exit: + return false + default: + return true + } +} + +func (b *Bot) GetCurrentUser() (*Self, error) { + if b.self == nil { + return nil, errors.New("user not login") + } + return b.self, nil +} + +func (b *Bot) Login() error { + b.prepare() + uuid, err := b.Caller.GetLoginUUID() + if err != nil { + return err + } + if b.UUIDCallback != nil { + b.UUIDCallback(uuid) + } + for { + resp, err := b.Caller.CheckLogin(uuid) + if err != nil { + return err + } + switch resp.Code { + case statusSuccess: + return b.login(resp.Raw) + case statusScanned: + if b.ScanCallBack != nil { + b.ScanCallBack(resp.Raw) + } + case statusTimeout: + return errors.New("login time out") + case statusWait: + continue + } + } +} + +func (b *Bot) login(data []byte) error { + if b.LoginCallBack != nil { + b.LoginCallBack(data) + } + info, err := b.Caller.GetLoginInfo(data) + if err != nil { + return err + } + + b.storage.SetLoginInfo(*info) + + request := BaseRequest{ + Uin: info.WxUin, + Sid: info.WxSid, + Skey: info.SKey, + DeviceID: GetRandomDeviceId(), + } + + b.storage.SetBaseRequest(request) + resp, err := b.Caller.WebInit(request) + if err != nil { + return err + } + b.self = &Self{Bot: b, User: &resp.User} + b.storage.SetWebInitResponse(*resp) + + if err = b.Caller.WebWxStatusNotify(request, *resp, *info); err != nil { + return err + } + go func() { + b.stopAsyncCALL(b.asyncCall()) + }() + return nil +} + +func (b *Bot) asyncCall() error { + var ( + err error + resp *SyncCheckResponse + ) + for b.Alive() { + info := b.storage.GetLoginInfo() + response := b.storage.GetWebInitResponse() + resp, err = b.Caller.SyncCheck(info, response) + if err != nil { + return err + } + if !resp.Success() { + return fmt.Errorf("unknow code got %s", resp.RetCode) + } + if !resp.NorMal() { + if err = b.getMessage(); err != nil { + return err + } + } + } + return err +} + +func (b *Bot) stopAsyncCALL(err error) { + if err != nil { + b.exit <- true + b.err = err + } +} +func (b *Bot) getMessage() error { + info := b.storage.GetLoginInfo() + response := b.storage.GetWebInitResponse() + request := b.storage.GetBaseRequest() + resp, err := b.Caller.WebWxSync(request, response, info) + if err != nil { + return err + } + response.SyncKey = resp.SyncKey + b.storage.SetWebInitResponse(response) + for _, message := range resp.AddMsgList { + processMessage(message, b) + b.messageHandlerGroups.ProcessMessage(message) + } + return nil +} + +func (b *Bot) prepare() { + if b.storage == nil { + panic("WechatStorage can not be nil") + } + if b.messageHandlerGroups == nil { + panic("message can not be nil") + } +} + +func (b *Bot) RegisterMessageHandler(handler MessageHandler) { + b.messageHandlerGroups.RegisterHandler(handler) +} + +func (b *Bot) Block() { + <-b.exit +} + +func NewBot(caller *Caller, storage WechatStorage) *Bot { + return &Bot{Caller: caller, storage: storage} +} + +func DefaultBot() *Bot { + return NewBot(DefaultCaller(), NewSimpleWechatStorage()) +} + +func PrintlnQrcodeUrl(uuid string) { + println(qrcodeUrl + uuid) +} diff --git a/bot_test.go b/bot_test.go new file mode 100644 index 0000000..f7f27e4 --- /dev/null +++ b/bot_test.go @@ -0,0 +1,26 @@ +package openwechat + +import ( + "fmt" + "testing" +) + +func TestDefaultBot(t *testing.T) { + messageHandler := func(message Message) { + fmt.Println(message) + } + bot := DefaultBot(messageHandler) + bot.UUIDCallback = PrintlnQrcodeUrl + if err := bot.Login(); err != nil { + fmt.Println(err) + return + } + //for bot.Alive() { + // message := messageHandler.GetMessage() + // if message.Content == "6666" { + // err := message.ReplyText("nihao") + // fmt.Println(err) + // } + // fmt.Println(message) + //} +} diff --git a/caller.go b/caller.go new file mode 100644 index 0000000..3d4c06b --- /dev/null +++ b/caller.go @@ -0,0 +1,216 @@ +package openwechat + +import ( + "errors" + "fmt" + "os" +) + +type Caller struct { + Client *Client +} + +func NewCaller(client *Client) *Caller { + return &Caller{Client: client} +} + +func DefaultCaller() *Caller { + return NewCaller(DefaultClient()) +} + +func (c *Caller) GetLoginUUID() (string, error) { + resp := NewReturnResponse(c.Client.GetLoginUUID()) + if resp.Err() != nil { + return "", resp.Err() + } + defer resp.Body.Close() + data, err := resp.ReadAll() + if err != nil { + return "", err + } + results := uuidRegexp.FindSubmatch(data) + if len(results) != 2 { + return "", errors.New("uuid does not match") + } + return string(results[1]), nil +} + +func (c *Caller) CheckLogin(uuid string) (*CheckLoginResponse, error) { + resp := NewReturnResponse(c.Client.CheckLogin(uuid)) + if resp.Err() != nil { + return nil, resp.Err() + } + defer resp.Body.Close() + data, err := resp.ReadAll() + if err != nil { + return nil, err + } + results := statusCodeRegexp.FindSubmatch(data) + if len(results) != 2 { + return nil, nil + } + code := string(results[1]) + return &CheckLoginResponse{Code: code, Raw: data}, nil +} + +func (c *Caller) GetLoginInfo(body []byte) (*LoginInfo, error) { + results := redirectUriRegexp.FindSubmatch(body) + if len(results) != 2 { + return nil, errors.New("redirect url does not match") + } + path := string(results[1]) + resp := NewReturnResponse(c.Client.GetLoginInfo(path)) + if resp.Err() != nil { + return nil, resp.Err() + } + defer resp.Body.Close() + var loginInfo LoginInfo + if err := resp.ScanXML(&loginInfo); err != nil { + return nil, err + } + return &loginInfo, nil +} + +func (c *Caller) WebInit(request BaseRequest) (*WebInitResponse, error) { + resp := NewReturnResponse(c.Client.WebInit(request)) + if resp.Err() != nil { + return nil, resp.Err() + } + var webInitResponse WebInitResponse + defer resp.Body.Close() + if err := resp.ScanJSON(&webInitResponse); err != nil { + return nil, err + } + return &webInitResponse, nil +} + +func (c *Caller) WebWxStatusNotify(request BaseRequest, response WebInitResponse, info LoginInfo) error { + resp := NewReturnResponse(c.Client.WebWxStatusNotify(request, response, info)) + if resp.Err() != nil { + return resp.Err() + } + var item struct{ BaseResponse BaseResponse } + defer resp.Body.Close() + if err := resp.ScanJSON(&item); err != nil { + return err + } + if !item.BaseResponse.Ok() { + return item.BaseResponse + } + return nil +} + +func (c *Caller) SyncCheck(info LoginInfo, response WebInitResponse) (*SyncCheckResponse, error) { + resp := NewReturnResponse(c.Client.SyncCheck(info, response)) + if resp.Err() != nil { + return nil, resp.Err() + } + defer resp.Body.Close() + data, err := resp.ReadAll() + fmt.Println(string(data)) + if err != nil { + return nil, err + } + results := syncCheckRegexp.FindSubmatch(data) + if len(results) != 3 { + return nil, errors.New("parse sync key failed") + } + retCode, selector := string(results[1]), string(results[2]) + syncCheckResponse := &SyncCheckResponse{RetCode: retCode, Selector: selector} + return syncCheckResponse, nil +} + +func (c *Caller) WebWxGetContact(info LoginInfo) (Members, error) { + resp := NewReturnResponse(c.Client.WebWxGetContact(info)) + if resp.Err() != nil { + return nil, resp.Err() + } + defer resp.Body.Close() + var item WebWxContactResponse + if err := resp.ScanJSON(&item); err != nil { + return nil, err + } + if !item.BaseResponse.Ok() { + return nil, item.BaseResponse + } + return item.MemberList, nil +} + +func (c *Caller) WebWxBatchGetContact(members Members, request BaseRequest) (Members, error) { + resp := NewReturnResponse(c.Client.WebWxBatchGetContact(members, request)) + if resp.Err() != nil { + return nil, resp.Err() + } + defer resp.Body.Close() + var item WebWxBatchContactResponse + if err := resp.ScanJSON(&item); err != nil { + return nil, err + } + if !item.BaseResponse.Ok() { + return nil, item.BaseResponse + } + return item.ContactList, nil +} + +func (c *Caller) WebWxSync(request BaseRequest, response WebInitResponse, info LoginInfo) (*WebWxSyncResponse, error) { + resp := NewReturnResponse(c.Client.WebWxSync(request, response, info)) + if resp.Err() != nil { + return nil, resp.Err() + } + defer resp.Body.Close() + var webWxSyncResponse WebWxSyncResponse + if err := resp.ScanJSON(&webWxSyncResponse); err != nil { + return nil, err + } + return &webWxSyncResponse, nil +} + +func (c *Caller) WebWxSendMsg(msg *SendMessage, info LoginInfo, request BaseRequest) error { + resp := NewReturnResponse(c.Client.WebWxSendMsg(msg, info, request)) + return parseBaseResponseError(resp) +} + +func (c *Caller) WebWxOplog(request BaseRequest, remarkName, toUserName string) error { + resp := NewReturnResponse(c.Client.WebWxOplog(request, remarkName, toUserName)) + return parseBaseResponseError(resp) +} + +func (c *Caller) WebWxSendImageMsg(file *os.File, request BaseRequest, info LoginInfo, fromUserName, toUserName string) error { + resp := NewReturnResponse(c.Client.WebWxUploadMedia(file, request, info, fromUserName, toUserName, "image/jpeg", "pic")) + if resp.Err() != nil { + return resp.Err() + } + defer resp.Body.Close() + var item struct { + BaseResponse BaseResponse + MediaId string + } + if err := resp.ScanJSON(&item); err != nil { + return err + } + if !item.BaseResponse.Ok() { + return item.BaseResponse + } + msg := NewMediaSendMessage(ImageMessage, fromUserName, toUserName, item.MediaId) + resp = NewReturnResponse(c.Client.WebWxSendMsgImg(msg, request, info)) + return parseBaseResponseError(resp) +} + +//func (c *Caller) WebWxBatchGetContact() { +// +//} + +func parseBaseResponseError(resp *ReturnResponse) error { + if resp.Err() != nil { + return resp.Err() + } + defer resp.Body.Close() + var item struct{ BaseResponse BaseResponse } + if err := resp.ScanJSON(&item); err != nil { + return err + } + if !item.BaseResponse.Ok() { + return item.BaseResponse + } + return nil +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..24eb42e --- /dev/null +++ b/client.go @@ -0,0 +1,328 @@ +package openwechat + +import ( + "bytes" + "crypto/md5" + "encoding/json" + "fmt" + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "strconv" + "strings" + "time" +) + +type Client struct { + *http.Client +} + +func NewClient(client *http.Client) *Client { + return &Client{Client: client} +} + +func DefaultClient() *Client { + jar, _ := cookiejar.New(nil) + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Jar: jar, + } + return NewClient(client) +} + +func (c *Client) GetLoginUUID() (*http.Response, error) { + path, _ := url.Parse(jsLoginUrl) + params := url.Values{} + params.Add("appid", appId) + params.Add("redirect_uri", webWxNewLoginPage) + params.Add("fun", "new") + params.Add("lang", "zh_CN") + params.Add("_", strconv.FormatInt(time.Now().Unix(), 10)) + path.RawQuery = params.Encode() + return c.Get(path.String()) +} + +func (c *Client) GetLoginQrcode(uuid string) (*http.Response, error) { + path := qrcodeUrl + uuid + return c.Get(path) +} + +func (c *Client) CheckLogin(uuid string) (*http.Response, error) { + path, _ := url.Parse(loginUrl) + now := time.Now().Unix() + params := url.Values{} + params.Add("r", strconv.FormatInt(now/1579, 10)) + params.Add("_", strconv.FormatInt(now, 10)) + params.Add("loginicon", "true") + params.Add("uuid", uuid) + params.Add("tip", "0") + path.RawQuery = params.Encode() + return c.Get(path.String()) +} + +func (c *Client) GetLoginInfo(path string) (*http.Response, error) { + return c.Get(path) +} + +func (c *Client) WebInit(request BaseRequest) (*http.Response, error) { + path, _ := url.Parse(webWxInitUrl) + params := url.Values{} + params.Add("_", fmt.Sprintf("%d", time.Now().Unix())) + path.RawQuery = params.Encode() + content := struct{ BaseRequest BaseRequest }{BaseRequest: request} + body, err := ToBuffer(content) + if err != nil { + return nil, err + } + return c.Post(path.String(), jsonContentType, body) +} + +func (c *Client) WebWxStatusNotify(request BaseRequest, response WebInitResponse, info LoginInfo) (*http.Response, error) { + path, _ := url.Parse(webWxStatusNotifyUrl) + params := url.Values{} + params.Add("lang", "zh_CN") + params.Add("pass_ticket", info.PassTicket) + username := response.User.UserName + content := map[string]interface{}{ + "BaseRequest": request, + "ClientMsgId": time.Now().Unix(), + "Code": 3, + "FromUserName": username, + "ToUserName": username, + } + path.RawQuery = params.Encode() + buffer, _ := ToBuffer(content) + req, _ := http.NewRequest(http.MethodPost, path.String(), buffer) + req.Header.Add("Content-Type", jsonContentType) + return c.Do(req) +} + +func (c *Client) SyncCheck(info LoginInfo, response WebInitResponse) (*http.Response, error) { + path, _ := url.Parse(syncCheckUrl) + params := url.Values{} + params.Add("r", strconv.FormatInt(time.Now().Unix(), 10)) + params.Add("skey", info.SKey) + params.Add("sid", info.WxSid) + params.Add("uin", strconv.Itoa(info.WxUin)) + params.Add("deviceid", GetRandomDeviceId()) + params.Add("_", strconv.FormatInt(time.Now().Unix(), 10)) + syncKeyStringSlice := make([]string, 0) + for _, item := range response.SyncKey.List { + i := fmt.Sprintf("%d_%d", item.Key, item.Val) + syncKeyStringSlice = append(syncKeyStringSlice, i) + } + syncKey := strings.Join(syncKeyStringSlice, "|") + params.Add("synckey", syncKey) + path.RawQuery = params.Encode() + req, _ := http.NewRequest(http.MethodGet, path.String(), nil) + req.Header.Add("User-Agent", "Mozilla/5.0") + return c.Do(req) +} + +func (c *Client) WebWxGetContact(info LoginInfo) (*http.Response, error) { + path, _ := url.Parse(webWxGetContactUrl) + params := url.Values{} + params.Add("r", strconv.FormatInt(time.Now().Unix(), 10)) + params.Add("skey", info.SKey) + params.Add("req", "0") + path.RawQuery = params.Encode() + return c.Get(path.String()) +} + +func (c *Client) WebWxBatchGetContact(members Members, request BaseRequest) (*http.Response, error) { + path, _ := url.Parse(webWxBatchGetContactUrl) + params := url.Values{} + params.Add("type", "ex") + params.Add("r", strconv.FormatInt(time.Now().Unix(), 10)) + path.RawQuery = params.Encode() + list := NewUserDetailItemList(members) + content := map[string]interface{}{ + "BaseRequest": request, + "Count": members.Count(), + "List": list, + } + body, _ := ToBuffer(content) + req, _ := http.NewRequest(http.MethodPost, path.String(), body) + req.Header.Add("Content-Type", jsonContentType) + return c.Do(req) +} + +func (c *Client) WebWxSync(request BaseRequest, response WebInitResponse, info LoginInfo) (*http.Response, error) { + path, _ := url.Parse(webWxSyncUrl) + params := url.Values{} + params.Add("sid", info.WxSid) + params.Add("skey", info.SKey) + params.Add("pass_ticket", info.PassTicket) + path.RawQuery = params.Encode() + content := map[string]interface{}{ + "BaseRequest": request, + "SyncKey": response.SyncKey, + "rr": strconv.FormatInt(time.Now().Unix(), 10), + } + data, _ := json.Marshal(content) + body := bytes.NewBuffer(data) + req, _ := http.NewRequest(http.MethodPost, path.String(), body) + req.Header.Add("Content-Type", jsonContentType) + return c.Do(req) +} + +func (c *Client) sendMessage(request BaseRequest, url string, msg *SendMessage) (*http.Response, error) { + content := map[string]interface{}{ + "BaseRequest": request, + "Msg": msg, + "Scene": 0, + } + body, _ := ToBuffer(content) + req, _ := http.NewRequest(http.MethodPost, url, body) + req.Header.Add("Content-Type", jsonContentType) + return c.Do(req) +} + +func (c *Client) WebWxSendMsg(msg *SendMessage, info LoginInfo, request BaseRequest) (*http.Response, error) { + msg.Type = TextMessage + path, _ := url.Parse(webWxSendMsgUrl) + params := url.Values{} + params.Add("lang", "zh_CN") + params.Add("pass_ticket", info.PassTicket) + path.RawQuery = params.Encode() + return c.sendMessage(request, path.String(), msg) +} + +func (c *Client) WebWxGetHeadImg(headImageUrl string) (*http.Response, error) { + path := baseUrl + headImageUrl + return c.Get(path) +} + +func (c *Client) WebWxUploadMedia(file *os.File, request BaseRequest, info LoginInfo, forUserName, toUserName, contentType, mediaType string) (*http.Response, error) { + path, _ := url.Parse(webWxUpLoadMediaUrl) + params := url.Values{} + params.Add("f", "json") + path.RawQuery = params.Encode() + sate, err := file.Stat() + if err != nil { + return nil, err + } + data, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + fileMd5 := fmt.Sprintf("%x", md5.Sum(data)) + cookies := c.Jar.Cookies(path) + 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": time.Now().Format(http.TimeFormat), + "size": sate.Size(), + "mediatype": mediaType, + "webwx_data_ticket": getWebWxDataTicket(cookies), + "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 + } + return c.Post(path.String(), ct, body) +} + +func (c *Client) WebWxSendMsgImg(msg *SendMessage, request BaseRequest, info LoginInfo) (*http.Response, error) { + msg.Type = ImageMessage + path, _ := url.Parse(webWxSendMsgImgUrl) + params := url.Values{} + params.Add("fun", "async") + params.Add("f", "json") + params.Add("lang", "zh_CN") + params.Add("pass_ticket", info.PassTicket) + path.RawQuery = params.Encode() + return c.sendMessage(request, path.String(), msg) +} + +func (c *Client) WebWxSendAppMsg(msg *SendMessage, request BaseRequest) (*http.Response, error) { + msg.Type = AppMessage + path, _ := url.Parse(webWxSendAppMsgUrl) + params := url.Values{} + params.Add("fun", "async") + params.Add("f", "json") + path.RawQuery = params.Encode() + return c.sendMessage(request, path.String(), msg) +} + +func (c *Client) WebWxOplog(request BaseRequest, remarkName, userName string, ) (*http.Response, error) { + path, _ := url.Parse(webWxOplogUrl) + params := url.Values{} + params.Add("lang", "zh_CN") + path.RawQuery = params.Encode() + content := map[string]interface{}{ + "BaseRequest": request, + "CmdId": 2, + "RemarkName": remarkName, + "UserName": userName, + } + body, _ := ToBuffer(content) + req, _ := http.NewRequest(http.MethodPost, path.String(), body) + req.Header.Add("Content-Type", jsonContentType) + return c.Do(req) +} + +func (c *Client) WebWxVerifyUser(storage WechatStorage, info RecommendInfo, verifyContent string) (*http.Response, error) { + loginInfo := storage.GetLoginInfo() + path, _ := url.Parse(webWxVerifyUserUrl) + params := url.Values{} + params.Add("r", strconv.FormatInt(time.Now().Unix(), 10)) + params.Add("lang", "zh_CN") + params.Add("pass_ticket", loginInfo.PassTicket) + path.RawQuery = params.Encode() + content := map[string]interface{}{ + "BaseRequest": storage.GetBaseRequest(), + "Opcode": 3, + "SceneList": []int{33}, + "SceneListCount": 1, + "VerifyContent": verifyContent, + "VerifyUserList": []interface{}{map[string]string{ + "Value": info.UserName, + "VerifyUserTicket": info.Ticket, + }}, + "VerifyUserListSize": 1, + "skey": loginInfo.SKey, + } + 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 new file mode 100644 index 0000000..ee6b5ef --- /dev/null +++ b/global.go @@ -0,0 +1,47 @@ +package openwechat + +import "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="(.*?)"`) +) + +const ( + appId = "wx782c26e4c19acffb" + + baseUrl = "https://wx2.qq.com" + jsLoginUrl = "https://login.wx.qq.com/jslogin" + webWxNewLoginPage = "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage" + qrcodeUrl = "https://login.weixin.qq.com/qrcode/" + loginUrl = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login" + webWxInitUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxinit" + webWxStatusNotifyUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxstatusnotify" + webWxSyncUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsync" + webWxSendMsgUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg" + webWxGetContactUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact" + webWxSendMsgImgUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsgimg" + webWxSendAppMsgUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxsendappmsg" + webWxBatchGetContactUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxbatchgetcontact" + webWxOplogUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxoplog" + webWxVerifyUserUrl = "https://wx2.qq.com/cgi-bin/mmwebwx-bin/webwxverifyuser" + syncCheckUrl = "https://webpush.wx2.qq.com/cgi-bin/mmwebwx-bin/synccheck" + webWxUpLoadMediaUrl = "https://file.wx2.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia" + + jsonContentType = "application/json; charset=utf-8" +) + +const ( + TextMessage = 1 + ImageMessage = 3 + AppMessage = 6 +) + +const ( + statusSuccess = "200" + statusScanned = "201" + statusTimeout = "400" + statusWait = "408" +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..af9e7c2 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module openwechat + +go 1.15 diff --git a/http.go b/http.go new file mode 100644 index 0000000..5cef057 --- /dev/null +++ b/http.go @@ -0,0 +1,44 @@ +package openwechat + +import ( + "encoding/json" + "encoding/xml" + "io/ioutil" + "net/http" +) + +type ReturnResponse struct { + *http.Response + err error +} + +func NewReturnResponse(response *http.Response, err error) *ReturnResponse { + return &ReturnResponse{Response: response, err: err} +} + +func (r *ReturnResponse) Err() error { + return r.err +} + +func (r *ReturnResponse) ScanJSON(v interface{}) error { + if data, err := r.ReadAll(); err != nil { + return err + } else { + return json.Unmarshal(data, v) + } +} + +func (r *ReturnResponse) ScanXML(v interface{}) error { + if data, err := r.ReadAll(); err != nil { + return err + } else { + return xml.Unmarshal(data, v) + } +} + +func (r *ReturnResponse) ReadAll() ([]byte, error) { + if r.Err() != nil { + return nil, r.Err() + } + return ioutil.ReadAll(r.Body) +} diff --git a/items.go b/items.go new file mode 100644 index 0000000..1c9e64c --- /dev/null +++ b/items.go @@ -0,0 +1,149 @@ +package openwechat + +import "fmt" + +type LoginInfo struct { + Ret int `xml:"ret"` + Message string `xml:"message"` + SKey string `xml:"skey"` + WxSid string `xml:"wxsid"` + WxUin int `xml:"wxuin"` + PassTicket string `xml:"pass_ticket"` + IsGrayScale int `xml:"isgrayscale"` +} + +type BaseRequest struct { + Uin int + Sid, Skey, DeviceID string +} + +type BaseResponse struct { + ErrMsg string + Ret int +} + +func (b BaseResponse) Ok() bool { + returnRet == 0 +} + +func (b BaseResponse) Error() string { + switch b.Ret { + 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 "op too often" + default: + if b.ErrMsg != "" { + return b.ErrMsg + } + return fmt.Sprintf("base response error code %d", b.Ret) + } +} + +type SyncKey struct { + Count int + List []struct{ Key, Val int64 } +} + +type WebInitResponse struct { + BaseResponse BaseResponse + Count int + ChatSet string + SKey string + SyncKey SyncKey + User User + ClientVersion int + SystemTime int64 + GrayScale int + InviteStartCount int + MPSubscribeMsgCount int + MPSubscribeMsgList []MPSubscribeMsg + ClickReportInterval int + ContactList []User +} + +type MPSubscribeMsg struct { + UserName string + Time int64 + NickName string + MPArticleCount int + MPArticleList []struct { + Title string + Cover string + Digest string + Url string + } +} + +type UserDetailItem struct { + UserName string + EncryChatRoomId string +} + +type UserDetailItemList []UserDetailItem + +func NewUserDetailItemList(members Members) UserDetailItemList { + list := make(UserDetailItemList, members.Count()-1) + 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 +} + +func (s *SyncCheckResponse) Success() bool { + return s.RetCode == "0" +} + +func (s *SyncCheckResponse) NorMal() bool { + return s.Success() && s.Selector == "0" +} + +type WebWxSyncResponse struct { + AddMsgCount int + AddMsgList []*Message + BaseResponse BaseResponse + ContinueFlag int + DelContactCount int + ModChatRoomMemberCount int + ModChatRoomMemberList Members + ModContactCount int + Skey string + SyncCheckKey SyncKey + SyncKey SyncKey +} + +type WebWxContactResponse struct { + BaseResponse BaseResponse + MemberCount int + MemberList []*User + Seq int +} + +type WebWxBatchContactResponse struct { + BaseResponse BaseResponse + ContactList []*User + Count int +} + +type CheckLoginResponse struct { + Code string + Raw []byte +} diff --git a/manager.go b/manager.go new file mode 100644 index 0000000..020f93f --- /dev/null +++ b/manager.go @@ -0,0 +1,249 @@ +package openwechat +// +//import ( +// "encoding/json" +// "encoding/xml" +// "errors" +// "fmt" +// "io/ioutil" +// "log" +//) +// +//var DefaultMessageMaxLength uint64 = 200 +// +//const ( +// statusSuccess = "200" +// statusScanned = "201" +// statusTimeout = "400" +// statusWait = "408" +//) +// +//type Bot struct { +// Caller *Caller +// Self *Self +// ScanCallback func(body []byte) +// LoginCallback func(body []byte) +// storage WechatStorage +// messageHandler MessageHandler +// notAlive bool +// err error +//} +// +//func (m *Bot) GetLoginUUID() (uuid string, err error) { +// return m.Caller.GetLoginUUID() +//} +// +//func (m *Bot) checkLogin(uuid string) (body []byte, err error) { +// resp, err := m.Client.CheckLogin(uuid) +// if err != nil { +// return nil, err +// } +// defer resp.Body.Close() +// data, err := ioutil.ReadAll(resp.Body) +// if +// +// err != nil { +// return nil, err +// } +// return data, nil +//} +// +//func (m *Bot) CheckLogin(uuid string) error { +// var ( +// body []byte +// err error +// ) +// for { +// body, err = m.checkLogin(uuid) +// if err != nil { +// return err +// } +// results := statusCodeRegexp.FindSubmatch(body) +// if len(results) != 2 { +// return errors.New("login status code does not match") +// } +// code := string(results[1]) +// switch code { +// case statusSuccess: +// return m.loginCallback(body) +// case statusScanned: +// if m.ScanCallback != nil { +// m.ScanCallback(body) +// } +// case statusWait: +// log.Println(string(body)) +// case statusTimeout: +// return errors.New("login time out") +// default: +// return errors.New("unknow code found " + code) +// } +// } +//} +// +//func (m *Bot) getLoginInfo(body []byte) error { +// resp, err := m.Client.GetLoginInfo(body) +// if err != nil { +// return err +// } +// defer resp.Body.Close() +// data, err := ioutil.ReadAll(resp.Body) +// if err != nil { +// return err +// } +// var loginInfo LoginInfo +// if err = xml.Unmarshal(data, &loginInfo); err != nil { +// return err +// } +// if loginInfo.Ret != 0 { +// return errors.New(loginInfo.Message) +// } +// m.storage.SetLoginInfo(loginInfo) +// return nil +//} +// +//func (m *Bot) webInit() error { +// loginInfo := m.storage.GetLoginInfo() +// baseRequest := BaseRequest{ +// Uin: loginInfo.WxUin, +// Sid: loginInfo.WxSid, +// Skey: loginInfo.SKey, +// DeviceID: GetRandomDeviceId(), +// } +// m.storage.SetBaseRequest(baseRequest) +// resp, err := m.Client.WebInit(m.storage) +// if err != nil { +// return err +// } +// defer resp.Body.Close() +// data, err := ioutil.ReadAll(resp.Body) +// if err != nil { +// return err +// } +// var webInitResponse WebInitResponse +// if err = json.Unmarshal(data, &webInitResponse); err != nil { +// return err +// } +// m.storage.SetWebInitResponse(webInitResponse) +// m.Self = &Self{User: &webInitResponse.User, Manager: m} +// return nil +//} +// +//func (m *Bot) WebWxStatusNotify() error { +// resp, err := m.Client.WebWxStatusNotify(m.storage) +// if err != nil { +// return err +// } +// defer resp.Body.Close() +// data, err := ioutil.ReadAll(resp.Body) +// if err != nil { +// return err +// } +// var item map[string]interface{} +// err = json.Unmarshal(data, &item) +// if err != nil { +// return err +// } +// if request, ok := item["BaseResponse"].(map[string]interface{}); ok { +// if ret, exist := request["Ret"]; exist { +// if ret, ok := ret.(float64); ok { +// if ret == 0 { +// return nil +// } +// } +// } +// } +// return errors.New("web status notify failed") +//} +// +//func (m *Bot) SyncCheck() error { +// for m.Alive() { +// resp, err := m.Client.SyncCheck(m.storage) +// if err != nil { +// return err +// } +// data, err := ioutil.ReadAll(resp.Body) +// fmt.Println(string(data)) +// resp.Body.Close() +// if err != nil { +// return err +// } +// results := syncCheckRegexp.FindSubmatch(data) +// if len(results) != 3 { +// return errors.New("parse sync key failed") +// } +// code, _ := results[1], results[2] +// switch string(code) { +// case "0": +// if err = m.getMessage(); err != nil { +// return err +// } +// case "1101": +// return errors.New("logout") +// } +// return fmt.Errorf("error ret code: %s", string(code)) +// } +// return nil +//} +// +//func (m *Bot) getMessage() error { +// resp, err := m.Client.GetMessage(m.storage) +// if err != nil { +// return err +// } +// defer resp.Body.Close() +// data, err := ioutil.ReadAll(resp.Body) +// if err != nil { +// return err +// } +// var syncKey struct{ SyncKey SyncKey } +// if err = json.Unmarshal(data, &syncKey); err != nil { +// return err +// } +// webInitResponse := m.storage.GetWebInitResponse() +// webInitResponse.SyncKey = syncKey.SyncKey +// m.storage.SetWebInitResponse(webInitResponse) +// var messageList MessageList +// if err = json.Unmarshal(data, &messageList); err != nil { +// return err +// } +// for _, message := range messageList.AddMsgList { +// message.ClientManager = m +// m.messageHandler.ReceiveMessage(message) +// } +// return nil +//} +// +//func (m *Bot) loginCallback(body []byte) error { +// var err error +// if m.LoginCallback != nil { +// m.LoginCallback(body) +// } +// if err = m.getLoginInfo(body); err != nil { +// return err +// } +// if err = m.webInit(); err != nil { +// return err +// } +// if err = m.WebWxStatusNotify(); err != nil { +// return err +// } +// go func() { +// if err := m.SyncCheck(); err != nil { +// m.exit(err) +// } +// }() +// return err +//} +// +//func (m *Bot) Alive() bool { +// return !m.notAlive +//} +// +//func (m *Bot) Err() error { +// return m.err +//} +// +//func (m *Bot) exit(err error) { +// m.notAlive = true +// m.err = err +//} diff --git a/message.go b/message.go new file mode 100644 index 0000000..e087919 --- /dev/null +++ b/message.go @@ -0,0 +1,213 @@ +package openwechat + +import ( + "errors" + "os" + "strings" + "time" +) + +type Message struct { + AppInfo struct { + AppID string + Type int + } + AppMsgType int + Content string + CreateTime int64 + EncryFileName string + FileName string + FileSize string + ForwardFlag int + FromUserName string + HasProductId int + ImgHeight int + ImgStatus int + ImgWidth int + MediaId string + MsgId string + MsgType int + NewMsgId int64 + OriContent string + PlayLength int64 + RecommendInfo RecommendInfo + Status int + StatusNotifyCode int + StatusNotifyUserName string + SubMsgType int + Ticket string + ToUserName string + Url string + VoiceLength int + Bot *Bot + senderInGroupUserName string +} + +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 + } + for _, member := range members { + if member.UserName == m.FromUserName { + return member.Detail() + } + } + return nil, errors.New("no such user found") +} + +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 + } + for _, member := range group.MemberList { + if m.senderInGroupUserName == member.UserName { + return member, nil + } + } + return nil, errors.New("no such user found") +} + +// +func (m *Message) IsSendBySelf() bool { + return m.FromUserName == m.Bot.self.User.UserName +} + +func (m *Message) IsSendByFriend() bool { + return !m.IsSendByGroup() && strings.HasPrefix(m.FromUserName, "@") +} + +func (m *Message) IsSendByGroup() bool { + return strings.HasPrefix(m.FromUserName, "@@") +} + +func (m *Message) Reply(msgType int, content, mediaId string) error { + msg := NewSendMessage(msgType, content, m.Bot.self.User.UserName, m.FromUserName, mediaId) + info := m.Bot.storage.GetLoginInfo() + request := m.Bot.storage.GetBaseRequest() + return m.Bot.Caller.WebWxSendMsg(msg, info, request) +} + +func (m *Message) ReplyText(content string) error { + return m.Reply(TextMessage, content, "") +} + +func (m *Message) ReplyImage(file *os.File) error { + info := m.Bot.storage.GetLoginInfo() + request := m.Bot.storage.GetBaseRequest() + 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 == "" +} + +func (m *Message) IsMap() bool { + return m.MsgType == 1 && m.Url != "" +} + +func (m *Message) IsPicture() bool { + return m.MsgType == 3 || m.MsgType == 47 +} + +func (m *Message) IsVoice() bool { + return m.MsgType == 34 +} + +func (m *Message) IsFriendAdd() bool { + return m.MsgType == 37 +} + +func (m *Message) IsCard() bool { + return m.MsgType == 42 +} + +func (m *Message) IsVideo() bool { + return m.MsgType == 43 || m.MsgType == 62 +} + +func (m *Message) IsSharing() bool { + return m.MsgType == 49 +} + +func (m *Message) IsRecalled() bool { + return m.MsgType == 10002 +} + +func (m *Message) IsSystem() bool { + return m.MsgType == 10000 +} + +//func (m Message) Agree() error { +// if !m.IsFriendAdd() { +// return fmt.Errorf("the excepted message type is 37, but got %d", m.MsgType) +// } +// m.ClientManager.Client.WebWxVerifyUser(m.ClientManager.storage, m.RecommendInfo, "") +//} + +type SendMessage struct { + Type int + Content string + FromUserName string + ToUserName string + LocalID int64 + ClientMsgId int64 + MediaId string +} + +func NewSendMessage(msgType int, content, fromUserName, toUserName, mediaId string) *SendMessage { + return &SendMessage{ + Type: msgType, + Content: content, + FromUserName: fromUserName, + ToUserName: toUserName, + LocalID: time.Now().Unix() * 1e4, + ClientMsgId: time.Now().Unix() * 1e4, + MediaId: mediaId, + } +} + +func NewTextSendMessage(content, fromUserName, toUserName string) *SendMessage { + return NewSendMessage(TextMessage, content, fromUserName, toUserName, "") +} + +func NewMediaSendMessage(msgType int, fromUserName, toUserName, mediaId string) *SendMessage { + return NewSendMessage(msgType, "", fromUserName, toUserName, mediaId) +} + +type RecommendInfo struct { + Alias string + AttrStatus int64 + City string + Content string + NickName string + OpCode int + Province string + QQNum int64 + Scene int + Sex int + Signature string + Ticket string + UserName string + VerifyFlag int +} + +func processMessage(message *Message, bot *Bot) { + message.Bot = bot + if message.IsSendByGroup() { + data := strings.Split(message.Content, ":
") + message.Content = strings.Join(data[1:], "") + message.senderInGroupUserName = data[0] + } +} diff --git a/message_handler.go b/message_handler.go new file mode 100644 index 0000000..03066af --- /dev/null +++ b/message_handler.go @@ -0,0 +1,17 @@ +package openwechat + +type MessageHandler func(message *Message) + +type MessageHandlerGroup struct { + handlers []MessageHandler +} + +func (m MessageHandlerGroup) ProcessMessage(message *Message) { + for _, handler := range m.handlers { + handler(message) + } +} + +func (m *MessageHandlerGroup) RegisterHandler(handler MessageHandler) { + m.handlers = append(m.handlers, handler) +} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..7ba61d3 --- /dev/null +++ b/parser.go @@ -0,0 +1,83 @@ +package openwechat + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "math/rand" + "net/http" + "strconv" + "time" +) + +func ToBuffer(v interface{}) (*bytes.Buffer, error) { + buf, err := json.Marshal(v) + if err != nil { + return nil, err + } + return bytes.NewBuffer(buf), nil +} + +func GetRandomDeviceId() string { + rand.Seed(time.Now().Unix()) + str := "" + for i := 0; i < 15; i++ { + r := rand.Intn(9) + str += strconv.Itoa(r) + } + return "e" + str +} + +//func getSendMessageError(body io.Reader) error { +// data, err := ioutil.ReadAll(body) +// if err != nil { +// return err +// } +// var item struct{ BaseResponse BaseResponse } +// if err = json.Unmarshal(data, &item); err != nil { +// return err +// } +// if !item.BaseResponse.Ok() { +// return errors.New(item.BaseResponse.ErrMsg) +// } +// return nil +//} + +func getWebWxDataTicket(cookies []*http.Cookie) string { + for _, cookie := range cookies { + if cookie.Name == "webwx_data_ticket" { + return cookie.Value + } + } + return "" +} + +func getUpdateMember(resp *http.Response, err error) (Members, error) { + if err != nil { + return nil, err + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + var item struct { + BaseResponse BaseResponse + ContactList Members + } + if err = json.Unmarshal(data, &item); err != nil { + return nil, err + } + if !item.BaseResponse.Ok() { + return nil, item.BaseResponse + } + return item.ContactList, nil +} + +func getResponseBody(resp *http.Response) ([]byte, error) { + if data, err := ioutil.ReadAll(resp.Body); err != nil { + return nil, err + } else { + return data, nil + } +} + diff --git a/relations.go b/relations.go new file mode 100644 index 0000000..e05eb9e --- /dev/null +++ b/relations.go @@ -0,0 +1,77 @@ +package openwechat + +import ( + "fmt" + "os" + "strings" +) + +type Friend struct{ *User } + +// implement fmt.Stringer +func (f Friend) String() string { + return fmt.Sprintf("", f.NickName) +} + +func (f *Friend) RemarkName(name string) error { + return f.remakeName(name) +} + +func (f *Friend) SendMsg(msg *SendMessage) error { + return f.sendMsg(msg) +} + +func (f *Friend) SendText(content string) error { + return f.sendText(content) +} + +func (f *Friend) SendImage(file *os.File) error { + return f.sendImage(file) +} + +type Friends []*Friend + +func (f Friends) Count() int { + return len(f) +} + +type Group struct{ *User } + +// implement fmt.Stringer +func (g Group) String() string { + return fmt.Sprintf("", g.NickName) +} + +func (g *Group) SendMsg(msg *SendMessage) error { + return g.sendMsg(msg) +} + +func (g *Group) SendText(content string) error { + return g.sendText(content) +} + +func (g *Group) SendImage(file *os.File) error { + return g.sendImage(file) +} + +func (g *Group) Members() (Members, error) { + group, err := g.Detail() + if err != nil { + return nil, err + } + return group.MemberList, nil +} + +type Groups []*Group + +func (g Groups) Count() int { + return len(g) +} + +func isFriend(user User) bool { + return !isGroup(user) && strings.HasPrefix(user.UserName, "@") && user.VerifyFlag == 0 +} + +func isGroup(user User) bool { + return strings.HasPrefix(user.UserName, "@@") && user.VerifyFlag == 0 +} diff --git a/stroage.go b/stroage.go new file mode 100644 index 0000000..bddcba1 --- /dev/null +++ b/stroage.go @@ -0,0 +1,45 @@ +package openwechat + +type WechatStorage interface { + SetLoginInfo(loginInfo LoginInfo) + SetBaseRequest(baseRequest BaseRequest) + SetWebInitResponse(webInitResponse WebInitResponse) + GetLoginInfo() LoginInfo + GetBaseRequest() BaseRequest + GetWebInitResponse() WebInitResponse +} + +// implement WechatStorage +type SimpleWechatStorage struct { + loginInfo LoginInfo + baseRequest BaseRequest + webInitResponse WebInitResponse +} + +func NewSimpleWechatStorage() *SimpleWechatStorage { + return &SimpleWechatStorage{} +} + +func (s *SimpleWechatStorage) SetLoginInfo(loginInfo LoginInfo) { + s.loginInfo = loginInfo +} + +func (s *SimpleWechatStorage) SetBaseRequest(baseRequest BaseRequest) { + s.baseRequest = baseRequest +} + +func (s *SimpleWechatStorage) SetWebInitResponse(webInitResponse WebInitResponse) { + s.webInitResponse = webInitResponse +} + +func (s *SimpleWechatStorage) GetLoginInfo() LoginInfo { + return s.loginInfo +} + +func (s *SimpleWechatStorage) GetBaseRequest() BaseRequest { + return s.baseRequest +} + +func (s *SimpleWechatStorage) GetWebInitResponse() WebInitResponse { + return s.webInitResponse +} diff --git a/user.go b/user.go new file mode 100644 index 0000000..b523c0f --- /dev/null +++ b/user.go @@ -0,0 +1,278 @@ +package openwechat + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" +) + +type User struct { + Uin int + HideInputBarFlag int + StarFriend int + Sex int + AppAccountFlag int + VerifyFlag int + ContactFlag int + WebWxPluginSwitch int + HeadImgFlag int + SnsFlag int + UserName string + NickName string + HeadImgUrl string + RemarkName string + PYInitial string + PYQuanPin string + RemarkPYInitial string + RemarkPYQuanPin string + Signature string + MemberCount int + MemberList []*User + OwnerUin int + Statues int + AttrStatus int + Province string + City string + Alias string + UniFriend int + DisplayName string + ChatRoomId int + KeyWord string + EncryChatRoomId string + IsOwner int + + Self *Self +} + +// implement fmt.Stringer +func (u *User) String() string { + return fmt.Sprintf("", u.NickName) +} + +// +func (u *User) GetAvatarResponse() (*http.Response, error) { + 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() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return ioutil.WriteFile(filename, data, os.ModePerm) +} + +func (u *User) sendMsg(msg *SendMessage) error { + msg.FromUserName = u.Self.UserName + msg.ToUserName = u.UserName + info := u.Self.Bot.storage.GetLoginInfo() + request := u.Self.Bot.storage.GetBaseRequest() + return u.Self.Bot.Caller.WebWxSendMsg(msg, info, request) +} + +func (u *User) sendText(content string) error { + msg := NewTextSendMessage(content, u.Self.UserName, u.UserName) + return u.sendMsg(msg) +} + +func (u *User) sendImage(file *os.File) error { + request := u.Self.Bot.storage.GetBaseRequest() + info := u.Self.Bot.storage.GetLoginInfo() + return u.Self.Bot.Caller.WebWxSendImageMsg(file, request, info, u.Self.UserName, u.UserName) +} + +func (u *User) remakeName(remarkName string) error { + request := u.Self.Bot.storage.GetBaseRequest() + return u.Self.Bot.Caller.WebWxOplog(request, remarkName, u.UserName) +} + +func (u *User) Detail() (*User, error) { + members := Members{u} + request := u.Self.Bot.storage.GetBaseRequest() + newMembers, err := u.Self.Bot.Caller.WebWxBatchGetContact(members, request) + if err != nil { + return nil, err + } + user := newMembers[0] + user.Self = u.Self + return user, nil +} + +type Self struct { + *User + Bot *Bot + fileHelper *Friend + members Members + friends Friends + groups Groups +} + +func (s *Self) Members(update ...bool) (Members, error) { + if s.members == nil { + if err := s.updateMembers(); err != nil { + return nil, err + } else { + return s.members, nil + } + } + var isUpdate bool + if len(update) > 0 { + isUpdate = update[len(update)-1] + } + if isUpdate { + if err := s.updateMembers(); err != nil { + return nil, err + } + } + return s.members, nil +} + +func (s *Self) updateMembers() error { + info := s.Bot.storage.GetLoginInfo() + members, err := s.Bot.Caller.WebWxGetContact(info) + if err != nil { + return err + } + members.SetOwner(s) + s.members = members + return nil +} + +func (s *Self) FileHelper() (*Friend, error) { + if s.fileHelper != nil { + return s.fileHelper, nil + } + members, err := s.Members() + if err != nil { + return nil, err + } + for _, member := range members { + if member.UserName == "filehelper" { + fileHelper := &Friend{member} + s.fileHelper = fileHelper + return s.fileHelper, nil + } + } + return nil, errors.New("filehelper does not exist") +} + +func (s *Self) Friends(update ...bool) (Friends, error) { + if s.friends == nil { + if err := s.updateFriends(update...); err != nil { + return nil, err + } + } + return s.friends, nil +} + +func (s *Self) Groups(update ...bool) (Groups, error) { + if s.groups == nil { + if err := s.updateGroups(update...); err != nil { + return nil, err + } + } + return s.groups, nil +} + +func (s *Self) updateFriends(update ...bool) error { + var isUpdate bool + if len(update) > 0 { + isUpdate = update[len(update)-1] + } + if isUpdate || s.members == nil { + if err := s.updateMembers(); err != nil { + return err + } + } + friends := make(Friends, 0) + for _, member := range s.members { + if isFriend(*member) { + friend := &Friend{member} + friends = append(friends, friend) + } + } + s.friends = friends + return nil +} + +func (s *Self) updateGroups(update ...bool) error { + var isUpdate bool + if len(update) > 0 { + isUpdate = update[len(update)-1] + } + if isUpdate || s.members == nil { + if err := s.updateMembers(); err != nil { + return err + } + } + groups := make(Groups, 0) + for _, member := range s.members { + if isGroup(*member) { + group := &Group{member} + groups = append(groups, group) + } + } + s.groups = groups + return nil +} + +func (s *Self) UpdateMembersDetail() error { + members, err := s.Members() + if err != nil { + return err + } + count := members.Count() + var times int + if count < 50 { + times = 1 + } else { + times = count / 50 + } + newMembers := make(Members, 0) + request := s.Self.Bot.storage.GetBaseRequest() + var pMembers Members + for i := 0; i < times; i++ { + if times == 1 { + pMembers = members + } else { + pMembers = members[i*50 : (i+1)*times] + } + nMembers, err := s.Self.Bot.Caller.WebWxBatchGetContact(pMembers, request) + if err != nil { + return err + } + newMembers = append(newMembers, nMembers...) + } + total := times * 50 + if total < count { + left := total - count + pMembers = members[total : total+left] + nMembers, err := s.Self.Bot.Caller.WebWxBatchGetContact(pMembers, request) + if err != nil { + return err + } + newMembers = append(newMembers, nMembers...) + } + newMembers.SetOwner(s) + s.members = newMembers + return nil +} + +type Members []*User + +func (m Members) Count() int { + return len(m) +} + +func (m Members) SetOwner(s *Self) { + for _, member := range m { + member.Self = s + } +}