feat: sync rich text message support

This commit is contained in:
A 2026-07-04 21:15:35 +08:00
parent 4d3bbeabd8
commit 7c9d8dda16
41 changed files with 1164 additions and 94 deletions

View file

@ -9,8 +9,7 @@ import (
)
func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChannelMessageRequest) (domain.EditChannelMessageResult, error) {
// 空文本只在媒体替换live location 续报/停止)时合法。
if req.UserID == 0 || req.ChannelID == 0 || req.ID <= 0 || (strings.TrimSpace(req.Message) == "" && req.Media == nil) {
if req.UserID == 0 || req.ChannelID == 0 || req.ID <= 0 {
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
@ -26,6 +25,17 @@ func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChan
}
prevMsg := s.messages[req.ChannelID][idx]
msg := prevMsg
finalMedia := msg.Media
if req.Media != nil {
finalMedia = req.Media
}
finalRich := msg.RichMessage
if req.SetRichMessage {
finalRich = req.RichMessage
}
if strings.TrimSpace(req.Message) == "" && finalMedia.IsZero() && finalRich.IsZero() {
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
}
// WebPageResolve频道链接预览就地替换服务端内部幂等守卫即授权。只换 media、
// 不碰 body/entities/edit_date事件为 channel_web_page。
if req.WebPageResolve {
@ -67,7 +77,8 @@ func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChan
if !viaBotEdit && msg.SenderUserID != req.UserID && !canEditChannelMessage(member) && !participantTodoEdit {
return domain.EditChannelMessageResult{}, domain.ErrMessageAuthorRequired
}
if req.Media == nil && !req.SetReplyMarkup && msg.Body == req.Message && sameMessageEntities(msg.Entities, req.Entities) {
richChanged := req.SetRichMessage && !richMessagesEqual(msg.RichMessage, req.RichMessage)
if req.Media == nil && !req.SetReplyMarkup && !richChanged && msg.Body == req.Message && sameMessageEntities(msg.Entities, req.Entities) {
return domain.EditChannelMessageResult{}, domain.ErrMessageNotModified
}
pts := s.nextChannelPtsLocked(req.ChannelID)
@ -80,6 +91,9 @@ func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChan
if req.SetReplyMarkup {
msg.ReplyMarkup = cloneReplyMarkup(req.ReplyMarkup)
}
if req.SetRichMessage {
msg.RichMessage = cloneRichMessage(req.RichMessage)
}
msg.EditDate = req.EditDate
msg.Pts = pts
s.messages[req.ChannelID][idx] = msg

View file

@ -30,6 +30,7 @@ func cloneChannelMessage(in domain.ChannelMessage) domain.ChannelMessage {
in.ReplyTo = cloneMessageReply(in.ReplyTo)
in.Forward = cloneMessageForward(in.Forward)
in.ReplyMarkup = cloneReplyMarkup(in.ReplyMarkup)
in.RichMessage = cloneRichMessage(in.RichMessage)
in.Discussion = cloneChannelDiscussionRef(in.Discussion)
in.Replies = cloneChannelMessageReplies(in.Replies)
in.Reactions = cloneChannelMessageReactionsPtr(in.Reactions)

View file

@ -11,7 +11,7 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
if req.UserID == 0 || req.ChannelID == 0 {
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
}
if strings.TrimSpace(req.Message) == "" && req.Action == nil && req.Media.IsZero() {
if strings.TrimSpace(req.Message) == "" && req.Action == nil && req.Media.IsZero() && req.RichMessage.IsZero() {
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
@ -83,6 +83,7 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
NoForwards: req.NoForwards || channel.NoForwards || linked.NoForwards,
Body: req.Message,
Entities: append([]domain.MessageEntity(nil), req.Entities...),
RichMessage: cloneRichMessage(req.RichMessage),
Forward: &domain.MessageForward{From: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}, Date: req.Date, ChannelPost: msgID, SavedFrom: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}, SavedFromMsgID: msgID},
ViaBotID: req.ViaBotID,
GroupedID: req.GroupedID,
@ -131,6 +132,7 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
Body: req.Message,
Entities: append([]domain.MessageEntity(nil), req.Entities...),
Media: req.Media,
RichMessage: cloneRichMessage(req.RichMessage),
ReplyTo: replyTo,
Forward: cloneMessageForward(req.Forward),
ViaBotID: req.ViaBotID,

View file

@ -840,6 +840,7 @@ func cloneDialogDraft(draft domain.DialogDraft) domain.DialogDraft {
webpage := *draft.WebPage
draft.WebPage = &webpage
}
draft.RichMessage = cloneRichMessage(draft.RichMessage)
return draft
}

View file

@ -41,10 +41,19 @@ func (s *MessageStore) EditMessage(_ context.Context, req domain.EditMessageRequ
return res, domain.ErrMessageNotModified
}
}
if req.Message == "" && req.Media == nil && target.Media.IsZero() {
finalMedia := target.Media
if req.Media != nil {
finalMedia = req.Media
}
finalRich := target.RichMessage
if req.SetRichMessage {
finalRich = req.RichMessage
}
if req.Message == "" && finalMedia.IsZero() && finalRich.IsZero() {
return res, domain.ErrMessageEmpty
}
if req.Media == nil && !req.SetReplyMarkup && target.Body == req.Message && target.HideEdited == req.HideEdited && equalMessageEntities(target.Entities, req.Entities) {
richChanged := req.SetRichMessage && !richMessagesEqual(target.RichMessage, req.RichMessage)
if req.Media == nil && !req.SetReplyMarkup && !richChanged && target.Body == req.Message && target.HideEdited == req.HideEdited && equalMessageEntities(target.Entities, req.Entities) {
return res, domain.ErrMessageNotModified
}
messageSenderID := target.From.ID
@ -73,6 +82,9 @@ func (s *MessageStore) EditMessage(_ context.Context, req domain.EditMessageRequ
// 替换 markupnil/空 = 清空键盘);双盒一致。
msg.ReplyMarkup = cloneReplyMarkup(req.ReplyMarkup)
}
if req.SetRichMessage {
msg.RichMessage = cloneRichMessage(req.RichMessage)
}
msg.EditDate = req.EditDate
msg.HideEdited = req.HideEdited
msg.Pts = s.nextPtsLocked(userID)

View file

@ -3,6 +3,7 @@ package memory
import (
"encoding/binary"
"hash/fnv"
"reflect"
"sort"
"telesrv/internal/domain"
)
@ -118,6 +119,13 @@ func cloneRichMessage(m *domain.MessageRichMessage) *domain.MessageRichMessage {
return &clone
}
func richMessagesEqual(a, b *domain.MessageRichMessage) bool {
if a.IsZero() && b.IsZero() {
return true
}
return reflect.DeepEqual(a, b)
}
func cloneMessageReply(reply *domain.MessageReply) *domain.MessageReply {
if reply == nil {
return nil

View file

@ -9,6 +9,12 @@ import (
"telesrv/internal/domain"
)
var validRichMessageBlocks = []byte{
0x15, 0xc4, 0xb5, 0x1c, // vector
0x01, 0x00, 0x00, 0x00, // count
0x8a, 0x7e, 0x56, 0x13, // pageBlockUnsupported
}
func TestMessageStoreSendPrivateTextCreatesBothOwnerBoxes(t *testing.T) {
ctx := context.Background()
dialogs := NewDialogStore()
@ -84,6 +90,41 @@ func TestMessageStoreSendPrivateTextCreatesBothOwnerBoxes(t *testing.T) {
}
}
func TestMessageStoreEditRichOnlyMessageUsesFinalContentState(t *testing.T) {
ctx := context.Background()
messages := NewMessageStore()
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: 1000000001,
RecipientUserID: 1000000002,
RandomID: 201,
Date: 1700000200,
RichMessage: &domain.MessageRichMessage{Blocks: validRichMessageBlocks},
})
if err != nil {
t.Fatalf("SendPrivateText rich-only: %v", err)
}
edited, err := messages.EditMessage(ctx, domain.EditMessageRequest{
OwnerUserID: 1000000001,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002},
ID: sent.SenderMessage.ID,
Message: "",
HideEdited: true,
EditDate: 1700000210,
})
if err != nil {
t.Fatalf("EditMessage rich-only hide-edit: %v", err)
}
if len(edited.Edited) != 2 {
t.Fatalf("edited boxes = %d, want 2", len(edited.Edited))
}
for _, item := range edited.Edited {
if item.Message.RichMessage.IsZero() {
t.Fatalf("edited rich-only box for user %d lost rich message: %+v", item.UserID, item.Message)
}
}
}
func TestMessageStoreWebViewDataServiceActionRoundTrip(t *testing.T) {
ctx := context.Background()
messages := NewMessageStore()