chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View 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)
}
}
// ② 点置顶栏跳最旧 pinscrollToMessageId → 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 构造器 #93d7b347id: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])
}
}