Skip to content

Commit 9b04a04

Browse files
committed
Add async python client for Delta Chat core JSON-RPC
1 parent f2c97bd commit 9b04a04

File tree

14 files changed

+546
-0
lines changed

14 files changed

+546
-0
lines changed

.github/workflows/ci.yml

+14
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,20 @@ jobs:
147147
working-directory: python
148148
run: tox -e lint,mypy,doc,py3
149149

150+
- name: build deltachat-rpc-server
151+
if: ${{ matrix.python }}
152+
uses: actions-rs/cargo@v1
153+
with:
154+
command: build
155+
args: -p deltachat-rpc-server
156+
157+
- name: run deltachat-rpc-client tests
158+
if: ${{ matrix.python }}
159+
env:
160+
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
161+
working-directory: deltachat-rpc-client
162+
run: tox -e py3
163+
150164
- name: install pypy
151165
if: ${{ matrix.python }}
152166
uses: actions/setup-python@v4

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### API-Changes
1010
- Add Python API to send reactions #3762
1111
- jsonrpc: add message errors to MessageObject #3788
12+
- jsonrpc: Add async Python client #3734
1213

1314
### Fixes
1415
- Make sure malformed messsages will never block receiving further messages anymore #3771

deltachat-rpc-client/README.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Delta Chat RPC python client
2+
3+
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
4+
and provides asynchronous interface to it.
5+
6+
## Getting started
7+
8+
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
9+
Install it anywhere in your `PATH`.
10+
11+
## Testing
12+
13+
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
14+
2. Run `tox`.
15+
16+
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
17+
18+
## Using in REPL
19+
20+
It is recommended to use IPython, because it supports using `await` directly
21+
from the REPL.
22+
23+
```
24+
PATH="../target/debug:$PATH" ipython
25+
...
26+
In [1]: from deltachat_rpc_client import *
27+
In [2]: dc = Deltachat(await start_rpc_server())
28+
In [3]: await dc.get_all_accounts()
29+
Out [3]: []
30+
In [4]: alice = await dc.add_account()
31+
In [5]: (await alice.get_info())["journal_mode"]
32+
Out [5]: 'wal'
33+
```
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env python3
2+
import asyncio
3+
import logging
4+
import sys
5+
6+
import deltachat_rpc_client as dc
7+
8+
9+
async def main():
10+
rpc = await dc.start_rpc_server()
11+
deltachat = dc.Deltachat(rpc)
12+
system_info = await deltachat.get_system_info()
13+
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
14+
15+
accounts = await deltachat.get_all_accounts()
16+
account = accounts[0] if accounts else await deltachat.add_account()
17+
18+
await account.set_config("bot", "1")
19+
if not await account.is_configured():
20+
logging.info("Account is not configured, configuring")
21+
await account.set_config("addr", sys.argv[1])
22+
await account.set_config("mail_pw", sys.argv[2])
23+
await account.configure()
24+
logging.info("Configured")
25+
else:
26+
logging.info("Account is already configured")
27+
await deltachat.start_io()
28+
29+
async def process_messages():
30+
fresh_messages = await account.get_fresh_messages()
31+
fresh_message_snapshot_tasks = [
32+
message.get_snapshot() for message in fresh_messages
33+
]
34+
fresh_message_snapshots = await asyncio.gather(*fresh_message_snapshot_tasks)
35+
for snapshot in reversed(fresh_message_snapshots):
36+
if not snapshot.is_info:
37+
await snapshot.chat.send_text(snapshot.text)
38+
await snapshot.message.mark_seen()
39+
40+
# Process old messages.
41+
await process_messages()
42+
43+
while True:
44+
event = await account.wait_for_event()
45+
if event["type"] == "Info":
46+
logging.info("%s", event["msg"])
47+
elif event["type"] == "Warning":
48+
logging.warning("%s", event["msg"])
49+
elif event["type"] == "Error":
50+
logging.error("%s", event["msg"])
51+
elif event["type"] == "IncomingMsg":
52+
logging.info("Got an incoming message")
53+
await process_messages()
54+
55+
56+
if __name__ == "__main__":
57+
logging.basicConfig(level=logging.INFO)
58+
asyncio.run(main())

