deltachat-rpc-client 1.142.8__tar.gz → 2.24.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.

Potentially problematic release.


This version of deltachat-rpc-client might be problematic. Click here for more details.

Files changed (36) hide show
  1. {deltachat_rpc_client-1.142.8/src/deltachat_rpc_client.egg-info → deltachat_rpc_client-2.24.0}/PKG-INFO +9 -4
  2. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/README.md +2 -1
  3. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/pyproject.toml +16 -4
  4. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/__init__.py +1 -1
  5. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/_utils.py +6 -5
  6. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/account.py +158 -33
  7. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/chat.py +25 -4
  8. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/client.py +6 -3
  9. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/const.py +64 -26
  10. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/contact.py +8 -5
  11. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/deltachat.py +6 -5
  12. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/events.py +31 -26
  13. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/message.py +48 -1
  14. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/pytestplugin.py +88 -27
  15. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/rpc.py +24 -15
  16. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0/src/deltachat_rpc_client.egg-info}/PKG-INFO +9 -4
  17. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client.egg-info/SOURCES.txt +4 -2
  18. deltachat_rpc_client-2.24.0/tests/test_account_events.py +30 -0
  19. deltachat_rpc_client-2.24.0/tests/test_calls.py +109 -0
  20. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/tests/test_chatlist_events.py +6 -11
  21. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/tests/test_iroh_webxdc.py +35 -16
  22. deltachat_rpc_client-2.24.0/tests/test_key_transfer.py +49 -0
  23. deltachat_rpc_client-2.24.0/tests/test_multidevice.py +113 -0
  24. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/tests/test_securejoin.py +251 -188
  25. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/tests/test_something.py +453 -79
  26. deltachat_rpc_client-2.24.0/tests/test_vcard.py +27 -0
  27. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/tests/test_webxdc.py +8 -13
  28. deltachat_rpc_client-1.142.8/src/deltachat_rpc_client/direct_imap.py +0 -226
  29. deltachat_rpc_client-1.142.8/src/deltachat_rpc_client.egg-info/requires.txt +0 -1
  30. deltachat_rpc_client-1.142.8/tests/test_vcard.py +0 -15
  31. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/LICENSE +0 -0
  32. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/setup.cfg +0 -0
  33. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client/py.typed +0 -0
  34. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client.egg-info/dependency_links.txt +0 -0
  35. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client.egg-info/entry_points.txt +0 -0
  36. {deltachat_rpc_client-1.142.8 → deltachat_rpc_client-2.24.0}/src/deltachat_rpc_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: deltachat-rpc-client
3
- Version: 1.142.8
3
+ Version: 2.24.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
@@ -12,11 +12,15 @@ Classifier: Programming Language :: Python :: 3.8
12
12
  Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
15
18
  Classifier: Topic :: Communications :: Chat
16
19
  Classifier: Topic :: Communications :: Email
20
+ Requires-Python: >=3.8
17
21
  Description-Content-Type: text/markdown
18
22
  License-File: LICENSE
19
- Requires-Dist: imap-tools
23
+ Dynamic: license-file
20
24
 
21
25
  # Delta Chat RPC python client
22
26
 
@@ -45,7 +49,8 @@ $ pip install .
45
49
  ## Testing
46
50
 
47
51
  1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
48
- 2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
52
+ 2. Install tox `pip install -U tox`
53
+ 3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
49
54
 
50
55
  Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
51
56
 
@@ -25,7 +25,8 @@ $ pip install .
25
25
  ## Testing
26
26
 
27
27
  1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
28
- 2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
28
+ 2. Install tox `pip install -U tox`
29
+ 3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
29
30
 
30
31
  Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
31
32
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deltachat-rpc-client"
7
- version = "1.142.8"
7
+ version = "2.24.0"
8
8
  description = "Python client for Delta Chat core JSON-RPC interface"
