Merge pull request #3813 from deltachat/adb/rpc-client-improvements

python deltachat-rpc-client improvements
This commit is contained in:
Asiel Díaz Benítez
2022-12-10 14:34:23 -05:00
committed by GitHub
16 changed files with 1427 additions and 187 deletions

View File

@@ -1,54 +1,25 @@
#!/usr/bin/env python3
"""Minimal echo bot example.
it will echo back any text send to it, it also will print to console all Delta Chat core events.
Pass --help to the CLI to see available options.
"""
import asyncio
import logging
import sys
import deltachat_rpc_client as dc
from deltachat_rpc_client import events, run_bot_cli
hooks = events.HookCollection()
async def main():
async with dc.Rpc() as rpc:
deltachat = dc.DeltaChat(rpc)
system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
@hooks.on(events.RawEvent)
async def log_event(event):
print(event)
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
await account.set_config("bot", "1")
if not await account.is_configured():
logging.info("Account is not configured, configuring")
await account.set_config("addr", sys.argv[1])
await account.set_config("mail_pw", sys.argv[2])
await account.configure()
logging.info("Configured")
else:
logging.info("Account is already configured")
await deltachat.start_io()
async def process_messages():
for message in await account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()
# Process old messages.
await process_messages()
while True:
event = await account.wait_for_event()
if event["type"] == "Info":
logging.info("%s", event["msg"])
elif event["type"] == "Warning":
logging.warning("%s", event["msg"])
elif event["type"] == "Error":
logging.error("%s", event["msg"])
elif event["type"] == "IncomingMsg":
logging.info("Got an incoming message")
await process_messages()
@hooks.on(events.NewMessage)
async def echo(msg):
await msg.chat.send_text(msg.text)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
asyncio.run(run_bot_cli(hooks))

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Advanced echo bot example.
it will echo back any message that has non-empty text and also supports the /help command.
"""
import asyncio
import logging
import sys
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
hooks = events.HookCollection()
@hooks.on(events.RawEvent)
async def log_event(event):
if event.type == EventType.INFO:
logging.info(event.msg)
elif event.type == EventType.WARNING:
logging.warning(event.msg)
@hooks.on(events.RawEvent(EventType.ERROR))
async def log_error(event):
logging.error(event.msg)
@hooks.on(events.NewMessage(r".+", func=lambda msg: not msg.text.startswith("/")))
async def echo(msg):
await msg.chat.send_text(msg.text)
@hooks.on(events.NewMessage(r"/help"))
async def help_command(msg):
await msg.chat.send_text("Send me any text message and I will echo it back")
async def main():
async with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
bot = Bot(account, hooks)
if not await bot.is_configured():
asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
await bot.run_forever()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""
Example echo bot without using hooks
"""
import asyncio
import logging
import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc
async def main():
async with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
await account.set_config("bot", "1")
if not await account.is_configured():
logging.info("Account is not configured, configuring")
await account.set_config("addr", sys.argv[1])
await account.set_config("mail_pw", sys.argv[2])
await account.configure()
logging.info("Configured")
else:
logging.info("Account is already configured")
await deltachat.start_io()
async def process_messages():
for message in await account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()
# Process old messages.
await process_messages()
while True:
event = await account.wait_for_event()
if event["type"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["type"] == EventType.WARNING:
logging.warning("%s", event["msg"])
elif event["type"] == EventType.ERROR:
logging.error("%s", event["msg"])
elif event["type"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
await process_messages()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())

View File

@@ -1,5 +1,10 @@
"""Delta Chat asynchronous high-level API"""
from .account import Account
from .chat import Chat
from .client import Bot, Client
from .const import EventType
from .contact import Contact
from .deltachat import DeltaChat
from .message import Message
from .rpc import Rpc
from .utils import AttrDict, run_bot_cli, run_client_cli

View File

@@ -1,79 +1,265 @@
from typing import List, Optional
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from .chat import Chat
from .const import ChatlistFlag, ContactFlag, SpecialContactId
from .contact import Contact
from .message import Message
from .rpc import Rpc
from .utils import AttrDict
if TYPE_CHECKING:
from .deltachat import DeltaChat
class Account:
def __init__(self, rpc, account_id) -> None:
self._rpc = rpc
self.account_id = account_id
"""Delta Chat account."""
def __init__(self, manager: "DeltaChat", account_id: int) -> None:
self.manager = manager
self.id = account_id
@property
def _rpc(self) -> Rpc:
return self.manager.rpc
def __eq__(self, other) -> bool:
if not isinstance(other, Account):
return False
return self.id == other.id and self.manager == other.manager
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Account id={self.account_id}>"
return f"<Account id={self.id}>"
async def wait_for_event(self) -> dict:
async def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
return await self._rpc.wait_for_event(self.account_id)
return AttrDict(await self._rpc.wait_for_event(self.id))
async def remove(self) -> None:
"""Remove the account."""
await self._rpc.remove_account(self.account_id)
await self._rpc.remove_account(self.id)
async def start_io(self) -> None:
"""Start the account I/O."""
await self._rpc.start_io(self.account_id)
await self._rpc.start_io(self.id)
async def stop_io(self) -> None:
"""Stop the account I/O."""
await self._rpc.stop_io(self.account_id)
await self._rpc.stop_io(self.id)
async def get_info(self) -> dict:
return await self._rpc.get_info(self.account_id)
async def get_info(self) -> AttrDict:
"""Return dictionary of this account configuration parameters."""
return AttrDict(await self._rpc.get_info(self.id))
async def get_file_size(self) -> int:
return await self._rpc.get_account_file_size(self.account_id)
async def get_size(self) -> int:
"""Get the combined filesize of an account in bytes."""
return await self._rpc.get_account_file_size(self.id)
async def is_configured(self) -> bool:
"""Return True for configured accounts."""
return await self._rpc.is_configured(self.account_id)
"""Return True if this account is configured."""
return await self._rpc.is_configured(self.id)
async def set_config(self, key: str, value: Optional[str] = None) -> None:
"""Set the configuration value key pair."""
await self._rpc.set_config(self.account_id, key, value)
"""Set configuration value."""
await self._rpc.set_config(self.id, key, value)
async def get_config(self, key: str) -> Optional[str]:
"""Get the configuration value."""
return await self._rpc.get_config(self.account_id, key)
"""Get configuration value."""
return await self._rpc.get_config(self.id, key)
async def update_config(self, **kwargs) -> None:
"""update config values."""
for key, value in kwargs.items():
await self.set_config(key, value)
async def set_avatar(self, img_path: Optional[str] = None) -> None:
"""Set self avatar.
Passing None will discard the currently set avatar.
"""
await self.set_config("selfavatar", img_path)
async def get_avatar(self) -> Optional[str]:
"""Get self avatar."""
return await self.get_config("selfavatar")
async def configure(self) -> None:
"""Configure an account."""
await self._rpc.configure(self.account_id)
await self._rpc.configure(self.id)
async def create_contact(self, address: str, name: Optional[str] = None) -> Contact:
"""Create a contact with the given address and, optionally, a name."""
return Contact(
self._rpc,
self.account_id,
await self._rpc.create_contact(self.account_id, address, name),
async def create_contact(
self, obj: Union[int, str, Contact], name: Optional[str] = None
) -> Contact:
"""Create a new Contact or return an existing one.
Calling this method will always result in the same
underlying contact id. If there already is a Contact
with that e-mail address, it is unblocked and its display
name is updated if specified.
:param obj: email-address or contact id.
:param name: (optional) display name for this contact.
"""
if isinstance(obj, int):
obj = Contact(self, obj)
if isinstance(obj, Contact):
obj = (await obj.get_snapshot()).address
return Contact(self, await self._rpc.create_contact(self.id, obj, name))
async def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID."""
return Contact(self, contact_id)
async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""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)
return contact_id and Contact(self, contact_id)
async def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts."""
contacts = await self._rpc.get_blocked_contacts(self.id)
return [
AttrDict(contact=Contact(self, contact["id"]), **contact)
for contact in contacts
]
async def get_contacts(
self,
query: Optional[str] = None,
with_self: bool = False,
verified_only: bool = False,
snapshot: bool = False,
) -> Union[List[Contact], List[AttrDict]]:
"""Get a filtered list of contacts.
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
:param with_self: if True the self-contact is also included if it matches the query.
:param only_verified: if True only return verified contacts.
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
"""
flags = 0
if verified_only:
flags |= ContactFlag.VERIFIED_ONLY
if with_self:
flags |= ContactFlag.ADD_SELF
if snapshot:
contacts = await self._rpc.get_contacts(self.id, flags, query)
return [
AttrDict(contact=Contact(self, contact["id"]), **contact)
for contact in contacts
]
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
return [Contact(self, contact_id) for contact_id in contacts]
@property
def self_contact(self) -> Contact:
"""This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
async def get_chatlist(
self,
query: Optional[str] = None,
contact: Optional[Contact] = None,
archived_only: bool = False,
for_forwarding: bool = False,
no_specials: bool = False,
alldone_hint: bool = False,
snapshot: bool = False,
) -> Union[List[Chat], List[AttrDict]]:
"""Return list of chats.
:param query: if a string is specified only chats matching this query are returned.
:param contact: if a contact is specified only chats including this contact are returned.
:param archived_only: if True only archived chats are returned.
:param for_forwarding: if True the chat list is sorted with "Saved messages" at the top
and withot "Device chat" and contact requests.
:param no_specials: if True archive link is not added to the list.
:param alldone_hint: if True the "all done hint" special chat will be added to the list
as needed.
:param snapshot: If True return a list of chat snapshots instead of Chat instances.
"""
flags = 0
if archived_only:
flags |= ChatlistFlag.ARCHIVED_ONLY
if for_forwarding:
flags |= ChatlistFlag.FOR_FORWARDING
if no_specials:
flags |= ChatlistFlag.NO_SPECIALS
if alldone_hint:
flags |= ChatlistFlag.ADD_ALLDONE_HINT
entries = await self._rpc.get_chatlist_entries(
self.id, flags, query, contact and contact.id
)
if not snapshot:
return [Chat(self, entry[0]) for entry in entries]
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
chats = []
for item in items.values():
item["chat"] = Chat(self, item["id"])
chats.append(AttrDict(item))
return chats
async def create_group(self, name: str, protect: bool = False) -> Chat:
"""Create a new group chat.
After creation, the group has only self-contact as member and is in unpromoted state.
"""
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
async def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID."""
return Chat(self, chat_id)
async def secure_join(self, qrdata: str) -> Chat:
chat_id = await self._rpc.secure_join(self.account_id, qrdata)
return Chat(self._rpc, self.account_id, chat_id)
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device.
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
:param qrdata: The text of the scanned QR code.
"""
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
async def get_qr_code(self) -> Tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data.
this data needs to be transferred to another Delta Chat account
in a second channel, typically used by mobiles with QRcode-show + scan UX.
"""
return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
async def get_message_by_id(self, msg_id: int) -> Message:
"""Return the Message instance with the given ID."""
return Message(self, msg_id)
async def mark_seen_messages(self, messages: List[Message]) -> None:
"""Mark the given set of messages as seen."""
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
async def delete_messages(self, messages: List[Message]) -> None:
"""Delete messages (local and remote)."""
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
async def get_fresh_messages(self) -> List[Message]:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
If you are writing a bot, use get_fresh_messages_in_arrival_order instead,
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
to process oldest messages first.
"""
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.account_id)
return [Message(self._rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids]
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.account_id))
return [Message(self._rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids]
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]

View File

@@ -1,41 +1,263 @@
from typing import TYPE_CHECKING
import calendar
from datetime import datetime
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from .const import ChatVisibility
from .contact import Contact
from .message import Message
from .rpc import Rpc
from .utils import AttrDict
if TYPE_CHECKING:
from .message import Message
from .account import Account
class Chat:
def __init__(self, rpc: Rpc, account_id: int, chat_id: int) -> None:
self._rpc = rpc
self.account_id = account_id
self.chat_id = chat_id
"""Chat object which manages members and through which you can send and retrieve messages."""
async def block(self) -> None:
"""Block the chat."""
await self._rpc.block_chat(self.account_id, self.chat_id)
def __init__(self, account: "Account", chat_id: int) -> None:
self.account = account
self.id = chat_id
async def accept(self) -> None:
"""Accept the contact request."""
await self._rpc.accept_chat(self.account_id, self.chat_id)
@property
def _rpc(self) -> Rpc:
return self.account._rpc
def __eq__(self, other) -> bool:
if not isinstance(other, Chat):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Chat id={self.id} account={self.account.id}>"
async def delete(self) -> None:
await self._rpc.delete_chat(self.account_id, self.chat_id)
"""Delete this chat and all its messages.
async def get_encryption_info(self) -> str:
return await self._rpc.get_chat_encryption_info(self.account_id, self.chat_id)
Note:
async def send_text(self, text: str) -> "Message":
from .message import Message
- does not delete messages on server
- the chat or contact is not blocked, new message will arrive
"""
await self._rpc.delete_chat(self.account.id, self.id)
msg_id = await self._rpc.misc_send_text_message(
self.account_id, self.chat_id, text
)
return Message(self._rpc, self.account_id, msg_id)
async def block(self) -> None:
"""Block this chat."""
await self._rpc.block_chat(self.account.id, self.id)
async def accept(self) -> None:
"""Accept this contact request chat."""
await self._rpc.accept_chat(self.account.id, self.id)
async def leave(self) -> None:
await self._rpc.leave_group(self.account_id, self.chat_id)
"""Leave this chat."""
await self._rpc.leave_group(self.account.id, self.id)
async def mute(self, duration: Optional[int] = None) -> None:
"""Mute this chat, if a duration is not provided the chat is muted forever.
:param duration: mute duration from now in seconds. Must be greater than zero.
"""
if duration is not None:
assert duration > 0, "Invalid duration"
dur: Union[str, dict] = dict(Until=duration)
else:
dur = "Forever"
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
async def unmute(self) -> None:
"""Unmute this chat."""
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
async def pin(self) -> None:
"""Pin this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.PINNED
)
async def unpin(self) -> None:
"""Unpin this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.NORMAL
)
async def archive(self) -> None:
"""Archive this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.ARCHIVED
)
async def unarchive(self) -> None:
"""Unarchive this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.NORMAL
)
async def set_name(self, name: str) -> None:
"""Set name of this chat."""
await self._rpc.set_chat_name(self.account.id, self.id, name)
async def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat."""
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
async def get_encryption_info(self) -> str:
"""Return encryption info for this chat."""
return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
async def get_qr_code(self) -> Tuple[str, str]:
"""Get Join-Group QR code text and SVG data."""
return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
async def get_basic_snapshot(self) -> AttrDict:
"""Get a chat snapshot with basic info about this chat."""
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
return AttrDict(chat=self, **info)
async def get_full_snapshot(self) -> AttrDict:
"""Get a full snapshot of this chat."""
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
return AttrDict(chat=self, **info)
async def send_message(
self,
text: Optional[str] = None,
file: Optional[str] = None,
location: Optional[Tuple[float, float]] = None,
quoted_msg: Optional[Union[int, Message]] = None,
) -> Message:
"""Send a message and return the resulting Message instance."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
msg_id, _ = await self._rpc.misc_send_msg(
self.account.id, self.id, text, file, location, quoted_msg
)
return Message(self.account, msg_id)
async def send_text(self, text: str) -> Message:
"""Send a text message and return the resulting Message instance."""
msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
return Message(self.account, msg_id)
async def send_videochat_invitation(self) -> Message:
"""Send a videochat invitation and return the resulting Message instance."""
msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
return Message(self.account, msg_id)
async def send_sticker(self, path: str) -> Message:
"""Send an sticker and return the resulting Message instance."""
msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
async def forward_messages(self, messages: List[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
async def set_draft(
self,
text: Optional[str] = None,
file: Optional[str] = None,
quoted_msg: Optional[int] = None,
) -> None:
"""Set draft message."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
async def remove_draft(self) -> None:
"""Remove draft message."""
await self._rpc.remove_draft(self.account.id, self.id)
async def get_draft(self) -> Optional[AttrDict]:
"""Get draft message."""
snapshot = await self._rpc.get_draft(self.account.id, self.id)
if not snapshot:
return None
snapshot = AttrDict(snapshot)
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
async def get_messages(self, flags: int = 0) -> List[Message]:
"""get the list of messages in this chat."""
msgs = await self._rpc.get_message_ids(self.account.id, self.id, flags)
return [Message(self.account, msg_id) for msg_id in msgs]
async def get_fresh_message_count(self) -> int:
return await self._rpc.get_fresh_msg_cnt(self.account_id, self.chat_id)
"""Get number of fresh messages in this chat"""
return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
async def mark_noticed(self) -> None:
"""Mark all messages in this chat as noticed."""
await self._rpc.marknoticed_chat(self.account.id, self.id)
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Add contacts to this group."""
for cnt in contact:
if isinstance(cnt, str):
cnt = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
cnt = cnt.id
await self._rpc.add_contact_to_chat(self.account.id, self.id, cnt)
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group."""
for cnt in contact:
if isinstance(cnt, str):
cnt = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
cnt = cnt.id
await self._rpc.remove_contact_from_chat(self.account.id, self.id, cnt)
async def get_contacts(self) -> List[Contact]:
"""Get the contacts belonging to this chat.
For single/direct chats self-address is not included.
"""
contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts]
async def set_image(self, path: str) -> None:
"""Set profile image of this chat.
:param path: Full path of the image to use as the group image.
"""
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
async def remove_image(self) -> None:
"""Remove profile image of this chat."""
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
async def get_locations(
self,
contact: Optional[Contact] = None,
timestamp_from: Optional[datetime] = None,
timestamp_to: Optional[datetime] = None,
) -> List[AttrDict]:
"""Get list of location snapshots for the given contact in the given timespan."""
time_from = (
calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
)
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
contact_id = contact.id if contact else 0
result = await self._rpc.get_locations(
self.account.id, self.id, contact_id, time_from, time_to
)
locations = []
contacts: Dict[int, Contact] = {}
for loc in result:
loc = AttrDict(loc)
loc["chat"] = self
loc["contact"] = contacts.setdefault(
loc.contact_id, Contact(self.account, loc.contact_id)
)
loc["message"] = Message(self.account, loc.msg_id)
locations.append(loc)
return locations

