package coolq
import (
"bytes"
"crypto/md5"
"encoding/hex"
xml2 "encoding/xml"
"errors"
"fmt"
"io"
"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"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/Mrs4s/go-cqhttp/db"
"github.com/Mrs4s/go-cqhttp/global"
"github.com/Mrs4s/go-cqhttp/internal/base"
"github.com/Mrs4s/go-cqhttp/internal/cache"
"github.com/Mrs4s/go-cqhttp/internal/param"
)
/*
var matchReg = regexp.MustCompile(`\[CQ:\w+?.*?]`)
var typeReg = regexp.MustCompile(`\[CQ:(\w+)`)
var paramReg = regexp.MustCompile(`,([\w\-.]+?)=([^,\]]+)`)
*/
// PokeElement 拍一拍
type PokeElement struct {
Target int64
}
// LocalImageElement 本地图片
type LocalImageElement struct {
Stream io.ReadSeeker
File string
Flash bool
EffectID int32
}
// LocalVoiceElement 本地语音
type LocalVoiceElement struct {
message.VoiceElement
Stream io.ReadSeeker
}
// LocalVideoElement 本地视频
type LocalVideoElement struct {
File string
thumb io.ReadSeeker
}
// MessageSource 消息来源
// 如果为私聊或者群聊, PrimaryID 将代表群号/QQ号
// 如果为频道, PrimaryID 为 GuildID, SubID 为 ChannelID
type MessageSource struct {
SourceType MessageSourceType
PrimaryID uint64
SubID uint64
}
// MessageSourceType 消息来源类型
type MessageSourceType int32
// MessageSourceType 常量
const (
MessageSourcePrivate MessageSourceType = 0
MessageSourceGroup MessageSourceType = 1
MessageSourceGuildChannel MessageSourceType = 2
)
const (
maxImageSize = 1024 * 1024 * 30 // 30MB
maxVideoSize = 1024 * 1024 * 100 // 100MB
)
// Type implements the message.IMessageElement.
func (e *LocalImageElement) Type() message.ElementType {
return message.Image
}
// Type impl message.IMessageElement
func (e *LocalVideoElement) Type() message.ElementType {
return message.Video
}
// Type 获取元素类型ID
func (e *PokeElement) Type() message.ElementType {
// Make message.IMessageElement Happy
return message.At
}
// ToArrayMessage 将消息元素数组转为MSG数组以用于消息上报
func ToArrayMessage(e []message.IMessageElement, source MessageSource) (r []global.MSG) {
r = make([]global.MSG, 0, len(e))
m := &message.SendingMessage{Elements: e}
reply := m.FirstOrNil(func(e message.IMessageElement) bool {
_, ok := e.(*message.ReplyElement)
return ok
})
if reply != nil && source.SourceType == MessageSourceGroup {
replyElem := reply.(*message.ReplyElement)
rid := int64(source.PrimaryID)
if rid == 0 {
rid = replyElem.Sender
}
if replyElem.GroupID != 0 {
rid = replyElem.GroupID
}
if base.ExtraReplyData {
r = append(r, global.MSG{
"type": "reply",
"data": map[string]string{
"id": strconv.FormatInt(int64(db.ToGlobalID(rid, replyElem.ReplySeq)), 10),
"seq": strconv.FormatInt(int64(replyElem.ReplySeq), 10),
"qq": strconv.FormatInt(replyElem.Sender, 10),
"time": strconv.FormatInt(int64(replyElem.Time), 10),
"text": ToStringMessage(replyElem.Elements, source),
},
})
} else {
r = append(r, global.MSG{
"type": "reply",
"data": map[string]string{"id": strconv.FormatInt(int64(db.ToGlobalID(rid, replyElem.ReplySeq)), 10)},
})
}
}
for i, elem := range e {
var m global.MSG
switch o := elem.(type) {
case *message.ReplyElement:
if base.RemoveReplyAt && i+1 < len(e) {
elem, ok := e[i+1].(*message.AtElement)
if ok && elem.Target == o.Sender {
e[i+1] = nil
}
}
case *message.TextElement:
m = global.MSG{
"type": "text",
"data": map[string]string{"text": o.Content},
}
case *message.LightAppElement:
m = global.MSG{
"type": "json",
"data": map[string]string{"data": o.Content},
}
case *message.AtElement:
if o.Target == 0 {
m = global.MSG{
"type": "at",
"data": map[string]string{"qq": "all"},
}
} else {
m = global.MSG{
"type": "at",
"data": map[string]string{"qq": strconv.FormatInt(o.Target, 10)},
}
}
case *message.RedBagElement:
m = global.MSG{
"type": "redbag",
"data": map[string]string{"title": o.Title},
}
case *message.ForwardElement:
m = global.MSG{
"type": "forward",
"data": map[string]string{"id": o.ResId},
}
case *message.FaceElement:
m = global.MSG{
"type": "face",
"data": map[string]string{"id": strconv.FormatInt(int64(o.Index), 10)},
}
case *message.VoiceElement:
m = global.MSG{
"type": "record",
"data": map[string]string{"file": o.Name, "url": o.Url},
}
case *message.ShortVideoElement:
m = global.MSG{
"type": "video",
"data": map[string]string{"file": o.Name, "url": o.Url},
}
case *message.GroupImageElement:
data := map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url, "subType": strconv.FormatInt(int64(o.ImageBizType), 10)}
switch {
case o.Flash:
data["type"] = "flash"
case o.EffectID != 0:
data["type"] = "show"
data["id"] = strconv.FormatInt(int64(o.EffectID), 10)
}
m = global.MSG{
"type": "image",
"data": data,
}
case *message.GuildImageElement:
data := map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url}
m = global.MSG{
"type": "image",
"data": data,
}
case *message.FriendImageElement:
data := map[string]string{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url}
if o.Flash {
data["type"] = "flash"
}
m = global.MSG{
"type": "image",
"data": data,
}
case *message.DiceElement:
m = global.MSG{
"type": "dice",
"data": map[string]string{"value": fmt.Sprint(o.Value)},
}
case *message.MarketFaceElement:
m = global.MSG{
"type": "text",
"data": map[string]string{"text": o.Name},
}
case *message.ServiceElement:
if isOk := strings.Contains(o.Content, " i+1 {
elem, ok := e[i+1].(*message.AtElement)
if ok && elem.Target == o.Sender {
e[i+1] = nil
}
}
case *message.TextElement:
sb.WriteString(CQCodeEscapeText(o.Content))
case *message.AtElement:
if o.Target == 0 {
write("[CQ:at,qq=all]")
continue
}
write("[CQ:at,qq=%d]", o.Target)
case *message.RedBagElement:
write("[CQ:redbag,title=%s]", o.Title)
case *message.ForwardElement:
write("[CQ:forward,id=%s]", o.ResId)
case *message.FaceElement:
write(`[CQ:face,id=%d]`, o.Index)
case *message.VoiceElement:
if ur {
write(`[CQ:record,file=%s]`, o.Name)
} else {
write(`[CQ:record,file=%s,url=%s]`, o.Name, CQCodeEscapeValue(o.Url))
}
case *message.ShortVideoElement:
if ur {
write(`[CQ:video,file=%s]`, o.Name)
} else {
write(`[CQ:video,file=%s,url=%s]`, o.Name, CQCodeEscapeValue(o.Url))
}
case *message.GroupImageElement:
var arg string
if o.Flash {
arg = ",type=flash"
} else if o.EffectID != 0 {
arg = ",type=show,id=" + strconv.FormatInt(int64(o.EffectID), 10)
}
arg += ",subType=" + strconv.FormatInt(int64(o.ImageBizType), 10)
if ur {
write("[CQ:image,file=%s%s]", hex.EncodeToString(o.Md5)+".image", arg)
} else {
write("[CQ:image,file=%s,url=%s%s]", hex.EncodeToString(o.Md5)+".image", CQCodeEscapeValue(o.Url), arg)
}
case *message.FriendImageElement:
var arg string
if o.Flash {
arg = ",type=flash"
}
if ur {
write("[CQ:image,file=%s%s]", hex.EncodeToString(o.Md5)+".image", arg)
} else {
write("[CQ:image,file=%s,url=%s%s]", hex.EncodeToString(o.Md5)+".image", CQCodeEscapeValue(o.Url), arg)
}
case *message.GuildImageElement:
write("[CQ:image,file=%s,url=%s]", hex.EncodeToString(o.Md5)+".image", CQCodeEscapeValue(o.Url))
case *message.DiceElement:
write("[CQ:dice,value=%v]", o.Value)
case *message.MarketFaceElement:
sb.WriteString(o.Name)
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(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(CQCodeUnescapeText(raw[:i])) {
r = append(r, message.NewText(txt))
}
} else {
r = append(r, message.NewText(CQCodeUnescapeText(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] = CQCodeUnescapeValue(raw[:i])
raw = raw[i:]
i = 0
}
}
return
}
// ConvertObjectMessage 将消息JSON对象转为消息元素数组
func (bot *CQBot) ConvertObjectMessage(m gjson.Result, sourceType MessageSourceType) (r []message.IMessageElement) {
d := make(map[string]string)
convertElem := func(e gjson.Result) {
t := e.Get("type").Str
if t == "reply" && sourceType == MessageSourceGroup {
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 t == "forward" {
id := e.Get("data.id").String()
if id == "" {
log.Warnf("警告: Forward 元素中必须包含 id")
} else {
if fwdMsg := bot.Client.DownloadForwardMessage(id); fwdMsg == nil {
log.Warnf("警告: Forward 信息不存在或已过期")
} else {
r = []message.IMessageElement{fwdMsg}
}
}
return
}
for i := range d {
delete(d, i)
}
e.Get("data").ForEach(func(key, value gjson.Result) bool {
d[key.Str] = value.String()
return true
})
elem, err := bot.ToElement(t, d, sourceType)
if err != nil {
log.Warnf("转换CQ码 (%v) 到MiraiGo Element时出现错误: %v 将忽略本段CQ码.", e.Raw, err)
return
}
switch i := elem.(type) {
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 MessageSourceType) (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":
e, err := bot.makeImageOrVideoElem(map[string]string{"file": data["file"].(string)}, 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 = data["id"].(int32)
if id < 40000 || id >= 40006 {
id = 40000
}
}
}
switch img := e.(type) {
case *LocalImageElement:
img.Flash = flash
img.EffectID = id
case *message.GroupImageElement:
img.Flash = flash
img.EffectID = id
img.ImageBizType = message.ImageBizType(data["subType"].(uint32))
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(data["target"].(int64), 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":
r = append(r, message.NewFace(data["id"].(int32)))
case "video":
e, err := bot.makeImageOrVideoElem(map[string]string{"file": data["file"].(string)}, true, sourceType)
if err != nil {
log.Warnf("make image elem error: %v", err)
continue
}
r = append(r, e)
}
}
return
}
// ToElement 将解码后的CQCode转换为Element.
//
// 返回 interface{} 存在三种类型
//
// message.IMessageElement []message.IMessageElement nil
func (bot *CQBot) ToElement(t string, d map[string]string, sourceType MessageSourceType) (m interface{}, err error) {
switch t {
case "text":
if base.SplitURL {
var ret []message.IMessageElement
for _, text := range param.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, sourceType)
if err != nil {
return nil, err
}
tp := d["type"]
flash, id := false, int64(0)
switch tp {
case "flash":
flash = true
case "show":
id, _ = strconv.ParseInt(d["id"], 10, 64)
if id < 40000 || id >= 40006 {
id = 40000
}
default:
return img, err
}
switch img := img.(type) {
case *LocalImageElement:
img.Flash = flash
img.EffectID = int32(id)
case *message.GroupImageElement:
img.Flash = flash
img.EffectID = int32(id)
i, _ := strconv.ParseInt(d["subType"], 10, 64)
img.ImageBizType = message.ImageBizType(i)
case *message.FriendImageElement:
img.Flash = flash
}
return img, err
case "poke":
t, _ := strconv.ParseInt(d["qq"], 10, 64)
return &PokeElement{Target: t}, 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: base.ResampleSilk(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) {
lawful, mt := base.IsLawfulAudio(bytes.NewReader(data))
if !lawful {
return nil, errors.New("audio type error: " + mt)
}
}
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 "mention":
if !base.AcceptOneBot12Message {
return nil, errors.New("unsupported onebot 12 style")
}
fallthrough
case "at":
qq := d["qq"]
if base.AcceptOneBot12Message && qq == "" {
qq = d["user_id"]
}
if qq == "all" {
return message.AtAll(), nil
}
t, _ := strconv.ParseInt(qq, 10, 64)
name := strings.TrimSpace(d["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
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")
}
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"
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 int
switch d["subtype"] {
default:
subType = message.QQMusic
case "163":
subType = message.CloudMusic
case "migu":
subType = message.MiguMusic
case "kugou":
subType = message.KugouMusic
case "kuwo":
subType = message.KuwoMusic
}
return &message.MusicShareElement{
MusicType: subType,
Title: d["title"],
Summary: d["content"],
Url: d["url"],
PictureUrl: d["image"],
MusicUrl: d["audio"],
}, nil
}
xml := fmt.Sprintf(`- %s%s
`,
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 "dice":
value := d["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 "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(d["data"])
return msg, nil
}
// resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO
msg := message.NewRichJson(d["data"])
return msg, nil
case "cardimage":
source := d["source"]
icon := d["icon"]
brief := d["brief"]
parseIntWithDefault := func(name string, origin int64) int64 {
v, _ := strconv.ParseInt(d[name], 10, 64)
if v <= 0 {
return origin
}
return v
}
minWidth := parseIntWithDefault("minwidth", 200)
maxWidth := parseIntWithDefault("maxwidth", 500)
minHeight := parseIntWithDefault("minheight", 200)
maxHeight := parseIntWithDefault("maxheight", 1000)
img, err := bot.makeImageOrVideoElem(d, false, sourceType)
if err != nil {
return nil, errors.New("send cardimage faild")
}
return bot.makeShowPic(img, source, brief, icon, minWidth, minHeight, maxWidth, maxHeight, sourceType == MessageSourceGroup)
case "video":
file, err := bot.makeImageOrVideoElem(d, true, sourceType)
if err != nil {
return nil, err
}
v, ok := file.(*LocalVideoElement)
if !ok {
return file, nil
}
if v.File == "" {
return v, nil
}
var data []byte
if cover, ok := d["cover"]; ok {
data, _ = global.FindFile(cover, d["cache"], global.ImagePath)
} else {
_ = global.ExtractCover(v.File, v.File+".jpg")
data, _ = os.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
}
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) && (d["cache"] == "" || d["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)
}
}
// XMLEscape 将字符串c转义为XML字符串
func XMLEscape(c string) string {
buf := global.NewBuffer()
defer global.PutBuffer(buf)
_ = xml2.EscapeText(buf, utils.S2B(c))
return buf.String()
}
/*CQCodeEscapeText 将字符串raw中部分字符转义
& -> &
[ -> [
] -> ]
*/
func CQCodeEscapeText(s string) string {
count := strings.Count(s, "&")
count += strings.Count(s, "[")
count += strings.Count(s, "]")
if count == 0 {
return s
}
// Apply replacements to buffer.
var b strings.Builder
b.Grow(len(s) + count*4)
start := 0
for i := 0; i < count; i++ {
j := start
for index, r := range s[start:] {
if r == '&' || r == '[' || r == ']' {
j += index
break
}
}
b.WriteString(s[start:j])
switch s[j] {
case '&':
b.WriteString("&")
case '[':
b.WriteString("[")
case ']':
b.WriteString("]")
}
start = j + 1
}
b.WriteString(s[start:])
return b.String()
}
/*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 bool, sourceType MessageSourceType) (message.IMessageElement, error) {
f := d["file"]
if strings.HasPrefix(f, "http") {
hash := md5.Sum([]byte(f))
cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
maxSize := int64(maxImageSize)
if video {
maxSize = maxVideoSize
}
thread, _ := strconv.Atoi(d["c"])
exist := global.PathExists(cacheFile)
if exist && (d["cache"] == "" || d["cache"] == "1") {
goto useCacheFile
}
if exist {
_ = os.Remove(cacheFile)
}
if err := global.DownloadFileMultiThreading(f, cacheFile, maxSize, thread, nil); err != nil {
return nil, err
}
useCacheFile:
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 runtime.GOOS == `windows` && strings.HasPrefix(fu.Path, "/") {
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
}
if strings.HasPrefix(f, "base64") && !video {
b, err := param.Base64DecodeString(strings.TrimPrefix(f, "base64://"))
if err != nil {
return nil, err
}
return &LocalImageElement{Stream: bytes.NewReader(b)}, nil
}
rawPath := path.Join(global.ImagePath, f)
if video {
if strings.HasSuffix(f, ".video") && cache.EnableCacheDB {
hash, err := hex.DecodeString(strings.TrimSuffix(f, ".video"))
if err == nil {
if b := cache.Video.Get(hash); b != nil {
return bot.readVideoCache(b), nil
}
}
}
rawPath = path.Join(global.VideoPath, f)
if !global.PathExists(rawPath) {
return nil, errors.New("invalid video")
}
if path.Ext(rawPath) != ".video" {
return &LocalVideoElement{File: rawPath}, nil
}
b, _ := os.ReadFile(rawPath)
return bot.readVideoCache(b), nil
}
// 目前频道内上传的图片均无法被查询到, 需要单独处理
if sourceType == MessageSourceGuildChannel {
cacheFile := path.Join(global.ImagePath, "guild-images", f)
if global.PathExists(cacheFile) {
return &LocalImageElement{File: cacheFile}, nil
}
}
if strings.HasSuffix(f, ".image") && cache.EnableCacheDB {
hash, err := hex.DecodeString(strings.TrimSuffix(f, ".image"))
if err == nil {
if b := cache.Image.Get(hash); b != nil {
return bot.readImageCache(b, sourceType)
}
}
}
exist := global.PathExists(rawPath)
if !exist && global.PathExists(path.Join(global.ImagePathOld, f)) {
exist = true
rawPath = path.Join(global.ImagePathOld, f)
}
if !exist {
if d["url"] != "" {
return bot.makeImageOrVideoElem(map[string]string{"file": d["url"]}, false, sourceType)
}
return nil, errors.New("invalid image")
}
if path.Ext(rawPath) != ".image" {
return &LocalImageElement{File: rawPath}, nil
}
b, err := os.ReadFile(rawPath)
if err != nil {
return nil, err
}
return bot.readImageCache(b, sourceType)
}
func (bot *CQBot) readImageCache(b []byte, sourceType MessageSourceType) (message.IMessageElement, error) {
var err error
if len(b) < 20 {
return nil, errors.New("invalid cache")
}
r := binary.NewReader(b)
hash := r.ReadBytes(16)
size := r.ReadInt32()
r.ReadString()
imageURL := r.ReadString()
if size == 0 {
if imageURL != "" {
return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, sourceType)
}
return nil, errors.New("img size is 0")
}
if len(hash) != 16 {
return nil, errors.New("invalid hash")
}
var rsp message.IMessageElement
if sourceType == MessageSourceGroup {
rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size)
goto ok
}
if sourceType == MessageSourceGuildChannel {
if len(bot.Client.GuildService.Guilds) == 0 {
err = errors.New("cannot query guild image: not any joined guild")
goto ok
}
guild := bot.Client.GuildService.Guilds[0]
rsp, err = bot.Client.GuildService.QueryImage(guild.GuildId, guild.Channels[0].ChannelId, hash, uint64(size))
goto ok
}
rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size)
ok:
if err != nil {
if imageURL != "" {
return bot.makeImageOrVideoElem(map[string]string{"file": imageURL}, false, sourceType)
}
return nil, err
}
return rsp, nil
}
func (bot *CQBot) readVideoCache(b []byte) message.IMessageElement {
r := binary.NewReader(b)
return &message.ShortVideoElement{ // todo 检查缓存是否有效
Md5: r.ReadBytes(16),
ThumbMd5: r.ReadBytes(16),
Size: r.ReadInt32(),
ThumbSize: r.ReadInt32(),
Name: r.ReadString(),
Uuid: r.ReadAvailable(),
}
}
// makeShowPic 一种xml 方式发送的群消息图片
func (bot *CQBot) makeShowPic(elem message.IMessageElement, source string, brief string, icon string, minWidth int64, minHeight int64, maxWidth int64, maxHeight int64, group bool) ([]message.IMessageElement, error) {
xml := ""
var suf message.IMessageElement
if brief == "" {
brief = "[分享]我看到一张很赞的图片,分享给你,快来看!"
}
if _, ok := elem.(*LocalImageElement); ok {
r := rand.Uint32()
e, err := bot.uploadMedia(elem, int64(r), group)
if err != nil {
log.Warnf("警告: 图片上传失败: %v", err)
return nil, err
}
elem = e
}
switch i := elem.(type) {
case *message.GroupImageElement:
xml = fmt.Sprintf(` `, brief, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
suf = i
case *message.FriendImageElement:
xml = fmt.Sprintf(` `, brief, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
suf = i
}
if xml == "" {
return nil, errors.New("生成xml图片消息失败")
}
ret := []message.IMessageElement{suf, message.NewRichXml(xml, 5)}
return ret, nil
}