fix: preserve formatted date message entities

This commit is contained in:
A 2026-07-05 20:42:13 +08:00
parent 53105b6823
commit 03b785ebf4
10 changed files with 214 additions and 31 deletions

View file

@ -473,6 +473,18 @@ func tgMessageEntities(entities []domain.MessageEntity) []tg.MessageEntityClass
out = append(out, &tg.MessageEntityPhone{Offset: entity.Offset, Length: entity.Length})
case domain.MessageEntityBankCard:
out = append(out, &tg.MessageEntityBankCard{Offset: entity.Offset, Length: entity.Length})
case domain.MessageEntityFormattedDate:
out = append(out, &tg.MessageEntityFormattedDate{
Offset: entity.Offset,
Length: entity.Length,
Date: entity.Date,
Relative: entity.Relative,
ShortTime: entity.ShortTime,
LongTime: entity.LongTime,
ShortDate: entity.ShortDate,
LongDate: entity.LongDate,
DayOfWeek: entity.DayOfWeek,
})
case domain.MessageEntityDiffInsert:
out = append(out, &tg.MessageEntityDiffInsert{Offset: entity.Offset, Length: entity.Length})
case domain.MessageEntityDiffReplace:
@ -549,6 +561,19 @@ func domainMessageEntitiesForViewer(viewerUserID int64, entities []tg.MessageEnt
out = append(out, domain.MessageEntity{Type: domain.MessageEntityPhone, Offset: e.Offset, Length: e.Length})
case *tg.MessageEntityBankCard:
out = append(out, domain.MessageEntity{Type: domain.MessageEntityBankCard, Offset: e.Offset, Length: e.Length})
case *tg.MessageEntityFormattedDate:
out = append(out, domain.MessageEntity{
Type: domain.MessageEntityFormattedDate,
Offset: e.Offset,
Length: e.Length,
Date: e.Date,
Relative: e.Relative || e.GetRelative(),
ShortTime: e.ShortTime || e.GetShortTime(),
LongTime: e.LongTime || e.GetLongTime(),
ShortDate: e.ShortDate || e.GetShortDate(),
LongDate: e.LongDate || e.GetLongDate(),
DayOfWeek: e.DayOfWeek || e.GetDayOfWeek(),
})
}
}
return out

View file

