deltachat-rpc-client 2.37.0__tar.gz → 2.39.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {deltachat_rpc_client-2.37.0/src/deltachat_rpc_client.egg-info → deltachat_rpc_client-2.39.0}/PKG-INFO +1 -1
  2. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/pyproject.toml +1 -1
  3. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/_utils.py +10 -3
  4. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/client.py +21 -8
  5. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/message.py +8 -0
  6. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/pytestplugin.py +5 -5
  7. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0/src/deltachat_rpc_client.egg-info}/PKG-INFO +1 -1
  8. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_chatlist_events.py +1 -3
  9. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_multitransport.py +23 -0
  10. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_something.py +264 -63
  11. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/LICENSE +0 -0
  12. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/README.md +0 -0
  13. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/setup.cfg +0 -0
  14. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/__init__.py +0 -0
  15. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/account.py +0 -0
  16. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/chat.py +0 -0
  17. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/const.py +0 -0
  18. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/contact.py +0 -0
  19. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/deltachat.py +0 -0
  20. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/events.py +0 -0
  21. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/py.typed +0 -0
  22. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client/rpc.py +0 -0
  23. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client.egg-info/SOURCES.txt +0 -0
  24. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client.egg-info/dependency_links.txt +0 -0
  25. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client.egg-info/entry_points.txt +0 -0
  26. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/src/deltachat_rpc_client.egg-info/top_level.txt +0 -0
  27. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_account_events.py +0 -0
  28. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_calls.py +0 -0
  29. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_cross_core.py +0 -0
  30. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_folders.py +0 -0
  31. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_iroh_webxdc.py +0 -0
  32. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_key_transfer.py +0 -0
  33. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_multidevice.py +0 -0
  34. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_securejoin.py +0 -0
  35. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_vcard.py +0 -0
  36. {deltachat_rpc_client-2.37.0 → deltachat_rpc_client-2.39.0}/tests/test_webxdc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deltachat-rpc-client
3
- Version: 2.37.0
3
+ Version: 2.39.0
4
4
  Summary: Python client for Delta Chat core JSON-RPC interface
5
5
  License-Expression: MPL-2.0
6
6
  Classifier: Development Status :: 5 - Production/Stable
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deltachat-rpc-client"
7
- version = "2.37.0"
7
+ version = "2.39.0"
8
8
  license = "MPL-2.0"
9
9
  description = "Python client for Delta Chat core JSON-RPC interface"
