feat: sync rich text message support
This commit is contained in:
parent
4d3bbeabd8
commit
7c9d8dda16
41 changed files with 1164 additions and 94 deletions
|
|
@ -12,8 +12,7 @@ import (
|
|||
)
|
||||
|
||||
func (s *ChannelStore) EditChannelMessage(ctx 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
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
|
|
@ -31,6 +30,10 @@ func (s *ChannelStore) EditChannelMessage(ctx context.Context, req domain.EditCh
|
|||
if err != nil {
|
||||
return domain.EditChannelMessageResult{}, fmt.Errorf("encode channel edit reply markup: %w", err)
|
||||
}
|
||||
richMessageJSON, err := encodeRichMessage(req.RichMessage)
|
||||
if err != nil {
|
||||
return domain.EditChannelMessageResult{}, fmt.Errorf("encode channel edit rich message: %w", err)
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return domain.EditChannelMessageResult{}, fmt.Errorf("begin edit channel message: %w", err)
|
||||
|
|
@ -52,6 +55,17 @@ func (s *ChannelStore) EditChannelMessage(ctx context.Context, req domain.EditCh
|
|||
if msg.Deleted || msg.Action != nil {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
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
|
||||
}
|
||||
if req.WebPageResolve {
|
||||
// 频道链接预览就地替换:只换 media(不碰 body/entities/edit_date)+ reserve 频道 pts +
|
||||
// channel_web_page 事件。幂等守卫:仅当前 media 仍是匹配 id 的 pending 占位才换。
|
||||
|
|
@ -110,7 +124,8 @@ WHERE channel_id = $1 AND id = $2`, req.ChannelID, req.ID, mediaJSON, pts); err
|
|||
if !canWriteEdit {
|
||||
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
|
||||
}
|
||||
ptsCount := 1
|
||||
|
|
@ -130,9 +145,10 @@ SET body = $4,
|
|||
edit_date = $6,
|
||||
pts = $7,
|
||||
reply_markup = CASE WHEN $9 THEN $10::jsonb ELSE reply_markup END,
|
||||
rich_message = CASE WHEN $11 THEN $12::jsonb ELSE rich_message END,
|
||||
updated_at = now()
|
||||
WHERE channel_id = $1 AND id = $2 AND NOT deleted AND (sender_user_id = $3 OR $8)`,
|
||||
req.ChannelID, req.ID, req.UserID, req.Message, entities, req.EditDate, editPts, canWriteEdit, req.SetReplyMarkup, string(replyMarkupJSON)); err != nil {
|
||||
req.ChannelID, req.ID, req.UserID, req.Message, entities, req.EditDate, editPts, canWriteEdit, req.SetReplyMarkup, string(replyMarkupJSON), req.SetRichMessage, string(richMessageJSON)); err != nil {
|
||||
return domain.EditChannelMessageResult{}, fmt.Errorf("update channel edit: %w", err)
|
||||
}
|
||||
if req.Media != nil {
|
||||
|
|
@ -160,6 +176,12 @@ WHERE channel_id = $1 AND id = $2`, req.ChannelID, req.ID, mediaJSON); err != ni
|
|||
return domain.EditChannelMessageResult{}, fmt.Errorf("decode channel edit reply markup: %w", err)
|
||||
}
|
||||
}
|
||||
if req.SetRichMessage {
|
||||
msg.RichMessage, err = decodeRichMessage(string(richMessageJSON))
|
||||
if err != nil {
|
||||
return domain.EditChannelMessageResult{}, fmt.Errorf("decode channel edit rich message: %w", err)
|
||||
}
|
||||
}
|
||||
msg.EditDate = req.EditDate
|
||||
msg.Pts = editPts
|
||||
event := domain.ChannelUpdateEvent{
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func scanChannelMessage(row rowScanner) (domain.ChannelMessage, error) {
|
|||
var entities, reply, forward, action string
|
||||
var mediaJSON string
|
||||
var replyMarkupJSON string
|
||||
var richMessageJSON string
|
||||
var savedPeerType string
|
||||
var savedPeerID int64
|
||||
if err := row.Scan(
|
||||
|
|
@ -37,7 +38,7 @@ func scanChannelMessage(row rowScanner) (domain.ChannelMessage, error) {
|
|||
&sendAsType, &sendAsID, &msg.Date, &msg.EditDate, &msg.Post, &msg.Silent, &msg.NoForwards,
|
||||
&msg.Body, &entities, &reply, &replyMsgID, &replyPeerType, &replyPeerID, &replyTopID,
|
||||
&forward, &discussionChannelID, &discussionMessageID, &action, &msg.Pts, &msg.Deleted, &mediaJSON,
|
||||
&replyMarkupJSON, &msg.TTLPeriod, &msg.ExpiresAt, &msg.ViewsCount, &msg.PostAuthor, &msg.Pinned, &msg.ViaBotID, &msg.GroupedID, &msg.FromBoostsApplied, &savedPeerType, &savedPeerID,
|
||||
&replyMarkupJSON, &richMessageJSON, &msg.TTLPeriod, &msg.ExpiresAt, &msg.ViewsCount, &msg.PostAuthor, &msg.Pinned, &msg.ViaBotID, &msg.GroupedID, &msg.FromBoostsApplied, &savedPeerType, &savedPeerID,
|
||||
); err != nil {
|
||||
return domain.ChannelMessage{}, err
|
||||
}
|
||||
|
|
@ -65,6 +66,10 @@ func scanChannelMessage(row rowScanner) (domain.ChannelMessage, error) {
|
|||
if err != nil {
|
||||
return domain.ChannelMessage{}, err
|
||||
}
|
||||
msg.RichMessage, err = decodeRichMessage(richMessageJSON)
|
||||
if err != nil {
|
||||
return domain.ChannelMessage{}, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +87,7 @@ func scanChannelMessageWithCount(row rowScanner) (domain.ChannelMessage, int, er
|
|||
var count int
|
||||
var mediaJSON string
|
||||
var replyMarkupJSON string
|
||||
var richMessageJSON string
|
||||
var savedPeerType string
|
||||
var savedPeerID int64
|
||||
if err := row.Scan(
|
||||
|
|
@ -89,7 +95,7 @@ func scanChannelMessageWithCount(row rowScanner) (domain.ChannelMessage, int, er
|
|||
&sendAsType, &sendAsID, &msg.Date, &msg.EditDate, &msg.Post, &msg.Silent, &msg.NoForwards,
|
||||
&msg.Body, &entities, &reply, &replyMsgID, &replyPeerType, &replyPeerID, &replyTopID,
|
||||
&forward, &discussionChannelID, &discussionMessageID, &action, &msg.Pts, &msg.Deleted, &mediaJSON,
|
||||
&replyMarkupJSON, &msg.TTLPeriod, &msg.ExpiresAt, &msg.ViewsCount, &msg.PostAuthor, &msg.Pinned, &msg.ViaBotID, &msg.GroupedID, &msg.FromBoostsApplied, &savedPeerType, &savedPeerID, &count,
|
||||
&replyMarkupJSON, &richMessageJSON, &msg.TTLPeriod, &msg.ExpiresAt, &msg.ViewsCount, &msg.PostAuthor, &msg.Pinned, &msg.ViaBotID, &msg.GroupedID, &msg.FromBoostsApplied, &savedPeerType, &savedPeerID, &count,
|
||||
); err != nil {
|
||||
return domain.ChannelMessage{}, 0, err
|
||||
}
|
||||
|
|
@ -117,6 +123,10 @@ func scanChannelMessageWithCount(row rowScanner) (domain.ChannelMessage, int, er
|
|||
if err != nil {
|
||||
return domain.ChannelMessage{}, 0, err
|
||||
}
|
||||
msg.RichMessage, err = decodeRichMessage(richMessageJSON)
|
||||
if err != nil {
|
||||
return domain.ChannelMessage{}, 0, err
|
||||
}
|
||||
return msg, count, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func (s *ChannelStore) SendChannelMessage(ctx context.Context, req domain.SendChannelMessageRequest) (domain.SendChannelMessageResult, error) {
|
||||
if req.UserID == 0 || req.ChannelID == 0 || (strings.TrimSpace(req.Message) == "" && req.Action == nil && req.Media.IsZero()) {
|
||||
if req.UserID == 0 || req.ChannelID == 0 || (strings.TrimSpace(req.Message) == "" && req.Action == nil && req.Media.IsZero() && req.RichMessage.IsZero()) {
|
||||
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
if req.Date == 0 {
|
||||
|
|
@ -123,6 +123,7 @@ func (s *ChannelStore) sendChannelMessageOnce(ctx context.Context, req domain.Se
|
|||
Body: req.Message,
|
||||
Entities: append([]domain.MessageEntity(nil), req.Entities...),
|
||||
Media: req.Media,
|
||||
RichMessage: req.RichMessage,
|
||||
ViaBotID: req.ViaBotID,
|
||||
GroupedID: req.GroupedID,
|
||||
ReplyMarkup: req.ReplyMarkup,
|
||||
|
|
@ -180,6 +181,7 @@ func (s *ChannelStore) sendChannelMessageOnce(ctx context.Context, req domain.Se
|
|||
Body: req.Message,
|
||||
Entities: append([]domain.MessageEntity(nil), req.Entities...),
|
||||
Media: req.Media,
|
||||
RichMessage: req.RichMessage,
|
||||
ViaBotID: req.ViaBotID,
|
||||
GroupedID: req.GroupedID,
|
||||
ReplyMarkup: req.ReplyMarkup,
|
||||
|
|
@ -422,6 +424,10 @@ func insertChannelMessageTx(ctx context.Context, tx pgx.Tx, msg domain.ChannelMe
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
richMessage, err := encodeRichMessage(msg.RichMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var sendAsType sql.NullString
|
||||
var sendAsID sql.NullInt64
|
||||
if msg.SendAs != nil && msg.SendAs.ID != 0 {
|
||||
|
|
@ -450,12 +456,12 @@ INSERT INTO channel_messages (
|
|||
channel_id, id, random_id, sender_user_id, from_peer_type, from_peer_id,
|
||||
send_as_peer_type, send_as_peer_id, message_date, edit_date, post, silent, noforwards,
|
||||
body, entities, reply_to, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id,
|
||||
fwd_from, discussion_channel_id, discussion_message_id, action, pts, deleted, media, reply_markup, ttl_period, expires_at, post_author, via_bot_id, from_boosts_applied, grouped_id, saved_peer_type, saved_peer_id
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36)`,
|
||||
fwd_from, discussion_channel_id, discussion_message_id, action, pts, deleted, media, reply_markup, rich_message, ttl_period, expires_at, post_author, via_bot_id, from_boosts_applied, grouped_id, saved_peer_type, saved_peer_id
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37)`,
|
||||
msg.ChannelID, msg.ID, msg.RandomID, msg.SenderUserID, string(msg.From.Type), msg.From.ID,
|
||||
sendAsType, sendAsID, msg.Date, msg.EditDate, msg.Post, msg.Silent, msg.NoForwards,
|
||||
msg.Body, entities, reply, replyMsgID, replyPeerType, replyPeerID, replyTopID,
|
||||
forward, discussionChannelID, discussionMessageID, action, msg.Pts, msg.Deleted, media, replyMarkup, msg.TTLPeriod, msg.ExpiresAt, msg.PostAuthor, msg.ViaBotID, msg.FromBoostsApplied, msg.GroupedID, string(msg.SavedPeer.Type), msg.SavedPeer.ID); err != nil {
|
||||
forward, discussionChannelID, discussionMessageID, action, msg.Pts, msg.Deleted, media, replyMarkup, richMessage, msg.TTLPeriod, msg.ExpiresAt, msg.PostAuthor, msg.ViaBotID, msg.FromBoostsApplied, msg.GroupedID, string(msg.SavedPeer.Type), msg.SavedPeer.ID); err != nil {
|
||||
return fmt.Errorf("insert channel message: %w", err)
|
||||
}
|
||||
// 共享媒体索引(迁移 0118):创建即按媒体类别建索引行,供 messages.search 媒体标签页。
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const channelMessageColumns = `channel_id, id, random_id, sender_user_id, from_p
|
|||
send_as_peer_type, send_as_peer_id, message_date, edit_date, post, silent, noforwards, body,
|
||||
entities::text, reply_to::text, reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id,
|
||||
fwd_from::text, discussion_channel_id, discussion_message_id, action::text, pts, deleted, media::text,
|
||||
reply_markup::text, ttl_period, expires_at, views_count, post_author, pinned, via_bot_id, grouped_id, from_boosts_applied, saved_peer_type, saved_peer_id`
|
||||
reply_markup::text, rich_message::text, ttl_period, expires_at, views_count, post_author, pinned, via_bot_id, grouped_id, from_boosts_applied, saved_peer_type, saved_peer_id`
|
||||
|
||||
const channelForumTopicColumns = `channel_id, topic_id, creator_user_id, title, icon_color, icon_emoji_id,
|
||||
title_missing, closed, hidden, pinned, pinned_order, date, top_message_id, read_inbox_max_id,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package postgres
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
|
@ -93,6 +94,13 @@ func decodeRichMessage(s string) (*domain.MessageRichMessage, error) {
|
|||
return &m, nil
|
||||
}
|
||||
|
||||
func richMessagesEqual(a, b *domain.MessageRichMessage) bool {
|
||||
if a.IsZero() && b.IsZero() {
|
||||
return true
|
||||
}
|
||||
return reflect.DeepEqual(a, b)
|
||||
}
|
||||
|
||||
func decodePhotoSizes(s string) ([]domain.PhotoSize, error) {
|
||||
if s == "" || s == "[]" || s == "null" {
|
||||
return nil, nil
|
||||
|
|
|
|||
|
|
@ -54,17 +54,26 @@ func (s *MessageStore) EditMessage(ctx context.Context, req domain.EditMessageRe
|
|||
PeerType: string(req.Peer.Type),
|
||||
PeerID: req.Peer.ID,
|
||||
})
|
||||
// 空文本只在目标消息携带媒体(或本次写入媒体)时合法(清空 caption);
|
||||
// 纯文本消息清空会留下既无 body 也无 media 的空壳。
|
||||
if err == nil && req.Message == "" && req.Media == nil && (target.MediaJson == "" || target.MediaJson == "{}") {
|
||||
return res, domain.ErrMessageEmpty
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return res, domain.ErrMessageIDInvalid
|
||||
}
|
||||
return res, fmt.Errorf("get message for edit: %w", err)
|
||||
}
|
||||
targetRich, err := decodeRichMessage(target.RichMessageJson)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("decode target rich message: %w", err)
|
||||
}
|
||||
// 空文本只在目标消息携带媒体/rich(或本次写入媒体/rich)时合法(清空 caption)。
|
||||
if req.Message == "" && req.Media == nil && (target.MediaJson == "" || target.MediaJson == "{}") {
|
||||
if req.SetRichMessage {
|
||||
if req.RichMessage.IsZero() {
|
||||
return res, domain.ErrMessageEmpty
|
||||
}
|
||||
} else if targetRich.IsZero() {
|
||||
return res, domain.ErrMessageEmpty
|
||||
}
|
||||
}
|
||||
oldEntities, err := decodeMessageEntities(target.EntitiesJson)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("decode target entities: %w", err)
|
||||
|
|
@ -74,13 +83,18 @@ func (s *MessageStore) EditMessage(ctx context.Context, req domain.EditMessageRe
|
|||
if !authorEdit && !viaBotEdit && !req.WebPageResolve && !validTodoParticipantEdit(req, target, oldEntities) {
|
||||
return res, domain.ErrMessageAuthorRequired
|
||||
}
|
||||
if req.Media == nil && !req.SetReplyMarkup && target.Body == req.Message && target.HideEdited == req.HideEdited && sameMessageEntities(oldEntities, req.Entities) {
|
||||
richChanged := req.SetRichMessage && !richMessagesEqual(targetRich, req.RichMessage)
|
||||
if req.Media == nil && !req.SetReplyMarkup && !richChanged && target.Body == req.Message && target.HideEdited == req.HideEdited && sameMessageEntities(oldEntities, req.Entities) {
|
||||
return res, domain.ErrMessageNotModified
|
||||
}
|
||||
replyMarkupJSON, err := encodeReplyMarkup(req.ReplyMarkup)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("encode edit reply markup: %w", err)
|
||||
}
|
||||
richMessageJSON, err := encodeRichMessage(req.RichMessage)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("encode edit rich message: %w", err)
|
||||
}
|
||||
messageSenderID := target.MessageSenderID
|
||||
boxes, err := qtx.ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{
|
||||
OwnerUserIds: privateMessageOwnerIDs(req.OwnerUserID, req.Peer.ID),
|
||||
|
|
@ -173,6 +187,8 @@ WHERE owner_user_id = $1 AND box_id = $2`, box.OwnerUserID, box.BoxID, int32(pts
|
|||
HideEdited: req.HideEdited,
|
||||
SetReplyMarkup: req.SetReplyMarkup,
|
||||
ReplyMarkupJson: replyMarkupJSON,
|
||||
SetRichMessage: req.SetRichMessage,
|
||||
RichMessageJson: richMessageJSON,
|
||||
}); err != nil {
|
||||
return res, fmt.Errorf("update private message edit: %w", err)
|
||||
}
|
||||
|
|
@ -210,6 +226,8 @@ WHERE message_sender_id = $1 AND private_message_id = $2`, messageSenderID, targ
|
|||
Pts: int32(pts),
|
||||
SetReplyMarkup: req.SetReplyMarkup,
|
||||
ReplyMarkupJson: replyMarkupJSON,
|
||||
SetRichMessage: req.SetRichMessage,
|
||||
RichMessageJson: richMessageJSON,
|
||||
})
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("update message box edit: %w", err)
|
||||
|
|
|
|||
|
|
@ -1036,6 +1036,10 @@ SET body = sqlc.arg(body)::text,
|
|||
reply_markup = CASE
|
||||
WHEN sqlc.arg(set_reply_markup)::boolean THEN sqlc.arg(reply_markup_json)::jsonb
|
||||
ELSE reply_markup
|
||||
END,
|
||||
rich_message = CASE
|
||||
WHEN sqlc.arg(set_rich_message)::boolean THEN sqlc.arg(rich_message_json)::jsonb
|
||||
ELSE rich_message
|
||||
END
|
||||
WHERE sender_user_id = sqlc.arg(sender_user_id)::bigint
|
||||
AND id = sqlc.arg(private_message_id)::bigint;
|
||||
|
|
@ -1050,6 +1054,10 @@ SET body = sqlc.arg(body)::text,
|
|||
reply_markup = CASE
|
||||
WHEN sqlc.arg(set_reply_markup)::boolean THEN sqlc.arg(reply_markup_json)::jsonb
|
||||
ELSE reply_markup
|
||||
END,
|
||||
rich_message = CASE
|
||||
WHEN sqlc.arg(set_rich_message)::boolean THEN sqlc.arg(rich_message_json)::jsonb
|
||||
ELSE rich_message
|
||||
END
|
||||
WHERE owner_user_id = sqlc.arg(owner_user_id)::bigint
|
||||
AND box_id = sqlc.arg(box_id)::int
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func (s *MessageStore) CreateScheduledMessage(ctx context.Context, req domain.Sc
|
|||
if req.Peer.Type != domain.PeerTypeUser && req.Peer.Type != domain.PeerTypeChannel {
|
||||
return domain.ScheduledMessage{}, fmt.Errorf("create scheduled message: invalid peer")
|
||||
}
|
||||
if req.Message == "" && req.Media.IsZero() {
|
||||
if req.Message == "" && req.Media.IsZero() && req.RichMessage.IsZero() {
|
||||
return domain.ScheduledMessage{}, fmt.Errorf("create scheduled message: empty message")
|
||||
}
|
||||
if req.Date == 0 {
|
||||
|
|
@ -41,6 +41,10 @@ func (s *MessageStore) CreateScheduledMessage(ctx context.Context, req domain.Sc
|
|||
if err != nil {
|
||||
return domain.ScheduledMessage{}, err
|
||||
}
|
||||
richMessage, err := encodeRichMessage(req.RichMessage)
|
||||
if err != nil {
|
||||
return domain.ScheduledMessage{}, err
|
||||
}
|
||||
meta, err := messageMetadataParamsFrom(req.Silent, req.NoForwards, req.ReplyTo, req.Forward)
|
||||
if err != nil {
|
||||
return domain.ScheduledMessage{}, err
|
||||
|
|
@ -87,7 +91,7 @@ func (s *MessageStore) CreateScheduledMessage(ctx context.Context, req domain.Sc
|
|||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO scheduled_messages (
|
||||
owner_user_id, scheduled_id, peer_type, peer_id, random_id, message_date,
|
||||
body, entities, media, silent, noforwards,
|
||||
body, entities, media, rich_message, silent, noforwards,
|
||||
reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id,
|
||||
quote_text, quote_entities, quote_offset,
|
||||
fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date,
|
||||
|
|
@ -95,14 +99,14 @@ INSERT INTO scheduled_messages (
|
|||
schedule_date, schedule_repeat_period, state, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8::jsonb, $9::jsonb, $10, $11,
|
||||
$12, $13, $14, $15,
|
||||
$16, $17::jsonb, $18,
|
||||
$19, $20, $21, $22,
|
||||
$23, $24,
|
||||
$25, $26, 'pending', $27, $27
|
||||
$7, $8::jsonb, $9::jsonb, $10::jsonb, $11, $12,
|
||||
$13, $14, $15, $16,
|
||||
$17, $18::jsonb, $19,
|
||||
$20, $21, $22, $23,
|
||||
$24, $25,
|
||||
$26, $27, 'pending', $28, $28
|
||||
)`, req.OwnerUserID, nextID, string(req.Peer.Type), req.Peer.ID, req.RandomID, req.Date,
|
||||
req.Message, entities, media, req.Silent, req.NoForwards,
|
||||
req.Message, entities, media, richMessage, req.Silent, req.NoForwards,
|
||||
meta.ReplyToMsgID, meta.ReplyToPeerType, meta.ReplyToPeerID, meta.ReplyToTopID,
|
||||
meta.QuoteText, meta.QuoteEntitiesJSON, meta.QuoteOffset,
|
||||
meta.FwdFromPeerType, meta.FwdFromPeerID, meta.FwdFromName, meta.FwdDate,
|
||||
|
|
@ -167,29 +171,38 @@ func (s *MessageStore) EditScheduledMessage(ctx context.Context, req domain.Edit
|
|||
}
|
||||
message := current.Message
|
||||
entities := append([]domain.MessageEntity(nil), current.Entities...)
|
||||
richMessage := current.RichMessage
|
||||
if req.SetMessage {
|
||||
if req.Message == "" && current.Media.IsZero() {
|
||||
return domain.ScheduledMessage{}, domain.ErrMessageEmpty
|
||||
}
|
||||
message = req.Message
|
||||
entities = append([]domain.MessageEntity(nil), req.Entities...)
|
||||
}
|
||||
if req.SetRichMessage {
|
||||
richMessage = req.RichMessage
|
||||
}
|
||||
if message == "" && current.Media.IsZero() && richMessage.IsZero() {
|
||||
return domain.ScheduledMessage{}, domain.ErrMessageEmpty
|
||||
}
|
||||
encodedEntities, err := encodeMessageEntities(entities)
|
||||
if err != nil {
|
||||
return domain.ScheduledMessage{}, err
|
||||
}
|
||||
encodedRichMessage, err := encodeRichMessage(richMessage)
|
||||
if err != nil {
|
||||
return domain.ScheduledMessage{}, err
|
||||
}
|
||||
row := tx.QueryRow(ctx, `
|
||||
UPDATE scheduled_messages
|
||||
SET body = $5,
|
||||
entities = $6::jsonb,
|
||||
schedule_date = $7,
|
||||
updated_at = $8
|
||||
rich_message = $7::jsonb,
|
||||
schedule_date = $8,
|
||||
updated_at = $9
|
||||
WHERE owner_user_id = $1
|
||||
AND peer_type = $2
|
||||
AND peer_id = $3
|
||||
AND scheduled_id = $4
|
||||
AND state = 'pending'
|
||||
RETURNING `+scheduledMessageSelectColumns(), req.OwnerUserID, string(req.Peer.Type), req.Peer.ID, req.ID, message, encodedEntities, req.ScheduleDate, req.Date)
|
||||
RETURNING `+scheduledMessageSelectColumns(), req.OwnerUserID, string(req.Peer.Type), req.Peer.ID, req.ID, message, encodedEntities, encodedRichMessage, req.ScheduleDate, req.Date)
|
||||
msg, err := scanScheduledMessage(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
|
|
@ -472,7 +485,7 @@ LIMIT $4`, filter.OwnerUserID, string(filter.Peer.Type), filter.Peer.ID, limit)
|
|||
|
||||
func scheduledMessageSelectColumns() string {
|
||||
return `owner_user_id, scheduled_id, peer_type, peer_id, random_id, message_date,
|
||||
body, entities::text, media::text, silent, noforwards,
|
||||
body, entities::text, media::text, rich_message::text, silent, noforwards,
|
||||
reply_to_msg_id, reply_to_peer_type, reply_to_peer_id, reply_to_top_id,
|
||||
quote_text, quote_entities::text, quote_offset,
|
||||
fwd_from_peer_type, fwd_from_peer_id, fwd_from_name, fwd_date,
|
||||
|
|
@ -486,7 +499,7 @@ func scheduledMessageSelectColumnsFor(alias string) string {
|
|||
}
|
||||
prefix := alias + "."
|
||||
return prefix + `owner_user_id, ` + prefix + `scheduled_id, ` + prefix + `peer_type, ` + prefix + `peer_id, ` + prefix + `random_id, ` + prefix + `message_date,
|
||||
` + prefix + `body, ` + prefix + `entities::text, ` + prefix + `media::text, ` + prefix + `silent, ` + prefix + `noforwards,
|
||||
` + prefix + `body, ` + prefix + `entities::text, ` + prefix + `media::text, ` + prefix + `rich_message::text, ` + prefix + `silent, ` + prefix + `noforwards,
|
||||
` + prefix + `reply_to_msg_id, ` + prefix + `reply_to_peer_type, ` + prefix + `reply_to_peer_id, ` + prefix + `reply_to_top_id,
|
||||
` + prefix + `quote_text, ` + prefix + `quote_entities::text, ` + prefix + `quote_offset,
|
||||
` + prefix + `fwd_from_peer_type, ` + prefix + `fwd_from_peer_id, ` + prefix + `fwd_from_name, ` + prefix + `fwd_date,
|
||||
|
|
@ -516,6 +529,7 @@ func scanScheduledMessage(scanner interface{ Scan(...any) error }) (domain.Sched
|
|||
peerType string
|
||||
entitiesJSON string
|
||||
mediaJSON string
|
||||
richMessageJSON string
|
||||
replyToMsgID int32
|
||||
replyToPeerType string
|
||||
replyToPeerID int64
|
||||
|
|
@ -533,7 +547,7 @@ func scanScheduledMessage(scanner interface{ Scan(...any) error }) (domain.Sched
|
|||
)
|
||||
if err := scanner.Scan(
|
||||
&msg.OwnerUserID, &msg.ID, &peerType, &msg.Peer.ID, &msg.RandomID, &msg.CreatedAt,
|
||||
&msg.Message, &entitiesJSON, &mediaJSON, &msg.Silent, &msg.NoForwards,
|
||||
&msg.Message, &entitiesJSON, &mediaJSON, &richMessageJSON, &msg.Silent, &msg.NoForwards,
|
||||
&replyToMsgID, &replyToPeerType, &replyToPeerID, &replyToTopID,
|
||||
"eText, "eEntitiesJSON, "eOffset,
|
||||
&fwdFromPeerType, &fwdFromPeerID, &fwdFromName, &fwdDate,
|
||||
|
|
@ -554,6 +568,11 @@ func scanScheduledMessage(scanner interface{ Scan(...any) error }) (domain.Sched
|
|||
return domain.ScheduledMessage{}, fmt.Errorf("decode scheduled media: %w", err)
|
||||
}
|
||||
msg.Media = media
|
||||
richMessage, err := decodeRichMessage(richMessageJSON)
|
||||
if err != nil {
|
||||
return domain.ScheduledMessage{}, fmt.Errorf("decode scheduled rich message: %w", err)
|
||||
}
|
||||
msg.RichMessage = richMessage
|
||||
// scheduled_messages 不存 saved_from:到点投递经 SendPrivateText 实时
|
||||
// 重算 saved 语义(self-chat 直发归 self),fwd saved 维度恒空。
|
||||
_, _, reply, forward, err := messageMetadataFromFields(
|
||||
|
|
|
|||
|
|
@ -107,4 +107,72 @@ func TestScheduledMessageEditPreservesContentWhenMessageUnset(t *testing.T) {
|
|||
if !errors.Is(err, domain.ErrMessageEmpty) {
|
||||
t.Fatalf("empty text scheduled edit err = %v, want ErrMessageEmpty", err)
|
||||
}
|
||||
|
||||
richInitial := &domain.MessageRichMessage{Rtl: true, Blocks: []byte{1, 2, 3, 4}}
|
||||
richOnly, err := messages.CreateScheduledMessage(ctx, domain.ScheduleMessageRequest{
|
||||
OwnerUserID: owner.ID,
|
||||
Peer: peer,
|
||||
RandomID: 7003,
|
||||
RichMessage: richInitial,
|
||||
ScheduleDate: 1700021600,
|
||||
Date: 1700000500,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create rich scheduled message: %v", err)
|
||||
}
|
||||
if !richMessagesEqual(richOnly.RichMessage, richInitial) || richOnly.Message != "" || !richOnly.Media.IsZero() {
|
||||
t.Fatalf("rich scheduled create = %+v, want rich-only content", richOnly)
|
||||
}
|
||||
gotRich, err := messages.GetScheduledMessages(ctx, domain.ScheduledMessageFilter{
|
||||
OwnerUserID: owner.ID,
|
||||
Peer: peer,
|
||||
IDs: []int{richOnly.ID},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get rich scheduled message: %v", err)
|
||||
}
|
||||
if len(gotRich.Messages) != 1 || !richMessagesEqual(gotRich.Messages[0].RichMessage, richInitial) {
|
||||
t.Fatalf("get rich scheduled = %+v, want original rich", gotRich.Messages)
|
||||
}
|
||||
richDateOnly, err := messages.EditScheduledMessage(ctx, domain.EditScheduledMessageRequest{
|
||||
OwnerUserID: owner.ID,
|
||||
Peer: peer,
|
||||
ID: richOnly.ID,
|
||||
ScheduleDate: 1700025200,
|
||||
Date: 1700000600,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("date-only edit rich scheduled message: %v", err)
|
||||
}
|
||||
if richDateOnly.ScheduleDate != 1700025200 || !richMessagesEqual(richDateOnly.RichMessage, richInitial) {
|
||||
t.Fatalf("date-only rich scheduled edit = %+v, want original rich and new date", richDateOnly)
|
||||
}
|
||||
richEdited := &domain.MessageRichMessage{Part: true, Blocks: []byte{5, 6, 7}}
|
||||
editedRich, err := messages.EditScheduledMessage(ctx, domain.EditScheduledMessageRequest{
|
||||
OwnerUserID: owner.ID,
|
||||
Peer: peer,
|
||||
ID: richOnly.ID,
|
||||
SetRichMessage: true,
|
||||
RichMessage: richEdited,
|
||||
ScheduleDate: 1700028800,
|
||||
Date: 1700000700,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("edit rich scheduled message: %v", err)
|
||||
}
|
||||
if !richMessagesEqual(editedRich.RichMessage, richEdited) || editedRich.ScheduleDate != 1700028800 {
|
||||
t.Fatalf("edited rich scheduled = %+v, want replacement rich", editedRich)
|
||||
}
|
||||
_, err = messages.EditScheduledMessage(ctx, domain.EditScheduledMessageRequest{
|
||||
OwnerUserID: owner.ID,
|
||||
Peer: peer,
|
||||
ID: richOnly.ID,
|
||||
SetRichMessage: true,
|
||||
ScheduleDate: 1700032400,
|
||||
Date: 1700000800,
|
||||
})
|
||||
if !errors.Is(err, domain.ErrMessageEmpty) {
|
||||
t.Fatalf("clear only rich scheduled edit err = %v, want ErrMessageEmpty", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3554,9 +3554,13 @@ SET body = $1::text,
|
|||
reply_markup = CASE
|
||||
WHEN $6::boolean THEN $7::jsonb
|
||||
ELSE reply_markup
|
||||
END,
|
||||
rich_message = CASE
|
||||
WHEN $8::boolean THEN $9::jsonb
|
||||
ELSE rich_message
|
||||
END
|
||||
WHERE owner_user_id = $8::bigint
|
||||
AND box_id = $9::int
|
||||
WHERE owner_user_id = $10::bigint
|
||||
AND box_id = $11::int
|
||||
AND NOT deleted
|
||||
RETURNING
|
||||
box_id,
|
||||
|
|
@ -3613,6 +3617,8 @@ type UpdateMessageBoxEditParams struct {
|
|||
Pts int32
|
||||
SetReplyMarkup bool
|
||||
ReplyMarkupJson []byte
|
||||
SetRichMessage bool
|
||||
RichMessageJson []byte
|
||||
OwnerUserID int64
|
||||
BoxID int32
|
||||
}
|
||||
|
|
@ -3673,6 +3679,8 @@ func (q *Queries) UpdateMessageBoxEdit(ctx context.Context, arg UpdateMessageBox
|
|||
arg.Pts,
|
||||
arg.SetReplyMarkup,
|
||||
arg.ReplyMarkupJson,
|
||||
arg.SetRichMessage,
|
||||
arg.RichMessageJson,
|
||||
arg.OwnerUserID,
|
||||
arg.BoxID,
|
||||
)
|
||||
|
|
@ -3735,9 +3743,13 @@ SET body = $1::text,
|
|||
reply_markup = CASE
|
||||
WHEN $5::boolean THEN $6::jsonb
|
||||
ELSE reply_markup
|
||||
END,
|
||||
rich_message = CASE
|
||||
WHEN $7::boolean THEN $8::jsonb
|
||||
ELSE rich_message
|
||||
END
|
||||
WHERE sender_user_id = $7::bigint
|
||||
AND id = $8::bigint
|
||||
WHERE sender_user_id = $9::bigint
|
||||
AND id = $10::bigint
|
||||
`
|
||||
|
||||
type UpdatePrivateMessageEditParams struct {
|
||||
|
|
@ -3747,6 +3759,8 @@ type UpdatePrivateMessageEditParams struct {
|
|||
HideEdited bool
|
||||
SetReplyMarkup bool
|
||||
ReplyMarkupJson []byte
|
||||
SetRichMessage bool
|
||||
RichMessageJson []byte
|
||||
SenderUserID int64
|
||||
PrivateMessageID int64
|
||||
}
|
||||
|
|
@ -3759,6 +3773,8 @@ func (q *Queries) UpdatePrivateMessageEdit(ctx context.Context, arg UpdatePrivat
|
|||
arg.HideEdited,
|
||||
arg.SetReplyMarkup,
|
||||
arg.ReplyMarkupJson,
|
||||
arg.SetRichMessage,
|
||||
arg.RichMessageJson,
|
||||
arg.SenderUserID,
|
||||
arg.PrivateMessageID,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue