github.com/Mrs4s/MiraiGo@v0.0.0-20240226124653-54bdd873e3fe/client/multimsg.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "fmt" 7 "math" 8 "math/rand" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/pkg/errors" 14 15 "github.com/Mrs4s/MiraiGo/binary" 16 "github.com/Mrs4s/MiraiGo/client/internal/highway" 17 "github.com/Mrs4s/MiraiGo/client/internal/network" 18 "github.com/Mrs4s/MiraiGo/client/pb/longmsg" 19 "github.com/Mrs4s/MiraiGo/client/pb/msg" 20 "github.com/Mrs4s/MiraiGo/client/pb/multimsg" 21 "github.com/Mrs4s/MiraiGo/internal/proto" 22 "github.com/Mrs4s/MiraiGo/message" 23 "github.com/Mrs4s/MiraiGo/utils" 24 ) 25 26 func init() { 27 decoders["MultiMsg.ApplyUp"] = decodeMultiApplyUpResponse 28 decoders["MultiMsg.ApplyDown"] = decodeMultiApplyDownResponse 29 } 30 31 // MultiMsg.ApplyUp 32 func (c *QQClient) buildMultiApplyUpPacket(data, hash []byte, buType int32, groupUin int64) (uint16, []byte) { 33 req := &multimsg.MultiReqBody{ 34 Subcmd: 1, 35 TermType: 5, 36 PlatformType: 9, 37 NetType: 3, 38 BuildVer: "8.2.0.1296", 39 MultimsgApplyupReq: []*multimsg.MultiMsgApplyUpReq{ 40 { 41 DstUin: groupUin, 42 MsgSize: int64(len(data)), 43 MsgMd5: hash, 44 MsgType: 3, 45 }, 46 }, 47 BuType: buType, 48 } 49 payload, _ := proto.Marshal(req) 50 return c.uniPacket("MultiMsg.ApplyUp", payload) 51 } 52 53 // MultiMsg.ApplyUp 54 func decodeMultiApplyUpResponse(_ *QQClient, pkt *network.Packet) (any, error) { 55 body := multimsg.MultiRspBody{} 56 if err := proto.Unmarshal(pkt.Payload, &body); err != nil { 57 return nil, errors.Wrap(err, "failed to unmarshal protobuf message") 58 } 59 if len(body.MultimsgApplyupRsp) == 0 { 60 return nil, errors.New("rsp is empty") 61 } 62 rsp := body.MultimsgApplyupRsp[0] 63 switch rsp.Result { 64 case 0: 65 return rsp, nil 66 case 193: 67 return nil, errors.New("too large") 68 } 69 return nil, errors.Errorf("unexpected multimsg apply up response: %d", rsp.Result) 70 } 71 72 // MultiMsg.ApplyDown 73 func (c *QQClient) buildMultiApplyDownPacket(resID string) (uint16, []byte) { 74 req := &multimsg.MultiReqBody{ 75 Subcmd: 2, 76 TermType: 5, 77 PlatformType: 9, 78 NetType: 3, 79 BuildVer: "8.2.0.1296", 80 MultimsgApplydownReq: []*multimsg.MultiMsgApplyDownReq{ 81 { 82 MsgResid: []byte(resID), 83 MsgType: 3, 84 }, 85 }, 86 BuType: 2, 87 ReqChannelType: 2, 88 } 89 payload, _ := proto.Marshal(req) 90 return c.uniPacket("MultiMsg.ApplyDown", payload) 91 } 92 93 // MultiMsg.ApplyDown 94 func decodeMultiApplyDownResponse(_ *QQClient, pkt *network.Packet) (any, error) { 95 body := multimsg.MultiRspBody{} 96 if err := proto.Unmarshal(pkt.Payload, &body); err != nil { 97 return nil, errors.Wrap(err, "failed to unmarshal protobuf message") 98 } 99 if len(body.MultimsgApplydownRsp) == 0 { 100 return nil, errors.New("message not found") 101 } 102 rsp := body.MultimsgApplydownRsp[0] 103 104 if rsp.ThumbDownPara == nil { 105 return nil, errors.New("message not found") 106 } 107 108 var prefix string 109 if rsp.MsgExternInfo != nil && rsp.MsgExternInfo.ChannelType == 2 { 110 prefix = "https://ssl.htdata.qq.com" 111 } else { 112 ma := body.MultimsgApplydownRsp[0] 113 if len(rsp.Uint32DownIp) == 0 || len(ma.Uint32DownPort) == 0 { 114 return nil, errors.New("message not found") 115 } 116 prefix = fmt.Sprintf("http://%s:%d", binary.UInt32ToIPV4Address(uint32(rsp.Uint32DownIp[0])), ma.Uint32DownPort[0]) 117 } 118 b, err := utils.HttpGetBytes(fmt.Sprintf("%s%s", prefix, string(rsp.ThumbDownPara)), "") 119 if err != nil { 120 return nil, errors.Wrap(err, "failed to download by multi apply down") 121 } 122 if b[0] != 40 { 123 return nil, errors.New("unexpected body data") 124 } 125 tea := binary.NewTeaCipher(body.MultimsgApplydownRsp[0].MsgKey) 126 r := binary.NewReader(b[1:]) 127 i1 := r.ReadInt32() 128 i2 := r.ReadInt32() 129 if i1 > 0 { 130 r.ReadBytes(int(i1)) // im msg head 131 } 132 data := tea.Decrypt(r.ReadBytes(int(i2))) 133 lb := longmsg.LongRspBody{} 134 if err = proto.Unmarshal(data, &lb); err != nil { 135 return nil, errors.Wrap(err, "failed to unmarshal protobuf message") 136 } 137 msgContent := lb.MsgDownRsp[0].MsgContent 138 if msgContent == nil { 139 return nil, errors.New("message content is empty") 140 } 141 uc := binary.GZipUncompress(msgContent) 142 mt := msg.PbMultiMsgTransmit{} 143 if err = proto.Unmarshal(uc, &mt); err != nil { 144 return nil, errors.Wrap(err, "failed to unmarshal protobuf message") 145 } 146 return &mt, nil 147 } 148 149 type forwardMsgLinker struct { 150 items map[string]*msg.PbMultiMsgItem 151 } 152 153 func (l *forwardMsgLinker) link(name string) *message.ForwardMessage { 154 item := l.items[name] 155 if item == nil { 156 return nil 157 } 158 nodes := make([]*message.ForwardNode, 0, len(item.Buffer.Msg)) 159 for _, m := range item.Buffer.Msg { 160 name := m.Head.FromNick.Unwrap() 161 if m.Head.MsgType.Unwrap() == 82 && m.Head.GroupInfo != nil { 162 name = m.Head.GroupInfo.GroupCard.Unwrap() 163 } 164 165 msgElems := message.ParseMessageElems(m.Body.RichText.Elems) 166 for i, elem := range msgElems { 167 if forward, ok := elem.(*message.ForwardElement); ok { 168 if forward.FileName != "" { 169 msgElems[i] = l.link(forward.FileName) // 递归处理嵌套转发 170 } 171 } 172 } 173 174 gid := int64(0) // 给群号一个缺省值0,防止在读合并转发的私聊内容时候会报错 175 if m.Head.GroupInfo != nil { 176 gid = m.Head.GroupInfo.GroupCode.Unwrap() 177 } 178 nodes = append(nodes, &message.ForwardNode{ 179 GroupId: gid, 180 SenderId: m.Head.FromUin.Unwrap(), 181 SenderName: name, 182 Time: m.Head.MsgTime.Unwrap(), 183 Message: msgElems, 184 }) 185 } 186 return &message.ForwardMessage{Nodes: nodes} 187 } 188 189 func (c *QQClient) GetForwardMessage(resID string) *message.ForwardMessage { 190 m := c.DownloadForwardMessage(resID) 191 if m == nil { 192 return nil 193 } 194 linker := forwardMsgLinker{ 195 items: make(map[string]*msg.PbMultiMsgItem), 196 } 197 for _, item := range m.Items { 198 linker.items[item.FileName.Unwrap()] = item 199 } 200 return linker.link("MultiMsg") 201 } 202 203 func (c *QQClient) DownloadForwardMessage(resId string) *message.ForwardElement { 204 i, err := c.sendAndWait(c.buildMultiApplyDownPacket(resId)) 205 if err != nil { 206 return nil 207 } 208 multiMsg := i.(*msg.PbMultiMsgTransmit) 209 if multiMsg.PbItemList == nil { 210 return nil 211 } 212 var pv bytes.Buffer 213 for i := 0; i < int(math.Min(4, float64(len(multiMsg.Msg)))); i++ { 214 m := multiMsg.Msg[i] 215 sender := m.Head.FromNick.Unwrap() 216 if m.Head.MsgType.Unwrap() == 82 && m.Head.GroupInfo != nil { 217 sender = m.Head.GroupInfo.GroupCard.Unwrap() 218 } 219 brief := message.ToReadableString(message.ParseMessageElems(multiMsg.Msg[i].Body.RichText.Elems)) 220 fmt.Fprintf(&pv, `<title size="26" color="#777777">%s: %s</title>`, sender, brief) 221 } 222 return genForwardTemplate( 223 resId, pv.String(), 224 fmt.Sprintf("查看 %d 条转发消息", len(multiMsg.Msg)), 225 time.Now().UnixNano(), 226 multiMsg.PbItemList, 227 ) 228 } 229 230 func forwardDisplay(resID, fileName, preview, summary string) string { 231 sb := strings.Builder{} 232 sb.WriteString(`<?xml version='1.0' encoding='UTF-8'?><msg serviceID="35" templateID="1" action="viewMultiMsg" brief="[聊天记录]" `) 233 if resID != "" { 234 sb.WriteString(`m_resid="`) 235 sb.WriteString(resID) 236 sb.WriteString("\" ") 237 } 238 sb.WriteString(`m_fileName="`) 239 sb.WriteString(fileName) 240 sb.WriteString(`" tSum="3" sourceMsgId="0" url="" flag="3" adverSign="0" multiMsgFlag="0"><item layout="1"><title color="#000000" size="34">群聊的聊天记录</title> `) 241 sb.WriteString(preview) 242 sb.WriteString(`<hr></hr><summary size="26" color="#808080">`) 243 sb.WriteString(summary) 244 // todo: 私聊的聊天记录? 245 sb.WriteString(`</summary></item><source name="聊天记录"></source></msg>`) 246 return sb.String() 247 } 248 249 func (c *QQClient) NewForwardMessageBuilder(groupCode int64) *ForwardMessageBuilder { 250 return &ForwardMessageBuilder{ 251 c: c, 252 groupCode: groupCode, 253 } 254 } 255 256 type ForwardMessageBuilder struct { 257 c *QQClient 258 groupCode int64 259 objs []*msg.PbMultiMsgItem 260 } 261 262 // NestedNode 返回一个嵌套转发节点,其内容将会被 Builder 重定位 263 func (builder *ForwardMessageBuilder) NestedNode() *message.ForwardElement { 264 filename := strconv.FormatInt(time.Now().UnixNano(), 10) // 大概率不会重复 265 return &message.ForwardElement{FileName: filename} 266 } 267 268 // Link 将真实的消息内容填充 reloc 269 func (builder *ForwardMessageBuilder) Link(reloc *message.ForwardElement, fmsg *message.ForwardMessage) { 270 seq := builder.c.nextGroupSeq() 271 m := fmsg.PackForwardMessage(seq, rand.Int31(), builder.groupCode) 272 builder.objs = append(builder.objs, &msg.PbMultiMsgItem{ 273 FileName: proto.String(reloc.FileName), 274 Buffer: &msg.PbMultiMsgNew{ 275 Msg: m, 276 }, 277 }) 278 reloc.Content = forwardDisplay("", reloc.FileName, fmsg.Preview(), fmt.Sprintf("查看 %d 条转发消息", fmsg.Length())) 279 } 280 281 // Main 最外层的转发消息, 调用该方法后即上传消息 282 func (builder *ForwardMessageBuilder) Main(m *message.ForwardMessage) *message.ForwardElement { 283 if m.Length() > 200 { 284 return nil 285 } 286 c := builder.c 287 seq := c.nextGroupSeq() 288 fm := m.PackForwardMessage(seq, rand.Int31(), builder.groupCode) 289 const filename = "MultiMsg" 290 builder.objs = append(builder.objs, &msg.PbMultiMsgItem{ 291 FileName: proto.String(filename), 292 Buffer: &msg.PbMultiMsgNew{ 293 Msg: fm, 294 }, 295 }) 296 trans := &msg.PbMultiMsgTransmit{ 297 Msg: fm, 298 PbItemList: builder.objs, 299 } 300 b, _ := proto.Marshal(trans) 301 data := binary.GZipCompress(b) 302 hash := md5.Sum(data) 303 rsp, body, err := c.multiMsgApplyUp(builder.groupCode, data, hash[:], 2) 304 if err != nil { 305 return nil 306 } 307 content := forwardDisplay(rsp.MsgResid, utils.RandomString(32), m.Preview(), fmt.Sprintf("查看 %d 条转发消息", m.Length())) 308 bodyHash := md5.Sum(body) 309 input := highway.Transaction{ 310 CommandID: 27, 311 Ticket: rsp.MsgSig, 312 Body: bytes.NewReader(body), 313 Sum: bodyHash[:], 314 Size: int64(len(body)), 315 } 316 _, err = c.highwaySession.Upload(input) 317 if err != nil { 318 return nil 319 } 320 return &message.ForwardElement{ 321 FileName: filename, 322 Content: content, 323 ResId: rsp.MsgResid, 324 } 325 }