api(deltachat-rpc-client)!: replace asyncio with threads

This commit is contained in:
link2xt
2023-10-04 21:43:02 +00:00
parent 47dbac9b50
commit 7bf44a237e
20 changed files with 609 additions and 623 deletions

View File

@@ -231,14 +231,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: 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 - os: ubuntu-latest
python: 3.11 python: 3.12
- os: macos-latest - os: macos-latest
python: 3.11 python: 3.12
# PyPy tests # PyPy tests
- os: ubuntu-latest - os: ubuntu-latest

View File

@@ -37,19 +37,14 @@ $ tox --devenv env
$ . env/bin/activate $ . env/bin/activate
``` ```
It is recommended to use IPython, because it supports using `await` directly
from the REPL.
``` ```
$ pip install ipython $ python
$ PATH="../target/debug:$PATH" ipython >>> from deltachat_rpc_client import *
... >>> rpc = Rpc()
In [1]: from deltachat_rpc_client import * >>> rpc.start()
In [2]: rpc = Rpc() >>> dc = DeltaChat(rpc)
In [3]: await rpc.start() >>> system_info = dc.get_system_info()
In [4]: dc = DeltaChat(rpc) >>> system_info["level"]
In [5]: system_info = await dc.get_system_info() 'awesome'
In [6]: system_info["level"] >>> rpc.close()
Out[6]: 'awesome'
In [7]: await rpc.close()
``` ```

View File

@@ -4,23 +4,21 @@
it will echo back any text send to it, it also will print to console all Delta Chat core events. 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. Pass --help to the CLI to see available options.
""" """
import asyncio
from deltachat_rpc_client import events, run_bot_cli from deltachat_rpc_client import events, run_bot_cli
hooks = events.HookCollection() hooks = events.HookCollection()
@hooks.on(events.RawEvent) @hooks.on(events.RawEvent)
async def log_event(event): def log_event(event):
print(event) print(event)
@hooks.on(events.NewMessage) @hooks.on(events.NewMessage)
async def echo(event): def echo(event):
snapshot = event.message_snapshot snapshot = event.message_snapshot
await snapshot.chat.send_text(snapshot.text) snapshot.chat.send_text(snapshot.text)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(run_bot_cli(hooks)) run_bot_cli(hooks)

View File

@@ -3,9 +3,9 @@
it will echo back any message that has non-empty text and also supports the /help command. it will echo back any message that has non-empty text and also supports the /help command.
""" """
import asyncio
import logging import logging
import sys import sys
from threading import Thread
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
@@ -13,7 +13,7 @@ hooks = events.HookCollection()
@hooks.on(events.RawEvent) @hooks.on(events.RawEvent)
async def log_event(event): def log_event(event):
if event.type == EventType.INFO: if event.type == EventType.INFO:
logging.info(event.msg) logging.info(event.msg)
elif event.type == EventType.WARNING: elif event.type == EventType.WARNING:
@@ -21,54 +21,54 @@ async def log_event(event):
@hooks.on(events.RawEvent(EventType.ERROR)) @hooks.on(events.RawEvent(EventType.ERROR))
async def log_error(event): def log_error(event):
logging.error(event.msg) logging.error(event.msg)
@hooks.on(events.MemberListChanged) @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") logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
@hooks.on(events.GroupImageChanged) @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") logging.info("group image %s", "deleted" if event.image_deleted else "changed")
@hooks.on(events.GroupNameChanged) @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) logging.info("group name changed, old name: %s", event.old_name)
@hooks.on(events.NewMessage(func=lambda e: not e.command)) @hooks.on(events.NewMessage(func=lambda e: not e.command))
async def echo(event): def echo(event):
snapshot = event.message_snapshot snapshot = event.message_snapshot
if snapshot.text or snapshot.file: 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")) @hooks.on(events.NewMessage(command="/help"))
async def help_command(event): def help_command(event):
snapshot = event.message_snapshot 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(): def main():
async with Rpc() as rpc: with Rpc() as rpc:
deltachat = DeltaChat(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) logging.info("Running deltachat core %s", system_info.deltachat_core_version)
accounts = await deltachat.get_all_accounts() accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account() account = accounts[0] if accounts else deltachat.add_account()
bot = Bot(account, hooks) bot = Bot(account, hooks)
if not await bot.is_configured(): if not bot.is_configured():
# Save a reference to avoid garbage collection of the task. configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
_configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2])) configure_thread.start()
await bot.run_forever() bot.run_forever()
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
asyncio.run(main()) main()

View File

@@ -2,45 +2,44 @@
""" """
Example echo bot without using hooks Example echo bot without using hooks
""" """
import asyncio
import logging import logging
import sys import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
async def main(): def main():
async with Rpc() as rpc: with Rpc() as rpc:
deltachat = DeltaChat(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"]) logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
accounts = await deltachat.get_all_accounts() accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account() account = accounts[0] if accounts else deltachat.add_account()
await account.set_config("bot", "1") account.set_config("bot", "1")
if not await account.is_configured(): if not account.is_configured():
logging.info("Account is not configured, configuring") logging.info("Account is not configured, configuring")
await account.set_config("addr", sys.argv[1]) account.set_config("addr", sys.argv[1])
await account.set_config("mail_pw", sys.argv[2]) account.set_config("mail_pw", sys.argv[2])
await account.configure() account.configure()
logging.info("Configured") logging.info("Configured")
else: else:
logging.info("Account is already configured") logging.info("Account is already configured")
await deltachat.start_io() deltachat.start_io()
async def process_messages(): def process_messages():
for message in await account.get_next_messages(): for message in account.get_next_messages():
snapshot = await message.get_snapshot() snapshot = message.get_snapshot()
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info: if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text) snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen() snapshot.message.mark_seen()
# Process old messages. # Process old messages.
await process_messages() process_messages()
while True: while True:
event = await account.wait_for_event() event = account.wait_for_event()
if event["type"] == EventType.INFO: if event["type"] == EventType.INFO:
logging.info("%s", event["msg"]) logging.info("%s", event["msg"])
elif event["type"] == EventType.WARNING: elif event["type"] == EventType.WARNING:
@@ -49,9 +48,9 @@ async def main():
logging.error("%s", event["msg"]) logging.error("%s", event["msg"])
elif event["type"] == EventType.INCOMING_MSG: elif event["type"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message") logging.info("Got an incoming message")
await process_messages() process_messages()
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
asyncio.run(main()) main()

View File

@@ -5,9 +5,6 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "deltachat-rpc-client" name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface" description = "Python client for Delta Chat core JSON-RPC interface"
dependencies = [
"aiohttp"
]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO", "Framework :: AsyncIO",

View File

@@ -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 ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account from .account import Account
from .chat import Chat from .chat import Chat

View File

@@ -1,7 +1,7 @@
import argparse import argparse
import asyncio
import re import re
import sys import sys
from threading import Thread
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -43,7 +43,7 @@ class AttrDict(dict):
super().__setattr__(attr, val) super().__setattr__(attr, val)
async def run_client_cli( def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
**kwargs, **kwargs,
@@ -54,10 +54,10 @@ async def run_client_cli(
""" """
from .client import Client 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, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
**kwargs, **kwargs,
@@ -68,10 +68,10 @@ async def run_bot_cli(
""" """
from .client import Bot 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"], client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
@@ -93,20 +93,20 @@ async def _run_cli(
parser.add_argument("--password", action="store", help="password") parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:]) 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) deltachat = DeltaChat(rpc)
core_version = (await deltachat.get_system_info()).deltachat_core_version core_version = (deltachat.get_system_info()).deltachat_core_version
accounts = await deltachat.get_all_accounts() accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account() account = accounts[0] if accounts else deltachat.add_account()
client = client_type(account, hooks) client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version) 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.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password 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_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
_configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password)) configure_thread.start()
await client.run_forever() client.run_forever()
def extract_addr(text: str) -> str: def extract_addr(text: str) -> str:

