This commit is contained in:
cdle
2021-09-16 09:26:59 +08:00
parent 3738ec952b
commit 68dfcc4c35
9 changed files with 755 additions and 37 deletions
+194
View File
@@ -0,0 +1,194 @@
package qq
import (
"bufio"
"bytes"
"os"
"strings"
"time"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/Mrs4s/MiraiGo/client"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/tuotoo/qrcode"
"github.com/Mrs4s/go-cqhttp/global"
)
var console = bufio.NewReader(os.Stdin)
var readLine = func() (str string) {
str, _ = console.ReadString('\n')
str = strings.TrimSpace(str)
return
}
var readLineTimeout = func(t time.Duration, de string) (str string) {
r := make(chan string)
go func() {
select {
case r <- readLine():
case <-time.After(t):
}
}()
str = de
select {
case str = <-r:
case <-time.After(t):
}
return
}
var cli *client.QQClient
// ErrSMSRequestError SMS请求出错
var ErrSMSRequestError = errors.New("sms request error")
func commonLogin() error {
res, err := cli.Login()
if err != nil {
return err
}
return loginResponseProcessor(res)
}
func qrcodeLogin() error {
rsp, err := cli.FetchQRCode()
if err != nil {
return err
}
fi, err := qrcode.Decode(bytes.NewReader(rsp.ImageData))
if err != nil {
return err
}
_ = os.WriteFile("qrcode.png", rsp.ImageData, 0o644)
defer func() { _ = os.Remove("qrcode.png") }()
if cli.Uin != 0 {
log.Infof("请使用账号 %v 登录手机QQ扫描二维码 (qrcode.png) : ", cli.Uin)
} else {
log.Infof("请使用手机QQ扫描二维码 (qrcode.png) : ")
}
time.Sleep(time.Second)
qrcodeTerminal.New().Get(fi.Content).Print()
s, err := cli.QueryQRCodeStatus(rsp.Sig)
if err != nil {
return err
}
prevState := s.State
for {
time.Sleep(time.Second)
s, _ = cli.QueryQRCodeStatus(rsp.Sig)
if s == nil {
continue
}
if prevState == s.State {
continue
}
prevState = s.State
switch s.State {
case client.QRCodeCanceled:
log.Fatalf("扫码被用户取消.")
case client.QRCodeTimeout:
log.Fatalf("二维码过期")
case client.QRCodeWaitingForConfirm:
log.Infof("扫码成功, 请在手机端确认登录.")
case client.QRCodeConfirmed:
res, err := cli.QRCodeLogin(s.LoginInfo)
if err != nil {
return err
}
return loginResponseProcessor(res)
case client.QRCodeImageFetch, client.QRCodeWaitingForScan:
// ignore
}
}
}
func loginResponseProcessor(res *client.LoginResponse) error {
var err error
for {
if err != nil {
return err
}
if res.Success {
return nil
}
var text string
switch res.Error {
case client.SliderNeededError:
log.Warnf("登录需要滑条验证码. ")
log.Warnf("请参考文档 -> https://docs.go-cqhttp.org/faq/slider.html <- 进行处理")
log.Warnf("1. 自行抓包并获取 Ticket 输入.")
log.Warnf("2. 使用手机QQ扫描二维码登入. (推荐)")
log.Warn("请输入(1 - 2) (将在10秒后自动选择2)")
text = readLineTimeout(time.Second*10, "2")
if strings.Contains(text, "1") {
println()
log.Warnf("请用浏览器打开 -> %v <- 并获取Ticket.", res.VerifyUrl)
println()
log.Warn("请输入Ticket (Enter 提交)")
text = readLine()
res, err = cli.SubmitTicket(text)
continue
}
cli.Disconnect()
cli.Release()
cli = client.NewClientEmpty()
return qrcodeLogin()
case client.NeedCaptcha:
log.Warnf("登录需要验证码.")
_ = os.WriteFile("captcha.jpg", res.CaptchaImage, 0o644)
log.Warnf("请输入验证码 (captcha.jpg) (Enter 提交)")
text = readLine()
global.DelFile("captcha.jpg")
res, err = cli.SubmitCaptcha(text, res.CaptchaSign)
continue
case client.SMSNeededError:
log.Warnf("账号已开启设备锁, 按 Enter 向手机 %v 发送短信验证码.", res.SMSPhone)
readLine()
if !cli.RequestSMS() {
log.Warnf("发送验证码失败,可能是请求过于频繁.")
return errors.WithStack(ErrSMSRequestError)
}
log.Warn("请输入短信验证码: (Enter 提交)")
text = readLine()
res, err = cli.SubmitSMS(text)
continue
case client.SMSOrVerifyNeededError:
log.Warnf("账号已开启设备锁,请选择验证方式:")
log.Warnf("1. 向手机 %v 发送短信验证码", res.SMSPhone)
log.Warnf("2. 使用手机QQ扫码验证.")
log.Warn("请输入(1 - 2) (将在10秒后自动选择2)")
text = readLineTimeout(time.Second*10, "2")
if strings.Contains(text, "1") {
if !cli.RequestSMS() {
log.Warnf("发送验证码失败,可能是请求过于频繁.")
return errors.WithStack(ErrSMSRequestError)
}
log.Warn("请输入短信验证码: (Enter 提交)")
text = readLine()
res, err = cli.SubmitSMS(text)
continue
}
fallthrough
case client.UnsafeDeviceError:
log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证后重启Bot.", res.VerifyUrl)
log.Infof("按 Enter 或等待 5s 后继续....")
readLineTimeout(time.Second*5, "")
// os.Exit(0)
case client.OtherLoginError, client.UnknownLoginError, client.TooManySMSRequestError:
msg := res.ErrorMessage
if strings.Contains(msg, "版本") {
msg = "密码错误或账号被冻结"
}
if strings.Contains(msg, "冻结") {
log.Fatalf("账号被冻结")
}
log.Warnf("登录失败: %v", msg)
log.Infof("按 Enter 或等待 5s 后继续....")
readLineTimeout(time.Second*5, "")
// os.Exit(0)
}
}
}
+218
View File
@@ -0,0 +1,218 @@
package qq
import (
"crypto/md5"
"os"
"path"
"sync"
"time"
"github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/global/config"
"github.com/cdle/sillyGirl/core"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
log "github.com/sirupsen/logrus"
easy "github.com/t-tomalak/logrus-easy-formatter"
)
var (
conf *config.Config
PasswordHash [16]byte
AccountToken []byte
allowStatus = [...]client.UserOnlineStatus{
client.StatusOnline, client.StatusAway, client.StatusInvisible, client.StatusBusy,
client.StatusListening, client.StatusConstellation, client.StatusWeather, client.StatusMeetSpring,
client.StatusTimi, client.StatusEatChicken, client.StatusLoving, client.StatusWangWang, client.StatusCookedRice,
client.StatusStudy, client.StatusStayUp, client.StatusPlayBall, client.StatusSignal, client.StatusStudyOnline,
client.StatusGaming, client.StatusVacationing, client.StatusWatchingTV, client.StatusFitness,
}
)
var bot *coolq.CQBot
var qq = core.NewBucket("qq")
func init() {
conf = &config.Config{}
conf.Account.Uin = int64(qq.GetInt("uin", 0))
conf.Account.Password = qq.Get("password")
conf.Message.ReportSelfMessage = true
if conf.Output.Debug {
log.SetReportCaller(true)
}
logFormatter := &easy.Formatter{
TimestampFormat: "2006/01/02 15:04:05.000",
LogFormat: "%time% [Q] %msg% \n",
}
rotateOptions := []rotatelogs.Option{
rotatelogs.WithRotationTime(time.Hour * 24),
}
if conf.Output.LogAging > 0 {
rotateOptions = append(rotateOptions, rotatelogs.WithMaxAge(time.Hour*24*time.Duration(conf.Output.LogAging)))
}
if conf.Output.LogForceNew {
rotateOptions = append(rotateOptions, rotatelogs.ForceNewFile())
}
w, err := rotatelogs.New(path.Join("logs/qq", "%Y-%m-%d.log"), rotateOptions...)
if err != nil {
log.Errorf("rotatelogs init err: %v", err)
panic(err)
}
log.AddHook(global.NewLocalHook(w, logFormatter, global.GetLogLevel(conf.Output.LogLevel)...))
if device := qq.Get("device.json"); device == "" {
client.GenRandomDevice()
qq.Set("device.json", string(client.SystemDeviceInfo.ToJson()))
} else {
if err := client.SystemDeviceInfo.ReadJson([]byte(device)); err != nil {
log.Fatalf("加载设备信息失败: %v", err)
}
}
PasswordHash = md5.Sum([]byte(conf.Account.Password))
log.Info("开始尝试登录并同步消息...")
log.Infof("使用协议: %v", func() string {
switch client.SystemDeviceInfo.Protocol {
case client.IPad:
return "iPad"
case client.AndroidPhone:
return "Android Phone"
case client.AndroidWatch:
return "Android Watch"
case client.MacOS:
return "MacOS"
case client.QiDian:
return "企点"
}
return "未知"
}())
cli = client.NewClientEmpty()
global.Proxy = conf.Message.ProxyRewrite
isQRCodeLogin := (conf.Account.Uin == 0 || len(conf.Account.Password) == 0) && !conf.Account.Encrypt
isTokenLogin := false
saveToken := func() {
AccountToken = cli.GenToken()
qq.Set("session.token", string(AccountToken))
}
if token := qq.Get("session.token"); token != "" {
if err == nil {
if conf.Account.Uin != 0 {
r := binary.NewReader([]byte(token))
cu := r.ReadInt64()
if cu != conf.Account.Uin {
log.Warnf("警告: 配置文件内的QQ号 (%v) 与缓存内的QQ号 (%v) 不相同", conf.Account.Uin, cu)
}
}
if err = cli.TokenLogin([]byte(token)); err != nil {
qq.Set("session.token", "")
log.Warnf("恢复会话失败: %v , 尝试使用正常流程登录.", err)
time.Sleep(time.Second)
cli.Disconnect()
cli.Release()
cli = client.NewClientEmpty()
} else {
isTokenLogin = true
}
}
}
if conf.Account.Uin != 0 && PasswordHash != [16]byte{} {
cli.Uin = conf.Account.Uin
cli.PasswordMd5 = PasswordHash
}
if !isTokenLogin {
if !isQRCodeLogin {
if err := commonLogin(); err != nil {
log.Fatalf("登录时发生致命错误: %v", err)
}
} else {
if err := qrcodeLogin(); err != nil {
log.Fatalf("登录时发生致命错误: %v", err)
}
}
}
var times uint = 10 // 重试次数
var reLoginLock sync.Mutex
cli.OnDisconnected(func(_ *client.QQClient, e *client.ClientDisconnectedEvent) {
reLoginLock.Lock()
defer reLoginLock.Unlock()
times = 1
if cli.Online {
return
}
log.Warnf("Bot已离线: %v", e.Message)
time.Sleep(time.Second * time.Duration(conf.Account.ReLogin.Delay))
for {
if conf.Account.ReLogin.Disabled {
os.Exit(1)
}
if times > conf.Account.ReLogin.MaxTimes && conf.Account.ReLogin.MaxTimes != 0 {
log.Fatalf("Bot重连次数超过限制, 停止")
}
times++
if conf.Account.ReLogin.Interval > 0 {
log.Warnf("将在 %v 秒后尝试重连. 重连次数:%v/%v", conf.Account.ReLogin.Interval, times, conf.Account.ReLogin.MaxTimes)
time.Sleep(time.Second * time.Duration(conf.Account.ReLogin.Interval))
} else {
time.Sleep(time.Second)
}
log.Warnf("尝试重连...")
err := cli.TokenLogin(AccountToken)
if err == nil {
saveToken()
return
}
log.Warnf("快速重连失败: %v", err)
if isQRCodeLogin {
log.Fatalf("快速重连失败, 扫码登录无法恢复会话.")
}
log.Warnf("快速重连失败, 尝试普通登录. 这可能是因为其他端强行T下线导致的.")
time.Sleep(time.Second)
if err := commonLogin(); err != nil {
log.Errorf("登录时发生致命错误: %v", err)
} else {
saveToken()
break
}
}
})
saveToken()
cli.AllowSlider = true
log.Infof("登录成功 欢迎使用: %v", cli.Nickname)
global.Check(cli.ReloadFriendList(), true)
global.Check(cli.ReloadGroupList(), true)
if conf.Account.Status >= int32(len(allowStatus)) || conf.Account.Status < 0 {
conf.Account.Status = 0
}
cli.SetOnlineStatus(allowStatus[int(conf.Account.Status)])
bot = coolq.NewQQBot(cli, conf)
_ = bot.Client
coolq.SetMessageFormat("string")
onPrivateMessage := func(c *client.QQClient, m *message.PrivateMessage) {
core.Senders <- &Sender{
Message: m,
}
// cqm := coolq.ToStringMessage(m.Elements, 0, true)
// fmt.Println(cqm, m.Self, m.Target, m.Sender.Uin)
if m.Sender.Uin != c.Uin {
c.MarkPrivateMessageReaded(m.Sender.Uin, int64(m.Time))
}
}
OnGroupMessage := func(c *client.QQClient, m *message.GroupMessage) {
// cqm := coolq.ToStringMessage(m.Elements, m.GroupCode, true)
// fmt.Println(cqm, m.GroupCode, m.Sender.Uin)
core.Senders <- &Sender{
Message: m,
}
}
bot.Client.OnPrivateMessage(onPrivateMessage)
bot.Client.OnSelfPrivateMessage(onPrivateMessage)
bot.Client.OnGroupMessage(OnGroupMessage)
bot.Client.OnSelfGroupMessage(OnGroupMessage)
}
-1
View File
@@ -1 +0,0 @@
package qq
+152
View File
@@ -0,0 +1,152 @@
package qq
import (
"bytes"
"io/ioutil"
"net/http"
"strings"
"github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/go-cqhttp/coolq"
)
type Sender struct {
Message interface{}
matches [][]string
}
func (sender *Sender) GetContent() string {
text := ""
switch sender.Message.(type) {
case *message.PrivateMessage:
text = coolq.ToStringMessage(sender.Message.(*message.PrivateMessage).Elements, 0, true)
case *message.GroupMessage:
m := sender.Message.(*message.GroupMessage)
text = coolq.ToStringMessage(m.Elements, m.GroupCode, true)
}
return text
}
func (sender *Sender) GetUserID() int {
id := 0
switch sender.Message.(type) {
case *message.PrivateMessage:
id = int(sender.Message.(*message.PrivateMessage).Sender.Uin)
case *message.GroupMessage:
id = int(sender.Message.(*message.GroupMessage).Sender.Uin)
}
return id
}
func (sender *Sender) GetChatID() int {
id := 0
switch sender.Message.(type) {
case *message.PrivateMessage:
case *message.GroupMessage:
id = int(sender.Message.(*message.GroupMessage).GroupCode)
}
return id
}
func (sender *Sender) GetImType() string {
return "qq"
}
func (sender *Sender) GetMessageID() int {
id := 0
switch sender.Message.(type) {
case *message.PrivateMessage:
id = int(sender.Message.(*message.PrivateMessage).Id)
case *message.GroupMessage:
id = int(sender.Message.(*message.GroupMessage).Id)
}
return id
}
func (sender *Sender) GetUsername() string {
name := ""
switch sender.Message.(type) {
case *message.PrivateMessage:
name = sender.Message.(*message.PrivateMessage).Sender.Nickname
case *message.GroupMessage:
name = sender.Message.(*message.GroupMessage).Sender.Nickname
}
return name
}
func (sender *Sender) IsReply() bool {
return false
}
func (sender *Sender) GetReplySenderUserID() int {
return 0
}
func (sender *Sender) GetRawMessage() interface{} {
return sender.Message
}
func (sender *Sender) SetMatch(ss []string) {
sender.matches = [][]string{ss}
}
func (sender *Sender) SetAllMatch(ss [][]string) {
sender.matches = ss
}
func (sender *Sender) GetMatch() []string {
return sender.matches[0]
}
func (sender *Sender) GetAllMatch() [][]string {
return sender.matches
}
func (sender *Sender) Get(index ...int) string {
i := 0
if len(index) != 0 {
i = index[0]
}
if len(sender.matches) == 0 {
return ""
}
if len(sender.matches[0]) < i+1 {
return ""
}
return sender.matches[0][i]
}
func (sender *Sender) IsAdmin() bool {
return true
// return sender.Message.Sender.ID == qq.GetInt("master")
}
func (sender *Sender) IsMedia() bool {
return false
}
func (sender *Sender) Reply(msg interface{}) error {
switch sender.Message.(type) {
case *message.PrivateMessage:
m := sender.Message.(*message.PrivateMessage)
switch msg.(type) {
case string:
bot.SendPrivateMessage(m.Sender.Uin, int64(qq.GetInt("groupCode")), &message.SendingMessage{Elements: []message.IMessageElement{&message.TextElement{Content: msg.(string)}}})
case *http.Response:
data, _ := ioutil.ReadAll(msg.(*http.Response).Body)
bot.SendPrivateMessage(m.Sender.Uin, int64(qq.GetInt("groupCode")), &message.SendingMessage{Elements: []message.IMessageElement{&coolq.LocalImageElement{Stream: bytes.NewReader(data)}}})
}
case *message.GroupMessage:
m := sender.Message.(*message.GroupMessage)
switch msg.(type) {
case string:
if strings.Contains(msg.(string), "\n") {
msg = "\n" + msg.(string)
}
bot.SendGroupMessage(m.GroupCode, &message.SendingMessage{Elements: []message.IMessageElement{&message.AtElement{Target: m.Sender.Uin}, &message.TextElement{Content: msg.(string)}}})
case *http.Response:
data, _ := ioutil.ReadAll(msg.(*http.Response).Body)
bot.SendGroupMessage(m.GroupCode, &message.SendingMessage{Elements: []message.IMessageElement{&message.AtElement{Target: m.Sender.Uin}, &message.TextElement{Content: "\n"}, &coolq.LocalImageElement{Stream: bytes.NewReader(data)}}})
}
}
return nil
}