1
0
mirror of https://github.com/Mrs4s/go-cqhttp.git synced 2025-05-04 19:17:37 +08:00

coolq: unified string/array message conversion

change to 2 step:
(1): parse to []internal/msg.Element, use msg.ParseObject/msg.ParseString
(2): transform to []IMessageElement, can share functions
This commit is contained in:
wdvxdr 2023-02-16 14:49:14 +08:00
parent 43dd9aa76d
commit debc1ed1ae
6 changed files with 335 additions and 482 deletions

View File

@ -1847,7 +1847,11 @@ func (bot *CQBot) CQCanSendRecord() global.MSG {
// @route(ocr_image,".ocr_image") // @route(ocr_image,".ocr_image")
// @rename(image_id->image) // @rename(image_id->image)
func (bot *CQBot) CQOcrImage(imageID string) global.MSG { func (bot *CQBot) CQOcrImage(imageID string) global.MSG {
img, err := bot.makeImageOrVideoElem(map[string]string{"file": imageID}, false, message.SourceGroup) // TODO: fix this
var elem msg.Element
elem.Type = "image"
elem.Data = []msg.Pair{{K: "file", V: imageID}}
img, err := bot.makeImageOrVideoElem(elem, false, message.SourceGroup)
if err != nil { if err != nil {
log.Warnf("load image error: %v", err) log.Warnf("load image error: %v", err)
return Failed(100, "LOAD_FILE_ERROR", err.Error()) return Failed(100, "LOAD_FILE_ERROR", err.Error())

View File

@ -11,7 +11,6 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"reflect"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -382,431 +381,163 @@ func ToMessageContent(e []message.IMessageElement) (r []global.MSG) {
// ConvertStringMessage 将消息字符串转为消息元素数组 // ConvertStringMessage 将消息字符串转为消息元素数组
func (bot *CQBot) ConvertStringMessage(raw string, sourceType message.SourceType) (r []message.IMessageElement) { func (bot *CQBot) ConvertStringMessage(raw string, sourceType message.SourceType) (r []message.IMessageElement) {
var t, key string elems := msg.ParseString(raw)
d := map[string]string{} return bot.ConvertElements(elems, sourceType)
saveCQCode := func() {
if t == "reply" { // reply 特殊处理
if len(r) > 0 {
if _, ok := r[0].(*message.ReplyElement); ok {
log.Warnf("警告: 一条信息只能包含一个 Reply 元素.")
return
}
}
mid, err := strconv.Atoi(d["id"])
customText := d["text"]
switch {
case customText != "":
var elem *message.ReplyElement
var org db.StoredMessage
sender, senderErr := strconv.ParseInt(d["qq"], 10, 64)
if senderErr != nil && err != nil {
log.Warnf("警告: 自定义 Reply 元素中必须包含 Uin 或 id")
break
}
msgTime, timeErr := strconv.ParseInt(d["time"], 10, 64)
if timeErr != nil {
msgTime = time.Now().Unix()
}
messageSeq, seqErr := strconv.ParseInt(d["seq"], 10, 64)
if err == nil {
org, _ = db.GetMessageByGlobalID(int32(mid))
}
if org != nil {
elem = &message.ReplyElement{
ReplySeq: org.GetAttribute().MessageSeq,
Sender: org.GetAttribute().SenderUin,
Time: int32(org.GetAttribute().Timestamp),
Elements: bot.ConvertStringMessage(customText, sourceType),
}
if senderErr != nil {
elem.Sender = sender
}
if timeErr != nil {
elem.Time = int32(msgTime)
}
if seqErr != nil {
elem.ReplySeq = int32(messageSeq)
}
} else {
elem = &message.ReplyElement{
ReplySeq: int32(messageSeq),
Sender: sender,
Time: int32(msgTime),
Elements: bot.ConvertStringMessage(customText, sourceType),
}
}
r = append([]message.IMessageElement{elem}, r...)
case err == nil:
org, err := db.GetMessageByGlobalID(int32(mid))
if err == nil {
r = append([]message.IMessageElement{
&message.ReplyElement{
ReplySeq: org.GetAttribute().MessageSeq,
Sender: org.GetAttribute().SenderUin,
Time: int32(org.GetAttribute().Timestamp),
Elements: bot.ConvertContentMessage(org.GetContent(), sourceType),
},
}, r...)
}
default:
log.Warnf("警告: Reply 元素中必须包含 text 或 id")
}
return
}
if t == "forward" { // 单独处理转发
if id, ok := d["id"]; ok {
if fwdMsg := bot.Client.DownloadForwardMessage(id); fwdMsg == nil {
log.Warnf("警告: Forward 信息不存在或已过期")
} else {
r = []message.IMessageElement{fwdMsg}
}
} else {
log.Warnf("警告: Forward 元素中必须包含 id")
}
return
}
elem, err := bot.ToElement(t, d, sourceType)
if err != nil {
org := "[CQ:" + t
for k, v := range d {
org += "," + k + "=" + v
}
org += "]"
if !base.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 raw != "" {
i := 0
for i < len(raw) && !(raw[i] == '[' && i+4 < len(raw) && raw[i:i+4] == "[CQ:") {
i++
}
if i > 0 {
if base.SplitURL {
for _, txt := range param.SplitURL(msg.UnescapeText(raw[:i])) {
r = append(r, message.NewText(txt))
}
} else {
r = append(r, message.NewText(msg.UnescapeText(raw[:i])))
}
}
if i+4 > len(raw) {
return
}
raw = raw[i+4:] // skip "[CQ:"
i = 0
for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
i++
}
if i+1 > len(raw) {
return
}
t = raw[:i]
for k := range d { // clear the map, reuse it
delete(d, k)
}
raw = raw[i:]
i = 0
for {
if raw[0] == ']' {
saveCQCode()
raw = raw[1:]
break
}
raw = raw[1:]
for i < len(raw) && raw[i] != '=' {
i++
}
if i+1 > len(raw) {
return
}
key = raw[:i]
raw = raw[i+1:] // skip "="
i = 0
for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
i++
}
if i+1 > len(raw) {
return
}
d[key] = msg.UnescapeValue(raw[:i])
raw = raw[i:]
i = 0
}
}
return
} }
// ConvertObjectMessage 将消息JSON对象转为消息元素数组 // ConvertObjectMessage 将消息JSON对象转为消息元素数组
func (bot *CQBot) ConvertObjectMessage(m gjson.Result, sourceType message.SourceType) (r []message.IMessageElement) { func (bot *CQBot) ConvertObjectMessage(m gjson.Result, sourceType message.SourceType) (r []message.IMessageElement) {
d := make(map[string]string) if m.Type == gjson.String {
convertElem := func(e gjson.Result) { return bot.ConvertStringMessage(m.Str, sourceType)
t := e.Get("type").Str }
if t == "reply" && sourceType&(message.SourceGroup|message.SourcePrivate) != 0 { elems := msg.ParseObject(m)
if len(r) > 0 { return bot.ConvertElements(elems, sourceType)
if _, ok := r[0].(*message.ReplyElement); ok { }
log.Warnf("警告: 一条信息只能包含一个 Reply 元素.")
return // ConvertContentMessage 将数据库用的 content 转换为消息元素数组
} func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message.SourceType) (r []message.IMessageElement) {
} elems := make([]msg.Element, len(content))
mid, err := strconv.Atoi(e.Get("data.id").String()) for i, v := range content {
customText := e.Get("data.text").String() elem := msg.Element{Type: v["type"].(string)}
switch { for k, v := range v["data"].(global.MSG) {
case customText != "": pair := msg.Pair{K: k, V: fmt.Sprint(v)}
var elem *message.ReplyElement elem.Data = append(elem.Data, pair)
var org db.StoredMessage
sender, senderErr := strconv.ParseInt(e.Get("data.[user_id,qq]").String(), 10, 64)
if senderErr != nil && err != nil {
log.Warnf("警告: 自定义 Reply 元素中必须包含 user_id 或 id")
break
}
msgTime, timeErr := strconv.ParseInt(e.Get("data.time").String(), 10, 64)
if timeErr != nil {
msgTime = time.Now().Unix()
}
messageSeq, seqErr := strconv.ParseInt(e.Get("data.seq").String(), 10, 64)
if err == nil {
org, _ = db.GetMessageByGlobalID(int32(mid))
}
if org != nil {
elem = &message.ReplyElement{
ReplySeq: org.GetAttribute().MessageSeq,
Sender: org.GetAttribute().SenderUin,
Time: int32(org.GetAttribute().Timestamp),
Elements: bot.ConvertStringMessage(customText, sourceType),
}
if senderErr != nil {
elem.Sender = sender
}
if timeErr != nil {
elem.Time = int32(msgTime)
}
if seqErr != nil {
elem.ReplySeq = int32(messageSeq)
}
} else {
elem = &message.ReplyElement{
ReplySeq: int32(messageSeq),
Sender: sender,
Time: int32(msgTime),
Elements: bot.ConvertStringMessage(customText, sourceType),
}
}
r = append([]message.IMessageElement{elem}, r...)
case err == nil:
org, err := db.GetMessageByGlobalID(int32(mid))
if err == nil {
r = append([]message.IMessageElement{
&message.ReplyElement{
ReplySeq: org.GetAttribute().MessageSeq,
Sender: org.GetAttribute().SenderUin,
Time: int32(org.GetAttribute().Timestamp),
Elements: bot.ConvertContentMessage(org.GetContent(), sourceType),
},
}, r...)
}
default:
log.Warnf("警告: Reply 元素中必须包含 text 或 id")
}
return
} }
if t == "forward" { elems[i] = elem
id := e.Get("data.id").String() }
if id == "" { return bot.ConvertElements(elems, sourceType)
log.Warnf("警告: Forward 元素中必须包含 id") }
} else {
if fwdMsg := bot.Client.DownloadForwardMessage(id); fwdMsg == nil { // ConvertElements 将解码后的消息数组转换为MiraiGo表示
log.Warnf("警告: Forward 信息不存在或已过期") func (bot *CQBot) ConvertElements(elems []msg.Element, sourceType message.SourceType) (r []message.IMessageElement) {
} else { var replyCount int
r = []message.IMessageElement{fwdMsg} for _, elem := range elems {
} me, err := bot.ConvertElement(elem, sourceType)
}
return
}
for i := range d {
delete(d, i)
}
e.Get("data").ForEach(func(key, value gjson.Result) bool {
d[key.Str] = value.String()
return true
})
elem, err := bot.ToElement(t, d, sourceType)
if err != nil { if err != nil {
log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) // TODO: don't use cqcode format
return if !base.IgnoreInvalidCQCode {
r = append(r, message.NewText(elem.CQCode()))
}
log.Warnf("转换消息 %v 到MiraiGo Element时出现错误: %v.", elem.CQCode(), err)
continue
} }
switch i := elem.(type) { switch i := me.(type) {
case *message.ReplyElement:
if replyCount > 0 {
log.Warnf("警告: 一条信息只能包含一个 Reply 元素.")
break
}
replyCount++
// 将回复消息放置于第一个
r = append([]message.IMessageElement{i}, r...)
case message.IMessageElement: case message.IMessageElement:
r = append(r, i) r = append(r, i)
case []message.IMessageElement: case []message.IMessageElement:
r = append(r, i...) r = append(r, i...)
} }
} }
if m.Type == gjson.String {
return bot.ConvertStringMessage(m.Str, sourceType)
}
if m.IsArray() {
m.ForEach(func(_, e gjson.Result) bool {
convertElem(e)
return true
})
}
if m.IsObject() {
convertElem(m)
}
return return
} }
// ConvertContentMessage 将数据库用的 content 转换为消息元素数组 func (bot *CQBot) reply(elem msg.Element, sourceType message.SourceType) (any, error) {
func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message.SourceType) (r []message.IMessageElement) { mid, err := strconv.Atoi(elem.Get("id"))
for _, c := range content { customText := elem.Get("text")
data := c["data"].(global.MSG) var re *message.ReplyElement
switch c["type"] { switch {
case "text": case customText != "":
r = append(r, message.NewText(data["text"].(string))) var org db.StoredMessage
case "image": sender, senderErr := strconv.ParseInt(elem.Get("user_id"), 10, 64)
u, ok := data["url"] if senderErr != nil {
d := make(map[string]string, 2) sender, senderErr = strconv.ParseInt(elem.Get("qq"), 10, 64)
if ok {
d["url"] = u.(string)
}
d["file"] = data["file"].(string)
e, err := bot.makeImageOrVideoElem(d, false, sourceType)
if err != nil {
log.Warnf("make image elem error: %v", err)
continue
}
flash, id := false, int32(0)
if t, ok := data["type"]; ok {
if t.(string) == "flash" {
flash = true
}
if t.(string) == "show" {
id := 0
switch idn := data["id"].(type) {
case int32:
id = int(idn)
case int:
id = idn
case int64:
id = int(idn)
default:
id = int(reflect.ValueOf(data["id"]).Convert(reflect.TypeOf(0)).Int())
}
if id < 40000 || id >= 40006 {
id = 40000
}
}
}
switch img := e.(type) {
case *msg.LocalImage:
img.Flash = flash
img.EffectID = id
case *message.GroupImageElement:
img.Flash = flash
img.EffectID = id
switch sub := data["subType"].(type) {
case int64:
img.ImageBizType = message.ImageBizType(sub)
case uint32:
img.ImageBizType = message.ImageBizType(sub)
}
case *message.FriendImageElement:
img.Flash = flash
}
r = append(r, e)
case "at":
switch data["subType"].(string) {
case "all":
r = append(r, message.NewAt(0))
case "user":
r = append(r, message.NewAt(reflect.ValueOf(data["target"]).Int(), data["display"].(string)))
default:
continue
}
case "redbag":
r = append(r, &message.RedBagElement{
MsgType: message.RedBagMessageType(data["type"].(int)),
Title: data["title"].(string),
})
case "forward":
r = append(r, &message.ForwardElement{
ResId: data["id"].(string),
})
case "face":
id := int32(0)
switch idn := data["id"].(type) {
case int32:
id = idn
case int:
id = int32(idn)
case int64:
id = int32(idn)
default:
id = int32(reflect.ValueOf(data["id"]).Convert(reflect.TypeOf(0)).Int())
}
r = append(r, message.NewFace(id))
case "video":
e, err := bot.makeImageOrVideoElem(map[string]string{"file": data["file"].(string)}, true, sourceType)
if err != nil {
log.Warnf("make image elem error: %v", err)
continue
}
r = append(r, e)
} }
if senderErr != nil && err != nil {
return nil, errors.New("警告: 自定义 reply 元素中必须包含 user_id 或 id")
}
msgTime, timeErr := strconv.ParseInt(elem.Get("time"), 10, 64)
if timeErr != nil {
msgTime = time.Now().Unix()
}
messageSeq, seqErr := strconv.ParseInt(elem.Get("seq"), 10, 64)
if err == nil {
org, _ = db.GetMessageByGlobalID(int32(mid))
}
if org != nil {
re = &message.ReplyElement{
ReplySeq: org.GetAttribute().MessageSeq,
Sender: org.GetAttribute().SenderUin,
Time: int32(org.GetAttribute().Timestamp),
Elements: bot.ConvertStringMessage(customText, sourceType),
}
if senderErr != nil {
re.Sender = sender
}
if timeErr != nil {
re.Time = int32(msgTime)
}
if seqErr != nil {
re.ReplySeq = int32(messageSeq)
}
break
}
re = &message.ReplyElement{
ReplySeq: int32(messageSeq),
Sender: sender,
Time: int32(msgTime),
Elements: bot.ConvertStringMessage(customText, sourceType),
}
case err == nil:
org, err := db.GetMessageByGlobalID(int32(mid))
if err != nil {
return nil, err
}
re = &message.ReplyElement{
ReplySeq: org.GetAttribute().MessageSeq,
Sender: org.GetAttribute().SenderUin,
Time: int32(org.GetAttribute().Timestamp),
Elements: bot.ConvertContentMessage(org.GetContent(), sourceType),
}
default:
return nil, errors.New("reply消息中必须包含 text 或 id")
} }
return return re, nil
} }
// ToElement 将解码后的CQCode转换为Element. // ConvertElement 将解码后的消息转换为MiraiGoElement.
// //
// 返回 interface{} 存在三种类型 // 返回 interface{} 存在三种类型
// //
// message.IMessageElement []message.IMessageElement nil // message.IMessageElement []message.IMessageElement nil
func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.SourceType) (m any, err error) { func (bot *CQBot) ConvertElement(elem msg.Element, sourceType message.SourceType) (m any, err error) {
switch t { switch elem.Type {
case "text": case "text":
text := elem.Get("text")
if base.SplitURL { if base.SplitURL {
var ret []message.IMessageElement var ret []message.IMessageElement
for _, text := range param.SplitURL(d["text"]) { for _, text := range param.SplitURL(text) {
ret = append(ret, message.NewText(text)) ret = append(ret, message.NewText(text))
} }
return ret, nil return ret, nil
} }
return message.NewText(d["text"]), nil return message.NewText(text), nil
case "image": case "image":
img, err := bot.makeImageOrVideoElem(d, false, sourceType) img, err := bot.makeImageOrVideoElem(elem, false, sourceType)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tp := d["type"] tp := elem.Get("type")
flash, id := false, int64(0) flash, id := false, int64(0)
switch tp { switch tp {
case "flash": case "flash":
flash = true flash = true
case "show": case "show":
id, _ = strconv.ParseInt(d["id"], 10, 64) id, _ = strconv.ParseInt(elem.Get("id"), 10, 64)
if id < 40000 || id >= 40006 { if id < 40000 || id >= 40006 {
id = 40000 id = 40000
} }
default: default:
return img, err return img, nil
} }
switch img := img.(type) { switch img := img.(type) {
case *msg.LocalImage: case *msg.LocalImage:
@ -815,24 +546,37 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
case *message.GroupImageElement: case *message.GroupImageElement:
img.Flash = flash img.Flash = flash
img.EffectID = int32(id) img.EffectID = int32(id)
i, _ := strconv.ParseInt(d["subType"], 10, 64) i, _ := strconv.ParseInt(elem.Get("subType"), 10, 64)
img.ImageBizType = message.ImageBizType(i) img.ImageBizType = message.ImageBizType(i)
case *message.FriendImageElement: case *message.FriendImageElement:
img.Flash = flash img.Flash = flash
} }
return img, err return img, nil
case "reply":
return bot.reply(elem, sourceType)
case "forward":
id := elem.Get("id")
if id != "" {
return nil, errors.New("forward 消息中必须包含 id")
}
fwdMsg := bot.Client.DownloadForwardMessage(id)
if fwdMsg == nil {
return nil, errors.New("forward 消息不存在或已过期")
}
return fwdMsg, nil
case "poke": case "poke":
t, _ := strconv.ParseInt(d["qq"], 10, 64) t, _ := strconv.ParseInt(elem.Get("qq"), 10, 64)
return &msg.Poke{Target: t}, nil return &msg.Poke{Target: t}, nil
case "tts": case "tts":
data, err := bot.Client.GetTts(d["text"]) data, err := bot.Client.GetTts(elem.Get("text"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &message.VoiceElement{Data: base.ResampleSilk(data)}, nil return &message.VoiceElement{Data: base.ResampleSilk(data)}, nil
case "record", "audio": case "record", "audio":
f := d["file"] f := elem.Get("file")
data, err := global.FindFile(f, d["cache"], global.VoicePath) data, err := global.FindFile(f, elem.Get("cache"), global.VoicePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -848,19 +592,18 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
} }
return &message.VoiceElement{Data: data}, nil return &message.VoiceElement{Data: data}, nil
case "face": case "face":
id, err := strconv.Atoi(d["id"]) id, err := strconv.Atoi(elem.Get("id"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if d["type"] == "sticker" { if elem.Get("type") == "sticker" {
return &message.AnimatedSticker{ID: int32(id)}, nil return &message.AnimatedSticker{ID: int32(id)}, nil
} }
return message.NewFace(int32(id)), nil return message.NewFace(int32(id)), nil
case "mention_all": case "mention_all":
d["qq"] = "all" return message.AtAll(), nil
fallthrough
case "at", "mention": case "at", "mention":
qq := d["qq"] qq := elem.Get("qq")
if qq == "all" { if qq == "all" {
return message.AtAll(), nil return message.AtAll(), nil
} }
@ -868,16 +611,17 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
if err != nil { if err != nil {
return nil, err return nil, err
} }
name := strings.TrimSpace(d["name"]) name := strings.TrimSpace(elem.Get("name"))
if len(name) > 0 { if len(name) > 0 {
name = "@" + name name = "@" + name
} }
return message.NewAt(t, name), nil return message.NewAt(t, name), nil
case "share": case "share":
return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil return message.NewUrlShare(elem.Get("url"), elem.Get("title"), elem.Get("content"), elem.Get("image")), nil
case "music": case "music":
if d["type"] == "qq" { id := elem.Get("id")
info, err := global.QQMusicSongInfo(d["id"]) if elem.Get("type") == "qq" {
info, err := global.QQMusicSongInfo(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -887,13 +631,13 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
name := info.Get("track_info.name").Str name := info.Get("track_info.name").Str
mid := info.Get("track_info.mid").Str mid := info.Get("track_info.mid").Str
albumMid := info.Get("track_info.album.mid").Str albumMid := info.Get("track_info.album.mid").Str
pinfo, _ := download.Request{URL: "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"}.JSON() pinfo, _ := download.Request{URL: "https://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"}.JSON()
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" 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 := pinfo.Get("url_mid.data.midurlinfo.0.purl").Str purl := pinfo.Get("url_mid.data.midurlinfo.0.purl").Str
preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" preview := "https://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg"
content := info.Get("track_info.singer.0.name").Str content := info.Get("track_info.singer.0.name").Str
if d["content"] != "" { if elem.Get("content") != "" {
content = d["content"] content = elem.Get("content")
} }
return &message.MusicShareElement{ return &message.MusicShareElement{
MusicType: message.QQMusic, MusicType: message.QQMusic,
@ -904,8 +648,8 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
MusicUrl: purl, MusicUrl: purl,
}, nil }, nil
} }
if d["type"] == "163" { if elem.Get("type") == "163" {
info, err := global.NeteaseMusicSongInfo(d["id"]) info, err := global.NeteaseMusicSongInfo(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -913,8 +657,8 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
return nil, errors.New("song not found") return nil, errors.New("song not found")
} }
name := info.Get("name").Str name := info.Get("name").Str
jumpURL := "https://y.music.163.com/m/song/" + d["id"] jumpURL := "https://y.music.163.com/m/song/" + id
musicURL := "http://music.163.com/song/media/outer/url?id=" + d["id"] musicURL := "http://music.163.com/song/media/outer/url?id=" + id
picURL := info.Get("album.picUrl").Str picURL := info.Get("album.picUrl").Str
artistName := "" artistName := ""
if info.Get("artists.0").Exists() { if info.Get("artists.0").Exists() {
@ -929,10 +673,10 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
MusicUrl: musicURL, MusicUrl: musicURL,
}, nil }, nil
} }
if d["type"] == "custom" { if elem.Get("type") == "custom" {
if d["subtype"] != "" { if elem.Get("subtype") != "" {
var subType int var subType int
switch d["subtype"] { switch elem.Get("subtype") {
default: default:
subType = message.QQMusic subType = message.QQMusic
case "163": case "163":
@ -946,59 +690,58 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
} }
return &message.MusicShareElement{ return &message.MusicShareElement{
MusicType: subType, MusicType: subType,
Title: d["title"], Title: elem.Get("title"),
Summary: d["content"], Summary: elem.Get("content"),
Url: d["url"], Url: elem.Get("url"),
PictureUrl: d["image"], PictureUrl: elem.Get("image"),
MusicUrl: d["audio"], MusicUrl: elem.Get("audio"),
}, nil }, nil
} }
xml := fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="[分享] %s" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2"><audio cover="%s" src="%s"/><title>%s</title><summary>%s</summary></item><source name="音乐" icon="https://i.gtimg.cn/open/app_icon/01/07/98/56/1101079856_100_m.png" url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app" a_actionData="com.tencent.qqmusic" i_actionData="tencent1101079856://" appid="1101079856" /></msg>`, xml := fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="[分享] %s" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2"><audio cover="%s" src="%s"/><title>%s</title><summary>%s</summary></item><source name="音乐" icon="https://i.gtimg.cn/open/app_icon/01/07/98/56/1101079856_100_m.png" url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app" a_actionData="com.tencent.qqmusic" i_actionData="tencent1101079856://" appid="1101079856" /></msg>`,
utils.XmlEscape(d["title"]), d["url"], d["image"], d["audio"], utils.XmlEscape(d["title"]), utils.XmlEscape(d["content"])) utils.XmlEscape(elem.Get("title")), elem.Get("url"), elem.Get("image"), elem.Get("audio"), utils.XmlEscape(elem.Get("title")), utils.XmlEscape(elem.Get("content")))
return &message.ServiceElement{ return &message.ServiceElement{
Id: 60, Id: 60,
Content: xml, Content: xml,
SubType: "music", SubType: "music",
}, nil }, nil
} }
return nil, errors.New("unsupported music type: " + d["type"]) return nil, errors.New("unsupported music type: " + elem.Get("type"))
case "dice": case "dice":
value := d["value"] value := elem.Get("value")
i, _ := strconv.ParseInt(value, 10, 64) i, _ := strconv.ParseInt(value, 10, 64)
if i < 0 || i > 6 { if i < 0 || i > 6 {
return nil, errors.New("invalid dice value " + value) return nil, errors.New("invalid dice value " + value)
} }
return message.NewDice(int32(i)), nil return message.NewDice(int32(i)), nil
case "rps": case "rps":
value := d["value"] value := elem.Get("value")
i, _ := strconv.ParseInt(value, 10, 64) i, _ := strconv.ParseInt(value, 10, 64)
if i < 0 || i > 2 { if i < 0 || i > 2 {
return nil, errors.New("invalid finger-guessing value " + value) return nil, errors.New("invalid finger-guessing value " + value)
} }
return message.NewFingerGuessing(int32(i)), nil return message.NewFingerGuessing(int32(i)), nil
case "xml": case "xml":
resID := d["resid"] resID := elem.Get("resid")
template := msg.EscapeValue(d["data"]) template := elem.Get("data")
i, _ := strconv.ParseInt(resID, 10, 64) i, _ := strconv.ParseInt(resID, 10, 64)
m := message.NewRichXml(template, i) m := message.NewRichXml(template, i)
return m, nil return m, nil
case "json": case "json":
resID := d["resid"] resID := elem.Get("resid")
data := elem.Get("data")
i, _ := strconv.ParseInt(resID, 10, 64) i, _ := strconv.ParseInt(resID, 10, 64)
if i == 0 { if i == 0 {
// 默认情况下走小程序通道 // 默认情况下走小程序通道
msg := message.NewLightApp(d["data"]) return message.NewLightApp(data), nil
return msg, nil
} }
// resid不为0的情况下走富文本通道后续补全透传service Id此处暂时不处理 TODO // resid不为0的情况下走富文本通道后续补全透传service Id此处暂时不处理 TODO
msg := message.NewRichJson(d["data"]) return message.NewRichJson(data), nil
return msg, nil
case "cardimage": case "cardimage":
source := d["source"] source := elem.Get("source")
icon := d["icon"] icon := elem.Get("icon")
brief := d["brief"] brief := elem.Get("brief")
parseIntWithDefault := func(name string, origin int64) int64 { parseIntWithDefault := func(name string, origin int64) int64 {
v, _ := strconv.ParseInt(d[name], 10, 64) v, _ := strconv.ParseInt(elem.Get(name), 10, 64)
if v <= 0 { if v <= 0 {
return origin return origin
} }
@ -1008,13 +751,13 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
maxWidth := parseIntWithDefault("maxwidth", 500) maxWidth := parseIntWithDefault("maxwidth", 500)
minHeight := parseIntWithDefault("minheight", 200) minHeight := parseIntWithDefault("minheight", 200)
maxHeight := parseIntWithDefault("maxheight", 1000) maxHeight := parseIntWithDefault("maxheight", 1000)
img, err := bot.makeImageOrVideoElem(d, false, sourceType) img, err := bot.makeImageOrVideoElem(elem, false, sourceType)
if err != nil { if err != nil {
return nil, errors.New("send cardimage faild") return nil, errors.New("send cardimage faild")
} }
return bot.makeShowPic(img, source, brief, icon, minWidth, minHeight, maxWidth, maxHeight, sourceType == message.SourceGroup) return bot.makeShowPic(img, source, brief, icon, minWidth, minHeight, maxWidth, maxHeight, sourceType == message.SourceGroup)
case "video": case "video":
file, err := bot.makeImageOrVideoElem(d, true, sourceType) file, err := bot.makeImageOrVideoElem(elem, true, sourceType)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1026,8 +769,8 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
return v, nil return v, nil
} }
var data []byte var data []byte
if cover, ok := d["cover"]; ok { if cover := elem.Get("cover"); cover != "" {
data, _ = global.FindFile(cover, d["cache"], global.ImagePath) data, _ = global.FindFile(cover, elem.Get("cache"), global.ImagePath)
} else { } else {
err = global.ExtractCover(v.File, v.File+".jpg") err = global.ExtractCover(v.File, v.File+".jpg")
if err != nil { if err != nil {
@ -1045,7 +788,7 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
_, _ = video.Seek(0, io.SeekStart) _, _ = video.Seek(0, io.SeekStart)
hash, _ := utils.ComputeMd5AndLength(video) hash, _ := utils.ComputeMd5AndLength(video)
cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash)+".mp4") cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash)+".mp4")
if !(d["cache"] == "" || d["cache"] == "1") || !global.PathExists(cacheFile) { if !(elem.Get("cache") == "" || elem.Get("cache") == "1") || !global.PathExists(cacheFile) {
err = global.EncodeMP4(v.File, cacheFile) err = global.EncodeMP4(v.File, cacheFile)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1055,17 +798,14 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
} }
return v, nil return v, nil
default: default:
return nil, errors.New("unsupported cq code: " + t) return nil, errors.New("unsupported message type: " + elem.Type)
} }
} }
// makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用 // makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用
func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceType message.SourceType) (message.IMessageElement, error) { func (bot *CQBot) makeImageOrVideoElem(elem msg.Element, video bool, sourceType message.SourceType) (message.IMessageElement, error) {
f := d["file"] f := elem.Get("file")
u, ok := d["url"] u := elem.Get("url")
if !ok {
u = ""
}
if strings.HasPrefix(f, "http") { if strings.HasPrefix(f, "http") {
hash := md5.Sum([]byte(f)) hash := md5.Sum([]byte(f))
cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache") cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
@ -1073,9 +813,9 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy
if video { if video {
maxSize = maxVideoSize maxSize = maxVideoSize
} }
thread, _ := strconv.Atoi(d["c"]) thread, _ := strconv.Atoi(elem.Get("c"))
exist := global.PathExists(cacheFile) exist := global.PathExists(cacheFile)
if exist && (d["cache"] == "" || d["cache"] == "1") { if exist && (elem.Get("cache") == "" || elem.Get("cache") == "1") {
goto useCacheFile goto useCacheFile
} }
if exist { if exist {
@ -1170,8 +910,9 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy
} }
exist := global.PathExists(rawPath) exist := global.PathExists(rawPath)
if !exist { if !exist {
if d["url"] != "" { if elem.Get("url") != "" {
return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, sourceType) elem.Data = []msg.Pair{{K: "file", V: elem.Get("url")}}
return bot.makeImageOrVideoElem(elem, false, sourceType)
} }
return nil, errors.New("invalid image") return nil, errors.New("invalid image")
} }
@ -1196,7 +937,11 @@ func (bot *CQBot) readImageCache(b []byte, sourceType message.SourceType) (messa
r.ReadString() r.ReadString()
imageURL := r.ReadString() imageURL := r.ReadString()
if size == 0 && imageURL != "" { if size == 0 && imageURL != "" {
return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, sourceType) // TODO: fix this
var elem msg.Element
elem.Type = "image"
elem.Data = []msg.Pair{{K: "file", V: imageURL}}
return bot.makeImageOrVideoElem(elem, false, sourceType)
} }
var rsp message.IMessageElement var rsp message.IMessageElement
switch sourceType { // nolint:exhaustive switch sourceType { // nolint:exhaustive
@ -1213,7 +958,10 @@ func (bot *CQBot) readImageCache(b []byte, sourceType message.SourceType) (messa
rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size)
} }
if err != nil && imageURL != "" { if err != nil && imageURL != "" {
return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, sourceType) var elem msg.Element
elem.Type = "image"
elem.Data = []msg.Pair{{K: "file", V: imageURL}}
return bot.makeImageOrVideoElem(elem, false, sourceType)
} }
return rsp, err return rsp, err
} }

