package client import ( "bytes" "crypto/md5" "fmt" "math" "math/rand" "strconv" "strings" "time" "github.com/pkg/errors" "github.com/Mrs4s/MiraiGo/binary" "github.com/Mrs4s/MiraiGo/client/internal/highway" "github.com/Mrs4s/MiraiGo/client/internal/network" "github.com/Mrs4s/MiraiGo/client/pb/longmsg" "github.com/Mrs4s/MiraiGo/client/pb/msg" "github.com/Mrs4s/MiraiGo/client/pb/multimsg" "github.com/Mrs4s/MiraiGo/internal/proto" "github.com/Mrs4s/MiraiGo/message" "github.com/Mrs4s/MiraiGo/utils" ) func init() { decoders["MultiMsg.ApplyUp"] = decodeMultiApplyUpResponse decoders["MultiMsg.ApplyDown"] = decodeMultiApplyDownResponse } // MultiMsg.ApplyUp func (c *QQClient) buildMultiApplyUpPacket(data, hash []byte, buType int32, groupUin int64) (uint16, []byte) { req := &multimsg.MultiReqBody{ Subcmd: 1, TermType: 5, PlatformType: 9, NetType: 3, BuildVer: "8.2.0.1296", MultimsgApplyupReq: []*multimsg.MultiMsgApplyUpReq{ { DstUin: groupUin, MsgSize: int64(len(data)), MsgMd5: hash, MsgType: 3, }, }, BuType: buType, } payload, _ := proto.Marshal(req) return c.uniPacket("MultiMsg.ApplyUp", payload) } // MultiMsg.ApplyUp func decodeMultiApplyUpResponse(_ *QQClient, _ *network.IncomingPacketInfo, payload []byte) (any, error) { body := multimsg.MultiRspBody{} if err := proto.Unmarshal(payload, &body); err != nil { return nil, errors.Wrap(err, "failed to unmarshal protobuf message") } if len(body.MultimsgApplyupRsp) == 0 { return nil, errors.New("rsp is empty") } rsp := body.MultimsgApplyupRsp[0] switch rsp.Result { case 0: return rsp, nil case 193: return nil, errors.New("too large") } return nil, errors.Errorf("unexpected multimsg apply up response: %d", rsp.Result) } // MultiMsg.ApplyDown func (c *QQClient) buildMultiApplyDownPacket(resID string) (uint16, []byte) { req := &multimsg.MultiReqBody{ Subcmd: 2, TermType: 5, PlatformType: 9, NetType: 3, BuildVer: "8.2.0.1296", MultimsgApplydownReq: []*multimsg.MultiMsgApplyDownReq{ { MsgResid: []byte(resID), MsgType: 3, }, }, BuType: 2, ReqChannelType: 2, } payload, _ := proto.Marshal(req) return c.uniPacket("MultiMsg.ApplyDown", payload) } // MultiMsg.ApplyDown func decodeMultiApplyDownResponse(_ *QQClient, _ *network.IncomingPacketInfo, payload []byte) (any, error) { body := multimsg.MultiRspBody{} if err := proto.Unmarshal(payload, &body); err != nil { return nil, errors.Wrap(err, "failed to unmarshal protobuf message") } if len(body.MultimsgApplydownRsp) == 0 { return nil, errors.New("message not found") } rsp := body.MultimsgApplydownRsp[0] if rsp.ThumbDownPara == nil { return nil, errors.New("message not found") } var prefix string if rsp.MsgExternInfo != nil && rsp.MsgExternInfo.ChannelType == 2 { prefix = "https://ssl.htdata.qq.com" } else { ma := body.MultimsgApplydownRsp[0] if len(rsp.Uint32DownIp) == 0 || len(ma.Uint32DownPort) == 0 { return nil, errors.New("message not found") } prefix = fmt.Sprintf("http://%s:%d", binary.UInt32ToIPV4Address(uint32(rsp.Uint32DownIp[0])), ma.Uint32DownPort[0]) } b, err := utils.HttpGetBytes(fmt.Sprintf("%s%s", prefix, string(rsp.ThumbDownPara)), "") if err != nil { return nil, errors.Wrap(err, "failed to download by multi apply down") } if b[0] != 40 { return nil, errors.New("unexpected body data") } tea := binary.NewTeaCipher(body.MultimsgApplydownRsp[0].MsgKey) r := binary.NewReader(b[1:]) i1 := r.ReadInt32() i2 := r.ReadInt32() if i1 > 0 { r.ReadBytes(int(i1)) // im msg head } data := tea.Decrypt(r.ReadBytes(int(i2))) lb := longmsg.LongRspBody{} if err = proto.Unmarshal(data, &lb); err != nil { return nil, errors.Wrap(err, "failed to unmarshal protobuf message") } msgContent := lb.MsgDownRsp[0].MsgContent if msgContent == nil { return nil, errors.New("message content is empty") } uc := binary.GZipUncompress(msgContent) mt := msg.PbMultiMsgTransmit{} if err = proto.Unmarshal(uc, &mt); err != nil { return nil, errors.Wrap(err, "failed to unmarshal protobuf message") } return &mt, nil } type forwardMsgLinker struct { items map[string]*msg.PbMultiMsgItem } func (l *forwardMsgLinker) link(name string) *message.ForwardMessage { item := l.items[name] if item == nil { return nil } nodes := make([]*message.ForwardNode, 0, len(item.Buffer.Msg)) for _, m := range item.Buffer.Msg { name := m.Head.FromNick.Unwrap() if m.Head.MsgType.Unwrap() == 82 && m.Head.GroupInfo != nil { name = m.Head.GroupInfo.GroupCard.Unwrap() } msgElems := message.ParseMessageElems(m.Body.RichText.Elems) for i, elem := range msgElems { if forward, ok := elem.(*message.ForwardElement); ok { if forward.FileName != "" { msgElems[i] = l.link(forward.FileName) // 递归处理嵌套转发 } } } gid := int64(0) // 给群号一个缺省值0,防止在读合并转发的私聊内容时候会报错 if m.Head.GroupInfo != nil { gid = m.Head.GroupInfo.GroupCode.Unwrap() } nodes = append(nodes, &message.ForwardNode{ GroupId: gid, SenderId: m.Head.FromUin.Unwrap(), SenderName: name, Time: m.Head.MsgTime.Unwrap(), Message: msgElems, }) } return &message.ForwardMessage{Nodes: nodes} } func (c *QQClient) GetForwardMessage(resID string) *message.ForwardMessage { m := c.DownloadForwardMessage(resID) if m == nil { return nil } linker := forwardMsgLinker{ items: make(map[string]*msg.PbMultiMsgItem), } for _, item := range m.Items { linker.items[item.FileName.Unwrap()] = item } return linker.link("MultiMsg") } func (c *QQClient) DownloadForwardMessage(resId string) *message.ForwardElement { i, err := c.sendAndWait(c.buildMultiApplyDownPacket(resId)) if err != nil { return nil } multiMsg := i.(*msg.PbMultiMsgTransmit) if multiMsg.PbItemList == nil { return nil } var pv bytes.Buffer for i := 0; i < int(math.Min(4, float64(len(multiMsg.Msg)))); i++ { m := multiMsg.Msg[i] sender := m.Head.FromNick.Unwrap() if m.Head.MsgType.Unwrap() == 82 && m.Head.GroupInfo != nil { sender = m.Head.GroupInfo.GroupCard.Unwrap() } brief := message.ToReadableString(message.ParseMessageElems(multiMsg.Msg[i].Body.RichText.Elems)) fmt.Fprintf(&pv, `%s: %s`, sender, brief) } return genForwardTemplate( resId, pv.String(), fmt.Sprintf("查看 %d 条转发消息", len(multiMsg.Msg)), time.Now().UnixNano(), multiMsg.PbItemList, ) } func forwardDisplay(resID, fileName, preview, summary string) string { sb := strings.Builder{} sb.WriteString(`群聊的聊天记录 `) sb.WriteString(preview) sb.WriteString(`
`) sb.WriteString(summary) // todo: 私聊的聊天记录? sb.WriteString(`
`) return sb.String() } func (c *QQClient) NewForwardMessageBuilder(groupCode int64) *ForwardMessageBuilder { return &ForwardMessageBuilder{ c: c, groupCode: groupCode, } } type ForwardMessageBuilder struct { c *QQClient groupCode int64 objs []*msg.PbMultiMsgItem } // NestedNode 返回一个嵌套转发节点,其内容将会被 Builder 重定位 func (builder *ForwardMessageBuilder) NestedNode() *message.ForwardElement { filename := strconv.FormatInt(time.Now().UnixNano(), 10) // 大概率不会重复 return &message.ForwardElement{FileName: filename} } // Link 将真实的消息内容填充 reloc func (builder *ForwardMessageBuilder) Link(reloc *message.ForwardElement, fmsg *message.ForwardMessage) { seq := builder.c.nextGroupSeq() m := fmsg.PackForwardMessage(seq, rand.Int31(), builder.groupCode) builder.objs = append(builder.objs, &msg.PbMultiMsgItem{ FileName: proto.String(reloc.FileName), Buffer: &msg.PbMultiMsgNew{ Msg: m, }, }) reloc.Content = forwardDisplay("", reloc.FileName, fmsg.Preview(), fmt.Sprintf("查看 %d 条转发消息", fmsg.Length())) } // Main 最外层的转发消息, 调用该方法后即上传消息 func (builder *ForwardMessageBuilder) Main(m *message.ForwardMessage) *message.ForwardElement { if m.Length() > 200 { return nil } c := builder.c seq := c.nextGroupSeq() fm := m.PackForwardMessage(seq, rand.Int31(), builder.groupCode) const filename = "MultiMsg" builder.objs = append(builder.objs, &msg.PbMultiMsgItem{ FileName: proto.String(filename), Buffer: &msg.PbMultiMsgNew{ Msg: fm, }, }) trans := &msg.PbMultiMsgTransmit{ Msg: fm, PbItemList: builder.objs, } b, _ := proto.Marshal(trans) data := binary.GZipCompress(b) hash := md5.Sum(data) rsp, body, err := c.multiMsgApplyUp(builder.groupCode, data, hash[:], 2) if err != nil { return nil } content := forwardDisplay(rsp.MsgResid, utils.RandomString(32), m.Preview(), fmt.Sprintf("查看 %d 条转发消息", m.Length())) bodyHash := md5.Sum(body) input := highway.Transaction{ CommandID: 27, Ticket: rsp.MsgSig, Body: bytes.NewReader(body), Sum: bodyHash[:], Size: int64(len(body)), } for i, ip := range rsp.Uint32UpIp { addr := highway.Addr{IP: uint32(ip), Port: int(rsp.Uint32UpPort[i])} err := c.highwaySession.Upload(addr, input) if err != nil { continue } return &message.ForwardElement{ FileName: filename, Content: content, ResId: rsp.MsgResid, } } return nil }