@ -3,6 +3,7 @@ package rpc
import (
"testing"
"github.com/gotd/td/bin"
"github.com/gotd/td/tg"
"telesrv/internal/domain"
@ -116,6 +117,7 @@ func TestMessageEntitiesRoundTripAllStyledTypes(t *testing.T) {
&tg.MessageEntityMention{Offset: 12, Length: 13},
&tg.MessageEntityHashtag{Offset: 13, Length: 14},
&tg.MessageEntityURL{Offset: 14, Length: 15},
&tg.MessageEntityFormattedDate{Offset: 15, Length: 16, Date: 1773436800, ShortTime: true, LongDate: true, DayOfWeek: true},
}
converted := domainMessageEntitiesForViewer(viewerID, in)
if len(converted) != len(in) {
@ -133,6 +135,9 @@ func TestMessageEntitiesRoundTripAllStyledTypes(t *testing.T) {
if converted[11].DocumentID != 777 {
t.Fatalf("custom emoji document = %d, want 777", converted[11].DocumentID)
}
if formatted := converted[15]; formatted.Type != domain.MessageEntityFormattedDate || formatted.Date != 1773436800 || !formatted.ShortTime || !formatted.LongDate || !formatted.DayOfWeek {
t.Fatalf("formatted date entity = %+v, want date and flags preserved", formatted)
}
out := tgMessageEntities(converted)
if len(out) != len(in) {
t.Fatalf("round-trip produced %d entities, want %d", len(out), len(in))
@ -143,4 +148,22 @@ func TestMessageEntitiesRoundTripAllStyledTypes(t *testing.T) {
if mention, ok := out[8].(*tg.MessageEntityMentionName); !ok || mention.UserID != viewerID {
t.Fatalf("self mention round-trip = %#v, want messageEntityMentionName self", out[8])
}
if formatted, ok := out[15].(*tg.MessageEntityFormattedDate); !ok || formatted.Date != 1773436800 || !formatted.ShortTime || !formatted.LongDate || !formatted.DayOfWeek {
t.Fatalf("formatted date round-trip = %#v, want date and flags preserved", out[15])
}
}
func TestMessageEntityFormattedDatePreservesFlagsOnlyInput(t *testing.T) {
var flags bin.Fields
flags.Set(1) // short_time
flags.Set(4) // long_date
converted := domainMessageEntities([]tg.MessageEntityClass{
&tg.MessageEntityFormattedDate{Flags: flags, Offset: 1, Length: 2, Date: 1773436800},
})
if len(converted) != 1 {
t.Fatalf("converted %d entities, want one", len(converted))
}
if got := converted[0]; got.Type != domain.MessageEntityFormattedDate || got.Date != 1773436800 || !got.ShortTime || !got.LongDate {
t.Fatalf("formatted date flags-only input = %+v, want short_time/long_date preserved", got)
}
}

View file

@ -29,6 +29,7 @@ func TestMessagesSendMessageReturnsUpdateAndRecordsOwnerContext(t *testing.T) {
RandomID: 123456,
Entities: []tg.MessageEntityClass{
&tg.MessageEntityBold{Offset: 0, Length: 5},
&tg.MessageEntityFormattedDate{Offset: 6, Length: 8, Date: 1773436800, ShortDate: true, ShortTime: true},
},
}
var in bin.Buffer
@ -51,8 +52,12 @@ func TestMessagesSendMessageReturnsUpdateAndRecordsOwnerContext(t *testing.T) {
if messages.sendUserID != sender.ID || messages.sendReq.SenderUserID != sender.ID || messages.sendReq.RecipientUserID != recipient.ID || messages.sendReq.OriginSessionID != 77 {
t.Fatalf("send context = user %d req %+v, want sender/recipient/session", messages.sendUserID, messages.sendReq)
}
if len(messages.sendReq.Entities) != 1 || messages.sendReq.Entities[0].Type != domain.MessageEntityBold {
t.Fatalf("entities = %+v, want bold entity converted to domain", messages.sendReq.Entities)
if len(messages.sendReq.Entities) != 2 || messages.sendReq.Entities[0].Type != domain.MessageEntityBold {
t.Fatalf("entities = %+v, want bold and formatted date converted to domain", messages.sendReq.Entities)
}
dateEntity := messages.sendReq.Entities[1]
if dateEntity.Type != domain.MessageEntityFormattedDate || dateEntity.Date != 1773436800 || !dateEntity.ShortDate || !dateEntity.ShortTime {
t.Fatalf("formatted date entity = %+v, want date and flags preserved", dateEntity)
}
if len(got.Updates) != 2 {
t.Fatalf("updates = %+v, want message id + new message", got.Updates)
@ -68,6 +73,13 @@ func TestMessagesSendMessageReturnsUpdateAndRecordsOwnerContext(t *testing.T) {
if !ok || !msg.Out || msg.PeerID.(*tg.PeerUser).UserID != recipient.ID || msg.Message != req.Message {
t.Fatalf("message = %#v, want outgoing private text to recipient", newMsg.Message)
}
if len(msg.Entities) != 2 {
t.Fatalf("message entities = %+v, want 2", msg.Entities)
}
formatted, ok := msg.Entities[1].(*tg.MessageEntityFormattedDate)
if !ok || formatted.Date != 1773436800 || !formatted.ShortDate || !formatted.ShortTime {
t.Fatalf("formatted response entity = %#v, want date and flags preserved", msg.Entities[1])
}
if metrics.messageSend != 1 || metrics.messageSendErr != nil {
t.Fatalf("metrics send=%d err=%v, want one successful send", metrics.messageSend, metrics.messageSendErr)
}

View file

@ -1481,7 +1481,8 @@ func storyCaptionEntitySupported(entity tg.MessageEntityClass) bool {
*tg.MessageEntityBankCard,
*tg.MessageEntitySpoiler,
*tg.MessageEntityCustomEmoji,
*tg.MessageEntityBlockquote:
*tg.MessageEntityBlockquote,
*tg.MessageEntityFormattedDate:
return true
default:
return false

View file

@ -7048,8 +7048,11 @@ func TestStoriesCaptionEntitiesRejectMalformedInputsBeforePeerAndMutation(t *tes
PrivacyRules: []tg.InputPrivacyRuleClass{&tg.InputPrivacyValueAllowAll{}},
RandomID: 7320,
Period: 86400,
Caption: "🙂 ok",
Entities: []tg.MessageEntityClass{&tg.MessageEntityBold{Offset: 0, Length: 2}},
Caption: "🙂 ok soon",
Entities: []tg.MessageEntityClass{
&tg.MessageEntityBold{Offset: 0, Length: 2},
&tg.MessageEntityFormattedDate{Offset: 6, Length: 4, Date: 1773436800, ShortDate: true, ShortTime: true},
},
}
updates, err := r.onStoriesSendStory(reqCtx, validReq)
if err != nil {
@ -7057,12 +7060,15 @@ func TestStoriesCaptionEntitiesRejectMalformedInputsBeforePeerAndMutation(t *tes
}
item := updates.(*tg.Updates).Updates[1].(*tg.UpdateStory).Story.(*tg.StoryItem)
entities, ok := item.GetEntities()
if !ok || len(entities) != 1 {
t.Fatalf("story entities = ok %v %+v, want one entity", ok, entities)
if !ok || len(entities) != 2 {
t.Fatalf("story entities = ok %v %+v, want two entities", ok, entities)
}
if bold, ok := entities[0].(*tg.MessageEntityBold); !ok || bold.Offset != 0 || bold.Length != 2 {
t.Fatalf("story entity = %T %+v, want bold offset 0 length 2", entities[0], entities[0])
}
if formatted, ok := entities[1].(*tg.MessageEntityFormattedDate); !ok || formatted.Offset != 6 || formatted.Length != 4 || formatted.Date != 1773436800 || !formatted.ShortDate || !formatted.ShortTime {
t.Fatalf("story entity = %T %+v, want formatted date preserved", entities[1], entities[1])
}
storyID := updates.(*tg.Updates).Updates[0].(*tg.UpdateStoryID).ID
var typedNilBold *tg.MessageEntityBold
@ -7139,7 +7145,7 @@ func TestStoriesCaptionEntitiesRejectMalformedInputsBeforePeerAndMutation(t *tes
t.Fatalf("stories after rejected caption entities = %+v, want original story only", stories.Stories.Stories)
}
stored := stories.Stories.Stories[0].(*tg.StoryItem)
if stored.Caption != "🙂 ok" || stored.Edited {
if stored.Caption != validReq.Caption || stored.Edited {
t.Fatalf("story after rejected caption entities = %+v, want original caption and not edited", stored)
}
}