Skip to content

Commit 6bcc795

Browse files
Simon-Lauxlink2xt
authored andcommitted
feat: message previews
Part of #7367 - Remove partial downloads (remove creation of the stub messages) (#7373) - Remove "Download maximum available until" and remove stock string `DC_STR_DOWNLOAD_AVAILABILITY` (#7369) - Send pre-message on messages with large attachments (#7410) - Pre messages can now get read receipts (#7433) progress / what's to do: - [x] send pre-message - [x] The message's state must be set to MessageState::OutDelivered only after both messages are sent. If a read receipt is received, the message can be OutMdnRcvd or OutPending; let's just do whatever is easiest for now. Take care not to revert from OutMdnReceived to OutDelivered if we first receive a read receipt and then deliver the full message. - this is already the case: - `OutDelivered` is set when a message is sent out and has no remaining send jobs in the smtp table for this message id - so already works since full message and pre message have same msgId in that table. - `OutMdnRcvd` is a "virtual" state (#7367 (comment)), so going back to `OutDelivered` can't happen anymore - [x] delimit `ChatFullMessageId` with `<` and `>` like the other message ids - [x] add tests - [x] test that pre message is sent for attachment larger than X - test that correct headers are present on both messages - also test that Autocrypt-gossip and selfavatar should never go into full-messages - [x] test that no pre message is sent for attachment smaller than X - no "is full message" header should be present - [x] test that pre message is not send for large webxdc update or large text - [x] fix test `receive_imf::receive_imf_tests::test_dont_reverify_by_self_on_outgoing_msg`
1 parent 9977545 commit 6bcc795

26 files changed

+646
-1325
lines changed

deltachat-ffi/deltachat.h

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7263,22 +7263,9 @@ void dc_event_unref(dc_event_t* event);
72637263
/// `%1$s` will be replaced by the percentage used
72647264
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
72657265

7266-
/// "%1$s message"
7267-
///
7268-
/// Used as the message body when a message
7269-
/// was not yet downloaded completely
7270-
/// (dc_msg_get_download_state() is e.g. @ref DC_DOWNLOAD_AVAILABLE).
7271-
///
7272-
/// `%1$s` will be replaced by human-readable size (e.g. "1.2 MiB").
7266+
/// @deprecated Deprecated 2025-11-12, this string is no longer needed.
72737267
#define DC_STR_PARTIAL_DOWNLOAD_MSG_BODY 99
72747268

7275-
/// "Download maximum available until %1$s"
7276-
///
7277-
/// Appended after some separator to @ref DC_STR_PARTIAL_DOWNLOAD_MSG_BODY.
7278-
///
7279-
/// `%1$s` will be replaced by human-readable date and time.
7280-
#define DC_STR_DOWNLOAD_AVAILABILITY 100
7281-
72827269
/// "Multi Device Synchronization"
72837270
///
72847271
/// Used in subjects of outgoing sync messages.

deltachat-rpc-client/tests/test_chatlist_events.py

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import annotations
22

3-
import base64
4-
import os
53
from typing import TYPE_CHECKING
64

75
from deltachat_rpc_client import Account, EventType, const
@@ -111,40 +109,6 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None:
111109
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
112110

113111

114-
def test_download_on_demand(acfactory: ACFactory) -> None:
115-
"""
116-
Test if download on demand emits chatlist update events.
117-
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
118-
"""
119-
alice, bob = acfactory.get_online_accounts(2)
120-
121-
alice_contact_bob = alice.create_contact(bob, "Bob")
122-
alice_chat_bob = alice_contact_bob.create_chat()
123-
alice_chat_bob.send_text("hi")
124-
125-
alice.set_config("download_limit", "1")
126-
127-
msg = bob.wait_for_incoming_msg()
128-
chat_id = msg.get_snapshot().chat_id
129-
msg.get_snapshot().chat.accept()
130-
bob.get_chat_by_id(chat_id).send_message(
131-
"Hello World, this message is bigger than 5 bytes",
132-
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
133-
)
134-
135-
message = alice.wait_for_incoming_msg()
136-
snapshot = message.get_snapshot()
137-
assert snapshot.download_state == const.DownloadState.AVAILABLE
138-
139-
alice.clear_all_events()
140-
141-
snapshot = message.get_snapshot()
142-
chat_id = snapshot.chat_id
143-
alice._rpc.download_full_message(alice.id, message.id)
144-
145-
wait_for_chatlist_specific_item(alice, chat_id)
146-
147-
148112
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
149113
alice, bob = acfactory.get_online_accounts(2)
150114

deltachat-rpc-client/tests/test_something.py