View File

@@ -0,0 +1,100 @@
"""Event loop implementations offering high level event handling/hooking."""
import logging
from typing import Callable, Dict, Iterable, Optional, Set, Tuple, Type, Union
from deltachat_rpc_client.account import Account
from .const import EventType
from .events import EventFilter, NewInfoMessage, NewMessage, RawEvent
from .utils import AttrDict
class Client:
"""Simple Delta Chat client that listen to events of a single account."""
def __init__(
self,
account: Account,
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
logger: Optional[logging.Logger] = None,
) -> None:
self.account = account
self.logger = logger or logging
self._hooks: Dict[type, Set[tuple]] = {}
self.add_hooks(hooks or [])
def add_hooks(
self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]
) -> None:
for hook, event in hooks:
self.add_hook(hook, event)
def add_hook(
self, hook: Callable, event: Union[type, EventFilter] = RawEvent
) -> None:
"""Register hook for the given event filter."""
if isinstance(event, type):
event = event()
assert isinstance(event, EventFilter)
self._hooks.setdefault(type(event), set()).add((hook, event))
def remove_hook(self, hook: Callable, event: Union[type, EventFilter]) -> None:
"""Unregister hook from the given event filter."""
if isinstance(event, type):
event = event()
self._hooks.get(type(event), set()).remove((hook, event))
async def is_configured(self) -> bool:
return await self.account.is_configured()
async def configure(self, email: str, password: str, **kwargs) -> None:
await self.account.set_config("addr", email)
await self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
await self.account.set_config(key, value)
await self.account.configure()
self.logger.debug("Account configured")
async def run_forever(self) -> None:
self.logger.debug("Listening to incoming events...")
if await self.is_configured():
await self.account.start_io()
await self._process_messages() # Process old messages.
while True:
event = await self.account.wait_for_event()
event["type"] = EventType(event.type)
event["account"] = self.account
await self._on_event(event)
if event.type == EventType.INCOMING_MSG:
await self._process_messages()
async def _on_event(
self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent
) -> None:
for hook, evfilter in self._hooks.get(filter_type, []):
if await evfilter.filter(event):
try:
await hook(event)
except Exception as ex:
self.logger.exception(ex)
def _should_process_messages(self) -> bool:
return any(issubclass(filter_type, NewMessage) for filter_type in self._hooks)
async def _process_messages(self) -> None:
if self._should_process_messages():
for message in await self.account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if snapshot.is_info:
await self._on_event(snapshot, NewInfoMessage)
else:
await self._on_event(snapshot, NewMessage)
await snapshot.message.mark_seen()
class Bot(Client):
"""Simple bot implementation that listent to events of a single account."""
async def configure(self, email: str, password: str, **kwargs) -> None:
kwargs.setdefault("bot", "1")
await super().configure(email, password, **kwargs)

