diff --git a/client/guild_msg.go b/client/guild_msg.go index b7b7f412..fe0bd6a8 100644 --- a/client/guild_msg.go +++ b/client/guild_msg.go @@ -1,38 +1,21 @@ package client import ( - "bytes" "encoding/hex" - "image" "io" "math/rand" "strconv" "github.com/pkg/errors" - "github.com/Mrs4s/MiraiGo/client/internal/highway" "github.com/Mrs4s/MiraiGo/client/internal/network" "github.com/Mrs4s/MiraiGo/client/pb/channel" "github.com/Mrs4s/MiraiGo/client/pb/cmd0x388" "github.com/Mrs4s/MiraiGo/client/pb/msg" "github.com/Mrs4s/MiraiGo/internal/proto" "github.com/Mrs4s/MiraiGo/message" - "github.com/Mrs4s/MiraiGo/utils" ) -type guildImageUploadResponse struct { - UploadKey []byte - UploadIp []uint32 - UploadPort []uint32 - Width int32 - Height int32 - Message string - DownloadIndex string - FileId int64 - ResultCode int32 - IsExists bool -} - func init() { decoders["ImgStore.QQMeetPicUp"] = decodeGuildImageStoreResponse } @@ -101,7 +84,7 @@ func (s *GuildService) QueryImage(guildId, channelId uint64, hash []byte, size u if err != nil { return nil, errors.Wrap(err, "send packet error") } - body := rsp.(*guildImageUploadResponse) + body := rsp.(*imageUploadResponse) if body.IsExists { return &message.GuildImageElement{ FileId: body.FileId, @@ -116,60 +99,18 @@ func (s *GuildService) QueryImage(guildId, channelId uint64, hash []byte, size u return nil, errors.New("image is not exists") } +// Deprecated: use QQClient.UploadImage instead func (s *GuildService) UploadGuildImage(guildId, channelId uint64, img io.ReadSeeker) (*message.GuildImageElement, error) { - _, _ = img.Seek(0, io.SeekStart) // safe - fh, length := utils.ComputeMd5AndLength(img) - _, _ = img.Seek(0, io.SeekStart) - rsp, err := s.c.sendAndWait(s.c.buildGuildImageStorePacket(guildId, channelId, fh, uint64(length))) + source := message.Source{ + SourceType: message.SourceGuildChannel, + PrimaryID: int64(guildId), + SecondaryID: int64(channelId), + } + image, err := s.c.uploadGroupOrGuildImage(source, img) if err != nil { return nil, err } - body := rsp.(*guildImageUploadResponse) - if body.IsExists { - goto ok - } - if s.c.highwaySession.AddrLength() == 0 { - for i, addr := range body.UploadIp { - s.c.highwaySession.AppendAddr(addr, body.UploadPort[i]) - } - } - if _, err = s.c.highwaySession.UploadBDH(highway.BdhInput{ - CommandID: 83, - Body: img, - Ticket: body.UploadKey, - Ext: proto.DynamicMessage{11: guildId, 12: channelId}.Encode(), - Encrypt: false, - }); err == nil { - goto ok - } - return nil, errors.Wrap(err, "highway upload error") -ok: - _, _ = img.Seek(0, io.SeekStart) - i, _, err := image.DecodeConfig(img) - var imageType int32 = 1000 - _, _ = img.Seek(0, io.SeekStart) - tmp := make([]byte, 4) - _, _ = img.Read(tmp) - if bytes.Equal(tmp, []byte{0x47, 0x49, 0x46, 0x38}) { - imageType = 2000 - } - width := int32(i.Width) - height := int32(i.Height) - if err != nil { - s.c.Warning("waring: decode image error: %v. this image will be displayed by wrong size in pc guild client", err) - width = 200 - height = 200 - } - return &message.GuildImageElement{ - FileId: body.FileId, - FilePath: hex.EncodeToString(fh) + ".jpg", - Size: int32(length), - DownloadIndex: body.DownloadIndex, - Width: width, - Height: height, - ImageType: imageType, - Md5: fh, - }, nil + return image.(*message.GuildImageElement), nil } func (s *GuildService) PullGuildChannelMessage(guildId, channelId, beginSeq, endSeq uint64) (r []*message.GuildChannelMessage, e error) { @@ -299,18 +240,24 @@ func decodeGuildImageStoreResponse(_ *QQClient, _ *network.IncomingPacketInfo, p } rsp := body.TryupImgRsp[0] if rsp.GetResult() != 0 { - return &guildImageUploadResponse{ + return &imageUploadResponse{ ResultCode: int32(rsp.GetResult()), - Message: utils.B2S(rsp.FailMsg), + Message: string(rsp.FailMsg), }, nil } if rsp.GetFileExit() { - if rsp.ImgInfo != nil { - return &guildImageUploadResponse{IsExists: true, FileId: int64(rsp.GetFileid()), DownloadIndex: string(rsp.DownloadIndex), Width: int32(rsp.ImgInfo.GetFileWidth()), Height: int32(rsp.ImgInfo.GetFileHeight())}, nil + resp := &imageUploadResponse{ + IsExists: true, + FileId: int64(rsp.GetFileid()), + DownloadIndex: string(rsp.DownloadIndex), } - return &guildImageUploadResponse{IsExists: true, FileId: int64(rsp.GetFileid()), DownloadIndex: string(rsp.DownloadIndex)}, nil + if rsp.ImgInfo != nil { + resp.Width = int32(rsp.ImgInfo.GetFileWidth()) + resp.Height = int32(rsp.ImgInfo.GetFileHeight()) + } + return rsp, nil } - return &guildImageUploadResponse{ + return &imageUploadResponse{ FileId: int64(rsp.GetFileid()), UploadKey: rsp.UpUkey, UploadIp: rsp.UpIp, diff --git a/client/image.go b/client/image.go index a382ce13..3806b493 100644 --- a/client/image.go +++ b/client/image.go @@ -31,19 +31,44 @@ func init() { var imgWaiter = utils.NewUploadWaiter() type imageUploadResponse struct { - UploadKey []byte - UploadIp []uint32 - UploadPort []uint32 - ResourceId string - Message string - FileId int64 - Width int32 - Height int32 - ResultCode int32 - IsExists bool + UploadKey []byte + UploadIp []uint32 + UploadPort []uint32 + Width int32 + Height int32 + Message string + DownloadIndex string + ResourceId string + FileId int64 + ResultCode int32 + IsExists bool } +func (c *QQClient) UploadImage(target message.Source, img io.ReadSeeker, thread ...int) (message.IMessageElement, error) { + switch target.SourceType { + case message.SourceGroup, message.SourceGuildChannel, message.SourceGuildDirect: + return c.uploadGroupOrGuildImage(target, img, thread...) + case message.SourcePrivate: + return c.uploadPrivateImage(target.PrimaryID, img, 0) + default: + return nil, errors.New("unsupported target type") + } +} + +// Deprecated: use UploadImage instead func (c *QQClient) UploadGroupImage(groupCode int64, img io.ReadSeeker, thread ...int) (*message.GroupImageElement, error) { + source := message.Source{ + SourceType: message.SourceGroup, + PrimaryID: groupCode, + } + x, err := c.UploadImage(source, img, thread...) + if err != nil { + return nil, err + } + return x.(*message.GroupImageElement), nil +} + +func (c *QQClient) uploadGroupOrGuildImage(target message.Source, img io.ReadSeeker, thread ...int) (message.IMessageElement, error) { _, _ = img.Seek(0, io.SeekStart) // safe fh, length := utils.ComputeMd5AndLength(img) _, _ = img.Seek(0, io.SeekStart) @@ -56,9 +81,24 @@ func (c *QQClient) UploadGroupImage(groupCode int64, img io.ReadSeeker, thread . if len(thread) > 0 { tc = thread[0] } + cmd := int32(2) + ext := EmptyBytes + if target.SourceType != message.SourceGroup { // guild + cmd = 83 + ext = proto.DynamicMessage{ + 11: uint64(target.PrimaryID), + 12: uint64(target.SecondaryID), + }.Encode() + } - seq, pkt := c.buildGroupImageStorePacket(groupCode, fh, int32(length)) - r, err := c.sendAndWait(seq, pkt) + var r interface{} + var err error + switch target.SourceType { + case message.SourceGroup: + r, err = c.sendAndWait(c.buildGroupImageStorePacket(target.PrimaryID, fh, int32(length))) + case message.SourceGuildChannel, message.SourceGuildDirect: + r, err = c.sendAndWait(c.buildGuildImageStorePacket(uint64(target.PrimaryID), uint64(target.SecondaryID), fh, uint64(length))) + } if err != nil { return nil, err } @@ -77,21 +117,19 @@ func (c *QQClient) UploadGroupImage(groupCode int64, img io.ReadSeeker, thread . if tc > 1 && length > 3*1024*1024 { _, err = c.highwaySession.UploadBDHMultiThread(highway.BdhMultiThreadInput{ - CommandID: 2, + CommandID: cmd, Body: utils.ReaderAtFrom2ReadSeeker(img, nil), Size: length, Sum: fh, Ticket: rsp.UploadKey, - Ext: EmptyBytes, - Encrypt: false, + Ext: ext, }, 4) } else { _, err = c.highwaySession.UploadBDH(highway.BdhInput{ - CommandID: 2, + CommandID: cmd, Body: img, Ticket: rsp.UploadKey, - Ext: EmptyBytes, - Encrypt: false, + Ext: ext, }) } if err != nil { @@ -104,9 +142,33 @@ ok: if t == "gif" { imageType = 2000 } - return message.NewGroupImage(binary.CalculateImageResourceId(fh), fh, rsp.FileId, int32(length), int32(i.Width), int32(i.Height), imageType), nil + width := int32(i.Width) + height := int32(i.Height) + if err != nil && target.SourceType != message.SourceGroup { + c.Warning("waring: decode image error: %v. this image will be displayed by wrong size in pc guild client", err) + width = 200 + height = 200 + } + if target.SourceType == message.SourceGroup { + return message.NewGroupImage( + binary.CalculateImageResourceId(fh), + fh, rsp.FileId, int32(length), + int32(i.Width), int32(i.Height), imageType, + ), nil + } + return &message.GuildImageElement{ + FileId: rsp.FileId, + FilePath: hex.EncodeToString(fh) + ".jpg", + Size: int32(length), + DownloadIndex: rsp.DownloadIndex, + Width: width, + Height: height, + ImageType: imageType, + Md5: fh, + }, nil } +// Deprecated: use UploadImage instead func (c *QQClient) UploadPrivateImage(target int64, img io.ReadSeeker) (*message.FriendImageElement, error) { return c.uploadPrivateImage(target, img, 0) } @@ -126,8 +188,12 @@ func (c *QQClient) uploadPrivateImage(target int64, img io.ReadSeeker, count int _, _ = img.Seek(0, io.SeekStart) e, err := c.QueryFriendImage(target, fh, int32(length)) if errors.Is(err, ErrNotExists) { + groupSource := message.Source{ + SourceType: message.SourceGroup, + PrimaryID: target, + } // use group highway upload and query again for image id. - if _, err = c.UploadGroupImage(target, img); err != nil { + if _, err = c.uploadGroupOrGuildImage(groupSource, img); err != nil { return nil, err } if count >= 5 { diff --git a/client/ptt.go b/client/ptt.go index 68485467..0d3e7fdf 100644 --- a/client/ptt.go +++ b/client/ptt.go @@ -1,7 +1,6 @@ package client import ( - "crypto/md5" "encoding/hex" "io" @@ -26,24 +25,35 @@ func init() { var pttWaiter = utils.NewUploadWaiter() -var c2cPttExtraInfo = binary.NewWriterF(func(w *binary.Writer) { +func c2cPttExtraInfo() []byte { + w := binary.SelectWriter() + defer binary.PutWriter(w) w.WriteByte(2) // tlv count - - w.WriteByte(8) - w.WriteUInt16(4) - w.WriteUInt32(1) // codec - - w.WriteByte(9) - w.WriteUInt16(4) - w.WriteUInt32(0) // 时长 - + { + w.WriteByte(8) + w.WriteUInt16(4) + w.WriteUInt32(1) // codec + } + { + w.WriteByte(9) + w.WriteUInt16(4) + w.WriteUInt32(0) // 时长 + } w.WriteByte(10) reserveInfo := []byte{0x08, 0x00, 0x28, 0x00, 0x38, 0x00} // todo w.WriteBytesShort(reserveInfo) -}) + return append([]byte(nil), w.Bytes()...) +} + +// UploadVoice 将语音数据使用群语音通道上传到服务器, 返回 message.GroupVoiceElement 可直接发送 +func (c *QQClient) UploadVoice(target message.Source, voice io.ReadSeeker) (*message.GroupVoiceElement, error) { + switch target.SourceType { + case message.SourceGroup, message.SourcePrivate: + // ok + default: + return nil, errors.New("unsupported source type") + } -// UploadGroupPtt 将语音数据使用群语音通道上传到服务器, 返回 message.GroupVoiceElement 可直接发送 -func (c *QQClient) UploadGroupPtt(groupCode int64, voice io.ReadSeeker) (*message.GroupVoiceElement, error) { fh, length := utils.ComputeMd5AndLength(voice) _, _ = voice.Seek(0, io.SeekStart) @@ -51,9 +61,18 @@ func (c *QQClient) UploadGroupPtt(groupCode int64, voice io.ReadSeeker) (*messag pttWaiter.Wait(key) defer pttWaiter.Done(key) - ext := c.buildGroupPttStoreBDHExt(groupCode, fh, int32(length), 0, int32(length)) + var cmd int32 + var ext []byte + if target.SourceType == message.SourcePrivate { + cmd = int32(26) + ext = c.buildC2CPttStoreBDHExt(target.PrimaryID, fh, int32(length), int32(length)) + } else { + cmd = int32(29) + ext = c.buildGroupPttStoreBDHExt(target.PrimaryID, fh, int32(length), 0, int32(length)) + } + // multi-thread upload is no need rsp, err := c.highwaySession.UploadBDH(highway.BdhInput{ - CommandID: 29, + CommandID: cmd, Body: voice, Ticket: c.highwaySession.SigSession, Ext: ext, @@ -65,71 +84,37 @@ func (c *QQClient) UploadGroupPtt(groupCode int64, voice io.ReadSeeker) (*messag if len(rsp) == 0 { return nil, errors.New("miss rsp") } - pkt := cmd0x388.D388RspBody{} - if err = proto.Unmarshal(rsp, &pkt); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal protobuf message") + ptt := &msg.Ptt{ + FileType: proto.Int32(4), + SrcUin: &c.Uin, + FileMd5: fh, + FileName: proto.String(hex.EncodeToString(fh) + ".amr"), + FileSize: proto.Int32(int32(length)), + BoolValid: proto.Bool(true), } - if len(pkt.TryupPttRsp) == 0 { - return nil, errors.New("miss try up rsp") + if target.SourceType == message.SourceGroup { + pkt := cmd0x388.D388RspBody{} + if err = proto.Unmarshal(rsp, &pkt); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal protobuf message") + } + if len(pkt.TryupPttRsp) == 0 { + return nil, errors.New("miss try up rsp") + } + ptt.PbReserve = []byte{8, 0, 40, 0, 56, 0} + ptt.GroupFileKey = pkt.TryupPttRsp[0].FileKey + return &message.GroupVoiceElement{Ptt: ptt}, nil + } else { + pkt := cmd0x346.C346RspBody{} + if err = proto.Unmarshal(rsp, &pkt); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal protobuf message") + } + if pkt.ApplyUploadRsp == nil { + return nil, errors.New("miss apply upload rsp") + } + ptt.FileUuid = pkt.ApplyUploadRsp.Uuid + ptt.Reserve = c2cPttExtraInfo() + return &message.PrivateVoiceElement{Ptt: ptt}, nil } - return &message.GroupVoiceElement{ - Ptt: &msg.Ptt{ - FileType: proto.Int32(4), - SrcUin: &c.Uin, - FileMd5: fh, - FileName: proto.String(hex.EncodeToString(fh) + ".amr"), - FileSize: proto.Int32(int32(length)), - GroupFileKey: pkt.TryupPttRsp[0].FileKey, - BoolValid: proto.Bool(true), - PbReserve: []byte{8, 0, 40, 0, 56, 0}, - }, - }, nil -} - -// UploadPrivatePtt 将语音数据使用好友语音通道上传到服务器, 返回 message.PrivateVoiceElement 可直接发送 -func (c *QQClient) UploadPrivatePtt(target int64, voice io.ReadSeeker) (*message.PrivateVoiceElement, error) { - h := md5.New() - length, _ := io.Copy(h, voice) - fh := h.Sum(nil) - _, _ = voice.Seek(0, io.SeekStart) - - key := hex.EncodeToString(fh) - pttWaiter.Wait(key) - defer pttWaiter.Done(key) - - ext := c.buildC2CPttStoreBDHExt(target, fh, int32(length), int32(length)) - rsp, err := c.highwaySession.UploadBDH(highway.BdhInput{ - CommandID: 26, - Body: voice, - Ticket: c.highwaySession.SigSession, - Ext: ext, - Encrypt: false, - }) - if err != nil { - return nil, err - } - if len(rsp) == 0 { - return nil, errors.New("miss rsp") - } - pkt := cmd0x346.C346RspBody{} - if err = proto.Unmarshal(rsp, &pkt); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal protobuf message") - } - if pkt.ApplyUploadRsp == nil { - return nil, errors.New("miss apply upload rsp") - } - return &message.PrivateVoiceElement{ - Ptt: &msg.Ptt{ - FileType: proto.Int32(4), - SrcUin: &c.Uin, - FileUuid: pkt.ApplyUploadRsp.Uuid, - FileMd5: fh, - FileName: proto.String(hex.EncodeToString(fh) + ".amr"), - FileSize: proto.Int32(int32(length)), - Reserve: c2cPttExtraInfo, - BoolValid: proto.Bool(true), - }, - }, nil } // UploadShortVideo 将视频和封面上传到服务器, 返回 message.ShortVideoElement 可直接发送 diff --git a/message/elements.go b/message/elements.go index 09e48915..c1d02ee0 100644 --- a/message/elements.go +++ b/message/elements.go @@ -28,10 +28,7 @@ type GroupVoiceElement struct { Ptt *msg.Ptt } -type PrivateVoiceElement struct { - Data []byte - Ptt *msg.Ptt -} +type PrivateVoiceElement = GroupVoiceElement type FaceElement struct { Index int32 @@ -256,10 +253,6 @@ func (e *GroupVoiceElement) Type() ElementType { return Voice } -func (e *PrivateVoiceElement) Type() ElementType { - return Voice -} - func (e *VoiceElement) Type() ElementType { return Voice }