From 0f1d42f6f1ecd0624579f1f7e84f1623359a970d Mon Sep 17 00:00:00 2001
From: Ivy1996-encode <2682904957@qq.com>
Date: Wed, 17 Feb 2021 13:33:20 +0800
Subject: [PATCH] first commit
---
bot.go | 177 ++++++++++++++++++++++++
bot_test.go | 26 ++++
caller.go | 216 +++++++++++++++++++++++++++++
client.go | 328 +++++++++++++++++++++++++++++++++++++++++++++
global.go | 47 +++++++
go.mod | 3 +
http.go | 44 ++++++
items.go | 149 ++++++++++++++++++++
manager.go | 249 ++++++++++++++++++++++++++++++++++
message.go | 213 +++++++++++++++++++++++++++++
message_handler.go | 17 +++
parser.go | 83 ++++++++++++
relations.go | 77 +++++++++++
stroage.go | 45 +++++++
user.go | 278 ++++++++++++++++++++++++++++++++++++++
15 files changed, 1952 insertions(+)
create mode 100644 bot.go
create mode 100644 bot_test.go
create mode 100644 caller.go
create mode 100644 client.go
create mode 100644 global.go
create mode 100644 go.mod
create mode 100644 http.go
create mode 100644 items.go
create mode 100644 manager.go
create mode 100644 message.go
create mode 100644 message_handler.go
create mode 100644 parser.go
create mode 100644 relations.go
create mode 100644 stroage.go
create mode 100644 user.go
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
+ }
+}