Compare commits

...

69 Commits

Author SHA1 Message Date
flipsimon
af25c8bead started porting checkQr function 2022-07-28 17:22:43 +02:00
flipsimon
8160a3c60a Added type DownloadState 2022-07-26 11:51:04 +02:00
Simon Laux
15019ce02b use camelCase in all js object properties 2022-07-15 13:48:32 +02:00
Simon Laux
7bb5dc4c3c add chat_get_media to jsonrpc
also add viewtype wrapper enum and use it in `MessageObject`,
additionally to using it in `chat_get_media`
2022-07-09 17:30:55 +02:00
Simon Laux
543edac105 add webxdc methods to jsonrpc:
- `webxdc_send_status_update`
- `webxdc_get_status_updates`
- `message_get_webxdc_info`
2022-07-09 17:02:11 +02:00
Simon Laux
55db8d1fe0 jsonrpc: add dm_chat_contact to ChatListItemFetchResult 2022-07-05 15:55:40 +02:00
Simon Laux
1d92f06834 jsonrpc: add get_fresh_msgs and get_fresh_msg_cnt 2022-07-05 15:55:40 +02:00
Simon Laux
2ff05c9dff add add_device_message to jsonrpc 2022-07-05 15:55:40 +02:00
Simon Laux
d6b6d96e21 add set_config_from_qr to jsonrpc 2022-07-05 15:55:40 +02:00
Simon Laux
8776767a44 make dcn_json_rpc_request return undefined instead of not returning
this might have been the cause for the second segfault
2022-07-05 15:55:40 +02:00
Simon Laux
9d83057a71 remove print from test 2022-07-05 15:55:40 +02:00
Simon Laux
8ce11ac62f found another segfault:
this time in batch_set_config
2022-07-05 15:55:40 +02:00
Simon Laux
918ec85767 restore same configure behaviour as desktop:
make configure restart io with the old configuration if it had one on error
2022-07-05 15:55:40 +02:00
Simon Laux
f3a9ab6d23 fix the typos
thanks to ralphtheninja for finding them
2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
e4def6a44d Increase online test timeouts for CI 2022-07-05 15:55:40 +02:00
Simon Laux
4b5c194ef3 make sure to use dc_str_unref instead of free
on cstrings returned/owned by rust
2022-07-05 15:55:40 +02:00
Simon Laux
97e2d85b28 Update deltachat-ffi/deltachat.h
Co-authored-by: bjoern <r10s@b44t.com>
2022-07-05 15:55:40 +02:00
Simon Laux
81bc7bd7bf Update deltachat-ffi/deltachat.h
Co-authored-by: bjoern <r10s@b44t.com>
2022-07-05 15:55:40 +02:00
Simon Laux
f53c456e50 remove unneeded context
thanks to link2xt for pointing that out
2022-07-05 15:55:40 +02:00
Simon Laux
64fa5675a9 reintroduce segfault test script 2022-07-05 15:55:40 +02:00
Simon Laux
1f0bdfa704 apply link2xt's suggestions:
- unref jsonrpc_instance in same thread it was created in
- increase `max_queue_size` from 1 to 1000
2022-07-05 15:55:40 +02:00
Simon Laux
5ac347b7ae make it more idiomatic:
rename `ContactObject::from_dc_contact -> `ContactObject::try_from_dc_contact`
2022-07-05 15:55:40 +02:00
Simon Laux
40fa2d4120 adress dig's comments
- description in cargo.toml
- impl From<EventType> for EventTypeName
- rename `CommandApi::new_from_arc` -> `CommandApi::from_arc`
- pre-allocate if we know the entry count already
- remove unused enumerate
- remove unused serde attribute comment
- rename `FullChat::from_dc_chat_id` -> `FullChat::try_from_dc_chat_id`
2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
aaf27e4434 fix docs 2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
2df10857ca Improve documentation 2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
7eae3a1072 Naming consistency: Use DeltaChat not Deltachat 2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
7fc162543a Improve JSON-RPC CI, no need to build things multiple times 2022-07-05 15:55:40 +02:00
Franz Heinzmann (Frando)
e7da0672ae Fix method name casings and cleanups 2022-07-05 15:55:38 +02:00
Simon Laux
69d9d48ae4 changelog entry about the jsonrpc 2022-07-05 15:54:31 +02:00
Franz Heinzmann (Frando)
802677222b remove debug logs 2022-07-05 15:54:00 +02:00
Simon Laux
d7b9febc33 update todo document
remove specific api stuff for now,
we now have the an incremental aproach on moving
not the all at-once effort I though it would be
2022-07-05 15:54:00 +02:00
Franz Heinzmann (Frando)
1d347f7369 Bump yerpc to 0.3.1 with fix for axum server 2022-07-05 15:53:58 +02:00
Franz Heinzmann (Frando)
e3fa42fe88 don't wait for IO on webserver start 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
2f00b098ac improve test setup and code style 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
bdd4aa0f10 use multi-threaded runtime in JSON-RPC webserver 2022-07-05 15:50:03 +02:00
Simon Laux
6fee4fd878 expose anyhow errors
feature name was wrong
2022-07-05 15:50:03 +02:00
Simon Laux
60d3a3cacf remove emtpy file
allow unused code for new_from_arc
2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
4bb1980f8d try to fix ci 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
35b70b1d1b fix ci 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
fd53b80c17 use stable toolchain not 1.56.0 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
978e4aec82 Fix CFFI for JSON-RPC changes 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
329f498651 improve docs, fix example 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
035e208e4f Improve docs. 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
3404996fdd Improvements to typescript package 2022-07-05 15:50:03 +02:00
Franz Heinzmann (Frando)
97e0e0137a Changes for tokio compat, upgrade to yerpc 0.3
This also changes the webserver binary to use axum in place of tide.
2022-07-05 15:49:59 +02:00
Simon Laux
659e48bd3f improve naming 2022-07-05 15:36:45 +02:00
Simon Laux
271d54e420 commit types.ts
that dc-node has everything it needs to provide @deltachat/jsonrpc-client
without an extra ts compile step
2022-07-05 15:36:45 +02:00
Simon Laux
9984ee5eb2 add @deltachat/jsonrpc-client
to make sure its dependencies are installed, too
whwn installing dc-node
2022-07-05 15:36:45 +02:00
Simon Laux
136bec0273 disable jsonrpc by default 2022-07-05 15:36:45 +02:00
Simon Laux
c5ff7427be put jsonrpc stuff in own module 2022-07-05 15:36:45 +02:00
Simon Laux
2319dfc3eb remove selectAccount from highlevel client 2022-07-05 15:36:45 +02:00
Simon Laux
d8d26b9cae activate other tests again 2022-07-05 15:36:45 +02:00
Simon Laux
d93622bc84 fix closing segfault
thanks again to link2xt for figguring this out
2022-07-05 15:36:45 +02:00
Simon Laux
2fde4962a1 break loop on empty response 2022-07-05 15:36:45 +02:00
Simon Laux
29a5d73f94 call a jsonrpc function in segfault example 2022-07-05 15:36:45 +02:00
Simon Laux
b51814aaaa add jsonrpc feature flag 2022-07-05 15:36:45 +02:00
Simon Laux
63e7179191 add jsonrpc crate to set_core_version 2022-07-05 15:36:45 +02:00
Simon Laux
9915803252 add some files to npm ignore
that don't need to be in the npm package
2022-07-05 15:36:45 +02:00
Simon Laux
e12aeb7bd8 add json api to cffi and expose it in dc node 2022-07-05 15:36:45 +02:00
Simon Laux
346fab7f26 fix compile after rebase 2022-07-05 15:36:45 +02:00
Simon Laux
6f6e6f24c9 change now returns event names as id
directly, no conversion method or number ids anymore

also longer timeout for requesting test accounts from mailadm
2022-07-05 15:36:45 +02:00
Simon Laux
079cd67da8 update .gitignore 2022-07-05 15:36:44 +02:00
Simon Laux
bfd97fdb05 fix formatting
make test  pass
fix clippy
2022-07-05 15:36:44 +02:00
Simon Laux
688326f21c refactor function name 2022-07-05 15:36:44 +02:00
Simon Laux
cc20d25b8d fix get_provider_info docs 2022-07-05 15:36:44 +02:00
Simon Laux
99d50615c3 use node 16 in ci
use `npm i` instead of `npm ci`
try fix ci script
and fix a doc comment
2022-07-05 15:36:44 +02:00
Simon Laux
a006825376 fix clippy 2022-07-05 15:36:44 +02:00
Simon Laux
c90fd1c9ce get target dir from cargo 2022-07-05 15:36:44 +02:00
Simon Laux
bf5d09e74a integrate json-rpc repo
https://github.com/deltachat/deltachat-jsonrpc
2022-07-05 15:36:44 +02:00
55 changed files with 4143 additions and 22 deletions

View File

@@ -131,7 +131,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: -p deltachat_ffi
args: -p deltachat_ffi --features jsonrpc
- name: run python tests
if: ${{ matrix.python }}

45
.github/workflows/jsonrpc.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: JSON-RPC API Test
on:
push:
branches: [master]
pull_request:
branches: [master]
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Add Rust cache
uses: Swatinem/rust-cache@v1.3.0
- name: npm install
run: |
cd deltachat-jsonrpc/typescript
npm install
- name: Build TypeScript, run Rust tests, generate bindings
run: |
cd deltachat-jsonrpc/typescript
npm run build
- name: Run integration tests
run: |
cd deltachat-jsonrpc/typescript
npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: Run linter
run: |
cd deltachat-jsonrpc/typescript
npm run prettier:check

View File

@@ -40,3 +40,17 @@ node/old_docs.md
.vscode/
.github/
node/.prettierrc.yml
deltachat-jsonrpc/TODO.md
deltachat-jsonrpc/README.MD
deltachat-jsonrpc/.gitignore
deltachat-jsonrpc/typescript/.gitignore
deltachat-jsonrpc/typescript/.prettierignore
deltachat-jsonrpc/typescript/accounts/
deltachat-jsonrpc/typescript/index.html
deltachat-jsonrpc/typescript/node-demo.js
deltachat-jsonrpc/typescript/report_api_coverage.mjs
deltachat-jsonrpc/typescript/test
deltachat-jsonrpc/typescript/example.ts
.DS_Store

View File

@@ -2,10 +2,22 @@
## Unreleased
### API-Changes
- jsonrpc api over websocket server (basically a new api next to the cffi) #3463
- jsonrpc methods in cffi #3463:
- `dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);`
- `void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);`
- `void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, char* request);`
- `char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);`
- node: json rpc methods #3463:
- `AccountManager.prototype.startJsonRpcHandler(callback: ((response: string) => void)): void`
- `AccountManager.prototype.jsonRpcRequest(message: string): void`
### Changes
- Implemented "Automatic e-mail address Porting" (AEAP). You can
configure a new address in DC now, and when receivers get messages
they will automatically recognize your moving to a new address. #3385
- added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463
- switch from `async-std` to `tokio` as the async runtime #3449
- upgrade to `pgp@0.8.0` #3467
- add IMAP ID extension support #3468

View File

@@ -21,7 +21,7 @@ add_custom_command(
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --release --no-default-features
${CARGO} build --release --no-default-features --features jsonrpc
# Build in `deltachat-ffi` directory instead of using
# `--package deltachat_ffi` to avoid feature resolver version

373
Cargo.lock generated
View File

@@ -134,6 +134,15 @@ dependencies = [
"tokio",
]
[[package]]
name = "async-mutex"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e"
dependencies = [
"event-listener",
]
[[package]]
name = "async-native-tls"
version = "0.4.0"
@@ -216,6 +225,54 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2cc6e8e8c993cb61a005fab8c1e5093a29199b7253b05a6883999312935c1ff"
dependencies = [
"async-trait",
"axum-core",
"base64 0.13.0",
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
"hyper",
"itoa 1.0.2",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"sha-1 0.10.0",
"sync_wrapper",
"tokio",
"tokio-tungstenite",
"tower",
"tower-http",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4d047478b986f14a13edad31a009e2e05cb241f9805d0d75e4cba4e129ad4d"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"mime",
]
[[package]]
name = "backtrace"
version = "0.3.65"
@@ -512,6 +569,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3"
[[package]]
name = "convert_case"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
[[package]]
name = "core-foundation"
version = "0.9.3"
@@ -706,8 +769,28 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.10.2",
"darling_macro 0.10.2",
]
[[package]]
name = "darling"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
"darling_core 0.13.4",
"darling_macro 0.13.4",
]
[[package]]
name = "darling"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
dependencies = [
"darling_core 0.14.1",
"darling_macro 0.14.1",
]
[[package]]
@@ -720,7 +803,35 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"strsim 0.9.3",
"syn",
]
[[package]]
name = "darling_core"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn",
]
[[package]]
name = "darling_core"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn",
]
@@ -730,7 +841,29 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
dependencies = [
"darling_core",
"darling_core 0.10.2",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
"darling_core 0.13.4",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
dependencies = [
"darling_core 0.14.1",
"quote",
"syn",
]
@@ -822,6 +955,26 @@ dependencies = [
"uuid 1.1.2",
]
[[package]]
name = "deltachat-jsonrpc"
version = "1.86.0"
dependencies = [
"anyhow",
"async-channel",
"axum",
"deltachat",
"env_logger 0.9.0",
"futures",
"log",
"num-traits",
"serde",
"serde_json",
"tempfile",
"tokio",
"typescript-type-def",
"yerpc",
]
[[package]]
name = "deltachat_derive"
version = "2.0.0"
@@ -836,6 +989,7 @@ version = "1.87.0"
dependencies = [
"anyhow",
"deltachat",
"deltachat-jsonrpc",
"human-panic",
"libc",
"num-traits",
@@ -863,7 +1017,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
dependencies = [
"darling",
"darling 0.10.2",
"derive_builder_core",
"proc-macro2",
"quote",
@@ -876,7 +1030,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
dependencies = [
"darling",
"darling 0.10.2",
"proc-macro2",
"quote",
"syn",
@@ -1130,7 +1284,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [
"atty",
"humantime",
"humantime 1.3.0",
"log",
"regex",
"termcolor",
]
[[package]]
name = "env_logger"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"atty",
"humantime 2.1.0",
"log",
"regex",
"termcolor",
@@ -1544,6 +1711,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"
[[package]]
name = "httparse"
version = "1.7.1"
@@ -1586,6 +1759,12 @@ dependencies = [
"quick-error 1.2.3",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.19"
@@ -1880,6 +2059,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "matchit"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
[[package]]
name = "md-5"
version = "0.9.1"
@@ -2406,7 +2591,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
dependencies = [
"env_logger",
"env_logger 0.7.1",
"log",
]
@@ -3045,6 +3230,15 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.5.0"
@@ -3125,6 +3319,12 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
@@ -3161,6 +3361,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
[[package]]
name = "synstructure"
version = "0.12.6"
@@ -3292,6 +3498,7 @@ dependencies = [
"once_cell",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"winapi",
@@ -3344,6 +3551,18 @@ dependencies = [
"xattr",
]
[[package]]
name = "tokio-tungstenite"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06cda1232a49558c46f8a504d5b93101d42c0bf7f911f12a105ba48168f821ae"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
]
[[package]]
name = "tokio-util"
version = "0.7.3"
@@ -3367,6 +3586,47 @@ dependencies = [
"serde",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62"
[[package]]
name = "tower-service"
version = "0.3.2"
@@ -3380,10 +3640,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.28"
@@ -3444,6 +3717,25 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "tungstenite"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5"
dependencies = [
"base64 0.13.0",
"byteorder",
"bytes",
"http",
"httparse",
"log",
"rand 0.8.5",
"sha-1 0.10.0",
"thiserror",
"url",
"utf-8",
]
[[package]]
name = "twofish"
version = "0.6.0"
@@ -3461,6 +3753,30 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "typescript-type-def"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b4d7f4a1cf8923b722bd56456e0dd560869a37fb849169f18b848f261f01b96"
dependencies = [
"serde_json",
"typescript-type-def-derive",
]
[[package]]
name = "typescript-type-def-derive"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a8b246d662eb07a8627513dc78bad0b79da6d05e8224ecd559963efc597af0"
dependencies = [
"darling 0.13.4",
"ident_case",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"
@@ -3521,6 +3837,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.0"
@@ -3809,6 +4131,41 @@ dependencies = [
"libc",
]
[[package]]
name = "yerpc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1baa6fce4cf16d1cff91b557baceac3e363106f66e555fff906a7f82dce8153"
dependencies = [
"anyhow",
"async-channel",
"async-mutex",
"async-trait",
"axum",
"futures",
"futures-util",
"log",
"serde",
"serde_json",
"tokio",
"tracing",
"typescript-type-def",
"yerpc_derive",
]
[[package]]
name = "yerpc_derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f944ca6789bc55ddc86839478f6d49c9d2a66e130f69fd1f8d171b3108990"
dependencies = [
"convert_case",
"darling 0.14.1",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zeroize"
version = "1.5.6"

View File

@@ -95,6 +95,7 @@ tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"]
members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc"
]
[[example]]

View File

@@ -16,6 +16,7 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = "0.2"
human-panic = "1"
num-traits = "0.2"
@@ -30,4 +31,5 @@ once_cell = "1.12.0"
default = ["vendored"]
vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"]
jsonrpc = ["deltachat-jsonrpc"]

View File

@@ -23,7 +23,7 @@ typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
/**
* @mainpage Getting started
@@ -5178,6 +5178,55 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
/**
* @class dc_jsonrpc_instance_t
*
* Opaque object for using the json rpc api from the cffi bindings.
*/
/**
* Create the jsonrpc instance that is used to call the jsonrpc.
*
* @memberof dc_accounts_t
* @param account_manager The accounts object as created by dc_accounts_new().
* @return Returns the jsonrpc instance, NULL on errors.
* Must be freed using dc_jsonrpc_unref() after usage.
*
*/
dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);
/**
* Free a jsonrpc instance.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* If NULL is given, nothing is done and an error is logged.
*/
void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Makes an asynchronous jsonrpc request,
* returns immediately and once the result is ready it can be retrieved via dc_jsonrpc_next_response()
* the jsonrpc specification defines an invocation id that can then be used to match request and response.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param request JSON-RPC request as string
*/
void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* request);
/**
* Get the next json_rpc response, blocks until there is a new event, so call this in a loop from a thread.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* If NULL is returned, the accounts_t belonging to the jsonrpc instance is unref'd and no more events will come;
* in this case, free the jsonrpc instance using dc_jsonrpc_unref().
*/
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* @class dc_event_emitter_t
*

View File

@@ -19,6 +19,7 @@ use std::future::Future;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
@@ -4108,11 +4109,11 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
/// `dc_accounts_t` in multiple threads at once.
pub struct AccountsWrapper {
inner: RwLock<Accounts>,
inner: Arc<RwLock<Accounts>>,
}
impl Deref for AccountsWrapper {
type Target = RwLock<Accounts>;
type Target = Arc<RwLock<Accounts>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -4121,7 +4122,7 @@ impl Deref for AccountsWrapper {
impl AccountsWrapper {
fn new(accounts: Accounts) -> Self {
let inner = RwLock::new(accounts);
let inner = Arc::new(RwLock::new(accounts));
Self { inner }
}
}
@@ -4437,3 +4438,77 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
}
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use super::*;
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *mut dc_accounts_t,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
}
let cmd_api =
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };
Box::into_raw(Box::new(instance))
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_unref(jsonrpc_instance: *mut dc_jsonrpc_instance_t) {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
Box::from_raw(jsonrpc_instance);
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_request(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
request: *const libc::c_char,
) {
if jsonrpc_instance.is_null() || request.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_request()");
return;
}
let api = &*jsonrpc_instance;
let handle = &api.handle;
let request = to_string_lossy(request);
spawn(async move {
handle.handle_incoming(&request).await;
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_next_response(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_next_response()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
block_on(api.receiver.recv())
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
}

3
deltachat-jsonrpc/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
accounts/
.cargo

View File

@@ -0,0 +1,39 @@
[package]
name = "deltachat-jsonrpc"
version = "1.86.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
default-run = "deltachat-jsonrpc-server"
license = "MPL-2.0"
[[bin]]
name = "deltachat-jsonrpc-server"
path = "src/webserver.rs"
required-features = ["webserver"]
[dependencies]
anyhow = "1"
deltachat = { path = ".." }
num-traits = "0.2"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.6.1" }
futures = { version = "0.3.19" }
serde_json = "1.0.75"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.19.2" }
# optional dependencies
axum = { version = "0.5.9", optional = true, features = ["ws"] }
env_logger = { version = "0.9.0", optional = true }
[dev-dependencies]
tokio = { version = "1.19.2", features = ["full", "rt-multi-thread"] }
[features]
default = []
webserver = ["env_logger", "axum", "tokio/full", "yerpc/support-axum"]

123
deltachat-jsonrpc/README.md Normal file
View File

@@ -0,0 +1,123 @@
# deltachat-jsonrpc
This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) interface to DeltaChat.
The JSON-RPC API is exposed in two fashions:
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
## Usage
#### Running the WebSocket server
From within this folder, you can start the WebSocket server with the following command:
```sh
cargo run --features webserver
```
If you want to use the server in a production setup, first build it in release mode:
```sh
cargo build --features webserver --release
```
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
The server can be configured with environment variables:
|variable|default|description|
|-|-|-|
|`DC_PORT`|`20808`|port to listen on|
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
If you are targetting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
```sh
cross build --features=webserver --target armv7-linux-androideabi --release
```
#### Using the TypeScript/JavaScript client
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
```sh
cd typescript
npm install
npm run build
```
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
```typescript
import { DeltaChat } from './deltachat.bundle.js'
const dc = new DeltaChat('ws://localhost:20808/ws')
const accounts = await dc.rpc.getAllAccounts()
console.log('accounts', accounts)
```
A script is included to build autogenerated documentation, which includes all RPC methods:
```sh
cd typescript
npm run docs
```
Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
## Development
#### Running the example app
We include a small demo web application that talks to the WebSocket server. It can be used for testing. Feel invited to expand this.
```sh
cd typescript
npm run build
npm run example:build
npm run example:start
```
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
Run `npm run example:dev` to live-rebuild the example app when files changes.
### Testing
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
#### Rust tests
To run the Rust test, use this command:
```
cargo test
```
#### TypeScript tests
```
cd typescript
npm run test
```
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
Then, set the `DCC_NEW_TMP_EMAIL` environment variable to your mailadm token before running the tests.
```
DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=yourtoken npm run test
```
#### Test Coverage
Running `npm run test` will report test coverage. For the coverage to be accurate the online tests need to be run.
> If you are offline and want to see the coverage results anyway (even though they are inaccurate), you can bypass the errors of the online tests by setting the `COVERAGE_OFFLINE=1` environment variable.
A summary of the coverage will be reported in the terminal after the test run. Open `coverage/index.html` in a web browser for a detailed report.

28
deltachat-jsonrpc/TODO.md Normal file
View File

@@ -0,0 +1,28 @@
# TODO
- [ ] different test type to simulate two devices: to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer
## MVP - Websocket server&client
For kaiOS and other experiments, like a deltachat "web" over network from an android phone.
- [ ] coverage for a majority of the API
- [ ] Blobs served
- [ ] Blob upload (for attachments, setting profile-picture, importing backup and so on)
- [ ] other way blobs can be addressed when using websocket vs. jsonrpc over dc-node
- [ ] Web push API? At least some kind of notification hook closure this lib can accept.
### Other Ideas for the Websocket server
- [ ] make sure there can only be one connection at a time to the ws
- why? , it could give problems if its commanded from multiple connections
- [ ] encrypted connection?
- [ ] authenticated connection?
- [ ] Look into unit-testing for the proc macros?
- [ ] proc macro taking over doc comments to generated typescript file
## Desktop Apis
Incomplete todo for desktop api porting, just some remainders for points that might need more work:
- [ ] manual start/stop io functions in the api for context and accounts, so "not syncing all accounts" can still be done in desktop -> webserver should then not do start io on all accounts by default

View File

@@ -0,0 +1,157 @@
use deltachat::{Event, EventType};
use serde::Serialize;
use serde_json::{json, Value};
use typescript_type_def::TypeDef;
pub fn event_to_json_rpc_notification(event: Event) -> Value {
let (field1, field2): (Value, Value) = match &event.typ {
// events with a single string in field1
EventType::Info(txt)
| EventType::SmtpConnected(txt)
| EventType::ImapConnected(txt)
| EventType::SmtpMessageSent(txt)
| EventType::ImapMessageDeleted(txt)
| EventType::ImapMessageMoved(txt)
| EventType::NewBlobFile(txt)
| EventType::DeletedBlobFile(txt)
| EventType::Warning(txt)
| EventType::Error(txt)
| EventType::ErrorSelfNotInGroup(txt) => (json!(txt), Value::Null),
EventType::ImexFileWritten(path) => (json!(path.to_str()), Value::Null),
// single number
EventType::MsgsNoticed(chat_id) | EventType::ChatModified(chat_id) => {
(json!(chat_id), Value::Null)
}
EventType::ImexProgress(progress) => (json!(progress), Value::Null),
// both fields contain numbers
EventType::MsgsChanged { chat_id, msg_id }
| EventType::IncomingMsg { chat_id, msg_id }
| EventType::MsgDelivered { chat_id, msg_id }
| EventType::MsgFailed { chat_id, msg_id }
| EventType::MsgRead { chat_id, msg_id } => (json!(chat_id), json!(msg_id)),
EventType::ChatEphemeralTimerModified { chat_id, timer } => (json!(chat_id), json!(timer)),
EventType::SecurejoinInviterProgress {
contact_id,
progress,
}
| EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => (json!(contact_id), json!(progress)),
// field 1 number or null
EventType::ContactsChanged(maybe_number) | EventType::LocationChanged(maybe_number) => (
match maybe_number {
Some(number) => json!(number),
None => Value::Null,
},
Value::Null,
),
// number and maybe string
EventType::ConfigureProgress { progress, comment } => (
json!(progress),
match comment {
Some(content) => json!(content),
None => Value::Null,
},
),
EventType::ConnectivityChanged => (Value::Null, Value::Null),
EventType::SelfavatarChanged => (Value::Null, Value::Null),
EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
} => (json!(msg_id), json!(status_update_serial)),
};
let id: EventTypeName = event.typ.into();
json!({
"id": id,
"contextId": event.id,
"field1": field1,
"field2": field2
})
}
#[derive(Serialize, TypeDef)]
pub enum EventTypeName {
Info,
SmtpConnected,
ImapConnected,
SmtpMessageSent,
ImapMessageDeleted,
ImapMessageMoved,
NewBlobFile,
DeletedBlobFile,
Warning,
Error,
ErrorSelfNotInGroup,
MsgsChanged,
IncomingMsg,
MsgsNoticed,
MsgDelivered,
MsgFailed,
MsgRead,
ChatModified,
ChatEphemeralTimerModified,
ContactsChanged,
LocationChanged,
ConfigureProgress,
ImexProgress,
ImexFileWritten,
SecurejoinInviterProgress,
SecurejoinJoinerProgress,
ConnectivityChanged,
SelfavatarChanged,
WebxdcStatusUpdate,
}
impl From<EventType> for EventTypeName {
fn from(event: EventType) -> Self {
use EventTypeName::*;
match event {
EventType::Info(_) => Info,
EventType::SmtpConnected(_) => SmtpConnected,
EventType::ImapConnected(_) => ImapConnected,
EventType::SmtpMessageSent(_) => SmtpMessageSent,
EventType::ImapMessageDeleted(_) => ImapMessageDeleted,
EventType::ImapMessageMoved(_) => ImapMessageMoved,
EventType::NewBlobFile(_) => NewBlobFile,
EventType::DeletedBlobFile(_) => DeletedBlobFile,
EventType::Warning(_) => Warning,
EventType::Error(_) => Error,
EventType::ErrorSelfNotInGroup(_) => ErrorSelfNotInGroup,
EventType::MsgsChanged { .. } => MsgsChanged,
EventType::IncomingMsg { .. } => IncomingMsg,
EventType::MsgsNoticed(_) => MsgsNoticed,
EventType::MsgDelivered { .. } => MsgDelivered,
EventType::MsgFailed { .. } => MsgFailed,
EventType::MsgRead { .. } => MsgRead,
EventType::ChatModified(_) => ChatModified,
EventType::ChatEphemeralTimerModified { .. } => ChatEphemeralTimerModified,
EventType::ContactsChanged(_) => ContactsChanged,
EventType::LocationChanged(_) => LocationChanged,
EventType::ConfigureProgress { .. } => ConfigureProgress,
EventType::ImexProgress(_) => ImexProgress,
EventType::ImexFileWritten(_) => ImexFileWritten,
EventType::SecurejoinInviterProgress { .. } => SecurejoinInviterProgress,
EventType::SecurejoinJoinerProgress { .. } => SecurejoinJoinerProgress,
EventType::ConnectivityChanged => ConnectivityChanged,
EventType::SelfavatarChanged => SelfavatarChanged,
EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate,
}
}
}
#[cfg(test)]
#[test]
fn generate_events_ts_types_definition() {
let events = {
let mut buf = Vec::new();
let options = typescript_type_def::DefinitionFileOptions {
root_namespace: None,
..typescript_type_def::DefinitionFileOptions::default()
};
typescript_type_def::write_definition_file::<_, EventTypeName>(&mut buf, options).unwrap();
String::from_utf8(buf).unwrap()
};
std::fs::write("typescript/generated/events.ts", events).unwrap();
}

View File

@@ -0,0 +1,691 @@
use anyhow::{anyhow, bail, Context, Result};
use deltachat::{
chat::{get_chat_media, get_chat_msgs, ChatId},
chatlist::Chatlist,
config::Config,
contact::{may_be_valid_addr, Contact, ContactId},
context::get_info,
message::{Message, MsgId, Viewtype},
provider::get_provider_info,
qr::{self, Qr},
webxdc::StatusUpdateSerial,
};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};
use tokio::sync::RwLock;
use yerpc::rpc;
pub use deltachat::accounts::Accounts;
pub mod events;
pub mod types;
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::QrObject;
use types::account::Account;
use types::chat::FullChat;
use types::chat_list::ChatListEntry;
use types::contact::ContactObject;
use types::message::MessageObject;
use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageViewtype;
#[derive(Clone, Debug)]
pub struct CommandApi {
pub(crate) accounts: Arc<RwLock<Accounts>>,
}
impl CommandApi {
pub fn new(accounts: Accounts) -> Self {
CommandApi {
accounts: Arc::new(RwLock::new(accounts)),
}
}
#[allow(dead_code)]
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
CommandApi { accounts }
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
let sc = self
.accounts
.read()
.await
.get_account(id)
.await
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
Ok(sc)
}
}
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
// ---------------------------------------------
// Misc top level functions
// ---------------------------------------------
/// Check if an email address is valid.
async fn check_email_validity(&self, email: String) -> bool {
may_be_valid_addr(&email)
}
/// Get general system info.
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
get_info()
}
// ---------------------------------------------
// Account Management
// ---------------------------------------------
async fn add_account(&self) -> Result<u32> {
self.accounts.write().await.add_account().await
}
async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts.write().await.remove_account(account_id).await
}
async fn get_all_account_ids(&self) -> Vec<u32> {
self.accounts.read().await.get_all().await
}
/// Select account id for internally selected state.
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn select_account(&self, id: u32) -> Result<()> {
self.accounts.write().await.select_account(id).await
}
/// Get the selected account id of the internal state..
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn get_selected_account_id(&self) -> Option<u32> {
self.accounts.read().await.get_selected_account_id().await
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
for id in self.accounts.read().await.get_all().await {
let context_option = self.accounts.read().await.get_account(id).await;
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
} else {
println!("account with id {} doesn't exist anymore", id);
}
}
Ok(accounts)
}
// ---------------------------------------------
// Methods that work on individual accounts
// ---------------------------------------------
/// Get top-level info for an account.
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
let context_option = self.accounts.read().await.get_account(account_id).await;
if let Some(ctx) = context_option {
Ok(Account::from_context(&ctx, account_id).await?)
} else {
Err(anyhow!(
"account with id {} doesn't exist anymore",
account_id
))
}
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
async fn get_provider_info(
&self,
account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let socks5_enabled = ctx
.get_config_bool(deltachat::config::Config::Socks5Enabled)
.await?;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
/// Checks if the context is already configured.
async fn is_configured(&self, account_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
ctx.is_configured().await
}
/// Get system info for an account.
async fn get_info(&self, account_id: u32) -> Result<BTreeMap<&'static str, String>> {
let ctx = self.get_context(account_id).await?;
ctx.get_info().await
}
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
set_config(&ctx, &key, value.as_deref()).await
}
async fn batch_set_config(
&self,
account_id: u32,
config: HashMap<String, Option<String>>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
for (key, value) in config.into_iter() {
set_config(&ctx, &key, value.as_deref())
.await
.with_context(|| format!("Can't set {} to {:?}", key, value))?;
}
Ok(())
}
/// Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
/// Before this function is called, dc_check_qr() should confirm the type of the
/// QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
///
/// Internally, the function will call dc_set_config() with the appropriate keys,
/// e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
/// or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
async fn set_config_from_qr(&self, account_id: u32, qr_content: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
qr::set_config_from_qr(&ctx, &qr_content).await
}
async fn check_qr(&self, account_id: u32, qr_content: String) -> Result<QrObject> {
let ctx = self.get_context(account_id).await?;
let qr = qr::check_qr(&ctx, &qr_content).await?;
let qr_object = QrObject::from(qr);
Ok(qr_object)
}
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
get_config(&ctx, &key).await
}
async fn batch_get_config(
&self,
account_id: u32,
keys: Vec<String>,
) -> Result<HashMap<String, Option<String>>> {
let ctx = self.get_context(account_id).await?;
let mut result: HashMap<String, Option<String>> = HashMap::new();
for key in keys {
result.insert(key.clone(), get_config(&ctx, &key).await?);
}
Ok(result)
}
/// Configures this account with the currently set parameters.
/// Setup the credential config before calling this.
async fn configure(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
let result = ctx.configure().await;
if result.is_err() {
if let Ok(true) = ctx.is_configured().await {
ctx.start_io().await;
}
return result;
}
ctx.start_io().await;
Ok(())
}
/// Signal an ongoing process to stop.
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_ongoing().await;
Ok(())
}
/// Returns the message IDs of all _fresh_ messages of any chat.
/// Typically used for implementing notification summaries
/// or badge counters e.g. on the app icon.
/// The list is already sorted and starts with the most recent fresh message.
///
/// Messages belonging to muted chats or to the contact requests are not returned;
/// these messages should not be notified
/// and also badge counters should not include these messages.
///
/// To get the number of fresh messages for a single chat, muted or not,
/// use `get_fresh_msg_cnt()`.
async fn get_fresh_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx
.get_fresh_msgs()
.await?
.iter()
.map(|msg_id| msg_id.to_u32())
.collect())
}
/// Get the number of _fresh_ messages in a chat.
/// Typically used to implement a badge with a number in the chatlist.
///
/// If the specified chat is muted,
/// the UI should show the badge counter "less obtrusive",
/// e.g. using "gray" instead of "red" color.
async fn get_fresh_msg_cnt(&self, account_id: u32, chat_id: u32) -> Result<usize> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
}
// ---------------------------------------------
// autocrypt
// ---------------------------------------------
async fn autocrypt_initiate_key_transfer(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
deltachat::imex::initiate_key_transfer(&ctx).await
}
async fn autocrypt_continue_key_transfer(
&self,
account_id: u32,
message_id: u32,
setup_code: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
deltachat::imex::continue_key_transfer(&ctx, MsgId::new(message_id), &setup_code).await
}
// ---------------------------------------------
// chat list
// ---------------------------------------------
async fn get_chatlist_entries(
&self,
account_id: u32,
list_flags: Option<u32>,
query_string: Option<String>,
query_contact_id: Option<u32>,
) -> Result<Vec<ChatListEntry>> {
let ctx = self.get_context(account_id).await?;
let list = Chatlist::try_load(
&ctx,
list_flags.unwrap_or(0) as usize,
query_string.as_deref(),
query_contact_id.map(ContactId::new),
)
.await?;
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
for i in 0..list.len() {
l.push(ChatListEntry(
list.get_chat_id(i)?.to_u32(),
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
));
}
Ok(l)
}
async fn get_chatlist_items_by_entries(
&self,
account_id: u32,
entries: Vec<ChatListEntry>,
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
// todo custom json deserializer for ChatListEntry?
let ctx = self.get_context(account_id).await?;
let mut result: HashMap<u32, ChatListItemFetchResult> =
HashMap::with_capacity(entries.len());
for entry in entries.iter() {
result.insert(
entry.0,
match get_chat_list_item_by_id(&ctx, entry).await {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
id: entry.0,
error: format!("{:?}", err),
},
},
);
}
Ok(result)
}
// ---------------------------------------------
// chat
// ---------------------------------------------
async fn chatlist_get_full_chat_by_id(
&self,
account_id: u32,
chat_id: u32,
) -> Result<FullChat> {
let ctx = self.get_context(account_id).await?;
FullChat::try_from_dc_chat_id(&ctx, chat_id).await
}
async fn accept_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).accept(&ctx).await
}
async fn block_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).block(&ctx).await
}
// for now only text messages, because we only used text messages in desktop thusfar
async fn add_device_message(
&self,
account_id: u32,
label: String,
text: String,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(text));
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
Ok(message_id.to_u32())
}
// ---------------------------------------------
// message list
// ---------------------------------------------
async fn message_list_get_message_ids(
&self,
account_id: u32,
chat_id: u32,
flags: u32,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs(&ctx, ChatId::new(chat_id), flags).await?;
Ok(msg
.iter()
.filter_map(|chat_item| match chat_item {
deltachat::chat::ChatItem::Message { msg_id } => Some(msg_id.to_u32()),
_ => None,
})
.collect())
}
async fn message_get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
MessageObject::from_message_id(&ctx, message_id).await
}
async fn message_get_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
) -> Result<HashMap<u32, MessageObject>> {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageObject> = HashMap::new();
for message_id in message_ids {
messages.insert(
message_id,
MessageObject::from_message_id(&ctx, message_id).await?,
);
}
Ok(messages)
}
// ---------------------------------------------
// contact
// ---------------------------------------------
/// Get a single contact options by ID.
async fn contacts_get_contact(
&self,
account_id: u32,
contact_id: u32,
) -> Result<ContactObject> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, contact_id).await?,
)
.await
}
/// Add a single contact as a result of an explicit user action.
///
/// Returns contact id of the created or existing contact
async fn contacts_create_contact(
&self,
account_id: u32,
email: String,
name: Option<String>,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
if !may_be_valid_addr(&email) {
bail!(anyhow!(
"provided email address is not a valid email address"
))
}
let contact_id = Contact::create(&ctx, &name.unwrap_or_default(), &email).await?;
Ok(contact_id.to_u32())
}
/// Returns contact id of the created or existing DM chat with that contact
async fn contacts_create_chat_by_contact_id(
&self,
account_id: u32,
contact_id: u32,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let contact = Contact::get_by_id(&ctx, ContactId::new(contact_id)).await?;
ChatId::create_for_contact(&ctx, contact.id)
.await
.map(|id| id.to_u32())
}
async fn contacts_block(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::block(&ctx, ContactId::new(contact_id)).await
}
async fn contacts_unblock(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::unblock(&ctx, ContactId::new(contact_id)).await
}
async fn contacts_get_blocked(&self, account_id: u32) -> Result<Vec<ContactObject>> {
let ctx = self.get_context(account_id).await?;
let blocked_ids = Contact::get_all_blocked(&ctx).await?;
let mut contacts: Vec<ContactObject> = Vec::with_capacity(blocked_ids.len());
for id in blocked_ids {
contacts.push(
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, id).await?,
)
.await?,
);
}
Ok(contacts)
}
async fn contacts_get_contact_ids(
&self,
account_id: u32,
list_flags: u32,
query: Option<String>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let contacts = Contact::get_all(&ctx, list_flags, query.as_deref()).await?;
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
}
/// Get a list of contacts.
/// (formerly called getContacts2 in desktop)
async fn contacts_get_contacts(
&self,
account_id: u32,
list_flags: u32,
query: Option<String>,
) -> Result<Vec<ContactObject>> {
let ctx = self.get_context(account_id).await?;
let contact_ids = Contact::get_all(&ctx, list_flags, query.as_deref()).await?;
let mut contacts: Vec<ContactObject> = Vec::with_capacity(contact_ids.len());
for id in contact_ids {
contacts.push(
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, id).await?,
)
.await?,
);
}
Ok(contacts)
}
async fn contacts_get_contacts_by_ids(
&self,
account_id: u32,
ids: Vec<u32>,
) -> Result<HashMap<u32, ContactObject>> {
let ctx = self.get_context(account_id).await?;
let mut contacts = HashMap::with_capacity(ids.len());
for id in ids {
contacts.insert(
id,
ContactObject::try_from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, ContactId::new(id)).await?,
)
.await?,
);
}
Ok(contacts)
}
// ---------------------------------------------
// chat
// ---------------------------------------------
/// Returns all message IDs of the given types in a chat.
/// Typically used to show a gallery.
///
/// The list is already sorted and starts with the oldest message.
/// Clients should not try to re-sort the list as this would be an expensive action
/// and would result in inconsistencies between clients.
async fn chat_get_media(
&self,
account_id: u32,
chat_id: u32,
message_type: MessageViewtype,
or_message_type2: Option<MessageViewtype>,
or_message_type3: Option<MessageViewtype>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg_type = message_type.into();
let or_msg_type2 = or_message_type2.map_or(Viewtype::Unknown, |v| v.into());
let or_msg_type3 = or_message_type3.map_or(Viewtype::Unknown, |v| v.into());
let media = get_chat_media(
&ctx,
ChatId::new(chat_id),
msg_type,
or_msg_type2,
or_msg_type3,
)
.await?;
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
}
// ---------------------------------------------
// webxdc
// ---------------------------------------------
async fn webxdc_send_status_update(
&self,
account_id: u32,
instance_msg_id: u32,
update_str: String,
description: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
.await
}
async fn webxdc_get_status_updates(
&self,
account_id: u32,
instance_msg_id: u32,
last_known_serial: u32,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ctx.get_webxdc_status_updates(
MsgId::new(instance_msg_id),
StatusUpdateSerial::new(last_known_serial),
)
.await
}
/// Get info from a webxdc message
async fn message_get_webxdc_info(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<WebxdcMessageInfo> {
let ctx = self.get_context(account_id).await?;
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
}
// ---------------------------------------------
// misc prototyping functions
// that might get removed later again
// ---------------------------------------------
/// Returns the messageid of the sent message
async fn misc_send_text_message(
&self,
account_id: u32,
text: String,
chat_id: u32,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(text));
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
}
// Helper functions (to prevent code duplication)
async fn set_config(
ctx: &deltachat::context::Context,
key: &str,
value: Option<&str>,
) -> Result<(), anyhow::Error> {
if key.starts_with("ui.") {
ctx.set_ui_config(key, value).await
} else {
ctx.set_config(Config::from_str(key).context("unknown key")?, value)
.await
}
}
async fn get_config(
ctx: &deltachat::context::Context,
key: &str,
) -> Result<Option<String>, anyhow::Error> {
if key.starts_with("ui.") {
ctx.get_ui_config(key).await
} else {
ctx.get_config(Config::from_str(key).context("unknown key")?)
.await
}
}

View File

@@ -0,0 +1,47 @@
use anyhow::Result;
use deltachat::config::Config;
use deltachat::contact::{Contact, ContactId};
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
pub enum Account {
#[serde(rename_all = "camelCase")]
Configured {
id: u32,
display_name: Option<String>,
addr: Option<String>,
// size: u32,
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
color: String,
},
#[serde(rename_all = "camelCase")]
Unconfigured {
id: u32,
},
}
impl Account {
pub async fn from_context(ctx: &deltachat::context::Context, id: u32) -> Result<Self> {
if ctx.is_configured().await? {
let display_name = ctx.get_config(Config::Displayname).await?;
let addr = ctx.get_config(Config::Addr).await?;
let profile_image = ctx.get_config(Config::Selfavatar).await?;
let color = color_int_to_hex_string(
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
);
Ok(Account::Configured {
id,
display_name,
addr,
profile_image,
color,
})
} else {
Ok(Account::Unconfigured { id })
}
}
}

View File

@@ -0,0 +1,92 @@
use anyhow::{anyhow, Result};
use deltachat::chat::get_chat_contacts;
use deltachat::chat::{Chat, ChatId};
use deltachat::contact::{Contact, ContactId};
use deltachat::context::Context;
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct FullChat {
id: u32,
name: String,
is_protected: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
// subtitle - will be moved to frontend because it uses translation functions
chat_type: u32,
is_unpromoted: bool,
is_self_talk: bool,
contacts: Vec<ContactObject>,
contact_ids: Vec<u32>,
color: String,
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
is_device_chat: bool,
self_in_group: bool,
is_muted: bool,
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
can_send: bool,
}
impl FullChat {
pub async fn try_from_dc_chat_id(context: &Context, chat_id: u32) -> Result<Self> {
let rust_chat_id = ChatId::new(chat_id);
let chat = Chat::load_from_db(context, rust_chat_id).await?;
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
let mut contacts = Vec::with_capacity(contact_ids.len());
for contact_id in &contact_ids {
contacts.push(
ContactObject::try_from_dc_contact(
context,
Contact::load_from_db(context, *contact_id).await?,
)
.await?,
)
}
let profile_image = match chat.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let color = color_int_to_hex_string(chat.get_color(context).await?);
let fresh_message_counter = rust_chat_id.get_fresh_msg_cnt(context).await?;
let ephemeral_timer = rust_chat_id.get_ephemeral_timer(context).await?.to_u32();
let can_send = chat.can_send(context).await?;
Ok(FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == deltachat::chat::ChatVisibility::Archived,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
color,
fresh_message_counter,
is_contact_request: chat.is_contact_request(),
is_device_chat: chat.is_device_talk(),
self_in_group: contact_ids.contains(&ContactId::SELF),
is_muted: chat.is_muted(),
ephemeral_timer,
can_send,
})
}
}

View File

@@ -0,0 +1,126 @@
use anyhow::Result;
use deltachat::constants::*;
use deltachat::contact::ContactId;
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
};
use deltachat::{
chat::{Chat, ChatId},
message::MsgId,
};
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Deserialize, Serialize, TypeDef)]
pub struct ChatListEntry(pub u32, pub u32);
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
pub enum ChatListItemFetchResult {
#[serde(rename_all = "camelCase")]
ChatListItem {
id: u32,
name: String,
avatar_path: Option<String>,
color: String,
last_updated: Option<i64>,
summary_text1: String,
summary_text2: String,
summary_status: u32,
is_protected: bool,
is_group: bool,
fresh_message_counter: usize,
is_self_talk: bool,
is_device_talk: bool,
is_sending_location: bool,
is_self_in_group: bool,
is_archived: bool,
is_pinned: bool,
is_muted: bool,
is_contact_request: bool,
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
},
ArchiveLink,
#[serde(rename_all = "camelCase")]
Error {
id: u32,
error: String,
},
}
pub(crate) async fn get_chat_list_item_by_id(
ctx: &deltachat::context::Context,
entry: &ChatListEntry,
) -> Result<ChatListItemFetchResult> {
let chat_id = ChatId::new(entry.0);
let last_msgid = match entry.1 {
0 => None,
_ => Some(MsgId::new(entry.1)),
};
if chat_id.is_archived_link() {
return Ok(ChatListItemFetchResult::ArchiveLink);
}
let chat = Chat::load_from_db(ctx, chat_id).await?;
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
let summary_text2 = summary.text.to_owned();
let visibility = chat.get_visibility();
let avatar_path = chat
.get_profile_image(ctx)
.await?
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
let last_updated = match last_msgid {
Some(id) => {
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
Some(last_message.get_timestamp() * 1000)
}
None => None,
};
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
let self_in_group = chat_contacts.contains(&ContactId::SELF);
let dm_chat_contact = if chat.get_type() == Chattype::Single {
chat_contacts.get(0).map(|contact_id| contact_id.to_u32())
} else {
None
};
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
let color = color_int_to_hex_string(chat.get_color(ctx).await?);
Ok(ChatListItemFetchResult::ChatListItem {
id: chat_id.to_u32(),
name: chat.get_name().to_owned(),
avatar_path,
color,
last_updated,
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
is_protected: chat.is_protected(),
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
is_self_talk: chat.is_self_talk(),
is_device_talk: chat.is_device_talk(),
is_self_in_group: self_in_group,
is_sending_location: chat.is_sending_locations(),
is_archived: visibility == ChatVisibility::Archived,
is_pinned: visibility == ChatVisibility::Pinned,
is_muted: chat.is_muted(),
is_contact_request: chat.is_contact_request(),
dm_chat_contact,
})
}

View File

@@ -0,0 +1,50 @@
use anyhow::Result;
use deltachat::contact::VerifiedStatus;
use deltachat::context::Context;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Contact", rename_all = "camelCase")]
pub struct ContactObject {
address: String,
color: String,
auth_name: String,
status: String,
display_name: String,
id: u32,
name: String,
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
is_verified: bool,
}
impl ContactObject {
pub async fn try_from_dc_contact(
context: &Context,
contact: deltachat::contact::Contact,
) -> Result<Self> {
let profile_image = match contact.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
Ok(ContactObject {
address: contact.get_addr().to_owned(),
color: color_int_to_hex_string(contact.get_color()),
auth_name: contact.get_authname().to_owned(),
status: contact.get_status().to_owned(),
display_name: contact.get_display_name().to_owned(),
id: contact.id.to_u32(),
name: contact.get_name().to_owned(),
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_verified,
})
}
}

View File

@@ -0,0 +1,241 @@
use anyhow::{anyhow, Result};
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download::DownloadState;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
use num_traits::cast::ToPrimitive;
use serde::Deserialize;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::contact::ContactObject;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Message", rename_all = "camelCase")]
pub struct MessageObject {
id: u32,
chat_id: u32,
from_id: u32,
quoted_text: Option<String>,
quoted_message_id: Option<u32>,
text: Option<String>,
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
state: u32,
timestamp: i64,
sort_timestamp: i64,
received_timestamp: i64,
has_deviating_timestamp: bool,
// summary - use/create another function if you need it
subject: String,
show_padlock: bool,
is_setupmessage: bool,
is_info: bool,
is_forwarded: bool,
duration: i32,
dimensions_height: i32,
dimensions_width: i32,
videochat_type: Option<u32>,
videochat_url: Option<String>,
override_sender_name: Option<String>,
sender: ContactObject,
setup_code_begin: Option<String>,
file: Option<String>,
file_mime: Option<String>,
file_bytes: u64,
file_name: Option<String>,
download_state: MessageDownloadState
}
impl MessageObject {
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
let msg_id = MsgId::new(message_id);
let message = Message::load_from_db(context, msg_id).await?;
let quoted_message_id = message
.quoted_message(context)
.await?
.map(|m| m.get_id().to_u32());
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
let file_bytes = message.get_filebytes(context).await;
let override_sender_name = message.get_override_sender_name();
Ok(MessageObject {
id: message_id,
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
quoted_text: message.quoted_text(),
quoted_message_id,
text: message.get_text(),
has_location: message.has_location(),
has_html: message.has_html(),
view_type: message.get_viewtype().into(),
state: message
.get_state()
.to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
timestamp: message.get_timestamp(),
sort_timestamp: message.get_sort_timestamp(),
received_timestamp: message.get_received_timestamp(),
has_deviating_timestamp: message.has_deviating_timestamp(),
subject: message.get_subject().to_owned(),
show_padlock: message.get_showpadlock(),
is_setupmessage: message.is_setupmessage(),
is_info: message.is_info(),
is_forwarded: message.is_forwarded(),
duration: message.get_duration(),
dimensions_height: message.get_height(),
dimensions_width: message.get_width(),
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
),
None => None,
},
videochat_url: message.get_videochat_url(),
override_sender_name,
sender,
setup_code_begin: message.get_setupcodebegin(context).await,
file: match message.get_file(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
}, //BLOBS
file_mime: message.get_filemime(),
file_bytes,
file_name: message.get_filename(),
download_state: message.download_state().try_into().unwrap()
})
}
}
#[derive(Serialize, Deserialize, TypeDef)]
#[serde(rename = "Viewtype")]
pub enum MessageViewtype {
Unknown,
/// Text message.
Text,
/// Image message.
/// If the image is an animated GIF, the type `Viewtype.Gif` should be used.
Image,
/// Animated GIF message.
Gif,
/// Message containing a sticker, similar to image.
/// If possible, the ui should display the image without borders in a transparent way.
/// A click on a sticker will offer to install the sticker set in some future.
Sticker,
/// Message containing an Audio file.
Audio,
/// A voice message that was directly recorded by the user.
/// For all other audio messages, the type `Viewtype.Audio` should be used.
Voice,
/// Video messages.
Video,
/// Message containing any file, eg. a PDF.
File,
/// Message is an invitation to a videochat.
VideochatInvitation,
/// Message is an webxdc instance.
Webxdc,
}
impl From<Viewtype> for MessageViewtype {
fn from(viewtype: Viewtype) -> Self {
match viewtype {
Viewtype::Unknown => MessageViewtype::Unknown,
Viewtype::Text => MessageViewtype::Text,
Viewtype::Image => MessageViewtype::Image,
Viewtype::Gif => MessageViewtype::Gif,
Viewtype::Sticker => MessageViewtype::Sticker,
Viewtype::Audio => MessageViewtype::Audio,
Viewtype::Voice => MessageViewtype::Voice,
Viewtype::Video => MessageViewtype::Video,
Viewtype::File => MessageViewtype::File,
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
Viewtype::Webxdc => MessageViewtype::Webxdc,
}
}
}
impl From<MessageViewtype> for Viewtype {
fn from(viewtype: MessageViewtype) -> Self {
match viewtype {
MessageViewtype::Unknown => Viewtype::Unknown,
MessageViewtype::Text => Viewtype::Text,
MessageViewtype::Image => Viewtype::Image,
MessageViewtype::Gif => Viewtype::Gif,
MessageViewtype::Sticker => Viewtype::Sticker,
MessageViewtype::Audio => Viewtype::Audio,
MessageViewtype::Voice => Viewtype::Voice,
MessageViewtype::Video => Viewtype::Video,
MessageViewtype::File => Viewtype::File,
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
MessageViewtype::Webxdc => Viewtype::Webxdc,
}
}
}
#[derive(Serialize, Deserialize, TypeDef)]
#[serde(rename = "DownloadState")]
pub enum MessageDownloadState {
Done,
Available,
Failure,
InProgress
}
impl From<DownloadState> for MessageDownloadState{
fn from(download_state: DownloadState) -> Self {
match download_state {
DownloadState::Done => MessageDownloadState::Done,
DownloadState::Available => MessageDownloadState::Available,
DownloadState::Failure => MessageDownloadState::Failure,
DownloadState::InProgress => MessageDownloadState::InProgress
}
}
}
impl From<MessageDownloadState> for DownloadState {
fn from(message_download_state: MessageDownloadState) -> Self {
match message_download_state {
MessageDownloadState::Done => DownloadState::Done,
MessageDownloadState::Available => DownloadState::Available,
MessageDownloadState::Failure => DownloadState::Failure,
MessageDownloadState::InProgress => DownloadState::InProgress
}
}
}

View File

@@ -0,0 +1,118 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
pub mod account;
pub mod chat;
pub mod chat_list;
pub mod contact;
pub mod message;
pub mod provider_info;
pub mod webxdc;
pub fn color_int_to_hex_string(color: u32) -> String {
format!("{:#08x}", color).replace("0x", "#")
}
fn maybe_empty_string_to_option(string: String) -> Option<String> {
if string.is_empty() {
None
} else {
Some(string)
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename = "Qr", rename_all = "camelCase")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
AskVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
FprOk {
contact_id: u32,
},
FprMismatch {
contact_id: Option<u32>,
},
FprWithoutAddr {
fingerprint: String,
},
Account {
domain: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
},
Addr {
contact_id: u32,
},
Url {
url: String,
},
Text {
text: String,
},
WithdrawVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
WithdrawVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyContact {
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
ReviveVerifyGroup {
grpname: String,
grpid: String,
contact_id: u32,
fingerprint: String,
invitenumber: String,
authcode: String,
},
}
impl From<Qr> for QrObject {
fn from(qr: Qr) -> Self {
match qr {
Qr::Account { domain } => {
QrObject::Account { domain }
}
Qr::FprOk {
contact_id,
} => {
let contact_id = contact_id.to_u32();
QrObject::FprOk { contact_id }
}
Qr::FprWithoutAddr {
fingerprint,
} => {
let fingerprint = fingerprint.to_string();
QrObject::FprWithoutAddr { fingerprint }
}
_ => todo!()
}
}
}

View File

@@ -0,0 +1,22 @@
use deltachat::provider::Provider;
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct ProviderInfo {
pub before_login_hint: String,
pub overview_page: String,
pub status: u32, // in reality this is an enum, but for simlicity and because it gets converted into a number anyway, we use an u32 here.
}
impl ProviderInfo {
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
provider.map(|p| ProviderInfo {
before_login_hint: p.before_login_hint.to_owned(),
overview_page: p.overview_page.to_owned(),
status: p.status.to_u32().unwrap(),
})
}
}

View File

@@ -0,0 +1,60 @@
use deltachat::{
context::Context,
message::{Message, MsgId},
webxdc::WebxdcInfo,
};
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::maybe_empty_string_to_option;
#[derive(Serialize, TypeDef)]
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
pub struct WebxdcMessageInfo {
/// The name of the app.
///
/// Defaults to the filename if not set in the manifest.
name: String,
/// App icon file name.
/// Defaults to an standard icon if nothing is set in the manifest.
///
/// To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
///
/// App icons should should be square,
/// the implementations will add round corners etc. as needed.
icon: String,
/// if the Webxdc represents a document, then this is the name of the document
document: Option<String>,
/// short string describing the state of the app,
/// sth. as "2 votes", "Highscore: 123",
/// can be changed by the apps
summary: Option<String>,
/// URL where the source code of the Webxdc and other information can be found;
/// defaults to an empty string.
/// Implementations may offer an menu or a button to open this URL.
source_code_url: Option<String>,
}
impl WebxdcMessageInfo {
pub async fn get_for_message(
context: &Context,
instance_message_id: MsgId,
) -> anyhow::Result<Self> {
let message = Message::load_from_db(context, instance_message_id).await?;
let WebxdcInfo {
name,
icon,
document,
summary,
source_code_url,
} = message.get_webxdc_info(context).await?;
Ok(Self {
name,
icon,
document: maybe_empty_string_to_option(document),
summary: maybe_empty_string_to_option(summary),
source_code_url: maybe_empty_string_to_option(source_code_url),
})
}
}

View File

@@ -0,0 +1,92 @@
pub mod api;
pub use api::events;
pub use yerpc;
#[cfg(test)]
mod tests {
use super::api::{Accounts, CommandApi};
use async_channel::unbounded;
use futures::StreamExt;
use tempfile::TempDir;
use yerpc::{RpcClient, RpcSession};
#[tokio::test(flavor = "multi_thread")]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api);
tokio::spawn({
async move {
while let Some(message) = rx.next().await {
let message = serde_json::to_string(&message)?;
sender.send(message).await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
}
});
{
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
println!("{:?}", result);
assert_eq!(result, Some(response.to_owned()));
}
{
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
println!("{:?}", result);
assert_eq!(result, Some(response.to_owned()));
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_batch_set_config() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api);
tokio::spawn({
async move {
while let Some(message) = rx.next().await {
let message = serde_json::to_string(&message)?;
sender.send(message).await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
}
});
{
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
assert_eq!(result, Some(response.to_owned()));
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
assert_eq!(result, Some(response.to_owned()));
}
Ok(())
}
}

View File

@@ -0,0 +1,54 @@
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
use std::net::SocketAddr;
use std::path::PathBuf;
use yerpc::axum::handle_ws_rpc;
use yerpc::{RpcClient, RpcSession};
mod api;
use api::events::event_to_json_rpc_notification;
use api::{Accounts, CommandApi};
const DEFAULT_PORT: u16 = 20808;
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), std::io::Error> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "./accounts".to_string());
let port = std::env::var("DC_PORT")
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
.unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`.");
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
let state = CommandApi::new(accounts);
let app = Router::new()
.route("/ws", get(handler))
.layer(Extension(state.clone()));
tokio::spawn(async move {
state.accounts.read().await.start_io().await;
});
let addr = SocketAddr::from(([127, 0, 0, 1], port));
log::info!("JSON-RPC WebSocket server listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
Ok(())
}
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
let (client, out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), api.clone());
tokio::spawn(async move {
let events = api.accounts.read().await.get_event_emitter().await;
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
client.send_notification("event", Some(event)).await.ok();
}
});
handle_ws_rpc(ws, out_receiver, session).await
}