View File

@@ -24,63 +24,63 @@ class Account:
def _rpc(self) -> "Rpc": def _rpc(self) -> "Rpc":
return self.manager.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.""" """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.""" """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.""" """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.""" """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 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.""" """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 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.""" """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.""" """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.""" """update config values."""
for key, value in kwargs.items(): 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. """Set self avatar.
Passing None will discard the currently set 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.""" """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.""" """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. """Create a new Contact or return an existing one.
Calling this method will always result in the same Calling this method will always result in the same
@@ -94,24 +94,24 @@ class Account:
if isinstance(obj, int): if isinstance(obj, int):
obj = Contact(self, obj) obj = Contact(self, obj)
if isinstance(obj, Contact): if isinstance(obj, Contact):
obj = (await obj.get_snapshot()).address obj = obj.get_snapshot().address
return Contact(self, await self._rpc.create_contact(self.id, obj, name)) return Contact(self, self._rpc.create_contact(self.id, obj, name))
def get_contact_by_id(self, contact_id: int) -> Contact: def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID.""" """Return Contact instance for the given contact ID."""
return Contact(self, 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.""" """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) 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.""" """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] return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
async def get_contacts( def get_contacts(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
with_self: bool = False, with_self: bool = False,
@@ -133,9 +133,9 @@ class Account:
flags |= ContactFlag.ADD_SELF flags |= ContactFlag.ADD_SELF
if snapshot: 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] 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] return [Contact(self, contact_id) for contact_id in contacts]
@property @property
@@ -143,7 +143,7 @@ class Account:
"""This account's identity as a Contact.""" """This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF) return Contact(self, SpecialContactId.SELF)
async def get_chatlist( def get_chatlist(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
contact: Optional[Contact] = None, contact: Optional[Contact] = None,
@@ -175,29 +175,29 @@ class Account:
if alldone_hint: if alldone_hint:
flags |= ChatlistFlag.ADD_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: if not snapshot:
return [Chat(self, entry) for entry in entries] 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 = [] chats = []
for item in items.values(): for item in items.values():
item["chat"] = Chat(self, item["id"]) item["chat"] = Chat(self, item["id"])
chats.append(AttrDict(item)) chats.append(AttrDict(item))
return chats 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. """Create a new group chat.
After creation, the group has only self-contact as member and is in unpromoted state. 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: def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID.""" """Return the Chat instance with the given ID."""
return Chat(self, chat_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 """Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device. another device.
@@ -208,62 +208,62 @@ class Account:
:param qrdata: The text of the scanned QR code. :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. """Get Setup-Contact QR Code text and SVG data.
this data needs to be transferred to another Delta Chat account this data needs to be transferred to another Delta Chat account
in a second channel, typically used by mobiles with QRcode-show + scan UX. 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: def get_message_by_id(self, msg_id: int) -> Message:
"""Return the Message instance with the given ID.""" """Return the Message instance with the given ID."""
return Message(self, msg_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.""" """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).""" """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. """Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications. This call is intended for displaying notifications.
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead, If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
to process oldest messages first. 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] 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.""" """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] 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.""" """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] 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.""" """Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn( warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.", "get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning, DeprecationWarning,
stacklevel=2, 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] 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.""" """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.""" """Import backup."""
await self._rpc.import_backup(self.id, str(path), passphrase) self._rpc.import_backup(self.id, str(path), passphrase)

View File

@@ -25,7 +25,7 @@ class Chat:
def _rpc(self) -> "Rpc": def _rpc(self) -> "Rpc":
return self.account._rpc return self.account._rpc
async def delete(self) -> None: def delete(self) -> None:
"""Delete this chat and all its messages. """Delete this chat and all its messages.
Note: Note:
@@ -33,21 +33,21 @@ class Chat:
- does not delete messages on server - does not delete messages on server
- the chat or contact is not blocked, new message will arrive - 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.""" """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.""" """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.""" """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. """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. :param duration: mute duration from now in seconds. Must be greater than zero.
@@ -57,59 +57,59 @@ class Chat:
dur: Union[str, dict] = {"Until": duration} dur: Union[str, dict] = {"Until": duration}
else: else:
dur = "Forever" dur = "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.""" """Unmute this chat."""
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted") self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
async def pin(self) -> None: def pin(self) -> None:
"""Pin this chat.""" """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.""" """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.""" """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.""" """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.""" """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.""" """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 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.""" """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.""" """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) 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.""" """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) 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 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, self,
text: Optional[str] = None, text: Optional[str] = None,
html: Optional[str] = None, html: Optional[str] = None,
@@ -132,30 +132,30 @@ class Chat:
"overrideSenderName": override_sender_name, "overrideSenderName": override_sender_name,
"quotedMessageId": quoted_msg, "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) 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.""" """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) 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.""" """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) 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.""" """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) 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.""" """Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages] 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, self,
text: Optional[str] = None, text: Optional[str] = None,
file: Optional[str] = None, file: Optional[str] = None,
@@ -164,15 +164,15 @@ class Chat:
"""Set draft message.""" """Set draft message."""
if isinstance(quoted_msg, Message): if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id 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.""" """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.""" """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: if not snapshot:
return None return None
snapshot = AttrDict(snapshot) snapshot = AttrDict(snapshot)
@@ -181,61 +181,61 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id) snapshot["message"] = Message(self.account, snapshot.id)
return snapshot 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.""" """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] 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""" """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.""" """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.""" """Add contacts to this group."""
for cnt in contact: for cnt in contact:
if isinstance(cnt, str): 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): elif not isinstance(cnt, int):
contact_id = cnt.id contact_id = cnt.id
else: else:
contact_id = cnt 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.""" """Remove members from this group."""
for cnt in contact: for cnt in contact:
if isinstance(cnt, str): 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): elif not isinstance(cnt, int):
contact_id = cnt.id contact_id = cnt.id
else: else:
contact_id = cnt 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. """Get the contacts belonging to this chat.
For single/direct chats self-address is not included. 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] 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. """Set profile image of this chat.
:param path: Full path of the image to use as the group image. :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.""" """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, self,
contact: Optional[Contact] = None, contact: Optional[Contact] = None,
timestamp_from: Optional["datetime"] = None, timestamp_from: Optional["datetime"] = None,
@@ -246,7 +246,7 @@ class Chat:
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0 time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
contact_id = contact.id if contact 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 = [] locations = []
contacts: Dict[int, Contact] = {} contacts: Dict[int, Contact] = {}
for loc in result: for loc in result:

View File

@@ -1,5 +1,4 @@
"""Event loop implementations offering high level event handling/hooking.""" """Event loop implementations offering high level event handling/hooking."""
import inspect
import logging import logging
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@@ -78,22 +77,22 @@ class Client:
) )
self._hooks.get(type(event), set()).remove((hook, event)) self._hooks.get(type(event), set()).remove((hook, event))
async def is_configured(self) -> bool: def is_configured(self) -> bool:
return await self.account.is_configured() return self.account.is_configured()
async def configure(self, email: str, password: str, **kwargs) -> None: def configure(self, email: str, password: str, **kwargs) -> None:
await self.account.set_config("addr", email) self.account.set_config("addr", email)
await self.account.set_config("mail_pw", password) self.account.set_config("mail_pw", password)
for key, value in kwargs.items(): for key, value in kwargs.items():
await self.account.set_config(key, value) self.account.set_config(key, value)
await self.account.configure() self.account.configure()
self.logger.debug("Account configured") self.logger.debug("Account configured")
async def run_forever(self) -> None: def run_forever(self) -> None:
"""Process events forever.""" """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. """Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the The callable should accept an AttrDict object representing the
@@ -101,39 +100,37 @@ class Client:
evaluates to True. evaluates to True.
""" """
self.logger.debug("Listening to incoming events...") self.logger.debug("Listening to incoming events...")
if await self.is_configured(): if self.is_configured():
await self.account.start_io() self.account.start_io()
await self._process_messages() # Process old messages. self._process_messages() # Process old messages.
while True: while True:
event = await self.account.wait_for_event() event = self.account.wait_for_event()
event["type"] = EventType(event.type) event["type"] = EventType(event.type)
event["account"] = self.account event["account"] = self.account
await self._on_event(event) self._on_event(event)
if event.type == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
await self._process_messages() self._process_messages()
stop = func(event) stop = func(event)
if inspect.isawaitable(stop):
stop = await stop
if stop: if stop:
return event 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, []): for hook, evfilter in self._hooks.get(filter_type, []):
if await evfilter.filter(event): if evfilter.filter(event):
try: try:
await hook(event) hook(event)
except Exception as ex: except Exception as ex:
self.logger.exception(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] cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
parts = event.message_snapshot.text.split(maxsplit=1) parts = event.message_snapshot.text.split(maxsplit=1)
payload = parts[1] if len(parts) > 1 else "" payload = parts[1] if len(parts) > 1 else ""
cmd = parts.pop(0) cmd = parts.pop(0)
if "@" in cmd: if "@" in cmd:
suffix = "@" + (await self.account.self_contact.get_snapshot()).address suffix = "@" + self.account.self_contact.get_snapshot().address
if cmd.endswith(suffix): if cmd.endswith(suffix):
cmd = cmd[: -len(suffix)] cmd = cmd[: -len(suffix)]
else: else:
@@ -153,32 +150,32 @@ class Client:
event["command"], event["payload"] = cmd, payload 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) event = AttrDict(command="", payload="", message_snapshot=snapshot)
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX): if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
await self._parse_command(event) self._parse_command(event)
await self._on_event(event, NewMessage) 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) event = AttrDict(message_snapshot=snapshot)
img_changed = parse_system_image_changed(snapshot.text) img_changed = parse_system_image_changed(snapshot.text)
if img_changed: if img_changed:
_, event["image_deleted"] = img_changed _, event["image_deleted"] = img_changed
await self._on_event(event, GroupImageChanged) self._on_event(event, GroupImageChanged)
return return
title_changed = parse_system_title_changed(snapshot.text) title_changed = parse_system_title_changed(snapshot.text)
if title_changed: if title_changed:
_, event["old_name"] = title_changed _, event["old_name"] = title_changed
await self._on_event(event, GroupNameChanged) self._on_event(event, GroupNameChanged)
return return
members_changed = parse_system_add_remove(snapshot.text) members_changed = parse_system_add_remove(snapshot.text)
if members_changed: if members_changed:
action, event["member"], _ = members_changed action, event["member"], _ = members_changed
event["member_added"] = action == "added" event["member_added"] = action == "added"
await self._on_event(event, MemberListChanged) self._on_event(event, MemberListChanged)
return return
self.logger.warning( self.logger.warning(
@@ -187,20 +184,20 @@ class Client:
snapshot.text, snapshot.text,
) )
async def _process_messages(self) -> None: def _process_messages(self) -> None:
if self._should_process_messages: if self._should_process_messages:
for message in await self.account.get_next_messages(): for message in self.account.get_next_messages():
snapshot = await message.get_snapshot() snapshot = message.get_snapshot()
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]: 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: if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
await self._handle_info_msg(snapshot) self._handle_info_msg(snapshot)
await snapshot.message.mark_seen() snapshot.message.mark_seen()
class Bot(Client): class Bot(Client):
"""Simple bot implementation that listent to events of a single account.""" """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") kwargs.setdefault("bot", "1")
await super().configure(email, password, **kwargs) super().configure(email, password, **kwargs)