9
9
  classifiers = [
10
10
  "Development Status :: 5 - Production/Stable",
@@ -17,13 +17,14 @@ classifiers = [
17
17
  "Programming Language :: Python :: 3.9",
18
18
  "Programming Language :: Python :: 3.10",
19
19
  "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
20
23
  "Topic :: Communications :: Chat",
21
24
  "Topic :: Communications :: Email"
22
25
  ]
23
26
  readme = "README.md"
24
- dependencies = [
25
- "imap-tools",
26
- ]
27
+ requires-python = ">=3.8"
27
28
 
28
29
  [tool.setuptools.package-data]
29
30
  deltachat_rpc_client = [
@@ -66,7 +67,18 @@ lint.select = [
66
67
 
67
68
  "RUF006" # asyncio-dangling-task
68
69
  ]
70
+ lint.ignore = [
71
+ "PLC0415" # `import` should be at the top-level of a file
72
+ ]
69
73
  line-length = 120
70
74
 
71
75
  [tool.isort]
72
76
  profile = "black"
77
+
78
+ [dependency-groups]
79
+ dev = [
80
+ "imap-tools",
81
+ "pytest",
82
+ "pytest-timeout",
83
+ "pytest-xdist",
84
+ ]
@@ -1,4 +1,4 @@
1
- """Delta Chat JSON-RPC high-level API"""
1
+ """Delta Chat JSON-RPC high-level API."""
2
2
 
3
3
  from ._utils import AttrDict, run_bot_cli, run_client_cli
4
4
  from .account import Account
@@ -1,4 +1,5 @@
1
1
  import argparse
2
+ import os
2
3
  import re
3
4
  import sys
4
5
  from threading import Thread
@@ -89,8 +90,8 @@ def _run_cli(
89
90
  help="accounts folder (default: current working directory)",
90
91
  nargs="?",
91
92
  )
92
- parser.add_argument("--email", action="store", help="email address")
93
- parser.add_argument("--password", action="store", help="password")
93
+ parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
94
+ parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
94
95
  args = parser.parse_args(argv[1:])
95
96
 
96
97
  with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
@@ -114,7 +115,7 @@ def _run_cli(
114
115
 
115
116
 
116
117
  def extract_addr(text: str) -> str:
117
- """extract email address from the given text."""
118
+ """Extract email address from the given text."""
118
119
  match = re.match(r".*\((.+@.+)\)", text)
119
120
  if match:
120
121
  text = match.group(1)
@@ -123,7 +124,7 @@ def extract_addr(text: str) -> str:
123
124
 
124
125
 
125
126
  def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
126
- """return image changed/deleted info from parsing the given system message text."""
127
+ """Return image changed/deleted info from parsing the given system message text."""
127
128
  text = text.lower()
128
129
  match = re.match(r"group image (changed|deleted) by (.+).", text)
129
130
  if match:
@@ -142,7 +143,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
142
143
 
143
144
 
144
145
  def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
145
- """return add/remove info from parsing the given system message text.
146
+ """Return add/remove info from parsing the given system message text.
146
147
 
147
148
  returns a (action, affected, actor) tuple.
148
149
  """
@@ -1,5 +1,8 @@
1
+ """Account module."""
2
+
1
3
  from __future__ import annotations
2
4
 
5
+ import json
3
6
  from dataclasses import dataclass
4
7
  from typing import TYPE_CHECKING, Optional, Union
5
8
  from warnings import warn
@@ -26,18 +29,36 @@ class Account:
26
29
  def _rpc(self) -> "Rpc":
27
30
  return self.manager.rpc
28
31
 
29
- def wait_for_event(self) -> AttrDict:
32
+ def wait_for_event(self, event_type=None) -> AttrDict:
30
33
  """Wait until the next event and return it."""
31
- return AttrDict(self._rpc.wait_for_event(self.id))
34
+ while True:
35
+ next_event = AttrDict(self._rpc.wait_for_event(self.id))
36
+ if event_type is None or next_event.kind == event_type:
37
+ return next_event
32
38
 
33
39
  def clear_all_events(self):
34
- """Removes all queued-up events for a given account. Useful for tests."""
40
+ """Remove all queued-up events for a given account.
41
+
42
+ Useful for tests.
43
+ """
35
44
  self._rpc.clear_all_events(self.id)
36
45
 
37
46
  def remove(self) -> None:
38
47
  """Remove the account."""
39
48
  self._rpc.remove_account(self.id)
40
49
 
50
+ def clone(self) -> "Account":
51
+ """Clone given account.
52
+
53
+ This uses backup-transfer via iroh, i.e. the 'Add second device' feature.
54
+ """
55
+ future = self._rpc.provide_backup.future(self.id)
56
+ qr = self._rpc.get_backup_qr(self.id)
57
+ new_account = self.manager.add_account()
58
+ new_account._rpc.get_backup(new_account.id, qr)
59
+ future()
60
+ return new_account
61
+
41
62
  def start_io(self) -> None:
42
63
  """Start the account I/O."""
43
64
  self._rpc.start_io(self.id)
@@ -67,7 +88,7 @@ class Account:
67
88
  return self._rpc.get_config(self.id, key)
68
89
 
69
90
  def update_config(self, **kwargs) -> None:
70
- """update config values."""
91
+ """Update config values."""
71
92
  for key, value in kwargs.items():
72
93
  self.set_config(key, value)
73
94
 
@@ -83,9 +104,15 @@ class Account:
83
104
  return self.get_config("selfavatar")
84
105
 
85
106
  def check_qr(self, qr):
107
+ """Parse QR code contents.
108
+
109
+ This function takes the raw text scanned
110
+ and checks what can be done with it.
111
+ """
86
112
  return self._rpc.check_qr(self.id, qr)
87
113
 
88
114
  def set_config_from_qr(self, qr: str):
115
+ """Set configuration values from a QR code."""
89
116
  self._rpc.set_config_from_qr(self.id, qr)
90
117
 
91
118
  @futuremethod
@@ -93,15 +120,28 @@ class Account:
93
120
  """Configure an account."""
94
121
  yield self._rpc.configure.future(self.id)
95
122
 
123
+ @futuremethod
124
+ def add_or_update_transport(self, params):
125
+ """Add a new transport."""
126
+ yield self._rpc.add_or_update_transport.future(self.id, params)
127
+
128
+ @futuremethod
129
+ def add_transport_from_qr(self, qr: str):
130
+ """Add a new transport using a QR code."""
131
+ yield self._rpc.add_transport_from_qr.future(self.id, qr)
132
+
133
+ @futuremethod
134
+ def list_transports(self):
135
+ """Return the list of all email accounts that are used as a transport in the current profile."""
136
+ transports = yield self._rpc.list_transports.future(self.id)
137
+ return transports
138
+
96
139
  def bring_online(self):
97
140
  """Start I/O and wait until IMAP becomes IDLE."""
98
141
  self.start_io()
99
- while True:
100
- event = self.wait_for_event()
101
- if event.kind == EventType.IMAP_INBOX_IDLE:
102
- break
142
+ self.wait_for_event(EventType.IMAP_INBOX_IDLE)
103
143
 
104
- def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
144
+ def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
105
145
  """Create a new Contact or return an existing one.
106
146
 
107
147
  Calling this method will always result in the same
@@ -109,26 +149,63 @@ class Account:
109
149
  with that e-mail address, it is unblocked and its display
110
150
  name is updated if specified.
111
151
 
112
- :param obj: email-address or contact id.
152
+ :param obj: email-address, contact id or account.
113
153
  :param name: (optional) display name for this contact.
114
154
  """
155
+ if isinstance(obj, Account):
156
+ vcard = obj.self_contact.make_vcard()
157
+ [contact] = self.import_vcard(vcard)
158
+ if name:
159
+ contact.set_name(name)
160
+ return contact
115
161
  if isinstance(obj, int):
116
162
  obj = Contact(self, obj)
117
163
  if isinstance(obj, Contact):
118
164
  obj = obj.get_snapshot().address
119
165
  return Contact(self, self._rpc.create_contact(self.id, obj, name))
120
166
 
167
+ def make_vcard(self, contacts: list[Contact]) -> str:
168
+ """Create vCard with the given contacts."""
169
+ assert all(contact.account == self for contact in contacts)
170
+ contact_ids = [contact.id for contact in contacts]
171
+ return self._rpc.make_vcard(self.id, contact_ids)
172
+
173
+ def import_vcard(self, vcard: str) -> list[Contact]:
174
+ """Import vCard.
175
+
176
+ Return created or modified contacts in the order they appear in vCard.
177
+ """
178
+ contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
179
+ return [Contact(self, contact_id) for contact_id in contact_ids]
180
+
121
181
  def create_chat(self, account: "Account") -> Chat:
122
- addr = account.get_config("addr")
123
- contact = self.create_contact(addr)
124
- return contact.create_chat()
182
+ """Create a 1:1 chat with another account."""
183
+ return self.create_contact(account).create_chat()
184
+
185
+ def get_device_chat(self) -> Chat:
186
+ """Return device chat."""
187
+ return self.device_contact.create_chat()
125
188
 
126
189
  def get_contact_by_id(self, contact_id: int) -> Contact:
127
190
  """Return Contact instance for the given contact ID."""
128
191
  return Contact(self, contact_id)
129
192
 
130
193
  def get_contact_by_addr(self, address: str) -> Optional[Contact]:
131
- """Check if an e-mail address belongs to a known and unblocked contact."""
194
+ """Looks up a known and unblocked contact with a given e-mail address.
195
+ To get a list of all known and unblocked contacts, use contacts_get_contacts().
196
+
197
+ **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
198
+ (e.g. an address-contact and a key-contact),
199
+ this looks up the most recently seen contact,
200
+ i.e. which contact is returned depends on which contact last sent a message.
201
+ If the user just clicked on a mailto: link, then this is the best thing you can do.
202
+ But **DO NOT** internally represent contacts by their email address
203
+ and do not use this function to look them up;
204
+ otherwise this function will sometimes look up the wrong contact.
205
+ Instead, you should internally represent contacts by their ids.
206
+
207
+ To validate an e-mail address independently of the contact database
208
+ use check_email_validity()."""
132
209
  contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
133
210
  return contact_id and Contact(self, contact_id)
134
211
 
@@ -154,8 +231,8 @@ class Account:
154
231
  def get_contacts(
155
232
  self,
156
233
  query: Optional[str] = None,
234
+ *,
157
235
  with_self: bool = False,
158
- verified_only: bool = False,
159
236
  snapshot: bool = False,
160
237
  ) -> Union[list[Contact], list[AttrDict]]:
161
238
  """Get a filtered list of contacts.
@@ -163,12 +240,9 @@ class Account:
163
240
  :param query: if a string is specified, only return contacts
164
241
  whose name or e-mail matches query.
165
242
  :param with_self: if True the self-contact is also included if it matches the query.
166
- :param only_verified: if True only return verified contacts.
167
243
  :param snapshot: If True return a list of contact snapshots instead of Contact instances.
168
244
  """
169
245
  flags = 0
170
- if verified_only:
171
- flags |= ContactFlag.VERIFIED_ONLY
172
246
  if with_self:
173
247
  flags |= ContactFlag.ADD_SELF
174
248
 
@@ -180,9 +254,14 @@ class Account:
180
254
 
181
255
  @property
182
256
  def self_contact(self) -> Contact:
183
- """This account's identity as a Contact."""
257
+ """Account's identity as a Contact."""
184
258
  return Contact(self, SpecialContactId.SELF)
185
259
 
260
+ @property
261
+ def device_contact(self) -> Chat:
262
+ """Account's device contact."""
263
+ return Contact(self, SpecialContactId.DEVICE)
264
+
186
265
  def get_chatlist(
187
266
  self,
188
267
  query: Optional[str] = None,
@@ -226,20 +305,51 @@ class Account:
226
305
  chats.append(AttrDict(item))
227
306
  return chats
228
307
 
229
- def create_group(self, name: str, protect: bool = False) -> Chat:
308
+ def create_group(self, name: str) -> Chat:
230
309
  """Create a new group chat.
231
310
 
232
- After creation, the group has only self-contact as member and is in unpromoted state.
311
+ After creation,
312
+ the group has only self-contact as member one member (see `SpecialContactId.SELF`)
313
+ and is in _unpromoted_ state.
314
+ This means, you can add or remove members, change the name,
315
+ the group image and so on without messages being sent to all group members.
316
+
317
+ This changes as soon as the first message is sent to the group members
318
+ and the group becomes _promoted_.
319
+ After that, all changes are synced with all group members
320
+ by sending status message.
321
+
322
+ To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
323
+ (see `get_full_snapshot()` / `get_basic_snapshot()`).
324
+ This may be useful if you want to show some help for just created groups.
233
325
  """
234
- return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
326
+ return Chat(self, self._rpc.create_group_chat(self.id, name, False))
327
+
328
+ def create_broadcast(self, name: str) -> Chat:
329
+ """Create a new, outgoing **broadcast channel**
330
+ (called "Channel" in the UI).
331
+
332
+ Broadcast channels are similar to groups on the sending device,
333
+ however, recipients get the messages in a read-only chat
334
+ and will not see who the other members are.
335
+
336
+ Called `broadcast` here rather than `channel`,
337
+ because the word "channel" already appears a lot in the code,
338
+ which would make it hard to grep for it.
339
+
340
+ After creation, the chat contains no recipients and is in _unpromoted_ state;
341
+ see `create_group()` for more information on the unpromoted state.
342
+
343
+ Returns the created chat.
344
+ """
345
+ return Chat(self, self._rpc.create_broadcast(self.id, name))
235
346
 
236
347
  def get_chat_by_id(self, chat_id: int) -> Chat:
237
348
  """Return the Chat instance with the given ID."""
238
349
  return Chat(self, chat_id)
239
350
 
240
351
  def secure_join(self, qrdata: str) -> Chat:
241
- """Continue a Setup-Contact or Verified-Group-Invite protocol started on
242
- another device.
352
+ """Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
243
353
 
244
354
  The function returns immediately and the handshake runs in background, sending
245
355
  and receiving several messages.
@@ -296,34 +406,40 @@ class Account:
296
406
 
297
407
  def wait_for_incoming_msg_event(self):
298
408
  """Wait for incoming message event and return it."""
299
- while True:
300
- event = self.wait_for_event()
301
- if event.kind == EventType.INCOMING_MSG:
302
- return event
409
+ return self.wait_for_event(EventType.INCOMING_MSG)
410
+
411
+ def wait_for_msgs_changed_event(self):
412
+ """Wait for messages changed event and return it."""
413
+ return self.wait_for_event(EventType.MSGS_CHANGED)
414
+
415
+ def wait_for_msgs_noticed_event(self):
416
+ """Wait for messages noticed event and return it."""
417
+ return self.wait_for_event(EventType.MSGS_NOTICED)
303
418
 
304
419
  def wait_for_incoming_msg(self):
305
420
  """Wait for incoming message and return it.
306
421
 
307
- Consumes all events before the next incoming message event."""
422
+ Consumes all events before the next incoming message event.
423
+ """
308
424
  return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
309
425
 
310
426
  def wait_for_securejoin_inviter_success(self):
427
+ """Wait until SecureJoin process finishes successfully on the inviter side."""
311
428
  while True:
312
429
  event = self.wait_for_event()
313
430
  if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
314
431
  break
315
432
 
316
433
  def wait_for_securejoin_joiner_success(self):
434
+ """Wait until SecureJoin process finishes successfully on the joiner side."""
317
435
  while True:
318
436
  event = self.wait_for_event()
319
437
  if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
320
438
  break
321
439
 
322
440
  def wait_for_reactions_changed(self):
323
- while True:
324
- event = self.wait_for_event()
325
- if event.kind == EventType.REACTIONS_CHANGED:
326
- return event
441
+ """Wait for reaction change event."""
442
+ return self.wait_for_event(EventType.REACTIONS_CHANGED)
327
443
 
328
444
  def get_fresh_messages_in_arrival_order(self) -> list[Message]:
329
445
  """Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
@@ -352,3 +468,12 @@ class Account:
352
468
  """Import keys."""
353
469
  passphrase = "" # Importing passphrase-protected keys is currently not supported.
354
470
  self._rpc.import_self_keys(self.id, str(path), passphrase)
471
+
472
+ def initiate_autocrypt_key_transfer(self) -> None:
473
+ """Send Autocrypt Setup Message."""
474
+ return self._rpc.initiate_autocrypt_key_transfer(self.id)
475
+
476
+ def ice_servers(self) -> list:
477
+ """Return ICE servers for WebRTC configuration."""
478
+ ice_servers_json = self._rpc.ice_servers(self.id)
479
+ return json.loads(ice_servers_json)
@@ -1,3 +1,5 @@
1
+ """Chat module."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  import calendar
@@ -89,7 +91,8 @@ class Chat:
89
91
  def set_ephemeral_timer(self, timer: int) -> None:
90
92
  """Set ephemeral timer of this chat in seconds.
91
93
 
92
- 0 means the timer is disabled, use 1 for immediate deletion."""
94
+ 0 means the timer is disabled, use 1 for immediate deletion.
95
+ """
93
96
  self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
94
97
 
95
98
  def get_encryption_info(self) -> str:
@@ -124,6 +127,7 @@ class Chat:
124
127
  html: Optional[str] = None,
125
128
  viewtype: Optional[ViewType] = None,
126
129
  file: Optional[str] = None,
130
+ filename: Optional[str] = None,
127
131
  location: Optional[tuple[float, float]] = None,
128
132
  override_sender_name: Optional[str] = None,
129
133
  quoted_msg: Optional[Union[int, Message]] = None,
@@ -137,6 +141,7 @@ class Chat:
137
141
  "html": html,
138
142
  "viewtype": viewtype,
139
143
  "file": file,
144
+ "filename": filename,
140
145
  "location": location,
141
146
  "overrideSenderName": override_sender_name,
142
147
  "quotedMessageId": quoted_msg,
@@ -163,6 +168,11 @@ class Chat:
163
168
  msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
164
169
  return Message(self.account, msg_id)
165
170
 
171
+ def resend_messages(self, messages: list[Message]) -> None:
172
+ """Resend a list of messages to this chat."""
173
+ msg_ids = [msg.id for msg in messages]
174
+ self._rpc.resend_messages(self.account.id, msg_ids)
175
+
166
176
  def forward_messages(self, messages: list[Message]) -> None:
167
177
  """Forward a list of messages to this chat."""
168
178
  msg_ids = [msg.id for msg in messages]
@@ -172,13 +182,14 @@ class Chat:
172
182
  self,
173
183
  text: Optional[str] = None,
174
184
  file: Optional[str] = None,
185
+ filename: Optional[str] = None,
175
186
  quoted_msg: Optional[int] = None,
176
187
  viewtype: Optional[str] = None,
177
188
  ) -> None:
178
189
  """Set draft message."""
179
190
  if isinstance(quoted_msg, Message):
180
191
  quoted_msg = quoted_msg.id
181
- self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype)
192
+ self._rpc.misc_set_draft(self.account.id, self.id, text, file, filename, quoted_msg, viewtype)
182
193
 
183
194
  def remove_draft(self) -> None:
184
195
  """Remove draft message."""
@@ -196,12 +207,12 @@ class Chat:
196
207
  return snapshot
197
208
 
198
209
  def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
199
- """get the list of messages in this chat."""
210
+ """Get the list of messages in this chat."""
200
211
  msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
201
212
  return [Message(self.account, msg_id) for msg_id in msgs]
202
213
 
203
214
  def get_fresh_message_count(self) -> int:
204
- """Get number of fresh messages in this chat"""
215
+ """Get number of fresh messages in this chat."""
205
216
  return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
206
217
 
207
218
  def mark_noticed(self) -> None:
@@ -238,6 +249,11 @@ class Chat:
238
249
  contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
239
250
  return [Contact(self.account, contact_id) for contact_id in contacts]
240
251
 
252
+ def get_past_contacts(self) -> list[Contact]:
253
+ """Get past contacts for this chat."""
254
+ past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
255
+ return [Contact(self.account, contact_id) for contact_id in past_contacts]
256
+
241
257
  def set_image(self, path: str) -> None:
242
258
  """Set profile image of this chat.
243
259
 
@@ -278,3 +294,8 @@ class Chat:
278
294
  f.write(vcard.encode())
279
295
  f.flush()
280
296
  self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
297
+
298
+ def place_outgoing_call(self, place_call_info: str) -> Message:
299
+ """Starts an outgoing call."""
300
+ msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
301
+ return Message(self.account, msg_id)
@@ -48,6 +48,7 @@ class Client:
48
48
  self.add_hooks(hooks or [])
49
49
 
50
50
  def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
51
+ """Register multiple hooks."""
51
52
  for hook, event in hooks:
52
53
  self.add_hook(hook, event)
53
54
 
@@ -77,14 +78,15 @@ class Client:
77
78
  self._hooks.get(type(event), set()).remove((hook, event))
78
79
 
79
80
  def is_configured(self) -> bool:
81
+ """Return True if the client is configured."""
80
82
  return self.account.is_configured()
81
83
 
82
84
  def configure(self, email: str, password: str, **kwargs) -> None:
83
- self.account.set_config("addr", email)
84
- self.account.set_config("mail_pw", password)
85
+ """Configure the client."""
85
86
  for key, value in kwargs.items():
86
87
  self.account.set_config(key, value)
87
- self.account.configure()
88
+ params = {"addr": email, "password": password}
89
+ self.account.add_or_update_transport(params)
88
90
  self.logger.debug("Account configured")
89
91
 
90
92
  def run_forever(self) -> None:
@@ -198,5 +200,6 @@ class Bot(Client):
198
200
  """Simple bot implementation that listens to events of a single account."""
199
201
 
200
202
  def configure(self, email: str, password: str, **kwargs) -> None:
203
+ """Configure the bot."""
201
204
  kwargs.setdefault("bot", "1")
202
205
  super().configure(email, password, **kwargs)