forward chat msg fix
This commit is contained in:
@@ -1325,6 +1325,40 @@ func (r *ChatRepo) SaveSticker(ctx context.Context, userID, key, fromMessageID s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CopyAttachments duplicates one message's attachments onto another.
|
||||||
|
//
|
||||||
|
// Forwarding needs this: a quote, a sticker, a photo or a shared post lives in
|
||||||
|
// an attachment row, not in the body, so copying only body+entities forwards
|
||||||
|
// an empty bubble.
|
||||||
|
//
|
||||||
|
// conversation_id is rewritten to the destination, which is what keeps access
|
||||||
|
// correct: reads authorize against the attachment's OWN conversation, so the
|
||||||
|
// copy is readable by the new thread's participants and the original stays
|
||||||
|
// scoped to the old one. Media rows point at the same stored object rather
|
||||||
|
// than duplicating it.
|
||||||
|
//
|
||||||
|
// Entity rows carry only their reference forward; visibility is re-resolved
|
||||||
|
// per viewer on every read, so forwarding a post cannot leak one.
|
||||||
|
func (r *ChatRepo) CopyAttachments(ctx context.Context, fromMessageID, toMessageID, toConversationID string) error {
|
||||||
|
_, err := r.pg.Pool.Exec(ctx, `
|
||||||
|
INSERT INTO message_attachments
|
||||||
|
(message_id, conversation_id, ord, kind, r2_key, mime, size_bytes,
|
||||||
|
width, height, duration_ms, blurhash, file_name,
|
||||||
|
url, title, description, site_name, image_url,
|
||||||
|
entity_type, entity_id, snapshot)
|
||||||
|
SELECT $2, $3, ord, kind, r2_key, mime, size_bytes,
|
||||||
|
width, height, duration_ms, blurhash, file_name,
|
||||||
|
url, title, description, site_name, image_url,
|
||||||
|
entity_type, entity_id, snapshot
|
||||||
|
FROM message_attachments
|
||||||
|
WHERE message_id = $1
|
||||||
|
ORDER BY ord`, fromMessageID, toMessageID, toConversationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("copy attachments: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReactionDetails lists everyone who reacted to one message, newest first.
|
// ReactionDetails lists everyone who reacted to one message, newest first.
|
||||||
//
|
//
|
||||||
// Separate from ReactionsFor: the grouped counts are what a thread renders,
|
// Separate from ReactionsFor: the grouped counts are what a thread renders,
|
||||||
|
|||||||
@@ -1090,6 +1090,13 @@ func (s *ChatService) ForwardMessage(ctx context.Context, userID, messageID, toC
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if created {
|
if created {
|
||||||
|
// Carry the attachments across. Without this a quote, sticker, photo
|
||||||
|
// or shared post forwards as an empty bubble: its content lives in an
|
||||||
|
// attachment row, not in the body.
|
||||||
|
if err := s.chat.CopyAttachments(ctx, messageID, msg.ID, toConversationID); err != nil {
|
||||||
|
s.logger.Warn("chat: forwarding attachments failed",
|
||||||
|
zap.String("message_id", messageID), zap.Error(err))
|
||||||
|
}
|
||||||
msg = s.hydrateSent(ctx, userID, toConversationID, msg)
|
msg = s.hydrateSent(ctx, userID, toConversationID, msg)
|
||||||
s.publish(ctx, toConversationID, userID, msg)
|
s.publish(ctx, toConversationID, userID, msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2609,3 +2609,96 @@ func TestStickers_SaveFromChat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A quote, sticker, photo or shared post lives in an attachment row, not in
|
||||||
|
// the body — so forwarding one has to carry the attachments across or the
|
||||||
|
// recipient gets an empty bubble.
|
||||||
|
func TestForward_CarriesAttachments(t *testing.T) {
|
||||||
|
svc, _, pg := testChatService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
a, b, c := chatUser(t, pg, "A"), chatUser(t, pg, "B"), chatUser(t, pg, "C")
|
||||||
|
|
||||||
|
source, err := svc.OpenDM(ctx, a, b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cleanupConv(t, pg, source.ID)
|
||||||
|
target, err := svc.OpenDM(ctx, a, c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cleanupConv(t, pg, target.ID)
|
||||||
|
|
||||||
|
// A quote with no body at all — the case that forwarded blank.
|
||||||
|
sent, err := svc.Send(ctx, a, source.ID, SendRequest{
|
||||||
|
Quote: &QuoteAttachment{
|
||||||
|
Path: "/books/bg/2/13?hl=abc",
|
||||||
|
Title: "Bhagavad-gītā 2.13",
|
||||||
|
Text: "As the embodied soul continually passes…",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if sent.Body != "" {
|
||||||
|
t.Fatalf("setup: expected a body-less quote, got %q", sent.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fwd, err := svc.ForwardMessage(ctx, a, sent.ID, target.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs, err := svc.Messages(ctx, c, target.ID, 0, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var quote *model.Attachment
|
||||||
|
for i := range msgs {
|
||||||
|
if msgs[i].ID != fwd.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j := range msgs[i].Attachments {
|
||||||
|
if msgs[i].Attachments[j].Kind == model.AttachmentQuote {
|
||||||
|
quote = &msgs[i].Attachments[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if quote == nil {
|
||||||
|
t.Fatal("the forwarded message has no quote — it would render empty")
|
||||||
|
}
|
||||||
|
if quote.Description == nil || *quote.Description == "" {
|
||||||
|
t.Error("the forwarded quote lost its passage")
|
||||||
|
}
|
||||||
|
if quote.URL == nil || *quote.URL != "/books/bg/2/13?hl=abc" {
|
||||||
|
t.Errorf("the forwarded quote lost its deep link: %v", quote.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The copy belongs to the DESTINATION thread: reads authorize against the
|
||||||
|
// attachment's own conversation, so a copy still scoped to the source
|
||||||
|
// would be unreadable to the recipient.
|
||||||
|
var convID string
|
||||||
|
if err := pg.Pool.QueryRow(ctx,
|
||||||
|
`SELECT conversation_id FROM message_attachments WHERE message_id = $1`,
|
||||||
|
fwd.ID).Scan(&convID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if convID != target.ID {
|
||||||
|
t.Errorf("copied attachment is scoped to %s, want the destination %s", convID, target.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The original is untouched.
|
||||||
|
back, err := svc.Messages(ctx, b, source.ID, 0, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for i := range back {
|
||||||
|
if back[i].ID == sent.ID && len(back[i].Attachments) == 1 {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("forwarding disturbed the original message's attachment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user