xdd-plus/qbot/main.go
2023-01-04 17:43:31 +08:00

722 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package qbot
import (
"bufio"
"bytes"
"crypto/aes"
"crypto/md5"
"crypto/sha1"
"encoding/hex"
"flag"
"fmt"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/beego/beego/v2/core/logs"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
base "github.com/Mrs4s/go-cqhttp"
"github.com/Mrs4s/go-cqhttp/coolq"
"github.com/Mrs4s/go-cqhttp/modules/config"
"github.com/cdle/xdd/models"
"github.com/Mrs4s/go-cqhttp/server"
"github.com/Mrs4s/MiraiGo/binary"
"github.com/Mrs4s/MiraiGo/client"
"github.com/Mrs4s/MiraiGo/message"
"github.com/guonaihong/gout"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
log "github.com/sirupsen/logrus"
easy "github.com/t-tomalak/logrus-easy-formatter"
"github.com/tidwall/gjson"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/term"
)
var (
conf *config.Config
isFastStart = false
// PasswordHash 存储QQ密码哈希供登录使用
PasswordHash [16]byte
// AccountToken 存储AccountToken供登录使用
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
func Main() {
time.Sleep(time.Second)
models.SendQQ = func(uid int64, msg interface{}) {
if bot == nil {
return
}
if uid == 0 {
return
}
switch msg.(type) {
case string:
if bot != nil {
if strings.Contains(msg.(string), "data:image") {
photo := msg.(string)
logs.Info(photo)
//b := []byte(photo)
//log.Error(b)
bot.SendPrivateMessage(uid, models.Config.QQGroupID, &message.SendingMessage{Elements: []message.IMessageElement{&coolq.LocalImageElement{File: "./output.jpg"}}})
} else {
bot.SendPrivateMessage(uid, models.Config.QQGroupID, &message.SendingMessage{Elements: []message.IMessageElement{&message.TextElement{Content: msg.(string)}}})
}
}
case *http.Response:
data, _ := ioutil.ReadAll(msg.(*http.Response).Body)
bot.SendPrivateMessage(uid, models.Config.QQGroupID, &message.SendingMessage{Elements: []message.IMessageElement{&coolq.LocalImageElement{Stream: bytes.NewReader(data)}}})
}
}
models.SendQQGroup = func(gid int64, uid int64, msg interface{}) {
if bot == nil {
return
}
switch msg.(type) {
case string:
if bot != nil {
bot.SendGroupMessage(gid, &message.SendingMessage{Elements: []message.IMessageElement{&message.AtElement{Target: uid}, &message.TextElement{Content: msg.(string)}}})
}
case *http.Response:
data, _ := ioutil.ReadAll(msg.(*http.Response).Body)
bot.SendGroupMessage(gid, &message.SendingMessage{Elements: []message.IMessageElement{&message.AtElement{Target: uid}, &message.TextElement{Content: "\n"}, &coolq.LocalImageElement{Stream: bytes.NewReader(data)}}})
}
}
coolq.PrivateMessageEventCallback = models.ListenQQPrivateMessage
coolq.GroupMessageEventCallback = models.ListenQQGroupMessage
// c := flag.String("c", config.DefaultConfigFile, "configuration filename default is config.hjson")
// d := flag.Bool("d", false, "running as a daemon")
// h := flag.Bool("h", false, "this help")
// wd := flag.String("w", "", "cover the working directory")
// debug := flag.Bool("D", false, "debug mode")
// flag.Parse()
// switch {
// case *h:
// help()
// case *d:xs
// server.Daemon()
// case *wd != "":
// resetWorkDir(*wd)
// }
// 通过-c 参数替换 配置文件路径
// config.DefaultConfigFile = models.ExecPath + "/qbot"
// conf = config.Get()
if models.Config.QbotConfigFile != "" {
conf = config.Parse(models.Config.QbotConfigFile)
} else {
conf = &config.Config{}
}
// if *debug {
// conf.Output.Debug = true
// }
if conf.Output.Debug {
log.SetReportCaller(true)
}
logFormatter := &easy.Formatter{
TimestampFormat: "2006-01-02 15:04:05",
LogFormat: "[%time%] [%lvl%]: %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", "%Y-%m-%d.log"), rotateOptions...)
if err != nil {
log.Errorf("rotatelogs init err: %v", err)
panic(err)
}
consoleFormatter := &easy.Formatter{
TimestampFormat: "2006-01-02 15:04:05",
LogFormat: "[%time%] [%lvl%]: %msg% \n",
}
log.AddHook(global.NewLocalHook(w, consoleFormatter, logFormatter, global.GetLogLevel(conf.Output.LogLevel)...))
mkCacheDir := func(path string, _type string) {
if !global.PathExists(path) {
if err := os.MkdirAll(path, 0o755); err != nil {
log.Fatalf("创建%s缓存文件夹失败: %v", _type, err)
}
}
}
mkCacheDir(global.ImagePath, "图片")
mkCacheDir(global.VoicePath, "语音")
mkCacheDir(global.VideoPath, "视频")
mkCacheDir(global.CachePath, "发送图片")
var byteKey []byte
arg := os.Args
if len(arg) > 1 {
for i := range arg {
switch arg[i] {
case "update":
if len(arg) > i+1 {
selfUpdate(arg[i+1])
} else {
selfUpdate("")
}
case "key":
if len(arg) > i+1 {
byteKey = []byte(arg[i+1])
}
case "faststart":
isFastStart = true
}
}
}
// if terminal.RunningByDoubleClick() && !isFastStart {
// log.Warning("警告: 强烈不推荐通过双击直接运行本程序, 这将导致一些非预料的后果.")
// log.Warning("将等待10s后启动")
// time.Sleep(time.Second * 10)
// }
if (conf.Account.Uin == 0 || (conf.Account.Password == "" && !conf.Account.Encrypt)) && !global.PathExists("session.token") {
// log.Warn("账号密码未配置, 将使用二维码登录.")
// if !isFastStart {
// log.Warn("将在 5秒 后继续.")
// time.Sleep(time.Second * 5)
// }
}
// log.Info("当前版本:", coolq.Version)
if conf.Output.Debug {
log.SetLevel(log.DebugLevel)
log.Warnf("已开启Debug模式.")
// log.Debugf("开发交流群: 192548878")
}
// log.Info("用户交流群: 721829413")
if !global.PathExists("device.json") {
log.Warn("虚拟设备信息不存在, 将自动生成随机设备.")
client.GenRandomDevice()
_ = os.WriteFile("device.json", client.SystemDeviceInfo.ToJson(), 0o644)
log.Info("已生成设备信息并保存到 device.json 文件.")
} else {
log.Info("将使用 device.json 内的设备信息运行Bot.")
if err := client.SystemDeviceInfo.ReadJson([]byte(global.ReadAllText("device.json"))); err != nil {
log.Fatalf("加载设备信息失败: %v", err)
}
}
if conf.Account.Encrypt {
if !global.PathExists("password.encrypt") {
if conf.Account.Password == "" {
log.Error("无法进行加密,请在配置文件中的添加密码后重新启动.")
readLine()
os.Exit(0)
}
log.Infof("密码加密已启用, 请输入Key对密码进行加密: (Enter 提交)")
byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
PasswordHash = md5.Sum([]byte(conf.Account.Password))
_ = os.WriteFile("password.encrypt", []byte(PasswordHashEncrypt(PasswordHash[:], byteKey)), 0o644)
log.Info("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
readLine()
os.Exit(0)
} else {
if conf.Account.Password != "" {
log.Error("密码已加密,为了您的账号安全,请删除配置文件中的密码后重新启动.")
readLine()
os.Exit(0)
}
if len(byteKey) == 0 {
log.Infof("密码加密已启用, 请输入Key对密码进行解密以继续: (Enter 提交)")
cancel := make(chan struct{}, 1)
state, _ := term.GetState(int(os.Stdin.Fd()))
go func() {
select {
case <-cancel:
return
case <-time.After(time.Second * 45):
log.Infof("解密key输入超时")
time.Sleep(3 * time.Second)
_ = term.Restore(int(os.Stdin.Fd()), state)
os.Exit(0)
}
}()
byteKey, _ = term.ReadPassword(int(os.Stdin.Fd()))
cancel <- struct{}{}
} else {
log.Infof("密码加密已启用, 使用运行时传递的参数进行解密,按 Ctrl+C 取消.")
}
encrypt, _ := os.ReadFile("password.encrypt")
ph, err := PasswordHashDecrypt(string(encrypt), byteKey)
if err != nil {
log.Fatalf("加密存储的密码损坏,请尝试重新配置密码")
}
copy(PasswordHash[:], ph)
}
} else {
PasswordHash = md5.Sum([]byte(conf.Account.Password))
}
// if !isFastStart {
// log.Info("Bot将在5秒后登录并开始信息处理, 按 Ctrl+C 取消.")
// time.Sleep(time.Second * 5)
// }
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 = newClient()
//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()
_ = os.WriteFile("session.token", AccountToken, 0o644)
}
if global.PathExists("session.token") {
token, err := os.ReadFile("session.token")
if err == nil {
if conf.Account.Uin != 0 {
r := binary.NewReader(token)
cu := r.ReadInt64()
if cu != conf.Account.Uin {
log.Warnf("警告: 配置文件内的QQ号 (%v) 与缓存内的QQ号 (%v) 不相同", conf.Account.Uin, cu)
log.Warnf("1. 使用会话缓存继续.")
log.Warnf("2. 删除会话缓存并重启.")
log.Warnf("请选择: (5秒后自动选1)")
text := readLineTimeout(time.Second*5, "1")
if text == "2" {
_ = os.Remove("session.token")
os.Exit(0)
}
}
}
if err = cli.TokenLogin(token); err != nil {
_ = os.Remove("session.token")
log.Warnf("恢复会话失败: %v , 尝试使用正常流程登录.", err)
time.Sleep(time.Second)
cli.Disconnect()
cli.Release()
cli = newClient()
} 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 = 1 // 重试次数
var reLoginLock sync.Mutex
cli.OnDisconnected(func(q *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)
log.Info("开始加载好友列表...")
global.Check(cli.ReloadFriendList(), true)
log.Infof("共加载 %v 个好友.", len(cli.FriendList))
log.Infof("开始加载群列表...")
global.Check(cli.ReloadGroupList(), true)
log.Infof("共加载 %v 个群.", len(cli.GroupList))
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
if models.Config.IsAddFriend {
bot.Client.OnNewFriendRequest(func(_ *client.QQClient, a *client.NewFriendRequest) {
a.Accept()
})
}
if conf.Message.PostFormat != "string" && conf.Message.PostFormat != "array" {
log.Warnf("post-format 配置错误, 将自动使用 string")
coolq.SetMessageFormat("string")
} else {
coolq.SetMessageFormat(conf.Message.PostFormat)
}
coolq.IgnoreInvalidCQCode = conf.Message.IgnoreInvalidCQCode
coolq.SplitURL = conf.Message.FixURL
coolq.ForceFragmented = conf.Message.ForceFragment
coolq.RemoveReplyAt = conf.Message.RemoveReplyAt
coolq.ExtraReplyData = conf.Message.ExtraReplyData
coolq.SkipMimeScan = conf.Message.SkipMimeScan
for _, m := range conf.Servers {
if h, ok := m["http"]; ok {
hc := new(config.HTTPServer)
if err := h.Decode(hc); err != nil {
log.Warn("读取http配置失败 :", err)
} else {
go server.RunHTTPServerAndClients(bot, hc)
}
}
if s, ok := m["ws"]; ok {
sc := new(config.WebsocketServer)
if err := s.Decode(sc); err != nil {
log.Warn("读取正向Websocket配置失败 :", err)
} else {
go server.RunWebSocketServer(bot, sc)
}
}
if c, ok := m["ws-reverse"]; ok {
rc := new(config.WebsocketReverse)
if err := c.Decode(rc); err != nil {
log.Warn("读取反向Websocket配置失败 :", err)
} else {
go server.RunWebSocketClient(bot, rc)
}
}
if p, ok := m["pprof"]; ok {
pc := new(config.PprofServer)
if err := p.Decode(pc); err != nil {
log.Warn("读取pprof配置失败 :", err)
} else {
go server.RunPprofServer(pc)
}
}
if p, ok := m["lambda"]; ok {
lc := new(config.LambdaServer)
if err := p.Decode(lc); err != nil {
log.Warn("读取pprof配置失败 :", err)
} else {
go server.RunLambdaClient(bot, lc)
}
}
}
log.Info("资源初始化完成, 开始处理信息.")
log.Info("アトリは、高性能ですから!")
// <-global.SetupMainSignalHandler()
}
// PasswordHashEncrypt 使用key加密给定passwordHash
func PasswordHashEncrypt(passwordHash []byte, key []byte) string {
if len(passwordHash) != 16 {
panic("密码加密参数错误")
}
key = pbkdf2.Key(key, key, 114514, 32, sha1.New)
cipher, _ := aes.NewCipher(key)
result := make([]byte, 16)
cipher.Encrypt(result, passwordHash)
return hex.EncodeToString(result)
}
// PasswordHashDecrypt 使用key解密给定passwordHash
func PasswordHashDecrypt(encryptedPasswordHash string, key []byte) ([]byte, error) {
ciphertext, err := hex.DecodeString(encryptedPasswordHash)
if err != nil {
return nil, err
}
key = pbkdf2.Key(key, key, 114514, 32, sha1.New)
cipher, _ := aes.NewCipher(key)
result := make([]byte, 16)
cipher.Decrypt(result, ciphertext)
return result, nil
}
func checkUpdate() {
log.Infof("正在检查更新.")
if coolq.Version == "(devel)" {
log.Warnf("检查更新失败: 使用的 Actions 测试版或自编译版本.")
return
}
var res string
if err := gout.GET("https://api.github.com/repos/Mrs4s/go-cqhttp/releases/latest").BindBody(&res).Do(); err != nil {
log.Warnf("检查更新失败: %v", err)
return
}
info := gjson.Parse(res)
if global.VersionNameCompare(coolq.Version, info.Get("tag_name").Str) {
log.Infof("当前有更新的 go-cqhttp 可供更新, 请前往 https://github.com/Mrs4s/go-cqhttp/releases 下载.")
log.Infof("当前版本: %v 最新版本: %v", coolq.Version, info.Get("tag_name").Str)
return
}
log.Infof("检查更新完成. 当前已运行最新版本.")
}
func selfUpdate(imageURL string) {
log.Infof("正在检查更新.")
var res, r string
if err := gout.GET("https://api.github.com/repos/Mrs4s/go-cqhttp/releases/latest").BindBody(&res).Do(); err != nil {
log.Warnf("检查更新失败: %v", err)
return
}
info := gjson.Parse(res)
version := info.Get("tag_name").Str
if base.Version == version {
log.Info("当前版本已经是最新版本!")
goto wait
}
log.Info("当前最新版本为 ", version)
log.Warn("是否更新(y/N): ")
r = strings.TrimSpace(readLine())
if r != "y" && r != "Y" {
log.Warn("已取消更新!")
} else {
log.Info("正在更新,请稍等...")
sumURL := fmt.Sprintf("%v/Mrs4s/go-cqhttp/releases/download/%v/go-cqhttp_checksums.txt",
func() string {
if imageURL != "" {
return imageURL
}
return "https://github.com"
}(), version)
closer, err := global.HTTPGetReadCloser(sumURL)
if err != nil {
log.Error("更新失败: ", err)
goto wait
}
rd := bufio.NewReader(closer)
binaryName := fmt.Sprintf("go-cqhttp_%v_%v.%v", runtime.GOOS, func() string {
if runtime.GOARCH == "arm" {
return "armv7"
}
return runtime.GOARCH
}(), func() string {
if runtime.GOOS == "windows" {
return "zip"
}
return "tar.gz"
}())
var sum []byte
for {
str, err := rd.ReadString('\n')
if err != nil {
break
}
str = strings.TrimSpace(str)
if strings.HasSuffix(str, binaryName) {
sum, _ = hex.DecodeString(strings.TrimSuffix(str, " "+binaryName))
break
}
}
url := fmt.Sprintf("%v/Mrs4s/go-cqhttp/releases/download/%v/%v",
func() string {
if imageURL != "" {
return imageURL
}
return "https://github.com"
}(), version, binaryName)
err = update.Update(url, sum)
if err != nil {
log.Error("更新失败: ", err)
} else {
log.Info("更新成功!")
}
}
wait:
log.Info("按 Enter 继续....")
readLine()
os.Exit(0)
}
// getCurrentPath 获取当前文件的路径直接返回string
func getCurrentPath() string {
cwd, e := os.Getwd()
if e != nil {
panic(e)
}
return cwd
}
/*
func restart(args []string) {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
file, err := exec.LookPath(args[0])
if err != nil {
log.Errorf("重启失败:%s", err.Error())
return
}
path, err := filepath.Abs(file)
if err != nil {
log.Errorf("重启失败:%s", err.Error())
}
args = append([]string{"/c", "start ", path, "faststart"}, args[1:]...)
cmd = &exec.Cmd{
Path: "cmd.exe",
Args: args,
Stderr: os.Stderr,
Stdout: os.Stdout,
}
} else {
args = append(args, "faststart")
cmd = &exec.Cmd{
Path: args[0],
Args: args,
Stderr: os.Stderr,
Stdout: os.Stdout,
}
}
_ = cmd.Start()
}
*/
// help cli命令行-h的帮助提示
func help() {
fmt.Printf(`go-cqhttp service
version: %s
Usage:
server [OPTIONS]
Options:
`, coolq.Version)
flag.PrintDefaults()
os.Exit(0)
}
func resetWorkDir(wd string) {
args := make([]string, 0, len(os.Args))
for i := 1; i < len(os.Args); i++ {
if os.Args[i] == "-w" {
i++ // skip value field
} else if !strings.HasPrefix(os.Args[i], "-w") {
args = append(args, os.Args[i])
}
}
p, _ := filepath.Abs(os.Args[0])
proc := exec.Command(p, args...)
proc.Stdin = os.Stdin
proc.Stdout = os.Stdout
proc.Stderr = os.Stderr
proc.Dir = wd
err := proc.Run()
if err != nil {
panic(err)
}
os.Exit(0)
}
func newClient() *client.QQClient {
c := client.NewClientEmpty()
c.OnServerUpdated(func(bot *client.QQClient, e *client.ServerUpdatedEvent) bool {
if !conf.Account.UseSSOAddress {
log.Infof("收到服务器地址更新通知, 根据配置文件已忽略.")
return false
}
log.Infof("收到服务器地址更新通知, 将在下一次重连时应用. ")
return true
})
if global.PathExists("address.txt") {
log.Infof("检测到 address.txt 文件. 将覆盖目标IP.")
addr := global.ReadAddrFile("address.txt")
if len(addr) > 0 {
cli.SetCustomServer(addr)
}
log.Infof("读取到 %v 个自定义地址.", len(addr))
}
c.OnLog(func(c *client.QQClient, e *client.LogEvent) {
switch e.Type {
case "INFO":
log.Info("Protocol -> " + e.Message)
case "ERROR":
log.Error("Protocol -> " + e.Message)
case "DEBUG":
log.Debug("Protocol -> " + e.Message)
}
})
return c
}