Skip to content

Commit

Permalink
Add async python client for Delta Chat core JSON-RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
link2xt committed Nov 29, 2022
1 parent f6a502a commit 0582cca
Show file tree
Hide file tree
Showing 14 changed files with 546 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,20 @@ jobs:
working-directory: python
run: tox -e lint,mypy,doc,py3

- name: build deltachat-rpc-server
if: ${{ matrix.python }}
uses: actions-rs/cargo@v1
with:
command: build
args: -p deltachat-rpc-server

- name: run deltachat-rpc-client tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client
run: tox -e py3

- name: install pypy
if: ${{ matrix.python }}
uses: actions/setup-python@v4
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

### API-Changes
- Add Python API to send reactions #3762
- jsonrpc: Add async Python client #3734

### Fixes
- Make sure malformed messsages will never block receiving further messages anymore #3771
Expand Down
33 changes: 33 additions & 0 deletions deltachat-rpc-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Delta Chat RPC python client

RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
and provides asynchronous interface to it.

## Getting started

To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
Install it anywhere in your `PATH`.

## Testing

1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Run `tox`.

Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.

## Using in REPL

It is recommended to use IPython, because it supports using `await` directly
from the REPL.

```
PATH="../target/debug:$PATH" ipython
...
In [1]: from deltachat_rpc_client import *
In [2]: dc = Deltachat(await start_rpc_server())
In [3]: await dc.get_all_accounts()
Out [3]: []
In [4]: alice = await dc.add_account()
In [5]: (await alice.get_info())["journal_mode"]
Out [5]: 'wal'
```
58 changes: 58 additions & 0 deletions deltachat-rpc-client/examples/echobot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
import asyncio
import logging
import sys

import deltachat_rpc_client as dc


async def main():
rpc = await dc.start_rpc_server()
deltachat = dc.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():
fresh_messages = await account.get_fresh_messages()
fresh_message_snapshot_tasks = [
message.get_snapshot() for message in fresh_messages
]
fresh_message_snapshots = await asyncio.gather(*fresh_message_snapshot_tasks)
for snapshot in reversed(fresh_message_snapshots):
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()


if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
14 changes: 14 additions & 0 deletions deltachat-rpc-client/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[build-system]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"

[project]
name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface"
dependencies = [
"aiohttp",
"aiodns"
]
dynamic = [
"version"
]
5 changes: 5 additions & 0 deletions deltachat-rpc-client/src/deltachat_rpc_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .account import Account
from .contact import Contact
from .deltachat import Deltachat
from .message import Message
from .rpc import Rpc, new_online_account, start_rpc_server
68 changes: 68 additions & 0 deletions deltachat-rpc-client/src/deltachat_rpc_client/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import Optional

from .chat import Chat
from .contact import Contact
from .message import Message


class Account:
def __init__(self, rpc, account_id):
self.rpc = rpc
self.account_id = account_id

def __repr__(self):
return "<Account id={}>".format(self.account_id)

async def wait_for_event(self):
"""Wait until the next event and return it."""
return await self.rpc.get_next_event(self.account_id)

async def remove(self) -> None:
"""Remove the account."""
await self.rpc.remove_account(self.account_id)

async def start_io(self) -> None:
"""Start the account I/O."""
await self.rpc.start_io(self.account_id)

async def stop_io(self) -> None:
"""Stop the account I/O."""
await self.rpc.stop_io(self.account_id)

async def get_info(self):
return await self.rpc.get_info(self.account_id)

async def get_file_size(self):
return await self.rpc.get_account_file_size(self.account_id)

async def is_configured(self) -> bool:
"""Return True for configured accounts."""
return await self.rpc.is_configured(self.account_id)

async def set_config(self, key: str, value: Optional[str]):
"""Set the configuration value key pair."""
await self.rpc.set_config(self.account_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)

async def configure(self):
"""Configure an account."""
await self.rpc.configure(self.account_id)

async def create_contact(self, address: str, name: Optional[str]) -> 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 secure_join(self, qr: str) -> Chat:
chat_id = await self.rpc.secure_join(self.account_id, qr)
return Chat(self.rpc, self.account_id, self.chat_id)

async def get_fresh_messages(self):
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]
33 changes: 33 additions & 0 deletions deltachat-rpc-client/src/deltachat_rpc_client/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class Chat:
def __init__(self, rpc, account_id, chat_id):
self.rpc = rpc
self.account_id = account_id
self.chat_id = chat_id

async def block(self):
"""Block the chat."""
await self.rpc.block_chat(self.account_id, self.chat_id)

async def accept(self):
"""Accept the contact request."""
await self.rpc.accept_chat(self.account_id, self.chat_id)

async def delete(self):
await self.rpc.delete_chat(self.account_id, self.chat_id)

async def get_encryption_info(self):
await self.rpc.get_chat_encryption_info(self.account_id, self.chat_id)

async def send_text(self, text: str):
from .message import Message

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 leave(self):
await self.rpc.leave_group(self.account_id, self.chat_id)

async def get_fresh_message_count() -> int:
await get_fresh_msg_cnt(self.account_id, self.chat_id)
44 changes: 44 additions & 0 deletions deltachat-rpc-client/src/deltachat_rpc_client/contact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class Contact:
"""
Contact API.
Essentially a wrapper for RPC, account ID and a contact ID.
"""

def __init__(self, rpc, account_id, contact_id):
self.rpc = rpc
self.account_id = account_id
self.contact_id = contact_id

async def block(self):
"""Block contact."""
await self.rpc.block_contact(self.account_id, self.contact_id)

async def unblock(self):
"""Unblock contact."""
await self.rpc.unblock_contact(self.account_id, self.contact_id)

async def delete(self):
"""Delete contact."""
await self.rpc.delete_contact(self.account_id, self.contact_id)

async def change_name(self, name: str):
await self.rpc.change_contact_name(self.account_id, self.contact_id, name)

async def get_encryption_info(self) -> str:
return await self.rpc.get_contact_encryption_info(
self.account_id, self.contact_id
)

async def get_dictionary(self):
"""Returns a dictionary with a snapshot of all contact properties."""
return await self.rpc.get_contact(self.account_id, self.contact_id)

async def create_chat(self):
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),
)
31 changes: 31 additions & 0 deletions deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from .account import Account


class Deltachat:
"""
Delta Chat account manager.
This is the root of the object oriented API.
"""

def __init__(self, rpc):
self.rpc = rpc

async def add_account(self):
account_id = await self.rpc.add_account()
return Account(self.rpc, account_id)

async def get_all_accounts(self):
account_ids = await self.rpc.get_all_account_ids()
return [Account(self.rpc, account_id) for account_id in account_ids]

async def start_io(self) -> None:
await self.rpc.start_io_for_all_accounts()

async def stop_io(self) -> None:
await self.rpc.stop_io_for_all_accounts()

async def maybe_network(self) -> None:
await self.rpc.maybe_network()

async def get_system_info(self):
return await self.rpc.get_system_info()
41 changes: 41 additions & 0 deletions deltachat-rpc-client/src/deltachat_rpc_client/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from dataclasses import dataclass
from typing import Optional

from .chat import Chat
from .contact import Contact


class Message:
def __init__(self, rpc, account_id, msg_id):
self.rpc = rpc
self.account_id = account_id
self.msg_id = msg_id

async def send_reaction(self, reactions):
msg_id = await self.rpc.send_reaction(self.account_id, self.msg_id, reactions)
return Message(self.rpc, self.account_id, msg_id)

async def get_snapshot(self):
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"],
)

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
Loading

0 comments on commit 0582cca

Please sign in to comment.