添加免扫码登录功能 (#178)

This commit is contained in:
多吃点苹果 2023-01-06 00:54:36 +08:00 committed by GitHub
parent 17fbbd350e
commit fc438ab83f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 287 additions and 96 deletions

130
bot.go
View File

@ -66,102 +66,43 @@ func (b *Bot) GetCurrentUser() (*Self, error) {
return b.self, nil
}
func (b *Bot) login(login BotLogin) error {
return login.Login(b)
}
// Login 用户登录
func (b *Bot) Login() error {
scanLogin := &SacnLogin{}
return b.login(scanLogin)
}
// 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()
func (b *Bot) HotLogin(storage HotReloadStorage, opts ...HotLoginOptionFunc) error {
hotLogin := &HotLogin{storage: storage}
for _, opt := range opts {
opt(&hotLogin.opt)
}
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
return b.login(hotLogin)
}
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
}
// Login 用户登录
func (b *Bot) Login() error {
uuid, err := b.Caller.GetLoginUUID()
if err != nil {
return err
}
return b.LoginWithUUID(uuid)
}
// LoginWithUUID 用户登录
// 该方法会一直阻塞,直到用户扫码登录,或者二维码过期
func (b *Bot) LoginWithUUID(uuid string) error {
b.uuid = uuid
// 二维码获取回调
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 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
}
// 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
}

209
bot_login.go Normal file
View File

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

View File

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

View File

@ -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 发送视频消息接口

View File

@ -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)
}

22
mode.go
View File

@ -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)
}