diff --git a/coolq/cqcode.go b/coolq/cqcode.go index 77c16ee..6268617 100644 --- a/coolq/cqcode.go +++ b/coolq/cqcode.go @@ -1,1131 +1,1131 @@ -package coolq - -import ( - "bytes" - "crypto/md5" - "encoding/base64" - "encoding/hex" - xml2 "encoding/xml" - "errors" - "fmt" - "io" - "io/ioutil" - "math" - "math/rand" - "net/url" - "os" - "path" - "runtime" - "strconv" - "strings" - "time" - - "github.com/Mrs4s/MiraiGo/binary" - "github.com/Mrs4s/MiraiGo/message" - "github.com/Mrs4s/MiraiGo/utils" - "github.com/Mrs4s/go-cqhttp/global" - log "github.com/sirupsen/logrus" - "github.com/tidwall/gjson" -) - -/* -var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`) -var typeReg = regexp.MustCompile(`\[CQ:(\w+)`) -var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) -*/ - -// IgnoreInvalidCQCode 是否忽略无效CQ码 -var IgnoreInvalidCQCode = false - -// SplitURL 是否分割URL -var SplitURL = false - -const maxImageSize = 1024 * 1024 * 30 // 30MB -const maxVideoSize = 1024 * 1024 * 100 // 100MB -// PokeElement 拍一拍 -type PokeElement struct { - Target int64 -} - -// GiftElement 礼物 -type GiftElement struct { - Target int64 - GiftID message.GroupGift -} - -// LocalImageElement 本地图片 -type LocalImageElement struct { - message.ImageElement - Stream io.ReadSeeker - File string -} - -// LocalVoiceElement 本地语音 -type LocalVoiceElement struct { - message.VoiceElement - Stream io.ReadSeeker -} - -// LocalVideoElement 本地视频 -type LocalVideoElement struct { - message.ShortVideoElement - File string - thumb io.ReadSeeker -} - -// Type 获取元素类型ID -func (e *GiftElement) Type() message.ElementType { - // Make message.IMessageElement Happy - return message.At -} - -// GiftID 礼物ID数组 -var GiftID = [...]message.GroupGift{ - message.SweetWink, - message.HappyCola, - message.LuckyBracelet, - message.Cappuccino, - message.CatWatch, - message.FleeceGloves, - message.RainbowCandy, - message.Stronger, - message.LoveMicrophone, - message.HoldingYourHand, - message.CuteCat, - message.MysteryMask, - message.ImBusy, - message.LoveMask, -} - -// Type 获取元素类型ID -func (e *PokeElement) Type() message.ElementType { - // Make message.IMessageElement Happy - return message.At -} - -// ToArrayMessage 将消息元素数组转为MSG数组以用于消息上报 -func ToArrayMessage(e []message.IMessageElement, id int64, isRaw ...bool) (r []MSG) { - r = []MSG{} - ur := false - if len(isRaw) != 0 { - ur = isRaw[0] - } - m := &message.SendingMessage{Elements: e} - reply := m.FirstOrNil(func(e message.IMessageElement) bool { - _, ok := e.(*message.ReplyElement) - return ok - }) - if reply != nil { - r = append(r, MSG{ - "type": "reply", - "data": map[string]string{"id": fmt.Sprint(toGlobalID(id, reply.(*message.ReplyElement).ReplySeq))}, - }) - } - for _, elem := range e { - var m MSG - switch o := elem.(type) { - case *message.TextElement: - m = MSG{ - "type": "text", - "data": map[string]string{"text": o.Content}, - } - case *message.LightAppElement: - // m = MSG{ - // "type": "text", - // "data": map[string]string{"text": o.Content}, - // } - m = MSG{ - "type": "json", - "data": map[string]string{"data": o.Content}, - } - case *message.AtElement: - if o.Target == 0 { - m = MSG{ - "type": "at", - "data": map[string]string{"qq": "all"}, - } - } else { - m = MSG{ - "type": "at", - "data": map[string]string{"qq": fmt.Sprint(o.Target)}, - } - } - case *message.RedBagElement: - m = MSG{ - "type": "redbag", - "data": map[string]string{"title": o.Title}, - } - case *message.ForwardElement: - m = MSG{ - "type": "forward", - "data": map[string]string{"id": o.ResId}, - } - case *message.FaceElement: - m = MSG{ - "type": "face", - "data": map[string]string{"id": fmt.Sprint(o.Index)}, - } - case *message.VoiceElement: - if ur { - m = MSG{ - "type": "record", - "data": map[string]string{"file": o.Name}, - } - } else { - m = MSG{ - "type": "record", - "data": map[string]string{"file": o.Name, "url": o.Url}, - } - } - case *message.ShortVideoElement: - if ur { - m = MSG{ - "type": "video", - "data": map[string]string{"file": o.Name}, - } - } else { - m = MSG{ - "type": "video", - "data": map[string]string{"file": o.Name, "url": o.Url}, - } - } - case *message.ImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": o.Filename}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": o.Filename, "url": o.Url}, - } - } - case *message.GroupImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, - } - } - case *message.FriendImageElement: - if ur { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, - } - } else { - m = MSG{ - "type": "image", - "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, - } - } - case *message.GroupFlashImgElement: - return []MSG{{ - "type": "image", - "data": map[string]string{"file": o.Filename, "type": "flash"}, - }} - case *message.FriendFlashImgElement: - return []MSG{{ - "type": "image", - "data": map[string]string{"file": o.Filename, "type": "flash"}, - }} - case *message.ServiceElement: - if isOk := strings.Contains(o.Content, " 0 { - if _, ok := r[0].(*message.ReplyElement); ok { - log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") - return - } - } - mid, err := strconv.Atoi(params["id"]) - customText := params["text"] - if err == nil { - org := bot.GetMessage(int32(mid)) - if org != nil { - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: org["message-id"].(int32), - Sender: org["sender"].(message.Sender).Uin, - Time: org["time"].(int32), - Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), - }, - }, r...) - return - } - } else if customText != "" { - sender, err := strconv.ParseInt(params["qq"], 10, 64) - if err != nil { - log.Warnf("警告:自定义 Reply 元素中必须包含Uin") - return - } - msgTime, err := strconv.ParseInt(params["time"], 10, 64) - if err != nil { - msgTime = time.Now().Unix() - } - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: int32(0), - Sender: sender, - Time: int32(msgTime), - Elements: bot.ConvertStringMessage(customText, isGroup), - }, - }, r...) - return - } - } - if t == "forward" { // 单独处理转发 - if id, ok := params["id"]; ok { - r = []message.IMessageElement{bot.Client.DownloadForwardMessage(id)} - return - } - } - elem, err := bot.ToElement(t, params, isGroup) - if err != nil { - org := "[CQ:" + string(cqCode) + "]" - if !IgnoreInvalidCQCode { - log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) - r = append(r, message.NewText(org)) - } else { - log.Warnf("转换CQ码 %v 时出现错误: %v 将忽略.", org, err) - } - return - } - switch i := elem.(type) { - case message.IMessageElement: - r = append(r, i) - case []message.IMessageElement: - r = append(r, i...) - } - } - for hasNext() { - ch := next() - switch stat { - case 0: - if isCQCodeBegin(ch) { - saveTempText() - tempText = append(tempText, []rune("[CQ:")...) - move(3) - stat = 1 - } else { - tempText = append(tempText, ch) - } - case 1: - if isCQCodeBegin(ch) { - move(-1) - stat = 0 - } else if ch == ']' { - saveCQCode() - stat = 0 - } else { - cqCode = append(cqCode, ch) - tempText = append(tempText, ch) - } - } - } - saveTempText() - return -} - -// ConvertObjectMessage 将消息JSON对象转为消息元素数组 -func (bot *CQBot) ConvertObjectMessage(m gjson.Result, isGroup bool) (r []message.IMessageElement) { - convertElem := func(e gjson.Result) { - t := e.Get("type").Str - if t == "reply" && isGroup { - if len(r) > 0 { - if _, ok := r[0].(*message.ReplyElement); ok { - log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") - return - } - } - mid, err := strconv.Atoi(e.Get("data").Get("id").String()) - customText := e.Get("data").Get("text").String() - if err == nil { - org := bot.GetMessage(int32(mid)) - if org != nil { - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: org["message-id"].(int32), - Sender: org["sender"].(message.Sender).Uin, - Time: org["time"].(int32), - Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), - }, - }, r...) - return - } - } else if customText != "" { - sender, err := strconv.ParseInt(e.Get("data").Get("qq").String(), 10, 64) - if err != nil { - log.Warnf("警告:自定义 Reply 元素中必须包含Uin") - return - } - msgTime, err := strconv.ParseInt(e.Get("data").Get("time").String(), 10, 64) - if err != nil { - msgTime = time.Now().Unix() - } - r = append([]message.IMessageElement{ - &message.ReplyElement{ - ReplySeq: int32(0), - Sender: sender, - Time: int32(msgTime), - Elements: bot.ConvertStringMessage(customText, isGroup), - }, - }, r...) - return - } - } - if t == "forward" { - r = []message.IMessageElement{bot.Client.DownloadForwardMessage(e.Get("data.id").String())} - return - } - d := make(map[string]string) - e.Get("data").ForEach(func(key, value gjson.Result) bool { - d[key.Str] = value.String() - return true - }) - elem, err := bot.ToElement(t, d, isGroup) - if err != nil { - log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) - return - } - switch i := elem.(type) { - case message.IMessageElement: - r = append(r, i) - case []message.IMessageElement: - r = append(r, i...) - } - } - if m.Type == gjson.String { - return bot.ConvertStringMessage(m.Str, isGroup) - } - if m.IsArray() { - for _, e := range m.Array() { - convertElem(e) - } - } - if m.IsObject() { - convertElem(m) - } - return -} - -// ToElement 将解码后的CQCode转换为Element. -// -// 返回 interface{} 存在三种类型 -// -// message.IMessageElement []message.IMessageElement nil -func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m interface{}, err error) { - switch t { - case "text": - if SplitURL { - var ret []message.IMessageElement - for _, text := range global.SplitURL(d["text"]) { - ret = append(ret, message.NewText(text)) - } - return ret, nil - } - return message.NewText(d["text"]), nil - case "image": - img, err := bot.makeImageOrVideoElem(d, false, isGroup) - if err != nil { - return nil, err - } - tp := d["type"] - if tp != "show" && tp != "flash" { - return img, nil - } - if i, ok := img.(*LocalImageElement); ok { // 秀图,闪照什么的就直接传了吧 - if isGroup { - img, err = bot.UploadLocalImageAsGroup(1, i) - } else { - img, err = bot.UploadLocalImageAsPrivate(1, i) - } - if err != nil { - return nil, err - } - } - switch tp { - case "flash": - if i, ok := img.(*message.GroupImageElement); ok { - return &message.GroupFlashPicElement{GroupImageElement: *i}, nil - } - if i, ok := img.(*message.FriendImageElement); ok { - return &message.FriendFlashPicElement{FriendImageElement: *i}, nil - } - case "show": - id, _ := strconv.ParseInt(d["id"], 10, 64) - if id < 40000 || id >= 40006 { - id = 40000 - } - if i, ok := img.(*message.GroupImageElement); ok { - return &message.GroupShowPicElement{GroupImageElement: *i, EffectId: int32(id)}, nil - } - return img, nil // 私聊还没做 - } - - case "poke": - t, _ := strconv.ParseInt(d["qq"], 10, 64) - return &PokeElement{Target: t}, nil - case "gift": - if !isGroup { - return nil, errors.New("private gift unsupported") // no free private gift - } - t, _ := strconv.ParseInt(d["qq"], 10, 64) - id, _ := strconv.Atoi(d["id"]) - if id < 0 || id >= 14 { - return nil, errors.New("invalid gift id") - } - return &GiftElement{Target: t, GiftID: GiftID[id]}, nil - case "tts": - defer func() { - if r := recover(); r != nil { - m = nil - err = errors.New("tts 转换失败") - } - }() - data, err := bot.Client.GetTts(d["text"]) - if err != nil { - return nil, err - } - return &message.VoiceElement{Data: data}, nil - case "record": - f := d["file"] - data, err := global.FindFile(f, d["cache"], global.VoicePath) - if err == global.ErrSyntax { - data, err = global.FindFile(f, d["cache"], global.VoicePathOld) - } - if err != nil { - return nil, err - } - if !global.IsAMRorSILK(data) { - data, err = global.EncoderSilk(data) - if err != nil { - return nil, err - } - } - return &message.VoiceElement{Data: data}, nil - case "face": - id, err := strconv.Atoi(d["id"]) - if err != nil { - return nil, err - } - return message.NewFace(int32(id)), nil - case "at": - qq := d["qq"] - if qq == "all" { - return message.AtAll(), nil - } - t, _ := strconv.ParseInt(qq, 10, 64) - return message.NewAt(t), nil - case "share": - return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil - case "music": - if d["type"] == "qq" { - info, err := global.QQMusicSongInfo(d["id"]) - if err != nil { - return nil, err - } - if !info.Get("track_info").Exists() { - return nil, errors.New("song not found") - } - aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10) - name := info.Get("track_info.name").Str - mid := info.Get("track_info.mid").Str - albumMid := info.Get("track_info.album.mid").Str - pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") - jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" - purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str - preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" - if len(aid) < 2 { - return nil, errors.New("song error") - } - content := info.Get("track_info.singer.0.name").Str - if d["content"] != "" { - content = d["content"] - } - return &message.MusicShareElement{ - MusicType: message.QQMusic, - Title: name, - Summary: content, - Url: jumpURL, - PictureUrl: preview, - MusicUrl: purl, - }, nil - } - if d["type"] == "163" { - info, err := global.NeteaseMusicSongInfo(d["id"]) - if err != nil { - return nil, err - } - if !info.Exists() { - return nil, errors.New("song not found") - } - name := info.Get("name").Str - jumpURL := "https://y.music.163.com/m/song/" + d["id"] - musicURL := "http://music.163.com/song/media/outer/url?id=" + d["id"] - picURL := info.Get("album.picUrl").Str - artistName := "" - if info.Get("artists.0").Exists() { - artistName = info.Get("artists.0.name").Str - } - return &message.MusicShareElement{ - MusicType: message.CloudMusic, - Title: name, - Summary: artistName, - Url: jumpURL, - PictureUrl: picURL, - MusicUrl: musicURL, - }, nil - } - if d["type"] == "custom" { - if d["subtype"] != "" { - var subtype = map[string]int{ - "qq": message.QQMusic, - "163": message.CloudMusic, - "migu": message.MiguMusic, - "kugou": message.KugouMusic, - "kuwo": message.KuwoMusic, - } - var musicType = 0 - if tp, ok := subtype[d["subtype"]]; ok { - musicType = tp - } - return &message.MusicShareElement{ - MusicType: musicType, - Title: d["title"], - Summary: d["content"], - Url: d["url"], - PictureUrl: d["image"], - MusicUrl: d["purl"], - }, nil - } - xml := fmt.Sprintf(``, - XMLEscape(d["title"]), d["url"], d["image"], d["audio"], XMLEscape(d["title"]), XMLEscape(d["content"])) - return &message.ServiceElement{ - Id: 60, - Content: xml, - SubType: "music", - }, nil - } - return nil, errors.New("unsupported music type: " + d["type"]) - case "xml": - resID := d["resid"] - template := CQCodeEscapeValue(d["data"]) - i, _ := strconv.ParseInt(resID, 10, 64) - msg := message.NewRichXml(template, i) - return msg, nil - case "json": - resID := d["resid"] - i, _ := strconv.ParseInt(resID, 10, 64) - if i == 0 { - // 默认情况下走小程序通道 - msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) - return msg, nil - } - // resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO - msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) - return msg, nil - case "cardimage": - source := d["source"] - icon := d["icon"] - minWidth, _ := strconv.ParseInt(d["minwidth"], 10, 64) - if minWidth == 0 { - minWidth = 200 - } - minHeight, _ := strconv.ParseInt(d["minheight"], 10, 64) - if minHeight == 0 { - minHeight = 200 - } - maxWidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64) - if maxWidth == 0 { - maxWidth = 500 - } - maxHeight, _ := strconv.ParseInt(d["maxheight"], 10, 64) - if maxHeight == 0 { - maxHeight = 1000 - } - img, err := bot.makeImageOrVideoElem(d, false, isGroup) - if err != nil { - return nil, errors.New("send cardimage faild") - } - return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, isGroup) - case "video": - cache := d["cache"] - if cache == "" { - cache = "1" - } - file, err := bot.makeImageOrVideoElem(d, true, isGroup) - if err != nil { - return nil, err - } - v := file.(*LocalVideoElement) - if v.File == "" { - return v, nil - } - var data []byte - if cover, ok := d["cover"]; ok { - data, _ = global.FindFile(cover, cache, global.ImagePath) - } else { - _ = global.ExtractCover(v.File, v.File+".jpg") - data, _ = ioutil.ReadFile(v.File + ".jpg") - } - v.thumb = bytes.NewReader(data) - video, _ := os.Open(v.File) - defer video.Close() - _, err = video.Seek(4, io.SeekStart) - if err != nil { - return nil, err - } - var header = make([]byte, 4) - _, err = video.Read(header) - if err != nil { - return nil, err - } - if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp - _, _ = video.Seek(0, io.SeekStart) - hash, _ := utils.ComputeMd5AndLength(video) - cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".mp4") - if global.PathExists(cacheFile) && cache == "1" { - goto ok - } - err = global.EncodeMP4(v.File, cacheFile) - if err != nil { - return nil, err - } - ok: - v.File = cacheFile - } - return v, nil - default: - return nil, errors.New("unsupported cq code: " + t) - } - return nil, nil -} - -// XMLEscape 将字符串c转义为XML字符串 -func XMLEscape(c string) string { - buf := new(bytes.Buffer) - _ = xml2.EscapeText(buf, []byte(c)) - return buf.String() -} - -/*CQCodeEscapeText 将字符串raw中部分字符转义 - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeEscapeText(raw string) string { - ret := raw - ret = strings.ReplaceAll(ret, "&", "&") - ret = strings.ReplaceAll(ret, "[", "[") - ret = strings.ReplaceAll(ret, "]", "]") - return ret -} - -/*CQCodeEscapeValue 将字符串value中部分字符转义 - -, -> , - -*/ -func CQCodeEscapeValue(value string) string { - ret := CQCodeEscapeText(value) - ret = strings.ReplaceAll(ret, ",", ",") - return ret -} - -/*CQCodeUnescapeText 将字符串content中部分字符反转义 - -& -> & - -[ -> [ - -] -> ] - -*/ -func CQCodeUnescapeText(content string) string { - ret := content - ret = strings.ReplaceAll(ret, "[", "[") - ret = strings.ReplaceAll(ret, "]", "]") - ret = strings.ReplaceAll(ret, "&", "&") - return ret -} - -/*CQCodeUnescapeValue 将字符串content中部分字符反转义 - -, -> , - -*/ -func CQCodeUnescapeValue(content string) string { - ret := strings.ReplaceAll(content, ",", ",") - ret = CQCodeUnescapeText(ret) - return ret -} - -// makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用 -func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) (message.IMessageElement, error) { - f := d["file"] - if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { - cache := d["cache"] - c := d["c"] - if cache == "" { - cache = "1" - } - hash := md5.Sum([]byte(f)) - cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache") - var maxSize = func() int64 { - if video { - return maxVideoSize - } - return maxImageSize - }() - thread, _ := strconv.Atoi(c) - if global.PathExists(cacheFile) && cache == "1" { - goto hasCacheFile - } - if global.PathExists(cacheFile) { - _ = os.Remove(cacheFile) - } - if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil { - return nil, err - } - hasCacheFile: - if video { - return &LocalVideoElement{File: cacheFile}, nil - } - return &LocalImageElement{File: cacheFile}, nil - } - if strings.HasPrefix(f, "file") { - fu, err := url.Parse(f) - if err != nil { - return nil, err - } - if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { - fu.Path = fu.Path[1:] - } - info, err := os.Stat(fu.Path) - if err != nil { - if !os.IsExist(err) { - return nil, errors.New("file not found") - } - return nil, err - } - if video { - if info.Size() == 0 || info.Size() >= maxVideoSize { - return nil, errors.New("invalid video size") - } - return &LocalVideoElement{File: fu.Path}, nil - } - if info.Size() == 0 || info.Size() >= maxImageSize { - return nil, errors.New("invalid image size") - } - return &LocalImageElement{File: fu.Path}, nil - } - rawPath := path.Join(global.ImagePath, f) - if video { - rawPath = path.Join(global.VideoPath, f) - if !global.PathExists(rawPath) { - return nil, errors.New("invalid video") - } - if path.Ext(rawPath) == ".video" { - b, _ := ioutil.ReadFile(rawPath) - r := binary.NewReader(b) - return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效 - Md5: r.ReadBytes(16), - ThumbMd5: r.ReadBytes(16), - Size: r.ReadInt32(), - ThumbSize: r.ReadInt32(), - Name: r.ReadString(), - Uuid: r.ReadAvailable(), - }}, nil - } - return &LocalVideoElement{File: rawPath}, nil - } - if strings.HasPrefix(f, "base64") { - b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) - if err != nil { - return nil, err - } - return &LocalImageElement{Stream: bytes.NewReader(b)}, nil - } - if !global.PathExists(rawPath) && global.PathExists(path.Join(global.ImagePathOld, f)) { - rawPath = path.Join(global.ImagePathOld, f) - } - if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { - rawPath += ".cqimg" - } - if !global.PathExists(rawPath) && d["url"] != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group) - } - if global.PathExists(rawPath) { - file, err := os.Open(rawPath) - if err != nil { - return nil, err - } - if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { - return &LocalImageElement{Stream: file}, nil - } - b, err := ioutil.ReadAll(file) - if err != nil { - return nil, err - } - if len(b) < 20 { - return nil, errors.New("invalid local file") - } - var ( - size int32 - hash []byte - url string - ) - if path.Ext(rawPath) == ".cqimg" { - for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { - kv := strings.SplitN(line, "=", 2) - switch kv[0] { - case "md5": - hash, _ = hex.DecodeString(strings.ReplaceAll(kv[1], "\r", "")) - case "size": - t, _ := strconv.Atoi(strings.ReplaceAll(kv[1], "\r", "")) - size = int32(t) - } - } - } else { - r := binary.NewReader(b) - hash = r.ReadBytes(16) - size = r.ReadInt32() - r.ReadString() - url = r.ReadString() - } - if size == 0 { - if url != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) - } - return nil, errors.New("img size is 0") - } - if len(hash) != 16 { - return nil, errors.New("invalid hash") - } - var rsp message.IMessageElement - if group { - rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) - goto ok - } - rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) - ok: - if err != nil { - if url != "" { - return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) - } - return nil, err - } - return rsp, nil - } - return nil, errors.New("invalid image") -} - -// makeShowPic 一种xml 方式发送的群消息图片 -func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) { - xml := "" - var suf message.IMessageElement - if i, ok := elem.(*LocalImageElement); ok { - if !group { - gm, err := bot.UploadLocalImageAsPrivate(1, i) - if err != nil { - log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) - return nil, err - } - suf = gm - xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - } else { - gm, err := bot.UploadLocalImageAsGroup(1, i) - if err != nil { - log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) - return nil, err - } - suf = gm - xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - } - } - - if i, ok := elem.(*message.GroupImageElement); ok { - xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - suf = i - } - if i, ok := elem.(*message.FriendImageElement); ok { - xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) - suf = i - } - if xml != "" { - // log.Warn(xml) - ret := []message.IMessageElement{suf} - ret = append(ret, message.NewRichXml(xml, 5)) - return ret, nil - } - return nil, errors.New("生成xml图片消息失败") -} +package coolq + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + xml2 "encoding/xml" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "net/url" + "os" + "path" + "runtime" + "strconv" + "strings" + "time" + + "github.com/Mrs4s/MiraiGo/binary" + "github.com/Mrs4s/MiraiGo/message" + "github.com/Mrs4s/MiraiGo/utils" + "github.com/Mrs4s/go-cqhttp/global" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" +) + +/* +var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`) +var typeReg = regexp.MustCompile(`\[CQ:(\w+)`) +var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`) +*/ + +// IgnoreInvalidCQCode 是否忽略无效CQ码 +var IgnoreInvalidCQCode = false + +// SplitURL 是否分割URL +var SplitURL = false + +const maxImageSize = 1024 * 1024 * 30 // 30MB +const maxVideoSize = 1024 * 1024 * 100 // 100MB +// PokeElement 拍一拍 +type PokeElement struct { + Target int64 +} + +// GiftElement 礼物 +type GiftElement struct { + Target int64 + GiftID message.GroupGift +} + +// LocalImageElement 本地图片 +type LocalImageElement struct { + message.ImageElement + Stream io.ReadSeeker + File string +} + +// LocalVoiceElement 本地语音 +type LocalVoiceElement struct { + message.VoiceElement + Stream io.ReadSeeker +} + +// LocalVideoElement 本地视频 +type LocalVideoElement struct { + message.ShortVideoElement + File string + thumb io.ReadSeeker +} + +// Type 获取元素类型ID +func (e *GiftElement) Type() message.ElementType { + // Make message.IMessageElement Happy + return message.At +} + +// GiftID 礼物ID数组 +var GiftID = [...]message.GroupGift{ + message.SweetWink, + message.HappyCola, + message.LuckyBracelet, + message.Cappuccino, + message.CatWatch, + message.FleeceGloves, + message.RainbowCandy, + message.Stronger, + message.LoveMicrophone, + message.HoldingYourHand, + message.CuteCat, + message.MysteryMask, + message.ImBusy, + message.LoveMask, +} + +// Type 获取元素类型ID +func (e *PokeElement) Type() message.ElementType { + // Make message.IMessageElement Happy + return message.At +} + +// ToArrayMessage 将消息元素数组转为MSG数组以用于消息上报 +func ToArrayMessage(e []message.IMessageElement, id int64, isRaw ...bool) (r []MSG) { + r = []MSG{} + ur := false + if len(isRaw) != 0 { + ur = isRaw[0] + } + m := &message.SendingMessage{Elements: e} + reply := m.FirstOrNil(func(e message.IMessageElement) bool { + _, ok := e.(*message.ReplyElement) + return ok + }) + if reply != nil { + r = append(r, MSG{ + "type": "reply", + "data": map[string]string{"id": fmt.Sprint(toGlobalID(id, reply.(*message.ReplyElement).ReplySeq))}, + }) + } + for _, elem := range e { + var m MSG + switch o := elem.(type) { + case *message.TextElement: + m = MSG{ + "type": "text", + "data": map[string]string{"text": o.Content}, + } + case *message.LightAppElement: + // m = MSG{ + // "type": "text", + // "data": map[string]string{"text": o.Content}, + // } + m = MSG{ + "type": "json", + "data": map[string]string{"data": o.Content}, + } + case *message.AtElement: + if o.Target == 0 { + m = MSG{ + "type": "at", + "data": map[string]string{"qq": "all"}, + } + } else { + m = MSG{ + "type": "at", + "data": map[string]string{"qq": fmt.Sprint(o.Target)}, + } + } + case *message.RedBagElement: + m = MSG{ + "type": "redbag", + "data": map[string]string{"title": o.Title}, + } + case *message.ForwardElement: + m = MSG{ + "type": "forward", + "data": map[string]string{"id": o.ResId}, + } + case *message.FaceElement: + m = MSG{ + "type": "face", + "data": map[string]string{"id": fmt.Sprint(o.Index)}, + } + case *message.VoiceElement: + if ur { + m = MSG{ + "type": "record", + "data": map[string]string{"file": o.Name}, + } + } else { + m = MSG{ + "type": "record", + "data": map[string]string{"file": o.Name, "url": o.Url}, + } + } + case *message.ShortVideoElement: + if ur { + m = MSG{ + "type": "video", + "data": map[string]string{"file": o.Name}, + } + } else { + m = MSG{ + "type": "video", + "data": map[string]string{"file": o.Name, "url": o.Url}, + } + } + case *message.ImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": o.Filename}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": o.Filename, "url": o.Url}, + } + } + case *message.GroupImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, + } + } + case *message.FriendImageElement: + if ur { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image"}, + } + } else { + m = MSG{ + "type": "image", + "data": map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": CQCodeEscapeText(o.Url)}, + } + } + case *message.GroupFlashImgElement: + return []MSG{{ + "type": "image", + "data": map[string]string{"file": o.Filename, "type": "flash"}, + }} + case *message.FriendFlashImgElement: + return []MSG{{ + "type": "image", + "data": map[string]string{"file": o.Filename, "type": "flash"}, + }} + case *message.ServiceElement: + if isOk := strings.Contains(o.Content, " 0 { + if _, ok := r[0].(*message.ReplyElement); ok { + log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") + return + } + } + mid, err := strconv.Atoi(params["id"]) + customText := params["text"] + if err == nil { + org := bot.GetMessage(int32(mid)) + if org != nil { + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: org["message-id"].(int32), + Sender: org["sender"].(message.Sender).Uin, + Time: org["time"].(int32), + Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), + }, + }, r...) + return + } + } else if customText != "" { + sender, err := strconv.ParseInt(params["qq"], 10, 64) + if err != nil { + log.Warnf("警告:自定义 Reply 元素中必须包含Uin") + return + } + msgTime, err := strconv.ParseInt(params["time"], 10, 64) + if err != nil { + msgTime = time.Now().Unix() + } + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: int32(0), + Sender: sender, + Time: int32(msgTime), + Elements: bot.ConvertStringMessage(customText, isGroup), + }, + }, r...) + return + } + } + if t == "forward" { // 单独处理转发 + if id, ok := params["id"]; ok { + r = []message.IMessageElement{bot.Client.DownloadForwardMessage(id)} + return + } + } + elem, err := bot.ToElement(t, params, isGroup) + if err != nil { + org := "[CQ:" + string(cqCode) + "]" + if !IgnoreInvalidCQCode { + log.Warnf("转换CQ码 %v 时出现错误: %v 将原样发送.", org, err) + r = append(r, message.NewText(org)) + } else { + log.Warnf("转换CQ码 %v 时出现错误: %v 将忽略.", org, err) + } + return + } + switch i := elem.(type) { + case message.IMessageElement: + r = append(r, i) + case []message.IMessageElement: + r = append(r, i...) + } + } + for hasNext() { + ch := next() + switch stat { + case 0: + if isCQCodeBegin(ch) { + saveTempText() + tempText = append(tempText, []rune("[CQ:")...) + move(3) + stat = 1 + } else { + tempText = append(tempText, ch) + } + case 1: + if isCQCodeBegin(ch) { + move(-1) + stat = 0 + } else if ch == ']' { + saveCQCode() + stat = 0 + } else { + cqCode = append(cqCode, ch) + tempText = append(tempText, ch) + } + } + } + saveTempText() + return +} + +// ConvertObjectMessage 将消息JSON对象转为消息元素数组 +func (bot *CQBot) ConvertObjectMessage(m gjson.Result, isGroup bool) (r []message.IMessageElement) { + convertElem := func(e gjson.Result) { + t := e.Get("type").Str + if t == "reply" && isGroup { + if len(r) > 0 { + if _, ok := r[0].(*message.ReplyElement); ok { + log.Warnf("警告: 一条信息只能包含一个 Reply 元素.") + return + } + } + mid, err := strconv.Atoi(e.Get("data").Get("id").String()) + customText := e.Get("data").Get("text").String() + if err == nil { + org := bot.GetMessage(int32(mid)) + if org != nil { + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: org["message-id"].(int32), + Sender: org["sender"].(message.Sender).Uin, + Time: org["time"].(int32), + Elements: bot.ConvertStringMessage(org["message"].(string), isGroup), + }, + }, r...) + return + } + } else if customText != "" { + sender, err := strconv.ParseInt(e.Get("data").Get("qq").String(), 10, 64) + if err != nil { + log.Warnf("警告:自定义 Reply 元素中必须包含Uin") + return + } + msgTime, err := strconv.ParseInt(e.Get("data").Get("time").String(), 10, 64) + if err != nil { + msgTime = time.Now().Unix() + } + r = append([]message.IMessageElement{ + &message.ReplyElement{ + ReplySeq: int32(0), + Sender: sender, + Time: int32(msgTime), + Elements: bot.ConvertStringMessage(customText, isGroup), + }, + }, r...) + return + } + } + if t == "forward" { + r = []message.IMessageElement{bot.Client.DownloadForwardMessage(e.Get("data.id").String())} + return + } + d := make(map[string]string) + e.Get("data").ForEach(func(key, value gjson.Result) bool { + d[key.Str] = value.String() + return true + }) + elem, err := bot.ToElement(t, d, isGroup) + if err != nil { + log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err) + return + } + switch i := elem.(type) { + case message.IMessageElement: + r = append(r, i) + case []message.IMessageElement: + r = append(r, i...) + } + } + if m.Type == gjson.String { + return bot.ConvertStringMessage(m.Str, isGroup) + } + if m.IsArray() { + for _, e := range m.Array() { + convertElem(e) + } + } + if m.IsObject() { + convertElem(m) + } + return +} + +// ToElement 将解码后的CQCode转换为Element. +// +// 返回 interface{} 存在三种类型 +// +// message.IMessageElement []message.IMessageElement nil +func (bot *CQBot) ToElement(t string, d map[string]string, isGroup bool) (m interface{}, err error) { + switch t { + case "text": + if SplitURL { + var ret []message.IMessageElement + for _, text := range global.SplitURL(d["text"]) { + ret = append(ret, message.NewText(text)) + } + return ret, nil + } + return message.NewText(d["text"]), nil + case "image": + img, err := bot.makeImageOrVideoElem(d, false, isGroup) + if err != nil { + return nil, err + } + tp := d["type"] + if tp != "show" && tp != "flash" { + return img, nil + } + if i, ok := img.(*LocalImageElement); ok { // 秀图,闪照什么的就直接传了吧 + if isGroup { + img, err = bot.UploadLocalImageAsGroup(1, i) + } else { + img, err = bot.UploadLocalImageAsPrivate(1, i) + } + if err != nil { + return nil, err + } + } + switch tp { + case "flash": + if i, ok := img.(*message.GroupImageElement); ok { + return &message.GroupFlashPicElement{GroupImageElement: *i}, nil + } + if i, ok := img.(*message.FriendImageElement); ok { + return &message.FriendFlashPicElement{FriendImageElement: *i}, nil + } + case "show": + id, _ := strconv.ParseInt(d["id"], 10, 64) + if id < 40000 || id >= 40006 { + id = 40000 + } + if i, ok := img.(*message.GroupImageElement); ok { + return &message.GroupShowPicElement{GroupImageElement: *i, EffectId: int32(id)}, nil + } + return img, nil // 私聊还没做 + } + + case "poke": + t, _ := strconv.ParseInt(d["qq"], 10, 64) + return &PokeElement{Target: t}, nil + case "gift": + if !isGroup { + return nil, errors.New("private gift unsupported") // no free private gift + } + t, _ := strconv.ParseInt(d["qq"], 10, 64) + id, _ := strconv.Atoi(d["id"]) + if id < 0 || id >= 14 { + return nil, errors.New("invalid gift id") + } + return &GiftElement{Target: t, GiftID: GiftID[id]}, nil + case "tts": + defer func() { + if r := recover(); r != nil { + m = nil + err = errors.New("tts 转换失败") + } + }() + data, err := bot.Client.GetTts(d["text"]) + if err != nil { + return nil, err + } + return &message.VoiceElement{Data: data}, nil + case "record": + f := d["file"] + data, err := global.FindFile(f, d["cache"], global.VoicePath) + if err == global.ErrSyntax { + data, err = global.FindFile(f, d["cache"], global.VoicePathOld) + } + if err != nil { + return nil, err + } + if !global.IsAMRorSILK(data) { + data, err = global.EncoderSilk(data) + if err != nil { + return nil, err + } + } + return &message.VoiceElement{Data: data}, nil + case "face": + id, err := strconv.Atoi(d["id"]) + if err != nil { + return nil, err + } + return message.NewFace(int32(id)), nil + case "at": + qq := d["qq"] + if qq == "all" { + return message.AtAll(), nil + } + t, _ := strconv.ParseInt(qq, 10, 64) + return message.NewAt(t), nil + case "share": + return message.NewUrlShare(d["url"], d["title"], d["content"], d["image"]), nil + case "music": + if d["type"] == "qq" { + info, err := global.QQMusicSongInfo(d["id"]) + if err != nil { + return nil, err + } + if !info.Get("track_info").Exists() { + return nil, errors.New("song not found") + } + aid := strconv.FormatInt(info.Get("track_info.album.id").Int(), 10) + name := info.Get("track_info.name").Str + mid := info.Get("track_info.mid").Str + albumMid := info.Get("track_info.album.mid").Str + pinfo, _ := global.GetBytes("http://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + mid + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576") + jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + mid + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare" + purl := gjson.ParseBytes(pinfo).Get("url_mid.data.midurlinfo.0.purl").Str + preview := "http://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg" + if len(aid) < 2 { + return nil, errors.New("song error") + } + content := info.Get("track_info.singer.0.name").Str + if d["content"] != "" { + content = d["content"] + } + return &message.MusicShareElement{ + MusicType: message.QQMusic, + Title: name, + Summary: content, + Url: jumpURL, + PictureUrl: preview, + MusicUrl: purl, + }, nil + } + if d["type"] == "163" { + info, err := global.NeteaseMusicSongInfo(d["id"]) + if err != nil { + return nil, err + } + if !info.Exists() { + return nil, errors.New("song not found") + } + name := info.Get("name").Str + jumpURL := "https://y.music.163.com/m/song/" + d["id"] + musicURL := "http://music.163.com/song/media/outer/url?id=" + d["id"] + picURL := info.Get("album.picUrl").Str + artistName := "" + if info.Get("artists.0").Exists() { + artistName = info.Get("artists.0.name").Str + } + return &message.MusicShareElement{ + MusicType: message.CloudMusic, + Title: name, + Summary: artistName, + Url: jumpURL, + PictureUrl: picURL, + MusicUrl: musicURL, + }, nil + } + if d["type"] == "custom" { + if d["subtype"] != "" { + var subtype = map[string]int{ + "qq": message.QQMusic, + "163": message.CloudMusic, + "migu": message.MiguMusic, + "kugou": message.KugouMusic, + "kuwo": message.KuwoMusic, + } + var musicType = 0 + if tp, ok := subtype[d["subtype"]]; ok { + musicType = tp + } + return &message.MusicShareElement{ + MusicType: musicType, + Title: d["title"], + Summary: d["content"], + Url: d["url"], + PictureUrl: d["image"], + MusicUrl: d["purl"], + }, nil + } + xml := fmt.Sprintf(``, + XMLEscape(d["title"]), d["url"], d["image"], d["audio"], XMLEscape(d["title"]), XMLEscape(d["content"])) + return &message.ServiceElement{ + Id: 60, + Content: xml, + SubType: "music", + }, nil + } + return nil, errors.New("unsupported music type: " + d["type"]) + case "xml": + resID := d["resid"] + template := CQCodeEscapeValue(d["data"]) + i, _ := strconv.ParseInt(resID, 10, 64) + msg := message.NewRichXml(template, i) + return msg, nil + case "json": + resID := d["resid"] + i, _ := strconv.ParseInt(resID, 10, 64) + if i == 0 { + // 默认情况下走小程序通道 + msg := message.NewLightApp(CQCodeUnescapeValue(d["data"])) + return msg, nil + } + // resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO + msg := message.NewRichJson(CQCodeUnescapeValue(d["data"])) + return msg, nil + case "cardimage": + source := d["source"] + icon := d["icon"] + minWidth, _ := strconv.ParseInt(d["minwidth"], 10, 64) + if minWidth == 0 { + minWidth = 200 + } + minHeight, _ := strconv.ParseInt(d["minheight"], 10, 64) + if minHeight == 0 { + minHeight = 200 + } + maxWidth, _ := strconv.ParseInt(d["maxwidth"], 10, 64) + if maxWidth == 0 { + maxWidth = 500 + } + maxHeight, _ := strconv.ParseInt(d["maxheight"], 10, 64) + if maxHeight == 0 { + maxHeight = 1000 + } + img, err := bot.makeImageOrVideoElem(d, false, isGroup) + if err != nil { + return nil, errors.New("send cardimage faild") + } + return bot.makeShowPic(img, source, icon, minWidth, minHeight, maxWidth, maxHeight, isGroup) + case "video": + cache := d["cache"] + if cache == "" { + cache = "1" + } + file, err := bot.makeImageOrVideoElem(d, true, isGroup) + if err != nil { + return nil, err + } + v := file.(*LocalVideoElement) + if v.File == "" { + return v, nil + } + var data []byte + if cover, ok := d["cover"]; ok { + data, _ = global.FindFile(cover, cache, global.ImagePath) + } else { + _ = global.ExtractCover(v.File, v.File+".jpg") + data, _ = ioutil.ReadFile(v.File + ".jpg") + } + v.thumb = bytes.NewReader(data) + video, _ := os.Open(v.File) + defer video.Close() + _, err = video.Seek(4, io.SeekStart) + if err != nil { + return nil, err + } + var header = make([]byte, 4) + _, err = video.Read(header) + if err != nil { + return nil, err + } + if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp + _, _ = video.Seek(0, io.SeekStart) + hash, _ := utils.ComputeMd5AndLength(video) + cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".mp4") + if global.PathExists(cacheFile) && cache == "1" { + goto ok + } + err = global.EncodeMP4(v.File, cacheFile) + if err != nil { + return nil, err + } + ok: + v.File = cacheFile + } + return v, nil + default: + return nil, errors.New("unsupported cq code: " + t) + } + return nil, nil +} + +// XMLEscape 将字符串c转义为XML字符串 +func XMLEscape(c string) string { + buf := new(bytes.Buffer) + _ = xml2.EscapeText(buf, []byte(c)) + return buf.String() +} + +/*CQCodeEscapeText 将字符串raw中部分字符转义 + +& -> & + +[ -> [ + +] -> ] + +*/ +func CQCodeEscapeText(raw string) string { + ret := raw + ret = strings.ReplaceAll(ret, "&", "&") + ret = strings.ReplaceAll(ret, "[", "[") + ret = strings.ReplaceAll(ret, "]", "]") + return ret +} + +/*CQCodeEscapeValue 将字符串value中部分字符转义 + +, -> , + +*/ +func CQCodeEscapeValue(value string) string { + ret := CQCodeEscapeText(value) + ret = strings.ReplaceAll(ret, ",", ",") + return ret +} + +/*CQCodeUnescapeText 将字符串content中部分字符反转义 + +& -> & + +[ -> [ + +] -> ] + +*/ +func CQCodeUnescapeText(content string) string { + ret := content + ret = strings.ReplaceAll(ret, "[", "[") + ret = strings.ReplaceAll(ret, "]", "]") + ret = strings.ReplaceAll(ret, "&", "&") + return ret +} + +/*CQCodeUnescapeValue 将字符串content中部分字符反转义 + +, -> , + +*/ +func CQCodeUnescapeValue(content string) string { + ret := strings.ReplaceAll(content, ",", ",") + ret = CQCodeUnescapeText(ret) + return ret +} + +// makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用 +func (bot *CQBot) makeImageOrVideoElem(d map[string]string, video, group bool) (message.IMessageElement, error) { + f := d["file"] + if strings.HasPrefix(f, "http") || strings.HasPrefix(f, "https") { + cache := d["cache"] + c := d["c"] + if cache == "" { + cache = "1" + } + hash := md5.Sum([]byte(f)) + cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache") + var maxSize = func() int64 { + if video { + return maxVideoSize + } + return maxImageSize + }() + thread, _ := strconv.Atoi(c) + if global.PathExists(cacheFile) && cache == "1" { + goto hasCacheFile + } + if global.PathExists(cacheFile) { + _ = os.Remove(cacheFile) + } + if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil { + return nil, err + } + hasCacheFile: + if video { + return &LocalVideoElement{File: cacheFile}, nil + } + return &LocalImageElement{File: cacheFile}, nil + } + if strings.HasPrefix(f, "file") { + fu, err := url.Parse(f) + if err != nil { + return nil, err + } + if strings.HasPrefix(fu.Path, "/") && runtime.GOOS == `windows` { + fu.Path = fu.Path[1:] + } + info, err := os.Stat(fu.Path) + if err != nil { + if !os.IsExist(err) { + return nil, errors.New("file not found") + } + return nil, err + } + if video { + if info.Size() == 0 || info.Size() >= maxVideoSize { + return nil, errors.New("invalid video size") + } + return &LocalVideoElement{File: fu.Path}, nil + } + if info.Size() == 0 || info.Size() >= maxImageSize { + return nil, errors.New("invalid image size") + } + return &LocalImageElement{File: fu.Path}, nil + } + rawPath := path.Join(global.ImagePath, f) + if video { + rawPath = path.Join(global.VideoPath, f) + if !global.PathExists(rawPath) { + return nil, errors.New("invalid video") + } + if path.Ext(rawPath) == ".video" { + b, _ := ioutil.ReadFile(rawPath) + r := binary.NewReader(b) + return &LocalVideoElement{ShortVideoElement: message.ShortVideoElement{ // todo 检查缓存是否有效 + Md5: r.ReadBytes(16), + ThumbMd5: r.ReadBytes(16), + Size: r.ReadInt32(), + ThumbSize: r.ReadInt32(), + Name: r.ReadString(), + Uuid: r.ReadAvailable(), + }}, nil + } + return &LocalVideoElement{File: rawPath}, nil + } + if strings.HasPrefix(f, "base64") { + b, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(f, "base64://", "")) + if err != nil { + return nil, err + } + return &LocalImageElement{Stream: bytes.NewReader(b)}, nil + } + if !global.PathExists(rawPath) && global.PathExists(path.Join(global.ImagePathOld, f)) { + rawPath = path.Join(global.ImagePathOld, f) + } + if !global.PathExists(rawPath) && global.PathExists(rawPath+".cqimg") { + rawPath += ".cqimg" + } + if !global.PathExists(rawPath) && d["url"] != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, group) + } + if global.PathExists(rawPath) { + file, err := os.Open(rawPath) + if err != nil { + return nil, err + } + if path.Ext(rawPath) != ".image" && path.Ext(rawPath) != ".cqimg" { + return &LocalImageElement{Stream: file}, nil + } + b, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + if len(b) < 20 { + return nil, errors.New("invalid local file") + } + var ( + size int32 + hash []byte + url string + ) + if path.Ext(rawPath) == ".cqimg" { + for _, line := range strings.Split(global.ReadAllText(rawPath), "\n") { + kv := strings.SplitN(line, "=", 2) + switch kv[0] { + case "md5": + hash, _ = hex.DecodeString(strings.ReplaceAll(kv[1], "\r", "")) + case "size": + t, _ := strconv.Atoi(strings.ReplaceAll(kv[1], "\r", "")) + size = int32(t) + } + } + } else { + r := binary.NewReader(b) + hash = r.ReadBytes(16) + size = r.ReadInt32() + r.ReadString() + url = r.ReadString() + } + if size == 0 { + if url != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) + } + return nil, errors.New("img size is 0") + } + if len(hash) != 16 { + return nil, errors.New("invalid hash") + } + var rsp message.IMessageElement + if group { + rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size) + goto ok + } + rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size) + ok: + if err != nil { + if url != "" { + return bot.makeImageOrVideoElem(map[string]string{"file": url}, false, group) + } + return nil, err + } + return rsp, nil + } + return nil, errors.New("invalid image") +} + +// makeShowPic 一种xml 方式发送的群消息图片 +func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) { + xml := "" + var suf message.IMessageElement + if i, ok := elem.(*LocalImageElement); ok { + if !group { + gm, err := bot.UploadLocalImageAsPrivate(1, i) + if err != nil { + log.Warnf("警告: 好友消息 %v 消息图片上传失败: %v", 1, err) + return nil, err + } + suf = gm + xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + } else { + gm, err := bot.UploadLocalImageAsGroup(1, i) + if err != nil { + log.Warnf("警告: 群 %v 消息图片上传失败: %v", 1, err) + return nil, err + } + suf = gm + xml = fmt.Sprintf(``, "", gm.Md5, gm.Md5, len(i.Data), "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + } + } + + if i, ok := elem.(*message.GroupImageElement); ok { + xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + suf = i + } + if i, ok := elem.(*message.FriendImageElement); ok { + xml = fmt.Sprintf(``, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon) + suf = i + } + if xml != "" { + // log.Warn(xml) + ret := []message.IMessageElement{suf} + ret = append(ret, message.NewRichXml(xml, 5)) + return ret, nil + } + return nil, errors.New("生成xml图片消息失败") +}