View File

@@ -0,0 +1,8 @@
node_modules
dist
test_dist
coverage
yarn.lock
package-lock.json
docs
accounts

View File

@@ -0,0 +1,6 @@
node_modules
accounts
docs
coverage
yarn*
package-lock.json

View File

@@ -0,0 +1,3 @@
coverage
dist
generated

View File

@@ -0,0 +1 @@
export * from "./src/lib.js";

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>DeltaChat JSON-RPC example</title>
<style>
body {
font-family: monospace;
background: black;
color: grey;
}
.grid {
display: grid;
grid-template-columns: 3fr 1fr;
grid-template-areas: "a a" "b c";
}
.message {
color: red;
}
#header {
grid-area: a;
color: white;
font-size: 1.2rem;
}
#header a {
color: white;
font-weight: bold;
}
#main {
grid-area: b;
color: green;
}
#main h2,
#main h3 {
color: blue;
}
#side {
grid-area: c;
color: #777;
overflow-y: auto;
}
</style>
<script type="module" src="dist/example.bundle.js"></script>
</head>
<body>
<h1>DeltaChat JSON-RPC example</h1>
<div class="grid">
<div id="header"></div>
<div id="main"></div>
<div id="side"><h2>log</h2></div>
</div>
<p>
Tip: open the dev console and use the client with
<code>window.client</code>
</p>
</body>
</html>

