From debc1ed1ae692e60fcf10b36d8c314fdbe122982 Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Thu, 16 Feb 2023 14:49:14 +0800 Subject: [PATCH] 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 --- coolq/api.go | 6 +- coolq/cqcode.go | 640 ++++++------------ internal/msg/element.go | 26 +- internal/msg/element_test.go | 7 +- internal/msg/parse.go | 104 +++ .../msg/parse_test.go | 34 +- 6 files changed, 335 insertions(+), 482 deletions(-) create mode 100644 internal/msg/parse.go rename coolq/cqcode_test.go => internal/msg/parse_test.go (59%) diff --git a/coolq/api.go b/coolq/api.go index 345c840..b7459b7 100644 --- a/coolq/api.go +++ b/coolq/api.go @@ -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()) diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 9d65ebb..9a0f1f3 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -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 { - 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 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) } - 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) + 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 { - log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) - return + // 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 := 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: 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) - } - 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) +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) } + 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{} 存在三种类型 // // 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(``, - 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 } diff --git a/internal/msg/element.go b/internal/msg/element.go index a787658..b547103 100644 --- a/internal/msg/element.go +++ b/internal/msg/element.go @@ -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() } diff --git a/internal/msg/element_test.go b/internal/msg/element_test.go index 4f37c26..775b6aa 100644 --- a/internal/msg/element_test.go +++ b/internal/msg/element_test.go @@ -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) diff --git a/internal/msg/parse.go b/internal/msg/parse.go new file mode 100644 index 0000000..9f981b9 --- /dev/null +++ b/internal/msg/parse.go @@ -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 +} diff --git a/coolq/cqcode_test.go b/internal/msg/parse_test.go similarity index 59% rename from coolq/cqcode_test.go rename to internal/msg/parse_test.go index 9e5dd89..21998c7 100644 --- a/coolq/cqcode_test.go +++ b/internal/msg/parse_test.go @@ -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, "&", "&") - ret = strings.ReplaceAll(ret, "[", "[") - strings.ReplaceAll(ret, "]", "]") + EscapeText(ret) } } @@ -64,6 +52,6 @@ func TestCQCodeEscapeText(t *testing.T) { ret = strings.ReplaceAll(ret, "&", "&") ret = strings.ReplaceAll(ret, "[", "[") ret = strings.ReplaceAll(ret, "]", "]") - assert.Equal(t, ret, msg.EscapeText(rs)) + assert.Equal(t, ret, EscapeText(rs)) } }