From 3e14a0d5ed363b730e2de12718cf7eeca7382f28 Mon Sep 17 00:00:00 2001 From: eatMoreApple <15055461510@163.com> Date: Wed, 4 Aug 2021 15:39:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=8E=B7=E5=8F=96=E7=83=AD?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=AF=B9=E8=B1=A1=E7=9A=84=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.go | 499 ++++++++++++++++++++++++++--------------------------- stroage.go | 77 +++++---- 2 files changed, 295 insertions(+), 281 deletions(-) diff --git a/bot.go b/bot.go index 4ac7ac2..1276649 100644 --- a/bot.go +++ b/bot.go @@ -1,44 +1,43 @@ package openwechat import ( - "bytes" - "context" - "encoding/json" - "errors" - "log" - "net/url" - "sync" + "context" + "encoding/json" + "errors" + "log" + "net/url" + "sync" ) type Bot struct { - ScanCallBack func(body []byte) // 扫码回调,可获取扫码用户的头像 - LoginCallBack func(body []byte) // 登陆回调 - LogoutCallBack func(bot *Bot) // 退出回调 - UUIDCallback func(uuid string) // 获取UUID的回调函数 - MessageHandler MessageHandler // 获取消息成功的handle - GetMessageErrorHandler func(err error) // 获取消息发生错误的handle - IsHot bool // 是否为热登录模式 - once sync.Once - err error - context context.Context - cancel context.CancelFunc - Caller *Caller - self *Self - Storage *Storage - HotReloadStorage HotReloadStorage + ScanCallBack func(body []byte) // 扫码回调,可获取扫码用户的头像 + LoginCallBack func(body []byte) // 登陆回调 + LogoutCallBack func(bot *Bot) // 退出回调 + UUIDCallback func(uuid string) // 获取UUID的回调函数 + MessageHandler MessageHandler // 获取消息成功的handle + GetMessageErrorHandler func(err error) // 获取消息发生错误的handle + IsHot bool // 是否为热登录模式 + once sync.Once + err error + context context.Context + cancel context.CancelFunc + Caller *Caller + self *Self + Storage *Storage + HotReloadStorage HotReloadStorage } // Alive 判断当前用户是否正常在线 func (b *Bot) Alive() bool { - if b.self == nil { - return false - } - select { - case <-b.context.Done(): - return false - default: - return true - } + if b.self == nil { + return false + } + select { + case <-b.context.Done(): + return false + default: + return true + } } // GetCurrentUser 获取当前的用户 @@ -48,10 +47,10 @@ func (b *Bot) Alive() bool { // } // fmt.Println(self.NickName) func (b *Bot) GetCurrentUser() (*Self, error) { - if b.self == nil { - return nil, errors.New("user not login") - } - return b.self, nil + if b.self == nil { + return nil, errors.New("user not login") + } + return b.self, nil } // HotLogin 热登录,可实现重复登录, @@ -60,318 +59,314 @@ func (b *Bot) GetCurrentUser() (*Self, error) { // err := bot.HotLogin(Storage, true) // fmt.Println(err) func (b *Bot) HotLogin(storage HotReloadStorage, retry ...bool) error { - b.IsHot = true - b.HotReloadStorage = storage + b.IsHot = true + b.HotReloadStorage = storage - var err error + var err error - // 如果load出错了,就执行正常登陆逻辑 - // 第一次没有数据load都会出错的 - var buffer bytes.Buffer - if _, err := buffer.ReadFrom(storage); err != nil { - return b.Login() - } - defer b.HotReloadStorage.Close() + // 如果load出错了,就执行正常登陆逻辑 + // 第一次没有数据load都会出错的 + item, err := NewHotReloadStorageItem(storage) - var item HotReloadStorageItem + if err != nil { + return b.Login() + } - if err = json.NewDecoder(&buffer).Decode(&item); err != nil { - return err - } + defer storage.Close() - if err = b.hotLoginInit(item); err != nil { - return err - } + if err = b.hotLoginInit(*item); err != nil { + return err + } - // 如果webInit出错,则说明可能身份信息已经失效 - // 如果retry为True的话,则进行正常登陆 - if err = b.WebInit(); err != nil { - if len(retry) > 0 && retry[0] { - return b.Login() - } - } - return err + // 如果webInit出错,则说明可能身份信息已经失效 + // 如果retry为True的话,则进行正常登陆 + if err = b.WebInit(); err != nil { + if len(retry) > 0 && retry[0] { + return b.Login() + } + } + return err } // 热登陆初始化 func (b *Bot) hotLoginInit(item HotReloadStorageItem) error { - cookies := item.Cookies - for u, ck := range cookies { - path, err := url.Parse(u) - if err != nil { - return err - } - b.Caller.Client.Jar.SetCookies(path, ck) - } - b.Storage.LoginInfo = item.LoginInfo - b.Storage.Request = item.BaseRequest - b.Caller.Client.Domain = item.WechatDomain - return nil + cookies := item.Cookies + for u, ck := range cookies { + path, err := url.Parse(u) + if err != nil { + return err + } + b.Caller.Client.Jar.SetCookies(path, ck) + } + b.Storage.LoginInfo = item.LoginInfo + b.Storage.Request = item.BaseRequest + b.Caller.Client.Domain = item.WechatDomain + return nil } // Login 用户登录 // 该方法会一直阻塞,直到用户扫码登录,或者二维码过期 func (b *Bot) Login() error { - 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: - // 判断是否有登录回调,如果有执行它 - if b.LoginCallBack != nil { - b.LoginCallBack(resp.Raw) - } - return b.HandleLogin(resp.Raw) - case StatusScanned: - // 执行扫码回调 - if b.ScanCallBack != nil { - b.ScanCallBack(resp.Raw) - } - case StatusTimeout: - return ErrLoginTimeout - case StatusWait: - continue - } - } + 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: + // 判断是否有登录回调,如果有执行它 + if b.LoginCallBack != nil { + b.LoginCallBack(resp.Raw) + } + return b.HandleLogin(resp.Raw) + case StatusScanned: + // 执行扫码回调 + if b.ScanCallBack != nil { + b.ScanCallBack(resp.Raw) + } + case StatusTimeout: + return ErrLoginTimeout + case StatusWait: + continue + } + } } // Logout 用户退出 func (b *Bot) Logout() error { - if b.Alive() { - if b.LogoutCallBack != nil { - b.LogoutCallBack(b) - } - info := b.Storage.LoginInfo - if err := b.Caller.Logout(info); err != nil { - return err - } - b.stopAsyncCALL(errors.New("logout")) - return nil - } - return errors.New("user not login") + if b.Alive() { + if b.LogoutCallBack != nil { + b.LogoutCallBack(b) + } + info := b.Storage.LoginInfo + if err := b.Caller.Logout(info); err != nil { + return err + } + b.stopAsyncCALL(errors.New("logout")) + return nil + } + return errors.New("user not login") } // HandleLogin 登录逻辑 func (b *Bot) HandleLogin(data []byte) error { - // 获取登录的一些基本的信息 - info, err := b.Caller.GetLoginInfo(data) - if err != nil { - return err - } - // 将LoginInfo存到storage里面 - b.Storage.LoginInfo = info + // 获取登录的一些基本的信息 + info, err := b.Caller.GetLoginInfo(data) + if err != nil { + return err + } + // 将LoginInfo存到storage里面 + b.Storage.LoginInfo = info - // 构建BaseRequest - request := &BaseRequest{ - Uin: info.WxUin, - Sid: info.WxSid, - Skey: info.SKey, - DeviceID: GetRandomDeviceId(), - } + // 构建BaseRequest + request := &BaseRequest{ + Uin: info.WxUin, + Sid: info.WxSid, + Skey: info.SKey, + DeviceID: GetRandomDeviceId(), + } - // 将BaseRequest存到storage里面方便后续调用 - b.Storage.Request = request + // 将BaseRequest存到storage里面方便后续调用 + b.Storage.Request = request - // 如果是热登陆,则将当前的重要信息写入hotReloadStorage - if b.IsHot { - if err = b.DumpHotReloadStorage(); err != nil { - return err - } - } + // 如果是热登陆,则将当前的重要信息写入hotReloadStorage + if b.IsHot { + if err = b.DumpHotReloadStorage(); err != nil { + return err + } + } - return b.WebInit() + return b.WebInit() } // WebInit 根据有效凭证获取和初始化用户信息 func (b *Bot) WebInit() error { - req := b.Storage.Request - info := b.Storage.LoginInfo - // 获取初始化的用户信息和一些必要的参数 - resp, err := b.Caller.WebInit(req) - if err != nil { - return err - } - // 设置当前的用户 - b.self = &Self{Bot: b, User: &resp.User} - b.self.Self = b.self - b.Storage.Response = resp + req := b.Storage.Request + info := b.Storage.LoginInfo + // 获取初始化的用户信息和一些必要的参数 + resp, err := b.Caller.WebInit(req) + if err != nil { + return err + } + // 设置当前的用户 + b.self = &Self{Bot: b, User: &resp.User} + b.self.Self = b.self + b.Storage.Response = resp - // 通知手机客户端已经登录 - if err = b.Caller.WebWxStatusNotify(req, resp, info); err != nil { - return err - } - // 开启协程,轮询获取是否有新的消息返回 + // 通知手机客户端已经登录 + if err = b.Caller.WebWxStatusNotify(req, resp, info); err != nil { + return err + } + // 开启协程,轮询获取是否有新的消息返回 - // FIX: 当bot在线的情况下执行热登录,会开启多次事件监听 - go b.once.Do(func() { - if b.GetMessageErrorHandler == nil { - b.GetMessageErrorHandler = b.stopAsyncCALL - } - if err = b.asyncCall(); err != nil { - b.GetMessageErrorHandler(err) - } - }) - return nil + // FIX: 当bot在线的情况下执行热登录,会开启多次事件监听 + go b.once.Do(func() { + if b.GetMessageErrorHandler == nil { + b.GetMessageErrorHandler = b.stopAsyncCALL + } + if err = b.asyncCall(); err != nil { + b.GetMessageErrorHandler(err) + } + }) + return nil } // 轮询请求 // 根据状态码判断是否有新的请求 func (b *Bot) asyncCall() error { - var ( - err error - resp *SyncCheckResponse - ) - for b.Alive() { - // 长轮询检查是否有消息返回 - resp, err = b.Caller.SyncCheck(b.Storage.LoginInfo, b.Storage.Response) - if err != nil { - return err - } - // 如果不是正常的状态码返回,发生了错误,直接退出 - if !resp.Success() { - return resp - } - // 如果Selector不为0,则获取消息 - if !resp.NorMal() { - if err = b.getNewWechatMessage(); err != nil { - return err - } - } - } - return err + var ( + err error + resp *SyncCheckResponse + ) + for b.Alive() { + // 长轮询检查是否有消息返回 + resp, err = b.Caller.SyncCheck(b.Storage.LoginInfo, b.Storage.Response) + if err != nil { + return err + } + // 如果不是正常的状态码返回,发生了错误,直接退出 + if !resp.Success() { + return resp + } + // 如果Selector不为0,则获取消息 + if !resp.NorMal() { + if err = b.getNewWechatMessage(); err != nil { + return err + } + } + } + return err } // 当获取消息发生错误时, 默认的错误处理行为 func (b *Bot) stopAsyncCALL(err error) { - b.cancel() - b.err = err - b.self = nil - log.Printf("exit with : %s", err.Error()) + b.cancel() + b.err = err + b.self = nil + log.Printf("exit with : %s", err.Error()) } // 获取新的消息 func (b *Bot) getNewWechatMessage() error { - resp, err := b.Caller.WebWxSync(b.Storage.Request, b.Storage.Response, b.Storage.LoginInfo) - if err != nil { - return err - } - // 更新SyncKey并且重新存入storage - b.Storage.Response.SyncKey = resp.SyncKey - // 遍历所有的新的消息,依次处理 - for _, message := range resp.AddMsgList { - // 根据不同的消息类型来进行处理,方便后续统一调用 - message.init(b) - // 调用自定义的处理方法 - if handler := b.MessageHandler; handler != nil { - handler(message) - } - } - return nil + resp, err := b.Caller.WebWxSync(b.Storage.Request, b.Storage.Response, b.Storage.LoginInfo) + if err != nil { + return err + } + // 更新SyncKey并且重新存入storage + b.Storage.Response.SyncKey = resp.SyncKey + // 遍历所有的新的消息,依次处理 + for _, message := range resp.AddMsgList { + // 根据不同的消息类型来进行处理,方便后续统一调用 + message.init(b) + // 调用自定义的处理方法 + if handler := b.MessageHandler; handler != nil { + handler(message) + } + } + return nil } // Block 当消息同步发生了错误或者用户主动在手机上退出,该方法会立即返回,否则会一直阻塞 func (b *Bot) Block() error { - if b.self == nil { - return errors.New("`Block` must be called after user login") - } - <-b.context.Done() - return nil + if b.self == nil { + return errors.New("`Block` must be called after user login") + } + <-b.context.Done() + return nil } // CrashReason 获取当前Bot崩溃的原因 func (b *Bot) CrashReason() error { - return b.err + return b.err } // MessageOnSuccess setter for Bot.MessageHandler func (b *Bot) MessageOnSuccess(h func(msg *Message)) { - b.MessageHandler = h + b.MessageHandler = h } // MessageOnError setter for Bot.GetMessageErrorHandler func (b *Bot) MessageOnError(h func(err error)) { - b.GetMessageErrorHandler = h + b.GetMessageErrorHandler = h } // DumpHotReloadStorage 写入HotReloadStorage func (b *Bot) DumpHotReloadStorage() error { - if b.HotReloadStorage == nil { - return errors.New("HotReloadStorage can not be nil") - } - cookies := b.Caller.Client.GetCookieMap() - item := HotReloadStorageItem{ - BaseRequest: b.Storage.Request, - Cookies: cookies, - LoginInfo: b.Storage.LoginInfo, - WechatDomain: b.Caller.Client.Domain, - } + if b.HotReloadStorage == nil { + return errors.New("HotReloadStorage can not be nil") + } + cookies := b.Caller.Client.GetCookieMap() + item := HotReloadStorageItem{ + BaseRequest: b.Storage.Request, + Cookies: cookies, + LoginInfo: b.Storage.LoginInfo, + WechatDomain: b.Caller.Client.Domain, + } - data, err := json.Marshal(item) - if err != nil { - return err - } - if _, err = b.HotReloadStorage.Write(data); err != nil { - return err - } - return b.HotReloadStorage.Close() + data, err := json.Marshal(item) + if err != nil { + return err + } + if _, err = b.HotReloadStorage.Write(data); err != nil { + return err + } + return b.HotReloadStorage.Close() } // OnLogin is a setter for LoginCallBack func (b *Bot) OnLogin(f func(body []byte)) { - b.LoginCallBack = f + b.LoginCallBack = f } // OnScanned is a setter for ScanCallBack func (b *Bot) OnScanned(f func(body []byte)) { - b.ScanCallBack = f + b.ScanCallBack = f } // OnLogout is a setter for LogoutCallBack func (b *Bot) OnLogout(f func(bot *Bot)) { - b.LogoutCallBack = f + b.LogoutCallBack = f } // NewBot Bot的构造方法,需要自己传入Caller func NewBot(caller *Caller) *Bot { - ctx, cancel := context.WithCancel(context.Background()) - return &Bot{Caller: caller, Storage: &Storage{}, context: ctx, cancel: cancel} + ctx, cancel := context.WithCancel(context.Background()) + return &Bot{Caller: caller, Storage: &Storage{}, context: ctx, cancel: cancel} } // DefaultBot 默认的Bot的构造方法, // mode不传入默认为openwechat.Normal,详情见mode // bot := openwechat.DefaultBot(openwechat.Desktop) func DefaultBot(modes ...mode) *Bot { - var m mode - if len(modes) == 0 { - m = Normal - } else { - m = modes[0] - } - caller := DefaultCaller() - caller.Client.mode = m - bot := NewBot(caller) - bot.UUIDCallback = PrintlnQrcodeUrl - return bot + var m mode + if len(modes) == 0 { + m = Normal + } else { + m = modes[0] + } + caller := DefaultCaller() + caller.Client.mode = m + bot := NewBot(caller) + bot.UUIDCallback = PrintlnQrcodeUrl + return bot } // GetQrcodeUrl 通过uuid获取登录二维码的url func GetQrcodeUrl(uuid string) string { - return qrcode + uuid + return qrcode + uuid } // PrintlnQrcodeUrl 打印登录二维码 func PrintlnQrcodeUrl(uuid string) { - println("访问下面网址扫描二维码登录") - println(GetQrcodeUrl(uuid)) + println("访问下面网址扫描二维码登录") + println(GetQrcodeUrl(uuid)) } diff --git a/stroage.go b/stroage.go index e59e534..424e16d 100644 --- a/stroage.go +++ b/stroage.go @@ -1,23 +1,26 @@ package openwechat import ( - "io" - "net/http" - "os" + "bytes" + "encoding/json" + "errors" + "io" + "net/http" + "os" ) // Storage 身份信息, 维持整个登陆的Session会话 type Storage struct { - LoginInfo *LoginInfo - Request *BaseRequest - Response *WebInitResponse + LoginInfo *LoginInfo + Request *BaseRequest + Response *WebInitResponse } type HotReloadStorageItem struct { - Cookies map[string][]*http.Cookie - BaseRequest *BaseRequest - LoginInfo *LoginInfo - WechatDomain WechatDomain + Cookies map[string][]*http.Cookie + BaseRequest *BaseRequest + LoginInfo *LoginInfo + WechatDomain WechatDomain } // HotReloadStorage 热登陆存储接口 @@ -26,37 +29,53 @@ type HotReloadStorage io.ReadWriteCloser // JsonFileHotReloadStorage 实现HotReloadStorage接口 // 默认以json文件的形式存储 type JsonFileHotReloadStorage struct { - FileName string - file *os.File + FileName string + file *os.File } func (j *JsonFileHotReloadStorage) Read(p []byte) (n int, err error) { - if j.file == nil { - j.file, err = os.Open(j.FileName) - if err != nil { - return 0, err - } - } - return j.file.Read(p) + if j.file == nil { + j.file, err = os.Open(j.FileName) + if err != nil { + return 0, err + } + } + return j.file.Read(p) } func (j *JsonFileHotReloadStorage) Write(p []byte) (n int, err error) { - j.file, err = os.Create(j.FileName) - if err != nil { - return 0, err - } - return j.file.Write(p) + j.file, err = os.Create(j.FileName) + if err != nil { + return 0, err + } + return j.file.Write(p) } func (j *JsonFileHotReloadStorage) Close() error { - if j.file != nil { - return j.file.Close() - } - return nil + if j.file != nil { + return j.file.Close() + } + return nil } func NewJsonFileHotReloadStorage(filename string) HotReloadStorage { - return &JsonFileHotReloadStorage{FileName: filename} + return &JsonFileHotReloadStorage{FileName: filename} } var _ HotReloadStorage = &JsonFileHotReloadStorage{} + +func NewHotReloadStorageItem(storage HotReloadStorage) (*HotReloadStorageItem, error) { + if storage == nil { + return nil, errors.New("storage can't be nil") + } + var buffer bytes.Buffer + if _, err := buffer.ReadFrom(storage); err != nil { + return nil, err + } + var item HotReloadStorageItem + + if err := json.NewDecoder(&buffer).Decode(&item); err != nil { + return nil, err + } + return &item, nil +}