deltachat-rpc-client/pyproject.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[build-system]
2+
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "deltachat-rpc-client"
7+
description = "Python client for Delta Chat core JSON-RPC interface"
8+
dependencies = [
9+
"aiohttp",
10+
"aiodns"
11+
]
12+
dynamic = [
13+
"version"
14+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .account import Account
2+
from .contact import Contact
3+
from .deltachat import Deltachat
4+
from .message import Message
5+
from .rpc import Rpc, new_online_account, start_rpc_server
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from typing import Optional
2+
3+
from .chat import Chat
4+
from .contact import Contact
5+
from .message import Message
6+
7+
8+
class Account:
9+
def __init__(self, rpc, account_id):
10+
self.rpc = rpc
11+
self.account_id = account_id
12+
13+
def __repr__(self):
14+
return "<Account id={}>".format(self.account_id)
15+
16+
async def wait_for_event(self):
17+
"""Wait until the next event and return it."""
18+
return await self.rpc.wait_for_event(self.account_id)
19+
20+
async def remove(self) -> None:
21+
"""Remove the account."""
22+
await self.rpc.remove_account(self.account_id)
23+
24+
async def start_io(self) -> None:
25+
"""Start the account I/O."""
26+
await self.rpc.start_io(self.account_id)
27+
28+
async def stop_io(self) -> None:
29+
"""Stop the account I/O."""
30+
await self.rpc.stop_io(self.account_id)
31+
32+
async def get_info(self):
33+
return await self.rpc.get_info(self.account_id)
34+
35+
async def get_file_size(self):
36+
return await self.rpc.get_account_file_size(self.account_id)
37+
38+
async def is_configured(self) -> bool:
39+
"""Return True for configured accounts."""
40+
return await self.rpc.is_configured(self.account_id)
41+
42+
async def set_config(self, key: str, value: Optional[str]):
43+
"""Set the configuration value key pair."""
44+
await self.rpc.set_config(self.account_id, key, value)
45+
46+
async def get_config(self, key: str) -> Optional[str]:
47+
"""Get the configuration value."""
48+
return await self.rpc.get_config(self.account_id, key)
49+
50+
async def configure(self):
51+
"""Configure an account."""
52+
await self.rpc.configure(self.account_id)
53+
54+
async def create_contact(self, address: str, name: Optional[str]) -> Contact:
55+
"""Create a contact with the given address and, optionally, a name."""
56+
return Contact(
57+
self.rpc,
58+
self.account_id,
59+
await self.rpc.create_contact(self.account_id, address, name),
60+
)
61+
62+
async def secure_join(self, qr: str) -> Chat:
63+
chat_id = await self.rpc.secure_join(self.account_id, qr)
64+
return Chat(self.rpc, self.account_id, self.chat_id)
65+
66+
async def get_fresh_messages(self):
67+
fresh_msg_ids = await self.rpc.get_fresh_msgs(self.account_id)
68+
return [Message(self.rpc, self.account_id, msg_id) for msg_id in fresh_msg_ids]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
class Chat:
2+
def __init__(self, rpc, account_id, chat_id):
3+
self.rpc = rpc
4+
self.account_id = account_id
5+
self.chat_id = chat_id
6+
7+
async def block(self):
8+
"""Block the chat."""
9+
await self.rpc.block_chat(self.account_id, self.chat_id)
10+
11+
async def accept(self):
12+
"""Accept the contact request."""
13+
await self.rpc.accept_chat(self.account_id, self.chat_id)
14+
15+
async def delete(self):
16+
await self.rpc.delete_chat(self.account_id, self.chat_id)
17+
18+
async def get_encryption_info(self):
19+
await self.rpc.get_chat_encryption_info(self.account_id, self.chat_id)
20+
21+
async def send_text(self, text: str):
22+
from .message import Message
23+
24+
msg_id = await self.rpc.misc_send_text_message(
25+
self.account_id, self.chat_id, text
26+
)
27+
return Message(self.rpc, self.account_id, msg_id)
28+
29+
async def leave(self):
30+
await self.rpc.leave_group(self.account_id, self.chat_id)
31+
32+
async def get_fresh_message_count() -> int:
33+
await get_fresh_msg_cnt(self.account_id, self.chat_id)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
class Contact:
2+
"""
3+
Contact API.
4+
5+
Essentially a wrapper for RPC, account ID and a contact ID.
6+
"""
7+
8+
def __init__(self, rpc, account_id, contact_id):
9+
self.rpc = rpc
10+
self.account_id = account_id
11+
self.contact_id = contact_id
12+
13+
async def block(self):
14+
"""Block contact."""
15+
await self.rpc.block_contact(self.account_id, self.contact_id)
16+
17+
async def unblock(self):
18+
"""Unblock contact."""
19+
await self.rpc.unblock_contact(self.account_id, self.contact_id)
20+
21+
async def delete(self):
22+
"""Delete contact."""
23+
await self.rpc.delete_contact(self.account_id, self.contact_id)
24+
25+
async def change_name(self, name: str):
26+
await self.rpc.change_contact_name(self.account_id, self.contact_id, name)
27+
28+
async def get_encryption_info(self) -> str:
29+
return await self.rpc.get_contact_encryption_info(
30+
self.account_id, self.contact_id
31+
)
32+
33+
async def get_dictionary(self):
34+
"""Returns a dictionary with a snapshot of all contact properties."""
35+
return await self.rpc.get_contact(self.account_id, self.contact_id)
36+
37+
async def create_chat(self):
38+
from .chat import Chat
39+
40+
return Chat(
41+
self.rpc,
42+
self.account_id,
43+
await self.rpc.create_chat_by_contact_id(self.account_id, self.contact_id),
44+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from .account import Account
2+
3+
4+
class Deltachat:
5+
"""
6+
Delta Chat account manager.
7+
This is the root of the object oriented API.
8+
"""
9+
10+
def __init__(self, rpc):
11+
self.rpc = rpc
12+
13+
async def add_account(self):
14+
account_id = await self.rpc.add_account()
15+
return Account(self.rpc, account_id)
16+
17+
async def get_all_accounts(self):
18+
account_ids = await self.rpc.get_all_account_ids()
19+
return [Account(self.rpc, account_id) for account_id in account_ids]
20+
21+
async def start_io(self) -> None:
22+
await self.rpc.start_io_for_all_accounts()
23+
24+
async def stop_io(self) -> None:
25+
await self.rpc.stop_io_for_all_accounts()
26+
27+
async def maybe_network(self) -> None:
28+
await self.rpc.maybe_network()
29+
30+
async def get_system_info(self):
31+
return await self.rpc.get_system_info()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
from .chat import Chat
5+
from .contact import Contact
6+
7+
8+
class Message:
9+
def __init__(self, rpc, account_id, msg_id):
10+
self.rpc = rpc
11+
self.account_id = account_id
12+
self.msg_id = msg_id
13+
14+
async def send_reaction(self, reactions):
15+
msg_id = await self.rpc.send_reaction(self.account_id, self.msg_id, reactions)
16+
return Message(self.rpc, self.account_id, msg_id)
17+
18+
async def get_snapshot(self):
19+
message_object = await self.rpc.get_message(self.account_id, self.msg_id)
20+
return MessageSnapshot(
21+
message=self,
22+
chat=Chat(self.rpc, self.account_id, message_object["chatId"]),
23+
sender=Contact(self.rpc, self.account_id, message_object["fromId"]),
24+
text=message_object["text"],
25+
error=message_object.get("error"),
26+
is_info=message_object["isInfo"],
27+
)
28+
29+
async def mark_seen(self) -> None:
30+
"""Mark the message as seen."""
31+
await self.rpc.markseen_msgs(self.account_id, [self.msg_id])
32+
33+
34+
@dataclass
35+
class MessageSnapshot:
36+
message: Message
37+
chat: Chat
38+
sender: Contact
39+
text: str
40+
error: Optional[str]
41+
is_info: bool

0 commit comments

Comments
 (0)