diff --git a/binary/jce/structs.go b/binary/jce/structs.go index fc3867ce..636fb460 100644 --- a/binary/jce/structs.go +++ b/binary/jce/structs.go @@ -136,6 +136,65 @@ type ( ClientAutoStatusInterval int64 `jceId:"19"` } + SvcReqRegisterNew struct { + IJceStruct + RequestOptional int64 `jceId:"0"` + C2CMsg IJceStruct `jceId:"1"` // SvcReqGetMsgV2 + GroupMsg IJceStruct `jceId:"2"` // SvcReqPullGroupMsgSeq + GroupMask byte `jceId:"15"` + EndSeq int64 `jceId:"16"` + } + + SvcReqGetMsgV2 struct { + IJceStruct + Uin int64 `jceId:"0"` + DateTime int32 `jceId:"1"` + RecivePic byte `jceId:"4"` + Ability int16 `jceId:"6"` + Channel byte `jceId:"9"` + Inst byte `jceId:"16"` + ChannelEx byte `jceId:"17"` + SyncCookie []byte `jceId:"18"` + SyncFlag int `jceId:"19"` + RambleFlag byte `jceId:"20"` + GeneralAbi int64 `jceId:"26"` + PubAccountCookie []byte `jceId:"27"` + } + + SvcReqPullGroupMsgSeq struct { + IJceStruct + GroupInfo []IJceStruct `jceId:"0"` // PullGroupSeqParam + VerifyType byte `jceId:"1"` + Filter int32 `jceId:"2"` + } + + PullGroupSeqParam struct { + IJceStruct + GroupCode int64 `jceId:"0"` + LastSeqId int64 `jceId:"1"` + } + + SvcRespParam struct { + PCStat int32 `jceId:"0"` + IsSupportC2CRoamMsg int32 `jceId:"1"` + IsSupportDataLine int32 `jceId:"2"` + IsSupportPrintable int32 `jceId:"3"` + IsSupportViewPCFile int32 `jceId:"4"` + PcVersion int32 `jceId:"5"` + RoamFlag int64 `jceId:"6"` + OnlineInfos []OnlineInfo `jceId:"7"` + PCClientType int32 `jceId:"8"` + } + + OnlineInfo struct { + InstanceId int32 `jceId:"0"` + ClientType int32 `jceId:"1"` + OnlineStatus int32 `jceId:"2"` + PlatformId int32 `jceId:"3"` + SubPlatform string `jceId:"4"` + UClientType int64 `jceId:"5"` + } + PushMessageInfo struct { FromUin int64 `jceId:"0"` MsgTime int64 `jceId:"1"` @@ -665,6 +724,28 @@ func (pkt *SvcDevLoginInfo) ReadFrom(r *JceReader) { pkt.CanBeKicked = r.ReadInt64(10) } +func (pkt *SvcRespParam) ReadFrom(r *JceReader) { + pkt.OnlineInfos = []OnlineInfo{} + pkt.PCStat = r.ReadInt32(0) + pkt.IsSupportC2CRoamMsg = r.ReadInt32(1) + pkt.IsSupportDataLine = r.ReadInt32(2) + pkt.IsSupportPrintable = r.ReadInt32(3) + pkt.IsSupportViewPCFile = r.ReadInt32(4) + pkt.PcVersion = r.ReadInt32(5) + pkt.RoamFlag = r.ReadInt64(6) + r.ReadSlice(&pkt.OnlineInfos, 7) + pkt.PCClientType = r.ReadInt32(8) +} + +func (pkt *OnlineInfo) ReadFrom(r *JceReader) { + pkt.InstanceId = r.ReadInt32(0) + pkt.ClientType = r.ReadInt32(1) + pkt.OnlineStatus = r.ReadInt32(2) + pkt.PlatformId = r.ReadInt32(3) + pkt.SubPlatform = string(r.ReadAny(4).([]byte)) + pkt.UClientType = r.ReadInt64(5) +} + func (pkt *SvcRespPushMsg) ToBytes() []byte { w := NewJceWriter() w.WriteJceStructRaw(pkt) @@ -682,3 +763,9 @@ func (pkt *SvcReqGetDevLoginInfo) ToBytes() []byte { w.WriteJceStructRaw(pkt) return w.Bytes() } + +func (pkt *SvcReqRegisterNew) ToBytes() []byte { + w := NewJceWriter() + w.WriteJceStructRaw(pkt) + return w.Bytes() +} diff --git a/binary/jce/writer.go b/binary/jce/writer.go index a4ba3350..3453b5b6 100644 --- a/binary/jce/writer.go +++ b/binary/jce/writer.go @@ -96,11 +96,12 @@ func (w *JceWriter) WriteString(s string, tag int) *JceWriter { return w } -func (w *JceWriter) WriteBytes(l []byte, tag int) { +func (w *JceWriter) WriteBytes(l []byte, tag int) *JceWriter { w.writeHead(13, tag) w.writeHead(0, 0) w.WriteInt32(int32(len(l)), 0) w.buf.Write(l) + return w } func (w *JceWriter) WriteInt64Slice(l []int64, tag int) { diff --git a/binary/tea.go b/binary/tea.go index e330d645..005f4ca1 100644 --- a/binary/tea.go +++ b/binary/tea.go @@ -1,8 +1,8 @@ package binary import ( - "crypto/rand" "encoding/binary" + "math/rand" "reflect" "unsafe" ) diff --git a/client/builders.go b/client/builders.go index ee7f3d6e..b8595112 100644 --- a/client/builders.go +++ b/client/builders.go @@ -302,29 +302,6 @@ func (c *QQClient) buildConfPushRespPacket(t int32, pktSeq int64, jceBuf []byte) return seq, packet } -// StatSvc.GetDevLoginInfo -func (c *QQClient) buildDeviceListRequestPacket() (uint16, []byte) { - seq := c.nextSeq() - req := &jce.SvcReqGetDevLoginInfo{ - Guid: SystemDeviceInfo.Guid, - LoginType: 1, - AppName: "com.tencent.mobileqq", - RequireMax: 20, - GetDevListType: 2, - } - buf := &jce.RequestDataVersion3{Map: map[string][]byte{"SvcReqGetDevLoginInfo": packUniRequestData(req.ToBytes())}} - pkt := &jce.RequestPacket{ - IVersion: 3, - SServantName: "StatSvc", - SFuncName: "SvcReqGetDevLoginInfo", - SBuffer: buf.ToBytes(), - Context: make(map[string]string), - Status: make(map[string]string), - } - packet := packets.BuildUniPacket(c.Uin, seq, "StatSvc.GetDevLoginInfo", 1, c.OutGoingPacketSessionId, []byte{}, c.sigInfo.d2Key, pkt.ToBytes()) - return seq, packet -} - // friendlist.getFriendGroupList func (c *QQClient) buildFriendGroupListRequestPacket(friendStartIndex, friendListCount, groupStartIndex, groupListCount int16) (uint16, []byte) { seq := c.nextSeq() diff --git a/client/client.go b/client/client.go index b4654966..28f654fb 100644 --- a/client/client.go +++ b/client/client.go @@ -36,13 +36,14 @@ type QQClient struct { PasswordMd5 [16]byte AllowSlider bool - Nickname string - Age uint16 - Gender uint16 - FriendList []*FriendInfo - GroupList []*GroupInfo - Online bool - NetLooping bool + Nickname string + Age uint16 + Gender uint16 + FriendList []*FriendInfo + GroupList []*GroupInfo + OnlineClients []*OtherClientInfo + Online bool + NetLooping bool SequenceId int32 OutGoingPacketSessionId []byte @@ -121,7 +122,6 @@ var decoders = map[string]func(*QQClient, uint16, []byte) (interface{}, error){ "wtlogin.exchange_emp": decodeExchangeEmpResponse, "StatSvc.register": decodeClientRegisterResponse, "StatSvc.ReqMSFOffline": decodeMSFOfflinePacket, - "StatSvc.GetDevLoginInfo": decodeDevListResponse, "MessageSvc.PushNotify": decodeSvcNotify, "OnlinePush.ReqPush": decodeOnlinePushReqPacket, "OnlinePush.PbPushTransMsg": decodeOnlinePushTransPacket, @@ -289,29 +289,6 @@ func (c *QQClient) SubmitSMS(code string) (*LoginResponse, error) { return &l, nil } -func (c *QQClient) init() { - c.Online = true - _ = c.registerClient() - c.groupSysMsgCache, _ = c.GetGroupSystemMessages() - if !c.heartbeatEnabled { - go c.doHeartbeat() - } - c.stat.once.Do(func() { - c.OnGroupMessage(func(_ *QQClient, _ *message.GroupMessage) { - c.stat.MessageReceived++ - }) - c.OnPrivateMessage(func(_ *QQClient, _ *message.PrivateMessage) { - c.stat.MessageReceived++ - }) - c.OnTempMessage(func(_ *QQClient, _ *message.TempMessage) { - c.stat.MessageReceived++ - }) - c.onGroupMessageReceipt("internal", func(_ *QQClient, _ *groupMessageReceiptEvent) { - c.stat.MessageSent++ - }) - }) -} - func (c *QQClient) RequestSMS() bool { rsp, err := c.sendAndWait(c.buildSMSRequestPacket()) if err != nil { @@ -321,6 +298,33 @@ func (c *QQClient) RequestSMS() bool { return rsp.(LoginResponse).Error == SMSNeededError } +func (c *QQClient) init() { + c.Online = true + _ = c.registerClient() + c.groupSysMsgCache, _ = c.GetGroupSystemMessages() + if !c.heartbeatEnabled { + go c.doHeartbeat() + } + _ = c.RefreshStatus() + c.stat.once.Do(func() { + c.OnGroupMessage(func(_ *QQClient, _ *message.GroupMessage) { + c.stat.MessageReceived++ + c.stat.LastMessageTime = time.Now().Unix() + }) + c.OnPrivateMessage(func(_ *QQClient, _ *message.PrivateMessage) { + c.stat.MessageReceived++ + c.stat.LastMessageTime = time.Now().Unix() + }) + c.OnTempMessage(func(_ *QQClient, _ *message.TempMessage) { + c.stat.MessageReceived++ + c.stat.LastMessageTime = time.Now().Unix() + }) + c.onGroupMessageReceipt("internal", func(_ *QQClient, _ *groupMessageReceiptEvent) { + c.stat.MessageSent++ + }) + }) +} + func (c *QQClient) GetVipInfo(target int64) (*VipInfo, error) { b, err := utils.HttpGetBytes(fmt.Sprintf("https://h5.vip.qq.com/p/mc/cardv2/other?platform=1&qq=%d&adtag=geren&aid=mvip.pingtai.mobileqq.androidziliaoka.fromqita", target), c.getCookiesWithDomain("h5.vip.qq.com")) if err != nil { diff --git a/client/decoders.go b/client/decoders.go index b31be6e1..874d6e93 100644 --- a/client/decoders.go +++ b/client/decoders.go @@ -413,19 +413,6 @@ func decodeSvcNotify(c *QQClient, _ uint16, _ []byte) (interface{}, error) { return nil, err } -// StatSvc.GetDevLoginInfo -func decodeDevListResponse(_ *QQClient, _ uint16, payload []byte) (interface{}, error) { - request := &jce.RequestPacket{} - request.ReadFrom(jce.NewJceReader(payload)) - data := &jce.RequestDataVersion2{} - data.ReadFrom(jce.NewJceReader(request.SBuffer)) - rsp := jce.NewJceReader(data.Map["SvcRspGetDevLoginInfo"]["QQService.SvcRspGetDevLoginInfo"][1:]) - d := []jce.SvcDevLoginInfo{} - ret := rsp.ReadInt64(3) - rsp.ReadSlice(&d, 5) - return ret, nil -} - // SummaryCard.ReqSummaryCard func decodeSummaryCardResponse(_ *QQClient, _ uint16, payload []byte) (interface{}, error) { request := &jce.RequestPacket{} diff --git a/client/entities.go b/client/entities.go index ee76fcab..03c37fea 100644 --- a/client/entities.go +++ b/client/entities.go @@ -58,6 +58,12 @@ type ( Qid string } + OtherClientInfo struct { + AppId int64 + DeviceName string + DeviceKind string + } + FriendListResponse struct { TotalCount int32 List []*FriendInfo diff --git a/client/group_file.go b/client/group_file.go index 05c5360b..1e35acf1 100644 --- a/client/group_file.go +++ b/client/group_file.go @@ -151,7 +151,7 @@ func (fs *GroupFileSystem) GetFilesByFolder(folderId string) ([]*GroupFile, []*G return files, folders, nil } -func (fs *GroupFileSystem) UploadFile(p, name, folderId string) error { +func (fs *GroupFileSystem) uploadFile(p, name, folderId string) error { file, err := os.OpenFile(p, os.O_RDONLY, 0666) if err != nil { return err diff --git a/client/group_msg.go b/client/group_msg.go index 96a9ab8f..15f96d8e 100644 --- a/client/group_msg.go +++ b/client/group_msg.go @@ -311,7 +311,7 @@ func decodeGroupMessagePacket(c *QQClient, _ uint16, payload []byte) (interface{ return nil, nil } if pkt.Message.Content != nil && pkt.Message.Content.GetPkgNum() > 1 { - var builder *groupMessageBuilder // TODO: 支持多SEQ + var builder *groupMessageBuilder i, ok := c.groupMsgBuilders.Load(pkt.Message.Content.GetDivSeq()) if !ok { builder = &groupMessageBuilder{} diff --git a/client/statistics.go b/client/statistics.go index 9df54262..1abf5ff4 100644 --- a/client/statistics.go +++ b/client/statistics.go @@ -10,6 +10,7 @@ type Statistics struct { MessageSent uint64 `json:"message_sent"` DisconnectTimes uint32 `json:"disconnect_times"` LostTimes uint32 `json:"lost_times"` + LastMessageTime int64 `json:"last_message_time"` once sync.Once } diff --git a/client/sync.go b/client/sync.go new file mode 100644 index 00000000..b8670fd9 --- /dev/null +++ b/client/sync.go @@ -0,0 +1,196 @@ +package client + +import ( + "github.com/Mrs4s/MiraiGo/binary/jce" + "github.com/Mrs4s/MiraiGo/client/pb/msg" + "github.com/Mrs4s/MiraiGo/protocol/packets" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + "time" +) + +func init() { + decoders["StatSvc.GetDevLoginInfo"] = decodeDevListResponse + decoders["RegPrxySvc.getOffMsg"] = decodeOfflineMsgResponse + decoders["RegPrxySvc.PushParam"] = decodePushParamPacket +} + +// GetAllowedClients 获取已允许的其他客户端 +func (c *QQClient) GetAllowedClients() ([]*OtherClientInfo, error) { + i, err := c.sendAndWait(c.buildDeviceListRequestPacket()) + if err != nil { + return nil, err + } + list := i.([]jce.SvcDevLoginInfo) + var ret []*OtherClientInfo + for _, l := range list { + ret = append(ret, &OtherClientInfo{ + AppId: l.AppId, + DeviceName: l.DeviceName, + DeviceKind: l.DeviceTypeInfo, + }) + } + return ret, nil +} + +// RefreshClientStatus 刷新客户端状态 +func (c *QQClient) RefreshStatus() error { + _, pkt := c.buildGetOfflineMsgRequest() + c.send(pkt) + return nil +} + +// StatSvc.GetDevLoginInfo +func (c *QQClient) buildDeviceListRequestPacket() (uint16, []byte) { + seq := c.nextSeq() + req := &jce.SvcReqGetDevLoginInfo{ + Guid: SystemDeviceInfo.Guid, + LoginType: 1, + AppName: "com.tencent.mobileqq", + RequireMax: 20, + GetDevListType: 2, + } + buf := &jce.RequestDataVersion3{Map: map[string][]byte{"SvcReqGetDevLoginInfo": packUniRequestData(req.ToBytes())}} + pkt := &jce.RequestPacket{ + IVersion: 3, + SServantName: "StatSvc", + SFuncName: "SvcReqGetDevLoginInfo", + SBuffer: buf.ToBytes(), + Context: make(map[string]string), + Status: make(map[string]string), + } + packet := packets.BuildUniPacket(c.Uin, seq, "StatSvc.GetDevLoginInfo", 1, c.OutGoingPacketSessionId, []byte{}, c.sigInfo.d2Key, pkt.ToBytes()) + return seq, packet +} + +// RegPrxySvc.getOffMsg +func (c *QQClient) buildGetOfflineMsgRequest() (uint16, []byte) { + seq := c.nextSeq() + regReq := &jce.SvcReqRegisterNew{ + RequestOptional: 0x101C2 | 32, + C2CMsg: &jce.SvcReqGetMsgV2{ + Uin: c.Uin, + DateTime: func() int32 { + if c.stat.LastMessageTime == 0 { + return 1 + } + return int32(c.stat.LastMessageTime) + }(), + RecivePic: 1, + Ability: 15, + Channel: 4, + Inst: 1, + ChannelEx: 1, + SyncCookie: c.syncCookie, + SyncFlag: 0, // START + RambleFlag: 0, + GeneralAbi: 1, + PubAccountCookie: c.pubAccountCookie, + }, + GroupMsg: &jce.SvcReqPullGroupMsgSeq{ + VerifyType: 0, + Filter: 1, // LIMIT_10_AND_IN_3_DAYS + }, + EndSeq: time.Now().Unix(), + } + flag := msg.SyncFlag_START + msgReq, _ := proto.Marshal(&msg.GetMessageRequest{ + SyncFlag: &flag, + SyncCookie: c.syncCookie, + RambleFlag: proto.Int32(0), + ContextFlag: proto.Int32(1), + OnlineSyncFlag: proto.Int32(0), + LatestRambleNumber: proto.Int32(20), + OtherRambleNumber: proto.Int32(3), + }) + buf := &jce.RequestDataVersion3{Map: map[string][]byte{ + "req_PbOffMsg": jce.NewJceWriter().WriteBytes(append([]byte{0, 0, 0, 0}, msgReq...), 0).Bytes(), + "req_OffMsg": packUniRequestData(regReq.ToBytes()), + }} + pkt := &jce.RequestPacket{ + IVersion: 3, + SServantName: "RegPrxySvc", + SBuffer: buf.ToBytes(), + Context: make(map[string]string), + Status: make(map[string]string), + } + packet := packets.BuildUniPacket(c.Uin, seq, "RegPrxySvc.getOffMsg", 1, c.OutGoingPacketSessionId, []byte{}, c.sigInfo.d2Key, pkt.ToBytes()) + return seq, packet +} + +// StatSvc.GetDevLoginInfo +func decodeDevListResponse(_ *QQClient, _ uint16, payload []byte) (interface{}, error) { + request := &jce.RequestPacket{} + request.ReadFrom(jce.NewJceReader(payload)) + data := &jce.RequestDataVersion2{} + data.ReadFrom(jce.NewJceReader(request.SBuffer)) + rsp := jce.NewJceReader(data.Map["SvcRspGetDevLoginInfo"]["QQService.SvcRspGetDevLoginInfo"][1:]) + d := []jce.SvcDevLoginInfo{} + rsp.ReadSlice(&d, 4) + if len(d) > 0 { + return d, nil + } + rsp.ReadSlice(&d, 5) + if len(d) > 0 { + return d, nil + } + rsp.ReadSlice(&d, 6) + if len(d) > 0 { + return d, nil + } + return nil, errors.New("not any device") +} + +// RegPrxySvc.getOffMsg +func decodeOfflineMsgResponse(c *QQClient, _ uint16, payload []byte) (interface{}, error) { + /* + request := &jce.RequestPacket{} + request.ReadFrom(jce.NewJceReader(payload)) + */ + return nil, nil +} + +// RegPrxySvc.PushParam +func decodePushParamPacket(c *QQClient, _ uint16, payload []byte) (interface{}, error) { + request := &jce.RequestPacket{} + request.ReadFrom(jce.NewJceReader(payload)) + data := &jce.RequestDataVersion2{} + data.ReadFrom(jce.NewJceReader(request.SBuffer)) + reader := jce.NewJceReader(data.Map["SvcRespParam"]["RegisterProxySvcPack.SvcRespParam"][1:]) + rsp := &jce.SvcRespParam{} + rsp.ReadFrom(reader) + allowedClients, _ := c.GetAllowedClients() + c.OnlineClients = []*OtherClientInfo{} + for _, i := range rsp.OnlineInfos { + c.OnlineClients = append(c.OnlineClients, &OtherClientInfo{ + AppId: int64(i.InstanceId), + DeviceName: func() string { + for _, ac := range allowedClients { + if ac.AppId == int64(i.InstanceId) { + return ac.DeviceName + } + } + return i.SubPlatform + }(), + DeviceKind: func() string { + switch i.UClientType { + case 65793: + return "Windows" + case 65805, 68104: + return "aPad" + case 66818, 66831, 81154: + return "Mac" + case 68361, 72194: + return "iPad" + case 75023, 78082, 78096: + return "Watch" + case 77313: + return "Windows TIM" + default: + return i.SubPlatform + } + }(), + }) + } + return nil, nil +}