View File

@@ -0,0 +1,109 @@
import { DeltaChat, DeltaChatEvent } from "../deltachat.js";
var SELECTED_ACCOUNT = 0;
window.addEventListener("DOMContentLoaded", (_event) => {
(window as any).selectDeltaAccount = (id: string) => {
SELECTED_ACCOUNT = Number(id);
window.dispatchEvent(new Event("account-changed"));
};
console.log('launch run script...')
run().catch((err) => console.error("run failed", err));
});
async function run() {
const $main = document.getElementById("main")!;
const $side = document.getElementById("side")!;
const $head = document.getElementById("header")!;
const client = new DeltaChat('ws://localhost:20808/ws')
;(window as any).client = client.rpc;
client.on("ALL", event => {
onIncomingEvent(event)
})
window.addEventListener("account-changed", async (_event: Event) => {
listChatsForSelectedAccount();
});
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
async function loadAccountsInHeader() {
console.log('load accounts')
const accounts = await client.rpc.getAllAccounts();
console.log('accounts loaded', accounts)
for (const account of accounts) {
if (account.type === "Configured") {
write(
$head,
`<a href="#" onclick="selectDeltaAccount(${account.id})">
${account.id}: ${account.addr!}
</a>&nbsp;`
);
} else {
write(
$head,
`<a href="#">
${account.id}: (unconfigured)
</a>&nbsp;`
)
}
}
}
async function listChatsForSelectedAccount() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT
const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.type !== "Configured") {
return write($main, "Account is not configured");
}
write($main, `<h2>${info.addr!}</h2>`);
const chats = await client.rpc.getChatlistEntries(
selectedAccount,
0,
null,
null
);
for (const [chatId, _messageId] of chats) {
const chat = await client.rpc.chatlistGetFullChatById(
selectedAccount,
chatId
);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.messageListGetMessageIds(
selectedAccount,
chatId,
0
);
const messages = await client.rpc.messageGetMessages(
selectedAccount,
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
write($main, `<p>${message.text}</p>`);
}
}
}
function onIncomingEvent(event: DeltaChatEvent) {
write(
$side,
`
<p class="message">
[<strong>${event.id}</strong> on account ${event.contextId}]<br>
<em>f1:</em> ${JSON.stringify(event.field1)}<br>
<em>f2:</em> ${JSON.stringify(event.field2)}
</p>`
);
}
}
function write(el: HTMLElement, html: string) {
el.innerHTML += html;
}
function clear(el: HTMLElement) {
el.innerHTML = "";
}

View File

@@ -0,0 +1,26 @@
import { DeltaChat } from "../dist/deltachat.js";
run().catch(console.error);
async function run() {
const delta = new DeltaChat('ws://localhost:20808/ws');
delta.on("event", (event) => {
console.log("event", event.data);
});
const email = process.argv[2]
const password = process.argv[3]
if (!email || !password) throw new Error('USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>')
console.log(`creating acccount for ${email}`)
const id = await delta.rpc.addAccount()
console.log(`created account id ${id}`)
await delta.rpc.setConfig(id, "addr", email);
await delta.rpc.setConfig(id, "mail_pw", password);
console.log('configuration updated')
await delta.rpc.configure(id)
console.log('account configured!')
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...")
}

View File

@@ -0,0 +1,14 @@
import { DeltaChat } from "../dist/deltachat.js";
run().catch(console.error);
async function run() {
const delta = new DeltaChat();
delta.on("event", (event) => {
console.log("event", event.data);
});
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...")
}

View File

@@ -0,0 +1,332 @@
// AUTO-GENERATED by yerpc-derive
import * as T from "./types.js"
import * as RPC from "./jsonrpc.js"
type RequestMethod = (method: string, params?: RPC.Params) => Promise<unknown>;
type NotificationMethod = (method: string, params?: RPC.Params) => void;
interface Transport {
request: RequestMethod,
notification: NotificationMethod
}
export class RawClient {
constructor(private _transport: Transport) {}
/**
* Check if an email address is valid.
*/
public checkEmailValidity(email: string): Promise<boolean> {
return (this._transport.request('check_email_validity', [email] as RPC.Params)) as Promise<boolean>;
}
/**
* Get general system info.
*/
public getSystemInfo(): Promise<Record<string,string>> {
return (this._transport.request('get_system_info', [] as RPC.Params)) as Promise<Record<string,string>>;
}
public addAccount(): Promise<T.U32> {
return (this._transport.request('add_account', [] as RPC.Params)) as Promise<T.U32>;
}
public removeAccount(accountId: T.U32): Promise<null> {
return (this._transport.request('remove_account', [accountId] as RPC.Params)) as Promise<null>;
}
public getAllAccountIds(): Promise<(T.U32)[]> {
return (this._transport.request('get_all_account_ids', [] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Select account id for internally selected state.
* TODO: Likely this is deprecated as all methods take an account id now.
*/
public selectAccount(id: T.U32): Promise<null> {
return (this._transport.request('select_account', [id] as RPC.Params)) as Promise<null>;
}
/**
* Get the selected account id of the internal state..
* TODO: Likely this is deprecated as all methods take an account id now.
*/
public getSelectedAccountId(): Promise<(T.U32|null)> {
return (this._transport.request('get_selected_account_id', [] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Get a list of all configured accounts.
*/
public getAllAccounts(): Promise<(T.Account)[]> {
return (this._transport.request('get_all_accounts', [] as RPC.Params)) as Promise<(T.Account)[]>;
}
/**
* Get top-level info for an account.
*/
public getAccountInfo(accountId: T.U32): Promise<T.Account> {
return (this._transport.request('get_account_info', [accountId] as RPC.Params)) as Promise<T.Account>;
}
/**
* Returns provider for the given domain.
*
* This function looks up domain in offline database.
*
* For compatibility, email address can be passed to this function
* instead of the domain.
*/
public getProviderInfo(accountId: T.U32, email: string): Promise<(T.ProviderInfo|null)> {
return (this._transport.request('get_provider_info', [accountId, email] as RPC.Params)) as Promise<(T.ProviderInfo|null)>;
}
/**
* Checks if the context is already configured.
*/
public isConfigured(accountId: T.U32): Promise<boolean> {
return (this._transport.request('is_configured', [accountId] as RPC.Params)) as Promise<boolean>;
}
/**
* Get system info for an account.
*/
public getInfo(accountId: T.U32): Promise<Record<string,string>> {
return (this._transport.request('get_info', [accountId] as RPC.Params)) as Promise<Record<string,string>>;
}
public setConfig(accountId: T.U32, key: string, value: (string|null)): Promise<null> {
return (this._transport.request('set_config', [accountId, key, value] as RPC.Params)) as Promise<null>;
}
public batchSetConfig(accountId: T.U32, config: Record<string,(string|null)>): Promise<null> {
return (this._transport.request('batch_set_config', [accountId, config] as RPC.Params)) as Promise<null>;
}
/**
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
*/
public setConfigFromQr(accountId: T.U32, qrContent: string): Promise<null> {
return (this._transport.request('set_config_from_qr', [accountId, qrContent] as RPC.Params)) as Promise<null>;
}
public checkQr(accountId: T.U32, qrContent: string): Promise<T.Qr> {
return (this._transport.request('check_qr', [accountId, qrContent] as RPC.Params)) as Promise<T.Qr>;
}
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
}
public batchGetConfig(accountId: T.U32, keys: (string)[]): Promise<Record<string,(string|null)>> {
return (this._transport.request('batch_get_config', [accountId, keys] as RPC.Params)) as Promise<Record<string,(string|null)>>;
}
/**
* Configures this account with the currently set parameters.
* Setup the credential config before calling this.
*/
public configure(accountId: T.U32): Promise<null> {
return (this._transport.request('configure', [accountId] as RPC.Params)) as Promise<null>;
}
/**
* Signal an ongoing process to stop.
*/
public stopOngoingProcess(accountId: T.U32): Promise<null> {
return (this._transport.request('stop_ongoing_process', [accountId] as RPC.Params)) as Promise<null>;
}
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries
* or badge counters e.g. on the app icon.
* The list is already sorted and starts with the most recent fresh message.
*
* Messages belonging to muted chats or to the contact requests are not returned;
* these messages should not be notified
* and also badge counters should not include these messages.
*
* To get the number of fresh messages for a single chat, muted or not,
* use `get_fresh_msg_cnt()`.
*/
public getFreshMsgs(accountId: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_fresh_msgs', [accountId] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get the number of _fresh_ messages in a chat.
* Typically used to implement a badge with a number in the chatlist.
*
* If the specified chat is muted,
* the UI should show the badge counter "less obtrusive",
* e.g. using "gray" instead of "red" color.
*/
public getFreshMsgCnt(accountId: T.U32, chatId: T.U32): Promise<T.Usize> {
return (this._transport.request('get_fresh_msg_cnt', [accountId, chatId] as RPC.Params)) as Promise<T.Usize>;
}
public autocryptInitiateKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('autocrypt_initiate_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
}
public autocryptContinueKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
return (this._transport.request('autocrypt_continue_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
}
public getChatlistEntries(accountId: T.U32, listFlags: (T.U32|null), queryString: (string|null), queryContactId: (T.U32|null)): Promise<(T.ChatListEntry)[]> {
return (this._transport.request('get_chatlist_entries', [accountId, listFlags, queryString, queryContactId] as RPC.Params)) as Promise<(T.ChatListEntry)[]>;
}
public getChatlistItemsByEntries(accountId: T.U32, entries: (T.ChatListEntry)[]): Promise<Record<T.U32,T.ChatListItemFetchResult>> {
return (this._transport.request('get_chatlist_items_by_entries', [accountId, entries] as RPC.Params)) as Promise<Record<T.U32,T.ChatListItemFetchResult>>;
}
public chatlistGetFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
return (this._transport.request('chatlist_get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
}
public acceptChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('accept_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public blockChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('block_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise<T.U32> {
return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise<T.U32>;
}
public messageListGetMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('message_list_get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
}
public messageGetMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
return (this._transport.request('message_get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
}
public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
/**
* Get a single contact options by ID.
*/
public contactsGetContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
return (this._transport.request('contacts_get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
}
/**
* Add a single contact as a result of an explicit user action.
*
* Returns contact id of the created or existing contact
*/
public contactsCreateContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
return (this._transport.request('contacts_create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
}
/**
* Returns contact id of the created or existing DM chat with that contact
*/
public contactsCreateChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
return (this._transport.request('contacts_create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
}
public contactsBlock(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('contacts_block', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public contactsUnblock(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('contacts_unblock', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public contactsGetBlocked(accountId: T.U32): Promise<(T.Contact)[]> {
return (this._transport.request('contacts_get_blocked', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public contactsGetContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return (this._transport.request('contacts_get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get a list of contacts.
* (formerly called getContacts2 in desktop)
*/
public contactsGetContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return (this._transport.request('contacts_get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public contactsGetContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return (this._transport.request('contacts_get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
}
/**
* Returns all message IDs of the given types in a chat.
* Typically used to show a gallery.
*
* The list is already sorted and starts with the oldest message.
* Clients should not try to re-sort the list as this would be an expensive action
* and would result in inconsistencies between clients.
*/
public chatGetMedia(accountId: T.U32, chatId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
}
public webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
}
public webxdcGetStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return (this._transport.request('webxdc_get_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
}
/**
* Get info from a webxdc message
*/
public messageGetWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return (this._transport.request('message_get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
}
/**
* Returns the messageid of the sent message
*/
public miscSendTextMessage(accountId: T.U32, text: string, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, text, chatId] as RPC.Params)) as Promise<T.U32>;
}
}

View File

@@ -0,0 +1,3 @@
// AUTO-GENERATED by typescript-type-def
export type EventTypeName=("Info"|"SmtpConnected"|"ImapConnected"|"SmtpMessageSent"|"ImapMessageDeleted"|"ImapMessageMoved"|"NewBlobFile"|"DeletedBlobFile"|"Warning"|"Error"|"ErrorSelfNotInGroup"|"MsgsChanged"|"IncomingMsg"|"MsgsNoticed"|"MsgDelivered"|"MsgFailed"|"MsgRead"|"ChatModified"|"ChatEphemeralTimerModified"|"ContactsChanged"|"LocationChanged"|"ConfigureProgress"|"ImexProgress"|"ImexFileWritten"|"SecurejoinInviterProgress"|"SecurejoinJoinerProgress"|"ConnectivityChanged"|"SelfavatarChanged"|"WebxdcStatusUpdate");

View File

@@ -0,0 +1,10 @@
// AUTO-GENERATED by typescript-type-def
export type JSONValue=(null|boolean|number|string|(JSONValue)[]|{[key:string]:JSONValue;});
export type Params=((JSONValue)[]|Record<string,JSONValue>);
export type U32=number;
export type Request={"jsonrpc":"2.0";"method":string;"params"?:Params;"id"?:U32;};
export type I32=number;
export type Error={"code":I32;"message":string;"data"?:JSONValue;};
export type Response={"jsonrpc":"2.0";"id":(U32|null);"result"?:JSONValue;"error"?:Error;};
export type Message=(Request|Response);

View File

@@ -0,0 +1,99 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
export type Qr=({"askVerifyContact":{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"askVerifyGroup":{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"fprOk":{"contact_id":U32;};}|{"fprMismatch":{"contact_id":(U32|null);};}|{"fprWithoutAddr":{"fingerprint":string;};}|{"account":{"domain":string;};}|{"webrtcInstance":{"domain":string;"instance_pattern":string;};}|{"addr":{"contact_id":U32;};}|{"url":{"url":string;};}|{"text":{"text":string;};}|{"withdrawVerifyContact":{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"withdrawVerifyGroup":{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"reviveVerifyContact":{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};}|{"reviveVerifyGroup":{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;};});
export type Usize=number;
export type ChatListEntry=[U32,U32];
export type I64=number;
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
/**
* contact id if this is a dm chat (for view profile entry in context menu)
*/
"dmChatContact":(U32|null);})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;};
export type Viewtype=("Unknown"|
/**
* Text message.
*/
"Text"|
/**
* Image message.
* If the image is an animated GIF, the type `Viewtype.Gif` should be used.
*/
"Image"|
/**
* Animated GIF message.
*/
"Gif"|
/**
* Message containing a sticker, similar to image.
* If possible, the ui should display the image without borders in a transparent way.
* A click on a sticker will offer to install the sticker set in some future.
*/
"Sticker"|
/**
* Message containing an Audio file.
*/
"Audio"|
/**
* A voice message that was directly recorded by the user.
* For all other audio messages, the type `Viewtype.Audio` should be used.
*/
"Voice"|
/**
* Video messages.
*/
"Video"|
/**
* Message containing any file, eg. a PDF.
*/
"File"|
/**
* Message is an invitation to a videochat.
*/
"VideochatInvitation"|
/**
* Message is an webxdc instance.
*/
"Webxdc");
export type I32=number;
export type U64=number;
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quotedText":(string|null);"quotedMessageId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"downloadState":DownloadState;};
export type WebxdcMessageInfo={
/**
* The name of the app.
*
* Defaults to the filename if not set in the manifest.
*/
"name":string;
/**
* App icon file name.
* Defaults to an standard icon if nothing is set in the manifest.
*
* To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
*
* App icons should should be square,
* the implementations will add round corners etc. as needed.
*/
"icon":string;
/**
* if the Webxdc represents a document, then this is the name of the document
*/
"document":(string|null);
/**
* short string describing the state of the app,
* sth. as "2 votes", "Highscore: 123",
* can be changed by the apps
*/
"summary":(string|null);
/**
* URL where the source code of the Webxdc and other information can be found;
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
*/
"sourceCodeUrl":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,null,U32,U32,null,U32,string,string,U32,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,string,U32,U32];

View File

@@ -0,0 +1,51 @@
{
"name": "deltachat-jsonrpc-client",
"version": "0.1.0",
"main": "dist/deltachat.js",
"types": "dist/deltachat.d.ts",
"type": "module",
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"license": "MPL-2.0",
"scripts": {
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"generate-bindings": "cargo test",
"build": "run-s generate-bindings build:tsc build:bundle",
"build:tsc": "tsc",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"example": "run-s build example:build example:start",
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:start": "http-server .",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
"test:run": "mocha dist/test",
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test",
"test:report-coverage": "node report_api_coverage.mjs",
"docs": "typedoc --out docs deltachat.ts"
},
"dependencies": {
"isomorphic-ws": "^4.0.1",
"tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git",
"yerpc": "^0.3.3"
},
"devDependencies": {
"@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.0.0",
"@types/node-fetch": "^2.5.7",
"@types/ws": "^7.2.4",
"c8": "^7.10.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"esbuild": "^0.14.11",
"http-server": "^14.1.1",
"mocha": "^9.1.1",
"node-fetch": "^2.6.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"typedoc": "^0.23.2",
"typescript": "^4.5.5",
"ws": "^8.5.0"
}
}

View File

@@ -0,0 +1,28 @@
import { readFileSync } from "fs";
// only checks for the coverge of the api functions in bindings.ts for now
const generatedFile = "typescript/generated/client.ts";
const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
const jsonCoverage =
json[Object.keys(json).find((k) => k.includes(generatedFile))];
const fnMap = Object.keys(jsonCoverage.fnMap).map(
(key) => jsonCoverage.fnMap[key]
);
const htmlCoverage = readFileSync(
"./coverage/" + generatedFile + ".html",
"utf8"
);
const uncoveredLines = htmlCoverage
.split("\n")
.filter((line) => line.includes(`"function not covered"`));
const uncoveredFunctions = uncoveredLines.map(
(line) => />([\w_]+)\(/.exec(line)[1]
);
console.log(
"\nUncovered api functions:\n" +
uncoveredFunctions
.map((uF) => fnMap.find(({ name }) => name === uF))
.map(
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
)
.join("\n")
);

View File

@@ -0,0 +1,77 @@
import * as T from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { EventTypeName } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "tiny-emitter";
export type DeltaChatEvent = {
id: EventTypeName;
contextId: number;
field1: any;
field2: any;
};
export type Events = Record<
EventTypeName | "ALL",
(event: DeltaChatEvent) => void
>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: TinyEmitter<Events>[] = [];
constructor(public transport: Transport) {
super();
this.rpc = new RawClient(this.transport);
this.transport.on("request", (request: Request) => {
const method = request.method;
if (method === "event") {
const event = request.params! as DeltaChatEvent;
this.emit(event.id, event);
this.emit("ALL", event);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(event.id, event);
this.contextEmitters[event.contextId].emit("ALL", event);
}
}
});
}
async listAccounts(): Promise<T.Account[]> {
return await this.rpc.getAllAccounts();
}
getContextEvents(account_id: number) {
if (this.contextEmitters[account_id]) {
return this.contextEmitters[account_id];
} else {
this.contextEmitters[account_id] = new TinyEmitter();
return this.contextEmitters[account_id];
}
}
}
export type Opts = {
url: string;
};
export const DEFAULT_OPTS: Opts = {
url: "ws://localhost:20808/ws",
};
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
opts: Opts;
close() {
this.transport.close();
}
constructor(opts?: Opts | string) {
if (typeof opts === "string") opts = { url: opts };
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
else opts = { ...DEFAULT_OPTS };
const transport = new WebsocketTransport(opts.url)
super(transport);
this.opts = opts;
}
}

View File

@@ -0,0 +1,6 @@
export * as RPC from "../generated/jsonrpc.js";
export * as T from "../generated/types.js";
export * from "../generated/events.js";
export { RawClient } from "../generated/client.js";
export * from "./client.js";
export * as yerpc from "yerpc";

View File

@@ -0,0 +1,154 @@
import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { DeltaChat } from "../deltachat.js";
import {
RpcServerHandle,
startServer,
} from "./test_base.js";
describe("basic tests", () => {
let serverHandle: RpcServerHandle;
let dc: DeltaChat;
before(async () => {
serverHandle = await startServer();
// make sure server is up by the time we continue
await new Promise((res) => setTimeout(res, 100));
dc = new DeltaChat(serverHandle.url)
// dc.on("ALL", (event) => {
//console.log("event", event);
// });
});
after(async () => {
dc && dc.close();
await serverHandle.close();
});
it("check email address validity", async () => {
const validAddresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
];
const invalidAddresses = ["email@", "example.com", "emai221"];
expect(
await Promise.all(
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
).to.not.contain(false);
expect(
await Promise.all(
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
).to.not.contain(true);
});
it("system info", async () => {
const systemInfo = await dc.rpc.getSystemInfo();
expect(systemInfo).to.contain.keys([
"arch",
"num_cpus",
"deltachat_core_version",
"sqlite_version",
]);
});
describe("account managment", () => {
it("should create account", async () => {
const res = await dc.rpc.addAccount();
assert((await dc.rpc.getAllAccountIds()).length === 1);
});
it("should remove the account again", async () => {
await dc.rpc.removeAccount((await dc.rpc.getAllAccountIds())[0]);
assert((await dc.rpc.getAllAccountIds()).length === 0);
});
it("should create multiple accounts", async () => {
await dc.rpc.addAccount();
await dc.rpc.addAccount();
await dc.rpc.addAccount();
await dc.rpc.addAccount();
assert((await dc.rpc.getAllAccountIds()).length === 4);
});
});
describe("contact managment", function () {
let accountId: number;
before(async () => {
accountId = await dc.rpc.addAccount();
});
it("should block and unblock contact", async function () {
const contactId = await dc.rpc.contactsCreateContact(
accountId,
"example@delta.chat",
null
);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.false;
await dc.rpc.contactsBlock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.true;
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(1);
await dc.rpc.contactsUnblock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
.false;
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(0);
});
});
describe("configuration", function () {
let accountId: number;
before(async () => {
accountId = await dc.rpc.addAccount();
});
it("set and retrive", async function () {
await dc.rpc.setConfig(accountId, "addr", "valid@email");
assert((await dc.rpc.getConfig(accountId, "addr")) == "valid@email");
});
it("set invalid key should throw", async function () {
await expect(dc.rpc.setConfig(accountId, "invalid_key", "some value")).to.be
.eventually.rejected;
});
it("get invalid key should throw", async function () {
await expect(dc.rpc.getConfig(accountId, "invalid_key")).to.be.eventually
.rejected;
});
it("set and retrive ui.*", async function () {
await dc.rpc.setConfig(accountId, "ui.chat_bg", "color:red");
assert((await dc.rpc.getConfig(accountId, "ui.chat_bg")) == "color:red");
});
it("set and retrive (batch)", async function () {
const config = { addr: "valid@email", mail_pw: "1234" };
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrive ui.* (batch)", async function () {
const config = {
"ui.chat_bg": "color:green",
"ui.enter_key_sends": "true",
};
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrive mixed(ui and core) (batch)", async function () {
const config = {
"ui.chat_bg": "color:yellow",
"ui.enter_key_sends": "false",
addr: "valid2@email",
mail_pw: "123456",
};
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
});
});

View File

@@ -0,0 +1,202 @@
import { assert, expect } from "chai";
import { DeltaChat, DeltaChatEvent, EventTypeName } from "../deltachat.js";
import {
RpcServerHandle,
createTempUser,
startServer,
} from "./test_base.js";
const EVENT_TIMEOUT = 20000
describe("online tests", function () {
let serverHandle: RpcServerHandle;
let dc: DeltaChat;
let account1: { email: string; password: string };
let account2: { email: string; password: string };
let accountId1: number, accountId2: number;
before(async function () {
this.timeout(12000)
if (!process.env.DCC_NEW_TMP_EMAIL) {
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
console.error(
"CAN NOT RUN COVERAGE correctly: Missing DCC_NEW_TMP_EMAIL environment variable!\n\n",
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
);
process.exit(1);
}
console.log(
"Missing DCC_NEW_TMP_EMAIL environment variable!, skip intergration tests"
);
this.skip();
}
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.url)
dc.on("ALL", ({ id, contextId }) => {
if (id !== "Info") console.log(contextId, id);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account1 || !account1.email || !account1.password) {
console.log(
"We didn't got back an account from the api, skip intergration tests"
);
this.skip();
}
account2 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account2 || !account2.email || !account2.password) {
console.log(
"We didn't got back an account2 from the api, skip intergration tests"
);
this.skip();
}
});
after(async () => {
dc && dc.close();
serverHandle && (await serverHandle.close());
});
let accountsConfigured = false;
it("configure test accounts", async function () {
this.timeout(40000);
accountId1 = await dc.rpc.addAccount();
await dc.rpc.setConfig(accountId1, "addr", account1.email);
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
await dc.rpc.configure(accountId1);
accountId2 = await dc.rpc.addAccount();
await dc.rpc.batchSetConfig(accountId2, {
addr: account2.email,
mail_pw: account2.password,
});
await dc.rpc.configure(accountId2)
accountsConfigured = true;
});
it("send and recieve text message", async function () {
if (!accountsConfigured) {
this.skip();
}
this.timeout(15000);
const contactId = await dc.rpc.contactsCreateContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId);
const { field1: chatIdOnAccountB } = await eventPromise;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
accountId2,
chatIdOnAccountB,
0
);
expect(messageList).have.length(1);
const message = await dc.rpc.messageGetMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
});
it("send and recieve text message roundtrip, encrypted on answer onwards", async function () {
if (!accountsConfigured) {
this.skip();
}
this.timeout(10000);
// send message from A to B
const contactId = await dc.rpc.contactsCreateContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(accountId1, "Hello2", chatId);
// wait for message from A
console.log("wait for message from A");
const event = await eventPromise;
const { field1: chatIdOnAccountB } = event;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
accountId2,
chatIdOnAccountB,
0
);
const message = await dc.rpc.messageGetMessage(
accountId2,
messageList.reverse()[0]
);
expect(message.text).equal("Hello2");
// Send message back from B to A
const eventPromise2 = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId1),
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, "super secret message", chatId);
// Check if answer arives at A and if it is encrypted
await eventPromise2;
const messageId = (
await dc.rpc.messageListGetMessageIds(accountId1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
expect(message2.text).equal("super secret message");
expect(message2.showPadlock).equal(true);
});
it("get provider info for example.com", async () => {
const acc = await dc.rpc.addAccount();
const info = await dc.rpc.getProviderInfo(acc, "example.com");
expect(info).to.be.not.null;
expect(info?.overviewPage).to.equal(
"https://providers.delta.chat/example-com"
);
expect(info?.status).to.equal(3);
});
it("get provider info - domain and email should give same result", async () => {
const acc = await dc.rpc.addAccount();
const info_domain = await dc.rpc.getProviderInfo(acc, "example.com");
const info_email = await dc.rpc.getProviderInfo(acc, "hi@example.com");
expect(info_email).to.deep.equal(info_domain);
});
});
async function waitForEvent(
dc: DeltaChat,
eventType: EventTypeName,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<DeltaChatEvent> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error('Timeout reached before event came in')),
timeout
)
const callback = (event: DeltaChatEvent) => {
if (event.contextId == accountId) {
dc.off(eventType, callback);
clearTimeout(rejectTimeout)
resolve(event);
}
};
dc.on(eventType, callback);
});
}

View File

@@ -0,0 +1,94 @@
import { tmpdir } from "os";
import { join, resolve } from "path";
import { mkdtemp, rm } from "fs/promises";
import { existsSync } from "fs";
import { spawn, exec } from "child_process";
import fetch from "node-fetch";
export const RPC_SERVER_PORT = 20808;
export type RpcServerHandle = {
url: string,
close: () => Promise<void>
}
export async function startServer(port: number = RPC_SERVER_PORT): Promise<RpcServerHandle> {
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
const pathToServerBinary = resolve(join(await getTargetDir(), "debug/deltachat-jsonrpc-server"));
console.log('using server binary: ' + pathToServerBinary);
if (!existsSync(pathToServerBinary)) {
throw new Error(
"server executable does not exist, you need to build it first" +
"\nserver executable not found at " +
pathToServerBinary
);
}
const server = spawn(pathToServerBinary, {
cwd: tmpDir,
env: {
RUST_LOG: process.env.RUST_LOG || "info",
DC_PORT: '' + port
},
});
let shouldClose = false;
server.on("exit", () => {
if (shouldClose) {
return;
}
throw new Error("Server quit");
});
server.stderr.pipe(process.stderr);
server.stdout.pipe(process.stdout)
const url = `ws://localhost:${port}/ws`
return {
url,
close: async () => {
shouldClose = true;
if (!server.kill()) {
console.log("server termination failed");
}
await rm(tmpDir, { recursive: true });
},
};
}
export async function createTempUser(url: string) {
const response = await fetch(url, {
method: "POST",
headers: {
"cache-control": "no-cache",
},
});
if (!response.ok) throw new Error('Received invalid response')
return response.json();
}
function getTargetDir(): Promise<string> {
return new Promise((resolve, reject) => {
exec(
"cargo metadata --no-deps --format-version 1",
(error, stdout, _stderr) => {
if (error) {
console.log("error", error);
reject(error);
} else {
try {
const json = JSON.parse(stdout);
resolve(json.target_directory);
} catch (error) {
console.log("json error", error);
reject(error);
}
}
}
);
});
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"alwaysStrict": true,
"strict": true,
"sourceMap": true,
"strictNullChecks": true,
"rootDir": ".",
"outDir": "dist",
"lib": ["ES2017", "dom"],
"target": "ES2017",
"module": "es2020",
"declaration": true,
"esModuleInterop": true,
"moduleResolution": "node",
"noImplicitAny": true,
"isolatedModules": true
},
"include": ["*.ts", "example/*.ts", "test/*.ts"],
"compileOnSave": false
}

View File

@@ -19,10 +19,11 @@ interface NativeAccount {}
export class AccountManager extends EventEmitter {
dcn_accounts: NativeAccount
accountDir: string
jsonRpcStarted = false
constructor(cwd: string, os = 'deltachat-node') {
debug('DeltaChat constructor')
super()
debug('DeltaChat constructor')
this.accountDir = cwd
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
@@ -114,6 +115,31 @@ export class AccountManager extends EventEmitter {
debug('Started event handler')
}
startJsonRpcHandler(callback: ((response: string) => void) | null) {
if (this.dcn_accounts === null) {
throw new Error('dcn_account is null')
}
if (!callback) {
throw new Error('no callback set')
}
if (this.jsonRpcStarted) {
throw new Error('jsonrpc was started already')
}
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this))
debug('Started JSON-RPC handler')
this.jsonRpcStarted = true
}
jsonRpcRequest(message: string) {
if (!this.jsonRpcStarted) {
throw new Error(
'jsonrpc is not active, start it with startJsonRpcHandler first'
)
}
binding.dcn_json_rpc_request(this.dcn_accounts, message)
}
startIO() {
binding.dcn_accounts_start_io(this.dcn_accounts)
}

View File

@@ -9,7 +9,7 @@ const buildArgs = [
'build',
'--release',
'--features',
'vendored',
'vendored,jsonrpc',
'-p',
'deltachat_ffi'
]

28
node/segfault.js Normal file
View File

@@ -0,0 +1,28 @@
const { default: dc } = require('./dist')
const ac = new dc('test1233490')
console.log("[1]");
ac.startJsonRpcHandler(console.log)
console.log("[2]");
console.log(
ac.jsonRpcRequest(
JSON.stringify({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 2,
})
)
)
console.log("[3]");
setTimeout(() => {
console.log("[4]");
ac.close() // This segfaults -> TODO Findout why?
console.log('still living')
}, 1000)

50
node/segfault2.js Normal file
View File

@@ -0,0 +1,50 @@
const { default: dc } = require('./dist')
const ac = new dc('test1233490')
console.log('[1]')
ac.startJsonRpcHandler(console.log)
console.log('[2]')
console.log(
ac.jsonRpcRequest(
JSON.stringify({
jsonrpc: '2.0',
method: 'batch_set_config',
id: 3,
params: [
69,
{
addr: '',
mail_user: '',
mail_pw: '',
mail_server: '',
mail_port: '',
mail_security: '',
imap_certificate_checks: '',
send_user: '',
send_pw: '',
send_server: '',
send_port: '',
send_security: '',
smtp_certificate_checks: '',
socks5_enabled: '0',
socks5_host: '',
socks5_port: '',
socks5_user: '',
socks5_password: '',
},
],
})
)
)
console.log('[3]')
setTimeout(() => {
console.log('[4]')
ac.close() // This segfaults -> TODO Findout why?
console.log('still living')
}, 1000)

View File

@@ -34,6 +34,9 @@ typedef struct dcn_accounts_t {
dc_accounts_t* dc_accounts;
napi_threadsafe_function threadsafe_event_handler;
uv_thread_t event_handler_thread;
napi_threadsafe_function threadsafe_jsonrpc_handler;
uv_thread_t jsonrpc_thread;
dc_jsonrpc_instance_t* jsonrpc_instance;
int gc;
} dcn_accounts_t;
@@ -2924,6 +2927,12 @@ NAPI_METHOD(dcn_accounts_unref) {
uv_thread_join(&dcn_accounts->event_handler_thread);
dcn_accounts->event_handler_thread = 0;
}
if (dcn_accounts->jsonrpc_instance) {
dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, "{}");
uv_thread_join(&dcn_accounts->jsonrpc_thread);
dc_jsonrpc_unref(dcn_accounts->jsonrpc_instance);
dcn_accounts->jsonrpc_instance = NULL;
}
dc_accounts_unref(dcn_accounts->dc_accounts);
dcn_accounts->dc_accounts = NULL;
@@ -3082,8 +3091,6 @@ static void accounts_event_handler_thread_func(void* arg)
{
dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg;
TRACE("event_handler_thread_func starting");
dc_accounts_event_emitter_t * dc_accounts_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
@@ -3173,7 +3180,7 @@ static void call_accounts_js_event_handler(napi_env env, napi_value js_callback,
if (status != napi_ok) {
napi_throw_error(env, NULL, "Unable to create argv[3] for event_handler arguments");
}
free(data2_string);
dc_str_unref(data2_string);
} else {
status = napi_create_int32(env, dc_event_get_data2_int(dc_event), &argv[3]);
if (status != napi_ok) {
@@ -3234,6 +3241,124 @@ NAPI_METHOD(dcn_accounts_start_event_handler) {
NAPI_RETURN_UNDEFINED();
}
// JSON RPC
static void accounts_jsonrpc_thread_func(void* arg)
{
dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg;
TRACE("accounts_jsonrpc_thread_func starting");
char* response;
while (true) {
response = dc_jsonrpc_next_response(dcn_accounts->jsonrpc_instance);
if (response == NULL) {
// done or broken
break;
}
if (!dcn_accounts->threadsafe_jsonrpc_handler) {
TRACE("threadsafe_jsonrpc_handler not set, bailing");
break;
}
// Don't process events if we're being garbage collected!
if (dcn_accounts->gc == 1) {
TRACE("dc_accounts has been destroyed, bailing");
break;
}
napi_status status = napi_call_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, response, napi_tsfn_blocking);
if (status == napi_closing) {
TRACE("JS function got released, bailing");
break;
}
}
TRACE("accounts_jsonrpc_thread_func ended");
napi_release_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, napi_tsfn_release);
}
static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callback, void* _context, void* data)
{
char* response = (char*)data;
napi_value global;
napi_status status = napi_get_global(env, &global);
if (status != napi_ok) {
napi_throw_error(env, NULL, "Unable to get global");
}
napi_value argv[1];
if (response != 0) {
status = napi_create_string_utf8(env, response, NAPI_AUTO_LENGTH, &argv[0]);
} else {
status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[0]);
}
if (status != napi_ok) {
napi_throw_error(env, NULL, "Unable to create argv for js jsonrpc_handler arguments");
}
dc_str_unref(response);
TRACE("calling back into js");
napi_value result;
status = napi_call_function(
env,
global,
js_callback,
1,
argv,
&result);
if (status != napi_ok) {
TRACE("Unable to call jsonrpc_handler callback2");
const napi_extended_error_info* error_result;
NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
}
}
NAPI_METHOD(dcn_accounts_start_jsonrpc) {
NAPI_ARGV(2);
NAPI_DCN_ACCOUNTS();
napi_value callback = argv[1];
TRACE("calling..");
napi_value async_resource_name;
NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_accounts_jsonrpc_callback", NAPI_AUTO_LENGTH, &async_resource_name));
TRACE("creating threadsafe function..");
NAPI_STATUS_THROWS(napi_create_threadsafe_function(
env,
callback,
0,
async_resource_name,
1000, // max_queue_size
1,
NULL,
NULL,
NULL,
call_accounts_js_jsonrpc_handler,
&dcn_accounts->threadsafe_jsonrpc_handler));
TRACE("done");
dcn_accounts->gc = 0;
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts);
TRACE("creating uv thread..");
uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts);
NAPI_RETURN_UNDEFINED();
}
NAPI_METHOD(dcn_json_rpc_request) {
NAPI_ARGV(2);
NAPI_DCN_ACCOUNTS();
if (!dcn_accounts->jsonrpc_instance) {
const char* msg = "dcn_accounts->jsonrpc_instance is null, have you called dcn_accounts_start_jsonrpc()?";
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg));
}
NAPI_ARGV_UTF8_MALLOC(request, 1);
dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, request);
free(request);
NAPI_RETURN_UNDEFINED();
}
NAPI_INIT() {
/**
@@ -3504,4 +3629,9 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_webxdc_status_update);
NAPI_EXPORT_FUNCTION(dcn_get_webxdc_status_updates);
NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_blob);
/** jsonrpc **/
NAPI_EXPORT_FUNCTION(dcn_accounts_start_jsonrpc);
NAPI_EXPORT_FUNCTION(dcn_json_rpc_request);
}

View File

@@ -23,7 +23,7 @@
dcn_accounts_t* dcn_accounts; \
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_accounts)); \
if (!dcn_accounts) { \
const char* msg = "Provided dnc_acounts is null"; \
const char* msg = "Provided dcn_acounts is null"; \
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
} \
if (!dcn_accounts->dc_accounts) { \

View File

@@ -2,7 +2,7 @@
import DeltaChat, { Message } from '../dist'
import binding from '../binding'
import { strictEqual } from 'assert'
import { deepEqual, deepStrictEqual, strictEqual } from 'assert'
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { EventId2EventName, C } from '../dist/constants'
@@ -84,6 +84,95 @@ describe('static tests', function () {
})
})
describe('JSON RPC', function () {
it('smoketest', async function () {
const { dc } = DeltaChat.newTemporary()
let promise_resolve
const promise = new Promise((res, _rej) => {
promise_resolve = res
})
dc.startJsonRpcHandler(promise_resolve)
dc.jsonRpcRequest(
JSON.stringify({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 2,
})
)
deepStrictEqual(
{
jsonrpc: '2.0',
id: 2,
result: [1],
},
JSON.parse(await promise)
)
dc.close()
})
it('basic test', async function () {
const { dc } = DeltaChat.newTemporary()
const promises = {}
dc.startJsonRpcHandler((msg) => {
const response = JSON.parse(msg)
promises[response.id](response)
delete promises[response.id]
})
const call = (request) => {
dc.jsonRpcRequest(JSON.stringify(request))
return new Promise((res, _rej) => {
promises[request.id] = res
})
}
deepStrictEqual(
{
jsonrpc: '2.0',
id: 2,
result: [1],
},
await call({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 2,
})
)
deepStrictEqual(
{
jsonrpc: '2.0',
id: 3,
result: 2,
},
await call({
jsonrpc: '2.0',
method: 'add_account',
params: [],
id: 3,
})
)
deepStrictEqual(
{
jsonrpc: '2.0',
id: 4,
result: [1, 2],
},
await call({
jsonrpc: '2.0',
method: 'get_all_account_ids',
params: [],
id: 4,
})
)
dc.close()
})
})
describe('Basic offline Tests', function () {
it('opens a context', async function () {
const { dc, context } = DeltaChat.newTemporary()

View File

@@ -1,5 +1,6 @@
{
"dependencies": {
"@deltachat/jsonrpc-client": "file:deltachat-jsonrpc/typescript",
"debug": "^4.1.1",
"napi-macros": "^2.0.0",
"node-gyp-build": "^4.1.0"

View File

@@ -63,7 +63,7 @@ def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml"]
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml", "deltachat-jsonrpc/Cargo.toml"]
try:
opts = parser.parse_args()
except SystemExit: