package coolq import ( "bytes" "crypto/md5" "encoding/base64" "encoding/hex" xml2 "encoding/xml" "errors" "fmt" "io/ioutil" "math" "math/rand" "net/url" "path" "runtime" "strconv" "strings" "github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/go-cqhttp/global" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" ) /* var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`) var typeReg = regexp.MustCompile(`\[CQ:(\w+)`) var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) */ var IgnoreInvalidCQCode = false var SplitUrl = false type PokeElement struct { Target int64 } type GiftElement struct { Target int64 GiftId message.GroupGift } type MusicElement struct { Title string Summary string Url string PictureUrl string MusicUrl string } type QQMusicElement struct { MusicElement } type CloudMusicElement struct { MusicElement } type MiguMusicElement struct { MusicElement } func (e *GiftElement) Type() message.ElementType { return message.At } func (e *MusicElement) Type() message.ElementType { return message.Service } var GiftId = [...]message.GroupGift{ message.SweetWink, message.HappyCola, message.LuckyBracelet, message.Cappuccino, message.CatWatch, message.FleeceGloves, message.RainbowCandy, message.Stronger, message.LoveMicrophone, message.HoldingYourHand, message.CuteCat, message.MysteryMask, message.ImBusy, message.LoveMask, } func (e *PokeElement) Type() message.ElementType { return message.At } func ToArrayMessage(e []message.IMessageElement, code int64, raw ...bool) (r []MSG) { ur := false if len(raw) != 0 { ur = raw[0] } m := &message.SendingMessage{Elements: e} reply := m.FirstOrNil(func(e message.IMessageElement) bool { _, ok := e.(*message.ReplyElement) return ok }) if reply != nil { r = append(r, MSG{ "type": "reply", "data": map[string]string{"id": fmt.Sprint(ToGlobalId(code, reply.(*message.ReplyElement).ReplySeq))}, }) } for _, elem := range e { m := MSG{} switch o := elem.(type) { case *message.TextElement: m = MSG{ "type": "text", "data": map[string]string{"text": o.Content}, } case *message.LightAppElement: //m = MSG{ // "type": "text", // "data": map[string]string{"text": o.Content}, //} m = MSG{ "type": "json", "data": map[string]string{"data": o.Content}, } case *message.AtElement: if o.Target == 0 { m = MSG{ "type": "at", "data": map[string]string{"qq": "all"}, } } else { m = MSG{ "type": "at", "data": map[string]string{"qq": fmt.Sprint(o.Target)}, } } case *message.RedBagElement: m = MSG{ "type": "redbag", "data": map[string]string{"title": o.Title}, } case *message.ForwardElement: m = MSG{ "type": "forward", "data": map[string]string{"id": o.ResId}, } case *message.FaceElement: m = MSG{ "type": "face", "data": map[string]string{"id": fmt.Sprint(o.Index)}, } case *message.VoiceElement: if ur { m = MSG{ "type": "record", "data": map[string]string{"file": o.Name}, } } else { m = MSG{ "type": "record", "data": map[string]string{"file": o.Name, "url": o.Url}, } } case *message.ShortVideoElement: if ur { m = MSG{ "type": "video", "data": map[string]string{"file": o.Name}, } } else { m = MSG{ "type": "video", "data": map[string]string{"file": o.Name, "url": o.Url}, } } case *message.ImageElement: if ur { m = MSG{ "type": "image", "data": map[string]string{"file": o.Filename}, } } else { m = MSG{ "type": "image", "data": map[string]string{"file": o.Filename, "url": o.Url}, } } case *message.GroupImageElement: if ur { m = MSG{ "type": "image", "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, } } else { m = MSG{ "type": "image", "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, } } case *message.FriendImageElement: if ur { m = MSG{ "type": "image", "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, } } else { m = MSG{ "type": "image", "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, } } case *message.GroupFlashImgElement: return []MSG{{ "type": "image", "data": map[string]string{"file": o.Filename, "type": "flash"}, }} case *message.FriendFlashImgElement: return []MSG{{ "type": "image", "data": map[string]string{"file": o.Filename, "type": "flash"}, }} case *message.ServiceElement: if isOk := strings.Contains(o.Content, " 0 { if _, ok := r[0].(*message.ReplyElement); ok { log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") return } } mid, err := strconv.Atoi(params["id"]) if err == nil { org := bot.GetMessage(int32(mid)) if org != nil { r = append([]message.IMessageElement{ &message.ReplyElement{ ReplySeq: org["message-id"].(int32), Sender: org["sender"].(message.Sender).Uin, Time: org["time"].(int32), Elements: bot.ConvertStringMessage(org["message"].(string), group), }, }, r...) return } } } if t == "forward" { // 单独处理转发 if id, ok := params["id"]; ok { r = []message.IMessageElement{bot.Client.DownloadForwardMessage(id)} return } } elem, err := bot.ToElement(t, params, group) if err != nil { org := "[" + string(cqCode) + "]" if !IgnoreInvalidCQCode { log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) r = append(r, message.NewText(org)) } else { log.Warnf("转换CQ码 %v 时出现错误: %v 将忽略.", org, err) } return } switch i := elem.(type) { case message.IMessageElement: r = append(r, i) case []message.IMessageElement: r = append(r, i...) } } for hasNext() { ch := next() switch stat { case 0: if isCQCodeBegin(ch) { saveTempText() tempText = append(tempText, []rune("[CQ:")...) move(3) stat = 1 } else { tempText = append(tempText, ch) } case 1: if isCQCodeBegin(ch) { move(-1) stat = 0 } else if ch == ']' { saveCQCode() stat = 0 } else { cqCode = append(cqCode, ch) tempText = append(tempText, ch) } } } saveTempText() return } func (bot *CQBot) ConvertObjectMessage(m gjson.Result, group bool) (r []message.IMessageElement) { convertElem := func(e gjson.Result) { t := e.Get("type").Str if t == "reply" && group { if len(r) > 0 { if _, ok := r[0].(*message.ReplyElement); ok { log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") return } } mid, err := strconv.Atoi(e.Get("data").Get("id").String()) if err == nil { org := bot.GetMessage(int32(mid)) if org != nil { r = append([]message.IMessageElement{ &message.ReplyElement{ ReplySeq: org["message-id"].(int32), Sender: org["sender"].(message.Sender).Uin, Time: org["time"].(int32), Elements: bot.ConvertStringMessage(org["message"].(string), group), }, }, r...) return } } } if t == "forward" { r = []message.IMessageElement{bot.Client.DownloadForwardMessage(e.Get("data.id").String())} return } d := make(map[string]string) e.Get("data").ForEach(func(key, value gjson.Result) bool { d[key.Str] = value.String() return true }) elem, err := bot.ToElement(t, d, group) if err != nil { log.Warnf("转换CQ码到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", err) return } switch i := elem.(type) { case message.IMessageElement: r = append(r, i) case []message.IMessageElement: r = append(r, i...) } } if m.Type == gjson.String { return bot.ConvertStringMessage(m.Str, group) } if m.IsArray() { for _, e := range m.Array() { convertElem(e) } } if m.IsObject() { convertElem(m) } return } // ToElement 将解码后的CQCode转换为Element. // 返回 interface{} 存在三种类型 // message.IMessageElement []message.IMessageElement nil func (bot *CQBot) ToElement(t string, d map[string]string, group bool) (m interface{}, err error) { switch t { case "text": if SplitUrl { var ret []message.IMessageElement for _, text := range global.SplitUrl(d["text"]) { ret = append(ret, message.NewText(text)) } return ret, nil } return message.NewText(d["text"]), nil case "image": img, err := bot.makeImageElem(d, group) if err != nil { return nil, err } tp := d["type"] if tp != "show" && tp != "flash" { return img, nil } if i, ok := img.(*message.ImageElement); ok { // 秀图,闪照什么的就直接传了吧 if group { img, err = bot.Client.UploadGroupImage(1, i.Data) } else { img, err = bot.Client.UploadPrivateImage(1, i.Data) } if err != nil { return nil, err } } switch tp { case "flash": if i, ok := img.(*message.GroupImageElement); ok { return &message.GroupFlashPicElement{GroupImageElement: *i}, nil } if i, ok := img.(*message.FriendImageElement); ok { return &message.FriendFlashPicElement{FriendImageElement: *i}, nil } case "show": id, _ := strconv.ParseInt(d["id"], 10, 64) if id < 40000 || id >= 40006 { id = 40000 } if i, ok := img.(*message.GroupImageElement); ok { return &message.GroupShowPicElement{GroupImageElement: *i, EffectId: int32(id)}, nil } return img, nil // 私聊还没做 } case "poke": t, _ := strconv.ParseInt(d["qq"], 10, 64) return &PokeElement{Target: t}, nil case "gift": if !group { return nil, errors.New("private gift unsupported") // no free private gift } t, _ := strconv.ParseInt(d["qq"], 10, 64) id, _ := strconv.Atoi(d["id"]) if id < 0 || id >= 14 { return nil, errors.New("invalid gift id") } return &GiftElement{Target: t, GiftId: GiftId[id]}, nil case "tts": defer func() { if r := recover(); r != nil { m = nil err = errors.New("tts 转换失败") } }() data, err := bot.Client.GetTts(d["text"]) ioutil.WriteFile("tts.silk", data, 777) if err != nil { return nil, err } return &message.VoiceElement{Data: data}, nil case "record": f := d["file"] data, err := global.FindFile(f, d["cache"], global.VOICE_PATH) if err == global.ErrSyntax { data, err = global.FindFile(f, d["cache"], global.VOICE_PATH_OLD) } if err != nil { return nil, err } if !global.IsAMRorSILK(data) { data, err = global.Encoder(data) if err != nil { return nil, err } } return &message.VoiceElement{Data: data}, nil case "face": id, err := strconv.Atoi(d["id"]) if err != nil { return nil, err } return message.NewFace(int32(id)), nil case "at": qq := d["qq"] if qq == "all" { return message.AtAll(), nil } t, _ := strconv.ParseInt(qq, 10, 64) return message.NewAt(t), nil case "share": return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil case "music": if d["type"] == "qq" { info, err := global.QQMusicSongInfo(d["id"]) if err != nil { return nil, err } if !info.Get("track_info").Exists() { return nil, errors.New("song not found") } aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10) name := info.Get("track_info.name").Str + " - " + info.Get("track_info.singer.0.name").Str mid := info.Get("track_info.mid").Str albumMid := info.Get("track_info.album.mid").Str pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") jumpUrl := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" if len(aid) < 2 { return nil, errors.New("song error") } content := "来自go-cqhttp" if d["content"] != "" { content = d["content"] } return &QQMusicElement{MusicElement: MusicElement{ Title: name, Summary: content, Url: jumpUrl, PictureUrl: preview, MusicUrl: purl, }}, nil } if d["type"] == "163" { info, err := global.NeteaseMusicSongInfo(d["id"]) if err != nil { return nil, err } if !info.Exists() { return nil, errors.New("song not found") } name := info.Get("name").Str jumpUrl := "https://y.music.163.com/m/song/" + d["id"] musicUrl := "http://music.163.com/song/media/outer/url?id=" + d["id"] picUrl := info.Get("album.picUrl").Str artistName := "" if info.Get("artists.0").Exists() { artistName = info.Get("artists.0.name").Str } return &CloudMusicElement{MusicElement{ Title: name, Summary: artistName, Url: jumpUrl, PictureUrl: picUrl, MusicUrl: musicUrl, }}, nil } if d["type"] == "custom" { if d["subtype"] == "qq" { return &QQMusicElement{MusicElement{ Title: d["title"], Summary: d["content"], Url: d["url"], PictureUrl: d["image"], MusicUrl: d["purl"], }}, nil } if d["subtype"] == "163" { return &CloudMusicElement{MusicElement{ Title: d["title"], Summary: d["content"], Url: d["url"], PictureUrl: d["image"], MusicUrl: d["purl"], }}, nil } if d["subtype"] == "migu" { return &MiguMusicElement{MusicElement{ Title: d["title"], Summary: d["content"], Url: d["url"], PictureUrl: d["image"], MusicUrl: d["purl"], }}, nil } xml := fmt.Sprintf(``, XmlEscape(d["title"]), d["url"], d["image"], d["audio"], XmlEscape(d["title"]), XmlEscape(d["content"])) return &message.ServiceElement{ Id: 60, Content: xml, SubType: "music", }, nil } return nil, errors.New("unsupported music type: " + d["type"]) case "xml": resId := d["resid"] template := CQCodeEscapeValue(d["data"]) i, _ := strconv.ParseInt(resId, 10, 64) msg := message.NewRichXml(template, i) return msg, nil case "json": resId := d["resid"] i, _ := strconv.ParseInt(resId, 10, 64) if i == 0 { //默认情况下走小程序通道 msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) return msg, nil } //resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) return msg, nil case "cardimage": source := d["source"] icon := d["icon"] minWidth, _ := strconv.ParseInt(d["minwidth"], 10, 64) if minWidth == 0 { minWidth = 200 } minHeight, _ := strconv.ParseInt(d["minheight"], 10, 64) if minHeight == 0 { minHeight = 200 } maxWidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64) if maxWidth == 0 { maxWidth = 500 } maxHeight, _ := strconv.ParseInt(d["maxheight"], 10, 64) if maxHeight == 0 { maxHeight = 1000 } img, err := bot.makeImageElem(d, group) if err != nil { return nil, errors.New("send cardimage faild") } return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, group) default: return nil, errors.New("unsupported cq code: " + t) } return nil, nil } func XmlEscape(c string) string { buf := new(bytes.Buffer) _ = xml2.EscapeText(buf, []byte(c)) return buf.String() } func CQCodeEscapeText(raw string) string { ret := raw ret = strings.ReplaceAll(ret, "&", "&") ret = strings.ReplaceAll(ret, "[", "[") ret = strings.ReplaceAll(ret, "]", "]") return ret } func CQCodeEscapeValue(value string) string { ret := CQCodeEscapeText(value) ret = strings.ReplaceAll(ret, ",", ",") return ret } func CQCodeUnescapeText(content string) string { ret := content ret = strings.ReplaceAll(ret, "[", "[") ret = strings.ReplaceAll(ret, "]", "]") ret = strings.ReplaceAll(ret, "&", "&") return ret } func CQCodeUnescapeValue(content string) string { ret := strings.ReplaceAll(content, ",", ",") ret = CQCodeUnescapeText(ret) return ret } // 图片 elem 生成器,单独拎出来,用于公用 func (bot *CQBot) makeImageElem(d map[string]string, group bool) (message.IMessageElement, error) { f := d["file"] if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { cache := d["cache"] if cache == "" { cache = "1" } hash := md5.Sum([]byte(f)) cacheFile := path.Join(global.CACHE_PATH, hex.EncodeToString(hash[:])+".cache") if global.PathExists(cacheFile) && cache == "1" { b, err := ioutil.ReadFile(cacheFile) if err == nil { return message.NewImage(b), nil } } b, err := global.GetBytes(f) if err != nil { return nil, err } _ = ioutil.WriteFile(cacheFile, b, 0644) return message.NewImage(b), nil } if strings.HasPrefix(f, "base64") { b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) if err != nil { return nil, err } return message.NewImage(b), nil } if strings.HasPrefix(f, "file") { fu, err := url.Parse(f) if err != nil { return nil, err } if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { fu.Path = fu.Path[1:] } b, err := ioutil.ReadFile(fu.Path) if err != nil { return nil, err } return message.NewImage(b), nil } rawPath := path.Join(global.IMAGE_PATH, f) if !global.PathExists(rawPath) && global.PathExists(path.Join(global.IMAGE_PATH_OLD, f)) { rawPath = path.Join(global.IMAGE_PATH_OLD, f) } if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { rawPath += ".cqimg" } if !global.PathExists(rawPath) && d["url"] != "" { return bot.makeImageElem(map[string]string{"file": d["url"]}, group) } if global.PathExists(rawPath) { b, err := ioutil.ReadFile(rawPath) if err != nil { return nil, err } if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { return message.NewImage(b), nil } if len(b) < 20 { return nil, errors.New("invalid local file") } var size int32 var hash []byte var url string if path.Ext(rawPath) == ".cqimg" { for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { kv := strings.SplitN(line, "=", 2) switch kv[0] { case "md5": hash, _ = hex.DecodeString(strings.ReplaceAll(kv[1], "\r", "")) case "size": t, _ := strconv.Atoi(strings.ReplaceAll(kv[1], "\r", "")) size = int32(t) } } } else { r := binary.NewReader(b) hash = r.ReadBytes(16) size = r.ReadInt32() r.ReadString() url = r.ReadString() } if size == 0 { if url != "" { return bot.makeImageElem(map[string]string{"file": url}, group) } return nil, errors.New("img size is 0") } if len(hash) != 16 { return nil, errors.New("invalid hash") } if group { rsp, err := bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) if err != nil { if url != "" { return bot.makeImageElem(map[string]string{"file": url}, group) } return nil, err } return rsp, nil } rsp, err := bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) if err != nil { if url != "" { return bot.makeImageElem(map[string]string{"file": url}, group) } return nil, err } return rsp, nil } return nil, errors.New("invalid image") } //makeShowPic 一种xml 方式发送的群消息图片 func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) { xml := "" var suf message.IMessageElement if i, ok := elem.(*message.ImageElement); ok { if group == false { gm, err := bot.Client.UploadPrivateImage(1, i.Data) if err != nil { log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) return nil, err } suf = gm xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) } else { gm, err := bot.Client.UploadGroupImage(1, i.Data) if err != nil { log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) return nil, err } suf = gm xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) } } if i, ok := elem.(*message.GroupImageElement); ok { xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) suf = i } if i, ok := elem.(*message.FriendImageElement); ok { xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) suf = i } if xml != "" { //log.Warn(xml) ret := []message.IMessageElement{suf} ret = append(ret, message.NewRichXml(xml, 5)) return ret, nil } return nil, errors.New("生成xml图片消息失败") }