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
+ }
+}