chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
229
internal/rpc/channels_multi_pin_android_rpc_test.go
Normal file
229
internal/rpc/channels_multi_pin_android_rpc_test.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/bin"
|
||||
"github.com/gotd/td/clock"
|
||||
"github.com/gotd/td/tg"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appchannels "telesrv/internal/app/channels"
|
||||
appdialogs "telesrv/internal/app/dialogs"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
// TestChannelMultiPinAndroidOpenAndJump 模拟 DrKLO Android 超级群多置顶消费链路:
|
||||
// 打开聊天 → messages.search(filterPinned, limit=40, offset_id=0) 全量拉置顶列表;
|
||||
// 点置顶栏跳最旧 pin → messages.getHistory(offset_id=pin, add_offset=-count/2, limit=count)
|
||||
// AROUND 加载,响应必须包含锚点消息本身,否则客户端弹 MessageNotFound 放弃跳转;
|
||||
// 本地缺对象 → channels.getMessages 精确补拉,messageEmpty 会被客户端丢弃。
|
||||
func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
owner, err := userStore.Create(ctx, domain.User{AccessHash: 61, Phone: "15550001161", FirstName: "Owner"})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
member, err := userStore.Create(ctx, domain.User{AccessHash: 62, Phone: "15550001162", FirstName: "Member"})
|
||||
if err != nil {
|
||||
t.Fatalf("create member: %v", err)
|
||||
}
|
||||
channelStore := memory.NewChannelStore()
|
||||
channelSvc := appchannels.NewService(channelStore)
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: channelSvc,
|
||||
Dialogs: appdialogs.NewService(memory.NewDialogStore(), channelStore),
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
created, err := channelSvc.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{
|
||||
Title: "MultiPin Android",
|
||||
Megagroup: true,
|
||||
MemberUserIDs: []int64{member.ID},
|
||||
Date: 1700001000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
channelID := created.Channel.ID
|
||||
|
||||
const total = 30
|
||||
ids := make([]int, 0, total)
|
||||
for i := 0; i < total; i++ {
|
||||
sent, err := channelSvc.SendMessage(ctx, owner.ID, domain.SendChannelMessageRequest{
|
||||
ChannelID: channelID,
|
||||
RandomID: int64(961000 + i),
|
||||
Message: "msg",
|
||||
Date: 1700001001 + i,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send %d: %v", i, err)
|
||||
}
|
||||
ids = append(ids, sent.Message.ID)
|
||||
}
|
||||
// 三条置顶:早期、中间、最新(Android 置顶栏循环跳转需要全部三条都可跳)。
|
||||
pins := []int{ids[4], ids[14], ids[27]}
|
||||
for _, id := range pins {
|
||||
if _, err := channelSvc.UpdatePinnedMessage(ctx, owner.ID, domain.UpdateChannelPinnedMessageRequest{
|
||||
ChannelID: channelID,
|
||||
MessageID: id,
|
||||
Pinned: true,
|
||||
Date: 1700001100,
|
||||
}); err != nil {
|
||||
t.Fatalf("pin %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
memberView, err := channelSvc.GetChannel(ctx, member.ID, channelID)
|
||||
if err != nil {
|
||||
t.Fatalf("member get channel: %v", err)
|
||||
}
|
||||
peer := &tg.InputPeerChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash}
|
||||
dispatch := func(req bin.Encoder) bin.Encoder {
|
||||
t.Helper()
|
||||
var b bin.Buffer
|
||||
if err := req.Encode(&b); err != nil {
|
||||
t.Fatalf("encode request: %v", err)
|
||||
}
|
||||
enc, err := r.Dispatch(WithUserID(androidClientContext(), member.ID), [8]byte{}, 0, &b)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch: %v", err)
|
||||
}
|
||||
return enc
|
||||
}
|
||||
|
||||
// ① 打开聊天:MediaDataController.loadPinnedMessages → messages.search filterPinned。
|
||||
searchEnc := dispatch(&tg.MessagesSearchRequest{
|
||||
Peer: peer,
|
||||
Q: "",
|
||||
Filter: &tg.InputMessagesFilterPinned{},
|
||||
Limit: 40,
|
||||
})
|
||||
if box, ok := searchEnc.(*tg.MessagesMessagesBox); ok {
|
||||
searchEnc = box.Messages
|
||||
}
|
||||
channelMessages, ok := searchEnc.(*tg.MessagesChannelMessages)
|
||||
if !ok {
|
||||
t.Fatalf("pinned search response = %T, want messages.channelMessages", searchEnc)
|
||||
}
|
||||
if len(channelMessages.Messages) != len(pins) {
|
||||
t.Fatalf("pinned search messages = %d, want %d", len(channelMessages.Messages), len(pins))
|
||||
}
|
||||
if channelMessages.Count != len(pins) {
|
||||
t.Fatalf("pinned search count = %d, want %d", channelMessages.Count, len(pins))
|
||||
}
|
||||
wantDesc := []int{pins[2], pins[1], pins[0]}
|
||||
for i, raw := range channelMessages.Messages {
|
||||
msg, ok := raw.(*tg.Message)
|
||||
if !ok {
|
||||
// TL_messageService / TL_messageEmpty 会被 Android loadPinnedMessages 直接跳过。
|
||||
t.Fatalf("pinned search message[%d] = %T, want *tg.Message", i, raw)
|
||||
}
|
||||
if msg.ID != wantDesc[i] {
|
||||
t.Fatalf("pinned search order[%d] = %d, want %d (id desc)", i, msg.ID, wantDesc[i])
|
||||
}
|
||||
if !msg.Pinned {
|
||||
t.Fatalf("pinned search message %d lacks pinned flag", msg.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// ② 点置顶栏跳最旧 pin:scrollToMessageId → getHistory AROUND(手机 count=20)。
|
||||
const aroundCount = 20
|
||||
histEnc := dispatch(&tg.MessagesGetHistoryRequest{
|
||||
Peer: peer,
|
||||
OffsetID: pins[0],
|
||||
AddOffset: -aroundCount / 2,
|
||||
Limit: aroundCount,
|
||||
})
|
||||
histMessages, _, _ := searchMessagesPayload(t, histEnc)
|
||||
if len(histMessages) == 0 || len(histMessages) > aroundCount {
|
||||
t.Fatalf("around history size = %d, want 1..%d (超出 count 时 Android 会丢最新一条)", len(histMessages), aroundCount)
|
||||
}
|
||||
anchorFound := false
|
||||
lastID := int(^uint(0) >> 1)
|
||||
for _, raw := range histMessages {
|
||||
if _, isEmpty := raw.(*tg.MessageEmpty); isEmpty {
|
||||
t.Fatalf("around history contains messageEmpty")
|
||||
}
|
||||
id := raw.GetID()
|
||||
if id >= lastID {
|
||||
t.Fatalf("around history not id-desc: %d then %d", lastID, id)
|
||||
}
|
||||
lastID = id
|
||||
if id == pins[0] {
|
||||
anchorFound = true
|
||||
}
|
||||
}
|
||||
if !anchorFound {
|
||||
// ChatActivity postponedScroll 在响应缺锚点时直接 MessageNotFound 放弃跳转。
|
||||
t.Fatalf("around history lacks anchor %d: jump shows MessageNotFound on Android", pins[0])
|
||||
}
|
||||
|
||||
// ③ 本地缺对象补拉:MessagesStorage.loadChatInfo → channels.getMessages。
|
||||
// DrKLO 发的是 pre-InputMessage 构造器 #93d7b347(id:Vector<int>),
|
||||
// 该请求 500 会让客户端把这批 pin 按「已取消置顶」从本地缓存删除。
|
||||
var legacy bin.Buffer
|
||||
legacy.PutID(0x93d7b347)
|
||||
if err := (&tg.InputChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash}).Encode(&legacy); err != nil {
|
||||
t.Fatalf("encode legacy input channel: %v", err)
|
||||
}
|
||||
legacy.PutVectorHeader(len(pins))
|
||||
for _, id := range pins {
|
||||
legacy.PutInt(id)
|
||||
}
|
||||
getEnc, err := r.Dispatch(WithUserID(androidClientContext(), member.ID), [8]byte{}, 0, &legacy)
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch legacy channels.getMessages#93d7b347: %v", err)
|
||||
}
|
||||
getMessages, _, _ := searchMessagesPayload(t, getEnc)
|
||||
if len(getMessages) != len(pins) {
|
||||
t.Fatalf("legacy channels.getMessages size = %d, want %d", len(getMessages), len(pins))
|
||||
}
|
||||
for i, raw := range getMessages {
|
||||
msg, ok := raw.(*tg.Message)
|
||||
if !ok {
|
||||
t.Fatalf("legacy channels.getMessages[%d] = %T, want *tg.Message (messageEmpty 会被客户端丢弃)", i, raw)
|
||||
}
|
||||
if !msg.Pinned {
|
||||
t.Fatalf("legacy channels.getMessages message %d lacks pinned flag", msg.ID)
|
||||
}
|
||||
}
|
||||
// 新构造器(TDesktop 路径)必须与 legacy 返回一致的消息集合。
|
||||
getIDs := make([]tg.InputMessageClass, 0, len(pins))
|
||||
for _, id := range pins {
|
||||
getIDs = append(getIDs, &tg.InputMessageID{ID: id})
|
||||
}
|
||||
modernEnc := dispatch(&tg.ChannelsGetMessagesRequest{
|
||||
Channel: &tg.InputChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash},
|
||||
ID: getIDs,
|
||||
})
|
||||
modernMessages, _, _ := searchMessagesPayload(t, modernEnc)
|
||||
if len(modernMessages) != len(getMessages) {
|
||||
t.Fatalf("modern channels.getMessages size = %d, want %d (legacy/modern must match)", len(modernMessages), len(getMessages))
|
||||
}
|
||||
for i := range modernMessages {
|
||||
if modernMessages[i].GetID() != getMessages[i].GetID() {
|
||||
t.Fatalf("modern/legacy mismatch at %d: %d != %d", i, modernMessages[i].GetID(), getMessages[i].GetID())
|
||||
}
|
||||
}
|
||||
|
||||
// ④ chatFull 降级缓存:pinned_msg_id 必须是最新置顶(Android 以它判断是否重拉列表)。
|
||||
fullEnc := dispatch(&tg.ChannelsGetFullChannelRequest{
|
||||
Channel: &tg.InputChannel{ChannelID: channelID, AccessHash: memberView.Channel.AccessHash},
|
||||
})
|
||||
full, ok := fullEnc.(*tg.MessagesChatFull)
|
||||
if !ok {
|
||||
t.Fatalf("getFullChannel response = %T, want messages.chatFull", fullEnc)
|
||||
}
|
||||
channelFull, ok := full.FullChat.(*tg.ChannelFull)
|
||||
if !ok {
|
||||
t.Fatalf("full chat = %T, want channelFull", full.FullChat)
|
||||
}
|
||||
if pinnedID, _ := channelFull.GetPinnedMsgID(); pinnedID != pins[2] {
|
||||
t.Fatalf("channelFull pinned_msg_id = %d, want latest pin %d", pinnedID, pins[2])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue