diff --git a/client/entities.go b/client/entities.go index bfbd5c65..effd05ef 100644 --- a/client/entities.go +++ b/client/entities.go @@ -265,19 +265,6 @@ type ( list []*GroupMemberInfo } - imageUploadResponse struct { - UploadKey []byte - UploadIp []uint32 - UploadPort []uint32 - ResourceId string - Message string - FileId int64 - Width int32 - Height int32 - ResultCode int32 - IsExists bool - } - pttUploadResponse struct { ResultCode int32 Message string diff --git a/client/guild_eventflow.go b/client/guild_eventflow.go index 7515d198..863be8b2 100644 --- a/client/guild_eventflow.go +++ b/client/guild_eventflow.go @@ -1,14 +1,13 @@ package client import ( - "sync" - "time" - "github.com/Mrs4s/MiraiGo/client/pb/channel" "github.com/Mrs4s/MiraiGo/client/pb/msg" "github.com/Mrs4s/MiraiGo/internal/packets" "github.com/pkg/errors" "google.golang.org/protobuf/proto" + "sync" + "time" ) func init() { @@ -19,6 +18,13 @@ var ( updateChanLock sync.Mutex ) +type tipsPushInfo struct { + TinyId uint64 + TargetMessageSenderUin int64 + GuildId uint64 + ChannelId uint64 +} + func decodeGuildEventFlowPacket(c *QQClient, _ *incomingPacketInfo, payload []byte) (interface{}, error) { push := new(channel.MsgOnlinePush) if err := proto.Unmarshal(payload, push); err != nil { @@ -26,12 +32,6 @@ func decodeGuildEventFlowPacket(c *QQClient, _ *incomingPacketInfo, payload []by } for _, m := range push.Msgs { if m.Head.ContentHead.GetType() == 3841 { - type tipsPushInfo struct { - TinyId uint64 - TargetMessageSenderUin int64 - GuildId uint64 - ChannelId uint64 - } // todo: 回头 event flow 的处理移出去重构下逻辑, 先暂时这样方便改 var common *msg.CommonElem if m.Body != nil { @@ -64,68 +64,7 @@ func decodeGuildEventFlowPacket(c *QQClient, _ *incomingPacketInfo, payload []by c.Error("failed to unmarshal guild channel event body: %v", err) continue } - if eventBody.ChangeChanInfo != nil { - updateChanLock.Lock() - defer updateChanLock.Unlock() - guild := c.GuildService.FindGuild(m.Head.RoutingHead.GetGuildId()) - oldInfo := guild.FindChannel(eventBody.ChangeChanInfo.GetChanId()) - if time.Now().Unix()-oldInfo.fetchTime <= 2 { - continue - } - newInfo, err := c.GuildService.FetchChannelInfo(m.Head.RoutingHead.GetGuildId(), eventBody.ChangeChanInfo.GetChanId()) - if err != nil { - c.Error("error to decode channel info updated event: fetch channel info failed: %v", err) - continue - } - for i := range guild.Channels { - if guild.Channels[i].ChannelId == newInfo.ChannelId { - guild.Channels[i] = newInfo - break - } - } - c.dispatchGuildChannelUpdatedEvent(&GuildChannelUpdatedEvent{ - OperatorId: m.Head.RoutingHead.GetFromTinyid(), - GuildId: m.Head.RoutingHead.GetGuildId(), - ChannelId: eventBody.ChangeChanInfo.GetChanId(), - OldChannelInfo: oldInfo, - NewChannelInfo: newInfo, - }) - continue - } - if eventBody.UpdateMsg != nil { - if eventBody.UpdateMsg.GetEventType() == 1 || eventBody.UpdateMsg.GetEventType() == 2 { // todo: 撤回消息 - continue - } - if eventBody.UpdateMsg.GetEventType() == 4 { // 消息贴表情更新 (包含添加或删除) - t, err := c.GuildService.pullRoamMsgByEventFlow(m.Head.RoutingHead.GetGuildId(), m.Head.RoutingHead.GetChannelId(), eventBody.UpdateMsg.GetMsgSeq(), eventBody.UpdateMsg.GetMsgSeq(), eventBody.UpdateMsg.GetEventVersion()-1) - if err != nil || len(t) == 0 { - c.Error("process guild event flow error: pull eventMsg message error: %v", err) - continue - } - // 自己的消息被贴表情会单独推送一个tips, 这里不需要解析 - if t[0].Head.RoutingHead.GetFromTinyid() == c.GuildService.TinyId { - continue - } - updatedEvent := &GuildMessageReactionsUpdatedEvent{ - GuildId: m.Head.RoutingHead.GetGuildId(), - ChannelId: m.Head.RoutingHead.GetChannelId(), - MessageId: t[0].Head.ContentHead.GetSeq(), - CurrentReactions: decodeGuildMessageEmojiReactions(t[0]), - } - tipsInfo, err := c.waitPacketTimeoutSyncF("MsgPush.PushGroupProMsg", time.Second, func(i interface{}) bool { - if i == nil { - return false - } - _, ok := i.(*tipsPushInfo) - return ok - }) - if err == nil { - updatedEvent.OperatorId = tipsInfo.(*tipsPushInfo).TinyId - updatedEvent.MessageSenderUin = tipsInfo.(*tipsPushInfo).TargetMessageSenderUin - } - c.dispatchGuildMessageReactionsUpdatedEvent(updatedEvent) - } - } + c.processGuildEventBody(m, eventBody) continue } if cm := c.parseGuildChannelMessage(m); cm != nil { @@ -135,6 +74,70 @@ func decodeGuildEventFlowPacket(c *QQClient, _ *incomingPacketInfo, payload []by return nil, nil } +func (c *QQClient) processGuildEventBody(m *channel.ChannelMsgContent, eventBody *channel.EventBody) { + switch { + case eventBody.ChangeChanInfo != nil: + updateChanLock.Lock() + defer updateChanLock.Unlock() + guild := c.GuildService.FindGuild(m.Head.RoutingHead.GetGuildId()) + oldInfo := guild.FindChannel(eventBody.ChangeChanInfo.GetChanId()) + if time.Now().Unix()-oldInfo.fetchTime <= 2 { + return + } + newInfo, err := c.GuildService.FetchChannelInfo(m.Head.RoutingHead.GetGuildId(), eventBody.ChangeChanInfo.GetChanId()) + if err != nil { + c.Error("error to decode channel info updated event: fetch channel info failed: %v", err) + return + } + for i := range guild.Channels { + if guild.Channels[i].ChannelId == newInfo.ChannelId { + guild.Channels[i] = newInfo + break + } + } + c.dispatchGuildChannelUpdatedEvent(&GuildChannelUpdatedEvent{ + OperatorId: m.Head.RoutingHead.GetFromTinyid(), + GuildId: m.Head.RoutingHead.GetGuildId(), + ChannelId: eventBody.ChangeChanInfo.GetChanId(), + OldChannelInfo: oldInfo, + NewChannelInfo: newInfo, + }) + case eventBody.UpdateMsg != nil: + if eventBody.UpdateMsg.GetEventType() == 1 || eventBody.UpdateMsg.GetEventType() == 2 { // todo: 撤回消息 + return + } + if eventBody.UpdateMsg.GetEventType() == 4 { // 消息贴表情更新 (包含添加或删除) + t, err := c.GuildService.pullRoamMsgByEventFlow(m.Head.RoutingHead.GetGuildId(), m.Head.RoutingHead.GetChannelId(), eventBody.UpdateMsg.GetMsgSeq(), eventBody.UpdateMsg.GetMsgSeq(), eventBody.UpdateMsg.GetEventVersion()-1) + if err != nil || len(t) == 0 { + c.Error("process guild event flow error: pull eventMsg message error: %v", err) + return + } + // 自己的消息被贴表情会单独推送一个tips, 这里不需要解析 + if t[0].Head.RoutingHead.GetFromTinyid() == c.GuildService.TinyId { + return + } + updatedEvent := &GuildMessageReactionsUpdatedEvent{ + GuildId: m.Head.RoutingHead.GetGuildId(), + ChannelId: m.Head.RoutingHead.GetChannelId(), + MessageId: t[0].Head.ContentHead.GetSeq(), + CurrentReactions: decodeGuildMessageEmojiReactions(t[0]), + } + tipsInfo, err := c.waitPacketTimeoutSyncF("MsgPush.PushGroupProMsg", time.Second, func(i interface{}) bool { + if i == nil { + return false + } + _, ok := i.(*tipsPushInfo) + return ok + }) + if err == nil { + updatedEvent.OperatorId = tipsInfo.(*tipsPushInfo).TinyId + updatedEvent.MessageSenderUin = tipsInfo.(*tipsPushInfo).TargetMessageSenderUin + } + c.dispatchGuildMessageReactionsUpdatedEvent(updatedEvent) + } + } +} + func (s *GuildService) pullRoamMsgByEventFlow(guildId, channelId, beginSeq, endSeq, eventVersion uint64) ([]*channel.ChannelMsgContent, error) { payload, _ := proto.Marshal(&channel.ChannelMsgReq{ ChannelParam: &channel.ChannelParam{ diff --git a/client/guild_msg.go b/client/guild_msg.go index cffa977e..2aa51c05 100644 --- a/client/guild_msg.go +++ b/client/guild_msg.go @@ -1,7 +1,13 @@ package client import ( + "bytes" "encoding/hex" + "fmt" + "github.com/Mrs4s/MiraiGo/binary" + "github.com/Mrs4s/MiraiGo/utils" + "image" + "io" "math/rand" "strconv" @@ -16,6 +22,25 @@ import ( "google.golang.org/protobuf/proto" ) +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 +} + func (s *GuildService) SendGuildChannelMessage(guildId, channelId uint64, m *message.SendingMessage) error { mr := rand.Uint32() // 客户端似乎是生成的 u32 虽然类型是u64 req := &channel.DF62ReqBody{Msg: &channel.ChannelMsgContent{ @@ -55,14 +80,84 @@ func (s *GuildService) SendGuildChannelMessage(guildId, channelId uint64, m *mes } func (s *GuildService) QueryImage(guildId, channelId uint64, hash []byte, size uint64) (*message.GuildImageElement, error) { - seq := s.c.nextSeq() + rsp, err := s.c.sendAndWait(s.c.buildGuildImageStorePacket(guildId, channelId, hash, size)) + if err != nil { + return nil, errors.Wrap(err, "send packet error") + } + body := rsp.(*guildImageUploadResponse) + if body.IsExists { + return &message.GuildImageElement{ + FileId: body.FileId, + FilePath: hex.EncodeToString(hash) + ".jpg", + Size: int32(size), + DownloadIndex: body.DownloadIndex, + Width: body.Width, + Height: body.Height, + Md5: hash, + }, nil + } + return nil, errors.New("image is not exists") +} + +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))) + if err != nil { + return nil, err + } + body := rsp.(*guildImageUploadResponse) + if body.IsExists { + goto ok + } + if len(s.c.srvSsoAddrs) == 0 { + for i, addr := range body.UploadIp { + s.c.srvSsoAddrs = append(s.c.srvSsoAddrs, fmt.Sprintf("%v:%v", binary.UInt32ToIPV4Address(addr), body.UploadPort[i])) + } + } + if _, err = s.c.highwayUploadByBDH(img, length, 83, body.UploadKey, fh, binary.DynamicProtoMessage{11: guildId, 12: channelId}.Encode(), 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 (c *QQClient) buildGuildImageStorePacket(guildId, channelId uint64, hash []byte, size uint64) (uint16, []byte) { + seq := c.nextSeq() payload, _ := proto.Marshal(&cmd0x388.D388ReqBody{ NetType: proto.Uint32(3), Subcmd: proto.Uint32(1), TryupImgReq: []*cmd0x388.TryUpImgReq{ { GroupCode: &channelId, - SrcUin: proto.Uint64(uint64(s.c.Uin)), + SrcUin: proto.Uint64(uint64(c.Uin)), FileId: proto.Uint64(0), FileMd5: hash, FileSize: &size, @@ -80,28 +175,8 @@ func (s *GuildService) QueryImage(guildId, channelId uint64, hash []byte, size u }, CommandId: proto.Uint32(83), }) - packet := packets.BuildUniPacket(s.c.Uin, seq, "ImgStore.QQMeetPicUp", 1, s.c.OutGoingPacketSessionId, []byte{}, s.c.sigInfo.d2Key, payload) - rsp, err := s.c.sendAndWaitDynamic(seq, packet) - if err != nil { - return nil, errors.Wrap(err, "send packet error") - } - body := new(cmd0x388.D388RspBody) - if err = proto.Unmarshal(rsp, body); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal protobuf message") - } - if len(body.TryupImgRsp) == 0 { - return nil, errors.New("response is empty") - } - if body.TryupImgRsp[0].GetFileExit() { - return &message.GuildImageElement{ - FileId: body.TryupImgRsp[0].GetFileid(), - FilePath: hex.EncodeToString(hash) + ".jpg", - Size: int32(size), - DownloadIndex: string(body.TryupImgRsp[0].GetDownloadIndex()), - Md5: hash, - }, nil - } - return nil, errors.New("image is not exists") + packet := packets.BuildUniPacket(c.Uin, seq, "ImgStore.QQMeetPicUp", 1, c.OutGoingPacketSessionId, []byte{}, c.sigInfo.d2Key, payload) + return seq, packet } func decodeGuildMessageEmojiReactions(content *channel.ChannelMsgContent) (r []*message.GuildMessageEmojiReaction) { @@ -140,6 +215,36 @@ func decodeGuildMessageEmojiReactions(content *channel.ChannelMsgContent) (r []* return } +func decodeGuildImageStoreResponse(_ *QQClient, _ *incomingPacketInfo, payload []byte) (interface{}, error) { + body := new(cmd0x388.D388RspBody) + if err := proto.Unmarshal(payload, body); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal protobuf message") + } + if len(body.TryupImgRsp) == 0 { + return nil, errors.New("response is empty") + } + rsp := body.TryupImgRsp[0] + if rsp.GetResult() != 0 { + return &guildImageUploadResponse{ + ResultCode: int32(rsp.GetResult()), + Message: utils.B2S(rsp.GetFailMsg()), + }, nil + } + if rsp.GetFileExit() { + if rsp.ImgInfo != nil { + return &guildImageUploadResponse{IsExists: true, FileId: int64(rsp.GetFileid()), DownloadIndex: string(rsp.GetDownloadIndex()), Width: int32(rsp.ImgInfo.GetFileWidth()), Height: int32(rsp.ImgInfo.GetFileHeight())}, nil + } + return &guildImageUploadResponse{IsExists: true, FileId: int64(rsp.GetFileid()), DownloadIndex: string(rsp.GetDownloadIndex())}, nil + } + return &guildImageUploadResponse{ + FileId: int64(rsp.GetFileid()), + UploadKey: rsp.UpUkey, + UploadIp: rsp.GetUpIp(), + UploadPort: rsp.GetUpPort(), + DownloadIndex: string(rsp.GetDownloadIndex()), + }, nil +} + func (c *QQClient) parseGuildChannelMessage(msg *channel.ChannelMsgContent) *message.GuildChannelMessage { guild := c.GuildService.FindGuild(msg.Head.RoutingHead.GetGuildId()) if guild == nil { diff --git a/client/image.go b/client/image.go index 1d612834..3fd205bb 100644 --- a/client/image.go +++ b/client/image.go @@ -34,6 +34,21 @@ 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 + } +) + func (c *QQClient) UploadGroupImage(groupCode int64, img io.ReadSeeker) (*message.GroupImageElement, error) { _, _ = img.Seek(0, io.SeekStart) // safe fh, length := utils.ComputeMd5AndLength(img) diff --git a/message/image.go b/message/image.go index b8cbdcc3..f3db709f 100644 --- a/message/image.go +++ b/message/image.go @@ -37,7 +37,7 @@ type FriendImageElement struct { } type GuildImageElement struct { - FileId uint64 + FileId int64 FilePath string ImageType int32 Size int32