diff --git a/client/guild.go b/client/guild.go index fd5a3677..1638cda3 100644 --- a/client/guild.go +++ b/client/guild.go @@ -2,6 +2,7 @@ package client import ( "fmt" + "github.com/Mrs4s/MiraiGo/topic" "math/rand" "sort" "time" @@ -525,17 +526,17 @@ func (s *GuildService) FetchChannelInfo(guildId, channelId uint64) (*ChannelInfo return convertChannelInfo(body.Info), nil } -func (s *GuildService) GetChannelTopics(guildId, channelId uint64) error { +func (s *GuildService) GetTopicChannelFeeds(guildId, channelId uint64) ([]*topic.Feed, error) { guild := s.FindGuild(guildId) if guild == nil { - return errors.New("guild not found") + return nil, errors.New("guild not found") } channelInfo := guild.FindChannel(channelId) if channelInfo == nil { - return errors.New("channel not found") + return nil, errors.New("channel not found") } if channelInfo.ChannelType != ChannelTypeTopic { - return errors.New("channel type error") + return nil, errors.New("channel type error") } req, _ := proto.Marshal(&channel.StGetChannelFeedsReq{ Count: proto.Uint32(12), @@ -571,17 +572,21 @@ func (s *GuildService) GetChannelTopics(guildId, channelId uint64) error { packet := packets.BuildUniPacket(s.c.Uin, seq, "QChannelSvr.trpc.qchannel.commreader.ComReader.GetChannelTimelineFeeds", 1, s.c.OutGoingPacketSessionId, []byte{}, s.c.sigInfo.d2Key, payload) rsp, err := s.c.sendAndWaitDynamic(seq, packet) if err != nil { - return errors.New("send packet error") + return nil, errors.New("send packet error") } pkg := new(qweb.QWebRsp) body := new(channel.StGetChannelFeedsRsp) if err = proto.Unmarshal(rsp, pkg); err != nil { - return errors.Wrap(err, "failed to unmarshal protobuf message") + return nil, errors.Wrap(err, "failed to unmarshal protobuf message") } if err = proto.Unmarshal(pkg.BusiBuff, body); err != nil { - return errors.Wrap(err, "failed to unmarshal protobuf message") + return nil, errors.Wrap(err, "failed to unmarshal protobuf message") } - return nil + feeds := make([]*topic.Feed, 0, len(body.VecFeed)) + for _, f := range body.VecFeed { + feeds = append(feeds, topic.DecodeFeed(f)) + } + return feeds, nil } /* need analysis diff --git a/client/pb/channel/GuildChannelBase.pb.go b/client/pb/channel/GuildChannelBase.pb.go index 5b3e9af3..a83d2422 100644 --- a/client/pb/channel/GuildChannelBase.pb.go +++ b/client/pb/channel/GuildChannelBase.pb.go @@ -176,6 +176,25 @@ func (x *StChannelSign) GetChannelId() uint64 { return 0 } +type StEmotionReactionInfo struct { + Id *string `protobuf:"bytes,1,opt"` + EmojiReactionList []*EmojiReaction `protobuf:"bytes,2,rep"` +} + +func (x *StEmotionReactionInfo) GetId() string { + if x != nil && x.Id != nil { + return *x.Id + } + return "" +} + +func (x *StEmotionReactionInfo) GetEmojiReactionList() []*EmojiReaction { + if x != nil { + return x.EmojiReactionList + } + return nil +} + type StCommonExt struct { MapInfo []*CommonEntry `protobuf:"bytes,1,rep"` AttachInfo *string `protobuf:"bytes,2,opt"` diff --git a/client/pb/channel/GuildChannelBase.proto b/client/pb/channel/GuildChannelBase.proto index 796d00e8..a2370a64 100644 --- a/client/pb/channel/GuildChannelBase.proto +++ b/client/pb/channel/GuildChannelBase.proto @@ -4,6 +4,8 @@ package channel; option go_package = "pb/channel;channel"; +import "pb/channel/MsgResponsesSvr.proto"; + message ChannelUserInfo { optional ClientIdentity clientIdentity = 1; optional uint32 memberType = 2; @@ -51,12 +53,13 @@ message StEmojiReaction { optional bool isClicked = 4; optional bool isDefaultEmoji = 10001; } + */ message StEmotionReactionInfo { optional string id = 1; - repeated StEmojiReaction emojiReactionList = 2; + repeated EmojiReaction emojiReactionList = 2; } - */ + message StCommonExt { repeated CommonEntry mapInfo = 1; diff --git a/client/pb/channel/GuildFeedCloudMeta.pb.go b/client/pb/channel/GuildFeedCloudMeta.pb.go index 47e03f54..344bf849 100644 --- a/client/pb/channel/GuildFeedCloudMeta.pb.go +++ b/client/pb/channel/GuildFeedCloudMeta.pb.go @@ -580,31 +580,31 @@ func (x *StExternalMedalWallInfo) GetNeedShowEntrance() bool { } type StFeed struct { - Id *string `protobuf:"bytes,1,opt"` - Title *StRichText `protobuf:"bytes,2,opt"` - Subtitle *StRichText `protobuf:"bytes,3,opt"` - Poster *StUser `protobuf:"bytes,4,opt"` - Videos []*StVideo `protobuf:"bytes,5,rep"` - Contents *StRichText `protobuf:"bytes,6,opt"` - CreateTime *uint64 `protobuf:"varint,7,opt"` - EmotionReaction *EmojiReaction `protobuf:"bytes,8,opt"` - CommentCount *uint32 `protobuf:"varint,9,opt"` - VecComment []*StComment `protobuf:"bytes,10,rep"` - Share *StShare `protobuf:"bytes,11,opt"` - VisitorInfo *StVisitor `protobuf:"bytes,12,opt"` - Images []*StImage `protobuf:"bytes,13,rep"` - PoiInfo *StPoiInfoV2 `protobuf:"bytes,14,opt"` - TagInfos []*StTagInfo `protobuf:"bytes,15,rep"` - BusiReport []byte `protobuf:"bytes,16,opt"` - OpMask []uint32 `protobuf:"varint,17,rep"` - Opinfo *StOpinfo `protobuf:"bytes,18,opt"` - ExtInfo []*CommonEntry `protobuf:"bytes,19,rep"` - PatternInfo *string `protobuf:"bytes,20,opt"` - ChannelInfo *StChannelInfo `protobuf:"bytes,21,opt"` - CreateTimeNs *uint64 `protobuf:"varint,22,opt"` - Summary *StFeedSummary `protobuf:"bytes,23,opt"` - RecomInfo *StRecomInfo `protobuf:"bytes,24,opt"` - Meta *FeedMetaData `protobuf:"bytes,25,opt"` + Id *string `protobuf:"bytes,1,opt"` + Title *StRichText `protobuf:"bytes,2,opt"` + Subtitle *StRichText `protobuf:"bytes,3,opt"` + Poster *StUser `protobuf:"bytes,4,opt"` + Videos []*StVideo `protobuf:"bytes,5,rep"` + Contents *StRichText `protobuf:"bytes,6,opt"` + CreateTime *uint64 `protobuf:"varint,7,opt"` + EmotionReaction *StEmotionReactionInfo `protobuf:"bytes,8,opt"` + CommentCount *uint32 `protobuf:"varint,9,opt"` + VecComment []*StComment `protobuf:"bytes,10,rep"` + Share *StShare `protobuf:"bytes,11,opt"` + VisitorInfo *StVisitor `protobuf:"bytes,12,opt"` + Images []*StImage `protobuf:"bytes,13,rep"` + PoiInfo *StPoiInfoV2 `protobuf:"bytes,14,opt"` + TagInfos []*StTagInfo `protobuf:"bytes,15,rep"` + BusiReport []byte `protobuf:"bytes,16,opt"` + OpMask []uint32 `protobuf:"varint,17,rep"` + Opinfo *StOpinfo `protobuf:"bytes,18,opt"` + ExtInfo []*CommonEntry `protobuf:"bytes,19,rep"` + PatternInfo *string `protobuf:"bytes,20,opt"` + ChannelInfo *StChannelInfo `protobuf:"bytes,21,opt"` + CreateTimeNs *uint64 `protobuf:"varint,22,opt"` + Summary *StFeedSummary `protobuf:"bytes,23,opt"` + RecomInfo *StRecomInfo `protobuf:"bytes,24,opt"` + Meta *FeedMetaData `protobuf:"bytes,25,opt"` } func (x *StFeed) GetId() string { @@ -656,7 +656,7 @@ func (x *StFeed) GetCreateTime() uint64 { return 0 } -func (x *StFeed) GetEmotionReaction() *EmojiReaction { +func (x *StFeed) GetEmotionReaction() *StEmotionReactionInfo { if x != nil { return x.EmotionReaction } @@ -1734,9 +1734,10 @@ func (x *StPoiInfoV2) GetDisplayName() string { } type StPrePullCacheFeed struct { - Id *string `protobuf:"bytes,1,opt"` - Poster *StUser `protobuf:"bytes,2,opt"` - CreateTime *uint64 `protobuf:"varint,3,opt"` //repeated GuildCommon.BytesEntry busiTranparent = 4; + Id *string `protobuf:"bytes,1,opt"` + Poster *StUser `protobuf:"bytes,2,opt"` + CreateTime *uint64 `protobuf:"varint,3,opt"` + BusiTranparent []*BytesEntry `protobuf:"bytes,4,rep"` } func (x *StPrePullCacheFeed) GetId() string { @@ -1760,6 +1761,13 @@ func (x *StPrePullCacheFeed) GetCreateTime() uint64 { return 0 } +func (x *StPrePullCacheFeed) GetBusiTranparent() []*BytesEntry { + if x != nil { + return x.BusiTranparent + } + return nil +} + type StProxyInfo struct { CmdId *int32 `protobuf:"varint,1,opt"` SubCmdId *int32 `protobuf:"varint,2,opt"` diff --git a/client/pb/channel/GuildFeedCloudMeta.proto b/client/pb/channel/GuildFeedCloudMeta.proto index 3aa82336..32c5e996 100644 --- a/client/pb/channel/GuildFeedCloudMeta.proto +++ b/client/pb/channel/GuildFeedCloudMeta.proto @@ -4,7 +4,6 @@ package channel; option go_package = "pb/channel;channel"; -import "pb/channel/MsgResponsesSvr.proto"; import "pb/channel/GuildChannelBase.proto"; message ContentMetaData { @@ -130,7 +129,7 @@ message StFeed { repeated StVideo videos = 5; optional StRichText contents = 6; optional uint64 createTime = 7; - optional EmojiReaction emotionReaction = 8; + optional StEmotionReactionInfo emotionReaction = 8; optional uint32 commentCount = 9; repeated StComment vecComment = 10; optional StShare share = 11; @@ -328,7 +327,7 @@ message StPrePullCacheFeed { optional string id = 1; optional StUser poster = 2; optional uint64 createTime = 3; - //repeated GuildCommon.BytesEntry busiTranparent = 4; + repeated BytesEntry busiTranparent = 4; } message StProxyInfo { diff --git a/client/pb/channel/servtype.pb.go b/client/pb/channel/servtype.pb.go index 8f855a9f..82811be5 100644 --- a/client/pb/channel/servtype.pb.go +++ b/client/pb/channel/servtype.pb.go @@ -1534,9 +1534,10 @@ func (x *SwitchDetail) GetPlatform() uint32 { type SwitchLiveRoom struct { GuildId *uint64 `protobuf:"varint,1,opt"` ChannelId *uint64 `protobuf:"varint,2,opt"` - RoomId *uint64 `protobuf:"varint,3,opt"` - Tinyid *uint64 `protobuf:"varint,4,opt"` - Action *uint32 `protobuf:"varint,5,opt"` + // optional uint64 roomId = 3; + // optional uint64 tinyid = 4; + UserInfo *SwitchLiveRoomUserInfo `protobuf:"bytes,3,opt"` + Action *uint32 `protobuf:"varint,4,opt"` // JOIN = 1 QUIT = 2 } func (x *SwitchLiveRoom) GetGuildId() uint64 { @@ -1553,18 +1554,11 @@ func (x *SwitchLiveRoom) GetChannelId() uint64 { return 0 } -func (x *SwitchLiveRoom) GetRoomId() uint64 { - if x != nil && x.RoomId != nil { - return *x.RoomId +func (x *SwitchLiveRoom) GetUserInfo() *SwitchLiveRoomUserInfo { + if x != nil { + return x.UserInfo } - return 0 -} - -func (x *SwitchLiveRoom) GetTinyid() uint64 { - if x != nil && x.Tinyid != nil { - return *x.Tinyid - } - return 0 + return nil } func (x *SwitchLiveRoom) GetAction() uint32 { @@ -1574,6 +1568,25 @@ func (x *SwitchLiveRoom) GetAction() uint32 { return 0 } +type SwitchLiveRoomUserInfo struct { + TinyId *uint64 `protobuf:"varint,1,opt"` + Nickname *string `protobuf:"bytes,2,opt"` +} + +func (x *SwitchLiveRoomUserInfo) GetTinyId() uint64 { + if x != nil && x.TinyId != nil { + return *x.TinyId + } + return 0 +} + +func (x *SwitchLiveRoomUserInfo) GetNickname() string { + if x != nil && x.Nickname != nil { + return *x.Nickname + } + return "" +} + type SwitchVoiceChannel struct { MemberId *uint64 `protobuf:"varint,1,opt"` EnterDetail *SwitchDetail `protobuf:"bytes,2,opt"` diff --git a/client/pb/channel/servtype.proto b/client/pb/channel/servtype.proto index 92d84768..cc1604c5 100644 --- a/client/pb/channel/servtype.proto +++ b/client/pb/channel/servtype.proto @@ -286,9 +286,15 @@ message SwitchDetail { message SwitchLiveRoom { optional uint64 guildId = 1; optional uint64 channelId = 2; - optional uint64 roomId = 3; - optional uint64 tinyid = 4; - optional uint32 action = 5; + // optional uint64 roomId = 3; + // optional uint64 tinyid = 4; + optional SwitchLiveRoomUserInfo userInfo = 3; + optional uint32 action = 4; // JOIN = 1 QUIT = 2 +} + +message SwitchLiveRoomUserInfo { + optional uint64 tinyId = 1; + optional string nickname = 2; } message SwitchVoiceChannel { diff --git a/message/message.go b/message/message.go index 4a1a4fe4..4531aa8c 100644 --- a/message/message.go +++ b/message/message.go @@ -607,5 +607,8 @@ func ToReadableString(m []IMessageElement) string { } func FaceNameById(id int) string { - return faceMap[id] + if name, ok := faceMap[id]; ok { + return name + } + return "未知表情" } diff --git a/topic/elements.go b/topic/elements.go new file mode 100644 index 00000000..8a1a279e --- /dev/null +++ b/topic/elements.go @@ -0,0 +1,141 @@ +package topic + +import ( + "strconv" +) + +type ( + TextElement struct { + Content string + } + + EmojiElement struct { + Index int32 + Id string + Name string + } + + AtElement struct { + Id string + TinyId uint64 + Nickname string + } + + ChannelQuoteElement struct { + GuildId uint64 + ChannelId uint64 + DisplayText string + } + + UrlQuoteElement struct { + Url string + DisplayText string + } +) + +func selectContent(b bool, c1, c2 content) content { + if b { + return c1 + } + return c2 +} + +func (e *TextElement) pack(patternId string, isPatternData bool) content { + return selectContent(isPatternData, + content{ + "type": 1, + "style": "n", + "text": e.Content, + "children": make([]int, 0), + }, + content{ + "type": 1, + "text_content": content{ + "text": e.Content, + }, + }) +} + +func (e *EmojiElement) pack(patternId string, isPatternData bool) content { + return selectContent(isPatternData, + content{ + "type": 2, + "id": patternId, + "emojiType": "1", + "emojiId": e.Id, + }, + content{ + "type": 4, + "pattern_id": patternId, + "emoji_content": content{ + "type": "1", + "id": e.Id, + }, + }) +} + +func (e *AtElement) pack(patternId string, isPatternData bool) content { + return selectContent(isPatternData, + content{ + "type": 3, + "id": patternId, + "user": content{ + "id": strconv.FormatUint(e.TinyId, 10), + "nick": e.Nickname, + }, + }, + content{ + "type": 2, + "pattern_id": patternId, + "at_content": content{ + "type": 1, + "user": content{ + "id": e.Id, + "nick": e.Nickname, + }, + }, + }) +} + +func (e *ChannelQuoteElement) pack(patternId string, isPatternData bool) content { + return selectContent(isPatternData, + content{ + "type": 4, + "id": patternId, + "guild_info": content{ + "channel_id": strconv.FormatUint(e.ChannelId, 10), + "name": e.DisplayText, + }, + }, + content{ + "type": 5, + "pattern_id": patternId, + "channel_content": content{ + "channel_info": content{ + "name": e.DisplayText, + "sign": content{ + "guild_id": strconv.FormatUint(e.GuildId, 10), + "channel_id": strconv.FormatUint(e.ChannelId, 10), + }, + }, + }, + }) +} + +func (e *UrlQuoteElement) pack(patternId string, isPatternData bool) content { + return selectContent(isPatternData, + content{ + "type": 5, + "desc": e.DisplayText, + "href": e.Url, + "id": patternId, + }, + content{ + "type": 3, + "pattern_id": patternId, + "url_content": content{ + "url": e.Url, + "displayText": e.DisplayText, + }, + }) +} diff --git a/topic/feed.go b/topic/feed.go new file mode 100644 index 00000000..fc153b4d --- /dev/null +++ b/topic/feed.go @@ -0,0 +1,193 @@ +package topic + +import ( + "encoding/json" + "fmt" + "github.com/Mrs4s/MiraiGo/client/pb/channel" + "github.com/Mrs4s/MiraiGo/message" + "github.com/Mrs4s/MiraiGo/utils" + "strconv" + "strings" + "sync/atomic" + "time" +) + +type ( + Feed struct { + Id string + Title string + SubTitle string + CreateTime int64 + Poster *FeedPoster + GuildId uint64 + ChannelId uint64 + Images []*FeedImageInfo + Videos []*FeedVideoInfo + Contents []IFeedRichContentElement + } + + FeedPoster struct { + TinyId uint64 + TinyIdStr string + Nickname string + IconUrl string + } + + FeedImageInfo struct { + FileId string + PatternId string + Url string + Width uint32 + Height uint32 + } + + FeedVideoInfo struct { + FileId string + PatternId string + Url string + Width uint32 + Height uint32 + // CoverImage FeedImageInfo + } + + IFeedRichContentElement interface { + pack(patternId string, isPatternData bool) content + } + + content map[string]interface{} +) + +var ( + globalBlockId int64 = 0 +) + +func genBlockId() string { + id := atomic.AddInt64(&globalBlockId, 1) + return fmt.Sprintf("%v_%v_%v", time.Now().UnixMilli(), utils.RandomStringRange(4, "0123456789"), id) +} + +func (f *Feed) ToSendingPayload(selfUin int64) string { + c := content{ // todo: support media + "images": make([]int, 0), + "videos": make([]int, 0), + "poster": content{ + "id": f.Poster.TinyIdStr, + "nick": f.Poster.Nickname, + }, + "channelInfo": content{ + "sign": content{ + "guild_id": strconv.FormatUint(f.GuildId, 10), + "channel_id": strconv.FormatUint(f.ChannelId, 10), + }, + }, + "title": content{ + "contents": []content{ + (&TextElement{Content: f.Title}).pack("", false), + }, + }, + } + patternInfo := []content{ + { + "id": genBlockId(), + "type": "blockParagraph", + "data": []content{ + (&TextElement{Content: f.Title}).pack("", true), + }, + }, + } + patternData := make([]content, len(f.Contents)) + contents := make([]content, len(f.Contents)) + for i, c := range f.Contents { + patternId := fmt.Sprintf("o%v_%v_%v", selfUin, time.Now().Format("2006_01_02_15_04_05"), strings.ToLower(utils.RandomStringRange(16, "0123456789abcdef"))) // readCookie("uin")_yyyy_MM_dd_hh_mm_ss_randomHex(16) + contents[i] = c.pack(patternId, false) + patternData[i] = c.pack(patternId, true) + } + c["contents"] = content{"contents": contents} + patternInfo = append(patternInfo, content{ + "id": genBlockId(), + "type": "blockParagraph", + "data": patternData, + }) + packedPattern, _ := json.Marshal(patternInfo) + c["patternInfo"] = utils.B2S(packedPattern) + packedContent, _ := json.Marshal(c) + return utils.B2S(packedContent) +} + +func DecodeFeed(p *channel.StFeed) *Feed { + f := &Feed{ + Id: p.GetId(), + Title: p.Title.Contents[0].TextContent.GetText(), + SubTitle: "", + CreateTime: int64(p.GetCreateTime()), + GuildId: p.ChannelInfo.Sign.GetGuildId(), + ChannelId: p.ChannelInfo.Sign.GetChannelId(), + } + if p.Subtitle != nil && len(p.Subtitle.Contents) > 0 { + f.SubTitle = p.Subtitle.Contents[0].TextContent.GetText() + } + if p.Poster != nil { + tinyId, _ := strconv.ParseUint(p.Poster.GetId(), 10, 64) + f.Poster = &FeedPoster{ + TinyId: tinyId, + TinyIdStr: p.Poster.GetId(), + Nickname: p.Poster.GetNick(), + } + if p.Poster.Icon != nil { + f.Poster.IconUrl = p.Poster.Icon.GetIconUrl() + } + } + for _, video := range p.Videos { + f.Videos = append(f.Videos, &FeedVideoInfo{ + FileId: video.GetFileId(), + PatternId: video.GetPatternId(), + Url: video.GetPlayUrl(), + Width: video.GetWidth(), + Height: video.GetHeight(), + }) + } + for _, image := range p.Images { + f.Images = append(f.Images, &FeedImageInfo{ + FileId: image.GetPicId(), + PatternId: image.GetPatternId(), + Url: image.GetPicUrl(), + Width: image.GetWidth(), + Height: image.GetHeight(), + }) + } + for _, c := range p.Contents.Contents { + if c.TextContent != nil { + f.Contents = append(f.Contents, &TextElement{Content: c.TextContent.GetText()}) + } + if c.EmojiContent != nil { + id, _ := strconv.ParseInt(c.EmojiContent.GetId(), 10, 64) + f.Contents = append(f.Contents, &EmojiElement{ + Index: int32(id), + Id: c.EmojiContent.GetId(), + Name: message.FaceNameById(int(id)), + }) + } + if c.ChannelContent != nil && c.ChannelContent.ChannelInfo != nil { + f.Contents = append(f.Contents, &ChannelQuoteElement{ + GuildId: c.ChannelContent.ChannelInfo.Sign.GetGuildId(), + ChannelId: c.ChannelContent.ChannelInfo.Sign.GetChannelId(), + DisplayText: c.ChannelContent.ChannelInfo.GetName(), + }) + } + if c.AtContent != nil && c.AtContent.User != nil { + tinyId, _ := strconv.ParseUint(c.AtContent.User.GetId(), 10, 64) + f.Contents = append(f.Contents, &AtElement{ + Id: c.AtContent.User.GetId(), + TinyId: tinyId, + Nickname: c.AtContent.User.GetNick(), + }) + } + if c.UrlContent != nil { + f.Contents = append(f.Contents, &UrlQuoteElement{ + Url: c.UrlContent.GetUrl(), + DisplayText: c.UrlContent.GetDisplayText(), + }) + } + } + return f +}