From c0a813a09309f46e9258df0c09b79922f89cb090 Mon Sep 17 00:00:00 2001 From: sam01101 Date: Sun, 24 Jan 2021 00:15:05 +0800 Subject: [PATCH 01/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 我只更新了我看到的 --- docs/cqhttp.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/cqhttp.md b/docs/cqhttp.md index f5686b6..08b7fdb 100644 --- a/docs/cqhttp.md +++ b/docs/cqhttp.md @@ -26,9 +26,9 @@ - [获取图片信息](#获取图片信息) - [获取消息](#获取消息) - [获取合并转发内容](#获取合并转发内容) -- [发送合并转发(群)](#发送合并转发(群)) +- [发送合并转发(群)](#发送合并转发群) - [获取中文分词](#获取中文分词) -- [图片OCR](#图片OCR) +- [图片OCR](#图片ocr) - [获取中文分词](#获取中文分词) - [获取群系统消息](#获取群文件系统信息) - [获取群文件系统信息](#获取群文件系统信息) @@ -36,8 +36,11 @@ - [获取群子目录文件列表](#获取群子目录文件列表) - [获取群文件资源链接](#获取群文件资源链接) - [获取状态](#获取状态) -- [获取群子目录文件列表](#设置群名) -- [获取用户VIP信息](#获取用户VIP信息) +- [获取群@全体成员剩余次数](#获取群全体成员剩余次数) +- [下载文件到缓存目录](#下载文件到缓存目录) +- [获取群消息历史记录](#获取群消息历史记录) +- [设置群名](#设置群名) +- [获取用户VIP信息](#获取用户vip信息) - [发送群公告](#发送群公告) - [重载事件过滤器](#重载事件过滤器) @@ -839,7 +842,7 @@ JSON数组: | 字段 | 类型 | 说明 | | ---------- | ------ | ------------------------- | -| `message_seq` | int64 | 起始消息序号, 可通过 `get_msg` 获得 | +| `message_seq` | int64 | 起始消息序号, 可通过 `get_msg` 的 `real_id` 获得 | | `group_id` | int64 | 群号 | **响应数据** From c918182dec10cec5f8967d2c0e00aca837316afd Mon Sep 17 00:00:00 2001 From: sam01101 Date: Sun, 24 Jan 2021 00:20:43 +0800 Subject: [PATCH 02/17] Add time arg optional tips --- docs/cqhttp.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cqhttp.md b/docs/cqhttp.md index 08b7fdb..b3e1894 100644 --- a/docs/cqhttp.md +++ b/docs/cqhttp.md @@ -106,7 +106,7 @@ Type : `reply` | `id` | int | 回复时所引用的消息id, 必须为本群消息. | | `text` | string | 自定义回复的信息 | | `qq` | int64 | 自定义回复时的自定义QQ, 如果使用自定义信息必须指定. | -| `time` | int64 | 自定义回复时的时间, 格式为Unix时间 | +| `time` | int64 | 可选. 自定义回复时的时间, 格式为Unix时间 | @@ -515,7 +515,7 @@ Type: `tts` 响应示例 -````json +````json5 { "data": { "messages": [ @@ -533,7 +533,7 @@ Type: `tts` "nickname": "发送者B", "user_id": 10087 }, - "time": 1595694393 + "time": 1595694393 // 可选 } ] }, From 667ad8fbd6a585c6fcb3932dd5afbb86b6be77af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E6=A9=98=20=E9=9B=AB=E9=9C=9E?= Date: Tue, 9 Feb 2021 03:58:05 +0800 Subject: [PATCH 03/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E5=92=8C=E9=87=8D=E8=BF=9E=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=87=8D=E8=BF=9E=E7=9A=84=E9=AA=8C=E8=AF=81=E7=A0=81?= =?UTF-8?q?=E7=AD=89=E5=A4=84=E7=90=86=EF=BC=8CFix=20#620?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apiAdmin.go | 333 ++++++++++++++++++++++----------------------- 1 file changed, 163 insertions(+), 170 deletions(-) diff --git a/server/apiAdmin.go b/server/apiAdmin.go index c871e3d..b3ec0f0 100644 --- a/server/apiAdmin.go +++ b/server/apiAdmin.go @@ -111,210 +111,203 @@ func (s *webServer) Run(addr string, cli *client.QQClient) *coolq.CQBot { return b } -func (s *webServer) Dologin() { +// logincore 登录核心实现 +func (s *webServer) logincore(relogin bool) { + s.Console = bufio.NewReader(os.Stdin) readLine := func() (str string) { str, _ = s.Console.ReadString('\n') str = strings.TrimSpace(str) return } - conf := GetConf() - cli := s.Cli - cli.AllowSlider = true - rsp, err := cli.Login() - count := 0 - for { - global.Check(err) + + if s.Cli.Online { + log.Warn("Bot已登录") + return + } + + var times uint = 1 // 重试次数 + for res, err := s.Cli.Login(); ; res, err = s.Cli.Login() { + var text string - if !rsp.Success { - switch rsp.Error { - case client.SliderNeededError: - log.Warnf("登录需要滑条验证码, 请选择解决方案: ") - log.Warnf("1. 自行抓包. (推荐)") - log.Warnf("2. 使用Cef自动处理.") - log.Warnf("3. 不提交滑块并继续.(可能会导致上网环境异常错误)") - log.Warnf("详细信息请参考文档 -> https://github.com/Mrs4s/go-cqhttp/blob/master/docs/slider.md <-") - log.Warn("请输入(1 - 3): ") + count := 0 + + if res == nil { + goto Relogin + } + + Again: // 不执行 s.Cli.Login() 的循环,适用输入验证码等更新 res 的操作 + if err == nil && res.Success { // 登录成功 + break + } else if err == client.ErrAlreadyOnline { + break + } + log.Error("登录遇到错误: %v", err) + + switch res.Error { + case client.SliderNeededError: + log.Warnf("登录需要滑条验证码, 请选择解决方案: ") + log.Warnf("1. 自行抓包. (推荐)") + log.Warnf("2. 使用Cef自动处理.") + log.Warnf("3. 不提交滑块并继续.(可能会导致上网环境异常错误)") + log.Warnf("详细信息请参考文档 -> https://github.com/Mrs4s/go-cqhttp/blob/master/docs/slider.md <-") + log.Warn("请输入(1 - 3): ") + text = readLine() + if strings.Contains(text, "1") { + log.Warnf("请用浏览器打开 -> %v <- 并获取Ticket.", res.VerifyUrl) + log.Warn("请输入Ticket: (Enter 提交)") text = readLine() - if strings.Contains(text, "1") { - log.Warnf("请用浏览器打开 -> %v <- 并获取Ticket.", rsp.VerifyUrl) - log.Warn("请输入Ticket: (Enter 提交)") - text = readLine() - rsp, err = cli.SubmitTicket(strings.TrimSpace(text)) - continue - } - if strings.Contains(text, "3") { - cli.AllowSlider = false - cli.Disconnect() - rsp, err = cli.Login() - continue - } - id := utils.RandomStringRange(6, "0123456789") - log.Warnf("滑块ID为 %v 请在30S内处理.", id) - ticket, err := global.GetSliderTicket(rsp.VerifyUrl, id) - if err != nil { - log.Warnf("错误: " + err.Error()) - os.Exit(0) - } - rsp, err = cli.SubmitTicket(ticket) - if err != nil { - log.Warnf("错误: " + err.Error()) - os.Exit(0) - } + res, err = s.Cli.SubmitTicket(strings.TrimSpace(text)) + goto Again + } + if strings.Contains(text, "3") { + s.Cli.AllowSlider = false + s.Cli.Disconnect() continue - case client.NeedCaptcha: - _ = ioutil.WriteFile("captcha.jpg", rsp.CaptchaImage, 0644) - img, _, _ := image.Decode(bytes.NewReader(rsp.CaptchaImage)) - fmt.Println(asciiart.New("image", img).Art) - if conf.WebUI != nil && conf.WebUI.WebInput { - log.Warnf("请输入验证码 (captcha.jpg): (http://%s:%d/admin/do_web_write 输入)", conf.WebUI.Host, conf.WebUI.WebUIPort) - text = <-WebInput - } else { - log.Warn("请输入验证码 (captcha.jpg): (Enter 提交)") - text = readLine() - } - rsp, err = cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), rsp.CaptchaSign) - global.DelFile("captcha.jpg") + } + id := utils.RandomStringRange(6, "0123456789") + log.Warnf("滑块ID为 %v 请在30S内处理.", id) + ticket, err := global.GetSliderTicket(res.VerifyUrl, id) + if err != nil { + log.Warnf("错误: " + err.Error()) + os.Exit(0) + } + res, err = s.Cli.SubmitTicket(ticket) + goto Again + case client.NeedCaptcha: + _ = ioutil.WriteFile("captcha.jpg", res.CaptchaImage, 0644) + img, _, _ := image.Decode(bytes.NewReader(res.CaptchaImage)) + fmt.Println(asciiart.New("image", img).Art) + if s.Conf.WebUI != nil && s.Conf.WebUI.WebInput { + log.Warnf("请输入验证码 (captcha.jpg): (http://%s:%d/admin/do_web_write 输入)", s.Conf.WebUI.Host, s.Conf.WebUI.WebUIPort) + text = <-WebInput + } else { + log.Warn("请输入验证码 (captcha.jpg): (Enter 提交)") + text = readLine() + } + global.DelFile("captcha.jpg") + res, err = s.Cli.SubmitCaptcha(strings.ReplaceAll(text, "\n", ""), res.CaptchaSign) + goto Again + case client.SMSNeededError: + log.Warnf("账号已开启设备锁, 按下 Enter 向手机 %v 发送短信验证码.", res.SMSPhone) + readLine() + if !s.Cli.RequestSMS() { + log.Warnf("发送验证码失败,可能是请求过于频繁.") + time.Sleep(time.Second * 5) continue - case client.SMSNeededError: - log.Warnf("账号已开启设备锁, 按下 Enter 向手机 %v 发送短信验证码.", rsp.SMSPhone) - readLine() - if !cli.RequestSMS() { + } + log.Warn("请输入短信验证码: (Enter 提交)") + text = readLine() + res, err = s.Cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", "")) + goto Again + case client.SMSOrVerifyNeededError: + log.Warnf("账号已开启设备锁,请选择验证方式:") + log.Warnf("1. 向手机 %v 发送短信验证码", res.SMSPhone) + log.Warnf("2. 使用手机QQ扫码验证.") + log.Warn("请输入(1 - 2): ") + text = readLine() + if strings.Contains(text, "1") { + if !s.Cli.RequestSMS() { log.Warnf("发送验证码失败,可能是请求过于频繁.") time.Sleep(time.Second * 5) os.Exit(0) } log.Warn("请输入短信验证码: (Enter 提交)") text = readLine() - rsp, err = cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", "")) + res, err = s.Cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", "")) + goto Again + } + log.Warnf("请前往 -> %v <- 验证.", res.VerifyUrl) + log.Infof("按 Enter 继续....") + readLine() + continue + case client.UnsafeDeviceError: + log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证.", res.VerifyUrl) + if s.Conf.WebUI != nil && s.Conf.WebUI.WebInput { + log.Infof(" (http://%s:%d/admin/do_web_write 确认后继续)....", s.Conf.WebUI.Host, s.Conf.WebUI.WebUIPort) + text = <-WebInput + } else { + log.Infof("按 Enter 继续....") + readLine() + } + log.Info(text) + continue + case client.OtherLoginError, client.UnknownLoginError: + msg := res.ErrorMessage + if strings.Contains(msg, "版本") { + msg = "密码错误或账号被冻结" + } + if strings.Contains(msg, "上网环境") && count < 5 { + s.Cli.Disconnect() + log.Warnf("错误: 当前上网环境异常. 将更换服务器并重试.") + count++ + time.Sleep(time.Second) continue - case client.SMSOrVerifyNeededError: - log.Warnf("账号已开启设备锁,请选择验证方式:") - log.Warnf("1. 向手机 %v 发送短信验证码", rsp.SMSPhone) - log.Warnf("2. 使用手机QQ扫码验证.") - log.Warn("请输入(1 - 2): ") - text = readLine() - if strings.Contains(text, "1") { - if !cli.RequestSMS() { - log.Warnf("发送验证码失败,可能是请求过于频繁.") - time.Sleep(time.Second * 5) - os.Exit(0) - } - log.Warn("请输入短信验证码: (Enter 提交)") - text = readLine() - rsp, err = cli.SubmitSMS(strings.ReplaceAll(strings.ReplaceAll(text, "\n", ""), "\r", "")) - continue - } - log.Warnf("请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl) - log.Infof("按 Enter 继续....") - readLine() - os.Exit(0) - return - case client.UnsafeDeviceError: - log.Warnf("账号已开启设备锁,请前往 -> %v <- 验证并重启Bot.", rsp.VerifyUrl) - if conf.WebUI != nil && conf.WebUI.WebInput { - log.Infof(" (http://%s:%d/admin/do_web_write 确认后继续)....", conf.WebUI.Host, conf.WebUI.WebUIPort) - text = <-WebInput - } else { - log.Infof("按 Enter 继续....") - readLine() - } - log.Info(text) - os.Exit(0) - return - case client.OtherLoginError, client.UnknownLoginError: - msg := rsp.ErrorMessage - if strings.Contains(msg, "版本") { - msg = "密码错误或账号被冻结" - } - if strings.Contains(msg, "上网环境") && count < 5 { - cli.Disconnect() - rsp, err = cli.Login() - count++ - log.Warnf("错误: 当前上网环境异常. 将更换服务器并重试.") - time.Sleep(time.Second) - continue - } - log.Warnf("登录失败: %v", msg) - log.Infof("按 Enter 继续....") - readLine() - os.Exit(0) + } + if strings.Contains(msg, "冻结") { + log.Fatalf("账号被冻结, 放弃重连") + } + log.Warnf("登录失败: %v", msg) + log.Infof("按 Enter 继续....") + readLine() + os.Exit(0) + } + + Relogin: + if relogin { + if times > s.Conf.ReLogin.MaxReloginTimes && s.Conf.ReLogin.MaxReloginTimes != 0 { + log.Fatal("重连失败: 重连次数达到设置的上限值") + s.bot.Release() return } + log.Warnf("将在 %v 秒后尝试重连. 重连次数:%v", s.Conf.ReLogin.ReLoginDelay, times) + times++ + time.Sleep(time.Second * time.Duration(s.Conf.ReLogin.ReLoginDelay)) + s.Cli.Disconnect() + continue } - break } - log.Infof("登录成功 欢迎使用: %v", cli.Nickname) - time.Sleep(time.Second) + if relogin { + log.Info("重连成功") + } +} + +func (s *webServer) Dologin() { + + s.Cli.AllowSlider = true + s.logincore(false) + log.Infof("登录成功 欢迎使用: %v", s.Cli.Nickname) log.Info("开始加载好友列表...") - global.Check(cli.ReloadFriendList()) - log.Infof("共加载 %v 个好友.", len(cli.FriendList)) + global.Check(s.Cli.ReloadFriendList()) + log.Infof("共加载 %v 个好友.", len(s.Cli.FriendList)) log.Infof("开始加载群列表...") - global.Check(cli.ReloadGroupList()) - log.Infof("共加载 %v 个群.", len(cli.GroupList)) - s.bot = coolq.NewQQBot(cli, conf) - if conf.PostMessageFormat != "string" && conf.PostMessageFormat != "array" { + global.Check(s.Cli.ReloadGroupList()) + log.Infof("共加载 %v 个群.", len(s.Cli.GroupList)) + s.bot = coolq.NewQQBot(s.Cli, s.Conf) + if s.Conf.PostMessageFormat != "string" && s.Conf.PostMessageFormat != "array" { log.Warnf("post_message_format 配置错误, 将自动使用 string") coolq.SetMessageFormat("string") } else { - coolq.SetMessageFormat(conf.PostMessageFormat) + coolq.SetMessageFormat(s.Conf.PostMessageFormat) } - if conf.RateLimit.Enabled { - global.InitLimiter(conf.RateLimit.Frequency, conf.RateLimit.BucketSize) + if s.Conf.RateLimit.Enabled { + global.InitLimiter(s.Conf.RateLimit.Frequency, s.Conf.RateLimit.BucketSize) } log.Info("正在加载事件过滤器.") global.BootFilter() global.InitCodec() - coolq.IgnoreInvalidCQCode = conf.IgnoreInvalidCQCode - coolq.SplitUrl = conf.FixURL - coolq.ForceFragmented = conf.ForceFragmented + coolq.IgnoreInvalidCQCode = s.Conf.IgnoreInvalidCQCode + coolq.SplitUrl = s.Conf.FixURL + coolq.ForceFragmented = s.Conf.ForceFragmented log.Info("资源初始化完成, 开始处理信息.") log.Info("アトリは、高性能ですから!") - cli.OnDisconnected(func(bot *client.QQClient, e *client.ClientDisconnectedEvent) { - if conf.ReLogin.Enabled { - conf.ReLogin.Enabled = false - defer func() { conf.ReLogin.Enabled = true }() - var times uint = 1 - for { - if cli.Online { - log.Warn("Bot已登录") - return - } - if times > conf.ReLogin.MaxReloginTimes && conf.ReLogin.MaxReloginTimes != 0 { - break - } - log.Warnf("Bot已离线 (%v),将在 %v 秒后尝试重连. 重连次数:%v", - e.Message, conf.ReLogin.ReLoginDelay, times) - times++ - time.Sleep(time.Second * time.Duration(conf.ReLogin.ReLoginDelay)) - rsp, err := cli.Login() - if err != nil { - log.Errorf("重连失败: %v", err) - cli.Disconnect() - continue - } - if !rsp.Success { - switch rsp.Error { - case client.NeedCaptcha: - log.Fatalf("重连失败: 需要验证码. (验证码处理正在开发中)") - case client.UnsafeDeviceError: - log.Fatalf("重连失败: 设备锁") - default: - log.Errorf("重连失败: %v", rsp.ErrorMessage) - if strings.Contains(rsp.ErrorMessage, "冻结") { - log.Fatalf("账号被冻结, 放弃重连") - } - cli.Disconnect() - continue - } - } - log.Info("重连成功") - return - } - log.Fatal("重连失败: 重连次数达到设置的上限值") + + s.Cli.OnDisconnected(func(q *client.QQClient, e *client.ClientDisconnectedEvent) { + if !s.Conf.ReLogin.Enabled { + return } - s.bot.Release() - log.Fatalf("Bot已离线:%v", e.Message) + log.Warnf("Bot已离线 (%v),尝试重连", e.Message) + s.logincore(true) }) } From cc14be66bde8063f0739126eaad3361cb69c999d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E6=A9=98=20=E9=9B=AB=E9=9C=9E?= Date: Tue, 9 Feb 2021 22:27:33 +0800 Subject: [PATCH 04/17] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apiAdmin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/apiAdmin.go b/server/apiAdmin.go index b3ec0f0..db5187a 100644 --- a/server/apiAdmin.go +++ b/server/apiAdmin.go @@ -272,6 +272,7 @@ func (s *webServer) logincore(relogin bool) { } } +// Dologin 主程序登录 func (s *webServer) Dologin() { s.Cli.AllowSlider = true From f5bd4644fdcd9de7543330b9a3576f7e86ee506a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E6=A9=98=20=E9=9B=AB=E9=9C=9E?= Date: Thu, 11 Feb 2021 15:44:47 +0800 Subject: [PATCH 05/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E5=88=86=E6=94=AF=E5=86=B2=E7=AA=81=E6=97=B6=E7=9A=84=E7=96=8F?= =?UTF-8?q?=E5=BF=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apiAdmin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/apiAdmin.go b/server/apiAdmin.go index a6ca504..ec442ac 100644 --- a/server/apiAdmin.go +++ b/server/apiAdmin.go @@ -303,7 +303,7 @@ func (s *webServer) Dologin() { log.Info("正在加载事件过滤器.") global.BootFilter() coolq.IgnoreInvalidCQCode = s.Conf.IgnoreInvalidCQCode - coolq.SplitUrl = s.Conf.FixURL + coolq.SplitURL = s.Conf.FixURL coolq.ForceFragmented = s.Conf.ForceFragmented log.Info("资源初始化完成, 开始处理信息.") log.Info("アトリは、高性能ですから!") From 775a210e3e74630b4c7009bd77637a46904fea07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E6=A9=98=20=E9=9B=AB=E9=9C=9E?= Date: Thu, 11 Feb 2021 15:54:52 +0800 Subject: [PATCH 06/17] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20GitHub=20Actions/=20?= =?UTF-8?q?lint=20=E7=9A=84=20Check=20failure=20=E5=91=9C=E5=91=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/apiAdmin.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/apiAdmin.go b/server/apiAdmin.go index ec442ac..0ec1b7f 100644 --- a/server/apiAdmin.go +++ b/server/apiAdmin.go @@ -148,7 +148,7 @@ func (s *webServer) logincore(relogin bool) { } else if err == client.ErrAlreadyOnline { break } - log.Error("登录遇到错误: %v", err) + log.Error("登录遇到错误: " + err.Error()) switch res.Error { case client.SliderNeededError: @@ -179,6 +179,10 @@ func (s *webServer) logincore(relogin bool) { os.Exit(0) } res, err = s.Cli.SubmitTicket(ticket) + if err != nil { + log.Warnf("错误: " + err.Error()) + continue // 尝试重新登录 + } goto Again case client.NeedCaptcha: _ = ioutil.WriteFile("captcha.jpg", res.CaptchaImage, 0644) From 879cbe4f191efc1f5c55ed3976ecdacee484558b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E6=A9=98=20=E9=9B=AB=E9=9C=9E?= Date: Sat, 13 Feb 2021 00:27:49 +0800 Subject: [PATCH 07/17] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E3=80=8A=E6=8F=90?= =?UTF-8?q?=E9=97=AE=E7=9A=84=E6=99=BA=E6=85=A7=E3=80=8B=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index db63ab3..21a267c 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,8 @@ go-cqhttp 在[原版 cqhttp](https://github.com/richardchien/coolq-http-api)的 - 提问找不到重点 - 重复提问 -> 请注意, 开发者并没有义务回复您的问题. 您应该具备基本的提问技巧。 +> 请注意, 开发者并没有义务回复您的问题. 您应该具备基本的提问技巧。 +> 有关如何提问,请阅读[《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md) ## 性能 From 0e71acaf38ba7ed9d9c0b77b4d8269f107c94cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E6=A9=98=20=E9=9B=AB=E9=9C=9E?= Date: Sat, 13 Feb 2021 00:35:01 +0800 Subject: [PATCH 08/17] =?UTF-8?q?=E5=BB=BA=E8=AE=AE=E9=98=85=E8=AF=BB?= =?UTF-8?q?=E3=80=8A=E6=8F=90=E9=97=AE=E7=9A=84=E6=99=BA=E6=85=A7=E3=80=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug--.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug--.md b/.github/ISSUE_TEMPLATE/bug--.md index 9135cd0..423a32f 100644 --- a/.github/ISSUE_TEMPLATE/bug--.md +++ b/.github/ISSUE_TEMPLATE/bug--.md @@ -13,6 +13,7 @@ assignees: '' 2: 准确填写环境信息. 3: 最好能打开DEBUG模式并复现相关问题. 4: 如果涉及内存泄漏/CPU占用异常请打开DEBUG模式并下载pprof性能分析. +注: 如果您不知道如何有效、精准地表述,我们建议您先阅读《提问的智慧》 -> https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md --> **环境信息** From 31cfb9283e85d209dd5277fb98aceec3f462b58e Mon Sep 17 00:00:00 2001 From: nnnewb Date: Sat, 13 Feb 2021 16:50:17 +0800 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20at=5Fsender?= =?UTF-8?q?=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coolq/api.go | 45 +++++++++++++++++++++++++++++++++++---------- coolq/cqcode.go | 2 +- go.mod | 14 ++++++++------ go.sum | 46 ++++++++-------------------------------------- 4 files changed, 52 insertions(+), 55 deletions(-) diff --git a/coolq/api.go b/coolq/api.go index 3bf1927..9a15135 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -3,6 +3,12 @@ package coolq import ( "crypto/md5" "encoding/hex" + "fmt" + "github.com/Mrs4s/MiraiGo/binary" + "github.com/Mrs4s/MiraiGo/client" + "github.com/Mrs4s/MiraiGo/message" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" "io/ioutil" "math" "os" @@ -13,12 +19,8 @@ import ( "strings" "time" - "github.com/Mrs4s/MiraiGo/binary" - "github.com/Mrs4s/MiraiGo/client" - "github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/go-cqhttp/global" log "github.com/sirupsen/logrus" - "github.com/tidwall/gjson" ) // Version go-cqhttp的版本信息,在编译时使用ldfalgs进行覆盖 @@ -809,13 +811,36 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { reply := operation.Get("reply") if reply.Exists() { autoEscape := global.EnsureBool(operation.Get("auto_escape"), false) - /* - at := true - if operation.Get("at_sender").Exists() { - at = operation.Get("at_sender").Bool() + + at := false + if operation.Get("at_sender").Exists() { + at = operation.Get("at_sender").Bool() + } + + if at && reply.IsArray() { + modified, err := sjson.Set( + reply.Raw, + "-1", + MSG{ + "type": "at", + "data": MSG{ + "qq": context.Get("sender.user_id").Int(), + }, + }, + ) + if err != nil { + return Failed(-1, "处理 at_sender 字段时出现错误", err.Error()) } - */ - // TODO: 处理at字段 + + reply = gjson.Parse(modified) + } else if at && reply.Type == gjson.String { + reply = gjson.Parse(fmt.Sprintf( + "\"%s[CQ:at,qq=%d]\"", + reply.String(), + context.Get("sender.user_id").Int(), + )) + } + if msgType == "group" { bot.CQSendGroupMessage(context.Get("group_id").Int(), reply, autoEscape) } diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 2a02a51..6268617 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -549,7 +549,7 @@ func (bot *CQBot) ConvertObjectMessage(m gjson.Result, isGroup bool) (r []messag }) elem, err := bot.ToElement(t, d, isGroup) if err != nil { - log.Warnf("转换CQ码到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", err) + log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) return } switch i := elem.(type) { diff --git a/go.mod b/go.mod index 22a4c82..21fd61c 100644 --- a/go.mod +++ b/go.mod @@ -3,27 +3,29 @@ module github.com/Mrs4s/go-cqhttp go 1.15 require ( - github.com/Mrs4s/MiraiGo v0.0.0-20210211030658-9f1cf68e0e7c + github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e github.com/dustin/go-humanize v1.0.0 github.com/gin-contrib/pprof v1.3.0 github.com/gin-gonic/gin v1.6.3 github.com/gorilla/websocket v1.4.2 - github.com/guonaihong/gout v0.1.5 + github.com/guonaihong/gout v0.1.4 github.com/hjson/hjson-go v3.1.0+incompatible + github.com/jonboulle/clockwork v0.2.2 // indirect github.com/json-iterator/go v1.1.10 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible + github.com/lestrrat-go/strftime v1.0.4 // indirect github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.7.0 github.com/syndtr/goleveldb v1.0.0 github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 github.com/tidwall/gjson v1.6.8 + github.com/tidwall/sjson v1.1.5 github.com/wdvxdr1123/go-silk v0.0.0-20210207032612-169bbdf8861d github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect - golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 - gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/go.sum b/go.sum index 5d5e3ac..a63ffde 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/LXY1226/fastrand v0.0.0-20210121160840-7a3db3e79031 h1:DnoCySrXUFvtngW2kSkuBeZoPfvOgctJXjTulCn7eV0= -github.com/LXY1226/fastrand v0.0.0-20210121160840-7a3db3e79031/go.mod h1:mEFi4jHUsE2sqQGSJ7eQfXnO8esMzEYcftiCGG+L/OE= -github.com/Mrs4s/MiraiGo v0.0.0-20210125093830-340977eb201f h1:v86jOk27ypxD3gT48KJDy/Y5w7PIaTvabZYdDszr3w0= -github.com/Mrs4s/MiraiGo v0.0.0-20210125093830-340977eb201f/go.mod h1:JBm2meosyXAASbl8mZ+mFZEkE/2cC7zNZdIOBe7+QhY= github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e h1:SnN+nyRdqN7sULnHUWCofP+Jxs3VJN/y8AlMpcz0nbk= github.com/Mrs4s/MiraiGo v0.0.0-20210206134348-800bf525ed0e/go.mod h1:yhqA0NyKxUf7I/0HR/1OMchveFggX8wde04gqdGrNfU= -github.com/Mrs4s/MiraiGo v0.0.0-20210211030658-9f1cf68e0e7c h1:H5RT6SybX5six6VZpdQRmUOV8XcqoQQ4cZM0gZ0yeNo= -github.com/Mrs4s/MiraiGo v0.0.0-20210211030658-9f1cf68e0e7c/go.mod h1:yhqA0NyKxUf7I/0HR/1OMchveFggX8wde04gqdGrNfU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,8 +29,6 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -52,8 +44,6 @@ github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= -github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -67,15 +57,12 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/guonaihong/gout v0.1.4 h1:uBBoyztMX9okC27OQxqhn6bZ0ROkGyvnEIHwtp3TM4g= github.com/guonaihong/gout v0.1.4/go.mod h1:0rFYAYyzbcxEg11eY2qUbffJs7hHRPeugAnlVYSp8Ic= -github.com/guonaihong/gout v0.1.5 h1:1FeFFJWWdWYApBW9d6vzMDB4eR4Zr8T/gaVrjDVcl5U= -github.com/guonaihong/gout v0.1.5/go.mod h1:0rFYAYyzbcxEg11eY2qUbffJs7hHRPeugAnlVYSp8Ic= github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw= github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -84,8 +71,6 @@ github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALr github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= @@ -96,12 +81,8 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -126,29 +107,24 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= -github.com/tidwall/gjson v1.6.7 h1:Mb1M9HZCRWEcXQ8ieJo7auYyyiSux6w9XN3AdTpxJrE= -github.com/tidwall/gjson v1.6.7/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w= github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE= +github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc= -github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo4= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8= -github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA= github.com/wdvxdr1123/go-silk v0.0.0-20210207032612-169bbdf8861d h1:gJTKbjZtlMt/almOeFi/UpVtT3RHqRWscgEuDtnF5TU= github.com/wdvxdr1123/go-silk v0.0.0-20210207032612-169bbdf8861d/go.mod h1:twOxzexmM2Il1ReUu1fB5tnUotOq/dp56xjk/ZHwb1I= github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 h1:4UJw9if55Fu3HOwbfcaQlJ27p3oeJU2JZqoeT3ITJQk= @@ -157,8 +133,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -171,8 +145,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -184,19 +158,16 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818 h1:f1CIuDlJhwANEC2MM87MBEVMr3jl5bifgsfj90XAF9c= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -205,8 +176,9 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -235,8 +207,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From fba4819adce27f1ef0a1d77f71420dc97863337c Mon Sep 17 00:00:00 2001 From: nnnewb Date: Sat, 13 Feb 2021 18:05:28 +0800 Subject: [PATCH 10/17] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20at=5Fsender?= =?UTF-8?q?=20=E9=BB=98=E8=AE=A4=E8=A1=8C=E4=B8=BA=E7=AC=A6=E5=90=88=20one?= =?UTF-8?q?bot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coolq/api.go | 44 +- coolq/cqcode.go | 2262 +++++++++++++++++++++++------------------------ go.mod | 1 - go.sum | 2 - 4 files changed, 1157 insertions(+), 1152 deletions(-) diff --git a/coolq/api.go b/coolq/api.go index 9a15135..da5f20e 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -8,7 +8,6 @@ import ( "github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/message" "github.com/tidwall/gjson" - "github.com/tidwall/sjson" "io/ioutil" "math" "os" @@ -805,6 +804,9 @@ func (bot *CQBot) CQGetStrangerInfo(userID int64) MSG { // https://git.io/Jtz15 func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { postType := context.Get("post_type").Str + anonymous := context.Get("anonymous") + isAnonymous := anonymous.Type == gjson.Null + switch postType { case "message": msgType := context.Get("message_type").Str @@ -812,32 +814,40 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { if reply.Exists() { autoEscape := global.EnsureBool(operation.Get("auto_escape"), false) - at := false + at := !isAnonymous // 除匿名消息场合外默认 true if operation.Get("at_sender").Exists() { at = operation.Get("at_sender").Bool() } - if at && reply.IsArray() { - modified, err := sjson.Set( - reply.Raw, - "-1", - MSG{ - "type": "at", - "data": MSG{ - "qq": context.Get("sender.user_id").Int(), - }, + if !isAnonymous && at && reply.IsArray() { + // 在 reply 数组头部插入CQ码 + replySegments := make([]MSG, 0) + segments := make([]MSG, 0) + segments = append(segments, MSG{ + "type": "at", + "data": MSG{ + "qq": context.Get("sender.user_id").Int(), }, - ) + }) + + err := json.UnmarshalFromString(reply.Raw, replySegments) if err != nil { - return Failed(-1, "处理 at_sender 字段时出现错误", err.Error()) + return Failed(-1, "处理 at_sender 过程中发生错误", err.Error()) + } + + segments = append(segments, replySegments...) + + modified, err := json.MarshalToString(segments) + if err != nil { + return Failed(-1, "处理 at_sender 过程中发生错误", err.Error()) } reply = gjson.Parse(modified) - } else if at && reply.Type == gjson.String { + } else if !isAnonymous && at && reply.Type == gjson.String { reply = gjson.Parse(fmt.Sprintf( - "\"%s[CQ:at,qq=%d]\"", - reply.String(), + "\"[CQ:at,qq=%d]%s\"", context.Get("sender.user_id").Int(), + reply.String(), )) } @@ -849,8 +859,6 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { } } if msgType == "group" { - anonymous := context.Get("anonymous") - isAnonymous := anonymous.Type == gjson.Null if operation.Get("delete").Bool() { bot.CQDeleteMessage(int32(context.Get("message_id").Int())) } diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 6268617..77c16ee 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -1,1131 +1,1131 @@ -package coolq - -import ( - "bytes" - "crypto/md5" - "encoding/base64" - "encoding/hex" - xml2 "encoding/xml" - "errors" - "fmt" - "io" - "io/ioutil" - "math" - "math/rand" - "net/url" - "os" - "path" - "runtime" - "strconv" - "strings" - "time" - - "github.com/Mrs4s/MiraiGo/binary" - "github.com/Mrs4s/MiraiGo/message" - "github.com/Mrs4s/MiraiGo/utils" - "github.com/Mrs4s/go-cqhttp/global" - log "github.com/sirupsen/logrus" - "github.com/tidwall/gjson" -) - -/* -var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`) -var typeReg = regexp.MustCompile(`\[CQ:(\w+)`) -var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) -*/ - -// IgnoreInvalidCQCode 是否忽略无效CQ码 -var IgnoreInvalidCQCode = false - -// SplitURL 是否分割URL -var SplitURL = false - -const maxImageSize = 1024 * 1024 * 30 // 30MB -const maxVideoSize = 1024 * 1024 * 100 // 100MB -// PokeElement 拍一拍 -type PokeElement struct { - Target int64 -} - -// GiftElement 礼物 -type GiftElement struct { - Target int64 - GiftID message.GroupGift -} - -// LocalImageElement 本地图片 -type LocalImageElement struct { - message.ImageElement - Stream io.ReadSeeker - File string -} - -// LocalVoiceElement 本地语音 -type LocalVoiceElement struct { - message.VoiceElement - Stream io.ReadSeeker -} - -// LocalVideoElement 本地视频 -type LocalVideoElement struct { - message.ShortVideoElement - File string - thumb io.ReadSeeker -} - -// Type 获取元素类型ID -func (e *GiftElement) Type() message.ElementType { - // Make message.IMessageElement Happy - return message.At -} - -// GiftID 礼物ID数组 -var GiftID = [...]message.GroupGift{ - message.SweetWink, - message.HappyCola, - message.LuckyBracelet, - message.Cappuccino, - message.CatWatch, - message.FleeceGloves, - message.RainbowCandy, - message.Stronger, - message.LoveMicrophone, - message.HoldingYourHand, - message.CuteCat, - message.MysteryMask, - message.ImBusy, - message.LoveMask, -} - -// Type 获取元素类型ID -func (e *PokeElement) Type() message.ElementType { - // Make message.IMessageElement Happy - return message.At -} - -// ToArrayMessage 将消息元素数组转为MSG数组以用于消息上报 -func ToArrayMessage(e []message.IMessageElement, id int64, isRaw ...bool) (r []MSG) { - r = []MSG{} - ur := false - if len(isRaw) != 0 { - ur = isRaw[0] - } - m := &message.SendingMessage{Elements: e} - reply := m.FirstOrNil(func(e message.IMessageElement) bool { - _, ok := e.(*message.ReplyElement) - return ok - }) - if reply != nil { - r = append(r, MSG{ - "type": "reply", - "data": map[string]string{"id": fmt.Sprint(toGlobalID(id, reply.(*message.ReplyElement).ReplySeq))}, - }) - } - for _, elem := range e { - var m MSG - switch o := elem.(type) { - case *message.TextElement: - m = MSG{ - "type": "text", - "data": map[string]string{"text": o.Content}, - } - case *message.LightAppElement: - // m = MSG{ - // "type": "text", - // "data": map[string]string{"text": o.Content}, - // } - m = MSG{ - "type": "json", - "data": map[string]string{"data": o.Content}, - } - case *message.AtElement: - if o.Target == 0 { - m = MSG{ - "type": "at", - "data": map[string]string{"qq": "all"}, - } - } else { - m = MSG{ - "type": "at", - "data": map[string]string{"qq": fmt.Sprint(o.Target)}, - } - } - case *message.RedBagElement: - m = MSG{ - "type": "redbag", - "data": map[string]string{"title": o.Title}, - } - case *message.ForwardElement: - m = MSG{ - "type": "forward", - "data": map[string]string{"id": o.ResId}, - } - case *message.FaceElement: - m = MSG{ - "type": "face", - "data": map[string]string{"id": fmt.Sprint(o.Index)}, - } - case *message.VoiceElement: - if ur { - m = MSG{ - "type": "record", - "data": map[string]string{"file": o.Name}, - } - } else { - m = MSG{ - "type": "record", - "data": map[string]string{"file": o.Name, "url": o.Url}, - } - } - case *message.ShortVideoElement: - if ur { - m = MSG{ - "type": "video", - "data": map[string]string{"file": o.Name}, - } - } else { - m = MSG{ - "type": "video", - "data": map[string]string{"file": o.Name, "url": o.Url}, - } - } - case *message.ImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": o.Filename}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": o.Filename, "url": o.Url}, - } - } - case *message.GroupImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, - } - } - case *message.FriendImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, - } - } - case *message.GroupFlashImgElement: - return []MSG{{ - "type": "image", - "data": map[string]string{"file": o.Filename, "type": "flash"}, - }} - case *message.FriendFlashImgElement: - return []MSG{{ - "type": "image", - "data": map[string]string{"file": o.Filename, "type": "flash"}, - }} - case *message.ServiceElement: - if isOk := strings.Contains(o.Content, " 0 { - if _, ok := r[0].(*message.ReplyElement); ok { - log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") - return - } - } - mid, err := strconv.Atoi(params["id"]) - customText := params["text"] - if err == nil { - org := bot.GetMessage(int32(mid)) - if org != nil { - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: org["message-id"].(int32), - Sender: org["sender"].(message.Sender).Uin, - Time: org["time"].(int32), - Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), - }, - }, r...) - return - } - } else if customText != "" { - sender, err := strconv.ParseInt(params["qq"], 10, 64) - if err != nil { - log.Warnf("警告:自定义 Reply 元素中必须包含Uin") - return - } - msgTime, err := strconv.ParseInt(params["time"], 10, 64) - if err != nil { - msgTime = time.Now().Unix() - } - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: int32(0), - Sender: sender, - Time: int32(msgTime), - Elements: bot.ConvertStringMessage(customText, isGroup), - }, - }, r...) - return - } - } - if t == "forward" { // 单独处理转发 - if id, ok := params["id"]; ok { - r = []message.IMessageElement{bot.Client.DownloadForwardMessage(id)} - return - } - } - elem, err := bot.ToElement(t, params, isGroup) - if err != nil { - org := "[CQ:" + string(cqCode) + "]" - if !IgnoreInvalidCQCode { - log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) - r = append(r, message.NewText(org)) - } else { - log.Warnf("转换CQ码 %v 时出现错误: %v 将忽略.", org, err) - } - return - } - switch i := elem.(type) { - case message.IMessageElement: - r = append(r, i) - case []message.IMessageElement: - r = append(r, i...) - } - } - for hasNext() { - ch := next() - switch stat { - case 0: - if isCQCodeBegin(ch) { - saveTempText() - tempText = append(tempText, []rune("[CQ:")...) - move(3) - stat = 1 - } else { - tempText = append(tempText, ch) - } - case 1: - if isCQCodeBegin(ch) { - move(-1) - stat = 0 - } else if ch == ']' { - saveCQCode() - stat = 0 - } else { - cqCode = append(cqCode, ch) - tempText = append(tempText, ch) - } - } - } - saveTempText() - return -} - -// ConvertObjectMessage 将消息JSON对象转为消息元素数组 -func (bot *CQBot) ConvertObjectMessage(m gjson.Result, isGroup bool) (r []message.IMessageElement) { - convertElem := func(e gjson.Result) { - t := e.Get("type").Str - if t == "reply" && isGroup { - if len(r) > 0 { - if _, ok := r[0].(*message.ReplyElement); ok { - log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") - return - } - } - mid, err := strconv.Atoi(e.Get("data").Get("id").String()) - customText := e.Get("data").Get("text").String() - if err == nil { - org := bot.GetMessage(int32(mid)) - if org != nil { - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: org["message-id"].(int32), - Sender: org["sender"].(message.Sender).Uin, - Time: org["time"].(int32), - Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), - }, - }, r...) - return - } - } else if customText != "" { - sender, err := strconv.ParseInt(e.Get("data").Get("qq").String(), 10, 64) - if err != nil { - log.Warnf("警告:自定义 Reply 元素中必须包含Uin") - return - } - msgTime, err := strconv.ParseInt(e.Get("data").Get("time").String(), 10, 64) - if err != nil { - msgTime = time.Now().Unix() - } - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: int32(0), - Sender: sender, - Time: int32(msgTime), - Elements: bot.ConvertStringMessage(customText, isGroup), - }, - }, r...) - return - } - } - if t == "forward" { - r = []message.IMessageElement{bot.Client.DownloadForwardMessage(e.Get("data.id").String())} - return - } - d := make(map[string]string) - e.Get("data").ForEach(func(key, value gjson.Result) bool { - d[key.Str] = value.String() - return true - }) - elem, err := bot.ToElement(t, d, isGroup) - if err != nil { - log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) - return - } - switch i := elem.(type) { - case message.IMessageElement: - r = append(r, i) - case []message.IMessageElement: - r = append(r, i...) - } - } - if m.Type == gjson.String { - return bot.ConvertStringMessage(m.Str, isGroup) - } - if m.IsArray() { - for _, e := range m.Array() { - convertElem(e) - } - } - if m.IsObject() { - convertElem(m) - } - return -} - -// ToElement 将解码后的CQCode转换为Element. -// -// 返回 interface{} 存在三种类型 -// -// message.IMessageElement []message.IMessageElement nil -func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m interface{}, err error) { - switch t { - case "text": - if SplitURL { - var ret []message.IMessageElement - for _, text := range global.SplitURL(d["text"]) { - ret = append(ret, message.NewText(text)) - } - return ret, nil - } - return message.NewText(d["text"]), nil - case "image": - img, err := bot.makeImageOrVideoElem(d, false, isGroup) - if err != nil { - return nil, err - } - tp := d["type"] - if tp != "show" && tp != "flash" { - return img, nil - } - if i, ok := img.(*LocalImageElement); ok { // 秀图,闪照什么的就直接传了吧 - if isGroup { - img, err = bot.UploadLocalImageAsGroup(1, i) - } else { - img, err = bot.UploadLocalImageAsPrivate(1, i) - } - if err != nil { - return nil, err - } - } - switch tp { - case "flash": - if i, ok := img.(*message.GroupImageElement); ok { - return &message.GroupFlashPicElement{GroupImageElement: *i}, nil - } - if i, ok := img.(*message.FriendImageElement); ok { - return &message.FriendFlashPicElement{FriendImageElement: *i}, nil - } - case "show": - id, _ := strconv.ParseInt(d["id"], 10, 64) - if id < 40000 || id >= 40006 { - id = 40000 - } - if i, ok := img.(*message.GroupImageElement); ok { - return &message.GroupShowPicElement{GroupImageElement: *i, EffectId: int32(id)}, nil - } - return img, nil // 私聊还没做 - } - - case "poke": - t, _ := strconv.ParseInt(d["qq"], 10, 64) - return &PokeElement{Target: t}, nil - case "gift": - if !isGroup { - return nil, errors.New("private gift unsupported") // no free private gift - } - t, _ := strconv.ParseInt(d["qq"], 10, 64) - id, _ := strconv.Atoi(d["id"]) - if id < 0 || id >= 14 { - return nil, errors.New("invalid gift id") - } - return &GiftElement{Target: t, GiftID: GiftID[id]}, nil - case "tts": - defer func() { - if r := recover(); r != nil { - m = nil - err = errors.New("tts 转换失败") - } - }() - data, err := bot.Client.GetTts(d["text"]) - if err != nil { - return nil, err - } - return &message.VoiceElement{Data: data}, nil - case "record": - f := d["file"] - data, err := global.FindFile(f, d["cache"], global.VoicePath) - if err == global.ErrSyntax { - data, err = global.FindFile(f, d["cache"], global.VoicePathOld) - } - if err != nil { - return nil, err - } - if !global.IsAMRorSILK(data) { - data, err = global.EncoderSilk(data) - if err != nil { - return nil, err - } - } - return &message.VoiceElement{Data: data}, nil - case "face": - id, err := strconv.Atoi(d["id"]) - if err != nil { - return nil, err - } - return message.NewFace(int32(id)), nil - case "at": - qq := d["qq"] - if qq == "all" { - return message.AtAll(), nil - } - t, _ := strconv.ParseInt(qq, 10, 64) - return message.NewAt(t), nil - case "share": - return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil - case "music": - if d["type"] == "qq" { - info, err := global.QQMusicSongInfo(d["id"]) - if err != nil { - return nil, err - } - if !info.Get("track_info").Exists() { - return nil, errors.New("song not found") - } - aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10) - name := info.Get("track_info.name").Str - mid := info.Get("track_info.mid").Str - albumMid := info.Get("track_info.album.mid").Str - pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") - jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" - purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str - preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" - if len(aid) < 2 { - return nil, errors.New("song error") - } - content := info.Get("track_info.singer.0.name").Str - if d["content"] != "" { - content = d["content"] - } - return &message.MusicShareElement{ - MusicType: message.QQMusic, - Title: name, - Summary: content, - Url: jumpURL, - PictureUrl: preview, - MusicUrl: purl, - }, nil - } - if d["type"] == "163" { - info, err := global.NeteaseMusicSongInfo(d["id"]) - if err != nil { - return nil, err - } - if !info.Exists() { - return nil, errors.New("song not found") - } - name := info.Get("name").Str - jumpURL := "https://y.music.163.com/m/song/" + d["id"] - musicURL := "http://music.163.com/song/media/outer/url?id=" + d["id"] - picURL := info.Get("album.picUrl").Str - artistName := "" - if info.Get("artists.0").Exists() { - artistName = info.Get("artists.0.name").Str - } - return &message.MusicShareElement{ - MusicType: message.CloudMusic, - Title: name, - Summary: artistName, - Url: jumpURL, - PictureUrl: picURL, - MusicUrl: musicURL, - }, nil - } - if d["type"] == "custom" { - if d["subtype"] != "" { - var subtype = map[string]int{ - "qq": message.QQMusic, - "163": message.CloudMusic, - "migu": message.MiguMusic, - "kugou": message.KugouMusic, - "kuwo": message.KuwoMusic, - } - var musicType = 0 - if tp, ok := subtype[d["subtype"]]; ok { - musicType = tp - } - return &message.MusicShareElement{ - MusicType: musicType, - Title: d["title"], - Summary: d["content"], - Url: d["url"], - PictureUrl: d["image"], - MusicUrl: d["purl"], - }, nil - } - xml := fmt.Sprintf(``, - XMLEscape(d["title"]), d["url"], d["image"], d["audio"], XMLEscape(d["title"]), XMLEscape(d["content"])) - return &message.ServiceElement{ - Id: 60, - Content: xml, - SubType: "music", - }, nil - } - return nil, errors.New("unsupported music type: " + d["type"]) - case "xml": - resID := d["resid"] - template := CQCodeEscapeValue(d["data"]) - i, _ := strconv.ParseInt(resID, 10, 64) - msg := message.NewRichXml(template, i) - return msg, nil - case "json": - resID := d["resid"] - i, _ := strconv.ParseInt(resID, 10, 64) - if i == 0 { - // 默认情况下走小程序通道 - msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) - return msg, nil - } - // resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO - msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) - return msg, nil - case "cardimage": - source := d["source"] - icon := d["icon"] - minWidth, _ := strconv.ParseInt(d["minwidth"], 10, 64) - if minWidth == 0 { - minWidth = 200 - } - minHeight, _ := strconv.ParseInt(d["minheight"], 10, 64) - if minHeight == 0 { - minHeight = 200 - } - maxWidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64) - if maxWidth == 0 { - maxWidth = 500 - } - maxHeight, _ := strconv.ParseInt(d["maxheight"], 10, 64) - if maxHeight == 0 { - maxHeight = 1000 - } - img, err := bot.makeImageOrVideoElem(d, false, isGroup) - if err != nil { - return nil, errors.New("send cardimage faild") - } - return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, isGroup) - case "video": - cache := d["cache"] - if cache == "" { - cache = "1" - } - file, err := bot.makeImageOrVideoElem(d, true, isGroup) - if err != nil { - return nil, err - } - v := file.(*LocalVideoElement) - if v.File == "" { - return v, nil - } - var data []byte - if cover, ok := d["cover"]; ok { - data, _ = global.FindFile(cover, cache, global.ImagePath) - } else { - _ = global.ExtractCover(v.File, v.File+".jpg") - data, _ = ioutil.ReadFile(v.File + ".jpg") - } - v.thumb = bytes.NewReader(data) - video, _ := os.Open(v.File) - defer video.Close() - _, err = video.Seek(4, io.SeekStart) - if err != nil { - return nil, err - } - var header = make([]byte, 4) - _, err = video.Read(header) - if err != nil { - return nil, err - } - if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp - _, _ = video.Seek(0, io.SeekStart) - hash, _ := utils.ComputeMd5AndLength(video) - cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".mp4") - if global.PathExists(cacheFile) && cache == "1" { - goto ok - } - err = global.EncodeMP4(v.File, cacheFile) - if err != nil { - return nil, err - } - ok: - v.File = cacheFile - } - return v, nil - default: - return nil, errors.New("unsupported cq code: " + t) - } - return nil, nil -} - -// XMLEscape 将字符串c转义为XML字符串 -func XMLEscape(c string) string { - buf := new(bytes.Buffer) - _ = xml2.EscapeText(buf, []byte(c)) - return buf.String() -} - -/*CQCodeEscapeText 将字符串raw中部分字符转义 - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeEscapeText(raw string) string { - ret := raw - ret = strings.ReplaceAll(ret, "&", "&") - ret = strings.ReplaceAll(ret, "[", "[") - ret = strings.ReplaceAll(ret, "]", "]") - return ret -} - -/*CQCodeEscapeValue 将字符串value中部分字符转义 - -, -> , - -*/ -func CQCodeEscapeValue(value string) string { - ret := CQCodeEscapeText(value) - ret = strings.ReplaceAll(ret, ",", ",") - return ret -} - -/*CQCodeUnescapeText 将字符串content中部分字符反转义 - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeUnescapeText(content string) string { - ret := content - ret = strings.ReplaceAll(ret, "[", "[") - ret = strings.ReplaceAll(ret, "]", "]") - ret = strings.ReplaceAll(ret, "&", "&") - return ret -} - -/*CQCodeUnescapeValue 将字符串content中部分字符反转义 - -, -> , - -*/ -func CQCodeUnescapeValue(content string) string { - ret := strings.ReplaceAll(content, ",", ",") - ret = CQCodeUnescapeText(ret) - return ret -} - -// makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用 -func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) (message.IMessageElement, error) { - f := d["file"] - if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { - cache := d["cache"] - c := d["c"] - if cache == "" { - cache = "1" - } - hash := md5.Sum([]byte(f)) - cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache") - var maxSize = func() int64 { - if video { - return maxVideoSize - } - return maxImageSize - }() - thread, _ := strconv.Atoi(c) - if global.PathExists(cacheFile) && cache == "1" { - goto hasCacheFile - } - if global.PathExists(cacheFile) { - _ = os.Remove(cacheFile) - } - if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil { - return nil, err - } - hasCacheFile: - if video { - return &LocalVideoElement{File: cacheFile}, nil - } - return &LocalImageElement{File: cacheFile}, nil - } - if strings.HasPrefix(f, "file") { - fu, err := url.Parse(f) - if err != nil { - return nil, err - } - if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { - fu.Path = fu.Path[1:] - } - info, err := os.Stat(fu.Path) - if err != nil { - if !os.IsExist(err) { - return nil, errors.New("file not found") - } - return nil, err - } - if video { - if info.Size() == 0 || info.Size() >= maxVideoSize { - return nil, errors.New("invalid video size") - } - return &LocalVideoElement{File: fu.Path}, nil - } - if info.Size() == 0 || info.Size() >= maxImageSize { - return nil, errors.New("invalid image size") - } - return &LocalImageElement{File: fu.Path}, nil - } - rawPath := path.Join(global.ImagePath, f) - if video { - rawPath = path.Join(global.VideoPath, f) - if !global.PathExists(rawPath) { - return nil, errors.New("invalid video") - } - if path.Ext(rawPath) == ".video" { - b, _ := ioutil.ReadFile(rawPath) - r := binary.NewReader(b) - return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效 - Md5: r.ReadBytes(16), - ThumbMd5: r.ReadBytes(16), - Size: r.ReadInt32(), - ThumbSize: r.ReadInt32(), - Name: r.ReadString(), - Uuid: r.ReadAvailable(), - }}, nil - } - return &LocalVideoElement{File: rawPath}, nil - } - if strings.HasPrefix(f, "base64") { - b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) - if err != nil { - return nil, err - } - return &LocalImageElement{Stream: bytes.NewReader(b)}, nil - } - if !global.PathExists(rawPath) && global.PathExists(path.Join(global.ImagePathOld, f)) { - rawPath = path.Join(global.ImagePathOld, f) - } - if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { - rawPath += ".cqimg" - } - if !global.PathExists(rawPath) && d["url"] != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group) - } - if global.PathExists(rawPath) { - file, err := os.Open(rawPath) - if err != nil { - return nil, err - } - if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { - return &LocalImageElement{Stream: file}, nil - } - b, err := ioutil.ReadAll(file) - if err != nil { - return nil, err - } - if len(b) < 20 { - return nil, errors.New("invalid local file") - } - var ( - size int32 - hash []byte - url string - ) - if path.Ext(rawPath) == ".cqimg" { - for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { - kv := strings.SplitN(line, "=", 2) - switch kv[0] { - case "md5": - hash, _ = hex.DecodeString(strings.ReplaceAll(kv[1], "\r", "")) - case "size": - t, _ := strconv.Atoi(strings.ReplaceAll(kv[1], "\r", "")) - size = int32(t) - } - } - } else { - r := binary.NewReader(b) - hash = r.ReadBytes(16) - size = r.ReadInt32() - r.ReadString() - url = r.ReadString() - } - if size == 0 { - if url != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) - } - return nil, errors.New("img size is 0") - } - if len(hash) != 16 { - return nil, errors.New("invalid hash") - } - var rsp message.IMessageElement - if group { - rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) - goto ok - } - rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) - ok: - if err != nil { - if url != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) - } - return nil, err - } - return rsp, nil - } - return nil, errors.New("invalid image") -} - -// makeShowPic 一种xml 方式发送的群消息图片 -func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) { - xml := "" - var suf message.IMessageElement - if i, ok := elem.(*LocalImageElement); ok { - if !group { - gm, err := bot.UploadLocalImageAsPrivate(1, i) - if err != nil { - log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) - return nil, err - } - suf = gm - xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - } else { - gm, err := bot.UploadLocalImageAsGroup(1, i) - if err != nil { - log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) - return nil, err - } - suf = gm - xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - } - } - - if i, ok := elem.(*message.GroupImageElement); ok { - xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - suf = i - } - if i, ok := elem.(*message.FriendImageElement); ok { - xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - suf = i - } - if xml != "" { - // log.Warn(xml) - ret := []message.IMessageElement{suf} - ret = append(ret, message.NewRichXml(xml, 5)) - return ret, nil - } - return nil, errors.New("生成xml图片消息失败") -} +package coolq + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + xml2 "encoding/xml" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "net/url" + "os" + "path" + "runtime" + "strconv" + "strings" + "time" + + "github.com/Mrs4s/MiraiGo/binary" + "github.com/Mrs4s/MiraiGo/message" + "github.com/Mrs4s/MiraiGo/utils" + "github.com/Mrs4s/go-cqhttp/global" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" +) + +/* +var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`) +var typeReg = regexp.MustCompile(`\[CQ:(\w+)`) +var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) +*/ + +// IgnoreInvalidCQCode 是否忽略无效CQ码 +var IgnoreInvalidCQCode = false + +// SplitURL 是否分割URL +var SplitURL = false + +const maxImageSize = 1024 * 1024 * 30 // 30MB +const maxVideoSize = 1024 * 1024 * 100 // 100MB +// PokeElement 拍一拍 +type PokeElement struct { + Target int64 +} + +// GiftElement 礼物 +type GiftElement struct { + Target int64 + GiftID message.GroupGift +} + +// LocalImageElement 本地图片 +type LocalImageElement struct { + message.ImageElement + Stream io.ReadSeeker + File string +} + +// LocalVoiceElement 本地语音 +type LocalVoiceElement struct { + message.VoiceElement + Stream io.ReadSeeker +} + +// LocalVideoElement 本地视频 +type LocalVideoElement struct { + message.ShortVideoElement + File string + thumb io.ReadSeeker +} + +// Type 获取元素类型ID +func (e *GiftElement) Type() message.ElementType { + // Make message.IMessageElement Happy + return message.At +} + +// GiftID 礼物ID数组 +var GiftID = [...]message.GroupGift{ + message.SweetWink, + message.HappyCola, + message.LuckyBracelet, + message.Cappuccino, + message.CatWatch, + message.FleeceGloves, + message.RainbowCandy, + message.Stronger, + message.LoveMicrophone, + message.HoldingYourHand, + message.CuteCat, + message.MysteryMask, + message.ImBusy, + message.LoveMask, +} + +// Type 获取元素类型ID +func (e *PokeElement) Type() message.ElementType { + // Make message.IMessageElement Happy + return message.At +} + +// ToArrayMessage 将消息元素数组转为MSG数组以用于消息上报 +func ToArrayMessage(e []message.IMessageElement, id int64, isRaw ...bool) (r []MSG) { + r = []MSG{} + ur := false + if len(isRaw) != 0 { + ur = isRaw[0] + } + m := &message.SendingMessage{Elements: e} + reply := m.FirstOrNil(func(e message.IMessageElement) bool { + _, ok := e.(*message.ReplyElement) + return ok + }) + if reply != nil { + r = append(r, MSG{ + "type": "reply", + "data": map[string]string{"id": fmt.Sprint(toGlobalID(id, reply.(*message.ReplyElement).ReplySeq))}, + }) + } + for _, elem := range e { + var m MSG + switch o := elem.(type) { + case *message.TextElement: + m = MSG{ + "type": "text", + "data": map[string]string{"text": o.Content}, + } + case *message.LightAppElement: + // m = MSG{ + // "type": "text", + // "data": map[string]string{"text": o.Content}, + // } + m = MSG{ + "type": "json", + "data": map[string]string{"data": o.Content}, + } + case *message.AtElement: + if o.Target == 0 { + m = MSG{ + "type": "at", + "data": map[string]string{"qq": "all"}, + } + } else { + m = MSG{ + "type": "at", + "data": map[string]string{"qq": fmt.Sprint(o.Target)}, + } + } + case *message.RedBagElement: + m = MSG{ + "type": "redbag", + "data": map[string]string{"title": o.Title}, + } + case *message.ForwardElement: + m = MSG{ + "type": "forward", + "data": map[string]string{"id": o.ResId}, + } + case *message.FaceElement: + m = MSG{ + "type": "face", + "data": map[string]string{"id": fmt.Sprint(o.Index)}, + } + case *message.VoiceElement: + if ur { + m = MSG{ + "type": "record", + "data": map[string]string{"file": o.Name}, + } + } else { + m = MSG{ + "type": "record", + "data": map[string]string{"file": o.Name, "url": o.Url}, + } + } + case *message.ShortVideoElement: + if ur { + m = MSG{ + "type": "video", + "data": map[string]string{"file": o.Name}, + } + } else { + m = MSG{ + "type": "video", + "data": map[string]string{"file": o.Name, "url": o.Url}, + } + } + case *message.ImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": o.Filename}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": o.Filename, "url": o.Url}, + } + } + case *message.GroupImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, + } + } + case *message.FriendImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, + } + } + case *message.GroupFlashImgElement: + return []MSG{{ + "type": "image", + "data": map[string]string{"file": o.Filename, "type": "flash"}, + }} + case *message.FriendFlashImgElement: + return []MSG{{ + "type": "image", + "data": map[string]string{"file": o.Filename, "type": "flash"}, + }} + case *message.ServiceElement: + if isOk := strings.Contains(o.Content, " 0 { + if _, ok := r[0].(*message.ReplyElement); ok { + log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") + return + } + } + mid, err := strconv.Atoi(params["id"]) + customText := params["text"] + if err == nil { + org := bot.GetMessage(int32(mid)) + if org != nil { + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: org["message-id"].(int32), + Sender: org["sender"].(message.Sender).Uin, + Time: org["time"].(int32), + Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), + }, + }, r...) + return + } + } else if customText != "" { + sender, err := strconv.ParseInt(params["qq"], 10, 64) + if err != nil { + log.Warnf("警告:自定义 Reply 元素中必须包含Uin") + return + } + msgTime, err := strconv.ParseInt(params["time"], 10, 64) + if err != nil { + msgTime = time.Now().Unix() + } + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: int32(0), + Sender: sender, + Time: int32(msgTime), + Elements: bot.ConvertStringMessage(customText, isGroup), + }, + }, r...) + return + } + } + if t == "forward" { // 单独处理转发 + if id, ok := params["id"]; ok { + r = []message.IMessageElement{bot.Client.DownloadForwardMessage(id)} + return + } + } + elem, err := bot.ToElement(t, params, isGroup) + if err != nil { + org := "[CQ:" + string(cqCode) + "]" + if !IgnoreInvalidCQCode { + log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) + r = append(r, message.NewText(org)) + } else { + log.Warnf("转换CQ码 %v 时出现错误: %v 将忽略.", org, err) + } + return + } + switch i := elem.(type) { + case message.IMessageElement: + r = append(r, i) + case []message.IMessageElement: + r = append(r, i...) + } + } + for hasNext() { + ch := next() + switch stat { + case 0: + if isCQCodeBegin(ch) { + saveTempText() + tempText = append(tempText, []rune("[CQ:")...) + move(3) + stat = 1 + } else { + tempText = append(tempText, ch) + } + case 1: + if isCQCodeBegin(ch) { + move(-1) + stat = 0 + } else if ch == ']' { + saveCQCode() + stat = 0 + } else { + cqCode = append(cqCode, ch) + tempText = append(tempText, ch) + } + } + } + saveTempText() + return +} + +// ConvertObjectMessage 将消息JSON对象转为消息元素数组 +func (bot *CQBot) ConvertObjectMessage(m gjson.Result, isGroup bool) (r []message.IMessageElement) { + convertElem := func(e gjson.Result) { + t := e.Get("type").Str + if t == "reply" && isGroup { + if len(r) > 0 { + if _, ok := r[0].(*message.ReplyElement); ok { + log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") + return + } + } + mid, err := strconv.Atoi(e.Get("data").Get("id").String()) + customText := e.Get("data").Get("text").String() + if err == nil { + org := bot.GetMessage(int32(mid)) + if org != nil { + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: org["message-id"].(int32), + Sender: org["sender"].(message.Sender).Uin, + Time: org["time"].(int32), + Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), + }, + }, r...) + return + } + } else if customText != "" { + sender, err := strconv.ParseInt(e.Get("data").Get("qq").String(), 10, 64) + if err != nil { + log.Warnf("警告:自定义 Reply 元素中必须包含Uin") + return + } + msgTime, err := strconv.ParseInt(e.Get("data").Get("time").String(), 10, 64) + if err != nil { + msgTime = time.Now().Unix() + } + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: int32(0), + Sender: sender, + Time: int32(msgTime), + Elements: bot.ConvertStringMessage(customText, isGroup), + }, + }, r...) + return + } + } + if t == "forward" { + r = []message.IMessageElement{bot.Client.DownloadForwardMessage(e.Get("data.id").String())} + return + } + d := make(map[string]string) + e.Get("data").ForEach(func(key, value gjson.Result) bool { + d[key.Str] = value.String() + return true + }) + elem, err := bot.ToElement(t, d, isGroup) + if err != nil { + log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) + return + } + switch i := elem.(type) { + case message.IMessageElement: + r = append(r, i) + case []message.IMessageElement: + r = append(r, i...) + } + } + if m.Type == gjson.String { + return bot.ConvertStringMessage(m.Str, isGroup) + } + if m.IsArray() { + for _, e := range m.Array() { + convertElem(e) + } + } + if m.IsObject() { + convertElem(m) + } + return +} + +// ToElement 将解码后的CQCode转换为Element. +// +// 返回 interface{} 存在三种类型 +// +// message.IMessageElement []message.IMessageElement nil +func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m interface{}, err error) { + switch t { + case "text": + if SplitURL { + var ret []message.IMessageElement + for _, text := range global.SplitURL(d["text"]) { + ret = append(ret, message.NewText(text)) + } + return ret, nil + } + return message.NewText(d["text"]), nil + case "image": + img, err := bot.makeImageOrVideoElem(d, false, isGroup) + if err != nil { + return nil, err + } + tp := d["type"] + if tp != "show" && tp != "flash" { + return img, nil + } + if i, ok := img.(*LocalImageElement); ok { // 秀图,闪照什么的就直接传了吧 + if isGroup { + img, err = bot.UploadLocalImageAsGroup(1, i) + } else { + img, err = bot.UploadLocalImageAsPrivate(1, i) + } + if err != nil { + return nil, err + } + } + switch tp { + case "flash": + if i, ok := img.(*message.GroupImageElement); ok { + return &message.GroupFlashPicElement{GroupImageElement: *i}, nil + } + if i, ok := img.(*message.FriendImageElement); ok { + return &message.FriendFlashPicElement{FriendImageElement: *i}, nil + } + case "show": + id, _ := strconv.ParseInt(d["id"], 10, 64) + if id < 40000 || id >= 40006 { + id = 40000 + } + if i, ok := img.(*message.GroupImageElement); ok { + return &message.GroupShowPicElement{GroupImageElement: *i, EffectId: int32(id)}, nil + } + return img, nil // 私聊还没做 + } + + case "poke": + t, _ := strconv.ParseInt(d["qq"], 10, 64) + return &PokeElement{Target: t}, nil + case "gift": + if !isGroup { + return nil, errors.New("private gift unsupported") // no free private gift + } + t, _ := strconv.ParseInt(d["qq"], 10, 64) + id, _ := strconv.Atoi(d["id"]) + if id < 0 || id >= 14 { + return nil, errors.New("invalid gift id") + } + return &GiftElement{Target: t, GiftID: GiftID[id]}, nil + case "tts": + defer func() { + if r := recover(); r != nil { + m = nil + err = errors.New("tts 转换失败") + } + }() + data, err := bot.Client.GetTts(d["text"]) + if err != nil { + return nil, err + } + return &message.VoiceElement{Data: data}, nil + case "record": + f := d["file"] + data, err := global.FindFile(f, d["cache"], global.VoicePath) + if err == global.ErrSyntax { + data, err = global.FindFile(f, d["cache"], global.VoicePathOld) + } + if err != nil { + return nil, err + } + if !global.IsAMRorSILK(data) { + data, err = global.EncoderSilk(data) + if err != nil { + return nil, err + } + } + return &message.VoiceElement{Data: data}, nil + case "face": + id, err := strconv.Atoi(d["id"]) + if err != nil { + return nil, err + } + return message.NewFace(int32(id)), nil + case "at": + qq := d["qq"] + if qq == "all" { + return message.AtAll(), nil + } + t, _ := strconv.ParseInt(qq, 10, 64) + return message.NewAt(t), nil + case "share": + return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil + case "music": + if d["type"] == "qq" { + info, err := global.QQMusicSongInfo(d["id"]) + if err != nil { + return nil, err + } + if !info.Get("track_info").Exists() { + return nil, errors.New("song not found") + } + aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10) + name := info.Get("track_info.name").Str + mid := info.Get("track_info.mid").Str + albumMid := info.Get("track_info.album.mid").Str + pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") + jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" + purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str + preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" + if len(aid) < 2 { + return nil, errors.New("song error") + } + content := info.Get("track_info.singer.0.name").Str + if d["content"] != "" { + content = d["content"] + } + return &message.MusicShareElement{ + MusicType: message.QQMusic, + Title: name, + Summary: content, + Url: jumpURL, + PictureUrl: preview, + MusicUrl: purl, + }, nil + } + if d["type"] == "163" { + info, err := global.NeteaseMusicSongInfo(d["id"]) + if err != nil { + return nil, err + } + if !info.Exists() { + return nil, errors.New("song not found") + } + name := info.Get("name").Str + jumpURL := "https://y.music.163.com/m/song/" + d["id"] + musicURL := "http://music.163.com/song/media/outer/url?id=" + d["id"] + picURL := info.Get("album.picUrl").Str + artistName := "" + if info.Get("artists.0").Exists() { + artistName = info.Get("artists.0.name").Str + } + return &message.MusicShareElement{ + MusicType: message.CloudMusic, + Title: name, + Summary: artistName, + Url: jumpURL, + PictureUrl: picURL, + MusicUrl: musicURL, + }, nil + } + if d["type"] == "custom" { + if d["subtype"] != "" { + var subtype = map[string]int{ + "qq": message.QQMusic, + "163": message.CloudMusic, + "migu": message.MiguMusic, + "kugou": message.KugouMusic, + "kuwo": message.KuwoMusic, + } + var musicType = 0 + if tp, ok := subtype[d["subtype"]]; ok { + musicType = tp + } + return &message.MusicShareElement{ + MusicType: musicType, + Title: d["title"], + Summary: d["content"], + Url: d["url"], + PictureUrl: d["image"], + MusicUrl: d["purl"], + }, nil + } + xml := fmt.Sprintf(``, + XMLEscape(d["title"]), d["url"], d["image"], d["audio"], XMLEscape(d["title"]), XMLEscape(d["content"])) + return &message.ServiceElement{ + Id: 60, + Content: xml, + SubType: "music", + }, nil + } + return nil, errors.New("unsupported music type: " + d["type"]) + case "xml": + resID := d["resid"] + template := CQCodeEscapeValue(d["data"]) + i, _ := strconv.ParseInt(resID, 10, 64) + msg := message.NewRichXml(template, i) + return msg, nil + case "json": + resID := d["resid"] + i, _ := strconv.ParseInt(resID, 10, 64) + if i == 0 { + // 默认情况下走小程序通道 + msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) + return msg, nil + } + // resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO + msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) + return msg, nil + case "cardimage": + source := d["source"] + icon := d["icon"] + minWidth, _ := strconv.ParseInt(d["minwidth"], 10, 64) + if minWidth == 0 { + minWidth = 200 + } + minHeight, _ := strconv.ParseInt(d["minheight"], 10, 64) + if minHeight == 0 { + minHeight = 200 + } + maxWidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64) + if maxWidth == 0 { + maxWidth = 500 + } + maxHeight, _ := strconv.ParseInt(d["maxheight"], 10, 64) + if maxHeight == 0 { + maxHeight = 1000 + } + img, err := bot.makeImageOrVideoElem(d, false, isGroup) + if err != nil { + return nil, errors.New("send cardimage faild") + } + return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, isGroup) + case "video": + cache := d["cache"] + if cache == "" { + cache = "1" + } + file, err := bot.makeImageOrVideoElem(d, true, isGroup) + if err != nil { + return nil, err + } + v := file.(*LocalVideoElement) + if v.File == "" { + return v, nil + } + var data []byte + if cover, ok := d["cover"]; ok { + data, _ = global.FindFile(cover, cache, global.ImagePath) + } else { + _ = global.ExtractCover(v.File, v.File+".jpg") + data, _ = ioutil.ReadFile(v.File + ".jpg") + } + v.thumb = bytes.NewReader(data) + video, _ := os.Open(v.File) + defer video.Close() + _, err = video.Seek(4, io.SeekStart) + if err != nil { + return nil, err + } + var header = make([]byte, 4) + _, err = video.Read(header) + if err != nil { + return nil, err + } + if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp + _, _ = video.Seek(0, io.SeekStart) + hash, _ := utils.ComputeMd5AndLength(video) + cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".mp4") + if global.PathExists(cacheFile) && cache == "1" { + goto ok + } + err = global.EncodeMP4(v.File, cacheFile) + if err != nil { + return nil, err + } + ok: + v.File = cacheFile + } + return v, nil + default: + return nil, errors.New("unsupported cq code: " + t) + } + return nil, nil +} + +// XMLEscape 将字符串c转义为XML字符串 +func XMLEscape(c string) string { + buf := new(bytes.Buffer) + _ = xml2.EscapeText(buf, []byte(c)) + return buf.String() +} + +/*CQCodeEscapeText 将字符串raw中部分字符转义 + +& -> & + +[ -> [ + +] -> ] + +*/ +func CQCodeEscapeText(raw string) string { + ret := raw + ret = strings.ReplaceAll(ret, "&", "&") + ret = strings.ReplaceAll(ret, "[", "[") + ret = strings.ReplaceAll(ret, "]", "]") + return ret +} + +/*CQCodeEscapeValue 将字符串value中部分字符转义 + +, -> , + +*/ +func CQCodeEscapeValue(value string) string { + ret := CQCodeEscapeText(value) + ret = strings.ReplaceAll(ret, ",", ",") + return ret +} + +/*CQCodeUnescapeText 将字符串content中部分字符反转义 + +& -> & + +[ -> [ + +] -> ] + +*/ +func CQCodeUnescapeText(content string) string { + ret := content + ret = strings.ReplaceAll(ret, "[", "[") + ret = strings.ReplaceAll(ret, "]", "]") + ret = strings.ReplaceAll(ret, "&", "&") + return ret +} + +/*CQCodeUnescapeValue 将字符串content中部分字符反转义 + +, -> , + +*/ +func CQCodeUnescapeValue(content string) string { + ret := strings.ReplaceAll(content, ",", ",") + ret = CQCodeUnescapeText(ret) + return ret +} + +// makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用 +func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) (message.IMessageElement, error) { + f := d["file"] + if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { + cache := d["cache"] + c := d["c"] + if cache == "" { + cache = "1" + } + hash := md5.Sum([]byte(f)) + cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache") + var maxSize = func() int64 { + if video { + return maxVideoSize + } + return maxImageSize + }() + thread, _ := strconv.Atoi(c) + if global.PathExists(cacheFile) && cache == "1" { + goto hasCacheFile + } + if global.PathExists(cacheFile) { + _ = os.Remove(cacheFile) + } + if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil { + return nil, err + } + hasCacheFile: + if video { + return &LocalVideoElement{File: cacheFile}, nil + } + return &LocalImageElement{File: cacheFile}, nil + } + if strings.HasPrefix(f, "file") { + fu, err := url.Parse(f) + if err != nil { + return nil, err + } + if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { + fu.Path = fu.Path[1:] + } + info, err := os.Stat(fu.Path) + if err != nil { + if !os.IsExist(err) { + return nil, errors.New("file not found") + } + return nil, err + } + if video { + if info.Size() == 0 || info.Size() >= maxVideoSize { + return nil, errors.New("invalid video size") + } + return &LocalVideoElement{File: fu.Path}, nil + } + if info.Size() == 0 || info.Size() >= maxImageSize { + return nil, errors.New("invalid image size") + } + return &LocalImageElement{File: fu.Path}, nil + } + rawPath := path.Join(global.ImagePath, f) + if video { + rawPath = path.Join(global.VideoPath, f) + if !global.PathExists(rawPath) { + return nil, errors.New("invalid video") + } + if path.Ext(rawPath) == ".video" { + b, _ := ioutil.ReadFile(rawPath) + r := binary.NewReader(b) + return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效 + Md5: r.ReadBytes(16), + ThumbMd5: r.ReadBytes(16), + Size: r.ReadInt32(), + ThumbSize: r.ReadInt32(), + Name: r.ReadString(), + Uuid: r.ReadAvailable(), + }}, nil + } + return &LocalVideoElement{File: rawPath}, nil + } + if strings.HasPrefix(f, "base64") { + b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) + if err != nil { + return nil, err + } + return &LocalImageElement{Stream: bytes.NewReader(b)}, nil + } + if !global.PathExists(rawPath) && global.PathExists(path.Join(global.ImagePathOld, f)) { + rawPath = path.Join(global.ImagePathOld, f) + } + if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { + rawPath += ".cqimg" + } + if !global.PathExists(rawPath) && d["url"] != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group) + } + if global.PathExists(rawPath) { + file, err := os.Open(rawPath) + if err != nil { + return nil, err + } + if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { + return &LocalImageElement{Stream: file}, nil + } + b, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + if len(b) < 20 { + return nil, errors.New("invalid local file") + } + var ( + size int32 + hash []byte + url string + ) + if path.Ext(rawPath) == ".cqimg" { + for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { + kv := strings.SplitN(line, "=", 2) + switch kv[0] { + case "md5": + hash, _ = hex.DecodeString(strings.ReplaceAll(kv[1], "\r", "")) + case "size": + t, _ := strconv.Atoi(strings.ReplaceAll(kv[1], "\r", "")) + size = int32(t) + } + } + } else { + r := binary.NewReader(b) + hash = r.ReadBytes(16) + size = r.ReadInt32() + r.ReadString() + url = r.ReadString() + } + if size == 0 { + if url != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) + } + return nil, errors.New("img size is 0") + } + if len(hash) != 16 { + return nil, errors.New("invalid hash") + } + var rsp message.IMessageElement + if group { + rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) + goto ok + } + rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) + ok: + if err != nil { + if url != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) + } + return nil, err + } + return rsp, nil + } + return nil, errors.New("invalid image") +} + +// makeShowPic 一种xml 方式发送的群消息图片 +func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) { + xml := "" + var suf message.IMessageElement + if i, ok := elem.(*LocalImageElement); ok { + if !group { + gm, err := bot.UploadLocalImageAsPrivate(1, i) + if err != nil { + log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) + return nil, err + } + suf = gm + xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + } else { + gm, err := bot.UploadLocalImageAsGroup(1, i) + if err != nil { + log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) + return nil, err + } + suf = gm + xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + } + } + + if i, ok := elem.(*message.GroupImageElement); ok { + xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + suf = i + } + if i, ok := elem.(*message.FriendImageElement); ok { + xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + suf = i + } + if xml != "" { + // log.Warn(xml) + ret := []message.IMessageElement{suf} + ret = append(ret, message.NewRichXml(xml, 5)) + return ret, nil + } + return nil, errors.New("生成xml图片消息失败") +} diff --git a/go.mod b/go.mod index 21fd61c..45acf79 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/syndtr/goleveldb v1.0.0 github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 github.com/tidwall/gjson v1.6.8 - github.com/tidwall/sjson v1.1.5 github.com/wdvxdr1123/go-silk v0.0.0-20210207032612-169bbdf8861d github.com/yinghau76/go-ascii-art v0.0.0-20190517192627-e7f465a30189 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 diff --git a/go.sum b/go.sum index a63ffde..cea2100 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,6 @@ github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE= -github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= From 3bfdc4d8a29227474e0fa817aeab2be9291720b1 Mon Sep 17 00:00:00 2001 From: nnnewb Date: Sat, 13 Feb 2021 18:24:20 +0800 Subject: [PATCH 11/17] =?UTF-8?q?fix:=20=E8=A7=A3=E5=86=B3=E5=8E=9F?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=20isAnonymous=20=E5=88=A4=E6=96=AD=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coolq/api.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coolq/api.go b/coolq/api.go index da5f20e..5e94212 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -805,7 +805,7 @@ func (bot *CQBot) CQGetStrangerInfo(userID int64) MSG { func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { postType := context.Get("post_type").Str anonymous := context.Get("anonymous") - isAnonymous := anonymous.Type == gjson.Null + isAnonymous := anonymous.Type != gjson.Null switch postType { case "message": @@ -830,8 +830,9 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { }, }) - err := json.UnmarshalFromString(reply.Raw, replySegments) + err := json.UnmarshalFromString(reply.Raw, &replySegments) if err != nil { + log.WithError(err).Warnf("处理 at_sender 过程中发生错误") return Failed(-1, "处理 at_sender 过程中发生错误", err.Error()) } @@ -839,6 +840,7 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { modified, err := json.MarshalToString(segments) if err != nil { + log.WithError(err).Warnf("处理 at_sender 过程中发生错误") return Failed(-1, "处理 at_sender 过程中发生错误", err.Error()) } From 19556cbab3dfb84614618cd27672117d6fa9f237 Mon Sep 17 00:00:00 2001 From: nnnewb Date: Sat, 13 Feb 2021 18:37:22 +0800 Subject: [PATCH 12/17] =?UTF-8?q?refactor:=20=E7=95=A5=E5=BE=AE=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=B8=8B=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coolq/api.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/coolq/api.go b/coolq/api.go index 5e94212..607e71a 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -804,22 +804,23 @@ func (bot *CQBot) CQGetStrangerInfo(userID int64) MSG { // https://git.io/Jtz15 func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { postType := context.Get("post_type").Str - anonymous := context.Get("anonymous") - isAnonymous := anonymous.Type != gjson.Null switch postType { case "message": + anonymous := context.Get("anonymous") + isAnonymous := anonymous.Type != gjson.Null msgType := context.Get("message_type").Str reply := operation.Get("reply") + if reply.Exists() { autoEscape := global.EnsureBool(operation.Get("auto_escape"), false) at := !isAnonymous // 除匿名消息场合外默认 true if operation.Get("at_sender").Exists() { - at = operation.Get("at_sender").Bool() + at = operation.Get("at_sender").Bool() && !isAnonymous } - if !isAnonymous && at && reply.IsArray() { + if at && reply.IsArray() { // 在 reply 数组头部插入CQ码 replySegments := make([]MSG, 0) segments := make([]MSG, 0) @@ -845,7 +846,7 @@ func (bot *CQBot) CQHandleQuickOperation(context, operation gjson.Result) MSG { } reply = gjson.Parse(modified) - } else if !isAnonymous && at && reply.Type == gjson.String { + } else if at && reply.Type == gjson.String { reply = gjson.Parse(fmt.Sprintf( "\"[CQ:at,qq=%d]%s\"", context.Get("sender.user_id").Int(), From ce30d63907ed16533fb9af8e58ae2286d58a65bd Mon Sep 17 00:00:00 2001 From: nnnewb Date: Sat, 13 Feb 2021 18:50:50 +0800 Subject: [PATCH 13/17] fix: fix line ending --- coolq/cqcode.go | 2262 +++++++++++++++++++++++------------------------ 1 file changed, 1131 insertions(+), 1131 deletions(-) diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 77c16ee..6268617 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -1,1131 +1,1131 @@ -package coolq - -import ( - "bytes" - "crypto/md5" - "encoding/base64" - "encoding/hex" - xml2 "encoding/xml" - "errors" - "fmt" - "io" - "io/ioutil" - "math" - "math/rand" - "net/url" - "os" - "path" - "runtime" - "strconv" - "strings" - "time" - - "github.com/Mrs4s/MiraiGo/binary" - "github.com/Mrs4s/MiraiGo/message" - "github.com/Mrs4s/MiraiGo/utils" - "github.com/Mrs4s/go-cqhttp/global" - log "github.com/sirupsen/logrus" - "github.com/tidwall/gjson" -) - -/* -var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`) -var typeReg = regexp.MustCompile(`\[CQ:(\w+)`) -var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) -*/ - -// IgnoreInvalidCQCode 是否忽略无效CQ码 -var IgnoreInvalidCQCode = false - -// SplitURL 是否分割URL -var SplitURL = false - -const maxImageSize = 1024 * 1024 * 30 // 30MB -const maxVideoSize = 1024 * 1024 * 100 // 100MB -// PokeElement 拍一拍 -type PokeElement struct { - Target int64 -} - -// GiftElement 礼物 -type GiftElement struct { - Target int64 - GiftID message.GroupGift -} - -// LocalImageElement 本地图片 -type LocalImageElement struct { - message.ImageElement - Stream io.ReadSeeker - File string -} - -// LocalVoiceElement 本地语音 -type LocalVoiceElement struct { - message.VoiceElement - Stream io.ReadSeeker -} - -// LocalVideoElement 本地视频 -type LocalVideoElement struct { - message.ShortVideoElement - File string - thumb io.ReadSeeker -} - -// Type 获取元素类型ID -func (e *GiftElement) Type() message.ElementType { - // Make message.IMessageElement Happy - return message.At -} - -// GiftID 礼物ID数组 -var GiftID = [...]message.GroupGift{ - message.SweetWink, - message.HappyCola, - message.LuckyBracelet, - message.Cappuccino, - message.CatWatch, - message.FleeceGloves, - message.RainbowCandy, - message.Stronger, - message.LoveMicrophone, - message.HoldingYourHand, - message.CuteCat, - message.MysteryMask, - message.ImBusy, - message.LoveMask, -} - -// Type 获取元素类型ID -func (e *PokeElement) Type() message.ElementType { - // Make message.IMessageElement Happy - return message.At -} - -// ToArrayMessage 将消息元素数组转为MSG数组以用于消息上报 -func ToArrayMessage(e []message.IMessageElement, id int64, isRaw ...bool) (r []MSG) { - r = []MSG{} - ur := false - if len(isRaw) != 0 { - ur = isRaw[0] - } - m := &message.SendingMessage{Elements: e} - reply := m.FirstOrNil(func(e message.IMessageElement) bool { - _, ok := e.(*message.ReplyElement) - return ok - }) - if reply != nil { - r = append(r, MSG{ - "type": "reply", - "data": map[string]string{"id": fmt.Sprint(toGlobalID(id, reply.(*message.ReplyElement).ReplySeq))}, - }) - } - for _, elem := range e { - var m MSG - switch o := elem.(type) { - case *message.TextElement: - m = MSG{ - "type": "text", - "data": map[string]string{"text": o.Content}, - } - case *message.LightAppElement: - // m = MSG{ - // "type": "text", - // "data": map[string]string{"text": o.Content}, - // } - m = MSG{ - "type": "json", - "data": map[string]string{"data": o.Content}, - } - case *message.AtElement: - if o.Target == 0 { - m = MSG{ - "type": "at", - "data": map[string]string{"qq": "all"}, - } - } else { - m = MSG{ - "type": "at", - "data": map[string]string{"qq": fmt.Sprint(o.Target)}, - } - } - case *message.RedBagElement: - m = MSG{ - "type": "redbag", - "data": map[string]string{"title": o.Title}, - } - case *message.ForwardElement: - m = MSG{ - "type": "forward", - "data": map[string]string{"id": o.ResId}, - } - case *message.FaceElement: - m = MSG{ - "type": "face", - "data": map[string]string{"id": fmt.Sprint(o.Index)}, - } - case *message.VoiceElement: - if ur { - m = MSG{ - "type": "record", - "data": map[string]string{"file": o.Name}, - } - } else { - m = MSG{ - "type": "record", - "data": map[string]string{"file": o.Name, "url": o.Url}, - } - } - case *message.ShortVideoElement: - if ur { - m = MSG{ - "type": "video", - "data": map[string]string{"file": o.Name}, - } - } else { - m = MSG{ - "type": "video", - "data": map[string]string{"file": o.Name, "url": o.Url}, - } - } - case *message.ImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": o.Filename}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": o.Filename, "url": o.Url}, - } - } - case *message.GroupImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, - } - } - case *message.FriendImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, - } - } - case *message.GroupFlashImgElement: - return []MSG{{ - "type": "image", - "data": map[string]string{"file": o.Filename, "type": "flash"}, - }} - case *message.FriendFlashImgElement: - return []MSG{{ - "type": "image", - "data": map[string]string{"file": o.Filename, "type": "flash"}, - }} - case *message.ServiceElement: - if isOk := strings.Contains(o.Content, " 0 { - if _, ok := r[0].(*message.ReplyElement); ok { - log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") - return - } - } - mid, err := strconv.Atoi(params["id"]) - customText := params["text"] - if err == nil { - org := bot.GetMessage(int32(mid)) - if org != nil { - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: org["message-id"].(int32), - Sender: org["sender"].(message.Sender).Uin, - Time: org["time"].(int32), - Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), - }, - }, r...) - return - } - } else if customText != "" { - sender, err := strconv.ParseInt(params["qq"], 10, 64) - if err != nil { - log.Warnf("警告:自定义 Reply 元素中必须包含Uin") - return - } - msgTime, err := strconv.ParseInt(params["time"], 10, 64) - if err != nil { - msgTime = time.Now().Unix() - } - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: int32(0), - Sender: sender, - Time: int32(msgTime), - Elements: bot.ConvertStringMessage(customText, isGroup), - }, - }, r...) - return - } - } - if t == "forward" { // 单独处理转发 - if id, ok := params["id"]; ok { - r = []message.IMessageElement{bot.Client.DownloadForwardMessage(id)} - return - } - } - elem, err := bot.ToElement(t, params, isGroup) - if err != nil { - org := "[CQ:" + string(cqCode) + "]" - if !IgnoreInvalidCQCode { - log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) - r = append(r, message.NewText(org)) - } else { - log.Warnf("转换CQ码 %v 时出现错误: %v 将忽略.", org, err) - } - return - } - switch i := elem.(type) { - case message.IMessageElement: - r = append(r, i) - case []message.IMessageElement: - r = append(r, i...) - } - } - for hasNext() { - ch := next() - switch stat { - case 0: - if isCQCodeBegin(ch) { - saveTempText() - tempText = append(tempText, []rune("[CQ:")...) - move(3) - stat = 1 - } else { - tempText = append(tempText, ch) - } - case 1: - if isCQCodeBegin(ch) { - move(-1) - stat = 0 - } else if ch == ']' { - saveCQCode() - stat = 0 - } else { - cqCode = append(cqCode, ch) - tempText = append(tempText, ch) - } - } - } - saveTempText() - return -} - -// ConvertObjectMessage 将消息JSON对象转为消息元素数组 -func (bot *CQBot) ConvertObjectMessage(m gjson.Result, isGroup bool) (r []message.IMessageElement) { - convertElem := func(e gjson.Result) { - t := e.Get("type").Str - if t == "reply" && isGroup { - if len(r) > 0 { - if _, ok := r[0].(*message.ReplyElement); ok { - log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") - return - } - } - mid, err := strconv.Atoi(e.Get("data").Get("id").String()) - customText := e.Get("data").Get("text").String() - if err == nil { - org := bot.GetMessage(int32(mid)) - if org != nil { - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: org["message-id"].(int32), - Sender: org["sender"].(message.Sender).Uin, - Time: org["time"].(int32), - Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), - }, - }, r...) - return - } - } else if customText != "" { - sender, err := strconv.ParseInt(e.Get("data").Get("qq").String(), 10, 64) - if err != nil { - log.Warnf("警告:自定义 Reply 元素中必须包含Uin") - return - } - msgTime, err := strconv.ParseInt(e.Get("data").Get("time").String(), 10, 64) - if err != nil { - msgTime = time.Now().Unix() - } - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: int32(0), - Sender: sender, - Time: int32(msgTime), - Elements: bot.ConvertStringMessage(customText, isGroup), - }, - }, r...) - return - } - } - if t == "forward" { - r = []message.IMessageElement{bot.Client.DownloadForwardMessage(e.Get("data.id").String())} - return - } - d := make(map[string]string) - e.Get("data").ForEach(func(key, value gjson.Result) bool { - d[key.Str] = value.String() - return true - }) - elem, err := bot.ToElement(t, d, isGroup) - if err != nil { - log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) - return - } - switch i := elem.(type) { - case message.IMessageElement: - r = append(r, i) - case []message.IMessageElement: - r = append(r, i...) - } - } - if m.Type == gjson.String { - return bot.ConvertStringMessage(m.Str, isGroup) - } - if m.IsArray() { - for _, e := range m.Array() { - convertElem(e) - } - } - if m.IsObject() { - convertElem(m) - } - return -} - -// ToElement 将解码后的CQCode转换为Element. -// -// 返回 interface{} 存在三种类型 -// -// message.IMessageElement []message.IMessageElement nil -func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m interface{}, err error) { - switch t { - case "text": - if SplitURL { - var ret []message.IMessageElement - for _, text := range global.SplitURL(d["text"]) { - ret = append(ret, message.NewText(text)) - } - return ret, nil - } - return message.NewText(d["text"]), nil - case "image": - img, err := bot.makeImageOrVideoElem(d, false, isGroup) - if err != nil { - return nil, err - } - tp := d["type"] - if tp != "show" && tp != "flash" { - return img, nil - } - if i, ok := img.(*LocalImageElement); ok { // 秀图,闪照什么的就直接传了吧 - if isGroup { - img, err = bot.UploadLocalImageAsGroup(1, i) - } else { - img, err = bot.UploadLocalImageAsPrivate(1, i) - } - if err != nil { - return nil, err - } - } - switch tp { - case "flash": - if i, ok := img.(*message.GroupImageElement); ok { - return &message.GroupFlashPicElement{GroupImageElement: *i}, nil - } - if i, ok := img.(*message.FriendImageElement); ok { - return &message.FriendFlashPicElement{FriendImageElement: *i}, nil - } - case "show": - id, _ := strconv.ParseInt(d["id"], 10, 64) - if id < 40000 || id >= 40006 { - id = 40000 - } - if i, ok := img.(*message.GroupImageElement); ok { - return &message.GroupShowPicElement{GroupImageElement: *i, EffectId: int32(id)}, nil - } - return img, nil // 私聊还没做 - } - - case "poke": - t, _ := strconv.ParseInt(d["qq"], 10, 64) - return &PokeElement{Target: t}, nil - case "gift": - if !isGroup { - return nil, errors.New("private gift unsupported") // no free private gift - } - t, _ := strconv.ParseInt(d["qq"], 10, 64) - id, _ := strconv.Atoi(d["id"]) - if id < 0 || id >= 14 { - return nil, errors.New("invalid gift id") - } - return &GiftElement{Target: t, GiftID: GiftID[id]}, nil - case "tts": - defer func() { - if r := recover(); r != nil { - m = nil - err = errors.New("tts 转换失败") - } - }() - data, err := bot.Client.GetTts(d["text"]) - if err != nil { - return nil, err - } - return &message.VoiceElement{Data: data}, nil - case "record": - f := d["file"] - data, err := global.FindFile(f, d["cache"], global.VoicePath) - if err == global.ErrSyntax { - data, err = global.FindFile(f, d["cache"], global.VoicePathOld) - } - if err != nil { - return nil, err - } - if !global.IsAMRorSILK(data) { - data, err = global.EncoderSilk(data) - if err != nil { - return nil, err - } - } - return &message.VoiceElement{Data: data}, nil - case "face": - id, err := strconv.Atoi(d["id"]) - if err != nil { - return nil, err - } - return message.NewFace(int32(id)), nil - case "at": - qq := d["qq"] - if qq == "all" { - return message.AtAll(), nil - } - t, _ := strconv.ParseInt(qq, 10, 64) - return message.NewAt(t), nil - case "share": - return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil - case "music": - if d["type"] == "qq" { - info, err := global.QQMusicSongInfo(d["id"]) - if err != nil { - return nil, err - } - if !info.Get("track_info").Exists() { - return nil, errors.New("song not found") - } - aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10) - name := info.Get("track_info.name").Str - mid := info.Get("track_info.mid").Str - albumMid := info.Get("track_info.album.mid").Str - pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") - jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" - purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str - preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" - if len(aid) < 2 { - return nil, errors.New("song error") - } - content := info.Get("track_info.singer.0.name").Str - if d["content"] != "" { - content = d["content"] - } - return &message.MusicShareElement{ - MusicType: message.QQMusic, - Title: name, - Summary: content, - Url: jumpURL, - PictureUrl: preview, - MusicUrl: purl, - }, nil - } - if d["type"] == "163" { - info, err := global.NeteaseMusicSongInfo(d["id"]) - if err != nil { - return nil, err - } - if !info.Exists() { - return nil, errors.New("song not found") - } - name := info.Get("name").Str - jumpURL := "https://y.music.163.com/m/song/" + d["id"] - musicURL := "http://music.163.com/song/media/outer/url?id=" + d["id"] - picURL := info.Get("album.picUrl").Str - artistName := "" - if info.Get("artists.0").Exists() { - artistName = info.Get("artists.0.name").Str - } - return &message.MusicShareElement{ - MusicType: message.CloudMusic, - Title: name, - Summary: artistName, - Url: jumpURL, - PictureUrl: picURL, - MusicUrl: musicURL, - }, nil - } - if d["type"] == "custom" { - if d["subtype"] != "" { - var subtype = map[string]int{ - "qq": message.QQMusic, - "163": message.CloudMusic, - "migu": message.MiguMusic, - "kugou": message.KugouMusic, - "kuwo": message.KuwoMusic, - } - var musicType = 0 - if tp, ok := subtype[d["subtype"]]; ok { - musicType = tp - } - return &message.MusicShareElement{ - MusicType: musicType, - Title: d["title"], - Summary: d["content"], - Url: d["url"], - PictureUrl: d["image"], - MusicUrl: d["purl"], - }, nil - } - xml := fmt.Sprintf(``, - XMLEscape(d["title"]), d["url"], d["image"], d["audio"], XMLEscape(d["title"]), XMLEscape(d["content"])) - return &message.ServiceElement{ - Id: 60, - Content: xml, - SubType: "music", - }, nil - } - return nil, errors.New("unsupported music type: " + d["type"]) - case "xml": - resID := d["resid"] - template := CQCodeEscapeValue(d["data"]) - i, _ := strconv.ParseInt(resID, 10, 64) - msg := message.NewRichXml(template, i) - return msg, nil - case "json": - resID := d["resid"] - i, _ := strconv.ParseInt(resID, 10, 64) - if i == 0 { - // 默认情况下走小程序通道 - msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) - return msg, nil - } - // resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO - msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) - return msg, nil - case "cardimage": - source := d["source"] - icon := d["icon"] - minWidth, _ := strconv.ParseInt(d["minwidth"], 10, 64) - if minWidth == 0 { - minWidth = 200 - } - minHeight, _ := strconv.ParseInt(d["minheight"], 10, 64) - if minHeight == 0 { - minHeight = 200 - } - maxWidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64) - if maxWidth == 0 { - maxWidth = 500 - } - maxHeight, _ := strconv.ParseInt(d["maxheight"], 10, 64) - if maxHeight == 0 { - maxHeight = 1000 - } - img, err := bot.makeImageOrVideoElem(d, false, isGroup) - if err != nil { - return nil, errors.New("send cardimage faild") - } - return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, isGroup) - case "video": - cache := d["cache"] - if cache == "" { - cache = "1" - } - file, err := bot.makeImageOrVideoElem(d, true, isGroup) - if err != nil { - return nil, err - } - v := file.(*LocalVideoElement) - if v.File == "" { - return v, nil - } - var data []byte - if cover, ok := d["cover"]; ok { - data, _ = global.FindFile(cover, cache, global.ImagePath) - } else { - _ = global.ExtractCover(v.File, v.File+".jpg") - data, _ = ioutil.ReadFile(v.File + ".jpg") - } - v.thumb = bytes.NewReader(data) - video, _ := os.Open(v.File) - defer video.Close() - _, err = video.Seek(4, io.SeekStart) - if err != nil { - return nil, err - } - var header = make([]byte, 4) - _, err = video.Read(header) - if err != nil { - return nil, err - } - if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp - _, _ = video.Seek(0, io.SeekStart) - hash, _ := utils.ComputeMd5AndLength(video) - cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".mp4") - if global.PathExists(cacheFile) && cache == "1" { - goto ok - } - err = global.EncodeMP4(v.File, cacheFile) - if err != nil { - return nil, err - } - ok: - v.File = cacheFile - } - return v, nil - default: - return nil, errors.New("unsupported cq code: " + t) - } - return nil, nil -} - -// XMLEscape 将字符串c转义为XML字符串 -func XMLEscape(c string) string { - buf := new(bytes.Buffer) - _ = xml2.EscapeText(buf, []byte(c)) - return buf.String() -} - -/*CQCodeEscapeText 将字符串raw中部分字符转义 - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeEscapeText(raw string) string { - ret := raw - ret = strings.ReplaceAll(ret, "&", "&") - ret = strings.ReplaceAll(ret, "[", "[") - ret = strings.ReplaceAll(ret, "]", "]") - return ret -} - -/*CQCodeEscapeValue 将字符串value中部分字符转义 - -, -> , - -*/ -func CQCodeEscapeValue(value string) string { - ret := CQCodeEscapeText(value) - ret = strings.ReplaceAll(ret, ",", ",") - return ret -} - -/*CQCodeUnescapeText 将字符串content中部分字符反转义 - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeUnescapeText(content string) string { - ret := content - ret = strings.ReplaceAll(ret, "[", "[") - ret = strings.ReplaceAll(ret, "]", "]") - ret = strings.ReplaceAll(ret, "&", "&") - return ret -} - -/*CQCodeUnescapeValue 将字符串content中部分字符反转义 - -, -> , - -*/ -func CQCodeUnescapeValue(content string) string { - ret := strings.ReplaceAll(content, ",", ",") - ret = CQCodeUnescapeText(ret) - return ret -} - -// makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用 -func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) (message.IMessageElement, error) { - f := d["file"] - if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { - cache := d["cache"] - c := d["c"] - if cache == "" { - cache = "1" - } - hash := md5.Sum([]byte(f)) - cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache") - var maxSize = func() int64 { - if video { - return maxVideoSize - } - return maxImageSize - }() - thread, _ := strconv.Atoi(c) - if global.PathExists(cacheFile) && cache == "1" { - goto hasCacheFile - } - if global.PathExists(cacheFile) { - _ = os.Remove(cacheFile) - } - if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil { - return nil, err - } - hasCacheFile: - if video { - return &LocalVideoElement{File: cacheFile}, nil - } - return &LocalImageElement{File: cacheFile}, nil - } - if strings.HasPrefix(f, "file") { - fu, err := url.Parse(f) - if err != nil { - return nil, err - } - if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { - fu.Path = fu.Path[1:] - } - info, err := os.Stat(fu.Path) - if err != nil { - if !os.IsExist(err) { - return nil, errors.New("file not found") - } - return nil, err - } - if video { - if info.Size() == 0 || info.Size() >= maxVideoSize { - return nil, errors.New("invalid video size") - } - return &LocalVideoElement{File: fu.Path}, nil - } - if info.Size() == 0 || info.Size() >= maxImageSize { - return nil, errors.New("invalid image size") - } - return &LocalImageElement{File: fu.Path}, nil - } - rawPath := path.Join(global.ImagePath, f) - if video { - rawPath = path.Join(global.VideoPath, f) - if !global.PathExists(rawPath) { - return nil, errors.New("invalid video") - } - if path.Ext(rawPath) == ".video" { - b, _ := ioutil.ReadFile(rawPath) - r := binary.NewReader(b) - return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效 - Md5: r.ReadBytes(16), - ThumbMd5: r.ReadBytes(16), - Size: r.ReadInt32(), - ThumbSize: r.ReadInt32(), - Name: r.ReadString(), - Uuid: r.ReadAvailable(), - }}, nil - } - return &LocalVideoElement{File: rawPath}, nil - } - if strings.HasPrefix(f, "base64") { - b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) - if err != nil { - return nil, err - } - return &LocalImageElement{Stream: bytes.NewReader(b)}, nil - } - if !global.PathExists(rawPath) && global.PathExists(path.Join(global.ImagePathOld, f)) { - rawPath = path.Join(global.ImagePathOld, f) - } - if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { - rawPath += ".cqimg" - } - if !global.PathExists(rawPath) && d["url"] != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group) - } - if global.PathExists(rawPath) { - file, err := os.Open(rawPath) - if err != nil { - return nil, err - } - if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { - return &LocalImageElement{Stream: file}, nil - } - b, err := ioutil.ReadAll(file) - if err != nil { - return nil, err - } - if len(b) < 20 { - return nil, errors.New("invalid local file") - } - var ( - size int32 - hash []byte - url string - ) - if path.Ext(rawPath) == ".cqimg" { - for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { - kv := strings.SplitN(line, "=", 2) - switch kv[0] { - case "md5": - hash, _ = hex.DecodeString(strings.ReplaceAll(kv[1], "\r", "")) - case "size": - t, _ := strconv.Atoi(strings.ReplaceAll(kv[1], "\r", "")) - size = int32(t) - } - } - } else { - r := binary.NewReader(b) - hash = r.ReadBytes(16) - size = r.ReadInt32() - r.ReadString() - url = r.ReadString() - } - if size == 0 { - if url != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) - } - return nil, errors.New("img size is 0") - } - if len(hash) != 16 { - return nil, errors.New("invalid hash") - } - var rsp message.IMessageElement - if group { - rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) - goto ok - } - rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) - ok: - if err != nil { - if url != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) - } - return nil, err - } - return rsp, nil - } - return nil, errors.New("invalid image") -} - -// makeShowPic 一种xml 方式发送的群消息图片 -func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) { - xml := "" - var suf message.IMessageElement - if i, ok := elem.(*LocalImageElement); ok { - if !group { - gm, err := bot.UploadLocalImageAsPrivate(1, i) - if err != nil { - log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) - return nil, err - } - suf = gm - xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - } else { - gm, err := bot.UploadLocalImageAsGroup(1, i) - if err != nil { - log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) - return nil, err - } - suf = gm - xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - } - } - - if i, ok := elem.(*message.GroupImageElement); ok { - xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - suf = i - } - if i, ok := elem.(*message.FriendImageElement); ok { - xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - suf = i - } - if xml != "" { - // log.Warn(xml) - ret := []message.IMessageElement{suf} - ret = append(ret, message.NewRichXml(xml, 5)) - return ret, nil - } - return nil, errors.New("生成xml图片消息失败") -} +package coolq + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + xml2 "encoding/xml" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "net/url" + "os" + "path" + "runtime" + "strconv" + "strings" + "time" + + "github.com/Mrs4s/MiraiGo/binary" + "github.com/Mrs4s/MiraiGo/message" + "github.com/Mrs4s/MiraiGo/utils" + "github.com/Mrs4s/go-cqhttp/global" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" +) + +/* +var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`) +var typeReg = regexp.MustCompile(`\[CQ:(\w+)`) +var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) +*/ + +// IgnoreInvalidCQCode 是否忽略无效CQ码 +var IgnoreInvalidCQCode = false + +// SplitURL 是否分割URL +var SplitURL = false + +const maxImageSize = 1024 * 1024 * 30 // 30MB +const maxVideoSize = 1024 * 1024 * 100 // 100MB +// PokeElement 拍一拍 +type PokeElement struct { + Target int64 +} + +// GiftElement 礼物 +type GiftElement struct { + Target int64 + GiftID message.GroupGift +} + +// LocalImageElement 本地图片 +type LocalImageElement struct { + message.ImageElement + Stream io.ReadSeeker + File string +} + +// LocalVoiceElement 本地语音 +type LocalVoiceElement struct { + message.VoiceElement + Stream io.ReadSeeker +} + +// LocalVideoElement 本地视频 +type LocalVideoElement struct { + message.ShortVideoElement + File string + thumb io.ReadSeeker +} + +// Type 获取元素类型ID +func (e *GiftElement) Type() message.ElementType { + // Make message.IMessageElement Happy + return message.At +} + +// GiftID 礼物ID数组 +var GiftID = [...]message.GroupGift{ + message.SweetWink, + message.HappyCola, + message.LuckyBracelet, + message.Cappuccino, + message.CatWatch, + message.FleeceGloves, + message.RainbowCandy, + message.Stronger, + message.LoveMicrophone, + message.HoldingYourHand, + message.CuteCat, + message.MysteryMask, + message.ImBusy, + message.LoveMask, +} + +// Type 获取元素类型ID +func (e *PokeElement) Type() message.ElementType { + // Make message.IMessageElement Happy + return message.At +} + +// ToArrayMessage 将消息元素数组转为MSG数组以用于消息上报 +func ToArrayMessage(e []message.IMessageElement, id int64, isRaw ...bool) (r []MSG) { + r = []MSG{} + ur := false + if len(isRaw) != 0 { + ur = isRaw[0] + } + m := &message.SendingMessage{Elements: e} + reply := m.FirstOrNil(func(e message.IMessageElement) bool { + _, ok := e.(*message.ReplyElement) + return ok + }) + if reply != nil { + r = append(r, MSG{ + "type": "reply", + "data": map[string]string{"id": fmt.Sprint(toGlobalID(id, reply.(*message.ReplyElement).ReplySeq))}, + }) + } + for _, elem := range e { + var m MSG + switch o := elem.(type) { + case *message.TextElement: + m = MSG{ + "type": "text", + "data": map[string]string{"text": o.Content}, + } + case *message.LightAppElement: + // m = MSG{ + // "type": "text", + // "data": map[string]string{"text": o.Content}, + // } + m = MSG{ + "type": "json", + "data": map[string]string{"data": o.Content}, + } + case *message.AtElement: + if o.Target == 0 { + m = MSG{ + "type": "at", + "data": map[string]string{"qq": "all"}, + } + } else { + m = MSG{ + "type": "at", + "data": map[string]string{"qq": fmt.Sprint(o.Target)}, + } + } + case *message.RedBagElement: + m = MSG{ + "type": "redbag", + "data": map[string]string{"title": o.Title}, + } + case *message.ForwardElement: + m = MSG{ + "type": "forward", + "data": map[string]string{"id": o.ResId}, + } + case *message.FaceElement: + m = MSG{ + "type": "face", + "data": map[string]string{"id": fmt.Sprint(o.Index)}, + } + case *message.VoiceElement: + if ur { + m = MSG{ + "type": "record", + "data": map[string]string{"file": o.Name}, + } + } else { + m = MSG{ + "type": "record", + "data": map[string]string{"file": o.Name, "url": o.Url}, + } + } + case *message.ShortVideoElement: + if ur { + m = MSG{ + "type": "video", + "data": map[string]string{"file": o.Name}, + } + } else { + m = MSG{ + "type": "video", + "data": map[string]string{"file": o.Name, "url": o.Url}, + } + } + case *message.ImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": o.Filename}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": o.Filename, "url": o.Url}, + } + } + case *message.GroupImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, + } + } + case *message.FriendImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, + } + } + case *message.GroupFlashImgElement: + return []MSG{{ + "type": "image", + "data": map[string]string{"file": o.Filename, "type": "flash"}, + }} + case *message.FriendFlashImgElement: + return []MSG{{ + "type": "image", + "data": map[string]string{"file": o.Filename, "type": "flash"}, + }} + case *message.ServiceElement: + if isOk := strings.Contains(o.Content, " 0 { + if _, ok := r[0].(*message.ReplyElement); ok { + log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") + return + } + } + mid, err := strconv.Atoi(params["id"]) + customText := params["text"] + if err == nil { + org := bot.GetMessage(int32(mid)) + if org != nil { + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: org["message-id"].(int32), + Sender: org["sender"].(message.Sender).Uin, + Time: org["time"].(int32), + Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), + }, + }, r...) + return + } + } else if customText != "" { + sender, err := strconv.ParseInt(params["qq"], 10, 64) + if err != nil { + log.Warnf("警告:自定义 Reply 元素中必须包含Uin") + return + } + msgTime, err := strconv.ParseInt(params["time"], 10, 64) + if err != nil { + msgTime = time.Now().Unix() + } + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: int32(0), + Sender: sender, + Time: int32(msgTime), + Elements: bot.ConvertStringMessage(customText, isGroup), + }, + }, r...) + return + } + } + if t == "forward" { // 单独处理转发 + if id, ok := params["id"]; ok { + r = []message.IMessageElement{bot.Client.DownloadForwardMessage(id)} + return + } + } + elem, err := bot.ToElement(t, params, isGroup) + if err != nil { + org := "[CQ:" + string(cqCode) + "]" + if !IgnoreInvalidCQCode { + log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) + r = append(r, message.NewText(org)) + } else { + log.Warnf("转换CQ码 %v 时出现错误: %v 将忽略.", org, err) + } + return + } + switch i := elem.(type) { + case message.IMessageElement: + r = append(r, i) + case []message.IMessageElement: + r = append(r, i...) + } + } + for hasNext() { + ch := next() + switch stat { + case 0: + if isCQCodeBegin(ch) { + saveTempText() + tempText = append(tempText, []rune("[CQ:")...) + move(3) + stat = 1 + } else { + tempText = append(tempText, ch) + } + case 1: + if isCQCodeBegin(ch) { + move(-1) + stat = 0 + } else if ch == ']' { + saveCQCode() + stat = 0 + } else { + cqCode = append(cqCode, ch) + tempText = append(tempText, ch) + } + } + } + saveTempText() + return +} + +// ConvertObjectMessage 将消息JSON对象转为消息元素数组 +func (bot *CQBot) ConvertObjectMessage(m gjson.Result, isGroup bool) (r []message.IMessageElement) { + convertElem := func(e gjson.Result) { + t := e.Get("type").Str + if t == "reply" && isGroup { + if len(r) > 0 { + if _, ok := r[0].(*message.ReplyElement); ok { + log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") + return + } + } + mid, err := strconv.Atoi(e.Get("data").Get("id").String()) + customText := e.Get("data").Get("text").String() + if err == nil { + org := bot.GetMessage(int32(mid)) + if org != nil { + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: org["message-id"].(int32), + Sender: org["sender"].(message.Sender).Uin, + Time: org["time"].(int32), + Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), + }, + }, r...) + return + } + } else if customText != "" { + sender, err := strconv.ParseInt(e.Get("data").Get("qq").String(), 10, 64) + if err != nil { + log.Warnf("警告:自定义 Reply 元素中必须包含Uin") + return + } + msgTime, err := strconv.ParseInt(e.Get("data").Get("time").String(), 10, 64) + if err != nil { + msgTime = time.Now().Unix() + } + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: int32(0), + Sender: sender, + Time: int32(msgTime), + Elements: bot.ConvertStringMessage(customText, isGroup), + }, + }, r...) + return + } + } + if t == "forward" { + r = []message.IMessageElement{bot.Client.DownloadForwardMessage(e.Get("data.id").String())} + return + } + d := make(map[string]string) + e.Get("data").ForEach(func(key, value gjson.Result) bool { + d[key.Str] = value.String() + return true + }) + elem, err := bot.ToElement(t, d, isGroup) + if err != nil { + log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) + return + } + switch i := elem.(type) { + case message.IMessageElement: + r = append(r, i) + case []message.IMessageElement: + r = append(r, i...) + } + } + if m.Type == gjson.String { + return bot.ConvertStringMessage(m.Str, isGroup) + } + if m.IsArray() { + for _, e := range m.Array() { + convertElem(e) + } + } + if m.IsObject() { + convertElem(m) + } + return +} + +// ToElement 将解码后的CQCode转换为Element. +// +// 返回 interface{} 存在三种类型 +// +// message.IMessageElement []message.IMessageElement nil +func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m interface{}, err error) { + switch t { + case "text": + if SplitURL { + var ret []message.IMessageElement + for _, text := range global.SplitURL(d["text"]) { + ret = append(ret, message.NewText(text)) + } + return ret, nil + } + return message.NewText(d["text"]), nil + case "image": + img, err := bot.makeImageOrVideoElem(d, false, isGroup) + if err != nil { + return nil, err + } + tp := d["type"] + if tp != "show" && tp != "flash" { + return img, nil + } + if i, ok := img.(*LocalImageElement); ok { // 秀图,闪照什么的就直接传了吧 + if isGroup { + img, err = bot.UploadLocalImageAsGroup(1, i) + } else { + img, err = bot.UploadLocalImageAsPrivate(1, i) + } + if err != nil { + return nil, err + } + } + switch tp { + case "flash": + if i, ok := img.(*message.GroupImageElement); ok { + return &message.GroupFlashPicElement{GroupImageElement: *i}, nil + } + if i, ok := img.(*message.FriendImageElement); ok { + return &message.FriendFlashPicElement{FriendImageElement: *i}, nil + } + case "show": + id, _ := strconv.ParseInt(d["id"], 10, 64) + if id < 40000 || id >= 40006 { + id = 40000 + } + if i, ok := img.(*message.GroupImageElement); ok { + return &message.GroupShowPicElement{GroupImageElement: *i, EffectId: int32(id)}, nil + } + return img, nil // 私聊还没做 + } + + case "poke": + t, _ := strconv.ParseInt(d["qq"], 10, 64) + return &PokeElement{Target: t}, nil + case "gift": + if !isGroup { + return nil, errors.New("private gift unsupported") // no free private gift + } + t, _ := strconv.ParseInt(d["qq"], 10, 64) + id, _ := strconv.Atoi(d["id"]) + if id < 0 || id >= 14 { + return nil, errors.New("invalid gift id") + } + return &GiftElement{Target: t, GiftID: GiftID[id]}, nil + case "tts": + defer func() { + if r := recover(); r != nil { + m = nil + err = errors.New("tts 转换失败") + } + }() + data, err := bot.Client.GetTts(d["text"]) + if err != nil { + return nil, err + } + return &message.VoiceElement{Data: data}, nil + case "record": + f := d["file"] + data, err := global.FindFile(f, d["cache"], global.VoicePath) + if err == global.ErrSyntax { + data, err = global.FindFile(f, d["cache"], global.VoicePathOld) + } + if err != nil { + return nil, err + } + if !global.IsAMRorSILK(data) { + data, err = global.EncoderSilk(data) + if err != nil { + return nil, err + } + } + return &message.VoiceElement{Data: data}, nil + case "face": + id, err := strconv.Atoi(d["id"]) + if err != nil { + return nil, err + } + return message.NewFace(int32(id)), nil + case "at": + qq := d["qq"] + if qq == "all" { + return message.AtAll(), nil + } + t, _ := strconv.ParseInt(qq, 10, 64) + return message.NewAt(t), nil + case "share": + return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil + case "music": + if d["type"] == "qq" { + info, err := global.QQMusicSongInfo(d["id"]) + if err != nil { + return nil, err + } + if !info.Get("track_info").Exists() { + return nil, errors.New("song not found") + } + aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10) + name := info.Get("track_info.name").Str + mid := info.Get("track_info.mid").Str + albumMid := info.Get("track_info.album.mid").Str + pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") + jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" + purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str + preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" + if len(aid) < 2 { + return nil, errors.New("song error") + } + content := info.Get("track_info.singer.0.name").Str + if d["content"] != "" { + content = d["content"] + } + return &message.MusicShareElement{ + MusicType: message.QQMusic, + Title: name, + Summary: content, + Url: jumpURL, + PictureUrl: preview, + MusicUrl: purl, + }, nil + } + if d["type"] == "163" { + info, err := global.NeteaseMusicSongInfo(d["id"]) + if err != nil { + return nil, err + } + if !info.Exists() { + return nil, errors.New("song not found") + } + name := info.Get("name").Str + jumpURL := "https://y.music.163.com/m/song/" + d["id"] + musicURL := "http://music.163.com/song/media/outer/url?id=" + d["id"] + picURL := info.Get("album.picUrl").Str + artistName := "" + if info.Get("artists.0").Exists() { + artistName = info.Get("artists.0.name").Str + } + return &message.MusicShareElement{ + MusicType: message.CloudMusic, + Title: name, + Summary: artistName, + Url: jumpURL, + PictureUrl: picURL, + MusicUrl: musicURL, + }, nil + } + if d["type"] == "custom" { + if d["subtype"] != "" { + var subtype = map[string]int{ + "qq": message.QQMusic, + "163": message.CloudMusic, + "migu": message.MiguMusic, + "kugou": message.KugouMusic, + "kuwo": message.KuwoMusic, + } + var musicType = 0 + if tp, ok := subtype[d["subtype"]]; ok { + musicType = tp + } + return &message.MusicShareElement{ + MusicType: musicType, + Title: d["title"], + Summary: d["content"], + Url: d["url"], + PictureUrl: d["image"], + MusicUrl: d["purl"], + }, nil + } + xml := fmt.Sprintf(``, + XMLEscape(d["title"]), d["url"], d["image"], d["audio"], XMLEscape(d["title"]), XMLEscape(d["content"])) + return &message.ServiceElement{ + Id: 60, + Content: xml, + SubType: "music", + }, nil + } + return nil, errors.New("unsupported music type: " + d["type"]) + case "xml": + resID := d["resid"] + template := CQCodeEscapeValue(d["data"]) + i, _ := strconv.ParseInt(resID, 10, 64) + msg := message.NewRichXml(template, i) + return msg, nil + case "json": + resID := d["resid"] + i, _ := strconv.ParseInt(resID, 10, 64) + if i == 0 { + // 默认情况下走小程序通道 + msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) + return msg, nil + } + // resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO + msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) + return msg, nil + case "cardimage": + source := d["source"] + icon := d["icon"] + minWidth, _ := strconv.ParseInt(d["minwidth"], 10, 64) + if minWidth == 0 { + minWidth = 200 + } + minHeight, _ := strconv.ParseInt(d["minheight"], 10, 64) + if minHeight == 0 { + minHeight = 200 + } + maxWidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64) + if maxWidth == 0 { + maxWidth = 500 + } + maxHeight, _ := strconv.ParseInt(d["maxheight"], 10, 64) + if maxHeight == 0 { + maxHeight = 1000 + } + img, err := bot.makeImageOrVideoElem(d, false, isGroup) + if err != nil { + return nil, errors.New("send cardimage faild") + } + return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, isGroup) + case "video": + cache := d["cache"] + if cache == "" { + cache = "1" + } + file, err := bot.makeImageOrVideoElem(d, true, isGroup) + if err != nil { + return nil, err + } + v := file.(*LocalVideoElement) + if v.File == "" { + return v, nil + } + var data []byte + if cover, ok := d["cover"]; ok { + data, _ = global.FindFile(cover, cache, global.ImagePath) + } else { + _ = global.ExtractCover(v.File, v.File+".jpg") + data, _ = ioutil.ReadFile(v.File + ".jpg") + } + v.thumb = bytes.NewReader(data) + video, _ := os.Open(v.File) + defer video.Close() + _, err = video.Seek(4, io.SeekStart) + if err != nil { + return nil, err + } + var header = make([]byte, 4) + _, err = video.Read(header) + if err != nil { + return nil, err + } + if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp + _, _ = video.Seek(0, io.SeekStart) + hash, _ := utils.ComputeMd5AndLength(video) + cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".mp4") + if global.PathExists(cacheFile) && cache == "1" { + goto ok + } + err = global.EncodeMP4(v.File, cacheFile) + if err != nil { + return nil, err + } + ok: + v.File = cacheFile + } + return v, nil + default: + return nil, errors.New("unsupported cq code: " + t) + } + return nil, nil +} + +// XMLEscape 将字符串c转义为XML字符串 +func XMLEscape(c string) string { + buf := new(bytes.Buffer) + _ = xml2.EscapeText(buf, []byte(c)) + return buf.String() +} + +/*CQCodeEscapeText 将字符串raw中部分字符转义 + +& -> & + +[ -> [ + +] -> ] + +*/ +func CQCodeEscapeText(raw string) string { + ret := raw + ret = strings.ReplaceAll(ret, "&", "&") + ret = strings.ReplaceAll(ret, "[", "[") + ret = strings.ReplaceAll(ret, "]", "]") + return ret +} + +/*CQCodeEscapeValue 将字符串value中部分字符转义 + +, -> , + +*/ +func CQCodeEscapeValue(value string) string { + ret := CQCodeEscapeText(value) + ret = strings.ReplaceAll(ret, ",", ",") + return ret +} + +/*CQCodeUnescapeText 将字符串content中部分字符反转义 + +& -> & + +[ -> [ + +] -> ] + +*/ +func CQCodeUnescapeText(content string) string { + ret := content + ret = strings.ReplaceAll(ret, "[", "[") + ret = strings.ReplaceAll(ret, "]", "]") + ret = strings.ReplaceAll(ret, "&", "&") + return ret +} + +/*CQCodeUnescapeValue 将字符串content中部分字符反转义 + +, -> , + +*/ +func CQCodeUnescapeValue(content string) string { + ret := strings.ReplaceAll(content, ",", ",") + ret = CQCodeUnescapeText(ret) + return ret +} + +// makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用 +func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) (message.IMessageElement, error) { + f := d["file"] + if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { + cache := d["cache"] + c := d["c"] + if cache == "" { + cache = "1" + } + hash := md5.Sum([]byte(f)) + cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache") + var maxSize = func() int64 { + if video { + return maxVideoSize + } + return maxImageSize + }() + thread, _ := strconv.Atoi(c) + if global.PathExists(cacheFile) && cache == "1" { + goto hasCacheFile + } + if global.PathExists(cacheFile) { + _ = os.Remove(cacheFile) + } + if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil { + return nil, err + } + hasCacheFile: + if video { + return &LocalVideoElement{File: cacheFile}, nil + } + return &LocalImageElement{File: cacheFile}, nil + } + if strings.HasPrefix(f, "file") { + fu, err := url.Parse(f) + if err != nil { + return nil, err + } + if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { + fu.Path = fu.Path[1:] + } + info, err := os.Stat(fu.Path) + if err != nil { + if !os.IsExist(err) { + return nil, errors.New("file not found") + } + return nil, err + } + if video { + if info.Size() == 0 || info.Size() >= maxVideoSize { + return nil, errors.New("invalid video size") + } + return &LocalVideoElement{File: fu.Path}, nil + } + if info.Size() == 0 || info.Size() >= maxImageSize { + return nil, errors.New("invalid image size") + } + return &LocalImageElement{File: fu.Path}, nil + } + rawPath := path.Join(global.ImagePath, f) + if video { + rawPath = path.Join(global.VideoPath, f) + if !global.PathExists(rawPath) { + return nil, errors.New("invalid video") + } + if path.Ext(rawPath) == ".video" { + b, _ := ioutil.ReadFile(rawPath) + r := binary.NewReader(b) + return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效 + Md5: r.ReadBytes(16), + ThumbMd5: r.ReadBytes(16), + Size: r.ReadInt32(), + ThumbSize: r.ReadInt32(), + Name: r.ReadString(), + Uuid: r.ReadAvailable(), + }}, nil + } + return &LocalVideoElement{File: rawPath}, nil + } + if strings.HasPrefix(f, "base64") { + b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) + if err != nil { + return nil, err + } + return &LocalImageElement{Stream: bytes.NewReader(b)}, nil + } + if !global.PathExists(rawPath) && global.PathExists(path.Join(global.ImagePathOld, f)) { + rawPath = path.Join(global.ImagePathOld, f) + } + if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { + rawPath += ".cqimg" + } + if !global.PathExists(rawPath) && d["url"] != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group) + } + if global.PathExists(rawPath) { + file, err := os.Open(rawPath) + if err != nil { + return nil, err + } + if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { + return &LocalImageElement{Stream: file}, nil + } + b, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + if len(b) < 20 { + return nil, errors.New("invalid local file") + } + var ( + size int32 + hash []byte + url string + ) + if path.Ext(rawPath) == ".cqimg" { + for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { + kv := strings.SplitN(line, "=", 2) + switch kv[0] { + case "md5": + hash, _ = hex.DecodeString(strings.ReplaceAll(kv[1], "\r", "")) + case "size": + t, _ := strconv.Atoi(strings.ReplaceAll(kv[1], "\r", "")) + size = int32(t) + } + } + } else { + r := binary.NewReader(b) + hash = r.ReadBytes(16) + size = r.ReadInt32() + r.ReadString() + url = r.ReadString() + } + if size == 0 { + if url != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) + } + return nil, errors.New("img size is 0") + } + if len(hash) != 16 { + return nil, errors.New("invalid hash") + } + var rsp message.IMessageElement + if group { + rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) + goto ok + } + rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) + ok: + if err != nil { + if url != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) + } + return nil, err + } + return rsp, nil + } + return nil, errors.New("invalid image") +} + +// makeShowPic 一种xml 方式发送的群消息图片 +func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) { + xml := "" + var suf message.IMessageElement + if i, ok := elem.(*LocalImageElement); ok { + if !group { + gm, err := bot.UploadLocalImageAsPrivate(1, i) + if err != nil { + log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) + return nil, err + } + suf = gm + xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + } else { + gm, err := bot.UploadLocalImageAsGroup(1, i) + if err != nil { + log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) + return nil, err + } + suf = gm + xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + } + } + + if i, ok := elem.(*message.GroupImageElement); ok { + xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + suf = i + } + if i, ok := elem.(*message.FriendImageElement); ok { + xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + suf = i + } + if xml != "" { + // log.Warn(xml) + ret := []message.IMessageElement{suf} + ret = append(ret, message.NewRichXml(xml, 5)) + return ret, nil + } + return nil, errors.New("生成xml图片消息失败") +} From ee0da0a14f8082d4835e53a646381c5305f8d17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E6=A9=98=20=E9=9B=AB=E9=9C=9E?= Date: Mon, 15 Feb 2021 15:12:16 +0800 Subject: [PATCH 14/17] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E2=80=9C=E6=88=91?= =?UTF-8?q?=E5=B7=B2=E9=98=85=E8=AF=BB=E6=8F=90=E7=A4=BA=E2=80=9D=E5=A4=8D?= =?UTF-8?q?=E9=80=89=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/Mrs4s/go-cqhttp/pull/632#discussion_r575967478 --- .github/ISSUE_TEMPLATE/bug--.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug--.md b/.github/ISSUE_TEMPLATE/bug--.md index 423a32f..183e229 100644 --- a/.github/ISSUE_TEMPLATE/bug--.md +++ b/.github/ISSUE_TEMPLATE/bug--.md @@ -16,6 +16,8 @@ assignees: '' 注: 如果您不知道如何有效、精准地表述,我们建议您先阅读《提问的智慧》 -> https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md --> +- [ ] 我已经阅读"提问前需知 [图+文]": https://github.com/Mrs4s/go-cqhttp/issues/633 + **环境信息** 请根据实际使用环境修改以下信息 go-cqhttp版本: From 569a718dc666e3377abb2250097a5eefe284c7d3 Mon Sep 17 00:00:00 2001 From: Akiba <36563862+Akegarasu@users.noreply.github.com> Date: Tue, 16 Feb 2021 18:20:18 +0800 Subject: [PATCH 15/17] fix #638 --- coolq/bot.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coolq/bot.go b/coolq/bot.go index bad338f..a853a24 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -409,6 +409,9 @@ func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) MSG { gm["sub_type"] = "anonymous" } else { mem := bot.Client.FindGroup(m.GroupCode).FindMember(m.Sender.Uin) + if mem == nil{ + return Failed(100,"MEMBER_NOT_FOUND","群员不存在") + } ms := gm["sender"].(MSG) ms["role"] = func() string { switch mem.Permission { From 1ff9de190d494647c35c696c8231df1f07699bb7 Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Wed, 17 Feb 2021 16:04:28 +0800 Subject: [PATCH 16/17] fix cqcode --- coolq/bot.go | 7 ++----- coolq/cqcode.go | 22 +++++++++++++++++----- global/filter.go | 4 ++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/coolq/bot.go b/coolq/bot.go index 6930e40..da884ab 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -13,17 +13,14 @@ import ( "sync" "time" - "github.com/Mrs4s/MiraiGo/utils" - - "github.com/syndtr/goleveldb/leveldb" - "github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/client" "github.com/Mrs4s/MiraiGo/message" + "github.com/Mrs4s/MiraiGo/utils" "github.com/Mrs4s/go-cqhttp/global" - jsoniter "github.com/json-iterator/go" log "github.com/sirupsen/logrus" + "github.com/syndtr/goleveldb/leveldb" ) var json = jsoniter.ConfigCompatibleWithStandardLibrary diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 2a02a51..da69761 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -361,11 +361,11 @@ func (bot *CQBot) ConvertStringMessage(msg string, isGroup bool) (r []message.IM saveTempText := func() { if len(tempText) != 0 { if SplitURL { - for _, t := range global.SplitURL(CQCodeUnescapeValue(string(tempText))) { + for _, t := range global.SplitURL(CQCodeUnescapeText(string(tempText))) { r = append(r, message.NewText(t)) } } else { - r = append(r, message.NewText(CQCodeUnescapeValue(string(tempText)))) + r = append(r, message.NewText(CQCodeUnescapeText(string(tempText)))) } } tempText = []rune{} @@ -783,11 +783,11 @@ func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m inte i, _ := strconv.ParseInt(resID, 10, 64) if i == 0 { // 默认情况下走小程序通道 - msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) + msg := message.NewLightApp(d["data"]) return msg, nil } // resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO - msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) + msg := message.NewRichJson(d["data"]) return msg, nil case "cardimage": source := d["source"] @@ -894,6 +894,12 @@ func CQCodeEscapeText(raw string) string { , -> , +& -> & + +[ -> [ + +] -> ] + */ func CQCodeEscapeValue(value string) string { ret := CQCodeEscapeText(value) @@ -920,7 +926,13 @@ func CQCodeUnescapeText(content string) string { /*CQCodeUnescapeValue 将字符串content中部分字符反转义 -, -> , +, -> , + +& -> & + +[ -> [ + +] -> ] */ func CQCodeUnescapeValue(content string) string { diff --git a/global/filter.go b/global/filter.go index 80e641a..cdbea5d 100644 --- a/global/filter.go +++ b/global/filter.go @@ -109,9 +109,9 @@ func andOperatorConstruct(argument gjson.Result) *AndOperator { } // Eval 对payload执行And过滤 -func (andOperator *AndOperator) Eval(payload MSG) bool { +func (op *AndOperator) Eval(payload MSG) bool { res := true - for _, operand := range andOperator.operands { + for _, operand := range op.operands { if len(operand.key) == 0 { // is an operator From a1fb629798705951c54b7b330b35796bf733963c Mon Sep 17 00:00:00 2001 From: Akiba <36563862+Akegarasu@users.noreply.github.com> Date: Wed, 17 Feb 2021 17:34:57 +0800 Subject: [PATCH 17/17] refresh group mem --- coolq/bot.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/coolq/bot.go b/coolq/bot.go index a853a24..94ef00c 100644 --- a/coolq/bot.go +++ b/coolq/bot.go @@ -408,9 +408,20 @@ func (bot *CQBot) formatGroupMessage(m *message.GroupMessage) MSG { gm["sender"].(MSG)["nickname"] = "匿名消息" gm["sub_type"] = "anonymous" } else { - mem := bot.Client.FindGroup(m.GroupCode).FindMember(m.Sender.Uin) + group := bot.Client.FindGroup(m.GroupCode) + mem := group.FindMember(m.Sender.Uin) if mem == nil{ - return Failed(100,"MEMBER_NOT_FOUND","群员不存在") + log.Warnf("获取 %v 成员信息失败,尝试刷新成员列表", m.Sender.Uin) + t, err := bot.Client.GetGroupMembers(group) + if err != nil { + log.Warnf("刷新群 %v 成员列表失败: %v", group.Uin, err) + return Failed(100, "GET_MEMBERS_API_ERROR", err.Error()) + } + group.Members = t + mem = group.FindMember(m.Sender.Uin) + if mem != nil{ + return Failed(100,"MEMBER_NOT_FOUND","群员不存在") + } } ms := gm["sender"].(MSG) ms["role"] = func() string {