View File

@@ -0,0 +1,120 @@
from enum import Enum, IntEnum
class ContactFlag(IntEnum):
VERIFIED_ONLY = 0x01
ADD_SELF = 0x02
class ChatlistFlag(IntEnum):
ARCHIVED_ONLY = 0x01
NO_SPECIALS = 0x02
ADD_ALLDONE_HINT = 0x04
FOR_FORWARDING = 0x08
class SpecialContactId(IntEnum):
SELF = 1
INFO = 2 # centered messages as "member added", used in all chats
DEVICE = 5 # messages "update info" in the device-chat
LAST_SPECIAL = 9
class EventType(str, Enum):
"""Core event types"""
INFO = "Info"
SMTP_CONNECTED = "SmtpConnected"
IMAP_CONNECTED = "ImapConnected"
SMTP_MESSAGE_SENT = "SmtpMessageSent"
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
NEW_BLOB_FILE = "NewBlobFile"
DELETED_BLOB_FILE = "DeletedBlobFile"
WARNING = "Warning"
ERROR = "Error"
ERROR_SELF_NOT_IN_GROUP = "ErrorSelfNotInGroup"
MSGS_CHANGED = "MsgsChanged"
REACTIONS_CHANGED = "ReactionsChanged"
INCOMING_MSG = "IncomingMsg"
INCOMING_MSG_BUNCH = "IncomingMsgBunch"
MSGS_NOTICED = "MsgsNoticed"
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"
MSG_READ = "MsgRead"
CHAT_MODIFIED = "ChatModified"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"
LOCATION_CHANGED = "LocationChanged"
CONFIGURE_PROGRESS = "ConfigureProgress"
IMEX_PROGRESS = "ImexProgress"
IMEX_FILE_WRITTEN = "ImexFileWritten"
SECUREJOIN_INVITER_PROGRESS = "SecurejoinInviterProgress"
SECUREJOIN_JOINER_PROGRESS = "SecurejoinJoinerProgress"
CONNECTIVITY_CHANGED = "ConnectivityChanged"
SELFAVATAR_CHANGED = "SelfavatarChanged"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
class ChatType(IntEnum):
"""Chat types"""
UNDEFINED = 0
SINGLE = 100
GROUP = 120
MAILINGLIST = 140
BROADCAST = 160
class ChatVisibility(str, Enum):
"""Chat visibility types"""
NORMAL = "Normal"
ARCHIVED = "Archived"
PINNED = "Pinned"
class DownloadState(str, Enum):
"""Message download state"""
DONE = "Done"
AVAILABLE = "Available"
FAILURE = "Failure"
IN_PROGRESS = "InProgress"
class ViewType(str, Enum):
"""Message view type."""
UNKNOWN = "Unknown"
TEXT = "Text"
IMAGE = "Image"
GIF = "Gif"
STICKER = "Sticker"
AUDIO = "Audio"
VOICE = "Voice"
VIDEO = "Video"
FILE = "File"
VIDEOCHAT_INVITATION = "VideochatInvitation"
WEBXDC = "Webxdc"
class SystemMessageType(str, Enum):
"""System message type."""
UNKNOWN = "Unknown"
GROUP_NAME_CHANGED = "GroupNameChanged"
GROUP_IMAGE_CHANGED = "GroupImageChanged"
MEMBER_ADDED_TO_GROUP = "MemberAddedToGroup"
MEMBER_REMOVED_FROM_GROUP = "MemberRemovedFromGroup"
AUTOCRYPT_SETUP_MESSAGE = "AutocryptSetupMessage"
SECUREJOIN_MESSAGE = "SecurejoinMessage"
LOCATION_STREAMING_ENABLED = "LocationStreamingEnabled"
LOCATION_ONLY = "LocationOnly"
CHAT_PROTECTION_ENABLED = "ChatProtectionEnabled"
CHAT_PROTECTION_DISABLED = "ChatProtectionDisabled"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
MULTI_DEVICE_SYNC = "MultiDeviceSync"
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"

