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  }