1
0
mirror of https://github.com/Mrs4s/MiraiGo.git synced 2025-05-04 19:17:38 +08:00

client: new API UploadImage, UploadVoice and deprecate old API

This commit is contained in:
wdvxdr 2022-02-21 23:35:44 +08:00
parent 1e32793eef
commit 6b5e7d35f0
No known key found for this signature in database
GPG Key ID: 703F8C071DE7A1B6
4 changed files with 172 additions and 181 deletions

View File

@ -1,38 +1,21 @@
package client package client
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"image"
"io" "io"
"math/rand" "math/rand"
"strconv" "strconv"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/Mrs4s/MiraiGo/client/internal/highway"
"github.com/Mrs4s/MiraiGo/client/internal/network" "github.com/Mrs4s/MiraiGo/client/internal/network"
"github.com/Mrs4s/MiraiGo/client/pb/channel" "github.com/Mrs4s/MiraiGo/client/pb/channel"
"github.com/Mrs4s/MiraiGo/client/pb/cmd0x388" "github.com/Mrs4s/MiraiGo/client/pb/cmd0x388"
"github.com/Mrs4s/MiraiGo/client/pb/msg" "github.com/Mrs4s/MiraiGo/client/pb/msg"
"github.com/Mrs4s/MiraiGo/internal/proto" "github.com/Mrs4s/MiraiGo/internal/proto"
"github.com/Mrs4s/MiraiGo/message" "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() { func init() {
decoders["ImgStore.QQMeetPicUp"] = decodeGuildImageStoreResponse decoders["ImgStore.QQMeetPicUp"] = decodeGuildImageStoreResponse
} }
@ -101,7 +84,7 @@ func (s *GuildService) QueryImage(guildId, channelId uint64, hash []byte, size u
if err != nil { if err != nil {
return nil, errors.Wrap(err, "send packet error") return nil, errors.Wrap(err, "send packet error")
} }
body := rsp.(*guildImageUploadResponse) body := rsp.(*imageUploadResponse)
if body.IsExists { if body.IsExists {
return &message.GuildImageElement{ return &message.GuildImageElement{
FileId: body.FileId, 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") 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) { func (s *GuildService) UploadGuildImage(guildId, channelId uint64, img io.ReadSeeker) (*message.GuildImageElement, error) {
_, _ = img.Seek(0, io.SeekStart) // safe source := message.Source{
fh, length := utils.ComputeMd5AndLength(img) SourceType: message.SourceGuildChannel,
_, _ = img.Seek(0, io.SeekStart) PrimaryID: int64(guildId),
rsp, err := s.c.sendAndWait(s.c.buildGuildImageStorePacket(guildId, channelId, fh, uint64(length))) SecondaryID: int64(channelId),
}
image, err := s.c.uploadGroupOrGuildImage(source, img)
if err != nil { if err != nil {
return nil, err return nil, err
} }
body := rsp.(*guildImageUploadResponse) return image.(*message.GuildImageElement), nil
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
} }
func (s *GuildService) PullGuildChannelMessage(guildId, channelId, beginSeq, endSeq uint64) (r []*message.GuildChannelMessage, e error) { 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] rsp := body.TryupImgRsp[0]
if rsp.GetResult() != 0 { if rsp.GetResult() != 0 {
return &guildImageUploadResponse{ return &imageUploadResponse{
ResultCode: int32(rsp.GetResult()), ResultCode: int32(rsp.GetResult()),
Message: utils.B2S(rsp.FailMsg), Message: string(rsp.FailMsg),
}, nil }, nil
} }
if rsp.GetFileExit() { if rsp.GetFileExit() {
resp := &imageUploadResponse{
IsExists: true,
FileId: int64(rsp.GetFileid()),
DownloadIndex: string(rsp.DownloadIndex),
}
if rsp.ImgInfo != nil { 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.Width = int32(rsp.ImgInfo.GetFileWidth())
resp.Height = int32(rsp.ImgInfo.GetFileHeight())
} }
return &guildImageUploadResponse{IsExists: true, FileId: int64(rsp.GetFileid()), DownloadIndex: string(rsp.DownloadIndex)}, nil return rsp, nil
} }
return &guildImageUploadResponse{ return &imageUploadResponse{
FileId: int64(rsp.GetFileid()), FileId: int64(rsp.GetFileid()),
UploadKey: rsp.UpUkey, UploadKey: rsp.UpUkey,
UploadIp: rsp.UpIp, UploadIp: rsp.UpIp,

View File

@ -34,16 +34,41 @@ type imageUploadResponse struct {
UploadKey []byte UploadKey []byte
UploadIp []uint32 UploadIp []uint32
UploadPort []uint32 UploadPort []uint32
ResourceId string
Message string
FileId int64
Width int32 Width int32
Height int32 Height int32
Message string
DownloadIndex string
ResourceId string
FileId int64
ResultCode int32 ResultCode int32
IsExists bool 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) { 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 _, _ = img.Seek(0, io.SeekStart) // safe
fh, length := utils.ComputeMd5AndLength(img) fh, length := utils.ComputeMd5AndLength(img)
_, _ = img.Seek(0, io.SeekStart) _, _ = img.Seek(0, io.SeekStart)
@ -56,9 +81,24 @@ func (c *QQClient) UploadGroupImage(groupCode int64, img io.ReadSeeker, thread .
if len(thread) > 0 { if len(thread) > 0 {
tc = 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)) var r interface{}
r, err := c.sendAndWait(seq, pkt) 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 { if err != nil {
return nil, err return nil, err
} }
@ -77,21 +117,19 @@ func (c *QQClient) UploadGroupImage(groupCode int64, img io.ReadSeeker, thread .
if tc > 1 && length > 3*1024*1024 { if tc > 1 && length > 3*1024*1024 {
_, err = c.highwaySession.UploadBDHMultiThread(highway.BdhMultiThreadInput{ _, err = c.highwaySession.UploadBDHMultiThread(highway.BdhMultiThreadInput{
CommandID: 2, CommandID: cmd,
Body: utils.ReaderAtFrom2ReadSeeker(img, nil), Body: utils.ReaderAtFrom2ReadSeeker(img, nil),
Size: length, Size: length,
Sum: fh, Sum: fh,
Ticket: rsp.UploadKey, Ticket: rsp.UploadKey,
Ext: EmptyBytes, Ext: ext,
Encrypt: false,
}, 4) }, 4)
} else { } else {
_, err = c.highwaySession.UploadBDH(highway.BdhInput{ _, err = c.highwaySession.UploadBDH(highway.BdhInput{
CommandID: 2, CommandID: cmd,
Body: img, Body: img,
Ticket: rsp.UploadKey, Ticket: rsp.UploadKey,
Ext: EmptyBytes, Ext: ext,
Encrypt: false,
}) })
} }
if err != nil { if err != nil {
@ -104,9 +142,33 @@ ok:
if t == "gif" { if t == "gif" {
imageType = 2000 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) { func (c *QQClient) UploadPrivateImage(target int64, img io.ReadSeeker) (*message.FriendImageElement, error) {
return c.uploadPrivateImage(target, img, 0) 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) _, _ = img.Seek(0, io.SeekStart)
e, err := c.QueryFriendImage(target, fh, int32(length)) e, err := c.QueryFriendImage(target, fh, int32(length))
if errors.Is(err, ErrNotExists) { if errors.Is(err, ErrNotExists) {
groupSource := message.Source{
SourceType: message.SourceGroup,
PrimaryID: target,
}
// use group highway upload and query again for image id. // 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 return nil, err
} }
if count >= 5 { if count >= 5 {

View File

@ -1,7 +1,6 @@
package client package client
import ( import (
"crypto/md5"
"encoding/hex" "encoding/hex"
"io" "io"
@ -26,24 +25,35 @@ func init() {
var pttWaiter = utils.NewUploadWaiter() 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(2) // tlv count
{
w.WriteByte(8) w.WriteByte(8)
w.WriteUInt16(4) w.WriteUInt16(4)
w.WriteUInt32(1) // codec w.WriteUInt32(1) // codec
}
{
w.WriteByte(9) w.WriteByte(9)
w.WriteUInt16(4) w.WriteUInt16(4)
w.WriteUInt32(0) // 时长 w.WriteUInt32(0) // 时长
}
w.WriteByte(10) w.WriteByte(10)
reserveInfo := []byte{0x08, 0x00, 0x28, 0x00, 0x38, 0x00} // todo reserveInfo := []byte{0x08, 0x00, 0x28, 0x00, 0x38, 0x00} // todo
w.WriteBytesShort(reserveInfo) 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) fh, length := utils.ComputeMd5AndLength(voice)
_, _ = voice.Seek(0, io.SeekStart) _, _ = voice.Seek(0, io.SeekStart)
@ -51,9 +61,18 @@ func (c *QQClient) UploadGroupPtt(groupCode int64, voice io.ReadSeeker) (*messag
pttWaiter.Wait(key) pttWaiter.Wait(key)
defer pttWaiter.Done(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{ rsp, err := c.highwaySession.UploadBDH(highway.BdhInput{
CommandID: 29, CommandID: cmd,
Body: voice, Body: voice,
Ticket: c.highwaySession.SigSession, Ticket: c.highwaySession.SigSession,
Ext: ext, Ext: ext,
@ -65,6 +84,15 @@ func (c *QQClient) UploadGroupPtt(groupCode int64, voice io.ReadSeeker) (*messag
if len(rsp) == 0 { if len(rsp) == 0 {
return nil, errors.New("miss rsp") return nil, errors.New("miss rsp")
} }
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 target.SourceType == message.SourceGroup {
pkt := cmd0x388.D388RspBody{} pkt := cmd0x388.D388RspBody{}
if err = proto.Unmarshal(rsp, &pkt); err != nil { if err = proto.Unmarshal(rsp, &pkt); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal protobuf message") return nil, errors.Wrap(err, "failed to unmarshal protobuf message")
@ -72,45 +100,10 @@ func (c *QQClient) UploadGroupPtt(groupCode int64, voice io.ReadSeeker) (*messag
if len(pkt.TryupPttRsp) == 0 { if len(pkt.TryupPttRsp) == 0 {
return nil, errors.New("miss try up rsp") return nil, errors.New("miss try up rsp")
} }
return &message.GroupVoiceElement{ ptt.PbReserve = []byte{8, 0, 40, 0, 56, 0}
Ptt: &msg.Ptt{ ptt.GroupFileKey = pkt.TryupPttRsp[0].FileKey
FileType: proto.Int32(4), return &message.GroupVoiceElement{Ptt: ptt}, nil
SrcUin: &c.Uin, } else {
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{} pkt := cmd0x346.C346RspBody{}
if err = proto.Unmarshal(rsp, &pkt); err != nil { if err = proto.Unmarshal(rsp, &pkt); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal protobuf message") return nil, errors.Wrap(err, "failed to unmarshal protobuf message")
@ -118,18 +111,10 @@ func (c *QQClient) UploadPrivatePtt(target int64, voice io.ReadSeeker) (*message
if pkt.ApplyUploadRsp == nil { if pkt.ApplyUploadRsp == nil {
return nil, errors.New("miss apply upload rsp") return nil, errors.New("miss apply upload rsp")
} }
return &message.PrivateVoiceElement{ ptt.FileUuid = pkt.ApplyUploadRsp.Uuid
Ptt: &msg.Ptt{ ptt.Reserve = c2cPttExtraInfo()
FileType: proto.Int32(4), return &message.PrivateVoiceElement{Ptt: ptt}, nil
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 可直接发送 // UploadShortVideo 将视频和封面上传到服务器, 返回 message.ShortVideoElement 可直接发送

View File

@ -28,10 +28,7 @@ type GroupVoiceElement struct {
Ptt *msg.Ptt Ptt *msg.Ptt
} }
type PrivateVoiceElement struct { type PrivateVoiceElement = GroupVoiceElement
Data []byte
Ptt *msg.Ptt
}
type FaceElement struct { type FaceElement struct {
Index int32 Index int32
@ -256,10 +253,6 @@ func (e *GroupVoiceElement) Type() ElementType {
return Voice return Voice
} }
func (e *PrivateVoiceElement) Type() ElementType {
return Voice
}
func (e *VoiceElement) Type() ElementType { func (e *VoiceElement) Type() ElementType {
return Voice return Voice
} }