View File

@@ -24,39 +24,39 @@ class Contact:
def _rpc(self) -> "Rpc": def _rpc(self) -> "Rpc":
return self.account._rpc return self.account._rpc
async def block(self) -> None: def block(self) -> None:
"""Block contact.""" """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.""" """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.""" """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.""" """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 """Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact. 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.""" """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 snapshot["contact"] = self
return snapshot return snapshot
async def create_chat(self) -> "Chat": def create_chat(self) -> "Chat":
"""Create or get an existing 1:1 chat for this contact.""" """Create or get an existing 1:1 chat for this contact."""
from .chat import Chat from .chat import Chat
return Chat( return Chat(
self.account, 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),
) )

View File

@@ -16,34 +16,34 @@ class DeltaChat:
def __init__(self, rpc: "Rpc") -> None: def __init__(self, rpc: "Rpc") -> None:
self.rpc = rpc self.rpc = rpc
async def add_account(self) -> Account: def add_account(self) -> Account:
"""Create a new account database.""" """Create a new account database."""
account_id = await self.rpc.add_account() account_id = self.rpc.add_account()
return Account(self, account_id) 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.""" """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] 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.""" """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.""" """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 """Indicate that the network likely has come back or just that the network
conditions might have changed. 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.""" """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.""" """Set stock translation strings."""
await self.rpc.set_stock_strings(translations) self.rpc.set_stock_strings(translations)

