mirror of
https://github.com/chatmail/core.git
synced 2026-05-01 20:36:31 +03:00
api(deltachat-rpc-client)!: replace asyncio with threads
This commit is contained in:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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()
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user