10
10
  classifiers = [
@@ -44,8 +44,13 @@ class AttrDict(dict):
44
44
  super().__setattr__(attr, val)
45
45
 
46
46
 
47
+ def _forever(_event: AttrDict) -> bool:
48
+ return False
49
+
50
+
47
51
  def run_client_cli(
48
52
  hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
53
+ until: Callable[[AttrDict], bool] = _forever,
49
54
  argv: Optional[list] = None,
50
55
  **kwargs,
51
56
  ) -> None:
@@ -55,10 +60,11 @@ def run_client_cli(
55
60
  """
56
61
  from .client import Client
57
62
 
58
- _run_cli(Client, hooks, argv, **kwargs)
63
+ _run_cli(Client, until, hooks, argv, **kwargs)
59
64
 
60
65
 
61
66
  def run_bot_cli(
67
+ until: Callable[[AttrDict], bool] = _forever,
62
68
  hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
63
69
  argv: Optional[list] = None,
64
70
  **kwargs,
@@ -69,11 +75,12 @@ def run_bot_cli(
69
75
  """
70
76
  from .client import Bot
71
77
 
72
- _run_cli(Bot, hooks, argv, **kwargs)
78
+ _run_cli(Bot, until, hooks, argv, **kwargs)
73
79
 
74
80
 
75
81
  def _run_cli(
76
82
  client_type: Type["Client"],
83
+ until: Callable[[AttrDict], bool] = _forever,
77
84
  hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
78
85
  argv: Optional[list] = None,
79
86
  **kwargs,
@@ -111,7 +118,7 @@ def _run_cli(
111
118
  kwargs={"email": args.email, "password": args.password},
112
119
  )
113
120
  configure_thread.start()
114
- client.run_forever()
121
+ client.run_until(until)
115
122
 
116
123
 
117
124
  def extract_addr(text: str) -> str:
@@ -14,6 +14,7 @@ from typing import (
14
14
 
15
15
  from ._utils import (
16
16
  AttrDict,
17
+ _forever,
17
18
  parse_system_add_remove,
18
19
  parse_system_image_changed,
19
20
  parse_system_title_changed,
@@ -91,19 +92,28 @@ class Client:
91
92
 
92
93
  def run_forever(self) -> None:
93
94
  """Process events forever."""
94
- self.run_until(lambda _: False)
95
+ self.run_until(_forever)
95
96
 
96
97
  def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
97
- """Process events until the given callable evaluates to True.
98
-
99
- The callable should accept an AttrDict object representing the
100
- last processed event. The event is returned when the callable
101
- evaluates to True.
102
- """
98
+ """Start the event processing loop."""
103
99
  self.logger.debug("Listening to incoming events...")
104
100
  if self.is_configured():
105
101
  self.account.start_io()
106
102
  self._process_messages() # Process old messages.
103
+ return self._process_events(until_func=func) # Loop over incoming events
104
+
105
+ def _process_events(
106
+ self,
107
+ until_func: Callable[[AttrDict], bool],
108
+ until_event: EventType = False,
109
+ ) -> AttrDict:
110
+ """Process events until the given callable evaluates to True,
111
+ or until a certain event happens.
112
+
113
+ The until_func callable should accept an AttrDict object representing
114
+ the last processed event. The event is returned when the callable
115
+ evaluates to True.
116
+ """
107
117
  while True:
108
118
  event = self.account.wait_for_event()
109
119
  event["kind"] = EventType(event.kind)
@@ -112,10 +122,13 @@ class Client:
112
122
  if event.kind == EventType.INCOMING_MSG:
113
123
  self._process_messages()
114
124
 
115
- stop = func(event)
125
+ stop = until_func(event)
116
126
  if stop:
117
127
  return event
118
128
 
129
+ if event.kind == until_event:
130
+ return event
131
+
119
132
  def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
120
133
  for hook, evfilter in self._hooks.get(filter_type, []):
121
134
  if evfilter.filter(event):
@@ -44,6 +44,14 @@ class Message:
44
44
  read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
45
45
  return [AttrDict(read_receipt) for read_receipt in read_receipts]
46
46
 
47
+ def get_read_receipt_count(self) -> int:
48
+ """
49
+ Returns count of read receipts on message.
50
+
51
+ This view count is meant as a feedback measure for the channel owner only.
52
+ """
53
+ return self._rpc.get_message_read_receipt_count(self.account.id, self.id)
54
+
47
55
  def get_reactions(self) -> Optional[AttrDict]:
48
56
  """Get message reactions."""
49
57
  reactions = self._rpc.get_message_reactions(self.account.id, self.id)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  import os
6
7
  import pathlib
7
8
  import platform
@@ -204,14 +205,13 @@ def log():
204
205
 
205
206
  class Printer:
206
207
  def section(self, msg: str) -> None:
207
- print()
208
- print("=" * 10, msg, "=" * 10)
208
+ logging.info("\n%s %s %s", "=" * 10, msg, "=" * 10)
209
209
 
210
210
  def step(self, msg: str) -> None:
211
- print("-" * 5, "step " + msg, "-" * 5)
211
+ logging.info("%s step %s %s", "-" * 5, msg, "-" * 5)
212
212
 
213
213
  def indent(self, msg: str) -> None:
214
- print(" " + msg)
214
+ logging.info(" " + msg)
215
215
 
216
216
  return Printer()
217
217
 
@@ -261,7 +261,7 @@ def get_core_python_env(tmp_path_factory):
261
261
  envs[core_version] = venv
262
262
  python = find_path(venv, "python")
263
263
  rpc_server_path = find_path(venv, "deltachat-rpc-server")
264
- print(f"python={python}\nrpc_server={rpc_server_path}")
264
+ logging.info(f"Paths:\npython={python}\nrpc_server={rpc_server_path}")
265
265
  return python, rpc_server_path
266
266
 
267
267
  return get_versioned_venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deltachat-rpc-client
3
- Version: 2.37.0
3
+ Version: 2.39.0
4
4
  Summary: Python client for Delta Chat core JSON-RPC interface
5
5
  License-Expression: MPL-2.0
6
6
  Classifier: Development Status :: 5 - Production/Stable
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import base64
4
- import os
5
3
  from typing import TYPE_CHECKING
6
4
 
7
5
  from deltachat_rpc_client import Account, EventType, const
@@ -129,7 +127,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
129
127
  msg.get_snapshot().chat.accept()
130
128
  bob.get_chat_by_id(chat_id).send_message(
131
129
  "Hello World, this message is bigger than 5 bytes",
132
- html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
130
+ file="../test-data/image/screenshot.jpg",
133
131
  )
134
132
 
135
133
  message = alice.wait_for_incoming_msg()
@@ -207,6 +207,29 @@ def test_transport_synchronization(acfactory, log) -> None:
207
207
  assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
208
208
 
209
209
 
210
+ def test_transport_sync_new_as_primary(acfactory, log) -> None:
211
+ """Test synchronization of new transport as primary between devices."""
212
+ ac1 = acfactory.get_online_account()
213
+ ac1_clone = ac1.clone()
214
+ ac1_clone.bring_online()
215
+
216
+ qr = acfactory.get_account_qr()
217
+
218
+ ac1.add_transport_from_qr(qr)
219
+ ac1_transports = ac1.list_transports()
220
+ assert len(ac1_transports) == 2
221
+ [transport1, transport2] = ac1_transports
222
+ ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
223
+ assert len(ac1_clone.list_transports()) == 2
224
+ assert ac1_clone.get_config("configured_addr") == transport1["addr"]
225
+
226
+ log.section("ac1 changes the primary transport")
227
+ ac1.set_config("configured_addr", transport2["addr"])
228
+
229
+ ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
230
+ assert ac1_clone.get_config("configured_addr") == transport2["addr"]
231
+
232
+
210
233
  def test_recognize_self_address(acfactory) -> None:
211
234
  alice, bob = acfactory.get_online_accounts(2)
212
235
 
@@ -5,12 +5,11 @@ import logging
5
5
  import os
6
6
  import socket
7
7
  import subprocess
8
- import time
9
8
  from unittest.mock import MagicMock
10
9
 
11
10
  import pytest
12
11
 
13
- from deltachat_rpc_client import Contact, EventType, Message, events
12
+ from deltachat_rpc_client import EventType, events
14
13
  from deltachat_rpc_client.const import DownloadState, MessageState
15
14
  from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
16
15
  from deltachat_rpc_client.rpc import JsonRpcError
@@ -333,7 +332,7 @@ def test_receive_imf_failure(acfactory) -> None:
333
332
  alice_contact_bob = alice.create_contact(bob, "Bob")
334
333
  alice_chat_bob = alice_contact_bob.create_chat()
335
334
 
336
- bob.set_config("fail_on_receiving_full_msg", "1")
335
+ bob.set_config("simulate_receive_imf_error", "1")
337
336
  alice_chat_bob.send_text("Hello!")
338
337
  event = bob.wait_for_event(EventType.MSGS_CHANGED)
339
338
  assert event.chat_id == bob.get_device_chat().id
@@ -343,18 +342,17 @@ def test_receive_imf_failure(acfactory) -> None:
343
342
  version = bob.get_info()["deltachat_core_version"]
344
343
  assert (
345
344
  snapshot.text == "❌ Failed to receive a message:"
346
- " Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
345
+ " Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
347
346
  f" Core version {version}."
348
347
  " Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
349
348
  )
350
349
 
351
350
  # The failed message doesn't break the IMAP loop.
352
- bob.set_config("fail_on_receiving_full_msg", "0")
351
+ bob.set_config("simulate_receive_imf_error", "0")
353
352
  alice_chat_bob.send_text("Hello again!")
354
353
  message = bob.wait_for_incoming_msg()
355
354
  snapshot = message.get_snapshot()
356
355
  assert snapshot.text == "Hello again!"
357
- assert snapshot.download_state == DownloadState.DONE
358
356
  assert snapshot.error is None
359
357
 
360
358
 
@@ -372,17 +370,41 @@ def test_selfavatar_sync(acfactory, data, log) -> None:
372
370
  alice.set_config("selfavatar", image)
373
371
  avatar_config = alice.get_config("selfavatar")
374
372
  avatar_hash = os.path.basename(avatar_config)
375
- print("Info: avatar hash is ", avatar_hash)
373
+ logging.info(f"Avatar hash is {avatar_hash}")
376
374
 
377
375
  log.section("First device receives avatar change")
378
376
  alice2.wait_for_event(EventType.SELFAVATAR_CHANGED)
379
377
  avatar_config2 = alice2.get_config("selfavatar")
380
378
  avatar_hash2 = os.path.basename(avatar_config2)
381
- print("Info: avatar hash on second device is ", avatar_hash2)
379
+ logging.info(f"Avatar hash on second device is {avatar_hash2}")
382
380
  assert avatar_hash == avatar_hash2
383
381
  assert avatar_config != avatar_config2
384
382
 
385
383
 
384
+ def test_dont_move_sync_msgs(acfactory, direct_imap):
385
+ addr, password = acfactory.get_credentials()
386
+ ac1 = acfactory.get_unconfigured_account()
387
+ ac1.set_config("bcc_self", "1")
388
+ ac1.set_config("fix_is_chatmail", "1")
389
+ ac1.add_or_update_transport({"addr": addr, "password": password})
390
+ ac1.bring_online()
391
+ ac1_direct_imap = direct_imap(ac1)
392
+
393
+ ac1_direct_imap.select_folder("Inbox")
394
+ # Sync messages may also be sent during configuration.
395
+ inbox_msg_cnt = len(ac1_direct_imap.get_all_messages())
396
+
397
+ ac1.set_config("displayname", "Alice")
398
+ ac1.wait_for_event(EventType.MSG_DELIVERED)
399
+ ac1.set_config("displayname", "Bob")
400
+ ac1.wait_for_event(EventType.MSG_DELIVERED)
401
+ ac1_direct_imap.select_folder("Inbox")
402
+ assert len(ac1_direct_imap.get_all_messages()) == inbox_msg_cnt + 2
403
+
404
+ ac1_direct_imap.select_folder("DeltaChat")
405
+ assert len(ac1_direct_imap.get_all_messages()) == 0
406
+
407
+
386
408
  def test_reaction_seen_on_another_dev(acfactory) -> None:
387
409
  alice, bob = acfactory.get_online_accounts(2)
388
410
  alice2 = alice.clone()
@@ -687,60 +709,6 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
687
709
  assert snapshot.show_padlock
688
710
 
689
711
 
690
- def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
691
- """See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
692
- messages are received out of order".
693
-
694
- If the Inbox contains X small messages followed by Y large messages followed by Z small
695
- messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
696
-
697
- This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
698
- with online test as follows:
699
- - Bob enables download limit and goes offline.
700
- - Alice sends a large message to Bob and reacts to this message with a thumbs-up.
701
- - Bob goes online
702
- - Bob first processes a reaction message and throws it away because there is no corresponding
703
- message, then processes a partially downloaded message.
704
- - As a result, Bob does not see a reaction
705
- """
706
- download_limit = 300000
707
- ac1, ac2 = acfactory.get_online_accounts(2)
708
- ac1_addr = ac1.get_config("addr")
709
- chat = ac1.create_chat(ac2)
710
- ac2.set_config("download_limit", str(download_limit))
711
- ac2.stop_io()
712
-
713
- logging.info("sending small+large messages from ac1 to ac2")
714
- msgs = []
715
- msgs.append(chat.send_text("hi"))
716
- path = tmp_path / "large"
717
- path.write_bytes(os.urandom(download_limit + 1))
718
- msgs.append(chat.send_file(str(path)))
719
- for m in msgs:
720
- m.wait_until_delivered()
721
-
722
- logging.info("sending a reaction to the large message from ac1 to ac2")
723
- # TODO: Find the reason of an occasional message reordering on the server (so that the reaction
724
- # has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
725
- # have a later INTERNALDATE.
726
- time.sleep(1.1)
727
- react_str = "\N{THUMBS UP SIGN}"
728
- msgs.append(msgs[-1].send_reaction(react_str))
729
- msgs[-1].wait_until_delivered()
730
-
731
- ac2.start_io()
732
-
733
- logging.info("wait for ac2 to receive a reaction")
734
- msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
735
- assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
736
- assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
737
- reactions = msg2.get_reactions()
738
- contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
739
- assert len(contacts) == 1
740
- assert contacts[0].get_snapshot().address == ac1_addr
741
- assert list(reactions.reactions_by_contact.values())[0] == [react_str]
742
-
743
-
744
712
  @pytest.mark.parametrize("n_accounts", [3, 2])
745
713
  def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
746
714
  download_limit = 300000
@@ -767,14 +735,159 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
767
735
  path = tmp_path / "large"
768
736
  path.write_bytes(os.urandom(download_limit + 1))
769
737
 
738
+ n_done = 0
770
739
  for i in range(10):
771
740
  logging.info("Sending message %s", i)
772
741
  alice_group.send_file(str(path))
773
742
  snapshot = bob.wait_for_incoming_msg().get_snapshot()
774
- assert snapshot.download_state == DownloadState.AVAILABLE
743
+ if snapshot.download_state == DownloadState.DONE:
744
+ n_done += 1
745
+ # Work around lost and reordered pre-messages.
746
+ assert n_done <= 1
747
+ else:
748
+ assert snapshot.download_state == DownloadState.AVAILABLE
775
749
  assert snapshot.chat == bob_group
776
750
 
777
751
 
752
+ def test_download_small_msg_first(acfactory, tmp_path):
753
+ download_limit = 70000
754
+
755
+ alice, bob0 = acfactory.get_online_accounts(2)
756
+ bob1 = bob0.clone()
757
+ bob1.set_config("download_limit", str(download_limit))
758
+
759
+ chat = alice.create_chat(bob0)
760
+ path = tmp_path / "large_enough"
761
+ path.write_bytes(os.urandom(download_limit + 1))
762
+ # Less than 140K, so sent w/o a pre-message.
763
+ chat.send_file(str(path))
764
+ chat.send_text("hi")
765
+ bob0.create_chat(alice)
766
+ assert bob0.wait_for_incoming_msg().get_snapshot().text == ""
767
+ assert bob0.wait_for_incoming_msg().get_snapshot().text == "hi"
768
+
769
+ bob1.start_io()
770
+ bob1.create_chat(alice)
771
+ assert bob1.wait_for_incoming_msg().get_snapshot().text == "hi"
772
+ assert bob1.wait_for_incoming_msg().get_snapshot().text == ""
773
+
774
+
775
+ @pytest.mark.parametrize("delete_chat", [False, True])
776
+ def test_delete_available_msg(acfactory, tmp_path, direct_imap, delete_chat):
777
+ """
778
+ Tests `DownloadState.AVAILABLE` message deletion on the receiver side.
779
+ Also tests pre- and post-message deletion on the sender side.
780
+ """
781
+ # Min. UI setting as of v2.35
782
+ download_limit = 163840
783
+ alice, bob = acfactory.get_online_accounts(2)
784
+ bob.set_config("download_limit", str(download_limit))
785
+ # Avoid immediate deletion from the server
786
+ alice.set_config("bcc_self", "1")
787
+ bob.set_config("bcc_self", "1")
788
+
789
+ chat_alice = alice.create_chat(bob)
790
+ path = tmp_path / "large"
791
+ path.write_bytes(os.urandom(download_limit + 1))
792
+ msg_alice = chat_alice.send_file(str(path))
793
+ msg_bob = bob.wait_for_incoming_msg()
794
+ msg_bob_snapshot = msg_bob.get_snapshot()
795
+ assert msg_bob_snapshot.download_state == DownloadState.AVAILABLE
796
+ chat_bob = bob.get_chat_by_id(msg_bob_snapshot.chat_id)
797
+
798
+ # Avoid DeleteMessages sync message
799
+ bob.set_config("bcc_self", "0")
800
+ if delete_chat:
801
+ chat_bob.delete()
802
+ else:
803
+ bob.delete_messages([msg_bob])
804
+ alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
805
+ alice.wait_for_event(EventType.SMTP_MESSAGE_SENT)
806
+ alice.set_config("bcc_self", "0")
807
+ if delete_chat:
808
+ chat_alice.delete()
809
+ else:
810
+ alice.delete_messages([msg_alice])
811
+ for acc in [bob, alice]:
812
+ if not delete_chat:
813
+ acc.wait_for_event(EventType.MSG_DELETED)
814
+ acc_direct_imap = direct_imap(acc)
815
+ # Messages may be deleted separately
816
+ while True:
817
+ acc.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
818
+ while True:
819
+ event = acc.wait_for_event()
820
+ if event.kind == EventType.INFO and "Close/expunge succeeded." in event.msg:
821
+ break
822
+ if len(acc_direct_imap.get_all_messages()) == 0:
823
+ break
824
+
825
+
826
+ def test_delete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
827
+ alice, bob = acfactory.get_online_accounts(2)
828
+ # Avoid immediate deletion from the server
829
+ bob.set_config("bcc_self", "1")
830
+
831
+ chat_alice = alice.create_chat(bob)
832
+ path = tmp_path / "large"
833
+ # Big enough to be sent with a pre-message
834
+ path.write_bytes(os.urandom(300000))
835
+ chat_alice.send_file(str(path))
836
+
837
+ msg = bob.wait_for_incoming_msg()
838
+ msg_snapshot = msg.get_snapshot()
839
+ assert msg_snapshot.download_state == DownloadState.AVAILABLE
840
+ msgs_changed_event = bob.wait_for_msgs_changed_event()
841
+ assert msgs_changed_event.msg_id == msg.id
842
+ msg_snapshot = msg.get_snapshot()
843
+ assert msg_snapshot.download_state == DownloadState.DONE
844
+
845
+ bob_direct_imap = direct_imap(bob)
846
+ assert len(bob_direct_imap.get_all_messages()) == 2
847
+ # Avoid DeleteMessages sync message
848
+ bob.set_config("bcc_self", "0")
849
+ bob.delete_messages([msg])
850
+ bob.wait_for_event(EventType.MSG_DELETED)
851
+ # Messages may be deleted separately
852
+ while True:
853
+ bob.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
854
+ while True:
855
+ event = bob.wait_for_event()
856
+ if event.kind == EventType.INFO and "Close/expunge succeeded." in event.msg:
857
+ break
858
+ if len(bob_direct_imap.get_all_messages()) == 0:
859
+ break
860
+
861
+
862
+ def test_imap_autodelete_fully_downloaded_msg(acfactory, tmp_path, direct_imap):
863
+ alice, bob = acfactory.get_online_accounts(2)
864
+
865
+ chat_alice = alice.create_chat(bob)
866
+ path = tmp_path / "large"
867
+ # Big enough to be sent with a pre-message
868
+ path.write_bytes(os.urandom(300000))
869
+ chat_alice.send_file(str(path))
870
+
871
+ msg = bob.wait_for_incoming_msg()
872
+ msg_snapshot = msg.get_snapshot()
873
+ assert msg_snapshot.download_state == DownloadState.AVAILABLE
874
+ msgs_changed_event = bob.wait_for_msgs_changed_event()
875
+ assert msgs_changed_event.msg_id == msg.id
876
+ msg_snapshot = msg.get_snapshot()
877
+ assert msg_snapshot.download_state == DownloadState.DONE
878
+
879
+ bob_direct_imap = direct_imap(bob)
880
+ # Messages may be deleted separately
881
+ while True:
882
+ if len(bob_direct_imap.get_all_messages()) == 0:
883
+ break
884
+ bob.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
885
+ while True:
886
+ event = bob.wait_for_event()
887
+ if event.kind == EventType.INFO and "Close/expunge succeeded." in event.msg:
888
+ break
889
+
890
+
778
891
  def test_markseen_contact_request(acfactory):
779
892
  """
780
893
  Test that seen status is synchronized for contact request messages
@@ -798,6 +911,47 @@ def test_markseen_contact_request(acfactory):
798
911
  assert message2.get_snapshot().state == MessageState.IN_SEEN
799
912
 
800
913
 
914
+ @pytest.mark.parametrize("team_profile", [True, False])
915
+ def test_no_markseen_in_team_profile(team_profile, acfactory):
916
+ """
917
+ Test that seen status is synchronized iff `team_profile` isn't set.
918
+ """
919
+ alice, bob = acfactory.get_online_accounts(2)
920
+ if team_profile:
921
+ bob.set_config("team_profile", "1")
922
+
923
+ # Bob sets up a second device.
924
+ bob2 = bob.clone()
925
+ bob2.start_io()
926
+
927
+ alice_chat_bob = alice.create_chat(bob)
928
+ bob_chat_alice = bob.create_chat(alice)
929
+ bob2.create_chat(alice)
930
+ alice_chat_bob.send_text("Hello Bob!")
931
+
932
+ message = bob.wait_for_incoming_msg()
933
+ message2 = bob2.wait_for_incoming_msg()
934
+ assert message2.get_snapshot().state == MessageState.IN_FRESH
935
+
936
+ message.mark_seen()
937
+
938
+ # Send a message and wait until it arrives
939
+ # in order to wait until Bob2 gets the markseen message.
940
+ # This also tests that outgoing messages
941
+ # don't mark preceeding messages as seen in team profiles.
942
+ bob_chat_alice.send_text("Outgoing message")
943
+ while True:
944
+ outgoing = bob2.wait_for_msg(EventType.MSGS_CHANGED)
945
+ if outgoing.id != 0:
946
+ break
947
+ assert outgoing.get_snapshot().text == "Outgoing message"
948
+
949
+ if team_profile:
950
+ assert message2.get_snapshot().state == MessageState.IN_FRESH
951
+ else:
952
+ assert message2.get_snapshot().state == MessageState.IN_SEEN
953
+
954
+
801
955
  def test_read_receipt(acfactory):
802
956
  """
803
957
  Test sending a read receipt and ensure it is attributed to the correct contact.
@@ -817,6 +971,9 @@ def test_read_receipt(acfactory):
817
971
  assert len(read_receipts) == 1
818
972
  assert read_receipts[0].contact_id == alice_contact_bob.id
819
973
 
974
+ read_receipt_cnt = read_msg.get_read_receipt_count()
975
+ assert read_receipt_cnt == 1
976
+
820
977
 
821
978
  def test_get_http_response(acfactory):
822
979
  alice = acfactory.new_configured_account()
@@ -1020,6 +1177,30 @@ def test_leave_broadcast(acfactory, all_devices_online):
1020
1177
  check_account(bob2, bob2.create_contact(alice), inviter_side=False)
1021
1178
 
1022
1179
 
1180
+ def test_leave_and_delete_group(acfactory, log):
1181
+ alice, bob = acfactory.get_online_accounts(2)
1182
+
1183
+ log.section("Alice creates a group")
1184
+ alice_chat = alice.create_group("Group")
1185
+ alice_chat.add_contact(bob)
1186
+ assert len(alice_chat.get_contacts()) == 2 # Alice and Bob
1187
+ alice_chat.send_text("hello")
1188
+
1189
+ log.section("Bob sees the group, and leaves and deletes it")
1190
+ msg = bob.wait_for_incoming_msg().get_snapshot()
1191
+ assert msg.text == "hello"
1192
+ msg.chat.accept()
1193
+
1194
+ msg.chat.leave()
1195
+ # Bob deletes the chat. This must not prevent the leave message from being sent.
1196
+ msg.chat.delete()
1197
+
1198
+ log.section("Alice receives the delete message")
1199
+ # After Bob left, only Alice will be left in the group:
1200
+ while len(alice_chat.get_contacts()) != 1:
1201
+ alice.wait_for_event(EventType.CHAT_MODIFIED)
1202
+
1203
+
1023
1204
  def test_immediate_autodelete(acfactory, direct_imap, log):
1024
1205
  ac1, ac2 = acfactory.get_online_accounts(2)
1025
1206
 
@@ -1152,3 +1333,23 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, log):
1152
1333
 
1153
1334
  assert chat.num_contacts() == 2
1154
1335
  assert msg.get_snapshot().chat.num_contacts() == 2
1336
+
1337
+
1338
+ def test_large_message(acfactory) -> None:
1339
+ """
1340
+ Test sending large message without download limit set,
1341
+ so it is sent with pre-message but downloaded without user interaction.
1342
+ """
1343
+ alice, bob = acfactory.get_online_accounts(2)
1344
+
1345
+ alice_chat_bob = alice.create_chat(bob)
1346
+ alice_chat_bob.send_message(
1347
+ "Hello World, this message is bigger than 5 bytes",
1348
+ file="../test-data/image/screenshot.jpg",
1349
+ )
1350
+
1351
+ msg = bob.wait_for_incoming_msg()
1352
+ msgs_changed_event = bob.wait_for_msgs_changed_event()
1353
+ assert msg.id == msgs_changed_event.msg_id
1354
+ snapshot = msg.get_snapshot()
1355
+ assert snapshot.text == "Hello World, this message is bigger than 5 bytes"