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

     1  // Package gocq 程序的主体部分
     2  package gocq
     3  
     4  import (
     5  	"crypto/aes"
     6  	"crypto/md5"
     7  	"crypto/sha1"
     8  	"encoding/hex"
     9  	"fmt"
    10  	"os"
    11  	"path"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/Mrs4s/MiraiGo/binary"
    16  	"github.com/Mrs4s/MiraiGo/client"
    17  	"github.com/Mrs4s/MiraiGo/wrapper"
    18  	para "github.com/fumiama/go-hide-param"
    19  	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
    20  	"github.com/pkg/errors"
    21  	log "github.com/sirupsen/logrus"
    22  	"github.com/tidwall/gjson"
    23  	"golang.org/x/crypto/pbkdf2"
    24  	"golang.org/x/term"
    25  
    26  	"github.com/Mrs4s/go-cqhttp/coolq"
    27  	"github.com/Mrs4s/go-cqhttp/db"
    28  	"github.com/Mrs4s/go-cqhttp/global"
    29  	"github.com/Mrs4s/go-cqhttp/global/terminal"
    30  	"github.com/Mrs4s/go-cqhttp/internal/base"
    31  	"github.com/Mrs4s/go-cqhttp/internal/cache"
    32  	"github.com/Mrs4s/go-cqhttp/internal/download"
    33  	"github.com/Mrs4s/go-cqhttp/internal/selfdiagnosis"
    34  	"github.com/Mrs4s/go-cqhttp/internal/selfupdate"
    35  	"github.com/Mrs4s/go-cqhttp/modules/servers"
    36  	"github.com/Mrs4s/go-cqhttp/server"
    37  )
    38  
    39  // 允许通过配置文件设置的状态列表
    40  var allowStatus = [...]client.UserOnlineStatus{
    41  	client.StatusOnline, client.StatusAway, client.StatusInvisible, client.StatusBusy,
    42  	client.StatusListening, client.StatusConstellation, client.StatusWeather, client.StatusMeetSpring,
    43  	client.StatusTimi, client.StatusEatChicken, client.StatusLoving, client.StatusWangWang, client.StatusCookedRice,
    44  	client.StatusStudy, client.StatusStayUp, client.StatusPlayBall, client.StatusSignal, client.StatusStudyOnline,
    45  	client.StatusGaming, client.StatusVacationing, client.StatusWatchingTV, client.StatusFitness,
    46  }
    47  
    48  // InitBase 解析参数并检测
    49  //
    50  //	如果在 windows 下双击打开了程序,程序将在此函数释出脚本后终止;
    51  //	如果传入 -h 参数,程序将打印帮助后终止;
    52  //	如果传入 -d 参数,程序将在启动 daemon 后终止。
    53  func InitBase() {
    54  	base.Parse()
    55  	if !base.FastStart && terminal.RunningByDoubleClick() {
    56  		err := terminal.NoMoreDoubleClick()
    57  		if err != nil {
    58  			log.Errorf("遇到错误: %v", err)
    59  			time.Sleep(time.Second * 5)
    60  		}
    61  		os.Exit(0)
    62  	}
    63  	switch {
    64  	case base.LittleH:
    65  		base.Help()
    66  	case base.LittleD:
    67  		server.Daemon()
    68  	}
    69  	if base.LittleWD != "" {
    70  		err := os.Chdir(base.LittleWD)
    71  		if err != nil {
    72  			log.Fatalf("重置工作目录时出现错误: %v", err)
    73  		}
    74  	}
    75  	base.Init()
    76  }
    77  
    78  // PrepareData 准备 log, 缓存, 数据库, 必须在 InitBase 之后执行
    79  func PrepareData() {
    80  	rotateOptions := []rotatelogs.Option{
    81  		rotatelogs.WithRotationTime(time.Hour * 24),
    82  	}
    83  	rotateOptions = append(rotateOptions, rotatelogs.WithMaxAge(base.LogAging))
    84  	if base.LogForceNew {
    85  		rotateOptions = append(rotateOptions, rotatelogs.ForceNewFile())
    86  	}
    87  	w, err := rotatelogs.New(path.Join("logs", "%Y-%m-%d.log"), rotateOptions...)
    88  	if err != nil {
    89  		log.Errorf("rotatelogs init err: %v", err)
    90  		panic(err)
    91  	}
    92  
    93  	consoleFormatter := global.LogFormat{EnableColor: base.LogColorful}
    94  	fileFormatter := global.LogFormat{EnableColor: false}
    95  	log.AddHook(global.NewLocalHook(w, consoleFormatter, fileFormatter, global.GetLogLevel(base.LogLevel)...))
    96  
    97  	mkCacheDir := func(path string, _type string) {
    98  		if !global.PathExists(path) {
    99  			if err := os.MkdirAll(path, 0o755); err != nil {
   100  				log.Fatalf("创建%s缓存文件夹失败: %v", _type, err)
   101  			}
   102  		}
   103  	}
   104  	mkCacheDir(global.ImagePath, "图片")
   105  	mkCacheDir(global.VoicePath, "语音")
   106  	mkCacheDir(global.VideoPath, "视频")
   107  	mkCacheDir(global.CachePath, "发送图片")
   108  	mkCacheDir(path.Join(global.ImagePath, "guild-images"), "频道图片缓存")
   109  	mkCacheDir(global.VersionsPath, "版本缓存")
   110  	cache.Init()
   111  
   112  	db.Init()
   113  	if err := db.Open(); err != nil {
   114  		log.Fatalf("打开数据库失败: %v", err)
   115  	}
   116  }
   117  
   118  // LoginInteract 登录交互, 可能需要键盘输入, 必须在 InitBase, PrepareData 之后执行
   119  func LoginInteract() {
   120  	var byteKey []byte
   121  	arg := os.Args
   122  	if len(arg) > 1 {
   123  		for i := range arg {
   124  			switch arg[i] {
   125  			case "update":
   126  				if len(arg) > i+1 {
   127  					selfupdate.SelfUpdate(arg[i+1])
   128  				} else {
   129  					selfupdate.SelfUpdate("")
   130  				}
   131  			case "key":
   132  				p := i + 1
   133  				if len(arg) > p {
   134  					byteKey = []byte(arg[p])
   135  					para.Hide(p)
   136  				}
   137  			}
   138  		}
   139  	}
   140  
   141  	if (base.Account.Uin == 0 || (base.Account.Password == "" && !base.Account.Encrypt)) && !global.PathExists("session.token") {
   142  		log.Warn("账号密码未配置, 将使用二维码登录.")
   143  		if !base.FastStart {
   144  			log.Warn("将在 5秒 后继续.")
   145  			time.Sleep(time.Second * 5)
   146  		}
   147  	}
   148  
   149  	log.Info("当前版本:", base.Version)
   150  	if base.Debug {
   151  		log.SetLevel(log.DebugLevel)
   152  		log.Warnf("已开启Debug模式.")
   153  	}
   154  	if !global.PathExists("device.json") {
   155  		log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
   156  		device = client.GenRandomDevice()
   157  		_ = os.WriteFile("device.json", device.ToJson(), 0o644)
   158  		log.Info("已生成设备信息并保存到 device.json 文件.")
   159  	} else {
   160  		log.Info("将使用 device.json 内的设备信息运行Bot.")
   161  		device = new(client.DeviceInfo)
   162  		if err := device.ReadJson([]byte(global.ReadAllText("device.json"))); err != nil {
   163  			log.Fatalf("加载设备信息失败: %v", err)
   164  		}
   165  	}
   166  	signServer, err := getAvaliableSignServer() // 获取可用签名服务器
   167  	if err != nil {
   168  		log.Warn(err)
   169  	}
   170  	if signServer != nil && len(signServer.URL) > 1 {
   171  		log.Infof("使用签名服务器:%v", signServer.URL)
   172  		go signStartRefreshToken(base.Account.RefreshInterval) // 定时刷新 token
   173  		wrapper.DandelionEnergy = energy
   174  		wrapper.FekitGetSign = sign
   175  		if !base.IsBelow110 {
   176  			if !base.Account.AutoRegister {
   177  				log.Warn("自动注册实例已关闭,请配置 sign-server 端自动注册实例以保持正常签名")
   178  			}
   179  			if !base.Account.AutoRefreshToken {
   180  				log.Info("自动刷新 token 已关闭,token 过期后获取签名时将不会立即尝试刷新获取新 token")
   181  			}
   182  		} else {
   183  			log.Warn("签名服务器版本 <= 1.1.0 ,无法使用刷新 token 等操作,建议使用 1.1.6 版本及以上签名服务器")
   184  		}
   185  	} else {
   186  		log.Warnf("警告: 未配置签名服务器或签名服务器不可用, 这可能会导致登录 45 错误码或发送消息被风控")
   187  	}
   188  
   189  	if base.Account.Encrypt {
   190  		if !global.PathExists("password.encrypt") {
   191  			if base.Account.Password == "" {
   192  				log.Error("无法进行加密,请在配置文件中的添加密码后重新启动.")
   193  			} else {
   194  				log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)")
   195  				byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
   196  				base.PasswordHash = md5.Sum([]byte(base.Account.Password))
   197  				_ = os.WriteFile("password.encrypt", []byte(PasswordHashEncrypt(base.PasswordHash[:], byteKey)), 0o644)
   198  				log.Info("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
   199  			}
   200  			readLine()
   201  			os.Exit(0)
   202  		}
   203  		if base.Account.Password != "" {
   204  			log.Error("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
   205  			readLine()
   206  			os.Exit(0)
   207  		}
   208  		if len(byteKey) == 0 {
   209  			log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
   210  			cancel := make(chan struct{}, 1)
   211  			state, _ := term.GetState(int(os.Stdin.Fd()))
   212  			go func() {
   213  				select {
   214  				case <-cancel:
   215  					return
   216  				case <-time.After(time.Second * 45):
   217  					log.Infof("解密key输入超时")
   218  					time.Sleep(3 * time.Second)
   219  					_ = term.Restore(int(os.Stdin.Fd()), state)
   220  					os.Exit(0)
   221  				}
   222  			}()
   223  			byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
   224  			cancel <- struct{}{}
   225  		} else {
   226  			log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.")
   227  		}
   228  
   229  		encrypt, _ := os.ReadFile("password.encrypt")
   230  		ph, err := PasswordHashDecrypt(string(encrypt), byteKey)
   231  		if err != nil {
   232  			log.Fatalf("加密存储的密码损坏,请尝试重新配置密码")
   233  		}
   234  		copy(base.PasswordHash[:], ph)
   235  	} else if len(base.Account.Password) > 0 {
   236  		base.PasswordHash = md5.Sum([]byte(base.Account.Password))
   237  	}
   238  	if !base.FastStart {
   239  		log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
   240  		time.Sleep(time.Second * 5)
   241  	}
   242  	log.Info("开始尝试登录并同步消息...")
   243  	log.Infof("使用协议: %s", device.Protocol.Version())
   244  	cli = newClient()
   245  	cli.UseDevice(device)
   246  	isQRCodeLogin := (base.Account.Uin == 0 || len(base.Account.Password) == 0) && !base.Account.Encrypt
   247  	isTokenLogin := false
   248  
   249  	if isQRCodeLogin && cli.Device().Protocol != 2 {
   250  		log.Warn("当前协议不支持二维码登录, 请配置账号密码登录.")
   251  		os.Exit(0)
   252  	}
   253  
   254  	// 加载本地版本信息, 一般是在上次登录时保存的
   255  	versionFile := path.Join(global.VersionsPath, fmt.Sprint(int(cli.Device().Protocol))+".json")
   256  	if global.PathExists(versionFile) {
   257  		b, err := os.ReadFile(versionFile)
   258  		if err == nil {
   259  			_ = cli.Device().Protocol.Version().UpdateFromJson(b)
   260  		}
   261  		log.Infof("从文件 %s 读取协议版本 %v.", versionFile, cli.Device().Protocol.Version())
   262  	}
   263  
   264  	saveToken := func() {
   265  		base.AccountToken = cli.GenToken()
   266  		_ = os.WriteFile("session.token", base.AccountToken, 0o644)
   267  	}
   268  	if global.PathExists("session.token") {
   269  		token, err := os.ReadFile("session.token")
   270  		if err == nil {
   271  			if base.Account.Uin != 0 {
   272  				r := binary.NewReader(token)
   273  				cu := r.ReadInt64()
   274  				if cu != base.Account.Uin {
   275  					log.Warnf("警告: 配置文件内的QQ号 (%v) 与缓存内的QQ号 (%v) 不相同", base.Account.Uin, cu)
   276  					log.Warnf("1. 使用会话缓存继续.")
   277  					log.Warnf("2. 删除会话缓存并重启.")
   278  					log.Warnf("请选择:")
   279  					text := readIfTTY("1")
   280  					if text == "2" {
   281  						_ = os.Remove("session.token")
   282  						log.Infof("缓存已删除.")
   283  						os.Exit(0)
   284  					}
   285  				}
   286  			}
   287  			if err = cli.TokenLogin(token); err != nil {
   288  				_ = os.Remove("session.token")
   289  				log.Warnf("恢复会话失败: %v , 尝试使用正常流程登录.", err)
   290  				time.Sleep(time.Second)
   291  				cli.Disconnect()
   292  				cli.Release()
   293  				cli = newClient()
   294  				cli.UseDevice(device)
   295  			} else {
   296  				isTokenLogin = true
   297  			}
   298  		}
   299  	}
   300  	if base.Account.Uin != 0 && base.PasswordHash != [16]byte{} {
   301  		cli.Uin = base.Account.Uin
   302  		cli.PasswordMd5 = base.PasswordHash
   303  	}
   304  	download.SetTimeout(time.Duration(base.HTTPTimeout) * time.Second)
   305  	if !base.FastStart {
   306  		log.Infof("正在检查协议更新...")
   307  		currentVersionName := device.Protocol.Version().SortVersionName
   308  		remoteVersion, err := getRemoteLatestProtocolVersion(int(device.Protocol.Version().Protocol))
   309  		if err == nil {
   310  			remoteVersionName := gjson.GetBytes(remoteVersion, "sort_version_name").String()
   311  			if remoteVersionName != currentVersionName {
   312  				switch {
   313  				case !base.UpdateProtocol:
   314  					log.Infof("检测到协议更新: %s -> %s", currentVersionName, remoteVersionName)
   315  					log.Infof("如果登录时出现版本过低错误, 可尝试使用 -update-protocol 参数启动")
   316  				case !isTokenLogin:
   317  					_ = device.Protocol.Version().UpdateFromJson(remoteVersion)
   318  					log.Infof("协议版本已更新: %s -> %s", currentVersionName, remoteVersionName)
   319  				default:
   320  					log.Infof("检测到协议更新: %s -> %s", currentVersionName, remoteVersionName)
   321  					log.Infof("由于使用了会话缓存, 无法自动更新协议, 请删除缓存后重试")
   322  				}
   323  			}
   324  		} else if err.Error() != "remote version unavailable" {
   325  			log.Warnf("检查协议更新失败: %v", err)
   326  		}
   327  	}
   328  	if !isTokenLogin {
   329  		if !isQRCodeLogin {
   330  			if err := commonLogin(); err != nil {
   331  				log.Fatalf("登录时发生致命错误: %v", err)
   332  			}
   333  		} else {
   334  			if err := qrcodeLogin(); err != nil {
   335  				log.Fatalf("登录时发生致命错误: %v", err)
   336  			}
   337  		}
   338  	}
   339  	var times uint = 1 // 重试次数
   340  	var reLoginLock sync.Mutex
   341  	cli.DisconnectedEvent.Subscribe(func(q *client.QQClient, e *client.ClientDisconnectedEvent) {
   342  		reLoginLock.Lock()
   343  		defer reLoginLock.Unlock()
   344  		times = 1
   345  		if cli.Online.Load() {
   346  			return
   347  		}
   348  		log.Warnf("Bot已离线: %v", e.Message)
   349  		time.Sleep(time.Second * time.Duration(base.Reconnect.Delay))
   350  		for {
   351  			if base.Reconnect.Disabled {
   352  				log.Warnf("未启用自动重连, 将退出.")
   353  				os.Exit(1)
   354  			}
   355  			if times > base.Reconnect.MaxTimes && base.Reconnect.MaxTimes != 0 {
   356  				log.Fatalf("Bot重连次数超过限制, 停止")
   357  			}
   358  			times++
   359  			if base.Reconnect.Interval > 0 {
   360  				log.Warnf("将在 %v 秒后尝试重连. 重连次数:%v/%v", base.Reconnect.Interval, times, base.Reconnect.MaxTimes)
   361  				time.Sleep(time.Second * time.Duration(base.Reconnect.Interval))
   362  			} else {
   363  				time.Sleep(time.Second)
   364  			}
   365  			if cli.Online.Load() {
   366  				log.Infof("登录已完成")
   367  				break
   368  			}
   369  			log.Warnf("尝试重连...")
   370  			err := cli.TokenLogin(base.AccountToken)
   371  			if err == nil {
   372  				saveToken()
   373  				return
   374  			}
   375  			log.Warnf("快速重连失败: %v", err)
   376  			if isQRCodeLogin {
   377  				log.Fatalf("快速重连失败, 扫码登录无法恢复会话.")
   378  			}
   379  			log.Warnf("快速重连失败, 尝试普通登录. 这可能是因为其他端强行T下线导致的.")
   380  			time.Sleep(time.Second)
   381  			if err := commonLogin(); err != nil {
   382  				log.Errorf("登录时发生致命错误: %v", err)
   383  			} else {
   384  				saveToken()
   385  				break
   386  			}
   387  		}
   388  	})
   389  	saveToken()
   390  	cli.AllowSlider = true
   391  	log.Infof("登录成功 欢迎使用: %v", cli.Nickname)
   392  	log.Info("开始加载好友列表...")
   393  	global.Check(cli.ReloadFriendList(), true)
   394  	log.Infof("共加载 %v 个好友.", len(cli.FriendList))
   395  	log.Infof("开始加载群列表...")
   396  	global.Check(cli.ReloadGroupList(), true)
   397  	log.Infof("共加载 %v 个群.", len(cli.GroupList))
   398  	if uint(base.Account.Status) >= uint(len(allowStatus)) {
   399  		base.Account.Status = 0
   400  	}
   401  	cli.SetOnlineStatus(allowStatus[base.Account.Status])
   402  	servers.Run(coolq.NewQQBot(cli))
   403  	log.Info("资源初始化完成, 开始处理信息.")
   404  	log.Info("アトリは、高性能ですから!")
   405  }
   406  
   407  // WaitSignal 在新线程检查更新和网络并等待信号, 必须在 InitBase, PrepareData, LoginInteract 之后执行
   408  //
   409  //   - 直接返回: os.Interrupt, syscall.SIGTERM
   410  //   - dump stack: syscall.SIGQUIT, syscall.SIGUSR1
   411  func WaitSignal() {
   412  	go func() {
   413  		selfupdate.CheckUpdate()
   414  		selfdiagnosis.NetworkDiagnosis(cli)
   415  	}()
   416  
   417  	<-global.SetupMainSignalHandler()
   418  }
   419  
   420  // PasswordHashEncrypt 使用key加密给定passwordHash
   421  func PasswordHashEncrypt(passwordHash []byte, key []byte) string {
   422  	if len(passwordHash) != 16 {
   423  		panic("密码加密参数错误")
   424  	}
   425  
   426  	key = pbkdf2.Key(key, key, 114514, 32, sha1.New)
   427  
   428  	cipher, _ := aes.NewCipher(key)
   429  	result := make([]byte, 16)
   430  	cipher.Encrypt(result, passwordHash)
   431  
   432  	return hex.EncodeToString(result)
   433  }
   434  
   435  // PasswordHashDecrypt 使用key解密给定passwordHash
   436  func PasswordHashDecrypt(encryptedPasswordHash string, key []byte) ([]byte, error) {
   437  	ciphertext, err := hex.DecodeString(encryptedPasswordHash)
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  
   442  	key = pbkdf2.Key(key, key, 114514, 32, sha1.New)
   443  
   444  	cipher, _ := aes.NewCipher(key)
   445  	result := make([]byte, 16)
   446  	cipher.Decrypt(result, ciphertext)
   447  
   448  	return result, nil
   449  }
   450  
   451  func newClient() *client.QQClient {
   452  	c := client.NewClientEmpty()
   453  	c.UseFragmentMessage = base.ForceFragmented
   454  	c.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool {
   455  		if !base.UseSSOAddress {
   456  			log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.")
   457  			return false
   458  		}
   459  		log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
   460  		return true
   461  	})
   462  	if global.PathExists("address.txt") {
   463  		log.Infof("检测到 address.txt 文件. 将覆盖目标IP.")
   464  		addr := global.ReadAddrFile("address.txt")
   465  		if len(addr) > 0 {
   466  			c.SetCustomServer(addr)
   467  		}
   468  		log.Infof("读取到 %v 个自定义地址.", len(addr))
   469  	}
   470  	c.SetLogger(protocolLogger{})
   471  	return c
   472  }
   473  
   474  var remoteVersions = map[int]string{
   475  	1: "https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_phone.json",
   476  	6: "https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_pad.json",
   477  }
   478  
   479  func getRemoteLatestProtocolVersion(protocolType int) ([]byte, error) {
   480  	url, ok := remoteVersions[protocolType]
   481  	if !ok {
   482  		return nil, errors.New("remote version unavailable")
   483  	}
   484  	response, err := download.Request{URL: url}.Bytes()
   485  	if err != nil {
   486  		return download.Request{URL: "https://ghproxy.com/" + url}.Bytes()
   487  	}
   488  	return response, nil
   489  }
   490  
   491  type protocolLogger struct{}
   492  
   493  const fromProtocol = "Protocol -> "
   494  
   495  func (p protocolLogger) Info(format string, arg ...any) {
   496  	log.Infof(fromProtocol+format, arg...)
   497  }
   498  
   499  func (p protocolLogger) Warning(format string, arg ...any) {
   500  	log.Warnf(fromProtocol+format, arg...)
   501  }
   502  
   503  func (p protocolLogger) Debug(format string, arg ...any) {
   504  	log.Debugf(fromProtocol+format, arg...)
   505  }
   506  
   507  func (p protocolLogger) Error(format string, arg ...any) {
   508  	log.Errorf(fromProtocol+format, arg...)
   509  }
   510  
   511  func (p protocolLogger) Dump(data []byte, format string, arg ...any) {
   512  	if !global.PathExists(global.DumpsPath) {
   513  		_ = os.MkdirAll(global.DumpsPath, 0o755)
   514  	}
   515  	dumpFile := path.Join(global.DumpsPath, fmt.Sprintf("%v.dump", time.Now().Unix()))
   516  	message := fmt.Sprintf(format, arg...)
   517  	log.Errorf("出现错误 %v. 详细信息已转储至文件 %v 请连同日志提交给开发者处理", message, dumpFile)
   518  	_ = os.WriteFile(dumpFile, data, 0o644)
   519  }