github.com/Mrs4s/go-cqhttp@v1.2.0/coolq/cqcode.go (about)

     1  package coolq
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"math/rand"
    11  	"net/url"
    12  	"os"
    13  	"path"
    14  	"runtime"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/Mrs4s/MiraiGo/binary"
    20  	"github.com/Mrs4s/MiraiGo/message"
    21  	"github.com/Mrs4s/MiraiGo/utils"
    22  	b14 "github.com/fumiama/go-base16384"
    23  	"github.com/segmentio/asm/base64"
    24  	log "github.com/sirupsen/logrus"
    25  	"github.com/tidwall/gjson"
    26  
    27  	"github.com/Mrs4s/go-cqhttp/db"
    28  	"github.com/Mrs4s/go-cqhttp/global"
    29  	"github.com/Mrs4s/go-cqhttp/internal/base"
    30  	"github.com/Mrs4s/go-cqhttp/internal/cache"
    31  	"github.com/Mrs4s/go-cqhttp/internal/download"
    32  	"github.com/Mrs4s/go-cqhttp/internal/mime"
    33  	"github.com/Mrs4s/go-cqhttp/internal/msg"
    34  	"github.com/Mrs4s/go-cqhttp/internal/param"
    35  	"github.com/Mrs4s/go-cqhttp/pkg/onebot"
    36  )
    37  
    38  // TODO: move this file to internal/msg, internal/onebot
    39  // TODO: support OneBot V12
    40  
    41  const (
    42  	maxImageSize = 1024 * 1024 * 30  // 30MB
    43  	maxVideoSize = 1024 * 1024 * 100 // 100MB
    44  )
    45  
    46  func replyID(r *message.ReplyElement, source message.Source) int32 {
    47  	id := source.PrimaryID
    48  	seq := r.ReplySeq
    49  	if r.GroupID != 0 {
    50  		id = r.GroupID
    51  	}
    52  	// 私聊时,部分(不确定)的账号会在 ReplyElement 中带有 GroupID 字段。
    53  	// 这里需要判断是由于 “直接回复” 功能,GroupID 为触发直接回复的来源那个群。
    54  	if source.SourceType == message.SourcePrivate && (r.Sender == source.PrimaryID || r.GroupID == source.PrimaryID || r.GroupID == 0) {
    55  		// 私聊似乎腾讯服务器有bug?
    56  		seq = int32(uint16(seq))
    57  		id = r.Sender
    58  	}
    59  	return db.ToGlobalID(id, seq)
    60  }
    61  
    62  // toElements 将消息元素数组转为MSG数组以用于消息上报
    63  //
    64  // nolint:govet
    65  func toElements(e []message.IMessageElement, source message.Source) (r []msg.Element) {
    66  	// TODO: support OneBot V12
    67  	type pair = msg.Pair // simplify code
    68  	type pairs = []pair
    69  
    70  	r = make([]msg.Element, 0, len(e))
    71  	m := &message.SendingMessage{Elements: e}
    72  	reply := m.FirstOrNil(func(e message.IMessageElement) bool {
    73  		_, ok := e.(*message.ReplyElement)
    74  		return ok
    75  	})
    76  	if reply != nil && source.SourceType&(message.SourceGroup|message.SourcePrivate) != 0 {
    77  		replyElem := reply.(*message.ReplyElement)
    78  		id := replyID(replyElem, source)
    79  		elem := msg.Element{
    80  			Type: "reply",
    81  			Data: pairs{
    82  				{K: "id", V: strconv.FormatInt(int64(id), 10)},
    83  			},
    84  		}
    85  		if base.ExtraReplyData {
    86  			elem.Data = append(elem.Data,
    87  				pair{K: "seq", V: strconv.FormatInt(int64(replyElem.ReplySeq), 10)},
    88  				pair{K: "qq", V: strconv.FormatInt(replyElem.Sender, 10)},
    89  				pair{K: "time", V: strconv.FormatInt(int64(replyElem.Time), 10)},
    90  				pair{K: "text", V: toStringMessage(replyElem.Elements, source)},
    91  			)
    92  		}
    93  		r = append(r, elem)
    94  	}
    95  	for i, elem := range e {
    96  		var m msg.Element
    97  		switch o := elem.(type) {
    98  		case *message.ReplyElement:
    99  			if base.RemoveReplyAt && i+1 < len(e) {
   100  				elem, ok := e[i+1].(*message.AtElement)
   101  				if ok && elem.Target == o.Sender {
   102  					e[i+1] = nil
   103  				}
   104  			}
   105  			continue
   106  		case *message.TextElement:
   107  			m = msg.Element{
   108  				Type: "text",
   109  				Data: pairs{
   110  					{K: "text", V: o.Content},
   111  				},
   112  			}
   113  		case *message.LightAppElement:
   114  			m = msg.Element{
   115  				Type: "json",
   116  				Data: pairs{
   117  					{K: "data", V: o.Content},
   118  				},
   119  			}
   120  		case *message.AtElement:
   121  			target := "all"
   122  			if o.Target != 0 {
   123  				target = strconv.FormatUint(uint64(o.Target), 10)
   124  			}
   125  			m = msg.Element{
   126  				Type: "at",
   127  				Data: pairs{
   128  					{K: "qq", V: target},
   129  				},
   130  			}
   131  		case *message.RedBagElement:
   132  			m = msg.Element{
   133  				Type: "redbag",
   134  				Data: pairs{
   135  					{K: "title", V: o.Title},
   136  				},
   137  			}
   138  		case *message.ForwardElement:
   139  			m = msg.Element{
   140  				Type: "forward",
   141  				Data: pairs{
   142  					{K: "id", V: o.ResId},
   143  				},
   144  			}
   145  		case *message.FaceElement:
   146  			m = msg.Element{
   147  				Type: "face",
   148  				Data: pairs{
   149  					{K: "id", V: strconv.FormatInt(int64(o.Index), 10)},
   150  				},
   151  			}
   152  		case *message.VoiceElement:
   153  			m = msg.Element{
   154  				Type: "record",
   155  				Data: pairs{
   156  					{K: "file", V: o.Name},
   157  					{K: "url", V: o.Url},
   158  				},
   159  			}
   160  		case *message.ShortVideoElement:
   161  			m = msg.Element{
   162  				Type: "video",
   163  				Data: pairs{
   164  					{K: "file", V: o.Name},
   165  					{K: "url", V: o.Url},
   166  				},
   167  			}
   168  		case *message.GroupImageElement:
   169  			data := pairs{
   170  				{K: "file", V: hex.EncodeToString(o.Md5) + ".image"},
   171  				{K: "subType", V: strconv.FormatInt(int64(o.ImageBizType), 10)},
   172  				{K: "url", V: o.Url},
   173  			}
   174  			switch {
   175  			case o.Flash:
   176  				data = append(data, pair{K: "type", V: "flash"})
   177  			case o.EffectID != 0:
   178  				data = append(data, pair{K: "type", V: "show"})
   179  				data = append(data, pair{K: "id", V: strconv.FormatInt(int64(o.EffectID), 10)})
   180  			}
   181  			m = msg.Element{
   182  				Type: "image",
   183  				Data: data,
   184  			}
   185  		case *message.GuildImageElement:
   186  			data := pairs{
   187  				{K: "file", V: hex.EncodeToString(o.Md5) + ".image"},
   188  				{K: "url", V: o.Url},
   189  			}
   190  			m = msg.Element{
   191  				Type: "image",
   192  				Data: data,
   193  			}
   194  		case *message.FriendImageElement:
   195  			data := pairs{
   196  				{K: "file", V: hex.EncodeToString(o.Md5) + ".image"},
   197  				{K: "url", V: o.Url},
   198  			}
   199  			if o.Flash {
   200  				data = append(data, pair{K: "type", V: "flash"})
   201  			}
   202  			m = msg.Element{
   203  				Type: "image",
   204  				Data: data,
   205  			}
   206  		case *message.DiceElement:
   207  			m = msg.Element{
   208  				Type: "dice",
   209  				Data: pairs{
   210  					{K: "value", V: strconv.FormatInt(int64(o.Value), 10)},
   211  				},
   212  			}
   213  		case *message.FingerGuessingElement:
   214  			m = msg.Element{
   215  				Type: "rps",
   216  				Data: pairs{
   217  					{K: "value", V: strconv.FormatInt(int64(o.Value), 10)},
   218  				},
   219  			}
   220  		case *message.MarketFaceElement:
   221  			m = msg.Element{
   222  				Type: "text",
   223  				Data: pairs{
   224  					{K: "text", V: o.Name},
   225  				},
   226  			}
   227  		case *message.ServiceElement:
   228  			m = msg.Element{
   229  				Type: "xml",
   230  				Data: pairs{
   231  					{K: "data", V: o.Content},
   232  					{K: "resid", V: o.ResId},
   233  				},
   234  			}
   235  			if !strings.Contains(o.Content, "<?xml") {
   236  				m.Type = "json"
   237  			}
   238  		case *message.AnimatedSticker:
   239  			m = msg.Element{
   240  				Type: "face",
   241  				Data: pairs{
   242  					{K: "id", V: strconv.FormatInt(int64(o.ID), 10)},
   243  					{K: "type", V: "sticker"},
   244  				},
   245  			}
   246  		case *message.GroupFileElement:
   247  			m = msg.Element{
   248  				Type: "file",
   249  				Data: pairs{
   250  					{K: "path", V: o.Path},
   251  					{K: "name", V: o.Name},
   252  					{K: "size", V: strconv.FormatInt(o.Size, 10)},
   253  					{K: "busid", V: strconv.FormatInt(int64(o.Busid), 10)},
   254  				},
   255  			}
   256  		case *msg.LocalImage:
   257  			data := pairs{
   258  				{K: "file", V: o.File},
   259  				{K: "url", V: o.URL},
   260  			}
   261  			if o.Flash {
   262  				data = append(data, pair{K: "type", V: "flash"})
   263  			}
   264  			m = msg.Element{
   265  				Type: "image",
   266  				Data: data,
   267  			}
   268  		default:
   269  			continue
   270  		}
   271  		r = append(r, m)
   272  	}
   273  	return
   274  }
   275  
   276  // ToMessageContent 将消息转换成 Content. 忽略 Reply
   277  // 不同于 onebot 的 Array Message, 此函数转换出来的 Content 的 data 段为实际类型
   278  // 方便数据库查询
   279  func ToMessageContent(e []message.IMessageElement, source message.Source) (r []global.MSG) {
   280  	for _, elem := range e {
   281  		var m global.MSG
   282  		switch o := elem.(type) {
   283  		case *message.ReplyElement:
   284  			m = global.MSG{
   285  				"type": "reply",
   286  				"data": global.MSG{"id": replyID(o, source)},
   287  			}
   288  		case *message.TextElement:
   289  			m = global.MSG{
   290  				"type": "text",
   291  				"data": global.MSG{"text": o.Content},
   292  			}
   293  		case *message.LightAppElement:
   294  			m = global.MSG{
   295  				"type": "json",
   296  				"data": global.MSG{"data": o.Content},
   297  			}
   298  		case *message.AtElement:
   299  			if o.Target == 0 {
   300  				m = global.MSG{
   301  					"type": "at",
   302  					"data": global.MSG{
   303  						"subType": "all",
   304  					},
   305  				}
   306  			} else {
   307  				m = global.MSG{
   308  					"type": "at",
   309  					"data": global.MSG{
   310  						"subType": "user",
   311  						"target":  o.Target,
   312  						"display": o.Display,
   313  					},
   314  				}
   315  			}
   316  		case *message.RedBagElement:
   317  			m = global.MSG{
   318  				"type": "redbag",
   319  				"data": global.MSG{"title": o.Title, "type": int(o.MsgType)},
   320  			}
   321  		case *message.ForwardElement:
   322  			m = global.MSG{
   323  				"type": "forward",
   324  				"data": global.MSG{"id": o.ResId},
   325  			}
   326  		case *message.FaceElement:
   327  			m = global.MSG{
   328  				"type": "face",
   329  				"data": global.MSG{"id": o.Index},
   330  			}
   331  		case *message.VoiceElement:
   332  			m = global.MSG{
   333  				"type": "record",
   334  				"data": global.MSG{"file": o.Name, "url": o.Url},
   335  			}
   336  		case *message.ShortVideoElement:
   337  			m = global.MSG{
   338  				"type": "video",
   339  				"data": global.MSG{"file": o.Name, "url": o.Url},
   340  			}
   341  		case *message.GroupImageElement:
   342  			data := global.MSG{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url, "subType": uint32(o.ImageBizType)}
   343  			switch {
   344  			case o.Flash:
   345  				data["type"] = "flash"
   346  			case o.EffectID != 0:
   347  				data["type"] = "show"
   348  				data["id"] = o.EffectID
   349  			}
   350  			m = global.MSG{
   351  				"type": "image",
   352  				"data": data,
   353  			}
   354  		case *message.GuildImageElement:
   355  			m = global.MSG{
   356  				"type": "image",
   357  				"data": global.MSG{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url},
   358  			}
   359  		case *message.FriendImageElement:
   360  			data := global.MSG{"file": hex.EncodeToString(o.Md5) + ".image", "url": o.Url}
   361  			if o.Flash {
   362  				data["type"] = "flash"
   363  			}
   364  			m = global.MSG{
   365  				"type": "image",
   366  				"data": data,
   367  			}
   368  		case *message.DiceElement:
   369  			m = global.MSG{"type": "dice", "data": global.MSG{"value": o.Value}}
   370  		case *message.FingerGuessingElement:
   371  			m = global.MSG{"type": "rps", "data": global.MSG{"value": o.Value}}
   372  		case *message.MarketFaceElement:
   373  			m = global.MSG{"type": "text", "data": global.MSG{"text": o.Name}}
   374  		case *message.ServiceElement:
   375  			if isOk := strings.Contains(o.Content, "<?xml"); isOk {
   376  				m = global.MSG{
   377  					"type": "xml",
   378  					"data": global.MSG{"data": o.Content, "resid": o.Id},
   379  				}
   380  			} else {
   381  				m = global.MSG{
   382  					"type": "json",
   383  					"data": global.MSG{"data": o.Content, "resid": o.Id},
   384  				}
   385  			}
   386  		case *message.AnimatedSticker:
   387  			m = global.MSG{
   388  				"type": "face",
   389  				"data": global.MSG{"id": o.ID, "type": "sticker"},
   390  			}
   391  		case *message.GroupFileElement:
   392  			m = global.MSG{
   393  				"type": "file",
   394  				"data": global.MSG{"path": o.Path, "name": o.Name, "size": strconv.FormatInt(o.Size, 10), "busid": strconv.FormatInt(int64(o.Busid), 10)},
   395  			}
   396  		default:
   397  			continue
   398  		}
   399  		r = append(r, m)
   400  	}
   401  	return
   402  }
   403  
   404  // ConvertStringMessage 将消息字符串转为消息元素数组
   405  func (bot *CQBot) ConvertStringMessage(spec *onebot.Spec, raw string, sourceType message.SourceType) (r []message.IMessageElement) {
   406  	elems := msg.ParseString(raw)
   407  	return bot.ConvertElements(spec, elems, sourceType, true)
   408  }
   409  
   410  // ConvertObjectMessage 将消息JSON对象转为消息元素数组
   411  func (bot *CQBot) ConvertObjectMessage(spec *onebot.Spec, m gjson.Result, sourceType message.SourceType) (r []message.IMessageElement) {
   412  	if spec.Version == 11 && m.Type == gjson.String {
   413  		return bot.ConvertStringMessage(spec, m.Str, sourceType)
   414  	}
   415  	elems := msg.ParseObject(m)
   416  	return bot.ConvertElements(spec, elems, sourceType, false)
   417  }
   418  
   419  // ConvertContentMessage 将数据库用的 content 转换为消息元素数组
   420  func (bot *CQBot) ConvertContentMessage(content []global.MSG, sourceType message.SourceType, noReply bool) (r []message.IMessageElement) {
   421  	elems := make([]msg.Element, len(content))
   422  	for i, v := range content {
   423  		elem := msg.Element{Type: v["type"].(string)}
   424  		for k, v := range v["data"].(global.MSG) {
   425  			pair := msg.Pair{K: k, V: fmt.Sprint(v)}
   426  			elem.Data = append(elem.Data, pair)
   427  		}
   428  		elems[i] = elem
   429  	}
   430  	return bot.ConvertElements(onebot.V11, elems, sourceType, noReply)
   431  }
   432  
   433  // ConvertElements 将解码后的消息数组转换为MiraiGo表示
   434  func (bot *CQBot) ConvertElements(spec *onebot.Spec, elems []msg.Element, sourceType message.SourceType, noReply bool) (r []message.IMessageElement) {
   435  	var replyCount int
   436  	for _, elem := range elems {
   437  		if noReply && elem.Type == "reply" {
   438  			continue
   439  		}
   440  		me, err := bot.ConvertElement(spec, elem, sourceType)
   441  		if err != nil {
   442  			// TODO: don't use cqcode format
   443  			if !base.IgnoreInvalidCQCode {
   444  				r = append(r, message.NewText(elem.CQCode()))
   445  			}
   446  			log.Warnf("转换消息 %v 到MiraiGo Element时出现错误: %v.", elem.CQCode(), err)
   447  			continue
   448  		}
   449  		switch i := me.(type) {
   450  		case *message.ReplyElement:
   451  			if replyCount > 0 {
   452  				log.Warnf("警告: 一条信息只能包含一个 Reply 元素.")
   453  				break
   454  			}
   455  			replyCount++
   456  			// 将回复消息放置于第一个
   457  			r = append([]message.IMessageElement{i}, r...)
   458  		case message.IMessageElement:
   459  			r = append(r, i)
   460  		case []message.IMessageElement:
   461  			r = append(r, i...)
   462  		}
   463  	}
   464  	return
   465  }
   466  
   467  func (bot *CQBot) reply(spec *onebot.Spec, elem msg.Element, sourceType message.SourceType) (any, error) {
   468  	mid, err := strconv.Atoi(elem.Get("id"))
   469  	customText := elem.Get("text")
   470  	var re *message.ReplyElement
   471  	switch {
   472  	case customText != "":
   473  		var org db.StoredMessage
   474  		sender, senderErr := strconv.ParseInt(elem.Get("user_id"), 10, 64)
   475  		if senderErr != nil {
   476  			sender, senderErr = strconv.ParseInt(elem.Get("qq"), 10, 64)
   477  		}
   478  		if senderErr != nil && err != nil {
   479  			return nil, errors.New("警告: 自定义 reply 元素中必须包含 user_id 或 id")
   480  		}
   481  		msgTime, timeErr := strconv.ParseInt(elem.Get("time"), 10, 64)
   482  		if timeErr != nil {
   483  			msgTime = time.Now().Unix()
   484  		}
   485  		messageSeq, seqErr := strconv.ParseInt(elem.Get("seq"), 10, 64)
   486  		if err == nil {
   487  			org, _ = db.GetMessageByGlobalID(int32(mid))
   488  		}
   489  		if org != nil {
   490  			re = &message.ReplyElement{
   491  				ReplySeq: org.GetAttribute().MessageSeq,
   492  				Sender:   org.GetAttribute().SenderUin,
   493  				Time:     int32(org.GetAttribute().Timestamp),
   494  				Elements: bot.ConvertStringMessage(spec, customText, sourceType),
   495  			}
   496  			if senderErr != nil {
   497  				re.Sender = sender
   498  			}
   499  			if timeErr != nil {
   500  				re.Time = int32(msgTime)
   501  			}
   502  			if seqErr != nil {
   503  				re.ReplySeq = int32(messageSeq)
   504  			}
   505  			break
   506  		}
   507  		re = &message.ReplyElement{
   508  			ReplySeq: int32(messageSeq),
   509  			Sender:   sender,
   510  			Time:     int32(msgTime),
   511  			Elements: bot.ConvertStringMessage(spec, customText, sourceType),
   512  		}
   513  
   514  	case err == nil:
   515  		org, err := db.GetMessageByGlobalID(int32(mid))
   516  		if err != nil {
   517  			return nil, err
   518  		}
   519  		re = &message.ReplyElement{
   520  			ReplySeq: org.GetAttribute().MessageSeq,
   521  			Sender:   org.GetAttribute().SenderUin,
   522  			Time:     int32(org.GetAttribute().Timestamp),
   523  			Elements: bot.ConvertContentMessage(org.GetContent(), sourceType, true),
   524  		}
   525  
   526  	default:
   527  		return nil, errors.New("reply消息中必须包含 text 或 id")
   528  	}
   529  	return re, nil
   530  }
   531  
   532  func (bot *CQBot) voice(elem msg.Element) (m any, err error) {
   533  	f := elem.Get("file")
   534  	data, err := global.FindFile(f, elem.Get("cache"), global.VoicePath)
   535  	if err != nil {
   536  		return nil, err
   537  	}
   538  	if !global.IsAMRorSILK(data) {
   539  		mt, ok := mime.CheckAudio(bytes.NewReader(data))
   540  		if !ok {
   541  			return nil, errors.New("voice type error: " + mt)
   542  		}
   543  		data, err = global.EncoderSilk(data)
   544  		if err != nil {
   545  			return nil, err
   546  		}
   547  	}
   548  	return &message.VoiceElement{Data: data}, nil
   549  }
   550  
   551  func (bot *CQBot) at(id, name string) (m any, err error) {
   552  	t, err := strconv.ParseInt(id, 10, 64)
   553  	if err != nil {
   554  		return nil, err
   555  	}
   556  	name = strings.TrimSpace(name)
   557  	if len(name) > 0 {
   558  		name = "@" + name
   559  	}
   560  	return message.NewAt(t, name), nil
   561  }
   562  
   563  // convertV11 ConvertElement11
   564  func (bot *CQBot) convertV11(elem msg.Element) (m any, ok bool, err error) {
   565  	switch elem.Type {
   566  	default:
   567  		// not ok
   568  		return
   569  	case "at":
   570  		qq := elem.Get("qq")
   571  		if qq == "" {
   572  			qq = elem.Get("target")
   573  		}
   574  		if qq == "all" {
   575  			m = message.AtAll()
   576  			break
   577  		}
   578  		m, err = bot.at(qq, elem.Get("name"))
   579  	case "record":
   580  		m, err = bot.voice(elem)
   581  	}
   582  	ok = true
   583  	return
   584  }
   585  
   586  // convertV12 ConvertElement12
   587  func (bot *CQBot) convertV12(elem msg.Element) (m any, ok bool, err error) {
   588  	switch elem.Type {
   589  	default:
   590  		// not ok
   591  		return
   592  	case "mention":
   593  		m, err = bot.at(elem.Get("user_id"), elem.Get("name"))
   594  	case "mention_all":
   595  		m = message.AtAll()
   596  	case "voice":
   597  		m, err = bot.voice(elem)
   598  	}
   599  	ok = true
   600  	return
   601  }
   602  
   603  // ConvertElement 将解码后的消息转换为MiraiGoElement.
   604  //
   605  // 返回 interface{} 存在三种类型
   606  //
   607  // message.IMessageElement []message.IMessageElement nil
   608  func (bot *CQBot) ConvertElement(spec *onebot.Spec, elem msg.Element, sourceType message.SourceType) (m any, err error) {
   609  	var ok bool
   610  	switch spec.Version {
   611  	case 11:
   612  		m, ok, err = bot.convertV11(elem)
   613  	case 12:
   614  		m, ok, err = bot.convertV12(elem)
   615  	default:
   616  		panic("invalid onebot version:" + strconv.Itoa(spec.Version))
   617  	}
   618  	if ok {
   619  		return m, err
   620  	}
   621  
   622  	switch elem.Type {
   623  	case "text":
   624  		text := elem.Get("text")
   625  		if base.SplitURL {
   626  			var ret []message.IMessageElement
   627  			for _, text := range param.SplitURL(text) {
   628  				ret = append(ret, message.NewText(text))
   629  			}
   630  			return ret, nil
   631  		}
   632  		return message.NewText(text), nil
   633  	case "image":
   634  		img, err := bot.makeImageOrVideoElem(elem, false, sourceType)
   635  		if err != nil {
   636  			return nil, err
   637  		}
   638  		tp := elem.Get("type")
   639  		flash, id := false, int64(0)
   640  		switch tp {
   641  		case "flash":
   642  			flash = true
   643  		case "show":
   644  			id, _ = strconv.ParseInt(elem.Get("id"), 10, 64)
   645  			if id < 40000 || id >= 40006 {
   646  				id = 40000
   647  			}
   648  		default:
   649  			return img, nil
   650  		}
   651  		switch img := img.(type) {
   652  		case *msg.LocalImage:
   653  			img.Flash = flash
   654  			img.EffectID = int32(id)
   655  		case *message.GroupImageElement:
   656  			img.Flash = flash
   657  			img.EffectID = int32(id)
   658  			i, _ := strconv.ParseInt(elem.Get("subType"), 10, 64)
   659  			img.ImageBizType = message.ImageBizType(i)
   660  		case *message.FriendImageElement:
   661  			img.Flash = flash
   662  		}
   663  		return img, nil
   664  	case "reply":
   665  		return bot.reply(spec, elem, sourceType)
   666  	case "forward":
   667  		id := elem.Get("id")
   668  		if id == "" {
   669  			return nil, errors.New("forward 消息中必须包含 id")
   670  		}
   671  		fwdMsg := bot.Client.DownloadForwardMessage(id)
   672  		if fwdMsg == nil {
   673  			return nil, errors.New("forward 消息不存在或已过期")
   674  		}
   675  		return fwdMsg, nil
   676  
   677  	case "poke":
   678  		t, _ := strconv.ParseInt(elem.Get("qq"), 10, 64)
   679  		return &msg.Poke{Target: t}, nil
   680  	case "tts":
   681  		data, err := bot.Client.GetTts(elem.Get("text"))
   682  		if err != nil {
   683  			return nil, err
   684  		}
   685  		return &message.VoiceElement{Data: base.ResampleSilk(data)}, nil
   686  	case "face":
   687  		id, err := strconv.Atoi(elem.Get("id"))
   688  		if err != nil {
   689  			return nil, err
   690  		}
   691  		if elem.Get("type") == "sticker" {
   692  			return &message.AnimatedSticker{ID: int32(id)}, nil
   693  		}
   694  		return message.NewFace(int32(id)), nil
   695  	case "share":
   696  		return message.NewUrlShare(elem.Get("url"), elem.Get("title"), elem.Get("content"), elem.Get("image")), nil
   697  	case "music":
   698  		id := elem.Get("id")
   699  		switch elem.Get("type") {
   700  		case "qq":
   701  			info, err := global.QQMusicSongInfo(id)
   702  			if err != nil {
   703  				return nil, err
   704  			}
   705  			if !info.Get("track_info").Exists() {
   706  				return nil, errors.New("song not found")
   707  			}
   708  			albumMid := info.Get("track_info.album.mid").String()
   709  			pinfo, _ := download.Request{URL: "https://u.y.qq.com/cgi-bin/musicu.fcg?g_tk=2034008533&uin=0&format=json&data={\"comm\":{\"ct\":23,\"cv\":0},\"url_mid\":{\"module\":\"vkey.GetVkeyServer\",\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"4311206557\",\"songmid\":[\"" + info.Get("track_info.mid").Str + "\"],\"songtype\":[0],\"uin\":\"0\",\"loginflag\":1,\"platform\":\"23\"}}}&_=1599039471576"}.JSON()
   710  			jumpURL := "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=" + info.Get("track_info.mid").Str + "&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare"
   711  			content := info.Get("track_info.singer.0.name").String()
   712  			if elem.Get("content") != "" {
   713  				content = elem.Get("content")
   714  			}
   715  			return &message.MusicShareElement{
   716  				MusicType:  message.QQMusic,
   717  				Title:      info.Get("track_info.name").Str,
   718  				Summary:    content,
   719  				Url:        jumpURL,
   720  				PictureUrl: "https://y.gtimg.cn/music/photo_new/T002R180x180M000" + albumMid + ".jpg",
   721  				MusicUrl:   pinfo.Get("url_mid.data.midurlinfo.0.purl").String(),
   722  			}, nil
   723  		case "163":
   724  			info, err := global.NeteaseMusicSongInfo(id)
   725  			if err != nil {
   726  				return nil, err
   727  			}
   728  			if !info.Exists() {
   729  				return nil, errors.New("song not found")
   730  			}
   731  			artistName := ""
   732  			if info.Get("artists.0").Exists() {
   733  				artistName = info.Get("artists.0.name").String()
   734  			}
   735  			return &message.MusicShareElement{
   736  				MusicType:  message.CloudMusic,
   737  				Title:      info.Get("name").String(),
   738  				Summary:    artistName,
   739  				Url:        "https://music.163.com/song/?id=" + id,
   740  				PictureUrl: info.Get("album.picUrl").String(),
   741  				MusicUrl:   "https://music.163.com/song/media/outer/url?id=" + id,
   742  			}, nil
   743  		case "custom":
   744  			if elem.Get("subtype") != "" {
   745  				var subType int
   746  				switch elem.Get("subtype") {
   747  				default:
   748  					subType = message.QQMusic
   749  				case "163":
   750  					subType = message.CloudMusic
   751  				case "migu":
   752  					subType = message.MiguMusic
   753  				case "kugou":
   754  					subType = message.KugouMusic
   755  				case "kuwo":
   756  					subType = message.KuwoMusic
   757  				}
   758  				return &message.MusicShareElement{
   759  					MusicType:  subType,
   760  					Title:      elem.Get("title"),
   761  					Summary:    elem.Get("content"),
   762  					Url:        elem.Get("url"),
   763  					PictureUrl: elem.Get("image"),
   764  					MusicUrl:   elem.Get("voice"),
   765  				}, nil
   766  			}
   767  			xml := fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="2" templateID="1" action="web" brief="[分享] %s" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="2"><voice cover="%s" src="%s"/><title>%s</title><summary>%s</summary></item><source name="音乐" icon="https://i.gtimg.cn/open/app_icon/01/07/98/56/1101079856_100_m.png" url="http://web.p.qq.com/qqmpmobile/aio/app.html?id=1101079856" action="app" a_actionData="com.tencent.qqmusic" i_actionData="tencent1101079856://" appid="1101079856" /></msg>`,
   768  				utils.XmlEscape(elem.Get("title")), elem.Get("url"), elem.Get("image"), elem.Get("voice"), utils.XmlEscape(elem.Get("title")), utils.XmlEscape(elem.Get("content")))
   769  			return &message.ServiceElement{
   770  				Id:      60,
   771  				Content: xml,
   772  				SubType: "music",
   773  			}, nil
   774  		}
   775  		return nil, errors.New("unsupported music type: " + elem.Get("type"))
   776  	case "dice":
   777  		value := elem.Get("value")
   778  		i, _ := strconv.ParseInt(value, 10, 64)
   779  		if i < 0 || i > 6 {
   780  			return nil, errors.New("invalid dice value " + value)
   781  		}
   782  		return message.NewDice(int32(i)), nil
   783  	case "rps":
   784  		value := elem.Get("value")
   785  		i, _ := strconv.ParseInt(value, 10, 64)
   786  		if i < 0 || i > 2 {
   787  			return nil, errors.New("invalid finger-guessing value " + value)
   788  		}
   789  		return message.NewFingerGuessing(int32(i)), nil
   790  	case "xml":
   791  		resID := elem.Get("resid")
   792  		template := elem.Get("data")
   793  		i, _ := strconv.ParseInt(resID, 10, 64)
   794  		m := message.NewRichXml(template, i)
   795  		return m, nil
   796  	case "json":
   797  		resID := elem.Get("resid")
   798  		data := elem.Get("data")
   799  		i, _ := strconv.ParseInt(resID, 10, 64)
   800  		if i == 0 {
   801  			// 默认情况下走小程序通道
   802  			return message.NewLightApp(data), nil
   803  		}
   804  		// resid不为0的情况下走富文本通道,后续补全透传service Id,此处暂时不处理 TODO
   805  		return message.NewRichJson(data), nil
   806  	case "cardimage":
   807  		source := elem.Get("source")
   808  		icon := elem.Get("icon")
   809  		brief := elem.Get("brief")
   810  		parseIntWithDefault := func(name string, origin int64) int64 {
   811  			v, _ := strconv.ParseInt(elem.Get(name), 10, 64)
   812  			if v <= 0 {
   813  				return origin
   814  			}
   815  			return v
   816  		}
   817  		minWidth := parseIntWithDefault("minwidth", 200)
   818  		maxWidth := parseIntWithDefault("maxwidth", 500)
   819  		minHeight := parseIntWithDefault("minheight", 200)
   820  		maxHeight := parseIntWithDefault("maxheight", 1000)
   821  		img, err := bot.makeImageOrVideoElem(elem, false, sourceType)
   822  		if err != nil {
   823  			return nil, errors.New("send cardimage faild")
   824  		}
   825  		return bot.makeShowPic(img, source, brief, icon, minWidth, minHeight, maxWidth, maxHeight, sourceType == message.SourceGroup)
   826  	case "video":
   827  		file, err := bot.makeImageOrVideoElem(elem, true, sourceType)
   828  		if err != nil {
   829  			return nil, err
   830  		}
   831  		v, ok := file.(*msg.LocalVideo)
   832  		if !ok {
   833  			return file, nil
   834  		}
   835  		if v.File == "" {
   836  			return v, nil
   837  		}
   838  		var data []byte
   839  		if cover := elem.Get("cover"); cover != "" {
   840  			data, _ = global.FindFile(cover, elem.Get("cache"), global.ImagePath)
   841  		} else {
   842  			err = global.ExtractCover(v.File, v.File+".jpg")
   843  			if err != nil {
   844  				return nil, err
   845  			}
   846  			data, _ = os.ReadFile(v.File + ".jpg")
   847  		}
   848  		v.Thumb = bytes.NewReader(data)
   849  		video, _ := os.Open(v.File)
   850  		defer video.Close()
   851  		_, _ = video.Seek(4, io.SeekStart)
   852  		header := make([]byte, 4)
   853  		_, _ = video.Read(header)
   854  		if !bytes.Equal(header, []byte{0x66, 0x74, 0x79, 0x70}) { // check file header ftyp
   855  			_, _ = video.Seek(0, io.SeekStart)
   856  			hash, _ := utils.ComputeMd5AndLength(video)
   857  			cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash)+".mp4")
   858  			if !(elem.Get("cache") == "" || elem.Get("cache") == "1") || !global.PathExists(cacheFile) {
   859  				err = global.EncodeMP4(v.File, cacheFile)
   860  				if err != nil {
   861  					return nil, err
   862  				}
   863  			}
   864  			v.File = cacheFile
   865  		}
   866  		return v, nil
   867  	case "file":
   868  		path := elem.Get("path")
   869  		name := elem.Get("name")
   870  		size, _ := strconv.ParseInt(elem.Get("size"), 10, 64)
   871  		busid, _ := strconv.ParseInt(elem.Get("busid"), 10, 64)
   872  		return &message.GroupFileElement{
   873  			Name:  name,
   874  			Size:  size,
   875  			Path:  path,
   876  			Busid: int32(busid),
   877  		}, nil
   878  	default:
   879  		return nil, errors.New("unsupported message type: " + elem.Type)
   880  	}
   881  }
   882  
   883  // makeImageOrVideoElem 图片 elem 生成器,单独拎出来,用于公用
   884  func (bot *CQBot) makeImageOrVideoElem(elem msg.Element, video bool, sourceType message.SourceType) (message.IMessageElement, error) {
   885  	f := elem.Get("file")
   886  	u := elem.Get("url")
   887  	if strings.HasPrefix(f, "http") {
   888  		hash := md5.Sum([]byte(f))
   889  		cacheFile := path.Join(global.CachePath, hex.EncodeToString(hash[:])+".cache")
   890  		maxSize := int64(maxImageSize)
   891  		if video {
   892  			maxSize = maxVideoSize
   893  		}
   894  		thread, _ := strconv.Atoi(elem.Get("c"))
   895  		exist := global.PathExists(cacheFile)
   896  		if exist && (elem.Get("cache") == "" || elem.Get("cache") == "1") {
   897  			goto useCacheFile
   898  		}
   899  		if exist {
   900  			_ = os.Remove(cacheFile)
   901  		}
   902  		{
   903  			r := download.Request{URL: f, Limit: maxSize}
   904  			if err := r.WriteToFileMultiThreading(cacheFile, thread); err != nil {
   905  				return nil, err
   906  			}
   907  		}
   908  	useCacheFile:
   909  		if video {
   910  			return &msg.LocalVideo{File: cacheFile}, nil
   911  		}
   912  		return &msg.LocalImage{File: cacheFile, URL: f}, nil
   913  	}
   914  	if strings.HasPrefix(f, "file") {
   915  		fu, err := url.Parse(f)
   916  		if err != nil {
   917  			return nil, err
   918  		}
   919  		if runtime.GOOS == `windows` && strings.HasPrefix(fu.Path, "/") {
   920  			fu.Path = fu.Path[1:]
   921  		}
   922  		info, err := os.Stat(fu.Path)
   923  		if err != nil {
   924  			if !os.IsExist(err) {
   925  				return nil, errors.New("file not found")
   926  			}
   927  			return nil, err
   928  		}
   929  		if video {
   930  			if info.Size() == 0 || info.Size() >= maxVideoSize {
   931  				return nil, errors.New("invalid video size")
   932  			}
   933  			return &msg.LocalVideo{File: fu.Path}, nil
   934  		}
   935  		if info.Size() == 0 || info.Size() >= maxImageSize {
   936  			return nil, errors.New("invalid image size")
   937  		}
   938  		return &msg.LocalImage{File: fu.Path, URL: f}, nil
   939  	}
   940  	if !video && strings.HasPrefix(f, "base64") {
   941  		b, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(f, "base64://"))
   942  		if err != nil {
   943  			return nil, err
   944  		}
   945  		return &msg.LocalImage{Stream: bytes.NewReader(b), URL: f}, nil
   946  	}
   947  	if !video && strings.HasPrefix(f, "base16384") {
   948  		b, err := b14.UTF82UTF16BE(utils.S2B(strings.TrimPrefix(f, "base16384://")))
   949  		if err != nil {
   950  			return nil, err
   951  		}
   952  		return &msg.LocalImage{Stream: bytes.NewReader(b14.Decode(b)), URL: f}, nil
   953  	}
   954  	rawPath := path.Join(global.ImagePath, f)
   955  	if video {
   956  		if strings.HasSuffix(f, ".video") {
   957  			hash, err := hex.DecodeString(strings.TrimSuffix(f, ".video"))
   958  			if err == nil {
   959  				if b := cache.Video.Get(hash); b != nil {
   960  					return bot.readVideoCache(b), nil
   961  				}
   962  			}
   963  		}
   964  		rawPath = path.Join(global.VideoPath, f)
   965  		if !global.PathExists(rawPath) {
   966  			return nil, errors.New("invalid video")
   967  		}
   968  		if path.Ext(rawPath) != ".video" {
   969  			return &msg.LocalVideo{File: rawPath}, nil
   970  		}
   971  		b, _ := os.ReadFile(rawPath)
   972  		return bot.readVideoCache(b), nil
   973  	}
   974  	// 目前频道内上传的图片均无法被查询到, 需要单独处理
   975  	if sourceType == message.SourceGuildChannel {
   976  		cacheFile := path.Join(global.ImagePath, "guild-images", f)
   977  		if global.PathExists(cacheFile) {
   978  			return &msg.LocalImage{File: cacheFile}, nil
   979  		}
   980  	}
   981  	if strings.HasSuffix(f, ".image") {
   982  		hash, err := hex.DecodeString(strings.TrimSuffix(f, ".image"))
   983  		if err == nil {
   984  			if b := cache.Image.Get(hash); b != nil {
   985  				return bot.readImageCache(b, sourceType)
   986  			}
   987  		}
   988  	}
   989  	exist := global.PathExists(rawPath)
   990  	if !exist {
   991  		if elem.Get("url") != "" {
   992  			elem.Data = []msg.Pair{{K: "file", V: elem.Get("url")}}
   993  			return bot.makeImageOrVideoElem(elem, false, sourceType)
   994  		}
   995  		return nil, errors.New("invalid image")
   996  	}
   997  	if path.Ext(rawPath) != ".image" {
   998  		return &msg.LocalImage{File: rawPath, URL: u}, nil
   999  	}
  1000  	b, err := os.ReadFile(rawPath)
  1001  	if err != nil {
  1002  		return nil, err
  1003  	}
  1004  	return bot.readImageCache(b, sourceType)
  1005  }
  1006  
  1007  func (bot *CQBot) readImageCache(b []byte, sourceType message.SourceType) (message.IMessageElement, error) {
  1008  	var err error
  1009  	if len(b) < 20 {
  1010  		return nil, errors.New("invalid cache")
  1011  	}
  1012  	r := binary.NewReader(b)
  1013  	hash := r.ReadBytes(16)
  1014  	size := r.ReadInt32()
  1015  	r.ReadString()
  1016  	imageURL := r.ReadString()
  1017  	if size == 0 && imageURL != "" {
  1018  		// TODO: fix this
  1019  		var elem msg.Element
  1020  		elem.Type = "image"
  1021  		elem.Data = []msg.Pair{{K: "file", V: imageURL}}
  1022  		return bot.makeImageOrVideoElem(elem, false, sourceType)
  1023  	}
  1024  	var rsp message.IMessageElement
  1025  	switch sourceType { // nolint:exhaustive
  1026  	case message.SourceGroup:
  1027  		rsp, err = bot.Client.QueryGroupImage(int64(rand.Uint32()), hash, size)
  1028  	case message.SourceGuildChannel:
  1029  		if len(bot.Client.GuildService.Guilds) == 0 {
  1030  			err = errors.New("cannot query guild image: not any joined guild")
  1031  			break
  1032  		}
  1033  		guild := bot.Client.GuildService.Guilds[0]
  1034  		rsp, err = bot.Client.GuildService.QueryImage(guild.GuildId, guild.Channels[0].ChannelId, hash, uint64(size))
  1035  	default:
  1036  		rsp, err = bot.Client.QueryFriendImage(int64(rand.Uint32()), hash, size)
  1037  	}
  1038  	if err != nil && imageURL != "" {
  1039  		var elem msg.Element
  1040  		elem.Type = "image"
  1041  		elem.Data = []msg.Pair{{K: "file", V: imageURL}}
  1042  		return bot.makeImageOrVideoElem(elem, false, sourceType)
  1043  	}
  1044  	return rsp, err
  1045  }
  1046  
  1047  func (bot *CQBot) readVideoCache(b []byte) message.IMessageElement {
  1048  	r := binary.NewReader(b)
  1049  	return &message.ShortVideoElement{ // todo 检查缓存是否有效
  1050  		Md5:       r.ReadBytes(16),
  1051  		ThumbMd5:  r.ReadBytes(16),
  1052  		Size:      r.ReadInt32(),
  1053  		ThumbSize: r.ReadInt32(),
  1054  		Name:      r.ReadString(),
  1055  		Uuid:      r.ReadAvailable(),
  1056  	}
  1057  }
  1058  
  1059  // makeShowPic 一种xml 方式发送的群消息图片
  1060  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) {
  1061  	xml := ""
  1062  	var suf message.IMessageElement
  1063  	if brief == "" {
  1064  		brief = "&#91;分享&#93;我看到一张很赞的图片,分享给你,快来看!"
  1065  	}
  1066  	if local, ok := elem.(*msg.LocalImage); ok {
  1067  		r := rand.Uint32()
  1068  		typ := message.SourceGroup
  1069  		if !group {
  1070  			typ = message.SourcePrivate
  1071  		}
  1072  		e, err := bot.uploadLocalImage(message.Source{SourceType: typ, PrimaryID: int64(r)}, local)
  1073  		if err != nil {
  1074  			log.Warnf("警告: 图片上传失败: %v", err)
  1075  			return nil, err
  1076  		}
  1077  		elem = e
  1078  	}
  1079  	switch i := elem.(type) {
  1080  	case *message.GroupImageElement:
  1081  		xml = fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="5" templateID="12345" action="" brief="%s" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="0" advertiser_id="0" aid="0"><image uuid="%x" md5="%x" GroupFiledid="0" filesize="%d" local_path="%s" minWidth="%d" minHeight="%d" maxWidth="%d" maxHeight="%d" /></item><source name="%s" icon="%s" action="" appid="-1" /></msg>`, brief, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
  1082  		suf = i
  1083  	case *message.FriendImageElement:
  1084  		xml = fmt.Sprintf(`<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><msg serviceID="5" templateID="12345" action="" brief="%s" sourceMsgId="0" url="%s" flag="0" adverSign="0" multiMsgFlag="0"><item layout="0" advertiser_id="0" aid="0"><image uuid="%x" md5="%x" GroupFiledid="0" filesize="%d" local_path="%s" minWidth="%d" minHeight="%d" maxWidth="%d" maxHeight="%d" /></item><source name="%s" icon="%s" action="" appid="-1" /></msg>`, brief, "", i.Md5, i.Md5, 0, "", minWidth, minHeight, maxWidth, maxHeight, source, icon)
  1085  		suf = i
  1086  	}
  1087  	if xml == "" {
  1088  		return nil, errors.New("生成xml图片消息失败")
  1089  	}
  1090  	ret := []message.IMessageElement{suf, message.NewRichXml(xml, 5)}
  1091  	return ret, nil
  1092  }