Lines changed: 5 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
import os
66
import socket
77
import subprocess
8-
import time
98
from unittest.mock import MagicMock
109

1110
import pytest
1211

13-
from deltachat_rpc_client import Contact, EventType, Message, events
14-
from deltachat_rpc_client.const import DownloadState, MessageState
12+
from deltachat_rpc_client import EventType, events
13+
from deltachat_rpc_client.const import MessageState
1514
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
1615
from deltachat_rpc_client.rpc import JsonRpcError
1716

@@ -336,7 +335,7 @@ def test_receive_imf_failure(acfactory) -> None:
336335
alice_contact_bob = alice.create_contact(bob, "Bob")
337336
alice_chat_bob = alice_contact_bob.create_chat()
338337

339-
bob.set_config("fail_on_receiving_full_msg", "1")
338+
bob.set_config("simulate_receive_imf_error", "1")
340339
alice_chat_bob.send_text("Hello!")
341340
event = bob.wait_for_event(EventType.MSGS_CHANGED)
342341
assert event.chat_id == bob.get_device_chat().id
@@ -345,17 +344,16 @@ def test_receive_imf_failure(acfactory) -> None:
345344
snapshot = message.get_snapshot()
346345
assert (
347346
snapshot.text == "❌ Failed to receive a message:"
348-
" Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
347+
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
349348
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
350349
)
351350

352351
# The failed message doesn't break the IMAP loop.
353-
bob.set_config("fail_on_receiving_full_msg", "0")
352+
bob.set_config("simulate_receive_imf_error", "0")
354353
alice_chat_bob.send_text("Hello again!")
355354
message = bob.wait_for_incoming_msg()
356355
snapshot = message.get_snapshot()
357356
assert snapshot.text == "Hello again!"
358-
assert snapshot.download_state == DownloadState.DONE
359357
assert snapshot.error is None
360358

361359

@@ -591,94 +589,6 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
591589
assert snapshot.show_padlock
592590

593591

594-
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
595-
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
596-
messages are received out of order".
597-
598-
If the Inbox contains X small messages followed by Y large messages followed by Z small
599-
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
600-
601-
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
602-
with online test as follows:
603-
- Bob enables download limit and goes offline.
604-
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
605-
- Bob goes online
606-
- Bob first processes a reaction message and throws it away because there is no corresponding
607-
message, then processes a partially downloaded message.
608-
- As a result, Bob does not see a reaction
609-
"""
610-
download_limit = 300000
611-
ac1, ac2 = acfactory.get_online_accounts(2)
612-
ac1_addr = ac1.get_config("addr")
613-
chat = ac1.create_chat(ac2)
614-
ac2.set_config("download_limit", str(download_limit))
615-
ac2.stop_io()
616-
617-
logging.info("sending small+large messages from ac1 to ac2")
618-
msgs = []
619-
msgs.append(chat.send_text("hi"))
620-
path = tmp_path / "large"
621-
path.write_bytes(os.urandom(download_limit + 1))
622-
msgs.append(chat.send_file(str(path)))
623-
for m in msgs:
624-
m.wait_until_delivered()
625-
626-
logging.info("sending a reaction to the large message from ac1 to ac2")
627-
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
628-
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
629-
# have a later INTERNALDATE.
630-
time.sleep(1.1)
631-
react_str = "\N{THUMBS UP SIGN}"
632-
msgs.append(msgs[-1].send_reaction(react_str))
633-
msgs[-1].wait_until_delivered()
634-
635-
ac2.start_io()
636-
637-
logging.info("wait for ac2 to receive a reaction")
638-
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
639-
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
640-
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
641-
reactions = msg2.get_reactions()
642-
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
643-
assert len(contacts) == 1
644-
assert contacts[0].get_snapshot().address == ac1_addr
645-
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
646-
647-
648-
@pytest.mark.parametrize("n_accounts", [3, 2])
649-
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
650-
download_limit = 300000
651-
652-
alice, *others = acfactory.get_online_accounts(n_accounts)
653-
bob = others[0]
654-
655-
alice_group = alice.create_group("test group")
656-
for account in others:
657-
chat = account.create_chat(alice)
658-
chat.send_text("Hello Alice!")
659-
assert alice.wait_for_incoming_msg().get_snapshot().text == "Hello Alice!"
660-
661-
contact = alice.create_contact(account)
662-
alice_group.add_contact(contact)
663-
664-
bob.set_config("download_limit", str(download_limit))
665-
666-
alice_group.send_text("hi")
667-
snapshot = bob.wait_for_incoming_msg().get_snapshot()
668-
assert snapshot.text == "hi"
669-
bob_group = snapshot.chat
670-
671-
path = tmp_path / "large"
672-
path.write_bytes(os.urandom(download_limit + 1))
673-
674-
for i in range(10):
675-
logging.info("Sending message %s", i)
676-
alice_group.send_file(str(path))
677-
snapshot = bob.wait_for_incoming_msg().get_snapshot()
678-
assert snapshot.download_state == DownloadState.AVAILABLE
679-
assert snapshot.chat == bob_group
680-
681-
682592
def test_markseen_contact_request(acfactory):
683593
"""
684594
Test that seen status is synchronized for contact request messages

