diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e331ddf945..65a83793f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: name: Lint Rust runs-on: ubuntu-latest env: - RUSTUP_TOOLCHAIN: 1.72.0 + RUSTUP_TOOLCHAIN: 1.73.0 steps: - uses: actions/checkout@v3 - name: Install rustfmt and clippy @@ -81,11 +81,11 @@ jobs: matrix: include: - os: ubuntu-latest - rust: 1.71.0 + rust: 1.73.0 - os: windows-latest - rust: 1.71.0 + rust: 1.73.0 - os: macos-latest - rust: 1.71.0 + rust: 1.73.0 # Minimum Supported Rust Version = 1.67.0 - os: ubuntu-latest @@ -229,14 +229,10 @@ jobs: fail-fast: false matrix: include: - # Async Python bindings do not depend on Python version, - # but are tested on Python 3.11 until Python 3.12 support - # is added to `aiohttp` dependency: - # https://github.com/aio-libs/aiohttp/issues/7646 - os: ubuntu-latest - python: 3.11 + python: 3.12 - os: macos-latest - python: 3.11 + python: 3.12 # PyPy tests - os: ubuntu-latest @@ -244,12 +240,9 @@ jobs: - os: macos-latest python: pypy3.10 - # Minimum Supported Python Version = 3.8 - # - # Python 3.7 has at least one known bug related to starting subprocesses - # in asyncio programs: + # Minimum Supported Python Version = 3.7 - os: ubuntu-latest - python: 3.8 + python: 3.7 runs-on: ${{ matrix.os }} steps: diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 1e88301b6d..40fa149fad 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -139,11 +139,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool { /* import a directory */ let dir_name = std::path::Path::new(&real_spec); let dir = fs::read_dir(dir_name).await; - if dir.is_err() { - error!(context, "Import: Cannot open directory \"{}\".", &real_spec,); - return false; - } else { - let mut dir = dir.unwrap(); + if let Ok(mut dir) = dir { while let Ok(Some(entry)) = dir.next_entry().await { let name_f = entry.file_name(); let name = name_f.to_string_lossy(); @@ -155,6 +151,9 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool { } } } + } else { + error!(context, "Import: Cannot open directory \"{}\".", &real_spec); + return false; } } println!("Import: {} items read from \"{}\".", read_cnt, &real_spec); diff --git a/deltachat-rpc-client/README.md b/deltachat-rpc-client/README.md index d799a5ef2b..4b7503fce8 100644 --- a/deltachat-rpc-client/README.md +++ b/deltachat-rpc-client/README.md @@ -37,19 +37,14 @@ $ tox --devenv env $ . env/bin/activate ``` -It is recommended to use IPython, because it supports using `await` directly -from the REPL. - -``` -$ pip install ipython -$ PATH="../target/debug:$PATH" ipython -... -In [1]: from deltachat_rpc_client import * -In [2]: rpc = Rpc() -In [3]: await rpc.start() -In [4]: dc = DeltaChat(rpc) -In [5]: system_info = await dc.get_system_info() -In [6]: system_info["level"] -Out[6]: 'awesome' -In [7]: await rpc.close() +``` +$ python +>>> from deltachat_rpc_client import * +>>> rpc = Rpc() +>>> rpc.start() +>>> dc = DeltaChat(rpc) +>>> system_info = dc.get_system_info() +>>> system_info["level"] +'awesome' +>>> rpc.close() ``` diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index 65d447bf97..7d2d79e8b6 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -4,23 +4,21 @@ it will echo back any text send to it, it also will print to console all Delta Chat core events. Pass --help to the CLI to see available options. """ -import asyncio - from deltachat_rpc_client import events, run_bot_cli hooks = events.HookCollection() @hooks.on(events.RawEvent) -async def log_event(event): +def log_event(event): print(event) @hooks.on(events.NewMessage) -async def echo(event): +def echo(event): snapshot = event.message_snapshot - await snapshot.chat.send_text(snapshot.text) + snapshot.chat.send_text(snapshot.text) if __name__ == "__main__": - asyncio.run(run_bot_cli(hooks)) + run_bot_cli(hooks) diff --git a/deltachat-rpc-client/examples/echobot_advanced.py b/deltachat-rpc-client/examples/echobot_advanced.py index 4fcbc52f26..221f69dda0 100644 --- a/deltachat-rpc-client/examples/echobot_advanced.py +++ b/deltachat-rpc-client/examples/echobot_advanced.py @@ -3,9 +3,9 @@ it will echo back any message that has non-empty text and also supports the /help command. """ -import asyncio import logging import sys +from threading import Thread from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events @@ -13,7 +13,7 @@ @hooks.on(events.RawEvent) -async def log_event(event): +def log_event(event): if event.kind == EventType.INFO: logging.info(event.msg) elif event.kind == EventType.WARNING: @@ -21,54 +21,54 @@ async def log_event(event): @hooks.on(events.RawEvent(EventType.ERROR)) -async def log_error(event): +def log_error(event): logging.error(event.msg) @hooks.on(events.MemberListChanged) -async def on_memberlist_changed(event): +def on_memberlist_changed(event): logging.info("member %s was %s", event.member, "added" if event.member_added else "removed") @hooks.on(events.GroupImageChanged) -async def on_group_image_changed(event): +def on_group_image_changed(event): logging.info("group image %s", "deleted" if event.image_deleted else "changed") @hooks.on(events.GroupNameChanged) -async def on_group_name_changed(event): +def on_group_name_changed(event): logging.info("group name changed, old name: %s", event.old_name) @hooks.on(events.NewMessage(func=lambda e: not e.command)) -async def echo(event): +def echo(event): snapshot = event.message_snapshot if snapshot.text or snapshot.file: - await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file) + snapshot.chat.send_message(text=snapshot.text, file=snapshot.file) @hooks.on(events.NewMessage(command="/help")) -async def help_command(event): +def help_command(event): snapshot = event.message_snapshot - await snapshot.chat.send_text("Send me any message and I will echo it back") + snapshot.chat.send_text("Send me any message and I will echo it back") -async def main(): - async with Rpc() as rpc: +def main(): + with Rpc() as rpc: deltachat = DeltaChat(rpc) - system_info = await deltachat.get_system_info() + system_info = deltachat.get_system_info() logging.info("Running deltachat core %s", system_info.deltachat_core_version) - accounts = await deltachat.get_all_accounts() - account = accounts[0] if accounts else await deltachat.add_account() + accounts = deltachat.get_all_accounts() + account = accounts[0] if accounts else deltachat.add_account() bot = Bot(account, hooks) - if not await bot.is_configured(): - # Save a reference to avoid garbage collection of the task. - _configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2])) - await bot.run_forever() + if not bot.is_configured(): + configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]}) + configure_thread.start() + bot.run_forever() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - asyncio.run(main()) + main() diff --git a/deltachat-rpc-client/examples/echobot_no_hooks.py b/deltachat-rpc-client/examples/echobot_no_hooks.py index 77fda86e72..73e875b0d9 100644 --- a/deltachat-rpc-client/examples/echobot_no_hooks.py +++ b/deltachat-rpc-client/examples/echobot_no_hooks.py @@ -2,45 +2,44 @@ """ Example echo bot without using hooks """ -import asyncio import logging import sys from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId -async def main(): - async with Rpc() as rpc: +def main(): + with Rpc() as rpc: deltachat = DeltaChat(rpc) - system_info = await deltachat.get_system_info() + system_info = deltachat.get_system_info() logging.info("Running deltachat core %s", system_info["deltachat_core_version"]) - accounts = await deltachat.get_all_accounts() - account = accounts[0] if accounts else await deltachat.add_account() + accounts = deltachat.get_all_accounts() + account = accounts[0] if accounts else deltachat.add_account() - await account.set_config("bot", "1") - if not await account.is_configured(): + account.set_config("bot", "1") + if not account.is_configured(): logging.info("Account is not configured, configuring") - await account.set_config("addr", sys.argv[1]) - await account.set_config("mail_pw", sys.argv[2]) - await account.configure() + account.set_config("addr", sys.argv[1]) + account.set_config("mail_pw", sys.argv[2]) + account.configure() logging.info("Configured") else: logging.info("Account is already configured") - await deltachat.start_io() + deltachat.start_io() - async def process_messages(): - for message in await account.get_next_messages(): - snapshot = await message.get_snapshot() + def process_messages(): + for message in account.get_next_messages(): + snapshot = message.get_snapshot() if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info: - await snapshot.chat.send_text(snapshot.text) - await snapshot.message.mark_seen() + snapshot.chat.send_text(snapshot.text) + snapshot.message.mark_seen() # Process old messages. - await process_messages() + process_messages() while True: - event = await account.wait_for_event() + event = account.wait_for_event() if event["type"] == EventType.INFO: logging.info("%s", event["msg"]) elif event["type"] == EventType.WARNING: @@ -49,9 +48,9 @@ async def process_messages(): logging.error("%s", event["msg"]) elif event["type"] == EventType.INCOMING_MSG: logging.info("Got an incoming message") - await process_messages() + process_messages() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - asyncio.run(main()) + main() diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml index 629ebb1ef0..9d7324ca65 100644 --- a/deltachat-rpc-client/pyproject.toml +++ b/deltachat-rpc-client/pyproject.toml @@ -5,9 +5,6 @@ build-backend = "setuptools.build_meta" [project] name = "deltachat-rpc-client" description = "Python client for Delta Chat core JSON-RPC interface" -dependencies = [ - "aiohttp" -] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index 727c51c802..6589813ae0 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -1,4 +1,4 @@ -"""Delta Chat asynchronous high-level API""" +"""Delta Chat JSON-RPC high-level API""" from ._utils import AttrDict, run_bot_cli, run_client_cli from .account import Account from .chat import Chat diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py index ec99f6dca7..90a8b3d511 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py @@ -1,7 +1,7 @@ import argparse -import asyncio import re import sys +from threading import Thread from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union if TYPE_CHECKING: @@ -43,7 +43,7 @@ def __setattr__(self, attr, val): super().__setattr__(attr, val) -async def run_client_cli( +def run_client_cli( hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, argv: Optional[list] = None, **kwargs, @@ -54,10 +54,10 @@ async def run_client_cli( """ from .client import Client - await _run_cli(Client, hooks, argv, **kwargs) + _run_cli(Client, hooks, argv, **kwargs) -async def run_bot_cli( +def run_bot_cli( hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, argv: Optional[list] = None, **kwargs, @@ -68,10 +68,10 @@ async def run_bot_cli( """ from .client import Bot - await _run_cli(Bot, hooks, argv, **kwargs) + _run_cli(Bot, hooks, argv, **kwargs) -async def _run_cli( +def _run_cli( client_type: Type["Client"], hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, argv: Optional[list] = None, @@ -93,20 +93,20 @@ async def _run_cli( parser.add_argument("--password", action="store", help="password") args = parser.parse_args(argv[1:]) - async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc: + with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc: deltachat = DeltaChat(rpc) - core_version = (await deltachat.get_system_info()).deltachat_core_version - accounts = await deltachat.get_all_accounts() - account = accounts[0] if accounts else await deltachat.add_account() + core_version = (deltachat.get_system_info()).deltachat_core_version + accounts = deltachat.get_all_accounts() + account = accounts[0] if accounts else deltachat.add_account() client = client_type(account, hooks) client.logger.debug("Running deltachat core %s", core_version) - if not await client.is_configured(): + if not client.is_configured(): assert args.email, "Account is not configured and email must be provided" assert args.password, "Account is not configured and password must be provided" - # Save a reference to avoid garbage collection of the task. - _configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password)) - await client.run_forever() + configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password}) + configure_thread.start() + client.run_forever() def extract_addr(text: str) -> str: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index ce5d52f9a2..0c6c53a2a9 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -24,63 +24,63 @@ class Account: def _rpc(self) -> "Rpc": return self.manager.rpc - async def wait_for_event(self) -> AttrDict: + def wait_for_event(self) -> AttrDict: """Wait until the next event and return it.""" - return AttrDict(await self._rpc.wait_for_event(self.id)) + return AttrDict(self._rpc.wait_for_event(self.id)) - async def remove(self) -> None: + def remove(self) -> None: """Remove the account.""" - await self._rpc.remove_account(self.id) + self._rpc.remove_account(self.id) - async def start_io(self) -> None: + def start_io(self) -> None: """Start the account I/O.""" - await self._rpc.start_io(self.id) + self._rpc.start_io(self.id) - async def stop_io(self) -> None: + def stop_io(self) -> None: """Stop the account I/O.""" - await self._rpc.stop_io(self.id) + self._rpc.stop_io(self.id) - async def get_info(self) -> AttrDict: + def get_info(self) -> AttrDict: """Return dictionary of this account configuration parameters.""" - return AttrDict(await self._rpc.get_info(self.id)) + return AttrDict(self._rpc.get_info(self.id)) - async def get_size(self) -> int: + def get_size(self) -> int: """Get the combined filesize of an account in bytes.""" - return await self._rpc.get_account_file_size(self.id) + return self._rpc.get_account_file_size(self.id) - async def is_configured(self) -> bool: + def is_configured(self) -> bool: """Return True if this account is configured.""" - return await self._rpc.is_configured(self.id) + return self._rpc.is_configured(self.id) - async def set_config(self, key: str, value: Optional[str] = None) -> None: + def set_config(self, key: str, value: Optional[str] = None) -> None: """Set configuration value.""" - await self._rpc.set_config(self.id, key, value) + self._rpc.set_config(self.id, key, value) - async def get_config(self, key: str) -> Optional[str]: + def get_config(self, key: str) -> Optional[str]: """Get configuration value.""" - return await self._rpc.get_config(self.id, key) + return self._rpc.get_config(self.id, key) - async def update_config(self, **kwargs) -> None: + def update_config(self, **kwargs) -> None: """update config values.""" for key, value in kwargs.items(): - await self.set_config(key, value) + self.set_config(key, value) - async def set_avatar(self, img_path: Optional[str] = None) -> None: + def set_avatar(self, img_path: Optional[str] = None) -> None: """Set self avatar. Passing None will discard the currently set avatar. """ - await self.set_config("selfavatar", img_path) + self.set_config("selfavatar", img_path) - async def get_avatar(self) -> Optional[str]: + def get_avatar(self) -> Optional[str]: """Get self avatar.""" - return await self.get_config("selfavatar") + return self.get_config("selfavatar") - async def configure(self) -> None: + def configure(self) -> None: """Configure an account.""" - await self._rpc.configure(self.id) + self._rpc.configure(self.id) - async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact: + def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact: """Create a new Contact or return an existing one. Calling this method will always result in the same @@ -94,24 +94,24 @@ async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str if isinstance(obj, int): obj = Contact(self, obj) if isinstance(obj, Contact): - obj = (await obj.get_snapshot()).address - return Contact(self, await self._rpc.create_contact(self.id, obj, name)) + obj = obj.get_snapshot().address + return Contact(self, self._rpc.create_contact(self.id, obj, name)) def get_contact_by_id(self, contact_id: int) -> Contact: """Return Contact instance for the given contact ID.""" return Contact(self, contact_id) - async def get_contact_by_addr(self, address: str) -> Optional[Contact]: + def get_contact_by_addr(self, address: str) -> Optional[Contact]: """Check if an e-mail address belongs to a known and unblocked contact.""" - contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address) + contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address) return contact_id and Contact(self, contact_id) - async def get_blocked_contacts(self) -> List[AttrDict]: + def get_blocked_contacts(self) -> List[AttrDict]: """Return a list with snapshots of all blocked contacts.""" - contacts = await self._rpc.get_blocked_contacts(self.id) + contacts = self._rpc.get_blocked_contacts(self.id) return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts] - async def get_contacts( + def get_contacts( self, query: Optional[str] = None, with_self: bool = False, @@ -133,9 +133,9 @@ async def get_contacts( flags |= ContactFlag.ADD_SELF if snapshot: - contacts = await self._rpc.get_contacts(self.id, flags, query) + contacts = self._rpc.get_contacts(self.id, flags, query) return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts] - contacts = await self._rpc.get_contact_ids(self.id, flags, query) + contacts = self._rpc.get_contact_ids(self.id, flags, query) return [Contact(self, contact_id) for contact_id in contacts] @property @@ -143,7 +143,7 @@ def self_contact(self) -> Contact: """This account's identity as a Contact.""" return Contact(self, SpecialContactId.SELF) - async def get_chatlist( + def get_chatlist( self, query: Optional[str] = None, contact: Optional[Contact] = None, @@ -175,29 +175,29 @@ async def get_chatlist( if alldone_hint: flags |= ChatlistFlag.ADD_ALLDONE_HINT - entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id) + entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id) if not snapshot: return [Chat(self, entry) for entry in entries] - items = await self._rpc.get_chatlist_items_by_entries(self.id, entries) + items = self._rpc.get_chatlist_items_by_entries(self.id, entries) chats = [] for item in items.values(): item["chat"] = Chat(self, item["id"]) chats.append(AttrDict(item)) return chats - async def create_group(self, name: str, protect: bool = False) -> Chat: + def create_group(self, name: str, protect: bool = False) -> Chat: """Create a new group chat. After creation, the group has only self-contact as member and is in unpromoted state. """ - return Chat(self, await self._rpc.create_group_chat(self.id, name, protect)) + return Chat(self, self._rpc.create_group_chat(self.id, name, protect)) def get_chat_by_id(self, chat_id: int) -> Chat: """Return the Chat instance with the given ID.""" return Chat(self, chat_id) - async def secure_join(self, qrdata: str) -> Chat: + def secure_join(self, qrdata: str) -> Chat: """Continue a Setup-Contact or Verified-Group-Invite protocol started on another device. @@ -208,62 +208,62 @@ async def secure_join(self, qrdata: str) -> Chat: :param qrdata: The text of the scanned QR code. """ - return Chat(self, await self._rpc.secure_join(self.id, qrdata)) + return Chat(self, self._rpc.secure_join(self.id, qrdata)) - async def get_qr_code(self) -> Tuple[str, str]: + def get_qr_code(self) -> Tuple[str, str]: """Get Setup-Contact QR Code text and SVG data. this data needs to be transferred to another Delta Chat account in a second channel, typically used by mobiles with QRcode-show + scan UX. """ - return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None) + return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None) def get_message_by_id(self, msg_id: int) -> Message: """Return the Message instance with the given ID.""" return Message(self, msg_id) - async def mark_seen_messages(self, messages: List[Message]) -> None: + def mark_seen_messages(self, messages: List[Message]) -> None: """Mark the given set of messages as seen.""" - await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages]) + self._rpc.markseen_msgs(self.id, [msg.id for msg in messages]) - async def delete_messages(self, messages: List[Message]) -> None: + def delete_messages(self, messages: List[Message]) -> None: """Delete messages (local and remote).""" - await self._rpc.delete_messages(self.id, [msg.id for msg in messages]) + self._rpc.delete_messages(self.id, [msg.id for msg in messages]) - async def get_fresh_messages(self) -> List[Message]: + def get_fresh_messages(self) -> List[Message]: """Return the list of fresh messages, newest messages first. This call is intended for displaying notifications. If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead, to process oldest messages first. """ - fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id) + fresh_msg_ids = self._rpc.get_fresh_msgs(self.id) return [Message(self, msg_id) for msg_id in fresh_msg_ids] - async def get_next_messages(self) -> List[Message]: + def get_next_messages(self) -> List[Message]: """Return a list of next messages.""" - next_msg_ids = await self._rpc.get_next_msgs(self.id) + next_msg_ids = self._rpc.get_next_msgs(self.id) return [Message(self, msg_id) for msg_id in next_msg_ids] - async def wait_next_messages(self) -> List[Message]: + def wait_next_messages(self) -> List[Message]: """Wait for new messages and return a list of them.""" - next_msg_ids = await self._rpc.wait_next_msgs(self.id) + next_msg_ids = self._rpc.wait_next_msgs(self.id) return [Message(self, msg_id) for msg_id in next_msg_ids] - async def get_fresh_messages_in_arrival_order(self) -> List[Message]: + def get_fresh_messages_in_arrival_order(self) -> List[Message]: """Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" warn( "get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.", DeprecationWarning, stacklevel=2, ) - fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id)) + fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id)) return [Message(self, msg_id) for msg_id in fresh_msg_ids] - async def export_backup(self, path, passphrase: str = "") -> None: + def export_backup(self, path, passphrase: str = "") -> None: """Export backup.""" - await self._rpc.export_backup(self.id, str(path), passphrase) + self._rpc.export_backup(self.id, str(path), passphrase) - async def import_backup(self, path, passphrase: str = "") -> None: + def import_backup(self, path, passphrase: str = "") -> None: """Import backup.""" - await self._rpc.import_backup(self.id, str(path), passphrase) + self._rpc.import_backup(self.id, str(path), passphrase) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 5eb1161188..20fc11b366 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -25,7 +25,7 @@ class Chat: def _rpc(self) -> "Rpc": return self.account._rpc - async def delete(self) -> None: + def delete(self) -> None: """Delete this chat and all its messages. Note: @@ -33,21 +33,21 @@ async def delete(self) -> None: - does not delete messages on server - the chat or contact is not blocked, new message will arrive """ - await self._rpc.delete_chat(self.account.id, self.id) + self._rpc.delete_chat(self.account.id, self.id) - async def block(self) -> None: + def block(self) -> None: """Block this chat.""" - await self._rpc.block_chat(self.account.id, self.id) + self._rpc.block_chat(self.account.id, self.id) - async def accept(self) -> None: + def accept(self) -> None: """Accept this contact request chat.""" - await self._rpc.accept_chat(self.account.id, self.id) + self._rpc.accept_chat(self.account.id, self.id) - async def leave(self) -> None: + def leave(self) -> None: """Leave this chat.""" - await self._rpc.leave_group(self.account.id, self.id) + self._rpc.leave_group(self.account.id, self.id) - async def mute(self, duration: Optional[int] = None) -> None: + def mute(self, duration: Optional[int] = None) -> None: """Mute this chat, if a duration is not provided the chat is muted forever. :param duration: mute duration from now in seconds. Must be greater than zero. @@ -57,59 +57,59 @@ async def mute(self, duration: Optional[int] = None) -> None: dur: dict = {"kind": "Until", "duration": duration} else: dur = {"kind": "Forever"} - await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur) + self._rpc.set_chat_mute_duration(self.account.id, self.id, dur) - async def unmute(self) -> None: + def unmute(self) -> None: """Unmute this chat.""" - await self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"}) + self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"}) - async def pin(self) -> None: + def pin(self) -> None: """Pin this chat.""" - await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED) + self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED) - async def unpin(self) -> None: + def unpin(self) -> None: """Unpin this chat.""" - await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) + self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) - async def archive(self) -> None: + def archive(self) -> None: """Archive this chat.""" - await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED) + self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED) - async def unarchive(self) -> None: + def unarchive(self) -> None: """Unarchive this chat.""" - await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) + self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) - async def set_name(self, name: str) -> None: + def set_name(self, name: str) -> None: """Set name of this chat.""" - await self._rpc.set_chat_name(self.account.id, self.id, name) + self._rpc.set_chat_name(self.account.id, self.id, name) - async def set_ephemeral_timer(self, timer: int) -> None: + def set_ephemeral_timer(self, timer: int) -> None: """Set ephemeral timer of this chat.""" - await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer) + self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer) - async def get_encryption_info(self) -> str: + def get_encryption_info(self) -> str: """Return encryption info for this chat.""" - return await self._rpc.get_chat_encryption_info(self.account.id, self.id) + return self._rpc.get_chat_encryption_info(self.account.id, self.id) - async def get_qr_code(self) -> Tuple[str, str]: + def get_qr_code(self) -> Tuple[str, str]: """Get Join-Group QR code text and SVG data.""" - return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id) + return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id) - async def get_basic_snapshot(self) -> AttrDict: + def get_basic_snapshot(self) -> AttrDict: """Get a chat snapshot with basic info about this chat.""" - info = await self._rpc.get_basic_chat_info(self.account.id, self.id) + info = self._rpc.get_basic_chat_info(self.account.id, self.id) return AttrDict(chat=self, **info) - async def get_full_snapshot(self) -> AttrDict: + def get_full_snapshot(self) -> AttrDict: """Get a full snapshot of this chat.""" - info = await self._rpc.get_full_chat_by_id(self.account.id, self.id) + info = self._rpc.get_full_chat_by_id(self.account.id, self.id) return AttrDict(chat=self, **info) - async def can_send(self) -> bool: + def can_send(self) -> bool: """Return true if messages can be sent to the chat.""" - return await self._rpc.can_send(self.account.id, self.id) + return self._rpc.can_send(self.account.id, self.id) - async def send_message( + def send_message( self, text: Optional[str] = None, html: Optional[str] = None, @@ -132,30 +132,30 @@ async def send_message( "overrideSenderName": override_sender_name, "quotedMessageId": quoted_msg, } - msg_id = await self._rpc.send_msg(self.account.id, self.id, draft) + msg_id = self._rpc.send_msg(self.account.id, self.id, draft) return Message(self.account, msg_id) - async def send_text(self, text: str) -> Message: + def send_text(self, text: str) -> Message: """Send a text message and return the resulting Message instance.""" - msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text) + msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text) return Message(self.account, msg_id) - async def send_videochat_invitation(self) -> Message: + def send_videochat_invitation(self) -> Message: """Send a videochat invitation and return the resulting Message instance.""" - msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id) + msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id) return Message(self.account, msg_id) - async def send_sticker(self, path: str) -> Message: + def send_sticker(self, path: str) -> Message: """Send an sticker and return the resulting Message instance.""" - msg_id = await self._rpc.send_sticker(self.account.id, self.id, path) + msg_id = self._rpc.send_sticker(self.account.id, self.id, path) return Message(self.account, msg_id) - async def forward_messages(self, messages: List[Message]) -> None: + def forward_messages(self, messages: List[Message]) -> None: """Forward a list of messages to this chat.""" msg_ids = [msg.id for msg in messages] - await self._rpc.forward_messages(self.account.id, msg_ids, self.id) + self._rpc.forward_messages(self.account.id, msg_ids, self.id) - async def set_draft( + def set_draft( self, text: Optional[str] = None, file: Optional[str] = None, @@ -164,15 +164,15 @@ async def set_draft( """Set draft message.""" if isinstance(quoted_msg, Message): quoted_msg = quoted_msg.id - await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg) + self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg) - async def remove_draft(self) -> None: + def remove_draft(self) -> None: """Remove draft message.""" - await self._rpc.remove_draft(self.account.id, self.id) + self._rpc.remove_draft(self.account.id, self.id) - async def get_draft(self) -> Optional[AttrDict]: + def get_draft(self) -> Optional[AttrDict]: """Get draft message.""" - snapshot = await self._rpc.get_draft(self.account.id, self.id) + snapshot = self._rpc.get_draft(self.account.id, self.id) if not snapshot: return None snapshot = AttrDict(snapshot) @@ -181,61 +181,61 @@ async def get_draft(self) -> Optional[AttrDict]: snapshot["message"] = Message(self.account, snapshot.id) return snapshot - async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]: + def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]: """get the list of messages in this chat.""" - msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker) + msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker) return [Message(self.account, msg_id) for msg_id in msgs] - async def get_fresh_message_count(self) -> int: + def get_fresh_message_count(self) -> int: """Get number of fresh messages in this chat""" - return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id) + return self._rpc.get_fresh_msg_cnt(self.account.id, self.id) - async def mark_noticed(self) -> None: + def mark_noticed(self) -> None: """Mark all messages in this chat as noticed.""" - await self._rpc.marknoticed_chat(self.account.id, self.id) + self._rpc.marknoticed_chat(self.account.id, self.id) - async def add_contact(self, *contact: Union[int, str, Contact]) -> None: + def add_contact(self, *contact: Union[int, str, Contact]) -> None: """Add contacts to this group.""" for cnt in contact: if isinstance(cnt, str): - contact_id = (await self.account.create_contact(cnt)).id + contact_id = self.account.create_contact(cnt).id elif not isinstance(cnt, int): contact_id = cnt.id else: contact_id = cnt - await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id) + self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id) - async def remove_contact(self, *contact: Union[int, str, Contact]) -> None: + def remove_contact(self, *contact: Union[int, str, Contact]) -> None: """Remove members from this group.""" for cnt in contact: if isinstance(cnt, str): - contact_id = (await self.account.create_contact(cnt)).id + contact_id = self.account.create_contact(cnt).id elif not isinstance(cnt, int): contact_id = cnt.id else: contact_id = cnt - await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id) + self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id) - async def get_contacts(self) -> List[Contact]: + def get_contacts(self) -> List[Contact]: """Get the contacts belonging to this chat. For single/direct chats self-address is not included. """ - contacts = await self._rpc.get_chat_contacts(self.account.id, self.id) + contacts = self._rpc.get_chat_contacts(self.account.id, self.id) return [Contact(self.account, contact_id) for contact_id in contacts] - async def set_image(self, path: str) -> None: + def set_image(self, path: str) -> None: """Set profile image of this chat. :param path: Full path of the image to use as the group image. """ - await self._rpc.set_chat_profile_image(self.account.id, self.id, path) + self._rpc.set_chat_profile_image(self.account.id, self.id, path) - async def remove_image(self) -> None: + def remove_image(self) -> None: """Remove profile image of this chat.""" - await self._rpc.set_chat_profile_image(self.account.id, self.id, None) + self._rpc.set_chat_profile_image(self.account.id, self.id, None) - async def get_locations( + def get_locations( self, contact: Optional[Contact] = None, timestamp_from: Optional["datetime"] = None, @@ -246,7 +246,7 @@ async def get_locations( time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0 contact_id = contact.id if contact else 0 - result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to) + result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to) locations = [] contacts: Dict[int, Contact] = {} for loc in result: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index c270fb0f7b..9486fcab46 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -1,5 +1,4 @@ """Event loop implementations offering high level event handling/hooking.""" -import inspect import logging from typing import ( TYPE_CHECKING, @@ -78,22 +77,22 @@ def remove_hook(self, hook: Callable, event: Union[type, EventFilter]) -> None: ) self._hooks.get(type(event), set()).remove((hook, event)) - async def is_configured(self) -> bool: - return await self.account.is_configured() + def is_configured(self) -> bool: + return self.account.is_configured() - async def configure(self, email: str, password: str, **kwargs) -> None: - await self.account.set_config("addr", email) - await self.account.set_config("mail_pw", password) + def configure(self, email: str, password: str, **kwargs) -> None: + self.account.set_config("addr", email) + self.account.set_config("mail_pw", password) for key, value in kwargs.items(): - await self.account.set_config(key, value) - await self.account.configure() + self.account.set_config(key, value) + self.account.configure() self.logger.debug("Account configured") - async def run_forever(self) -> None: + def run_forever(self) -> None: """Process events forever.""" - await self.run_until(lambda _: False) + self.run_until(lambda _: False) - async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict: + def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict: """Process events until the given callable evaluates to True. The callable should accept an AttrDict object representing the @@ -101,39 +100,37 @@ async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> evaluates to True. """ self.logger.debug("Listening to incoming events...") - if await self.is_configured(): - await self.account.start_io() - await self._process_messages() # Process old messages. + if self.is_configured(): + self.account.start_io() + self._process_messages() # Process old messages. while True: - event = await self.account.wait_for_event() + event = self.account.wait_for_event() event["kind"] = EventType(event.kind) event["account"] = self.account - await self._on_event(event) + self._on_event(event) if event.kind == EventType.INCOMING_MSG: - await self._process_messages() + self._process_messages() stop = func(event) - if inspect.isawaitable(stop): - stop = await stop if stop: return event - async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None: + def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None: for hook, evfilter in self._hooks.get(filter_type, []): - if await evfilter.filter(event): + if evfilter.filter(event): try: - await hook(event) + hook(event) except Exception as ex: self.logger.exception(ex) - async def _parse_command(self, event: AttrDict) -> None: + def _parse_command(self, event: AttrDict) -> None: cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command] parts = event.message_snapshot.text.split(maxsplit=1) payload = parts[1] if len(parts) > 1 else "" cmd = parts.pop(0) if "@" in cmd: - suffix = "@" + (await self.account.self_contact.get_snapshot()).address + suffix = "@" + self.account.self_contact.get_snapshot().address if cmd.endswith(suffix): cmd = cmd[: -len(suffix)] else: @@ -153,32 +150,32 @@ async def _parse_command(self, event: AttrDict) -> None: event["command"], event["payload"] = cmd, payload - async def _on_new_msg(self, snapshot: AttrDict) -> None: + def _on_new_msg(self, snapshot: AttrDict) -> None: event = AttrDict(command="", payload="", message_snapshot=snapshot) if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX): - await self._parse_command(event) - await self._on_event(event, NewMessage) + self._parse_command(event) + self._on_event(event, NewMessage) - async def _handle_info_msg(self, snapshot: AttrDict) -> None: + def _handle_info_msg(self, snapshot: AttrDict) -> None: event = AttrDict(message_snapshot=snapshot) img_changed = parse_system_image_changed(snapshot.text) if img_changed: _, event["image_deleted"] = img_changed - await self._on_event(event, GroupImageChanged) + self._on_event(event, GroupImageChanged) return title_changed = parse_system_title_changed(snapshot.text) if title_changed: _, event["old_name"] = title_changed - await self._on_event(event, GroupNameChanged) + self._on_event(event, GroupNameChanged) return members_changed = parse_system_add_remove(snapshot.text) if members_changed: action, event["member"], _ = members_changed event["member_added"] = action == "added" - await self._on_event(event, MemberListChanged) + self._on_event(event, MemberListChanged) return self.logger.warning( @@ -187,20 +184,20 @@ async def _handle_info_msg(self, snapshot: AttrDict) -> None: snapshot.text, ) - async def _process_messages(self) -> None: + def _process_messages(self) -> None: if self._should_process_messages: - for message in await self.account.get_next_messages(): - snapshot = await message.get_snapshot() + for message in self.account.get_next_messages(): + snapshot = message.get_snapshot() if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]: - await self._on_new_msg(snapshot) + self._on_new_msg(snapshot) if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE: - await self._handle_info_msg(snapshot) - await snapshot.message.mark_seen() + self._handle_info_msg(snapshot) + snapshot.message.mark_seen() class Bot(Client): """Simple bot implementation that listent to events of a single account.""" - async def configure(self, email: str, password: str, **kwargs) -> None: + def configure(self, email: str, password: str, **kwargs) -> None: kwargs.setdefault("bot", "1") - await super().configure(email, password, **kwargs) + super().configure(email, password, **kwargs) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index efb3e9297c..8f3c09d7f4 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -24,39 +24,39 @@ class Contact: def _rpc(self) -> "Rpc": return self.account._rpc - async def block(self) -> None: + def block(self) -> None: """Block contact.""" - await self._rpc.block_contact(self.account.id, self.id) + self._rpc.block_contact(self.account.id, self.id) - async def unblock(self) -> None: + def unblock(self) -> None: """Unblock contact.""" - await self._rpc.unblock_contact(self.account.id, self.id) + self._rpc.unblock_contact(self.account.id, self.id) - async def delete(self) -> None: + def delete(self) -> None: """Delete contact.""" - await self._rpc.delete_contact(self.account.id, self.id) + self._rpc.delete_contact(self.account.id, self.id) - async def set_name(self, name: str) -> None: + def set_name(self, name: str) -> None: """Change the name of this contact.""" - await self._rpc.change_contact_name(self.account.id, self.id, name) + self._rpc.change_contact_name(self.account.id, self.id, name) - async def get_encryption_info(self) -> str: + def get_encryption_info(self) -> str: """Get a multi-line encryption info, containing your fingerprint and the fingerprint of the contact. """ - return await self._rpc.get_contact_encryption_info(self.account.id, self.id) + return self._rpc.get_contact_encryption_info(self.account.id, self.id) - async def get_snapshot(self) -> AttrDict: + def get_snapshot(self) -> AttrDict: """Return a dictionary with a snapshot of all contact properties.""" - snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id)) + snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id)) snapshot["contact"] = self return snapshot - async def create_chat(self) -> "Chat": + def create_chat(self) -> "Chat": """Create or get an existing 1:1 chat for this contact.""" from .chat import Chat return Chat( self.account, - await self._rpc.create_chat_by_contact_id(self.account.id, self.id), + self._rpc.create_chat_by_contact_id(self.account.id, self.id), ) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py index c2cecd60d1..ec3ed2d761 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -16,34 +16,34 @@ class DeltaChat: def __init__(self, rpc: "Rpc") -> None: self.rpc = rpc - async def add_account(self) -> Account: + def add_account(self) -> Account: """Create a new account database.""" - account_id = await self.rpc.add_account() + account_id = self.rpc.add_account() return Account(self, account_id) - async def get_all_accounts(self) -> List[Account]: + def get_all_accounts(self) -> List[Account]: """Return a list of all available accounts.""" - account_ids = await self.rpc.get_all_account_ids() + account_ids = self.rpc.get_all_account_ids() return [Account(self, account_id) for account_id in account_ids] - async def start_io(self) -> None: + def start_io(self) -> None: """Start the I/O of all accounts.""" - await self.rpc.start_io_for_all_accounts() + self.rpc.start_io_for_all_accounts() - async def stop_io(self) -> None: + def stop_io(self) -> None: """Stop the I/O of all accounts.""" - await self.rpc.stop_io_for_all_accounts() + self.rpc.stop_io_for_all_accounts() - async def maybe_network(self) -> None: + def maybe_network(self) -> None: """Indicate that the network likely has come back or just that the network conditions might have changed. """ - await self.rpc.maybe_network() + self.rpc.maybe_network() - async def get_system_info(self) -> AttrDict: + def get_system_info(self) -> AttrDict: """Get information about the Delta Chat core in this system.""" - return AttrDict(await self.rpc.get_system_info()) + return AttrDict(self.rpc.get_system_info()) - async def set_translations(self, translations: Dict[str, str]) -> None: + def set_translations(self, translations: Dict[str, str]) -> None: """Set stock translation strings.""" - await self.rpc.set_stock_strings(translations) + self.rpc.set_stock_strings(translations) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py index 2c1f71369d..b90b6e0452 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py @@ -1,5 +1,4 @@ """High-level classes for event processing and filtering.""" -import inspect import re from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union @@ -24,7 +23,7 @@ def _tuple_of(obj, type_: type) -> tuple: class EventFilter(ABC): """The base event filter. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -43,16 +42,13 @@ def __eq__(self, other) -> bool: def __ne__(self, other): return not self == other - async def _call_func(self, event) -> bool: + def _call_func(self, event) -> bool: if not self.func: return True - res = self.func(event) - if inspect.isawaitable(res): - return await res - return res + return self.func(event) @abstractmethod - async def filter(self, event): + def filter(self, event): """Return True-like value if the event passed the filter and should be used, or False-like value otherwise. """ @@ -62,7 +58,7 @@ class RawEvent(EventFilter): """Matches raw core events. :param types: The types of event to match. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -82,10 +78,10 @@ def __eq__(self, other) -> bool: return (self.types, self.func) == (other.types, other.func) return False - async def filter(self, event: "AttrDict") -> bool: + def filter(self, event: "AttrDict") -> bool: if self.types and event.kind not in self.types: return False - return await self._call_func(event) + return self._call_func(event) class NewMessage(EventFilter): @@ -104,7 +100,7 @@ class NewMessage(EventFilter): :param is_info: If set to True only match info/system messages, if set to False only match messages that are not info/system messages. If omitted info/system messages as well as normal messages will be matched. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -159,7 +155,7 @@ def __eq__(self, other) -> bool: ) return False - async def filter(self, event: "AttrDict") -> bool: + def filter(self, event: "AttrDict") -> bool: if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot: return False if self.is_info is not None and self.is_info != event.message_snapshot.is_info: @@ -168,11 +164,9 @@ async def filter(self, event: "AttrDict") -> bool: return False if self.pattern: match = self.pattern(event.message_snapshot.text) - if inspect.isawaitable(match): - match = await match if not match: return False - return await super()._call_func(event) + return super()._call_func(event) class MemberListChanged(EventFilter): @@ -184,7 +178,7 @@ class MemberListChanged(EventFilter): :param added: If set to True only match if a member was added, if set to False only match if a member was removed. If omitted both, member additions and removals, will be matched. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -201,10 +195,10 @@ def __eq__(self, other) -> bool: return (self.added, self.func) == (other.added, other.func) return False - async def filter(self, event: "AttrDict") -> bool: + def filter(self, event: "AttrDict") -> bool: if self.added is not None and self.added != event.member_added: return False - return await self._call_func(event) + return self._call_func(event) class GroupImageChanged(EventFilter): @@ -216,7 +210,7 @@ class GroupImageChanged(EventFilter): :param deleted: If set to True only match if the image was deleted, if set to False only match if a new image was set. If omitted both, image changes and removals, will be matched. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -233,10 +227,10 @@ def __eq__(self, other) -> bool: return (self.deleted, self.func) == (other.deleted, other.func) return False - async def filter(self, event: "AttrDict") -> bool: + def filter(self, event: "AttrDict") -> bool: if self.deleted is not None and self.deleted != event.image_deleted: return False - return await self._call_func(event) + return self._call_func(event) class GroupNameChanged(EventFilter): @@ -245,7 +239,7 @@ class GroupNameChanged(EventFilter): Warning: registering a handler for this event will cause the messages to be marked as read. Its usage is mainly intended for bots. - :param func: A Callable (async or not) function that should accept the event as input + :param func: A Callable function that should accept the event as input parameter, and return a bool value indicating whether the event should be dispatched or not. """ @@ -258,8 +252,8 @@ def __eq__(self, other) -> bool: return self.func == other.func return False - async def filter(self, event: "AttrDict") -> bool: - return await self._call_func(event) + def filter(self, event: "AttrDict") -> bool: + return self._call_func(event) class HookCollection: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index 38a0b11f7d..e728e690fd 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -21,39 +21,39 @@ class Message: def _rpc(self) -> "Rpc": return self.account._rpc - async def send_reaction(self, *reaction: str): + def send_reaction(self, *reaction: str): """Send a reaction to this message.""" - await self._rpc.send_reaction(self.account.id, self.id, reaction) + self._rpc.send_reaction(self.account.id, self.id, reaction) - async def get_snapshot(self) -> AttrDict: + def get_snapshot(self) -> AttrDict: """Get a snapshot with the properties of this message.""" from .chat import Chat - snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id)) + snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id)) snapshot["chat"] = Chat(self.account, snapshot.chat_id) snapshot["sender"] = Contact(self.account, snapshot.from_id) snapshot["message"] = self return snapshot - async def get_reactions(self) -> Optional[AttrDict]: + def get_reactions(self) -> Optional[AttrDict]: """Get message reactions.""" - reactions = await self._rpc.get_message_reactions(self.account.id, self.id) + reactions = self._rpc.get_message_reactions(self.account.id, self.id) if reactions: return AttrDict(reactions) return None - async def mark_seen(self) -> None: + def mark_seen(self) -> None: """Mark the message as seen.""" - await self._rpc.markseen_msgs(self.account.id, [self.id]) + self._rpc.markseen_msgs(self.account.id, [self.id]) - async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None: + def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None: """Send a webxdc status update. This message must be a webxdc.""" if not isinstance(update, str): update = json.dumps(update) - await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description) + self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description) - async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: - return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial)) + def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: + return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial)) - async def get_webxdc_info(self) -> dict: - return await self._rpc.get_webxdc_info(self.account.id, self.id) + def get_webxdc_info(self) -> dict: + return self._rpc.get_webxdc_info(self.account.id, self.id) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index 8087327a65..4d569d3ea9 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -1,70 +1,67 @@ -import asyncio import json import os +import urllib.request from typing import AsyncGenerator, List, Optional -import aiohttp -import pytest_asyncio +import pytest from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message from .rpc import Rpc -async def get_temp_credentials() -> dict: +def get_temp_credentials() -> dict: url = os.getenv("DCC_NEW_TMP_EMAIL") assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set" - # Replace default 5 minute timeout with a 1 minute timeout. - timeout = aiohttp.ClientTimeout(total=60) - async with aiohttp.ClientSession() as session, session.post(url, timeout=timeout) as response: - return json.loads(await response.text()) + request = urllib.request.Request(url, method="POST") + with urllib.request.urlopen(request, timeout=60) as f: + return json.load(f) class ACFactory: def __init__(self, deltachat: DeltaChat) -> None: self.deltachat = deltachat - async def get_unconfigured_account(self) -> Account: - return await self.deltachat.add_account() + def get_unconfigured_account(self) -> Account: + return self.deltachat.add_account() - async def get_unconfigured_bot(self) -> Bot: - return Bot(await self.get_unconfigured_account()) + def get_unconfigured_bot(self) -> Bot: + return Bot(self.get_unconfigured_account()) - async def new_preconfigured_account(self) -> Account: + def new_preconfigured_account(self) -> Account: """Make a new account with configuration options set, but configuration not started.""" - credentials = await get_temp_credentials() - account = await self.get_unconfigured_account() - await account.set_config("addr", credentials["email"]) - await account.set_config("mail_pw", credentials["password"]) - assert not await account.is_configured() + credentials = get_temp_credentials() + account = self.get_unconfigured_account() + account.set_config("addr", credentials["email"]) + account.set_config("mail_pw", credentials["password"]) + assert not account.is_configured() return account - async def new_configured_account(self) -> Account: - account = await self.new_preconfigured_account() - await account.configure() - assert await account.is_configured() + def new_configured_account(self) -> Account: + account = self.new_preconfigured_account() + account.configure() + assert account.is_configured() return account - async def new_configured_bot(self) -> Bot: - credentials = await get_temp_credentials() - bot = await self.get_unconfigured_bot() - await bot.configure(credentials["email"], credentials["password"]) + def new_configured_bot(self) -> Bot: + credentials = get_temp_credentials() + bot = self.get_unconfigured_bot() + bot.configure(credentials["email"], credentials["password"]) return bot - async def get_online_account(self) -> Account: - account = await self.new_configured_account() - await account.start_io() + def get_online_account(self) -> Account: + account = self.new_configured_account() + account.start_io() while True: - event = await account.wait_for_event() - print(event) + event = account.wait_for_event() if event.kind == EventType.IMAP_INBOX_IDLE: break return account - async def get_online_accounts(self, num: int) -> List[Account]: - return await asyncio.gather(*[self.get_online_account() for _ in range(num)]) + def get_online_accounts(self, num: int) -> List[Account]: + return [self.get_online_account() for _ in range(num)] - async def send_message( + def send_message( self, to_account: Account, from_account: Optional[Account] = None, @@ -73,16 +70,16 @@ async def send_message( group: Optional[str] = None, ) -> Message: if not from_account: - from_account = (await self.get_online_accounts(1))[0] - to_contact = await from_account.create_contact(await to_account.get_config("addr")) + from_account = (self.get_online_accounts(1))[0] + to_contact = from_account.create_contact(to_account.get_config("addr")) if group: - to_chat = await from_account.create_group(group) - await to_chat.add_contact(to_contact) + to_chat = from_account.create_group(group) + to_chat.add_contact(to_contact) else: - to_chat = await to_contact.create_chat() - return await to_chat.send_message(text=text, file=file) + to_chat = to_contact.create_chat() + return to_chat.send_message(text=text, file=file) - async def process_message( + def process_message( self, to_client: Client, from_account: Optional[Account] = None, @@ -90,7 +87,7 @@ async def process_message( file: Optional[str] = None, group: Optional[str] = None, ) -> AttrDict: - await self.send_message( + self.send_message( to_account=to_client.account, from_account=from_account, text=text, @@ -98,16 +95,16 @@ async def process_message( group=group, ) - return await to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG) + return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG) -@pytest_asyncio.fixture -async def rpc(tmp_path) -> AsyncGenerator: +@pytest.fixture() +def rpc(tmp_path) -> AsyncGenerator: rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts")) - async with rpc_server: + with rpc_server: yield rpc_server -@pytest_asyncio.fixture -async def acfactory(rpc) -> AsyncGenerator: - yield ACFactory(DeltaChat(rpc)) +@pytest.fixture() +def acfactory(rpc) -> AsyncGenerator: + return ACFactory(DeltaChat(rpc)) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index a3df4723eb..4a181c528c 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -1,7 +1,10 @@ -import asyncio import json import logging import os +import subprocess +import sys +from queue import Queue +from threading import Event, Thread from typing import Any, Dict, Optional @@ -11,7 +14,7 @@ class JsonRpcError(Exception): class Rpc: def __init__(self, accounts_dir: Optional[str] = None, **kwargs): - """The given arguments will be passed to asyncio.create_subprocess_exec()""" + """The given arguments will be passed to subprocess.Popen()""" if accounts_dir: kwargs["env"] = { **kwargs.get("env", os.environ), @@ -19,92 +22,127 @@ def __init__(self, accounts_dir: Optional[str] = None, **kwargs): } self._kwargs = kwargs - self.process: asyncio.subprocess.Process + self.process: subprocess.Popen self.id: int - self.event_queues: Dict[int, asyncio.Queue] - # Map from request ID to `asyncio.Future` returning the response. - self.request_events: Dict[int, asyncio.Future] + self.event_queues: Dict[int, Queue] + # Map from request ID to `threading.Event`. + self.request_events: Dict[int, Event] + # Map from request ID to the result. + self.request_results: Dict[int, Any] + self.request_queue: Queue[Any] self.closing: bool - self.reader_task: asyncio.Task - self.events_task: asyncio.Task - - async def start(self) -> None: - # Use buffer of 64 MiB. - # Default limit as of Python 3.11 is 2**16 bytes, this is too low for some JSON-RPC responses, - # such as loading large HTML message content. - limit = 2**26 - - self.process = await asyncio.create_subprocess_exec( - "deltachat-rpc-server", - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - limit=limit, - **self._kwargs, - ) + self.reader_thread: Thread + self.writer_thread: Thread + self.events_thread: Thread + + def start(self) -> None: + if sys.version_info >= (3, 11): + self.process = subprocess.Popen( + "deltachat-rpc-server", + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + # Prevent subprocess from capturing SIGINT. + process_group=0, + **self._kwargs, + ) + else: + self.process = subprocess.Popen( + "deltachat-rpc-server", + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + # `process_group` is not supported before Python 3.11. + preexec_fn=os.setpgrp, # noqa: PLW1509 + **self._kwargs, + ) self.id = 0 self.event_queues = {} self.request_events = {} + self.request_results = {} + self.request_queue = Queue() self.closing = False - self.reader_task = asyncio.create_task(self.reader_loop()) - self.events_task = asyncio.create_task(self.events_loop()) - - async def close(self) -> None: + self.reader_thread = Thread(target=self.reader_loop) + self.reader_thread.start() + self.writer_thread = Thread(target=self.writer_loop) + self.writer_thread.start() + self.events_thread = Thread(target=self.events_loop) + self.events_thread.start() + + def close(self) -> None: """Terminate RPC server process and wait until the reader loop finishes.""" self.closing = True - await self.stop_io_for_all_accounts() - await self.events_task - self.process.terminate() - await self.reader_task - - async def __aenter__(self): - await self.start() + self.stop_io_for_all_accounts() + self.events_thread.join() + self.process.stdin.close() + self.reader_thread.join() + self.request_queue.put(None) + self.writer_thread.join() + + def __enter__(self): + self.start() return self - async def __aexit__(self, _exc_type, _exc, _tb): - await self.close() + def __exit__(self, _exc_type, _exc, _tb): + self.close() - async def reader_loop(self) -> None: + def reader_loop(self) -> None: try: while True: - line = await self.process.stdout.readline() # noqa + line = self.process.stdout.readline() if not line: # EOF break response = json.loads(line) if "id" in response: - fut = self.request_events.pop(response["id"]) - fut.set_result(response) + response_id = response["id"] + event = self.request_events.pop(response_id) + self.request_results[response_id] = response + event.set() else: - print(response) + logging.warning("Got a response without ID: %s", response) except Exception: # Log an exception if the reader loop dies. logging.exception("Exception in the reader loop") - async def get_queue(self, account_id: int) -> asyncio.Queue: + def writer_loop(self) -> None: + """Writer loop ensuring only a single thread writes requests.""" + try: + while True: + request = self.request_queue.get() + if not request: + break + data = (json.dumps(request) + "\n").encode() + self.process.stdin.write(data) + self.process.stdin.flush() + + except Exception: + # Log an exception if the writer loop dies. + logging.exception("Exception in the writer loop") + + def get_queue(self, account_id: int) -> Queue: if account_id not in self.event_queues: - self.event_queues[account_id] = asyncio.Queue() + self.event_queues[account_id] = Queue() return self.event_queues[account_id] - async def events_loop(self) -> None: + def events_loop(self) -> None: """Requests new events and distributes them between queues.""" try: while True: if self.closing: return - event = await self.get_next_event() + event = self.get_next_event() account_id = event["contextId"] - queue = await self.get_queue(account_id) - await queue.put(event["event"]) + queue = self.get_queue(account_id) + queue.put(event["event"]) except Exception: # Log an exception if the event loop dies. logging.exception("Exception in the event loop") - async def wait_for_event(self, account_id: int) -> Optional[dict]: + def wait_for_event(self, account_id: int) -> Optional[dict]: """Waits for the next event from the given account and returns it.""" - queue = await self.get_queue(account_id) - return await queue.get() + queue = self.get_queue(account_id) + return queue.get() def __getattr__(self, attr: str): - async def method(*args) -> Any: + def method(*args) -> Any: self.id += 1 request_id = self.id @@ -114,12 +152,12 @@ async def method(*args) -> Any: "params": args, "id": self.id, } - data = (json.dumps(request) + "\n").encode() - self.process.stdin.write(data) # noqa - loop = asyncio.get_running_loop() - fut = loop.create_future() - self.request_events[request_id] = fut - response = await fut + event = Event() + self.request_events[request_id] = event + self.request_queue.put(request) + event.wait() + + response = self.request_results.pop(request_id) if "error" in response: raise JsonRpcError(response["error"]) if "result" in response: diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index a3bd27d79f..f91f9844e2 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1,4 +1,4 @@ -import asyncio +import concurrent.futures import json import subprocess from unittest.mock import MagicMock @@ -8,26 +8,26 @@ from deltachat_rpc_client.rpc import JsonRpcError -@pytest.mark.asyncio() -async def test_system_info(rpc) -> None: - system_info = await rpc.get_system_info() +def test_system_info(rpc) -> None: + system_info = rpc.get_system_info() assert "arch" in system_info assert "deltachat_core_version" in system_info -@pytest.mark.asyncio() -async def test_sleep(rpc) -> None: +def test_sleep(rpc) -> None: """Test that long-running task does not block short-running task from completion.""" - sleep_5_task = asyncio.create_task(rpc.sleep(5.0)) - sleep_3_task = asyncio.create_task(rpc.sleep(3.0)) - done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED) - assert sleep_3_task in done - assert sleep_5_task in pending - sleep_5_task.cancel() - - -@pytest.mark.asyncio() -async def test_email_address_validity(rpc) -> None: + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + sleep_5_future = executor.submit(rpc.sleep, 5.0) + sleep_3_future = executor.submit(rpc.sleep, 3.0) + done, pending = concurrent.futures.wait( + [sleep_5_future, sleep_3_future], + return_when=concurrent.futures.FIRST_COMPLETED, + ) + assert sleep_3_future in done + assert sleep_5_future in pending + + +def test_email_address_validity(rpc) -> None: valid_addresses = [ "email@example.com", "36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail", @@ -35,16 +35,15 @@ async def test_email_address_validity(rpc) -> None: invalid_addresses = ["email@", "example.com", "emai221"] for addr in valid_addresses: - assert await rpc.check_email_validity(addr) + assert rpc.check_email_validity(addr) for addr in invalid_addresses: - assert not await rpc.check_email_validity(addr) + assert not rpc.check_email_validity(addr) -@pytest.mark.asyncio() -async def test_acfactory(acfactory) -> None: - account = await acfactory.new_configured_account() +def test_acfactory(acfactory) -> None: + account = acfactory.new_configured_account() while True: - event = await account.wait_for_event() + event = account.wait_for_event() if event.kind == EventType.CONFIGURE_PROGRESS: assert event.progress != 0 # Progress 0 indicates error. if event.progress == 1000: # Success @@ -54,248 +53,241 @@ async def test_acfactory(acfactory) -> None: print("Successful configuration") -@pytest.mark.asyncio() -async def test_configure_starttls(acfactory) -> None: - account = await acfactory.new_preconfigured_account() +def test_configure_starttls(acfactory) -> None: + account = acfactory.new_preconfigured_account() # Use STARTTLS - await account.set_config("mail_security", "2") - await account.set_config("send_security", "2") - await account.configure() - assert await account.is_configured() + account.set_config("mail_security", "2") + account.set_config("send_security", "2") + account.configure() + assert account.is_configured() -@pytest.mark.asyncio() -async def test_account(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_account(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() - await alice_chat_bob.send_text("Hello!") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_text("Hello!") while True: - event = await bob.wait_for_event() + event = bob.wait_for_event() if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break message = bob.get_message_by_id(msg_id) - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" - await bob.mark_seen_messages([message]) + bob.mark_seen_messages([message]) assert alice != bob assert repr(alice) - assert (await alice.get_info()).level - assert await alice.get_size() - assert await alice.is_configured() - assert not await alice.get_avatar() - assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob - assert await alice.get_contacts() - assert await alice.get_contacts(snapshot=True) + assert alice.get_info().level + assert alice.get_size() + assert alice.is_configured() + assert not alice.get_avatar() + assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob + assert alice.get_contacts() + assert alice.get_contacts(snapshot=True) assert alice.self_contact - assert await alice.get_chatlist() - assert await alice.get_chatlist(snapshot=True) - assert await alice.get_qr_code() - assert await alice.get_fresh_messages() - assert await alice.get_next_messages() + assert alice.get_chatlist() + assert alice.get_chatlist(snapshot=True) + assert alice.get_qr_code() + assert alice.get_fresh_messages() + assert alice.get_next_messages() # Test sending empty message. - assert len(await bob.wait_next_messages()) == 0 - await alice_chat_bob.send_text("") - messages = await bob.wait_next_messages() + assert len(bob.wait_next_messages()) == 0 + alice_chat_bob.send_text("") + messages = bob.wait_next_messages() assert len(messages) == 1 message = messages[0] - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.text == "" - await bob.mark_seen_messages([message]) + bob.mark_seen_messages([message]) - group = await alice.create_group("test group") - await group.add_contact(alice_contact_bob) - group_msg = await group.send_message(text="hello") + group = alice.create_group("test group") + group.add_contact(alice_contact_bob) + group_msg = group.send_message(text="hello") assert group_msg == alice.get_message_by_id(group_msg.id) assert group == alice.get_chat_by_id(group.id) - await alice.delete_messages([group_msg]) + alice.delete_messages([group_msg]) - await alice.set_config("selfstatus", "test") - assert await alice.get_config("selfstatus") == "test" - await alice.update_config(selfstatus="test2") - assert await alice.get_config("selfstatus") == "test2" + alice.set_config("selfstatus", "test") + assert alice.get_config("selfstatus") == "test" + alice.update_config(selfstatus="test2") + assert alice.get_config("selfstatus") == "test2" - assert not await alice.get_blocked_contacts() - await alice_contact_bob.block() - blocked_contacts = await alice.get_blocked_contacts() + assert not alice.get_blocked_contacts() + alice_contact_bob.block() + blocked_contacts = alice.get_blocked_contacts() assert blocked_contacts assert blocked_contacts[0].contact == alice_contact_bob - await bob.remove() - await alice.stop_io() + bob.remove() + alice.stop_io() -@pytest.mark.asyncio() -async def test_chat(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_chat(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() - await alice_chat_bob.send_text("Hello!") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_text("Hello!") while True: - event = await bob.wait_for_event() + event = bob.wait_for_event() if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break message = bob.get_message_by_id(msg_id) - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" bob_chat_alice = bob.get_chat_by_id(chat_id) assert alice_chat_bob != bob_chat_alice assert repr(alice_chat_bob) - await alice_chat_bob.delete() - assert not await bob_chat_alice.can_send() - await bob_chat_alice.accept() - assert await bob_chat_alice.can_send() - await bob_chat_alice.block() - bob_chat_alice = await snapshot.sender.create_chat() - await bob_chat_alice.mute() - await bob_chat_alice.unmute() - await bob_chat_alice.pin() - await bob_chat_alice.unpin() - await bob_chat_alice.archive() - await bob_chat_alice.unarchive() + alice_chat_bob.delete() + assert not bob_chat_alice.can_send() + bob_chat_alice.accept() + assert bob_chat_alice.can_send() + bob_chat_alice.block() + bob_chat_alice = snapshot.sender.create_chat() + bob_chat_alice.mute() + bob_chat_alice.unmute() + bob_chat_alice.pin() + bob_chat_alice.unpin() + bob_chat_alice.archive() + bob_chat_alice.unarchive() with pytest.raises(JsonRpcError): # can't set name for 1:1 chats - await bob_chat_alice.set_name("test") - await bob_chat_alice.set_ephemeral_timer(300) - await bob_chat_alice.get_encryption_info() + bob_chat_alice.set_name("test") + bob_chat_alice.set_ephemeral_timer(300) + bob_chat_alice.get_encryption_info() - group = await alice.create_group("test group") - await group.add_contact(alice_contact_bob) - await group.get_qr_code() + group = alice.create_group("test group") + group.add_contact(alice_contact_bob) + group.get_qr_code() - snapshot = await group.get_basic_snapshot() + snapshot = group.get_basic_snapshot() assert snapshot.name == "test group" - await group.set_name("new name") - snapshot = await group.get_full_snapshot() + group.set_name("new name") + snapshot = group.get_full_snapshot() assert snapshot.name == "new name" - msg = await group.send_message(text="hi") - assert (await msg.get_snapshot()).text == "hi" - await group.forward_messages([msg]) + msg = group.send_message(text="hi") + assert (msg.get_snapshot()).text == "hi" + group.forward_messages([msg]) - await group.set_draft(text="test draft") - draft = await group.get_draft() + group.set_draft(text="test draft") + draft = group.get_draft() assert draft.text == "test draft" - await group.remove_draft() - assert not await group.get_draft() + group.remove_draft() + assert not group.get_draft() - assert await group.get_messages() - await group.get_fresh_message_count() - await group.mark_noticed() - assert await group.get_contacts() - await group.remove_contact(alice_chat_bob) - await group.get_locations() + assert group.get_messages() + group.get_fresh_message_count() + group.mark_noticed() + assert group.get_contacts() + group.remove_contact(alice_chat_bob) + group.get_locations() -@pytest.mark.asyncio() -async def test_contact(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_contact(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id) assert repr(alice_contact_bob) - await alice_contact_bob.block() - await alice_contact_bob.unblock() - await alice_contact_bob.set_name("new name") - await alice_contact_bob.get_encryption_info() - snapshot = await alice_contact_bob.get_snapshot() + alice_contact_bob.block() + alice_contact_bob.unblock() + alice_contact_bob.set_name("new name") + alice_contact_bob.get_encryption_info() + snapshot = alice_contact_bob.get_snapshot() assert snapshot.address == bob_addr - await alice_contact_bob.create_chat() + alice_contact_bob.create_chat() -@pytest.mark.asyncio() -async def test_message(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_message(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() - await alice_chat_bob.send_text("Hello!") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_text("Hello!") while True: - event = await bob.wait_for_event() + event = bob.wait_for_event() if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break message = bob.get_message_by_id(msg_id) - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" assert not snapshot.is_bot assert repr(message) with pytest.raises(JsonRpcError): # chat is not accepted - await snapshot.chat.send_text("hi") - await snapshot.chat.accept() - await snapshot.chat.send_text("hi") + snapshot.chat.send_text("hi") + snapshot.chat.accept() + snapshot.chat.send_text("hi") - await message.mark_seen() - await message.send_reaction("😎") - reactions = await message.get_reactions() + message.mark_seen() + message.send_reaction("😎") + reactions = message.get_reactions() assert reactions - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert reactions == snapshot.reactions -@pytest.mark.asyncio() -async def test_is_bot(acfactory) -> None: +def test_is_bot(acfactory) -> None: """Test that we can recognize messages submitted by bots.""" - alice, bob = await acfactory.get_online_accounts(2) + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() # Alice becomes a bot. - await alice.set_config("bot", "1") - await alice_chat_bob.send_text("Hello!") + alice.set_config("bot", "1") + alice_chat_bob.send_text("Hello!") while True: - event = await bob.wait_for_event() + event = bob.wait_for_event() if event.kind == EventType.INCOMING_MSG: msg_id = event.msg_id message = bob.get_message_by_id(msg_id) - snapshot = await message.get_snapshot() + snapshot = message.get_snapshot() assert snapshot.chat_id == event.chat_id assert snapshot.text == "Hello!" assert snapshot.is_bot break -@pytest.mark.asyncio() -async def test_bot(acfactory) -> None: +def test_bot(acfactory) -> None: mock = MagicMock() - user = (await acfactory.get_online_accounts(1))[0] - bot = await acfactory.new_configured_bot() - bot2 = await acfactory.new_configured_bot() + user = (acfactory.get_online_accounts(1))[0] + bot = acfactory.new_configured_bot() + bot2 = acfactory.new_configured_bot() - assert await bot.is_configured() - assert await bot.account.get_config("bot") == "1" + assert bot.is_configured() + assert bot.account.get_config("bot") == "1" hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG) bot.add_hook(*hook) - event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!") - snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot() + event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!") + snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot() assert not snapshot.is_bot mock.hook.assert_called_once_with(event.msg_id) bot.remove_hook(*hook) @@ -307,58 +299,57 @@ def track(e): hook = track, events.NewMessage(r"hello") bot.add_hook(*hook) bot.add_hook(track, events.NewMessage(command="/help")) - event = await acfactory.process_message(from_account=user, to_client=bot, text="hello") + event = acfactory.process_message(from_account=user, to_client=bot, text="hello") mock.hook.assert_called_with(event.msg_id) - event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!") + event = acfactory.process_message(from_account=user, to_client=bot, text="hello!") mock.hook.assert_called_with(event.msg_id) - await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello") + acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello") assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots - await acfactory.process_message(from_account=user, to_client=bot, text="hey!") + acfactory.process_message(from_account=user, to_client=bot, text="hey!") assert len(mock.hook.mock_calls) == 2 bot.remove_hook(*hook) mock.hook.reset_mock() - await acfactory.process_message(from_account=user, to_client=bot, text="hello") - event = await acfactory.process_message(from_account=user, to_client=bot, text="/help") + acfactory.process_message(from_account=user, to_client=bot, text="hello") + event = acfactory.process_message(from_account=user, to_client=bot, text="/help") mock.hook.assert_called_once_with(event.msg_id) -@pytest.mark.asyncio() -async def test_wait_next_messages(acfactory) -> None: - alice = await acfactory.new_configured_account() +def test_wait_next_messages(acfactory) -> None: + alice = acfactory.new_configured_account() # Create a bot account so it does not receive device messages in the beginning. - bot = await acfactory.new_preconfigured_account() - await bot.set_config("bot", "1") - await bot.configure() + bot = acfactory.new_preconfigured_account() + bot.set_config("bot", "1") + bot.configure() # There are no old messages and the call returns immediately. - assert not await bot.wait_next_messages() + assert not bot.wait_next_messages() - # Bot starts waiting for messages. - next_messages_task = asyncio.create_task(bot.wait_next_messages()) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + # Bot starts waiting for messages. + next_messages_task = executor.submit(bot.wait_next_messages) - bot_addr = await bot.get_config("addr") - alice_contact_bot = await alice.create_contact(bot_addr, "Bob") - alice_chat_bot = await alice_contact_bot.create_chat() - await alice_chat_bot.send_text("Hello!") + bot_addr = bot.get_config("addr") + alice_contact_bot = alice.create_contact(bot_addr, "Bob") + alice_chat_bot = alice_contact_bot.create_chat() + alice_chat_bot.send_text("Hello!") - next_messages = await next_messages_task - assert len(next_messages) == 1 - snapshot = await next_messages[0].get_snapshot() - assert snapshot.text == "Hello!" + next_messages = next_messages_task.result() + assert len(next_messages) == 1 + snapshot = next_messages[0].get_snapshot() + assert snapshot.text == "Hello!" -@pytest.mark.asyncio() -async def test_import_export(acfactory, tmp_path) -> None: - alice = await acfactory.new_configured_account() - await alice.export_backup(tmp_path) +def test_import_export(acfactory, tmp_path) -> None: + alice = acfactory.new_configured_account() + alice.export_backup(tmp_path) files = list(tmp_path.glob("*.tar")) - alice2 = await acfactory.get_unconfigured_account() - await alice2.import_backup(files[0]) + alice2 = acfactory.get_unconfigured_account() + alice2.import_backup(files[0]) - assert await alice2.manager.get_system_info() + assert alice2.manager.get_system_info() def test_openrpc_command_line() -> None: diff --git a/deltachat-rpc-client/tests/test_webxdc.py b/deltachat-rpc-client/tests/test_webxdc.py index 4111ca39e6..8f3d8edcba 100644 --- a/deltachat-rpc-client/tests/test_webxdc.py +++ b/deltachat-rpc-client/tests/test_webxdc.py @@ -1,24 +1,22 @@ -import pytest from deltachat_rpc_client import EventType -@pytest.mark.asyncio() -async def test_webxdc(acfactory) -> None: - alice, bob = await acfactory.get_online_accounts(2) +def test_webxdc(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) - bob_addr = await bob.get_config("addr") - alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - alice_chat_bob = await alice_contact_bob.create_chat() - await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc") + bob_addr = bob.get_config("addr") + alice_contact_bob = alice.create_contact(bob_addr, "Bob") + alice_chat_bob = alice_contact_bob.create_chat() + alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc") while True: - event = await bob.wait_for_event() + event = bob.wait_for_event() if event.kind == EventType.INCOMING_MSG: bob_chat_alice = bob.get_chat_by_id(event.chat_id) message = bob.get_message_by_id(event.msg_id) break - webxdc_info = await message.get_webxdc_info() + webxdc_info = message.get_webxdc_info() assert webxdc_info == { "document": None, "icon": "icon.png", @@ -28,20 +26,20 @@ async def test_webxdc(acfactory) -> None: "summary": None, } - status_updates = await message.get_webxdc_status_updates() + status_updates = message.get_webxdc_status_updates() assert status_updates == [] - await bob_chat_alice.accept() - await message.send_webxdc_status_update({"payload": 42}, "") - await message.send_webxdc_status_update({"payload": "Second update"}, "description") + bob_chat_alice.accept() + message.send_webxdc_status_update({"payload": 42}, "") + message.send_webxdc_status_update({"payload": "Second update"}, "description") - status_updates = await message.get_webxdc_status_updates() + status_updates = message.get_webxdc_status_updates() assert status_updates == [ {"payload": 42, "serial": 1, "max_serial": 2}, {"payload": "Second update", "serial": 2, "max_serial": 2}, ] - status_updates = await message.get_webxdc_status_updates(1) + status_updates = message.get_webxdc_status_updates(1) assert status_updates == [ {"payload": "Second update", "serial": 2, "max_serial": 2}, ] diff --git a/deltachat-rpc-client/tox.ini b/deltachat-rpc-client/tox.ini index 1d0dba56cd..4bcdd7e0a3 100644 --- a/deltachat-rpc-client/tox.ini +++ b/deltachat-rpc-client/tox.ini @@ -6,7 +6,7 @@ envlist = [testenv] commands = - pytest {posargs} + pytest -n6 {posargs} setenv = # Avoid stack overflow when Rust core is built without optimizations. RUST_MIN_STACK=8388608 @@ -14,10 +14,8 @@ passenv = DCC_NEW_TMP_EMAIL deps = pytest - pytest-asyncio pytest-timeout - aiohttp - aiodns + pytest-xdist [testenv:lint] skipsdist = True