View File

@@ -1,8 +1,10 @@
from typing import TYPE_CHECKING
from .rpc import Rpc
from .utils import AttrDict
if TYPE_CHECKING:
from .account import Account
from .chat import Chat
@@ -13,40 +15,58 @@ class Contact:
Essentially a wrapper for RPC, account ID and a contact ID.
"""
def __init__(self, rpc: Rpc, account_id: int, contact_id: int) -> None:
self._rpc = rpc
self.account_id = account_id
self.contact_id = contact_id
def __init__(self, account: "Account", contact_id: int) -> None:
self.account = account
self.id = contact_id
def __eq__(self, other) -> bool:
if not isinstance(other, Contact):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Contact id={self.id} account={self.account.id}>"
@property
def _rpc(self) -> Rpc:
return self.account._rpc
async def block(self) -> None:
"""Block contact."""
await self._rpc.block_contact(self.account_id, self.contact_id)
await self._rpc.block_contact(self.account.id, self.id)
async def unblock(self) -> None:
"""Unblock contact."""
await self._rpc.unblock_contact(self.account_id, self.contact_id)
await self._rpc.unblock_contact(self.account.id, self.id)
async def delete(self) -> None:
"""Delete contact."""
await self._rpc.delete_contact(self.account_id, self.contact_id)
await self._rpc.delete_contact(self.account.id, self.id)
async def change_name(self, name: str) -> None:
await self._rpc.change_contact_name(self.account_id, self.contact_id, name)
async def set_name(self, name: str) -> None:
"""Change the name of this contact."""
await self._rpc.change_contact_name(self.account.id, self.id, name)
async def get_encryption_info(self) -> str:
return await self._rpc.get_contact_encryption_info(
self.account_id, self.contact_id
)
"""Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact.
"""
return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
async def get_dictionary(self) -> dict:
async def get_snapshot(self) -> AttrDict:
"""Return a dictionary with a snapshot of all contact properties."""
return await self._rpc.get_contact(self.account_id, self.contact_id)
snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
snapshot["contact"] = self
return snapshot
async def create_chat(self) -> "Chat":
"""Create or get an existing 1:1 chat for this contact."""
from .chat import Chat
return Chat(
self._rpc,
self.account_id,
await self._rpc.create_chat_by_contact_id(self.account_id, self.contact_id),
self.account,
await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
)

View File

@@ -1,12 +1,13 @@
from typing import List
from typing import Dict, List
from .account import Account
from .rpc import Rpc
from .utils import AttrDict
class DeltaChat:
"""
Delta Chat account manager.
Delta Chat accounts manager.
This is the root of the object oriented API.
"""
@@ -14,21 +15,33 @@ class DeltaChat:
self.rpc = rpc
async def add_account(self) -> Account:
"""Create a new account database."""
account_id = await self.rpc.add_account()
return Account(self.rpc, account_id)
return Account(self, account_id)
async def get_all_accounts(self) -> List[Account]:
"""Return a list of all available accounts."""
account_ids = await self.rpc.get_all_account_ids()
return [Account(self.rpc, 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:
"""Start the I/O of all accounts."""
await self.rpc.start_io_for_all_accounts()
async def stop_io(self) -> None:
"""Stop the I/O of all accounts."""
await self.rpc.stop_io_for_all_accounts()
async def maybe_network(self) -> None:
"""Indicate that the network likely has come back or just that the network
conditions might have changed.
"""
await self.rpc.maybe_network()
async def get_system_info(self) -> dict:
return await self.rpc.get_system_info()
async def get_system_info(self) -> AttrDict:
"""Get information about the Delta Chat core in this system."""
return AttrDict(await self.rpc.get_system_info())
async def set_translations(self, translations: Dict[str, str]) -> None:
"""Set stock translation strings."""
await self.rpc.set_stock_strings(translations)

View File

@@ -0,0 +1,161 @@
"""High-level classes for event processing and filtering."""
import inspect
import re
from abc import ABC, abstractmethod
from typing import Callable, Iterable, Iterator, Optional, Set, Tuple, Union
from .const import EventType
from .utils import AttrDict
def _tuple_of(obj, type_: type) -> tuple:
if not obj:
return tuple()
if isinstance(obj, type_):
obj = (obj,)
if not all(isinstance(elem, type_) for elem in obj):
raise TypeError()
return tuple(obj)
class EventFilter(ABC):
"""The base event filter.
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
def __init__(self, func: Optional[Callable] = None):
self.func = func
@abstractmethod
def __hash__(self) -> int:
"""Object's unique hash"""
@abstractmethod
def __eq__(self, other) -> bool:
"""Return True if two event filters are equal."""
def __ne__(self, other):
return not self.__eq__(other)
async def _call_func(self, event) -> bool:
if not self.func:
return True
res = self.func(event)
if inspect.isawaitable(res):
return await res
return res
@abstractmethod
async def filter(self, event):
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
class RawEvent(EventFilter):
"""Matches raw core events.
:param types: The types of event to match.
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
def __init__(
self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs
):
super().__init__(**kwargs)
try:
self.types = _tuple_of(types, EventType)
except TypeError as err:
raise TypeError(f"Invalid event type given: {types}") from err
def __hash__(self) -> int:
return hash((self.types, self.func))
def __eq__(self, other) -> bool:
if isinstance(other, RawEvent):
return (self.types, self.func) == (other.types, other.func)
return False
async def filter(self, event: AttrDict) -> bool:
if self.types and event.type not in self.types:
return False
return await self._call_func(event)
class NewMessage(EventFilter):
"""Matches whenever a new message arrives.
Warning: registering a handler for this event or any subclass will cause the messages
to be marked as read. Its usage is mainly intended for bots.
"""
def __init__(
self,
pattern: Union[
None,
str,
Callable[[str], bool],
re.Pattern,
] = None,
func: Optional[Callable[[AttrDict], bool]] = None,
) -> None:
super().__init__(func=func)
if isinstance(pattern, str):
pattern = re.compile(pattern)
if isinstance(pattern, re.Pattern):
self.pattern: Optional[Callable] = pattern.match
elif not pattern or callable(pattern):
self.pattern = pattern
else:
raise TypeError("Invalid pattern type")
def __hash__(self) -> int:
return hash((self.pattern, self.func))
def __eq__(self, other) -> bool:
if type(other) is self.__class__: # noqa
return (self.pattern, self.func) == (other.pattern, other.func)
return False
async def filter(self, event: AttrDict) -> bool:
if self.pattern:
match = self.pattern(event.text)
if inspect.isawaitable(match):
match = await match
if not match:
return False
return await super()._call_func(event)
class NewInfoMessage(NewMessage):
"""Matches whenever a new info/system message arrives."""
class HookCollection:
"""
Helper class to collect event hooks that can later be added to a Delta Chat client.
"""
def __init__(self) -> None:
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
return iter(self._hooks)
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
"""Register decorated function as listener for the given event."""
if isinstance(event, type):
event = event()
assert isinstance(event, EventFilter), "Invalid event filter"
def _decorator(func) -> Callable:
self._hooks.add((func, event))
return func
return _decorator

View File

@@ -1,42 +1,49 @@
from dataclasses import dataclass
from typing import Optional
from typing import TYPE_CHECKING
from .chat import Chat
from .contact import Contact
from .rpc import Rpc
from .utils import AttrDict
if TYPE_CHECKING:
from .account import Account
class Message:
def __init__(self, rpc: Rpc, account_id: int, msg_id: int) -> None:
self._rpc = rpc
self.account_id = account_id
self.msg_id = msg_id
"""Delta Chat Message object."""
async def send_reaction(self, reactions: str) -> "Message":
msg_id = await self._rpc.send_reaction(self.account_id, self.msg_id, reactions)
return Message(self._rpc, self.account_id, msg_id)
def __init__(self, account: "Account", msg_id: int) -> None:
self.account = account
self.id = msg_id
async def get_snapshot(self) -> "MessageSnapshot":
message_object = await self._rpc.get_message(self.account_id, self.msg_id)
return MessageSnapshot(
message=self,
chat=Chat(self._rpc, self.account_id, message_object["chatId"]),
sender=Contact(self._rpc, self.account_id, message_object["fromId"]),
text=message_object["text"],
error=message_object.get("error"),
is_info=message_object["isInfo"],
)
def __eq__(self, other) -> bool:
if not isinstance(other, Message):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Message id={self.id} account={self.account.id}>"
@property
def _rpc(self) -> Rpc:
return self.account._rpc
async def send_reaction(self, *reaction: str):
"""Send a reaction to this message."""
await self._rpc.send_reaction(self.account.id, self.id, reaction)
async def get_snapshot(self) -> AttrDict:
"""Get a snapshot with the properties of this message."""
from .chat import Chat
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = self
return snapshot
async def mark_seen(self) -> None:
"""Mark the message as seen."""
await self._rpc.markseen_msgs(self.account_id, [self.msg_id])
@dataclass
class MessageSnapshot:
message: Message
chat: Chat
sender: Contact
text: str
error: Optional[str]
is_info: bool
await self._rpc.markseen_msgs(self.account.id, [self.id])

View File

@@ -1,4 +1,3 @@
import asyncio
import json
import os
from typing import AsyncGenerator, List
@@ -7,6 +6,7 @@ import aiohttp
import pytest_asyncio
from .account import Account
from .client import Bot
from .deltachat import DeltaChat
from .rpc import Rpc
@@ -23,9 +23,15 @@ class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
async def get_unconfigured_account(self) -> Account:
return await self.deltachat.add_account()
async def get_unconfigured_bot(self) -> Bot:
return Bot(await self.get_unconfigured_account())
async def new_configured_account(self) -> Account:
credentials = await get_temp_credentials()
account = await self.deltachat.add_account()
account = await self.get_unconfigured_account()
assert not await account.is_configured()
await account.set_config("addr", credentials["email"])
await account.set_config("mail_pw", credentials["password"])
@@ -33,18 +39,24 @@ class ACFactory:
assert await account.is_configured()
return account
async def new_configured_bot(self) -> Bot:
credentials = await get_temp_credentials()
bot = await self.get_unconfigured_bot()
await bot.configure(credentials["email"], credentials["password"])
return bot
async def get_online_accounts(self, num: int) -> List[Account]:
accounts = [await self.new_configured_account() for _ in range(num)]
await self.deltachat.start_io()
for account in accounts:
await account.start_io()
return accounts
@pytest_asyncio.fixture
async def rpc(tmp_path) -> AsyncGenerator:
env = {**os.environ, "DC_ACCOUNTS_PATH": str(tmp_path / "accounts")}
rpc_server = Rpc(env=env)
async with rpc_server as rpc:
yield rpc
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
async with rpc_server:
yield rpc_server
@pytest_asyncio.fixture

View File

@@ -1,6 +1,7 @@
import asyncio
import json
from typing import Any, AsyncGenerator, Dict, Optional
import os
from typing import Any, Dict, Optional
class JsonRpcError(Exception):
@@ -8,26 +9,36 @@ class JsonRpcError(Exception):
class Rpc:
def __init__(self, *args, **kwargs):
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
self._args = args
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
"DC_ACCOUNTS_PATH": os.path.abspath(
os.path.expanduser(str(accounts_dir))
),
}
self._kwargs = kwargs
self.process: asyncio.subprocess.Process
self.id: int
self.event_queues: Dict[int, asyncio.Queue]
# Map from request ID to `asyncio.Future` returning the response.
self.request_events: Dict[int, asyncio.Future]
self.reader_task: asyncio.Task
async def start(self) -> None:
self.process = await asyncio.create_subprocess_exec(
"deltachat-rpc-server",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
*self._args,
**self._kwargs
)
self.event_queues: Dict[int, asyncio.Queue] = {}
self.id = 0
self.event_queues = {}
self.request_events = {}
self.reader_task = asyncio.create_task(self.reader_loop())
# Map from request ID to `asyncio.Future` returning the response.
self.request_events: Dict[int, asyncio.Future] = {}
async def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.process.terminate()
@@ -42,7 +53,7 @@ class Rpc:
async def reader_loop(self) -> None:
while True:
line = await self.process.stdout.readline()
line = await self.process.stdout.readline() # noqa
if not line: # EOF
break
response = json.loads(line)
@@ -79,7 +90,7 @@ class Rpc:
"id": self.id,
}
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data)
self.process.stdin.write(data) # noqa
loop = asyncio.get_running_loop()
fut = loop.create_future()
self.request_events[request_id] = fut

View File

@@ -0,0 +1,114 @@
import argparse
import asyncio
import re
import sys
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
if TYPE_CHECKING:
from .client import Client
from .events import EventFilter
def _camel_to_snake(name: str) -> str:
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
name = re.sub("__([A-Z])", r"_\1", name)
name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name)
return name.lower()
def _to_attrdict(obj):
if isinstance(obj, dict):
return AttrDict(obj)
if isinstance(obj, list):
return [_to_attrdict(elem) for elem in obj]
return obj
class AttrDict(dict):
"""Dictionary that allows accessing values usin the "dot notation" as attributes."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(
{
_camel_to_snake(key): _to_attrdict(value)
for key, value in dict(*args, **kwargs).items()
}
)
def __getattr__(self, attr):
if attr in self:
return self[attr]
raise AttributeError("Attribute not found: " + str(attr))
def __setattr__(self, attr, val):
if attr in self:
raise AttributeError("Attribute-style access is read only")
super().__setattr__(attr, val)
async def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
) -> None:
"""Run a simple command line app, using the given hooks.
Extra keyword arguments are passed to the internal Rpc object.
"""
from .client import Client
await _run_cli(Client, hooks, argv, **kwargs)
async def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
) -> None:
"""Run a simple bot command line using the given hooks.
Extra keyword arguments are passed to the internal Rpc object.
"""
from .client import Bot
await _run_cli(Bot, hooks, argv, **kwargs)
async def _run_cli(
client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
) -> None:
from .deltachat import DeltaChat
from .rpc import Rpc
if argv is None:
argv = sys.argv
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
parser.add_argument(
"accounts_dir",
help="accounts folder (default: current working directory)",
nargs="?",
)
parser.add_argument("--email", action="store", help="email address")
parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:])
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
deltachat = DeltaChat(rpc)
core_version = (await deltachat.get_system_info()).deltachat_core_version
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version)
if not await client.is_configured():
assert (
args.email and args.password
), "Account is not configured and email and password must be provided"
asyncio.create_task(
client.configure(email=args.email, password=args.password)
)
await client.run_forever()