View File

@@ -1,5 +1,4 @@
"""High-level classes for event processing and filtering.""" """High-level classes for event processing and filtering."""
import inspect
import re import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union 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): class EventFilter(ABC):
"""The base event filter. """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 parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -43,16 +42,13 @@ class EventFilter(ABC):
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
async def _call_func(self, event) -> bool: def _call_func(self, event) -> bool:
if not self.func: if not self.func:
return True return True
res = self.func(event) return self.func(event)
if inspect.isawaitable(res):
return await res
return res
@abstractmethod @abstractmethod
async def filter(self, event): def filter(self, event):
"""Return True-like value if the event passed the filter and should be """Return True-like value if the event passed the filter and should be
used, or False-like value otherwise. used, or False-like value otherwise.
""" """
@@ -62,7 +58,7 @@ class RawEvent(EventFilter):
"""Matches raw core events. """Matches raw core events.
:param types: The types of event to match. :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 parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -82,10 +78,10 @@ class RawEvent(EventFilter):
return (self.types, self.func) == (other.types, other.func) return (self.types, self.func) == (other.types, other.func)
return False return False
async def filter(self, event: "AttrDict") -> bool: def filter(self, event: "AttrDict") -> bool:
if self.types and event.type not in self.types: if self.types and event.type not in self.types:
return False return False
return await self._call_func(event) return self._call_func(event)
class NewMessage(EventFilter): 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 :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 only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched. 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 parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -159,7 +155,7 @@ class NewMessage(EventFilter):
) )
return False 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: if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False return False
if self.is_info is not None and self.is_info != event.message_snapshot.is_info: if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
@@ -168,11 +164,9 @@ class NewMessage(EventFilter):
return False return False
if self.pattern: if self.pattern:
match = self.pattern(event.message_snapshot.text) match = self.pattern(event.message_snapshot.text)
if inspect.isawaitable(match):
match = await match
if not match: if not match:
return False return False
return await super()._call_func(event) return super()._call_func(event)
class MemberListChanged(EventFilter): 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 :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 only match if a member was removed. If omitted both, member additions
and removals, will be matched. 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 parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -201,10 +195,10 @@ class MemberListChanged(EventFilter):
return (self.added, self.func) == (other.added, other.func) return (self.added, self.func) == (other.added, other.func)
return False 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: if self.added is not None and self.added != event.member_added:
return False return False
return await self._call_func(event) return self._call_func(event)
class GroupImageChanged(EventFilter): 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 :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 only match if a new image was set. If omitted both, image changes and
removals, will be matched. 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 parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -233,10 +227,10 @@ class GroupImageChanged(EventFilter):
return (self.deleted, self.func) == (other.deleted, other.func) return (self.deleted, self.func) == (other.deleted, other.func)
return False 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: if self.deleted is not None and self.deleted != event.image_deleted:
return False return False
return await self._call_func(event) return self._call_func(event)
class GroupNameChanged(EventFilter): class GroupNameChanged(EventFilter):
@@ -245,7 +239,7 @@ class GroupNameChanged(EventFilter):
Warning: registering a handler for this event will cause the messages Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots. 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 parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -258,8 +252,8 @@ class GroupNameChanged(EventFilter):
return self.func == other.func return self.func == other.func
return False return False
async def filter(self, event: "AttrDict") -> bool: def filter(self, event: "AttrDict") -> bool:
return await self._call_func(event) return self._call_func(event)
class HookCollection: class HookCollection:

View File

@@ -21,39 +21,39 @@ class Message:
def _rpc(self) -> "Rpc": def _rpc(self) -> "Rpc":
return self.account._rpc return self.account._rpc
async def send_reaction(self, *reaction: str): def send_reaction(self, *reaction: str):
"""Send a reaction to this message.""" """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.""" """Get a snapshot with the properties of this message."""
from .chat import Chat 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["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id) snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = self snapshot["message"] = self
return snapshot return snapshot
async def get_reactions(self) -> Optional[AttrDict]: def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions.""" """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: if reactions:
return AttrDict(reactions) return AttrDict(reactions)
return None return None
async def mark_seen(self) -> None: def mark_seen(self) -> None:
"""Mark the message as seen.""" """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.""" """Send a webxdc status update. This message must be a webxdc."""
if not isinstance(update, str): if not isinstance(update, str):
update = json.dumps(update) 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: 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)) return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
async def get_webxdc_info(self) -> dict: def get_webxdc_info(self) -> dict:
return await self._rpc.get_webxdc_info(self.account.id, self.id) return self._rpc.get_webxdc_info(self.account.id, self.id)

View File