View File

@ -88,16 +88,26 @@ func UnescapeValue(content string) string {
// @@@ 消息中间表示 @@@ // @@@ 消息中间表示 @@@
// Pair key value pair
type Pair struct {
K string
V string
}
// Element single message // Element single message
type Element struct { type Element struct {
Type string Type string
Data []Pair Data []Pair
} }
// Pair key value pair // Get 获取指定值
type Pair struct { func (e *Element) Get(k string) string {
K string for _, datum := range e.Data {
V string if datum.K == k {
return datum.V
}
}
return ""
} }
// CQCode convert element to cqcode // CQCode convert element to cqcode
@ -140,7 +150,7 @@ func (e *Element) MarshalJSON() ([]byte, error) {
buf.WriteByte('"') buf.WriteByte('"')
buf.WriteString(data.K) buf.WriteString(data.K)
buf.WriteString(`":`) buf.WriteString(`":`)
writeQuote(buf, data.V) buf.WriteString(QuoteJSON(data.V))
} }
buf.WriteString(`}}`) buf.WriteString(`}}`)
}), nil }), nil
@ -148,9 +158,10 @@ func (e *Element) MarshalJSON() ([]byte, error) {
const hex = "0123456789abcdef" const hex = "0123456789abcdef"
func writeQuote(b *bytes.Buffer, s string) { // QuoteJSON 按JSON转义为字符加上双引号
func QuoteJSON(s string) string {
i, j := 0, 0 i, j := 0, 0
var b strings.Builder
b.WriteByte('"') b.WriteByte('"')
for j < len(s) { for j < len(s) {
c := s[j] c := s[j]
@ -231,4 +242,5 @@ func writeQuote(b *bytes.Buffer, s string) {
b.WriteString(s[i:]) b.WriteString(s[i:])
b.WriteByte('"') b.WriteByte('"')
return b.String()
} }

View File

@ -1,7 +1,6 @@
package msg package msg
import ( import (
"bytes"
"encoding/json" "encoding/json"
"testing" "testing"
) )
@ -14,16 +13,14 @@ func jsonMarshal(s string) string {
return string(b) return string(b)
} }
func Test_quote(t *testing.T) { func TestQuoteJSON(t *testing.T) {
testcase := []string{ testcase := []string{
"\u0005", // issue 1773 "\u0005", // issue 1773
"\v", "\v",
} }
for _, input := range testcase { for _, input := range testcase {
var b bytes.Buffer got := QuoteJSON(input)
writeQuote(&b, input)
got := b.String()
expected := jsonMarshal(input) expected := jsonMarshal(input)
if got != expected { if got != expected {
t.Errorf("want %v but got %v", expected, got) t.Errorf("want %v but got %v", expected, got)

104
internal/msg/parse.go Normal file
View File

@ -0,0 +1,104 @@
package msg
import (
"github.com/tidwall/gjson"
)
// ParseObject 将消息JSON对象转为消息元素数组
func ParseObject(m gjson.Result) (r []Element) {
convert := func(e gjson.Result) {
var elem Element
elem.Type = e.Get("type").Str
e.Get("data").ForEach(func(key, value gjson.Result) bool {
pair := Pair{K: key.Str, V: value.String()}
elem.Data = append(elem.Data, pair)
return true
})
r = append(r, elem)
}
if m.IsArray() {
m.ForEach(func(_, e gjson.Result) bool {
convert(e)
return true
})
}
if m.IsObject() {
convert(m)
}
return
}
func text(txt string) Element {
return Element{
Type: "text",
Data: []Pair{
{
K: "text",
V: txt,
},
},
}
}
// ParseString 将字符串(CQ码)转为消息元素数组
func ParseString(raw string) (r []Element) {
var elem Element
for raw != "" {
i := 0
for i < len(raw) && !(raw[i] == '[' && i+4 < len(raw) && raw[i:i+4] == "[CQ:") {
i++
}
if i > 0 {
r = append(r, text(UnescapeText(raw[:i])))
}
if i+4 > len(raw) {
return
}
raw = raw[i+4:] // skip "[CQ:"
i = 0
for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
i++
}
if i+1 > len(raw) {
return
}
elem.Type = raw[:i]
elem.Data = nil // reset data
raw = raw[i:]
i = 0
for {
if raw[0] == ']' {
r = append(r, elem)
raw = raw[1:]
break
}
raw = raw[1:]
for i < len(raw) && raw[i] != '=' {
i++
}
if i+1 > len(raw) {
return
}
key := raw[:i]
raw = raw[i+1:] // skip "="
i = 0
for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
i++
}
if i+1 > len(raw) {
return
}
elem.Data = append(elem.Data, Pair{
K: key,
V: UnescapeValue(raw[:i]),
})
raw = raw[i:]
i = 0
}
}
return
}

View File

@ -1,22 +1,18 @@
package coolq package msg
import ( import (
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/MiraiGo/utils" "github.com/Mrs4s/MiraiGo/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/Mrs4s/go-cqhttp/internal/msg"
) )
var bot = CQBot{} func TestParseString(t *testing.T) {
// TODO: add more text
func TestCQBot_ConvertStringMessage(t *testing.T) { for _, v := range ParseString(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`) {
for _, v := range bot.ConvertStringMessage(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`, message.SourcePrivate) {
fmt.Println(v) fmt.Println(v)
} }
} }
@ -26,17 +22,18 @@ var (
benchArray = gjson.Parse(`[{"type":"text","data":{"text":"asdfqwerqwerqwer"}},{"type":"face","data":{"id":"115","text":"111"}},{"type":"text","data":{"text":"asdfasdfasdfasdfasdfasdfasd"}},{"type":"face","data":{"id":"217"}},{"type":"text","data":{"text":"] "}},{"type":"text","data":{"text":"123"}},{"type":"text","data":{"text":" ["}}]`) benchArray = gjson.Parse(`[{"type":"text","data":{"text":"asdfqwerqwerqwer"}},{"type":"face","data":{"id":"115","text":"111"}},{"type":"text","data":{"text":"asdfasdfasdfasdfasdfasdfasd"}},{"type":"face","data":{"id":"217"}},{"type":"text","data":{"text":"] "}},{"type":"text","data":{"text":"123"}},{"type":"text","data":{"text":" ["}}]`)
) )
func BenchmarkCQBot_ConvertStringMessage(b *testing.B) { func BenchmarkParseString(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
bot.ConvertStringMessage(bench, message.SourcePrivate) ParseString(bench)
} }
b.SetBytes(int64(len(bench))) b.SetBytes(int64(len(bench)))
} }
func BenchmarkCQBot_ConvertObjectMessage(b *testing.B) { func BenchmarkParseObject(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
bot.ConvertObjectMessage(benchArray, message.SourcePrivate) ParseObject(benchArray)
} }
b.SetBytes(int64(len(benchArray.Raw)))
} }
const bText = `123456789[]&987654321[]&987654321[]&987654321[]&987654321[]&987654321[]&` const bText = `123456789[]&987654321[]&987654321[]&987654321[]&987654321[]&987654321[]&`
@ -44,16 +41,7 @@ const bText = `123456789[]&987654321[]&987654321[]&987654321[]&987654321[]&98765
func BenchmarkCQCodeEscapeText(b *testing.B) { func BenchmarkCQCodeEscapeText(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
ret := bText ret := bText
msg.EscapeText(ret) EscapeText(ret)
}
}
func BenchmarkCQCodeEscapeTextBefore(b *testing.B) {
for i := 0; i < b.N; i++ {
ret := bText
ret = strings.ReplaceAll(ret, "&", "&amp;")
ret = strings.ReplaceAll(ret, "[", "&#91;")
strings.ReplaceAll(ret, "]", "&#93;")
} }
} }
@ -64,6 +52,6 @@ func TestCQCodeEscapeText(t *testing.T) {
ret = strings.ReplaceAll(ret, "&", "&amp;") ret = strings.ReplaceAll(ret, "&", "&amp;")
ret = strings.ReplaceAll(ret, "[", "&#91;") ret = strings.ReplaceAll(ret, "[", "&#91;")
ret = strings.ReplaceAll(ret, "]", "&#93;") ret = strings.ReplaceAll(ret, "]", "&#93;")
assert.Equal(t, ret, msg.EscapeText(rs)) assert.Equal(t, ret, EscapeText(rs))
} }
} }