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 }