github.com/Mrs4s/go-cqhttp@v1.2.0/cmd/gocq/login.go (about)

     1  package gocq
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"image"
     8  	"image/png"
     9  	"os"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/Mrs4s/MiraiGo/client"
    14  	"github.com/Mrs4s/MiraiGo/utils"
    15  	"github.com/mattn/go-colorable"
    16  	"github.com/pkg/errors"
    17  	log "github.com/sirupsen/logrus"
    18  	"gopkg.ilharper.com/x/isatty"
    19  
    20  	"github.com/Mrs4s/go-cqhttp/global"
    21  	"github.com/Mrs4s/go-cqhttp/internal/download"
    22  )
    23  
    24  var console = bufio.NewReader(os.Stdin)
    25  
    26  func readLine() (str string) {
    27  	str, _ = console.ReadString('\n')
    28  	str = strings.TrimSpace(str)
    29  	return
    30  }
    31  
    32  func readLineTimeout(t time.Duration) {
    33  	r := make(chan string)
    34  	go func() {
    35  		select {
    36  		case r <- readLine():
    37  		case <-time.After(t):
    38  		}
    39  	}()
    40  	select {
    41  	case <-r:
    42  	case <-time.After(t):
    43  	}
    44  }
    45  
    46  func readIfTTY(de string) (str string) {
    47  	if isatty.Isatty(os.Stdin.Fd()) {
    48  		return readLine()
    49  	}
    50  	log.Warnf("未检测到输入终端,自动选择%s.", de)
    51  	return de
    52  }
    53  
    54  var cli *client.QQClient
    55  var device *client.DeviceInfo
    56  
    57  // ErrSMSRequestError SMS请求出错
    58  var ErrSMSRequestError = errors.New("sms request error")
    59  
    60  func commonLogin() error {
    61  	res, err := cli.Login()
    62  	if err != nil {
    63  		return err
    64  	}
    65  	return loginResponseProcessor(res)
    66  }
    67  
    68  func printQRCode(imgData []byte) {
    69  	const (
    70  		black = "\033[48;5;0m  \033[0m"
    71  		white = "\033[48;5;7m  \033[0m"
    72  	)
    73  	img, err := png.Decode(bytes.NewReader(imgData))
    74  	if err != nil {
    75  		log.Panic(err)
    76  	}
    77  	data := img.(*image.Gray).Pix
    78  	bound := img.Bounds().Max.X
    79  	buf := make([]byte, 0, (bound*4+1)*(bound))
    80  	i := 0
    81  	for y := 0; y < bound; y++ {
    82  		i = y * bound
    83  		for x := 0; x < bound; x++ {
    84  			if data[i] != 255 {
    85  				buf = append(buf, white...)
    86  			} else {
    87  				buf = append(buf, black...)
    88  			}
    89  			i++
    90  		}
    91  		buf = append(buf, '\n')
    92  	}
    93  	_, _ = colorable.NewColorableStdout().Write(buf)
    94  }
    95  
    96  func qrcodeLogin() error {
    97  	rsp, err := cli.FetchQRCodeCustomSize(1, 2, 1)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	_ = os.WriteFile("qrcode.png", rsp.ImageData, 0o644)
   102  	defer func() { _ = os.Remove("qrcode.png") }()
   103  	if cli.Uin != 0 {
   104  		log.Infof("请使用账号 %v 登录手机QQ扫描二维码 (qrcode.png) : ", cli.Uin)
   105  	} else {
   106  		log.Infof("请使用手机QQ扫描二维码 (qrcode.png) : ")
   107  	}
   108  	time.Sleep(time.Second)
   109  	printQRCode(rsp.ImageData)
   110  	s, err := cli.QueryQRCodeStatus(rsp.Sig)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	prevState := s.State
   115  	for {
   116  		time.Sleep(time.Second)
   117  		s, _ = cli.QueryQRCodeStatus(rsp.Sig)
   118  		if s == nil {
   119  			continue
   120  		}
   121  		if prevState == s.State {
   122  			continue
   123  		}
   124  		prevState = s.State
   125  		switch s.State {
   126  		case client.QRCodeCanceled:
   127  			log.Fatalf("扫码被用户取消.")
   128  		case client.QRCodeTimeout:
   129  			log.Fatalf("二维码过期")
   130  		case client.QRCodeWaitingForConfirm:
   131  			log.Infof("扫码成功, 请在手机端确认登录.")
   132  		case client.QRCodeConfirmed:
   133  			res, err := cli.QRCodeLogin(s.LoginInfo)
   134  			if err != nil {
   135  				return err
   136  			}
   137  			return loginResponseProcessor(res)
   138  		case client.QRCodeImageFetch, client.QRCodeWaitingForScan:
   139  			// ignore
   140  		}
   141  	}
   142  }
   143  
   144  func loginResponseProcessor(res *client.LoginResponse) error {
   145  	var err error
   146  	for {
   147  		if err != nil {
   148  			return err
   149  		}
   150  		if res.Success {
   151  			return nil
   152  		}
   153  		var text string
   154  		switch res.Error {
   155  		case client.SliderNeededError:
   156  			log.Warnf("登录需要滑条验证码, 请验证后重试.")
   157  			ticket := getTicket(res.VerifyUrl)
   158  			if ticket == "" {
   159  				log.Infof("按 Enter 继续....")
   160  				readLine()
   161  				os.Exit(0)
   162  			}
   163  			res, err = cli.SubmitTicket(ticket)
   164  			continue
   165  		case client.NeedCaptcha:
   166  			log.Warnf("登录需要验证码.")
   167  			_ = os.WriteFile("captcha.jpg", res.CaptchaImage, 0o644)
   168  			log.Warnf("请输入验证码 (captcha.jpg): (Enter 提交)")
   169  			text = readLine()
   170  			global.DelFile("captcha.jpg")
   171  			res, err = cli.SubmitCaptcha(text, res.CaptchaSign)
   172  			continue
   173  		case client.SMSNeededError:
   174  			log.Warnf("账号已开启设备锁, 按 Enter 向手机 %v 发送短信验证码.", res.SMSPhone)
   175  			readLine()
   176  			if !cli.RequestSMS() {
   177  				log.Warnf("发送验证码失败,可能是请求过于频繁.")
   178  				return errors.WithStack(ErrSMSRequestError)
   179  			}
   180  			log.Warn("请输入短信验证码: (Enter 提交)")
   181  			text = readLine()
   182  			res, err = cli.SubmitSMS(text)
   183  			continue
   184  		case client.SMSOrVerifyNeededError:
   185  			log.Warnf("账号已开启设备锁,请选择验证方式:")
   186  			log.Warnf("1. 向手机 %v 发送短信验证码", res.SMSPhone)
   187  			log.Warnf("2. 使用手机QQ扫码验证.")
   188  			log.Warn("请输入(1 - 2):")
   189  			text = readIfTTY("2")
   190  			if strings.Contains(text, "1") {
   191  				if !cli.RequestSMS() {
   192  					log.Warnf("发送验证码失败,可能是请求过于频繁.")
   193  					return errors.WithStack(ErrSMSRequestError)
   194  				}
   195  				log.Warn("请输入短信验证码: (Enter 提交)")
   196  				text = readLine()
   197  				res, err = cli.SubmitSMS(text)
   198  				continue
   199  			}
   200  			fallthrough
   201  		case client.UnsafeDeviceError:
   202  			log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证后重启Bot.", res.VerifyUrl)
   203  			log.Infof("按 Enter 或等待 5s 后继续....")
   204  			readLineTimeout(time.Second * 5)
   205  			os.Exit(0)
   206  		case client.OtherLoginError, client.UnknownLoginError, client.TooManySMSRequestError:
   207  			msg := res.ErrorMessage
   208  			log.Warnf("登录失败: %v Code: %v", msg, res.Code)
   209  			switch res.Code {
   210  			case 235:
   211  				log.Warnf("设备信息被封禁, 请删除 device.json 后重试.")
   212  			case 237:
   213  				log.Warnf("登录过于频繁, 请在手机QQ登录并根据提示完成认证后等一段时间重试")
   214  			case 45:
   215  				log.Warnf("你的账号被限制登录, 请配置 SignServer 后重试")
   216  			}
   217  			log.Infof("按 Enter 继续....")
   218  			readLine()
   219  			os.Exit(0)
   220  		}
   221  	}
   222  }
   223  
   224  func getTicket(u string) string {
   225  	log.Warnf("请选择提交滑块ticket方式:")
   226  	log.Warnf("1. 自动提交")
   227  	log.Warnf("2. 手动抓取提交")
   228  	log.Warn("请输入(1 - 2):")
   229  	text := readLine()
   230  	id := utils.RandomString(8)
   231  	auto := !strings.Contains(text, "2")
   232  	if auto {
   233  		u = strings.ReplaceAll(u, "https://ssl.captcha.qq.com/template/wireless_mqq_captcha.html?", fmt.Sprintf("https://captcha.go-cqhttp.org/captcha?id=%v&", id))
   234  	}
   235  	log.Warnf("请前往该地址验证 -> %v ", u)
   236  	if !auto {
   237  		log.Warn("请输入ticket: (Enter 提交)")
   238  		return readLine()
   239  	}
   240  
   241  	for count := 120; count > 0; count-- {
   242  		str := fetchCaptcha(id)
   243  		if str != "" {
   244  			return str
   245  		}
   246  		time.Sleep(time.Second)
   247  	}
   248  	log.Warnf("验证超时")
   249  	return ""
   250  }
   251  
   252  func fetchCaptcha(id string) string {
   253  	g, err := download.Request{URL: "https://captcha.go-cqhttp.org/captcha/ticket?id=" + id}.JSON()
   254  	if err != nil {
   255  		log.Debugf("获取 Ticket 时出现错误: %v", err)
   256  		return ""
   257  	}
   258  	if g.Get("ticket").Exists() {
   259  		return g.Get("ticket").String()
   260  	}
   261  	return ""
   262  }