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 = "[分享]我看到一张很赞的图片,分享给你,快来看!" 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 }