fix: sync WebA labels and forward fixes

This commit is contained in:
A 2026-07-04 21:14:41 +08:00
parent 7bb7e11603
commit 4d3bbeabd8
11 changed files with 422 additions and 10 deletions

View file

@ -72,7 +72,7 @@ func (r *Router) langpackLanguage(ctx context.Context, langPack, langCode string
langCode = "en"
}
}
langCode = strings.ToLower(langCode)
langCode = normalizeLangpackCode(langCode)
languages := r.langpackLanguages(ctx, langPack)
for _, lang := range languages {
if strings.ToLower(lang.LangCode) == langCode {
@ -150,3 +150,11 @@ func langPackFromClient(ctx context.Context) string {
}
return "tdesktop"
}
func normalizeLangpackCode(langCode string) string {
code := strings.ToLower(strings.TrimSpace(langCode))
if code == "" {
return "en"
}
return strings.TrimSuffix(code, "-raw")
}

View file

@ -68,6 +68,11 @@ func TestLangpackGetLanguage(t *testing.T) {
if lang.LangCode != "zh-hans" || lang.PluralCode != "zh" {
t.Fatalf("language = %+v, want zh-hans", lang)
}
raw := r.langpackLanguage(context.Background(), "tdesktop", "en-raw")
if raw.LangCode != "en" {
t.Fatalf("language(en-raw) = %+v, want en", raw)
}
}
func TestLangpackAndroidPersianLanguage(t *testing.T) {

View file

@ -30,9 +30,9 @@ const (
maxPollVotesOffsetLength = 128
maxTodoItems = 30
maxTodoTitleLength = 200
// maxTodoItemID 是清单项 id 的防御上限:协议只要求列表内唯一正整数(客户端通常
// 顺序分配),不能用条目数上限当 id 边界,否则非顺序分配的合法 id 被误拒。
maxTodoItemID = 1 << 16
// maxTodoItemID 是清单项 id 的防御上限:协议字段是 int32,WebA 会用 8 位左右
// 的稀疏本地 id,不能用条目数上限或顺序分配假设当 id 边界。
maxTodoItemID = 1<<31 - 1
maxVenueTitleLength = 256
maxVenueAddressLength = 512
maxVenueProviderLength = 64

View file

@ -30,6 +30,9 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
if !topMsgIDSet && req.TopMsgID != 0 {
topMsgID, topMsgIDSet = req.TopMsgID, true
}
if topMsgIDSet && topMsgID == -1 {
topMsgID, topMsgIDSet = 0, false
}
if topMsgIDSet && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) {
return nil, replyMessageIDInvalidErr()
}

View file

@ -91,6 +91,52 @@ func TestMessagesForwardMessagesRecordsRequestAndReturnsUpdates(t *testing.T) {
}
}
func TestMessagesForwardMessagesTreatsMainThreadTopMsgSentinelAsAbsent(t *testing.T) {
const (
ownerID = int64(1000000101)
fromID = int64(1000000102)
toID = int64(1000000103)
)
ctx := context.Background()
messages := &captureMessages{list: domain.MessageList{Messages: []domain.Message{
{
ID: 8,
OwnerUserID: ownerID,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: fromID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: fromID},
Date: 1700000108,
Body: "main thread source",
},
}}}
r := New(Config{}, Deps{
Messages: messages,
Users: mapUsersService{users: map[int64]domain.User{
ownerID: {ID: ownerID, FirstName: "Owner"},
fromID: {ID: fromID, FirstName: "From"},
toID: {ID: toID, FirstName: "To"},
}},
}, zaptest.NewLogger(t), clock.System)
req := &tg.MessagesForwardMessagesRequest{
FromPeer: &tg.InputPeerUser{UserID: fromID},
ToPeer: &tg.InputPeerUser{UserID: toID},
ID: []int{8},
RandomID: []int64{8001},
}
req.SetTopMsgID(-1)
updatesClass, err := r.onMessagesForwardMessages(WithUserID(ctx, ownerID), req)
if err != nil {
t.Fatalf("forward with main thread top_msg_id sentinel: %v", err)
}
if messages.sendReq.ReplyTo != nil {
t.Fatalf("reply = %+v, want nil for main thread sentinel", messages.sendReq.ReplyTo)
}
updates, ok := updatesClass.(*tg.Updates)
if !ok || len(updates.Updates) != 2 {
t.Fatalf("updates = %T %+v, want updateMessageID + updateNewMessage", updatesClass, updatesClass)
}
}
func TestMessagesForwardMessagesLoadsPrivateSourcesInSingleBatch(t *testing.T) {
ctx := context.Background()
userStore := memory.NewUserStore()
@ -312,6 +358,30 @@ func TestMessagesForwardMessagesInputPeerEmptyRejectsBadIDsBeforeLookup(t *testi
}
}
func TestMessagesForwardMessagesRejectsOtherNegativeTopMsgID(t *testing.T) {
const ownerID = int64(1780243210)
ctx := context.Background()
messages := &captureMessages{}
r := New(Config{}, Deps{
Messages: messages,
}, zaptest.NewLogger(t), clock.System)
req := &tg.MessagesForwardMessagesRequest{
FromPeer: &tg.InputPeerUser{UserID: 1780243211},
ToPeer: &tg.InputPeerUser{UserID: 1780243212},
ID: []int{1},
RandomID: []int64{10001},
}
req.SetTopMsgID(-2)
_, err := r.onMessagesForwardMessages(WithUserID(ctx, ownerID), req)
if err == nil || !strings.Contains(err.Error(), "REPLY_MESSAGE_ID_INVALID") {
t.Fatalf("forward negative top_msg_id err = %v, want REPLY_MESSAGE_ID_INVALID", err)
}
if messages.getMessagesCalls != 0 {
t.Fatalf("GetMessages calls = %d, want no source lookup for invalid top_msg_id", messages.getMessagesCalls)
}
}
func TestMessagesForwardMessagesNormalizesAndroidDuplicateIDRetry(t *testing.T) {
const (
ownerID = int64(1780243210)

View file

@ -201,6 +201,9 @@ func (r *Router) messageReplyFromInput(ctx context.Context, userID int64, peer d
return nil, inputConstructorInvalidErr()
}
}
if reply.Zero() {
return nil, nil
}
if _, ok := reply.GetMonoforumPeerID(); ok {
return nil, replyToMonoforumPeerInvalidErr()
}

View file

@ -244,6 +244,37 @@ func TestMessageReplyFromInputStorySucceedsAndProjectsStoryHeader(t *testing.T)
}
}
func TestMessageReplyFromInputEmptyMessageIsAbsent(t *testing.T) {
const userID = int64(1000000001)
ctx := WithUserID(context.Background(), userID)
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}
reply, err := r.messageReplyFromInput(ctx, userID, peer, &tg.InputReplyToMessage{})
if err != nil {
t.Fatalf("empty reply err = %v, want nil", err)
}
if reply != nil {
t.Fatalf("empty reply = %+v, want nil", reply)
}
topicReply := &tg.InputReplyToMessage{}
topicReply.SetTopMsgID(123)
reply, err = r.messageReplyFromInput(ctx, userID, peer, topicReply)
if err != nil {
t.Fatalf("topic-only reply err = %v, want nil", err)
}
if reply == nil || reply.MessageID != 0 || reply.TopMessageID != 123 {
t.Fatalf("topic-only reply = %+v, want top_msg_id=123", reply)
}
quoteOnly := &tg.InputReplyToMessage{}
quoteOnly.SetQuoteText("orphan quote")
if _, err := r.messageReplyFromInput(ctx, userID, peer, quoteOnly); err == nil || !strings.Contains(err.Error(), "REPLY_MESSAGE_ID_INVALID") {
t.Fatalf("quote-only reply err = %v, want REPLY_MESSAGE_ID_INVALID", err)
}
}
func TestMessageReplyFromInputUnsupportedShapesReturnExplicitErrors(t *testing.T) {
const userID = int64(1000000001)
ctx := WithUserID(context.Background(), userID)

View file

@ -53,6 +53,59 @@ func TestSendMediaTodoEcho(t *testing.T) {
}
}
func TestSendMediaTodoWithEmptyReplyToTreatsReplyAsAbsent(t *testing.T) {
r, owner, friend := newMediaTestRouter(t)
req := &tg.MessagesSendMediaRequest{
Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash},
Media: &tg.InputMediaTodo{Todo: tg.TodoList{
Title: tg.TextWithEntities{Text: "web checklist", Entities: []tg.MessageEntityClass{}},
List: []tg.TodoItem{
{ID: 1, Title: tg.TextWithEntities{Text: "send item", Entities: []tg.MessageEntityClass{}}},
},
}},
RandomID: 7005,
}
req.SetReplyTo(&tg.InputReplyToMessage{})
updates, err := r.onMessagesSendMedia(WithUserID(context.Background(), owner.ID), req)
if err != nil {
t.Fatalf("sendMedia todo with empty reply: %v", err)
}
msg := newMessageFromUpdates(t, updates)
if msg.ReplyTo != nil {
t.Fatalf("reply_to = %T, want nil for empty input reply", msg.ReplyTo)
}
if media, ok := msg.Media.(*tg.MessageMediaToDo); !ok || media.Todo.Title.Text != "web checklist" {
t.Fatalf("media = %#v, want todo checklist", msg.Media)
}
}
func TestSendMediaTodoAcceptsSparseLargeItemIDs(t *testing.T) {
r, owner, friend := newMediaTestRouter(t)
updates, err := r.onMessagesSendMedia(WithUserID(context.Background(), owner.ID), &tg.MessagesSendMediaRequest{
Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash},
Media: &tg.InputMediaTodo{Todo: tg.TodoList{
Title: tg.TextWithEntities{Text: "web sparse ids", Entities: []tg.MessageEntityClass{}},
List: []tg.TodoItem{
{ID: 56151290, Title: tg.TextWithEntities{Text: "first", Entities: []tg.MessageEntityClass{}}},
{ID: 56151305, Title: tg.TextWithEntities{Text: "second", Entities: []tg.MessageEntityClass{}}},
},
}},
RandomID: 7006,
})
if err != nil {
t.Fatalf("sendMedia todo sparse ids: %v", err)
}
msg := newMessageFromUpdates(t, updates)
media, ok := msg.Media.(*tg.MessageMediaToDo)
if !ok {
t.Fatalf("media = %T, want MessageMediaToDo", msg.Media)
}
if got := media.Todo.List[0].ID; got != 56151290 {
t.Fatalf("first todo id = %d, want 56151290", got)
}
}
func TestToggleTodoCompletedAndAppend(t *testing.T) {
ctx := context.Background()
r, owner, friend := newMediaTestRouter(t)