deltachat-rpc-client 2.23.0__tar.gz → 2.25.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 (33) hide show
  1. {deltachat_rpc_client-2.23.0/src/deltachat_rpc_client.egg-info → deltachat_rpc_client-2.25.0}/PKG-INFO +1 -1
  2. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/pyproject.toml +1 -1
  3. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/account.py +1 -1
  4. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/message.py +11 -0
  5. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0/src/deltachat_rpc_client.egg-info}/PKG-INFO +1 -1
  6. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_securejoin.py +138 -0
  7. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_something.py +106 -54
  8. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/LICENSE +0 -0
  9. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/README.md +0 -0
  10. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/setup.cfg +0 -0
  11. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/__init__.py +0 -0
  12. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/_utils.py +0 -0
  13. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/chat.py +0 -0
  14. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/client.py +0 -0
  15. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/const.py +0 -0
  16. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/contact.py +0 -0
  17. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/deltachat.py +0 -0
  18. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/events.py +0 -0
  19. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/py.typed +0 -0
  20. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/pytestplugin.py +0 -0
  21. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client/rpc.py +0 -0
  22. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client.egg-info/SOURCES.txt +0 -0
  23. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client.egg-info/dependency_links.txt +0 -0
  24. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client.egg-info/entry_points.txt +0 -0
  25. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/src/deltachat_rpc_client.egg-info/top_level.txt +0 -0
  26. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_account_events.py +0 -0
  27. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_calls.py +0 -0
  28. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_chatlist_events.py +0 -0
  29. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_iroh_webxdc.py +0 -0
  30. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_key_transfer.py +0 -0
  31. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_multidevice.py +0 -0
  32. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.0}/tests/test_vcard.py +0 -0
  33. {deltachat_rpc_client-2.23.0 → deltachat_rpc_client-2.25.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.23.0
3
+ Version: 2.25.0
4
4
  Summary: Python client for Delta Chat core JSON-RPC interface
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Intended Audience :: Developers
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deltachat-rpc-client"
7
- version = "2.23.0"
7
+ version = "2.25.0"
8
8
  description = "Python client for Delta Chat core JSON-RPC interface"
9
9
  classifiers = [
10
10
  "Development Status :: 5 - Production/Stable",
@@ -326,7 +326,7 @@ class Account:
326
326
  return Chat(self, self._rpc.create_group_chat(self.id, name, False))
327
327
 
328
328
  def create_broadcast(self, name: str) -> Chat:
329
- """Create a new **broadcast channel**
329
+ """Create a new, outgoing **broadcast channel**
330
330
  (called "Channel" in the UI).
331
331
 
332
332
  Broadcast channels are similar to groups on the sending device,
@@ -93,6 +93,17 @@ class Message:
93
93
  if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
94
94
  break
95
95
 
96
+ def resend(self) -> None:
97
+ """Resend messages and make information available for newly added chat members.
98
+ Resending sends out the original message, however, recipients and webxdc-status may differ.
99
+ Clients that already have the original message can still ignore the resent message as
100
+ they have tracked the state by dedicated updates.
101
+
102
+ Some messages cannot be resent, eg. info-messages, drafts, already pending messages,
103
+ or messages that are not sent by SELF.
104
+ """
105
+ self._rpc.resend_messages(self.account.id, [self.id])
106
+
96
107
  @futuremethod
97
108
  def send_webxdc_realtime_advertisement(self):
98
109
  """Send an advertisement to join the realtime channel."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deltachat-rpc-client
3
- Version: 2.23.0
3
+ Version: 2.25.0
4
4
  Summary: Python client for Delta Chat core JSON-RPC interface
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Intended Audience :: Developers
@@ -3,6 +3,7 @@ import logging
3
3
  import pytest
4
4
 
5
5
  from deltachat_rpc_client import Chat, EventType, SpecialContactId
6
+ from deltachat_rpc_client.const import ChatType
6
7
  from deltachat_rpc_client.rpc import JsonRpcError
7
8
 
8
9
 
@@ -109,6 +110,143 @@ def test_qr_securejoin(acfactory):
109
110
  fiona.wait_for_securejoin_joiner_success()
110
111
 
111
112
 
113
+ @pytest.mark.parametrize("all_devices_online", [True, False])
114
+ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
115
+ alice, bob, fiona = acfactory.get_online_accounts(3)
116
+
117
+ alice2 = alice.clone()
118
+ bob2 = bob.clone()
119
+
120
+ if all_devices_online:
121
+ alice2.start_io()
122
+ bob2.start_io()
123
+
124
+ logging.info("===================== Alice creates a broadcast =====================")
125
+ alice_chat = alice.create_broadcast("Broadcast channel!")
126
+ snapshot = alice_chat.get_basic_snapshot()
127
+ assert not snapshot.is_unpromoted # Broadcast channels are never unpromoted
128
+
129
+ logging.info("===================== Bob joins the broadcast =====================")
130
+
131
+ qr_code = alice_chat.get_qr_code()
132
+ bob.secure_join(qr_code)
133
+ alice.wait_for_securejoin_inviter_success()
134
+ bob.wait_for_securejoin_joiner_success()
135
+ alice_chat.send_text("Hello everyone!")
136
+
137
+ def get_broadcast(ac):
138
+ chat = ac.get_chatlist(query="Broadcast channel!")[0]
139
+ assert chat.get_basic_snapshot().name == "Broadcast channel!"
140
+ return chat
141
+
142
+ def wait_for_broadcast_messages(ac):
143
+ chat = get_broadcast(ac)
144
+
145
+ snapshot = ac.wait_for_incoming_msg().get_snapshot()
146
+ assert snapshot.text == "You joined the channel."
147
+ assert snapshot.chat_id == chat.id
148
+
149
+ snapshot = ac.wait_for_incoming_msg().get_snapshot()
150
+ assert snapshot.text == "Hello everyone!"
151
+ assert snapshot.chat_id == chat.id
152
+
153
+ def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
154
+ # Check that the chat partner is verified.
155
+ contact_snapshot = contact.get_snapshot()
156
+ assert contact_snapshot.is_verified
157
+
158
+ chat = get_broadcast(ac)
159
+ chat_msgs = chat.get_messages()
160
+
161
+ if please_wait_info_msg:
162
+ first_msg = chat_msgs.pop(0).get_snapshot()
163
+ assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
164
+ assert first_msg.is_info
165
+
166
+ encrypted_msg = chat_msgs[0].get_snapshot()
167
+ assert encrypted_msg.text == "Messages are end-to-end encrypted."
168
+ assert encrypted_msg.is_info
169
+
170
+ member_added_msg = chat_msgs[1].get_snapshot()
171
+ if inviter_side:
172
+ assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
173
+ else:
174
+ assert member_added_msg.text == "You joined the channel."
175
+ assert member_added_msg.is_info
176
+
177
+ hello_msg = chat_msgs[2].get_snapshot()
178
+ assert hello_msg.text == "Hello everyone!"
179
+ assert not hello_msg.is_info
180
+ assert hello_msg.show_padlock
181
+ assert hello_msg.error is None
182
+
183
+ assert len(chat_msgs) == 3
184
+
185
+ chat_snapshot = chat.get_full_snapshot()
186
+ assert chat_snapshot.is_encrypted
187
+ assert chat_snapshot.name == "Broadcast channel!"
188
+ if inviter_side:
189
+ assert chat_snapshot.chat_type == ChatType.OUT_BROADCAST
190
+ else:
191
+ assert chat_snapshot.chat_type == ChatType.IN_BROADCAST
192
+ assert chat_snapshot.can_send == inviter_side
193
+
194
+ chat_contacts = chat_snapshot.contact_ids
195
+ assert contact.id in chat_contacts
196
+ if inviter_side:
197
+ assert len(chat_contacts) == 1
198
+ else:
199
+ assert len(chat_contacts) == 2
200
+ assert SpecialContactId.SELF in chat_contacts
201
+ assert chat_snapshot.self_in_group
202
+
203
+ wait_for_broadcast_messages(bob)
204
+
205
+ check_account(alice, alice.create_contact(bob), inviter_side=True)
206
+ check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
207
+
208
+ logging.info("===================== Test Alice's second device =====================")
209
+
210
+ # Start second Alice device, if it wasn't started already.
211
+ alice2.start_io()
212
+
213
+ while True:
214
+ msg_id = alice2.wait_for_msgs_changed_event().msg_id
215
+ if msg_id:
216
+ snapshot = alice2.get_message_by_id(msg_id).get_snapshot()
217
+ if snapshot.text == "Hello everyone!":
218
+ break
219
+
220
+ check_account(alice2, alice2.create_contact(bob), inviter_side=True)
221
+
222
+ logging.info("===================== Test Bob's second device =====================")
223
+
224
+ # Start second Bob device, if it wasn't started already.
225
+ bob2.start_io()
226
+ bob2.wait_for_securejoin_joiner_success()
227
+ wait_for_broadcast_messages(bob2)
228
+ check_account(bob2, bob2.create_contact(alice), inviter_side=False)
229
+
230
+ # The QR code token is synced, so alice2 must be able to handle join requests.
231
+ logging.info("===================== Fiona joins the group via alice2 =====================")
232
+ alice.stop_io()
233
+ fiona.secure_join(qr_code)
234
+ alice2.wait_for_securejoin_inviter_success()
235
+ fiona.wait_for_securejoin_joiner_success()
236
+
237
+ snapshot = fiona.wait_for_incoming_msg().get_snapshot()
238
+ assert snapshot.text == "You joined the channel."
239
+
240
+ get_broadcast(alice2).get_messages()[2].resend()
241
+ snapshot = fiona.wait_for_incoming_msg().get_snapshot()
242
+ assert snapshot.text == "Hello everyone!"
243
+
244
+ check_account(fiona, fiona.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
245
+
246
+ # For Bob, the channel must not have changed:
247
+ check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
248
+
249
+
112
250
  def test_qr_securejoin_contact_request(acfactory) -> None:
113
251
  """Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
114
252
  alice, bob = acfactory.get_online_accounts(2)
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
11
11
  import pytest
12
12
 
13
13
  from deltachat_rpc_client import Contact, EventType, Message, events
14
- from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
14
+ from deltachat_rpc_client.const import DownloadState, MessageState
15
15
  from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
16
16
  from deltachat_rpc_client.rpc import JsonRpcError
17
17
 
@@ -338,44 +338,27 @@ def test_receive_imf_failure(acfactory) -> None:
338
338
 
339
339
  bob.set_config("fail_on_receiving_full_msg", "1")
340
340
  alice_chat_bob.send_text("Hello!")
341
- event = bob.wait_for_incoming_msg_event()
342
- chat_id = event.chat_id
341
+ event = bob.wait_for_event(EventType.MSGS_CHANGED)
342
+ assert event.chat_id == bob.get_device_chat().id
343
343
  msg_id = event.msg_id
344
344
  message = bob.get_message_by_id(msg_id)
345
345
  snapshot = message.get_snapshot()
346
- assert snapshot.chat_id == chat_id
347
- assert snapshot.download_state == DownloadState.AVAILABLE
348
- assert snapshot.error is not None
349
- assert snapshot.show_padlock
350
- snapshot.chat.accept()
346
+ assert (
347
+ snapshot.text == "❌ Failed to receive a message:"
348
+ " Condition failed: `!context.get_config_bool(Config::FailOnReceivingFullMsg).await?`."
349
+ " Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
350
+ )
351
351
 
352
352
  # The failed message doesn't break the IMAP loop.
353
353
  bob.set_config("fail_on_receiving_full_msg", "0")
354
354
  alice_chat_bob.send_text("Hello again!")
355
355
  event = bob.wait_for_incoming_msg_event()
356
- assert event.chat_id == chat_id
357
- msg_id = event.msg_id
358
- message1 = bob.get_message_by_id(msg_id)
359
- snapshot = message1.get_snapshot()
360
- assert snapshot.chat_id == chat_id
361
- assert snapshot.download_state == DownloadState.DONE
362
- assert snapshot.error is None
363
-
364
- # The failed message can be re-downloaded later.
365
- bob._rpc.download_full_message(bob.id, message.id)
366
- event = bob.wait_for_event(EventType.MSGS_CHANGED)
367
- message = bob.get_message_by_id(event.msg_id)
368
- snapshot = message.get_snapshot()
369
- assert snapshot.download_state == DownloadState.IN_PROGRESS
370
- event = bob.wait_for_event(EventType.MSGS_CHANGED)
371
- assert event.chat_id == chat_id
372
356
  msg_id = event.msg_id
373
357
  message = bob.get_message_by_id(msg_id)
374
358
  snapshot = message.get_snapshot()
375
- assert snapshot.chat_id == chat_id
359
+ assert snapshot.text == "Hello again!"
376
360
  assert snapshot.download_state == DownloadState.DONE
377
361
  assert snapshot.error is None
378
- assert snapshot.text == "Hello!"
379
362
 
380
363
 
381
364
  def test_selfavatar_sync(acfactory, data, log) -> None:
@@ -930,34 +913,103 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
930
913
  assert "DeltaChat" in ac1_direct_imap.list_folders()
931
914
 
932
915
 
933
- def test_broadcast(acfactory):
916
+ @pytest.mark.parametrize("all_devices_online", [True, False])
917
+ def test_leave_broadcast(acfactory, all_devices_online):
934
918
  alice, bob = acfactory.get_online_accounts(2)
935
919
 
936
- alice_chat = alice.create_broadcast("My great channel")
937
- snapshot = alice_chat.get_basic_snapshot()
938
- assert snapshot.name == "My great channel"
939
- assert snapshot.is_unpromoted
940
- assert snapshot.is_encrypted
941
- assert snapshot.chat_type == ChatType.OUT_BROADCAST
920
+ bob2 = bob.clone()
942
921
 
943
- alice_contact_bob = alice.create_contact(bob, "Bob")
944
- alice_chat.add_contact(alice_contact_bob)
945
-
946
- alice_msg = alice_chat.send_message(text="hello").get_snapshot()
947
- assert alice_msg.text == "hello"
948
- assert alice_msg.show_padlock
949
-
950
- bob_msg = bob.wait_for_incoming_msg().get_snapshot()
951
- assert bob_msg.text == "hello"
952
- assert bob_msg.show_padlock
953
- assert bob_msg.error is None
954
-
955
- bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
956
- bob_chat_snapshot = bob_chat.get_basic_snapshot()
957
- assert bob_chat_snapshot.name == "My great channel"
958
- assert not bob_chat_snapshot.is_unpromoted
959
- assert bob_chat_snapshot.is_encrypted
960
- assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
961
- assert bob_chat_snapshot.is_contact_request
962
-
963
- assert not bob_chat.can_send()
922
+ if all_devices_online:
923
+ bob2.start_io()
924
+
925
+ logging.info("===================== Alice creates a broadcast =====================")
926
+ alice_chat = alice.create_broadcast("Broadcast channel!")
927
+
928
+ logging.info("===================== Bob joins the broadcast =====================")
929
+ qr_code = alice_chat.get_qr_code()
930
+ bob.secure_join(qr_code)
931
+ alice.wait_for_securejoin_inviter_success()
932
+ bob.wait_for_securejoin_joiner_success()
933
+
934
+ alice_bob_contact = alice.create_contact(bob)
935
+ alice_contacts = alice_chat.get_contacts()
936
+ assert len(alice_contacts) == 1 # 1 recipient
937
+ assert alice_contacts[0].id == alice_bob_contact.id
938
+
939
+ member_added_msg = bob.wait_for_incoming_msg()
940
+ assert member_added_msg.get_snapshot().text == "You joined the channel."
941
+
942
+ def get_broadcast(ac):
943
+ chat = ac.get_chatlist(query="Broadcast channel!")[0]
944
+ assert chat.get_basic_snapshot().name == "Broadcast channel!"
945
+ return chat
946
+
947
+ def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
948
+ chat = get_broadcast(ac)
949
+ contact_snapshot = contact.get_snapshot()
950
+ chat_msgs = chat.get_messages()
951
+
952
+ if please_wait_info_msg:
953
+ first_msg = chat_msgs.pop(0).get_snapshot()
954
+ assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
955
+ assert first_msg.is_info
956
+
957
+ encrypted_msg = chat_msgs.pop(0).get_snapshot()
958
+ assert encrypted_msg.text == "Messages are end-to-end encrypted."
959
+ assert encrypted_msg.is_info
960
+
961
+ member_added_msg = chat_msgs.pop(0).get_snapshot()
962
+ if inviter_side:
963
+ assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
964
+ else:
965
+ assert member_added_msg.text == "You joined the channel."
966
+ assert member_added_msg.is_info
967
+
968
+ if not inviter_side:
969
+ leave_msg = chat_msgs.pop(0).get_snapshot()
970
+ assert leave_msg.text == "You left the channel."
971
+
972
+ assert len(chat_msgs) == 0
973
+
974
+ chat_snapshot = chat.get_full_snapshot()
975
+
976
+ # On Alice's side, SELF is not in the list of contact ids
977
+ # because OutBroadcast chats never contain SELF in the list.
978
+ # On Bob's side, SELF is not in the list because he left.
979
+ if inviter_side:
980
+ assert len(chat_snapshot.contact_ids) == 0
981
+ else:
982
+ assert chat_snapshot.contact_ids == [contact.id]
983
+
984
+ logging.info("===================== Bob leaves the broadcast =====================")
985
+ bob_chat = get_broadcast(bob)
986
+ assert bob_chat.get_full_snapshot().self_in_group
987
+ assert len(bob_chat.get_contacts()) == 2 # Alice and Bob
988
+
989
+ bob_chat.leave()
990
+ assert not bob_chat.get_full_snapshot().self_in_group
991
+ # After Bob left, only Alice will be left in Bob's memberlist
992
+ assert len(bob_chat.get_contacts()) == 1
993
+
994
+ check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
995
+
996
+ logging.info("===================== Test Alice's device =====================")
997
+ while len(alice_chat.get_contacts()) != 0: # After Bob left, there will be 0 recipients
998
+ alice.wait_for_event(EventType.CHAT_MODIFIED)
999
+
1000
+ check_account(alice, alice.create_contact(bob), inviter_side=True)
1001
+
1002
+ logging.info("===================== Test Bob's second device =====================")
1003
+ # Start second Bob device, if it wasn't started already.
1004
+ bob2.start_io()
1005
+
1006
+ member_added_msg = bob2.wait_for_incoming_msg()
1007
+ assert member_added_msg.get_snapshot().text == "You joined the channel."
1008
+
1009
+ bob2_chat = get_broadcast(bob2)
1010
+
1011
+ # After Bob left, only Alice will be left in Bob's memberlist
1012
+ while len(bob2_chat.get_contacts()) != 1:
1013
+ bob2.wait_for_event(EventType.CHAT_MODIFIED)
1014
+
1015
+ check_account(bob2, bob2.create_contact(alice), inviter_side=False)