@@ -1,70 +1,68 @@
import asyncio
import json import json
import os import os
import urllib.request
from typing import AsyncGenerator, List, Optional from typing import AsyncGenerator, List, Optional
import aiohttp import pytest
import pytest_asyncio
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from .rpc import Rpc from .rpc import Rpc
async def get_temp_credentials() -> dict: def get_temp_credentials() -> dict:
url = os.getenv("DCC_NEW_TMP_EMAIL") url = os.getenv("DCC_NEW_TMP_EMAIL")
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set" assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
# Replace default 5 minute timeout with a 1 minute timeout. request = urllib.request.Request(url, method="POST")
timeout = aiohttp.ClientTimeout(total=60) with urllib.request.urlopen(request, timeout=60) as f:
async with aiohttp.ClientSession() as session, session.post(url, timeout=timeout) as response: return json.load(f)
return json.loads(await response.text())
class ACFactory: class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None: def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat self.deltachat = deltachat
async def get_unconfigured_account(self) -> Account: def get_unconfigured_account(self) -> Account:
return await self.deltachat.add_account() return self.deltachat.add_account()
async def get_unconfigured_bot(self) -> Bot: def get_unconfigured_bot(self) -> Bot:
return Bot(await self.get_unconfigured_account()) 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.""" """Make a new account with configuration options set, but configuration not started."""
credentials = await get_temp_credentials() credentials = get_temp_credentials()
account = await self.get_unconfigured_account() account = self.get_unconfigured_account()
await account.set_config("addr", credentials["email"]) account.set_config("addr", credentials["email"])
await account.set_config("mail_pw", credentials["password"]) account.set_config("mail_pw", credentials["password"])
assert not await account.is_configured() assert not account.is_configured()
return account return account
async def new_configured_account(self) -> Account: def new_configured_account(self) -> Account:
account = await self.new_preconfigured_account() account = self.new_preconfigured_account()
await account.configure() account.configure()
assert await account.is_configured() assert account.is_configured()
return account return account
async def new_configured_bot(self) -> Bot: def new_configured_bot(self) -> Bot:
credentials = await get_temp_credentials() credentials = get_temp_credentials()
bot = await self.get_unconfigured_bot() bot = self.get_unconfigured_bot()
await bot.configure(credentials["email"], credentials["password"]) bot.configure(credentials["email"], credentials["password"])
return bot return bot
async def get_online_account(self) -> Account: def get_online_account(self) -> Account:
account = await self.new_configured_account() account = self.new_configured_account()
await account.start_io() account.start_io()
while True: while True:
event = await account.wait_for_event() event = account.wait_for_event()
print(event) print(event)
if event.type == EventType.IMAP_INBOX_IDLE: if event.type == EventType.IMAP_INBOX_IDLE:
break break
return account return account
async def get_online_accounts(self, num: int) -> List[Account]: def get_online_accounts(self, num: int) -> List[Account]:
return await asyncio.gather(*[self.get_online_account() for _ in range(num)]) return [self.get_online_account() for _ in range(num)]
async def send_message( def send_message(
self, self,
to_account: Account, to_account: Account,
from_account: Optional[Account] = None, from_account: Optional[Account] = None,
@@ -73,16 +71,16 @@ class ACFactory:
group: Optional[str] = None, group: Optional[str] = None,
) -> Message: ) -> Message:
if not from_account: if not from_account:
from_account = (await self.get_online_accounts(1))[0] from_account = (self.get_online_accounts(1))[0]
to_contact = await from_account.create_contact(await to_account.get_config("addr")) to_contact = from_account.create_contact(to_account.get_config("addr"))
if group: if group:
to_chat = await from_account.create_group(group) to_chat = from_account.create_group(group)
await to_chat.add_contact(to_contact) to_chat.add_contact(to_contact)
else: else:
to_chat = await to_contact.create_chat() to_chat = to_contact.create_chat()
return await to_chat.send_message(text=text, file=file) return to_chat.send_message(text=text, file=file)
async def process_message( def process_message(
self, self,
to_client: Client, to_client: Client,
from_account: Optional[Account] = None, from_account: Optional[Account] = None,
@@ -90,7 +88,7 @@ class ACFactory:
file: Optional[str] = None, file: Optional[str] = None,
group: Optional[str] = None, group: Optional[str] = None,
) -> AttrDict: ) -> AttrDict:
await self.send_message( self.send_message(
to_account=to_client.account, to_account=to_client.account,
from_account=from_account, from_account=from_account,
text=text, text=text,
@@ -98,16 +96,16 @@ class ACFactory:
group=group, group=group,
) )
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG) return to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
@pytest_asyncio.fixture @pytest.fixture()
async def rpc(tmp_path) -> AsyncGenerator: def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts")) rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
async with rpc_server: with rpc_server:
yield rpc_server yield rpc_server
@pytest_asyncio.fixture @pytest.fixture()
async def acfactory(rpc) -> AsyncGenerator: def acfactory(rpc) -> AsyncGenerator:
yield ACFactory(DeltaChat(rpc)) return ACFactory(DeltaChat(rpc))

View File

@@ -1,7 +1,9 @@
import asyncio
import json import json
import logging import logging
import os import os
import subprocess
from queue import Queue
from threading import Event, Thread
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
@@ -11,7 +13,7 @@ class JsonRpcError(Exception):
class Rpc: class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs): 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: if accounts_dir:
kwargs["env"] = { kwargs["env"] = {
**kwargs.get("env", os.environ), **kwargs.get("env", os.environ),
@@ -19,92 +21,115 @@ class Rpc:
} }
self._kwargs = kwargs self._kwargs = kwargs
self.process: asyncio.subprocess.Process self.process: subprocess.Popen
self.id: int self.id: int
self.event_queues: Dict[int, asyncio.Queue] self.event_queues: Dict[int, Queue]
# Map from request ID to `asyncio.Future` returning the response. # Map from request ID to `threading.Event`.
self.request_events: Dict[int, asyncio.Future] 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.closing: bool
self.reader_task: asyncio.Task self.reader_thread: Thread
self.events_task: asyncio.Task self.writer_thread: Thread
self.events_thread: Thread
async def start(self) -> None: def start(self) -> None:
# Use buffer of 64 MiB. self.process = subprocess.Popen(
# 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", "deltachat-rpc-server",
stdin=asyncio.subprocess.PIPE, stdin=subprocess.PIPE,
stdout=asyncio.subprocess.PIPE, stdout=subprocess.PIPE,
limit=limit,
**self._kwargs, **self._kwargs,
) )
self.id = 0 self.id = 0
self.event_queues = {} self.event_queues = {}
self.request_events = {} self.request_events = {}
self.request_results = {}
self.request_queue = Queue()
self.closing = False self.closing = False
self.reader_task = asyncio.create_task(self.reader_loop()) self.reader_thread = Thread(target=self.reader_loop)
self.events_task = asyncio.create_task(self.events_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()
async def close(self) -> None: def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes.""" """Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True self.closing = True
await self.stop_io_for_all_accounts() self.stop_io_for_all_accounts()
await self.events_task self.events_thread.join()
self.process.terminate() self.process.terminate()
await self.reader_task self.reader_thread.join()
self.request_queue.put(None)
self.writer_thread.join()
async def __aenter__(self): def __enter__(self):
await self.start() self.start()
return self return self
async def __aexit__(self, _exc_type, _exc, _tb): def __exit__(self, _exc_type, _exc, _tb):
await self.close() self.close()
async def reader_loop(self) -> None: def reader_loop(self) -> None:
try: try:
while True: while True:
line = await self.process.stdout.readline() # noqa line = self.process.stdout.readline()
if not line: # EOF if not line: # EOF
break break
response = json.loads(line) response = json.loads(line)
if "id" in response: if "id" in response:
fut = self.request_events.pop(response["id"]) response_id = response["id"]
fut.set_result(response) event = self.request_events.pop(response_id)
self.request_results[response_id] = response
event.set()
else: else:
print(response) print(response)
except Exception: except Exception:
# Log an exception if the reader loop dies. # Log an exception if the reader loop dies.
logging.exception("Exception in the reader loop") 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: 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] 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.""" """Requests new events and distributes them between queues."""
try: try:
while True: while True:
if self.closing: if self.closing:
return return
event = await self.get_next_event() event = self.get_next_event()
account_id = event["contextId"] account_id = event["contextId"]
queue = await self.get_queue(account_id) queue = self.get_queue(account_id)
await queue.put(event["event"]) queue.put(event["event"])
except Exception: except Exception:
# Log an exception if the event loop dies. # Log an exception if the event loop dies.
logging.exception("Exception in the event loop") 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.""" """Waits for the next event from the given account and returns it."""
queue = await self.get_queue(account_id) queue = self.get_queue(account_id)
return await queue.get() return queue.get()
def __getattr__(self, attr: str): def __getattr__(self, attr: str):
async def method(*args) -> Any: def method(*args) -> Any:
self.id += 1 self.id += 1
request_id = self.id request_id = self.id
@@ -114,12 +139,12 @@ class Rpc:
"params": args, "params": args,
"id": self.id, "id": self.id,
} }
data = (json.dumps(request) + "\n").encode() event = Event()
self.process.stdin.write(data) # noqa self.request_events[request_id] = event
loop = asyncio.get_running_loop() self.request_queue.put(request)
fut = loop.create_future() event.wait()
self.request_events[request_id] = fut
response = await fut response = self.request_results.pop(request_id)
if "error" in response: if "error" in response:
raise JsonRpcError(response["error"]) raise JsonRpcError(response["error"])
if "result" in response: if "result" in response:

View File

@@ -1,4 +1,4 @@
import asyncio import concurrent.futures
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
@@ -6,26 +6,26 @@ from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError from deltachat_rpc_client.rpc import JsonRpcError
@pytest.mark.asyncio() def test_system_info(rpc) -> None:
async def test_system_info(rpc) -> None: system_info = rpc.get_system_info()
system_info = await rpc.get_system_info()
assert "arch" in system_info assert "arch" in system_info
assert "deltachat_core_version" in system_info assert "deltachat_core_version" in system_info
@pytest.mark.asyncio() def test_sleep(rpc) -> None:
async def test_sleep(rpc) -> None:
"""Test that long-running task does not block short-running task from completion.""" """Test that long-running task does not block short-running task from completion."""
sleep_5_task = asyncio.create_task(rpc.sleep(5.0)) with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
sleep_3_task = asyncio.create_task(rpc.sleep(3.0)) sleep_5_future = executor.submit(rpc.sleep, 5.0)
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED) sleep_3_future = executor.submit(rpc.sleep, 3.0)
assert sleep_3_task in done done, pending = concurrent.futures.wait(
assert sleep_5_task in pending [sleep_5_future, sleep_3_future],
sleep_5_task.cancel() return_when=concurrent.futures.FIRST_COMPLETED,
)
assert sleep_3_future in done
assert sleep_5_future in pending
@pytest.mark.asyncio() def test_email_address_validity(rpc) -> None:
async def test_email_address_validity(rpc) -> None:
valid_addresses = [ valid_addresses = [
"email@example.com", "email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail", "36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
@@ -33,16 +33,15 @@ async def test_email_address_validity(rpc) -> None:
invalid_addresses = ["email@", "example.com", "emai221"] invalid_addresses = ["email@", "example.com", "emai221"]
for addr in valid_addresses: for addr in valid_addresses:
assert await rpc.check_email_validity(addr) assert rpc.check_email_validity(addr)
for addr in invalid_addresses: for addr in invalid_addresses:
assert not await rpc.check_email_validity(addr) assert not rpc.check_email_validity(addr)
@pytest.mark.asyncio() def test_acfactory(acfactory) -> None:
async def test_acfactory(acfactory) -> None: account = acfactory.new_configured_account()
account = await acfactory.new_configured_account()
while True: while True:
event = await account.wait_for_event() event = account.wait_for_event()
if event.type == EventType.CONFIGURE_PROGRESS: if event.type == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error. assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success if event.progress == 1000: # Success
@@ -52,248 +51,241 @@ async def test_acfactory(acfactory) -> None:
print("Successful configuration") print("Successful configuration")
@pytest.mark.asyncio() def test_configure_starttls(acfactory) -> None:
async def test_configure_starttls(acfactory) -> None: account = acfactory.new_preconfigured_account()
account = await acfactory.new_preconfigured_account()
# Use STARTTLS # Use STARTTLS
await account.set_config("mail_security", "2") account.set_config("mail_security", "2")
await account.set_config("send_security", "2") account.set_config("send_security", "2")
await account.configure() account.configure()
assert await account.is_configured() assert account.is_configured()
@pytest.mark.asyncio() def test_account(acfactory) -> None:
async def test_account(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2)
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr") bob_addr = bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob") alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat() alice_chat_bob = alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!") alice_chat_bob.send_text("Hello!")
while True: while True:
event = await bob.wait_for_event() event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id chat_id = event.chat_id
msg_id = event.msg_id msg_id = event.msg_id
break break
message = bob.get_message_by_id(msg_id) 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.chat_id == chat_id
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
await bob.mark_seen_messages([message]) bob.mark_seen_messages([message])
assert alice != bob assert alice != bob
assert repr(alice) assert repr(alice)
assert (await alice.get_info()).level assert alice.get_info().level
assert await alice.get_size() assert alice.get_size()
assert await alice.is_configured() assert alice.is_configured()
assert not await alice.get_avatar() assert not alice.get_avatar()
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
assert await alice.get_contacts() assert alice.get_contacts()
assert await alice.get_contacts(snapshot=True) assert alice.get_contacts(snapshot=True)
assert alice.self_contact assert alice.self_contact
assert await alice.get_chatlist() assert alice.get_chatlist()
assert await alice.get_chatlist(snapshot=True) assert alice.get_chatlist(snapshot=True)
assert await alice.get_qr_code() assert alice.get_qr_code()
assert await alice.get_fresh_messages() assert alice.get_fresh_messages()
assert await alice.get_next_messages() assert alice.get_next_messages()
# Test sending empty message. # Test sending empty message.
assert len(await bob.wait_next_messages()) == 0 assert len(bob.wait_next_messages()) == 0
await alice_chat_bob.send_text("") alice_chat_bob.send_text("")
messages = await bob.wait_next_messages() messages = bob.wait_next_messages()
assert len(messages) == 1 assert len(messages) == 1
message = messages[0] message = messages[0]
snapshot = await message.get_snapshot() snapshot = message.get_snapshot()
assert snapshot.text == "" assert snapshot.text == ""
await bob.mark_seen_messages([message]) bob.mark_seen_messages([message])
group = await alice.create_group("test group") group = alice.create_group("test group")
await group.add_contact(alice_contact_bob) group.add_contact(alice_contact_bob)
group_msg = await group.send_message(text="hello") group_msg = group.send_message(text="hello")
assert group_msg == alice.get_message_by_id(group_msg.id) assert group_msg == alice.get_message_by_id(group_msg.id)
assert group == alice.get_chat_by_id(group.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") alice.set_config("selfstatus", "test")
assert await alice.get_config("selfstatus") == "test" assert alice.get_config("selfstatus") == "test"
await alice.update_config(selfstatus="test2") alice.update_config(selfstatus="test2")
assert await alice.get_config("selfstatus") == "test2" assert alice.get_config("selfstatus") == "test2"
assert not await alice.get_blocked_contacts() assert not alice.get_blocked_contacts()
await alice_contact_bob.block() alice_contact_bob.block()
blocked_contacts = await alice.get_blocked_contacts() blocked_contacts = alice.get_blocked_contacts()
assert blocked_contacts assert blocked_contacts
assert blocked_contacts[0].contact == alice_contact_bob assert blocked_contacts[0].contact == alice_contact_bob
await bob.remove() bob.remove()
await alice.stop_io() alice.stop_io()
@pytest.mark.asyncio() def test_chat(acfactory) -> None:
async def test_chat(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2)
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr") bob_addr = bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob") alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat() alice_chat_bob = alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!") alice_chat_bob.send_text("Hello!")
while True: while True:
event = await bob.wait_for_event() event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id chat_id = event.chat_id
msg_id = event.msg_id msg_id = event.msg_id
break break
message = bob.get_message_by_id(msg_id) 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.chat_id == chat_id
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
bob_chat_alice = bob.get_chat_by_id(chat_id) bob_chat_alice = bob.get_chat_by_id(chat_id)
assert alice_chat_bob != bob_chat_alice assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob) assert repr(alice_chat_bob)
await alice_chat_bob.delete() alice_chat_bob.delete()
assert not await bob_chat_alice.can_send() assert not bob_chat_alice.can_send()
await bob_chat_alice.accept() bob_chat_alice.accept()
assert await bob_chat_alice.can_send() assert bob_chat_alice.can_send()
await bob_chat_alice.block() bob_chat_alice.block()
bob_chat_alice = await snapshot.sender.create_chat() bob_chat_alice = snapshot.sender.create_chat()
await bob_chat_alice.mute() bob_chat_alice.mute()
await bob_chat_alice.unmute() bob_chat_alice.unmute()
await bob_chat_alice.pin() bob_chat_alice.pin()
await bob_chat_alice.unpin() bob_chat_alice.unpin()
await bob_chat_alice.archive() bob_chat_alice.archive()
await bob_chat_alice.unarchive() bob_chat_alice.unarchive()
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
await bob_chat_alice.set_name("test") bob_chat_alice.set_name("test")
await bob_chat_alice.set_ephemeral_timer(300) bob_chat_alice.set_ephemeral_timer(300)
await bob_chat_alice.get_encryption_info() bob_chat_alice.get_encryption_info()
group = await alice.create_group("test group") group = alice.create_group("test group")
await group.add_contact(alice_contact_bob) group.add_contact(alice_contact_bob)
await group.get_qr_code() group.get_qr_code()
snapshot = await group.get_basic_snapshot() snapshot = group.get_basic_snapshot()
assert snapshot.name == "test group" assert snapshot.name == "test group"
await group.set_name("new name") group.set_name("new name")
snapshot = await group.get_full_snapshot() snapshot = group.get_full_snapshot()
assert snapshot.name == "new name" assert snapshot.name == "new name"
msg = await group.send_message(text="hi") msg = group.send_message(text="hi")
assert (await msg.get_snapshot()).text == "hi" assert (msg.get_snapshot()).text == "hi"
await group.forward_messages([msg]) group.forward_messages([msg])
await group.set_draft(text="test draft") group.set_draft(text="test draft")
draft = await group.get_draft() draft = group.get_draft()
assert draft.text == "test draft" assert draft.text == "test draft"
await group.remove_draft() group.remove_draft()
assert not await group.get_draft() assert not group.get_draft()
assert await group.get_messages() assert group.get_messages()
await group.get_fresh_message_count() group.get_fresh_message_count()
await group.mark_noticed() group.mark_noticed()
assert await group.get_contacts() assert group.get_contacts()
await group.remove_contact(alice_chat_bob) group.remove_contact(alice_chat_bob)
await group.get_locations() group.get_locations()
@pytest.mark.asyncio() def test_contact(acfactory) -> None:
async def test_contact(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2)
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr") bob_addr = bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob") alice_contact_bob = alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id) assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob) assert repr(alice_contact_bob)
await alice_contact_bob.block() alice_contact_bob.block()
await alice_contact_bob.unblock() alice_contact_bob.unblock()
await alice_contact_bob.set_name("new name") alice_contact_bob.set_name("new name")
await alice_contact_bob.get_encryption_info() alice_contact_bob.get_encryption_info()
snapshot = await alice_contact_bob.get_snapshot() snapshot = alice_contact_bob.get_snapshot()
assert snapshot.address == bob_addr assert snapshot.address == bob_addr
await alice_contact_bob.create_chat() alice_contact_bob.create_chat()
@pytest.mark.asyncio() def test_message(acfactory) -> None:
async def test_message(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2)
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr") bob_addr = bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob") alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat() alice_chat_bob = alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!") alice_chat_bob.send_text("Hello!")
while True: while True:
event = await bob.wait_for_event() event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id chat_id = event.chat_id
msg_id = event.msg_id msg_id = event.msg_id
break break
message = bob.get_message_by_id(msg_id) 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.chat_id == chat_id
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
assert not snapshot.is_bot assert not snapshot.is_bot
assert repr(message) assert repr(message)
with pytest.raises(JsonRpcError): # chat is not accepted with pytest.raises(JsonRpcError): # chat is not accepted
await snapshot.chat.send_text("hi") snapshot.chat.send_text("hi")
await snapshot.chat.accept() snapshot.chat.accept()
await snapshot.chat.send_text("hi") snapshot.chat.send_text("hi")
await message.mark_seen() message.mark_seen()
await message.send_reaction("😎") message.send_reaction("😎")
reactions = await message.get_reactions() reactions = message.get_reactions()
assert reactions assert reactions
snapshot = await message.get_snapshot() snapshot = message.get_snapshot()
assert reactions == snapshot.reactions assert reactions == snapshot.reactions
@pytest.mark.asyncio() def test_is_bot(acfactory) -> None:
async def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots.""" """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") bob_addr = bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob") alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat() alice_chat_bob = alice_contact_bob.create_chat()
# Alice becomes a bot. # Alice becomes a bot.
await alice.set_config("bot", "1") alice.set_config("bot", "1")
await alice_chat_bob.send_text("Hello!") alice_chat_bob.send_text("Hello!")
while True: while True:
event = await bob.wait_for_event() event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
msg_id = event.msg_id msg_id = event.msg_id
message = bob.get_message_by_id(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.chat_id == event.chat_id
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
assert snapshot.is_bot assert snapshot.is_bot
break break
@pytest.mark.asyncio() def test_bot(acfactory) -> None:
async def test_bot(acfactory) -> None:
mock = MagicMock() mock = MagicMock()
user = (await acfactory.get_online_accounts(1))[0] user = (acfactory.get_online_accounts(1))[0]
bot = await acfactory.new_configured_bot() bot = acfactory.new_configured_bot()
bot2 = await acfactory.new_configured_bot() bot2 = acfactory.new_configured_bot()
assert await bot.is_configured() assert bot.is_configured()
assert await bot.account.get_config("bot") == "1" assert bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG) hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook) bot.add_hook(*hook)
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!")
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot() snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id) mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook) bot.remove_hook(*hook)
@@ -305,53 +297,52 @@ async def test_bot(acfactory) -> None:
hook = track, events.NewMessage(r"hello") hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook) bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help")) 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) 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) 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 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 assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook) bot.remove_hook(*hook)
mock.hook.reset_mock() mock.hook.reset_mock()
await acfactory.process_message(from_account=user, to_client=bot, text="hello") acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help") event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id) mock.hook.assert_called_once_with(event.msg_id)
@pytest.mark.asyncio() def test_wait_next_messages(acfactory) -> None:
async def test_wait_next_messages(acfactory) -> None: alice = acfactory.new_configured_account()
alice = await acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning. # Create a bot account so it does not receive device messages in the beginning.
bot = await acfactory.new_preconfigured_account() bot = acfactory.new_preconfigured_account()
await bot.set_config("bot", "1") bot.set_config("bot", "1")
await bot.configure() bot.configure()
# There are no old messages and the call returns immediately. # 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. with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
next_messages_task = asyncio.create_task(bot.wait_next_messages()) # Bot starts waiting for messages.
next_messages_task = executor.submit(bot.wait_next_messages)
bot_addr = await bot.get_config("addr") bot_addr = bot.get_config("addr")
alice_contact_bot = await alice.create_contact(bot_addr, "Bob") alice_contact_bot = alice.create_contact(bot_addr, "Bob")
alice_chat_bot = await alice_contact_bot.create_chat() alice_chat_bot = alice_contact_bot.create_chat()
await alice_chat_bot.send_text("Hello!") alice_chat_bot.send_text("Hello!")
next_messages = await next_messages_task next_messages = next_messages_task.result()
assert len(next_messages) == 1 assert len(next_messages) == 1
snapshot = await next_messages[0].get_snapshot() snapshot = next_messages[0].get_snapshot()
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
@pytest.mark.asyncio() def test_import_export(acfactory, tmp_path) -> None:
async def test_import_export(acfactory, tmp_path) -> None: alice = acfactory.new_configured_account()
alice = await acfactory.new_configured_account() alice.export_backup(tmp_path)
await alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar")) files = list(tmp_path.glob("*.tar"))
alice2 = await acfactory.get_unconfigured_account() alice2 = acfactory.get_unconfigured_account()
await alice2.import_backup(files[0]) alice2.import_backup(files[0])

View File

@@ -1,24 +1,22 @@
import pytest
from deltachat_rpc_client import EventType from deltachat_rpc_client import EventType
@pytest.mark.asyncio() def test_webxdc(acfactory) -> None:
async def test_webxdc(acfactory) -> None: alice, bob = acfactory.get_online_accounts(2)
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr") bob_addr = bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob") alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat() alice_chat_bob = alice_contact_bob.create_chat()
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc") alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
while True: while True:
event = await bob.wait_for_event() event = bob.wait_for_event()
if event.type == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
bob_chat_alice = bob.get_chat_by_id(event.chat_id) bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id) message = bob.get_message_by_id(event.msg_id)
break break
webxdc_info = await message.get_webxdc_info() webxdc_info = message.get_webxdc_info()
assert webxdc_info == { assert webxdc_info == {
"document": None, "document": None,
"icon": "icon.png", "icon": "icon.png",
@@ -28,20 +26,20 @@ async def test_webxdc(acfactory) -> None:
"summary": None, "summary": None,
} }
status_updates = await message.get_webxdc_status_updates() status_updates = message.get_webxdc_status_updates()
assert status_updates == [] assert status_updates == []
await bob_chat_alice.accept() bob_chat_alice.accept()
await message.send_webxdc_status_update({"payload": 42}, "") message.send_webxdc_status_update({"payload": 42}, "")
await message.send_webxdc_status_update({"payload": "Second update"}, "description") 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 == [ assert status_updates == [
{"payload": 42, "serial": 1, "max_serial": 2}, {"payload": 42, "serial": 1, "max_serial": 2},
{"payload": "Second update", "serial": 2, "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 == [ assert status_updates == [
{"payload": "Second update", "serial": 2, "max_serial": 2}, {"payload": "Second update", "serial": 2, "max_serial": 2},
] ]

View File

@@ -6,7 +6,7 @@ envlist =
[testenv] [testenv]
commands = commands =
pytest {posargs} pytest -n6 {posargs}
setenv = setenv =
# Avoid stack overflow when Rust core is built without optimizations. # Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608 RUST_MIN_STACK=8388608
@@ -14,10 +14,8 @@ passenv =
DCC_NEW_TMP_EMAIL DCC_NEW_TMP_EMAIL
deps = deps =
pytest pytest
pytest-asyncio
pytest-timeout pytest-timeout
aiohttp pytest-xdist
aiodns
[testenv:lint] [testenv:lint]
skipsdist = True skipsdist = True