github.com/Mrs4s/MiraiGo@v0.0.0-20240226124653-54bdd873e3fe/client/ptt.go (about) 1 package client 2 3 import ( 4 "crypto/md5" 5 "encoding/hex" 6 "fmt" 7 "io" 8 9 "github.com/pkg/errors" 10 11 "github.com/Mrs4s/MiraiGo/binary" 12 "github.com/Mrs4s/MiraiGo/client/internal/highway" 13 "github.com/Mrs4s/MiraiGo/client/internal/network" 14 "github.com/Mrs4s/MiraiGo/client/pb/cmd0x346" 15 "github.com/Mrs4s/MiraiGo/client/pb/cmd0x388" 16 "github.com/Mrs4s/MiraiGo/client/pb/msg" 17 "github.com/Mrs4s/MiraiGo/client/pb/pttcenter" 18 "github.com/Mrs4s/MiraiGo/internal/proto" 19 "github.com/Mrs4s/MiraiGo/message" 20 "github.com/Mrs4s/MiraiGo/utils" 21 ) 22 23 func init() { 24 decoders["PttCenterSvr.ShortVideoDownReq"] = decodePttShortVideoDownResponse 25 decoders["PttCenterSvr.GroupShortVideoUpReq"] = decodeGroupShortVideoUploadResponse 26 } 27 28 var pttWaiter = utils.NewUploadWaiter() 29 30 func c2cPttExtraInfo() []byte { 31 w := binary.SelectWriter() 32 defer binary.PutWriter(w) 33 w.WriteByte(2) // tlv count 34 { 35 w.WriteByte(8) 36 w.WriteUInt16(4) 37 w.WriteUInt32(1) // codec 38 } 39 { 40 w.WriteByte(9) 41 w.WriteUInt16(4) 42 w.WriteUInt32(0) // 时长 43 } 44 w.WriteByte(10) 45 reserveInfo := []byte{0x08, 0x00, 0x28, 0x00, 0x38, 0x00} // todo 46 w.WriteBytesShort(reserveInfo) 47 return append([]byte(nil), w.Bytes()...) 48 } 49 50 // UploadVoice 将语音数据使用群语音通道上传到服务器, 返回 message.GroupVoiceElement 可直接发送 51 func (c *QQClient) UploadVoice(target message.Source, voice io.ReadSeeker) (*message.GroupVoiceElement, error) { 52 switch target.SourceType { 53 case message.SourceGroup, message.SourcePrivate: 54 // ok 55 default: 56 return nil, errors.New("unsupported source type") 57 } 58 59 fh, length := utils.ComputeMd5AndLength(voice) 60 _, _ = voice.Seek(0, io.SeekStart) 61 62 key := string(fh) 63 pttWaiter.Wait(key) 64 defer pttWaiter.Done(key) 65 66 var cmd int32 67 var ext []byte 68 if target.SourceType == message.SourcePrivate { 69 cmd = int32(26) 70 ext = c.buildC2CPttStoreBDHExt(target.PrimaryID, fh, int32(length), int32(length)) 71 } else { 72 cmd = int32(29) 73 ext = c.buildGroupPttStoreBDHExt(target.PrimaryID, fh, int32(length), 0, int32(length)) 74 } 75 // multi-thread upload is no need 76 rsp, err := c.highwaySession.Upload(highway.Transaction{ 77 CommandID: cmd, 78 Body: voice, 79 Sum: fh, 80 Size: length, 81 Ticket: c.highwaySession.SigSession, 82 Ext: ext, 83 }) 84 if err != nil { 85 return nil, err 86 } 87 if len(rsp) == 0 { 88 return nil, errors.New("miss rsp") 89 } 90 ptt := &msg.Ptt{ 91 FileType: proto.Int32(4), 92 SrcUin: proto.Some(c.Uin), 93 FileMd5: fh, 94 FileName: proto.String(fmt.Sprintf("%x.amr", fh)), 95 FileSize: proto.Int32(int32(length)), 96 BoolValid: proto.Bool(true), 97 } 98 if target.SourceType == message.SourceGroup { 99 pkt := cmd0x388.D388RspBody{} 100 if err = proto.Unmarshal(rsp, &pkt); err != nil { 101 return nil, errors.Wrap(err, "failed to unmarshal protobuf message") 102 } 103 if len(pkt.TryupPttRsp) == 0 { 104 return nil, errors.New("miss try up rsp") 105 } 106 ptt.PbReserve = []byte{8, 0, 40, 0, 56, 0} 107 ptt.GroupFileKey = pkt.TryupPttRsp[0].FileKey 108 return &message.GroupVoiceElement{Ptt: ptt}, nil 109 } else { 110 pkt := cmd0x346.C346RspBody{} 111 if err = proto.Unmarshal(rsp, &pkt); err != nil { 112 return nil, errors.Wrap(err, "failed to unmarshal protobuf message") 113 } 114 if pkt.ApplyUploadRsp == nil { 115 return nil, errors.New("miss apply upload rsp") 116 } 117 ptt.FileUuid = pkt.ApplyUploadRsp.Uuid 118 ptt.Reserve = c2cPttExtraInfo() 119 return &message.PrivateVoiceElement{Ptt: ptt}, nil 120 } 121 } 122 123 // UploadShortVideo 将视频和封面上传到服务器, 返回 message.ShortVideoElement 可直接发送 124 func (c *QQClient) UploadShortVideo(target message.Source, video, thumb io.ReadSeeker) (*message.ShortVideoElement, error) { 125 thumbHash := md5.New() 126 thumbLen, _ := io.Copy(thumbHash, thumb) 127 thumbSum := thumbHash.Sum(nil) 128 videoSum, videoLen := utils.ComputeMd5AndLength(io.TeeReader(video, thumbHash)) 129 sum := thumbHash.Sum(nil) 130 131 key := string(sum) 132 pttWaiter.Wait(key) 133 defer pttWaiter.Done(key) 134 135 i, err := c.sendAndWait(c.buildPttGroupShortVideoUploadReqPacket(target, videoSum, thumbSum, videoLen, thumbLen)) 136 if err != nil { 137 return nil, errors.Wrap(err, "upload req error") 138 } 139 rsp := i.(*pttcenter.ShortVideoUploadRsp) 140 videoElement := &message.ShortVideoElement{ 141 Size: int32(videoLen), 142 ThumbSize: int32(thumbLen), 143 Md5: videoSum, 144 ThumbMd5: thumbSum, 145 Guild: target.SourceType == message.SourceGuildChannel, 146 } 147 if rsp.FileExists == 1 { 148 videoElement.Uuid = []byte(rsp.FileId) 149 return videoElement, nil 150 } 151 152 var hwRsp []byte 153 cmd := int32(25) 154 if target.SourceType == message.SourceGuildChannel { 155 cmd = 89 156 } 157 ext, _ := proto.Marshal(c.buildPttShortVideoProto(target, videoSum, thumbSum, videoLen, thumbLen).PttShortVideoUploadReq) 158 _, _ = thumb.Seek(0, io.SeekStart) 159 _, _ = video.Seek(0, io.SeekStart) 160 combined := io.MultiReader(thumb, video) 161 input := highway.Transaction{ 162 CommandID: cmd, 163 Body: combined, 164 Size: videoLen + thumbLen, 165 Sum: sum, 166 Ticket: c.highwaySession.SigSession, 167 Ext: ext, 168 Encrypt: true, 169 } 170 hwRsp, err = c.highwaySession.Upload(input) 171 if err != nil { 172 return nil, errors.Wrap(err, "upload video file error") 173 } 174 if len(hwRsp) == 0 { 175 return nil, errors.New("resp is empty") 176 } 177 rsp = &pttcenter.ShortVideoUploadRsp{} 178 if err = proto.Unmarshal(hwRsp, rsp); err != nil { 179 return nil, errors.Wrap(err, "decode error") 180 } 181 videoElement.Uuid = []byte(rsp.FileId) 182 return videoElement, nil 183 } 184 185 func (c *QQClient) GetShortVideoUrl(uuid, md5 []byte) string { 186 i, err := c.sendAndWait(c.buildPttShortVideoDownReqPacket(uuid, md5)) 187 if err != nil { 188 return "" 189 } 190 return i.(string) 191 } 192 193 func (c *QQClient) buildGroupPttStoreBDHExt(groupCode int64, md5 []byte, size, codec, voiceLength int32) []byte { 194 req := &cmd0x388.D388ReqBody{ 195 NetType: proto.Uint32(3), 196 Subcmd: proto.Uint32(3), 197 TryupPttReq: []*cmd0x388.TryUpPttReq{ 198 { 199 GroupCode: proto.Uint64(uint64(groupCode)), 200 SrcUin: proto.Uint64(uint64(c.Uin)), 201 FileMd5: md5, 202 FileSize: proto.Uint64(uint64(size)), 203 FileName: md5, 204 SrcTerm: proto.Uint32(5), 205 PlatformType: proto.Uint32(9), 206 BuType: proto.Uint32(4), 207 InnerIp: proto.Uint32(0), 208 BuildVer: utils.S2B("6.5.5.663"), 209 VoiceLength: proto.Uint32(uint32(voiceLength)), 210 Codec: proto.Uint32(uint32(codec)), 211 VoiceType: proto.Uint32(1), 212 NewUpChan: proto.Bool(true), 213 }, 214 }, 215 } 216 payload, _ := proto.Marshal(req) 217 return payload 218 } 219 220 // PttCenterSvr.ShortVideoDownReq 221 func (c *QQClient) buildPttShortVideoDownReqPacket(uuid, md5 []byte) (uint16, []byte) { 222 seq := c.nextSeq() 223 body := &pttcenter.ShortVideoReqBody{ 224 Cmd: 400, 225 Seq: int32(seq), 226 PttShortVideoDownloadReq: &pttcenter.ShortVideoDownloadReq{ 227 FromUin: c.Uin, 228 ToUin: c.Uin, 229 ChatType: 1, 230 ClientType: 7, 231 FileId: string(uuid), 232 GroupCode: 1, 233 FileMd5: md5, 234 BusinessType: 1, 235 FileType: 2, 236 DownType: 2, 237 SceneType: 2, 238 }, 239 } 240 payload, _ := proto.Marshal(body) 241 packet := c.uniPacketWithSeq(seq, "PttCenterSvr.ShortVideoDownReq", payload) 242 return seq, packet 243 } 244 245 func (c *QQClient) buildPttShortVideoProto(target message.Source, videoHash, thumbHash []byte, videoSize, thumbSize int64) *pttcenter.ShortVideoReqBody { 246 seq := c.nextSeq() 247 chatType := int32(1) 248 if target.SourceType == message.SourceGuildChannel { 249 chatType = 4 250 } 251 body := &pttcenter.ShortVideoReqBody{ 252 Cmd: 300, 253 Seq: int32(seq), 254 PttShortVideoUploadReq: &pttcenter.ShortVideoUploadReq{ 255 FromUin: c.Uin, 256 ToUin: target.PrimaryID, 257 ChatType: chatType, 258 ClientType: 2, 259 Info: &pttcenter.ShortVideoFileInfo{ 260 FileName: fmt.Sprintf("%x.mp4", videoHash), 261 FileMd5: videoHash, 262 ThumbFileMd5: thumbHash, 263 FileSize: videoSize, 264 FileResLength: 1280, 265 FileResWidth: 720, 266 FileFormat: 3, 267 FileTime: 120, 268 ThumbFileSize: thumbSize, 269 }, 270 GroupCode: target.PrimaryID, 271 SupportLargeSize: 1, 272 }, 273 ExtensionReq: []*pttcenter.ShortVideoExtensionReq{ 274 { 275 SubBusiType: 0, 276 UserCnt: 1, 277 }, 278 }, 279 } 280 if target.SourceType == message.SourceGuildChannel { 281 body.PttShortVideoUploadReq.BusinessType = 4601 282 body.PttShortVideoUploadReq.ToUin = target.SecondaryID 283 body.ExtensionReq[0].SubBusiType = 4601 284 } 285 return body 286 } 287 288 // PttCenterSvr.GroupShortVideoUpReq 289 func (c *QQClient) buildPttGroupShortVideoUploadReqPacket(target message.Source, videoHash, thumbHash []byte, videoSize, thumbSize int64) (uint16, []byte) { 290 pb := c.buildPttShortVideoProto(target, videoHash, thumbHash, videoSize, thumbSize) 291 payload, _ := proto.Marshal(pb) 292 return c.uniPacket("PttCenterSvr.GroupShortVideoUpReq", payload) 293 } 294 295 // PttCenterSvr.pb_pttCenter_CMD_REQ_APPLY_UPLOAD-500 296 func (c *QQClient) buildC2CPttStoreBDHExt(target int64, md5 []byte, size, voiceLength int32) []byte { 297 seq := c.nextSeq() 298 req := &cmd0x346.C346ReqBody{ 299 Cmd: 500, 300 Seq: int32(seq), 301 ApplyUploadReq: &cmd0x346.ApplyUploadReq{ 302 SenderUin: c.Uin, 303 RecverUin: target, 304 FileType: 2, 305 FileSize: int64(size), 306 FileName: hex.EncodeToString(md5), 307 Bytes_10MMd5: md5, // 超过10M可能会炸 308 }, 309 BusinessId: 17, 310 ClientType: 104, 311 ExtensionReq: &cmd0x346.ExtensionReq{ 312 Id: 3, 313 PttFormat: 1, 314 NetType: 3, 315 VoiceType: 2, 316 PttTime: voiceLength, 317 }, 318 } 319 payload, _ := proto.Marshal(req) 320 return payload 321 } 322 323 // PttCenterSvr.ShortVideoDownReq 324 func decodePttShortVideoDownResponse(_ *QQClient, pkt *network.Packet) (any, error) { 325 rsp := pttcenter.ShortVideoRspBody{} 326 if err := proto.Unmarshal(pkt.Payload, &rsp); err != nil { 327 return nil, errors.Wrap(err, "failed to unmarshal protobuf message") 328 } 329 if rsp.PttShortVideoDownloadRsp == nil || rsp.PttShortVideoDownloadRsp.DownloadAddr == nil { 330 return nil, errors.New("resp error") 331 } 332 return rsp.PttShortVideoDownloadRsp.DownloadAddr.Host[0] + rsp.PttShortVideoDownloadRsp.DownloadAddr.UrlArgs, nil 333 } 334 335 // PttCenterSvr.GroupShortVideoUpReq 336 func decodeGroupShortVideoUploadResponse(_ *QQClient, pkt *network.Packet) (any, error) { 337 rsp := pttcenter.ShortVideoRspBody{} 338 if err := proto.Unmarshal(pkt.Payload, &rsp); err != nil { 339 return nil, errors.Wrap(err, "failed to unmarshal protobuf message") 340 } 341 if rsp.PttShortVideoUploadRsp == nil { 342 return nil, errors.New("resp error") 343 } 344 if rsp.PttShortVideoUploadRsp.RetCode != 0 { 345 return nil, errors.Errorf("ret code error: %v", rsp.PttShortVideoUploadRsp.RetCode) 346 } 347 return rsp.PttShortVideoUploadRsp, nil 348 }