View File

@@ -1,5 +1,8 @@
import pytest
from deltachat_rpc_client import AttrDict, EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
@pytest.mark.asyncio
async def test_system_info(rpc) -> None:
@@ -27,12 +30,9 @@ async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
while True:
event = await account.wait_for_event()
if event["type"] == "ConfigureProgress":
# Progress 0 indicates error.
assert event["progress"] != 0
if event["progress"] == 1000:
# Success.
if event.type == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success
break
else:
print(event)
@@ -40,21 +40,207 @@ async def test_acfactory(acfactory) -> None:
@pytest.mark.asyncio
async def test_object_account(acfactory) -> None:
async def test_account(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
alice_contact_bob = await alice.create_contact(await bob.get_config("addr"), "Bob")
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event["type"] == "IncomingMsg":
chat_id = event["chatId"]
msg_id = event["msgId"]
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
rpc = acfactory.deltachat.rpc
message = await rpc.get_message(bob.account_id, msg_id)
assert message["chatId"] == chat_id
assert message["text"] == "Hello!"
message = await bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
await bob.mark_seen_messages([message])
assert alice != bob
assert repr(alice)
assert (await alice.get_info()).level
assert await alice.get_size()
assert await alice.is_configured()
assert not await alice.get_avatar()
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
assert await alice.get_contacts()
assert await alice.get_contacts(snapshot=True)
assert alice.self_contact
assert await alice.get_chatlist()
assert await alice.get_chatlist(snapshot=True)
assert await alice.get_qr_code()
await alice.get_fresh_messages()
await alice.get_fresh_messages_in_arrival_order()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
group_msg = await group.send_message(text="hello")
assert group_msg == await alice.get_message_by_id(group_msg.id)
assert group == await alice.get_chat_by_id(group.id)
await alice.delete_messages([group_msg])
await alice.set_config("selfstatus", "test")
assert await alice.get_config("selfstatus") == "test"
await alice.update_config(selfstatus="test2")
assert await alice.get_config("selfstatus") == "test2"
assert not await alice.get_blocked_contacts()
await alice_contact_bob.block()
blocked_contacts = await alice.get_blocked_contacts()
assert blocked_contacts
assert blocked_contacts[0].contact == alice_contact_bob
await bob.remove()
await alice.stop_io()
@pytest.mark.asyncio
async def test_chat(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = await bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
bob_chat_alice = await bob.get_chat_by_id(chat_id)
assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob)
await alice_chat_bob.delete()
await bob_chat_alice.accept()
await bob_chat_alice.block()
bob_chat_alice = await snapshot.sender.create_chat()
await bob_chat_alice.mute()
await bob_chat_alice.unmute()
await bob_chat_alice.pin()
await bob_chat_alice.unpin()
await bob_chat_alice.archive()
await bob_chat_alice.unarchive()
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
await bob_chat_alice.set_name("test")
await bob_chat_alice.set_ephemeral_timer(300)
await bob_chat_alice.get_encryption_info()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
await group.get_qr_code()
snapshot = await group.get_basic_snapshot()
assert snapshot.name == "test group"
await group.set_name("new name")
snapshot = await group.get_full_snapshot()
assert snapshot.name == "new name"
msg = await group.send_message(text="hi")
assert (await msg.get_snapshot()).text == "hi"
await group.forward_messages([msg])
await group.set_draft(text="test draft")
draft = await group.get_draft()
assert draft.text == "test draft"
await group.remove_draft()
assert not await group.get_draft()
assert await group.get_messages()
await group.get_fresh_message_count()
await group.mark_noticed()
assert await group.get_contacts()
await group.remove_contact(alice_chat_bob)
await group.get_locations()
@pytest.mark.asyncio
async def test_contact(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == await alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob)
await alice_contact_bob.block()
await alice_contact_bob.unblock()
await alice_contact_bob.set_name("new name")
await alice_contact_bob.get_encryption_info()
snapshot = await alice_contact_bob.get_snapshot()
assert snapshot.address == bob_addr
await alice_contact_bob.create_chat()
@pytest.mark.asyncio
async def test_message(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = await bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
assert repr(message)
with pytest.raises(JsonRpcError): # chat is not accepted
await snapshot.chat.send_text("hi")
await snapshot.chat.accept()
await snapshot.chat.send_text("hi")
await message.mark_seen()
await message.send_reaction("😎")
@pytest.mark.asyncio
async def test_bot(acfactory) -> None:
async def callback(e):
res.append(e)
res = []
bot = await acfactory.new_configured_bot()
assert await bot.is_configured()
assert await bot.account.get_config("bot") == "1"
bot.add_hook(callback, events.RawEvent(EventType.INFO))
info_event = AttrDict(account=bot.account, type=EventType.INFO, msg="info")
warn_event = AttrDict(account=bot.account, type=EventType.WARNING, msg="warning")
await bot._on_event(info_event)
await bot._on_event(warn_event)
assert info_event in res
assert warn_event not in res
assert len(res) == 1
res = []
bot.add_hook(callback, events.NewMessage(r"hello"))
snapshot1 = AttrDict(text="hello")
snapshot2 = AttrDict(text="hello, world")
snapshot3 = AttrDict(text="hey!")
for snapshot in [snapshot1, snapshot2, snapshot3]:
await bot._on_event(snapshot, events.NewMessage)
assert len(res) == 2
assert snapshot1 in res
assert snapshot2 in res
assert snapshot3 not in res