diff --git a/bot.go b/bot.go index 41f06a3..3980c82 100644 --- a/bot.go +++ b/bot.go @@ -66,102 +66,43 @@ func (b *Bot) GetCurrentUser() (*Self, error) { return b.self, nil } -// HotLogin 热登录,可实现重复登录, -// retry设置为true可在热登录失效后进行普通登录行为 -// -// Storage := NewJsonFileHotReloadStorage("Storage.json") -// err := bot.HotLogin(Storage, true) -// fmt.Println(err) -func (b *Bot) HotLogin(storage HotReloadStorage, retries ...bool) error { - err := b.hotLogin(storage) - // 判断是否为需要重新登录 - if errors.Is(err, ErrInvalidStorage) { - return b.Login() - } - if err != nil { - if len(retries) > 0 && retries[0] { - retErr, ok := err.(Ret) - if !ok { - return err - } - // TODO add more error code handle here - switch retErr { - case cookieInvalid: - return b.Login() - } - return err - } - } - return err -} - -func (b *Bot) hotLogin(storage HotReloadStorage) error { - b.hotReloadStorage = storage - var item HotReloadStorageItem - err := json.NewDecoder(storage).Decode(&item) - if err != nil { - return err - } - if err = b.hotLoginInit(&item); err != nil { - return err - } - return b.WebInit() -} - -// 热登陆初始化 -func (b *Bot) hotLoginInit(item *HotReloadStorageItem) error { - b.Caller.Client.Jar = item.Jar.AsCookieJar() - b.Storage.LoginInfo = item.LoginInfo - b.Storage.Request = item.BaseRequest - b.Caller.Client.Domain = item.WechatDomain - b.uuid = item.UUID - return nil +func (b *Bot) login(login BotLogin) error { + return login.Login(b) } // Login 用户登录 func (b *Bot) Login() error { - uuid, err := b.Caller.GetLoginUUID() - if err != nil { - return err - } - return b.LoginWithUUID(uuid) + scanLogin := &SacnLogin{} + return b.login(scanLogin) } -// LoginWithUUID 用户登录 -// 该方法会一直阻塞,直到用户扫码登录,或者二维码过期 -func (b *Bot) LoginWithUUID(uuid string) error { - b.uuid = uuid - // 二维码获取回调 - if b.UUIDCallback != nil { - b.UUIDCallback(uuid) +// HotLogin 热登录,可实现重复登录, +// retry设置为true可在热登录失效后进行普通登录行为 +// +// Storage := NewJsonFileHotReloadStorage("Storage.json") +// err := bot.HotLogin(Storage, true) +// fmt.Println(err) +func (b *Bot) HotLogin(storage HotReloadStorage, opts ...HotLoginOptionFunc) error { + hotLogin := &HotLogin{storage: storage} + for _, opt := range opts { + opt(&hotLogin.opt) } - for { - // 长轮询检查是否扫码登录 - resp, err := b.Caller.CheckLogin(uuid) - if err != nil { - return err - } - switch resp.Code { - case StatusSuccess: - // 判断是否有登录回调,如果有执行它 - if err = b.HandleLogin(resp.Raw); err != nil { - return err - } - if b.LoginCallBack != nil { - b.LoginCallBack(resp.Raw) - } - return nil - case StatusScanned: - // 执行扫码回调 - if b.ScanCallBack != nil { - b.ScanCallBack(resp.Raw) - } - case StatusTimeout: - return ErrLoginTimeout - case StatusWait: - continue - } + return b.login(hotLogin) +} + +// PushLogin 免扫码登录 +// 免扫码登录需要先扫码登录一次才可以进行扫码登录 +// 扫码登录成功后需要利用微信号发送一条消息,然后在手机上进行主动退出。 +// 这时候在进行一次 PushLogin 即可。 +func (b *Bot) PushLogin(storage HotReloadStorage, opts ...PushLoginOptionFunc) error { + pushLogin := &PushLogin{storage: storage} + // 进行相关设置。 + // 如果相对默认的行为进行修改,在opts里面进行追加即可。 + opts = append(defaultPushLoginOpts[:], opts...) + for _, opt := range opts { + opt(&pushLogin.opt) } + return b.login(pushLogin) } // Logout 用户退出 @@ -464,3 +405,20 @@ func (b *Bot) IsHot() bool { func (b *Bot) UUID() string { return b.uuid } + +func (b *Bot) reload() error { + if b.hotReloadStorage == nil { + return errors.New("hotReloadStorage is nil") + } + var item HotReloadStorageItem + err := json.NewDecoder(b.hotReloadStorage).Decode(&item) + if err != nil { + return err + } + b.Caller.Client.Jar = item.Jar.AsCookieJar() + b.Storage.LoginInfo = item.LoginInfo + b.Storage.Request = item.BaseRequest + b.Caller.Client.Domain = item.WechatDomain + b.uuid = item.UUID + return nil +} diff --git a/bot_login.go b/bot_login.go new file mode 100644 index 0000000..978110a --- /dev/null +++ b/bot_login.go @@ -0,0 +1,209 @@ +package openwechat + +// BotLogin 定义了一个Login的接口 +type BotLogin interface { + Login(bot *Bot) error +} + +// SacnLogin 扫码登录 +type SacnLogin struct{} + +// Login 实现了 BotLogin 接口 +func (s *SacnLogin) Login(bot *Bot) error { + uuid, err := bot.Caller.GetLoginUUID() + if err != nil { + return err + } + return s.checkLogin(bot, uuid) +} + +// checkLogin 该方法会一直阻塞,直到用户扫码登录,或者二维码过期 +func (s *SacnLogin) checkLogin(bot *Bot, uuid string) error { + bot.uuid = uuid + // 二维码获取回调 + if bot.UUIDCallback != nil { + bot.UUIDCallback(uuid) + } + for { + // 长轮询检查是否扫码登录 + resp, err := bot.Caller.CheckLogin(uuid, "0") + if err != nil { + return err + } + switch resp.Code { + case StatusSuccess: + // 判断是否有登录回调,如果有执行它 + if err = bot.HandleLogin(resp.Raw); err != nil { + return err + } + if bot.LoginCallBack != nil { + bot.LoginCallBack(resp.Raw) + } + return nil + case StatusScanned: + // 执行扫码回调 + if bot.ScanCallBack != nil { + bot.ScanCallBack(resp.Raw) + } + case StatusTimeout: + return ErrLoginTimeout + case StatusWait: + continue + } + } +} + +type hotLoginOption struct { + withRetry bool + _ struct{} +} + +type HotLoginOptionFunc func(o *hotLoginOption) + +func HotLoginWithRetry(flag bool) HotLoginOptionFunc { + return func(o *hotLoginOption) { + o.withRetry = flag + } +} + +// HotLogin 热登录模式 +type HotLogin struct { + storage HotReloadStorage + opt hotLoginOption +} + +// Login 实现了 BotLogin 接口 +func (h *HotLogin) Login(bot *Bot) error { + err := h.login(bot) + if err != nil && h.opt.withRetry { + scanLogin := SacnLogin{} + return scanLogin.Login(bot) + } + return err +} + +func (h *HotLogin) login(bot *Bot) error { + if err := h.hotLoginInit(bot); err != nil { + return err + } + return bot.WebInit() +} + +func (h *HotLogin) hotLoginInit(bot *Bot) error { + bot.hotReloadStorage = h.storage + return bot.reload() +} + +type pushLoginOption struct { + withoutUUIDCallback bool + withoutScanCallback bool + withoutLoginCallback bool + withRetry bool +} + +type PushLoginOptionFunc func(o *pushLoginOption) + +// PushLoginWithoutUUIDCallback 设置 PushLogin 不执行二维码回调 +func PushLoginWithoutUUIDCallback(flag bool) PushLoginOptionFunc { + return func(o *pushLoginOption) { + o.withoutUUIDCallback = flag + } +} + +// PushLoginWithoutScanCallback 设置 PushLogin 不执行扫码回调 +func PushLoginWithoutScanCallback(flag bool) PushLoginOptionFunc { + return func(o *pushLoginOption) { + o.withoutScanCallback = flag + } +} + +// PushLoginWithoutLoginCallback 设置 PushLogin 不执行登录回调 +func PushLoginWithoutLoginCallback(flag bool) PushLoginOptionFunc { + return func(o *pushLoginOption) { + o.withoutLoginCallback = flag + } +} + +// PushLoginWithRetry 设置 PushLogin 失败后执行扫码登录 +func PushLoginWithRetry(flag bool) PushLoginOptionFunc { + return func(o *pushLoginOption) { + o.withRetry = flag + } +} + +// defaultPushLoginOpts 默认的 PushLogin +var defaultPushLoginOpts = [...]PushLoginOptionFunc{ + PushLoginWithoutUUIDCallback(true), + PushLoginWithoutScanCallback(true), +} + +// PushLogin 免扫码登录模式 +type PushLogin struct { + storage HotReloadStorage + opt pushLoginOption +} + +// Login 实现了 BotLogin 接口 +func (p PushLogin) Login(bot *Bot) error { + if err := p.pushLoginInit(bot); err != nil { + return err + } + resp, err := bot.Caller.WebWxPushLogin(bot.Storage.LoginInfo.WxUin) + if err != nil { + return err + } + if err = resp.Err(); err != nil { + return err + } + err = p.checkLogin(bot, resp.UUID, "1") + if err != nil && p.opt.withRetry { + scanLogin := SacnLogin{} + return scanLogin.Login(bot) + } + return err +} + +func (p PushLogin) pushLoginInit(bot *Bot) error { + bot.hotReloadStorage = p.storage + return bot.reload() +} + +// checkLogin 登录检查 +func (p PushLogin) checkLogin(bot *Bot, uuid, tip string) error { + // todo 将checkLogin剥离出来 + bot.uuid = uuid + // 二维码获取回调 + if bot.UUIDCallback != nil && !p.opt.withoutUUIDCallback { + bot.UUIDCallback(uuid) + } + for { + // 长轮询检查是否扫码登录 + resp, err := bot.Caller.CheckLogin(uuid, tip) + if err != nil { + return err + } + if tip == "1" { + tip = "0" + } + switch resp.Code { + case StatusSuccess: + // 判断是否有登录回调,如果有执行它 + if err = bot.HandleLogin(resp.Raw); err != nil { + return err + } + if bot.LoginCallBack != nil && !p.opt.withoutLoginCallback { + bot.LoginCallBack(resp.Raw) + } + return nil + case StatusScanned: + // 执行扫码回调 + if bot.ScanCallBack != nil && !p.opt.withoutScanCallback { + bot.ScanCallBack(resp.Raw) + } + case StatusTimeout: + return ErrLoginTimeout + case StatusWait: + continue + } + } +} diff --git a/caller.go b/caller.go index db16029..3083e53 100644 --- a/caller.go +++ b/caller.go @@ -50,8 +50,8 @@ func (c *Caller) GetLoginUUID() (string, error) { } // CheckLogin 检查是否登录成功 -func (c *Caller) CheckLogin(uuid string) (*CheckLoginResponse, error) { - resp, err := c.Client.CheckLogin(uuid) +func (c *Caller) CheckLogin(uuid, tip string) (*CheckLoginResponse, error) { + resp, err := c.Client.CheckLogin(uuid, tip) if err != nil { return nil, err } diff --git a/client.go b/client.go index 26c1bcb..b9b5ca1 100644 --- a/client.go +++ b/client.go @@ -116,7 +116,7 @@ func (c *Client) GetLoginQrcode(uuid string) (*http.Response, error) { } // CheckLogin 检查是否登录 -func (c *Client) CheckLogin(uuid string) (*http.Response, error) { +func (c *Client) CheckLogin(uuid, tip string) (*http.Response, error) { path, _ := url.Parse(login) now := time.Now().Unix() params := url.Values{} @@ -124,7 +124,7 @@ func (c *Client) CheckLogin(uuid string) (*http.Response, error) { params.Add("_", strconv.FormatInt(now, 10)) params.Add("loginicon", "true") params.Add("uuid", uuid) - params.Add("tip", "0") + params.Add("tip", tip) path.RawQuery = params.Encode() req, _ := http.NewRequest(http.MethodGet, path.String(), nil) return c.Do(req) @@ -699,12 +699,7 @@ func (c *Client) WebWxRelationPin(request *BaseRequest, op uint8, user *User) (* // WebWxPushLogin 免扫码登陆接口 func (c *Client) WebWxPushLogin(uin int64) (*http.Response, error) { - path, _ := url.Parse(c.Domain.BaseHost() + webwxpushloginurl) - params := url.Values{} - params.Add("uin", strconv.FormatInt(uin, 10)) - path.RawQuery = params.Encode() - req, _ := http.NewRequest(http.MethodGet, path.String(), nil) - return c.Do(req) + return c.mode.PushLogin(c, uin) } // WebWxSendVideoMsg 发送视频消息接口 diff --git a/items.go b/items.go index b0d33ac..6fa19e1 100644 --- a/items.go +++ b/items.go @@ -167,3 +167,10 @@ type PushLoginResponse struct { func (p PushLoginResponse) Ok() bool { return p.Ret == "0" && p.UUID != "" } + +func (p PushLoginResponse) Err() error { + if p.Ok() { + return nil + } + return errors.New(p.Msg) +} diff --git a/mode.go b/mode.go index 49c9b15..2849b5b 100644 --- a/mode.go +++ b/mode.go @@ -10,16 +10,28 @@ import ( type Mode interface { GetLoginUUID(client *Client) (*http.Response, error) GetLoginInfo(client *Client, path string) (*http.Response, error) + PushLogin(client *Client, uin int64) (*http.Response, error) } var ( + // normal 网页版模式 normal Mode = normalMode{} + // desktop 桌面模式,uos electron套壳 desktop Mode = desktopMode{} ) type normalMode struct{} +func (n normalMode) PushLogin(client *Client, uin int64) (*http.Response, error) { + path, _ := url.Parse(client.Domain.BaseHost() + webwxpushloginurl) + params := url.Values{} + params.Add("uin", strconv.FormatInt(uin, 10)) + path.RawQuery = params.Encode() + req, _ := http.NewRequest(http.MethodGet, path.String(), nil) + return client.Do(req) +} + func (n normalMode) GetLoginUUID(client *Client) (*http.Response, error) { path, _ := url.Parse(jslogin) params := url.Values{} @@ -65,3 +77,13 @@ func (n desktopMode) GetLoginInfo(client *Client, path string) (*http.Response, req.Header.Add("extspam", uosPatchExtspam) return client.Do(req) } + +func (n desktopMode) PushLogin(client *Client, uin int64) (*http.Response, error) { + path, _ := url.Parse(client.Domain.BaseHost() + webwxpushloginurl) + params := url.Values{} + params.Add("uin", strconv.FormatInt(uin, 10)) + params.Add("mod", "desktop") + path.RawQuery = params.Encode() + req, _ := http.NewRequest(http.MethodGet, path.String(), nil) + return client.Do(req) +}