python/tests/test_1_online.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22
import queue
33
import sys
4-
import base64
54
from datetime import datetime, timezone
65

76
import pytest
@@ -222,38 +221,6 @@ def test_webxdc_huge_update(acfactory, data, lp):
222221
assert update["payload"] == payload
223222

224223

225-
def test_webxdc_download_on_demand(acfactory, data, lp):
226-
ac1, ac2 = acfactory.get_online_accounts(2)
227-
acfactory.introduce_each_other([ac1, ac2])
228-
chat = acfactory.get_accepted_chat(ac1, ac2)
229-
230-
msg1 = Message.new_empty(ac1, "webxdc")
231-
msg1.set_text("message1")
232-
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
233-
msg1 = chat.send_msg(msg1)
234-
assert msg1.is_webxdc()
235-
assert msg1.filename
236-
237-
msg2 = ac2._evtracker.wait_next_incoming_message()
238-
assert msg2.is_webxdc()
239-
240-
lp.sec("ac2 sets download limit")
241-
ac2.set_config("download_limit", "100")
242-
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(300000))}, "some test data")
243-
ac2_update = ac2._evtracker.wait_next_incoming_message()
244-
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
245-
assert not msg2.get_status_updates()
246-
247-
ac2_update.download_full()
248-
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
249-
assert msg2.get_status_updates()
250-
251-
# Get a event notifying that the message disappeared from the chat.
252-
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
253-
assert msgs_changed_event.data1 == msg2.chat.id
254-
assert msgs_changed_event.data2 == 0
255-
256-
257224
def test_enable_mvbox_move(acfactory, lp):
258225
(ac1,) = acfactory.get_online_accounts(1)
259226

src/calls/calls_tests.rs

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::*;
22
use crate::chat::forward_msgs;
33
use crate::config::Config;
44
use crate::constants::DC_CHAT_ID_TRASH;
5-
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
5+
use crate::receive_imf::receive_imf;
66
use crate::test_utils::{TestContext, TestContextManager};
77

88
struct CallSetup {
@@ -610,65 +610,3 @@ async fn test_end_text_call() -> Result<()> {
610610

611611
Ok(())
612612
}
613-
614-
/// Tests that partially downloaded "call ended"
615-
/// messages are not processed.
616-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
617-
async fn test_no_partial_calls() -> Result<()> {
618-
let mut tcm = TestContextManager::new();
619-
let alice = &tcm.alice().await;
620-
621-
let seen = false;
622-
623-
// The messages in the test
624-
// have no `Date` on purpose,
625-
// so they are treated as new.
626-
let received_call = receive_imf(
627-
alice,
628-
b"From: bob@example.net\n\
629-
To: alice@example.org\n\
630-
Message-ID: <first@example.net>\n\
631-
Chat-Version: 1.0\n\
632-
Chat-Content: call\n\
633-
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
634-
\n\
635-
Hello, this is a call\n",
636-
seen,
637-
)
638-
.await?
639-
.unwrap();
640-
assert_eq!(received_call.msg_ids.len(), 1);
641-
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
642-
.await
643-
.unwrap();
644-
assert_eq!(call_msg.viewtype, Viewtype::Call);
645-
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
646-
647-
let imf_raw = b"From: bob@example.net\n\
648-
To: alice@example.org\n\
649-
Message-ID: <second@example.net>\n\
650-
In-Reply-To: <first@example.net>\n\
651-
Chat-Version: 1.0\n\
652-
Chat-Content: call-ended\n\
653-
\n\
654-
Call ended\n";
655-
receive_imf_from_inbox(
656-
alice,
657-
"second@example.net",
658-
imf_raw,
659-
seen,
660-
Some(imf_raw.len().try_into().unwrap()),
661-
)
662-
.await?;
663-
664-
// The call is still not ended.
665-
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
666-
667-
// Fully downloading the message ends the call.
668-
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
669-
.await
670-
.context("Failed to fully download end call message")?;
671-
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
672-
673-
Ok(())
674-
}

0 commit comments

Comments
 (0)