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")
// @rename(image_id->image)
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 {
log.Warnf("load image error: %v", err)
return Failed(100, "LOAD_FILE_ERROR", err.Error())

View File

@ -11,7 +11,6 @@ import (
"net/url"
"os"
"path"
"reflect"
"runtime"
"strconv"
"strings"
@ -382,431 +381,163 @@ func ToMessageContent(e []message.IMessageElement) (r []global.MSG) {
// ConvertStringMessage 将消息字符串转为消息元素数组
func (bot *CQBot) ConvertStringMessage(raw string, sourceType message.SourceType) (r []message.IMessageElement) {
var t, key string
d := map[string]string{}
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
elems := msg.ParseString(raw)
return bot.ConvertElements(elems, sourceType)
}
// ConvertObjectMessage 将消息JSON对象转为消息元素数组
func (bot *CQBot) ConvertObjectMessage(m gjson.Result, sourceType message.SourceType) (r []message.IMessageElement) {
d := make(map[string]string)
convertElem := func(e gjson.Result) {
t := e.Get("type").Str
if t == "reply" && sourceType&(message.SourceGroup|message.SourcePrivate) != 0 {
if len(r) > 0 {
if _, ok := r[0].(*message.ReplyElement); ok {
if m.Type == gjson.String {
return bot.ConvertStringMessage(m.Str, sourceType)
}
elems := msg.ParseObject(m)
return bot.ConvertElements(elems, sourceType)
}
// ConvertContentMessage 将数据库用的 content 转换为消息元素数组
func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message.SourceType) (r []message.IMessageElement) {
elems := make([]msg.Element, len(content))
for i, v := range content {
elem := msg.Element{Type: v["type"].(string)}
for k, v := range v["data"].(global.MSG) {
pair := msg.Pair{K: k, V: fmt.Sprint(v)}
elem.Data = append(elem.Data, pair)
}
elems[i] = elem
}
return bot.ConvertElements(elems, sourceType)
}
// ConvertElements 将解码后的消息数组转换为MiraiGo表示
func (bot *CQBot) ConvertElements(elems []msg.Element, sourceType message.SourceType) (r []message.IMessageElement) {
var replyCount int
for _, elem := range elems {
me, err := bot.ConvertElement(elem, sourceType)
if err != nil {
// TODO: don't use cqcode format
if !base.IgnoreInvalidCQCode {
r = append(r, message.NewText(elem.CQCode()))
}
log.Warnf("转换消息 %v 到MiraiGo Element时出现错误: %v.", elem.CQCode(), err)
continue
}
switch i := me.(type) {
case *message.ReplyElement:
if replyCount > 0 {
log.Warnf("警告: 一条信息只能包含一个 Reply 元素.")
return
}
}
mid, err := strconv.Atoi(e.Get("data.id").String())
customText := e.Get("data.text").String()
switch {
case customText != "":
var elem *message.ReplyElement
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" {
id := e.Get("data.id").String()
if id == "" {
log.Warnf("警告: Forward 元素中必须包含 id")
} else {
if fwdMsg := bot.Client.DownloadForwardMessage(id); fwdMsg == nil {
log.Warnf("警告: Forward 信息不存在或已过期")
} else {
r = []message.IMessageElement{fwdMsg}
}
}
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 {
log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err)
return
}
switch i := elem.(type) {
replyCount++
// 将回复消息放置于第一个
r = append([]message.IMessageElement{i}, r...)
case message.IMessageElement:
r = append(r, i)
case []message.IMessageElement:
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
}
// ConvertContentMessage 将数据库用的 content 转换为消息元素数组
func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message.SourceType) (r []message.IMessageElement) {
for _, c := range content {
data := c["data"].(global.MSG)
switch c["type"] {
case "text":
r = append(r, message.NewText(data["text"].(string)))
case "image":
u, ok := data["url"]
d := make(map[string]string, 2)
if ok {
d["url"] = u.(string)
func (bot *CQBot) reply(elem msg.Element, sourceType message.SourceType) (any, error) {
mid, err := strconv.Atoi(elem.Get("id"))
customText := elem.Get("text")
var re *message.ReplyElement
switch {
case customText != "":
var org db.StoredMessage
sender, senderErr := strconv.ParseInt(elem.Get("user_id"), 10, 64)
if senderErr != nil {
sender, senderErr = strconv.ParseInt(elem.Get("qq"), 10, 64)
}
d["file"] = data["file"].(string)
e, err := bot.makeImageOrVideoElem(d, false, sourceType)
if err != nil {
log.Warnf("make image elem error: %v", err)
continue
if senderErr != nil && err != nil {
return nil, errors.New("警告: 自定义 reply 元素中必须包含 user_id 或 id")
}
flash, id := false, int32(0)
if t, ok := data["type"]; ok {
if t.(string) == "flash" {
flash = true
msgTime, timeErr := strconv.ParseInt(elem.Get("time"), 10, 64)
if timeErr != nil {
msgTime = time.Now().Unix()
}
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())
messageSeq, seqErr := strconv.ParseInt(elem.Get("seq"), 10, 64)
if err == nil {
org, _ = db.GetMessageByGlobalID(int32(mid))
}
if id < 40000 || id >= 40006 {
id = 40000
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)
}
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)
if seqErr != nil {
re.ReplySeq = int32(messageSeq)
}
case *message.FriendImageElement:
img.Flash = flash
break
}
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)
}
}
return
re = &message.ReplyElement{
ReplySeq: int32(messageSeq),
Sender: sender,
Time: int32(msgTime),
Elements: bot.ConvertStringMessage(customText, sourceType),
}
// ToElement 将解码后的CQCode转换为Element.
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 re, nil
}
// ConvertElement 将解码后的消息转换为MiraiGoElement.
//
// 返回 interface{} 存在三种类型
//
// message.IMessageElement []message.IMessageElement nil
func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.SourceType) (m any, err error) {
switch t {
func (bot *CQBot) ConvertElement(elem msg.Element, sourceType message.SourceType) (m any, err error) {
switch elem.Type {
case "text":
text := elem.Get("text")
if base.SplitURL {
var ret []message.IMessageElement
for _, text := range param.SplitURL(d["text"]) {
for _, text := range param.SplitURL(text) {
ret = append(ret, message.NewText(text))
}
return ret, nil
}
return message.NewText(d["text"]), nil
return message.NewText(text), nil
case "image":
img, err := bot.makeImageOrVideoElem(d, false, sourceType)
img, err := bot.makeImageOrVideoElem(elem, false, sourceType)
if err != nil {
return nil, err
}
tp := d["type"]
tp := elem.Get("type")
flash, id := false, int64(0)
switch tp {
case "flash":
flash = true
case "show":
id, _ = strconv.ParseInt(d["id"], 10, 64)
id, _ = strconv.ParseInt(elem.Get("id"), 10, 64)
if id < 40000 || id >= 40006 {
id = 40000
}
default:
return img, err
return img, nil
}
switch img := img.(type) {
case *msg.LocalImage:
@ -815,24 +546,37 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
case *message.GroupImageElement:
img.Flash = flash
img.EffectID = int32(id)
i, _ := strconv.ParseInt(d["subType"], 10, 64)
i, _ := strconv.ParseInt(elem.Get("subType"), 10, 64)
img.ImageBizType = message.ImageBizType(i)
case *message.FriendImageElement:
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":
t, _ := strconv.ParseInt(d["qq"], 10, 64)
t, _ := strconv.ParseInt(elem.Get("qq"), 10, 64)
return &msg.Poke{Target: t}, nil
case "tts":
data, err := bot.Client.GetTts(d["text"])
data, err := bot.Client.GetTts(elem.Get("text"))
if err != nil {
return nil, err
}
return &message.VoiceElement{Data: base.ResampleSilk(data)}, nil
case "record", "audio":
f := d["file"]
data, err := global.FindFile(f, d["cache"], global.VoicePath)
f := elem.Get("file")
data, err := global.FindFile(f, elem.Get("cache"), global.VoicePath)
if err != nil {
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
case "face":
id, err := strconv.Atoi(d["id"])
id, err := strconv.Atoi(elem.Get("id"))
if err != nil {
return nil, err
}
if d["type"] == "sticker" {
if elem.Get("type") == "sticker" {
return &message.AnimatedSticker{ID: int32(id)}, nil
}
return message.NewFace(int32(id)), nil
case "mention_all":
d["qq"] = "all"
fallthrough
return message.AtAll(), nil
case "at", "mention":
qq := d["qq"]
qq := elem.Get("qq")
if qq == "all" {
return message.AtAll(), nil
}
@ -868,16 +611,17 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
if err != nil {
return nil, err
}
name := strings.TrimSpace(d["name"])
name := strings.TrimSpace(elem.Get("name"))
if len(name) > 0 {
name = "@" + name
}
return message.NewAt(t, name), nil
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":
if d["type"] == "qq" {
info, err := global.QQMusicSongInfo(d["id"])
id := elem.Get("id")
if elem.Get("type") == "qq" {
info, err := global.QQMusicSongInfo(id)
if err != nil {
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
mid := info.Get("track_info.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"
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
if d["content"] != "" {
content = d["content"]
if elem.Get("content") != "" {
content = elem.Get("content")
}
return &message.MusicShareElement{
MusicType: message.QQMusic,
@ -904,8 +648,8 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
MusicUrl: purl,
}, nil
}
if d["type"] == "163" {
info, err := global.NeteaseMusicSongInfo(d["id"])
if elem.Get("type") == "163" {
info, err := global.NeteaseMusicSongInfo(id)
if err != nil {
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")
}
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"]
jumpURL := "https://y.music.163.com/m/song/" + id
musicURL := "http://music.163.com/song/media/outer/url?id=" + id
picURL := info.Get("album.picUrl").Str
artistName := ""
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,
}, nil
}
if d["type"] == "custom" {
if d["subtype"] != "" {
if elem.Get("type") == "custom" {
if elem.Get("subtype") != "" {
var subType int
switch d["subtype"] {
switch elem.Get("subtype") {
default:
subType = message.QQMusic
case "163":
@ -946,59 +690,58 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
}
return &message.MusicShareElement{
MusicType: subType,
Title: d["title"],
Summary: d["content"],
Url: d["url"],
PictureUrl: d["image"],
MusicUrl: d["audio"],
Title: elem.Get("title"),
Summary: elem.Get("content"),
Url: elem.Get("url"),
PictureUrl: elem.Get("image"),
MusicUrl: elem.Get("audio"),
}, 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>`,
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{
Id: 60,
Content: xml,
SubType: "music",
}, nil
}
return nil, errors.New("unsupported music type: " + d["type"])
return nil, errors.New("unsupported music type: " + elem.Get("type"))
case "dice":
value := d["value"]
value := elem.Get("value")
i, _ := strconv.ParseInt(value, 10, 64)
if i < 0 || i > 6 {
return nil, errors.New("invalid dice value " + value)
}
return message.NewDice(int32(i)), nil
case "rps":
value := d["value"]
value := elem.Get("value")
i, _ := strconv.ParseInt(value, 10, 64)
if i < 0 || i > 2 {
return nil, errors.New("invalid finger-guessing value " + value)
}
return message.NewFingerGuessing(int32(i)), nil
case "xml":
resID := d["resid"]
template := msg.EscapeValue(d["data"])
resID := elem.Get("resid")
template := elem.Get("data")
i, _ := strconv.ParseInt(resID, 10, 64)
m := message.NewRichXml(template, i)
return m, nil
case "json":
resID := d["resid"]
resID := elem.Get("resid")
data := elem.Get("data")
i, _ := strconv.ParseInt(resID, 10, 64)
if i == 0 {
// 默认情况下走小程序通道
msg := message.NewLightApp(d["data"])
return msg, nil
return message.NewLightApp(data), nil
}
// resid不为0的情况下走富文本通道后续补全透传service Id此处暂时不处理 TODO
msg := message.NewRichJson(d["data"])
return msg, nil
return message.NewRichJson(data), nil
case "cardimage":
source := d["source"]
icon := d["icon"]
brief := d["brief"]
source := elem.Get("source")
icon := elem.Get("icon")
brief := elem.Get("brief")
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 {
return origin
}
@ -1008,13 +751,13 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
maxWidth := parseIntWithDefault("maxwidth", 500)
minHeight := parseIntWithDefault("minheight", 200)
maxHeight := parseIntWithDefault("maxheight", 1000)
img, err := bot.makeImageOrVideoElem(d, false, sourceType)
img, err := bot.makeImageOrVideoElem(elem, false, sourceType)
if err != nil {
return nil, errors.New("send cardimage faild")
}
return bot.makeShowPic(img, source, brief, icon, minWidth, minHeight, maxWidth, maxHeight, sourceType == message.SourceGroup)
case "video":
file, err := bot.makeImageOrVideoElem(d, true, sourceType)
file, err := bot.makeImageOrVideoElem(elem, true, sourceType)
if err != nil {
return nil, err
}
@ -1026,8 +769,8 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
return v, nil
}
var data []byte
if cover, ok := d["cover"]; ok {
data, _ = global.FindFile(cover, d["cache"], global.ImagePath)
if cover := elem.Get("cover"); cover != "" {
data, _ = global.FindFile(cover, elem.Get("cache"), global.ImagePath)
} else {
err = global.ExtractCover(v.File, v.File+".jpg")
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)
hash, _ := utils.ComputeMd5AndLength(video)
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)
if err != nil {
return nil, err
@ -1055,17 +798,14 @@ func (bot *CQBot) ToElement(t string, d map[string]string, sourceType message.So
}
return v, nil
default:
return nil, errors.New("unsupported cq code: " + t)
return nil, errors.New("unsupported message type: " + elem.Type)
}
}
// makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用
func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceType message.SourceType) (message.IMessageElement, error) {
f := d["file"]
u, ok := d["url"]
if !ok {
u = ""
}
func (bot *CQBot) makeImageOrVideoElem(elem msg.Element, video bool, sourceType message.SourceType) (message.IMessageElement, error) {
f := elem.Get("file")
u := elem.Get("url")
if strings.HasPrefix(f, "http") {
hash := md5.Sum([]byte(f))
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 {
maxSize = maxVideoSize
}
thread, _ := strconv.Atoi(d["c"])
thread, _ := strconv.Atoi(elem.Get("c"))
exist := global.PathExists(cacheFile)
if exist && (d["cache"] == "" || d["cache"] == "1") {
if exist && (elem.Get("cache") == "" || elem.Get("cache") == "1") {
goto useCacheFile
}
if exist {
@ -1170,8 +910,9 @@ func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video bool, sourceTy
}
exist := global.PathExists(rawPath)
if !exist {
if d["url"] != "" {
return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, sourceType)
if elem.Get("url") != "" {
elem.Data = []msg.Pair{{K: "file", V: elem.Get("url")}}
return bot.makeImageOrVideoElem(elem, false, sourceType)
}
return nil, errors.New("invalid image")
}
@ -1196,7 +937,11 @@ func (bot *CQBot) readImageCache(b []byte, sourceType message.SourceType) (messa
r.ReadString()
imageURL := r.ReadString()
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
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)
}
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
}

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

View File

@ -1,7 +1,6 @@
package msg
import (
"bytes"
"encoding/json"
"testing"
)
@ -14,16 +13,14 @@ func jsonMarshal(s string) string {
return string(b)
}
func Test_quote(t *testing.T) {
func TestQuoteJSON(t *testing.T) {
testcase := []string{
"\u0005", // issue 1773
"\v",
}
for _, input := range testcase {
var b bytes.Buffer
writeQuote(&b, input)
got := b.String()
got := QuoteJSON(input)
expected := jsonMarshal(input)
if got != expected {
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 (
"fmt"
"strings"
"testing"
"github.com/Mrs4s/MiraiGo/message"
"github.com/Mrs4s/MiraiGo/utils"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
"github.com/Mrs4s/go-cqhttp/internal/msg"
)
var bot = CQBot{}
func TestCQBot_ConvertStringMessage(t *testing.T) {
for _, v := range bot.ConvertStringMessage(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`, message.SourcePrivate) {
func TestParseString(t *testing.T) {
// TODO: add more text
for _, v := range ParseString(`[CQ:face,id=115,text=111][CQ:face,id=217]] [CQ:text,text=123] [`) {
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":" ["}}]`)
)
func BenchmarkCQBot_ConvertStringMessage(b *testing.B) {
func BenchmarkParseString(b *testing.B) {
for i := 0; i < b.N; i++ {
bot.ConvertStringMessage(bench, message.SourcePrivate)
ParseString(bench)
}
b.SetBytes(int64(len(bench)))
}
func BenchmarkCQBot_ConvertObjectMessage(b *testing.B) {
func BenchmarkParseObject(b *testing.B) {
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[]&`
@ -44,16 +41,7 @@ const bText = `123456789[]&987654321[]&987654321[]&987654321[]&987654321[]&98765
func BenchmarkCQCodeEscapeText(b *testing.B) {
for i := 0; i < b.N; i++ {
ret := bText
msg.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;")
EscapeText(ret)
}
}
@ -64,6 +52,6 @@ func TestCQCodeEscapeText(t *testing.T) {
ret = strings.ReplaceAll(ret, "&", "&amp;")
ret = strings.ReplaceAll(ret, "[", "&#91;")
ret = strings.ReplaceAll(ret, "]", "&#93;")
assert.Equal(t, ret, msg.EscapeText(rs))
assert.Equal(t, ret, EscapeText(rs))
}
}