mirror of
https://github.com/chatmail/core.git
synced 2026-06-27 01:56:36 +03:00
Compare commits
150 Commits
py-1.86.0
...
jsonrpc-ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7147d32601 | ||
|
|
343bb34589 | ||
|
|
361b7f5b69 | ||
|
|
15019ce02b | ||
|
|
7bb5dc4c3c | ||
|
|
543edac105 | ||
|
|
55db8d1fe0 | ||
|
|
1d92f06834 | ||
|
|
2ff05c9dff | ||
|
|
d6b6d96e21 | ||
|
|
8776767a44 | ||
|
|
9d83057a71 | ||
|
|
8ce11ac62f | ||
|
|
918ec85767 | ||
|
|
f3a9ab6d23 | ||
|
|
e4def6a44d | ||
|
|
4b5c194ef3 | ||
|
|
97e2d85b28 | ||
|
|
81bc7bd7bf | ||
|
|
f53c456e50 | ||
|
|
64fa5675a9 | ||
|
|
1f0bdfa704 | ||
|
|
5ac347b7ae | ||
|
|
40fa2d4120 | ||
|
|
aaf27e4434 | ||
|
|
2df10857ca | ||
|
|
7eae3a1072 | ||
|
|
7fc162543a | ||
|
|
e7da0672ae | ||
|
|
69d9d48ae4 | ||
|
|
802677222b | ||
|
|
d7b9febc33 | ||
|
|
1d347f7369 | ||
|
|
e3fa42fe88 | ||
|
|
2f00b098ac | ||
|
|
bdd4aa0f10 | ||
|
|
6fee4fd878 | ||
|
|
60d3a3cacf | ||
|
|
4bb1980f8d | ||
|
|
35b70b1d1b | ||
|
|
fd53b80c17 | ||
|
|
978e4aec82 | ||
|
|
329f498651 | ||
|
|
035e208e4f | ||
|
|
3404996fdd | ||
|
|
97e0e0137a | ||
|
|
659e48bd3f | ||
|
|
271d54e420 | ||
|
|
9984ee5eb2 | ||
|
|
136bec0273 | ||
|
|
c5ff7427be | ||
|
|
2319dfc3eb | ||
|
|
d8d26b9cae | ||
|
|
d93622bc84 | ||
|
|
2fde4962a1 | ||
|
|
29a5d73f94 | ||
|
|
b51814aaaa | ||
|
|
63e7179191 | ||
|
|
9915803252 | ||
|
|
e12aeb7bd8 | ||
|
|
346fab7f26 | ||
|
|
6f6e6f24c9 | ||
|
|
079cd67da8 | ||
|
|
bfd97fdb05 | ||
|
|
688326f21c | ||
|
|
cc20d25b8d | ||
|
|
99d50615c3 | ||
|
|
a006825376 | ||
|
|
c90fd1c9ce | ||
|
|
bf5d09e74a | ||
|
|
e60164b5f3 | ||
|
|
9f4646e8bd | ||
|
|
5a9e18ed72 | ||
|
|
d40960bcfd | ||
|
|
ae8e81ceb2 | ||
|
|
a74c850031 | ||
|
|
ece5eb065a | ||
|
|
7598c50dba | ||
|
|
5078ca6d8e | ||
|
|
ddf9f0cd93 | ||
|
|
75f0537181 | ||
|
|
c6a47e359f | ||
|
|
51aead6b58 | ||
|
|
d738371848 | ||
|
|
6cabb32aa5 | ||
|
|
3e2af8537c | ||
|
|
26e802cf0f | ||
|
|
a467ca22fb | ||
|
|
b376790b78 | ||
|
|
6d4fecb274 | ||
|
|
14421c6e00 | ||
|
|
290ee20e63 | ||
|
|
997fb4061a | ||
|
|
92b38cebe4 | ||
|
|
8ebe86d9e9 | ||
|
|
84cabbcb7e | ||
|
|
f23fa1c9d3 | ||
|
|
5053a22f96 | ||
|
|
0291094c62 | ||
|
|
6220724bf4 | ||
|
|
102f6d9719 | ||
|
|
aad94f12c1 | ||
|
|
065ef7ab38 | ||
|
|
008e78a022 | ||
|
|
548335082b | ||
|
|
a36885e886 | ||
|
|
0d1afe0938 | ||
|
|
7969249d89 | ||
|
|
52fc973ead | ||
|
|
96f53f5cd2 | ||
|
|
f234bc19a1 | ||
|
|
fcdf766769 | ||
|
|
525b04e69e | ||
|
|
377fa01e98 | ||
|
|
7ce291fac5 | ||
|
|
b88042a902 | ||
|
|
dc29ede6e3 | ||
|
|
becbb03d80 | ||
|
|
bc604d4c24 | ||
|
|
10f3ad6122 | ||
|
|
0ed3480258 | ||
|
|
8033966a70 | ||
|
|
2361042ede | ||
|
|
f1ded231ed | ||
|
|
fa54e8a8ac | ||
|
|
252ef4dbfb | ||
|
|
d2d788662a | ||
|
|
82454243fa | ||
|
|
d947479a60 | ||
|
|
8a4e07c83e | ||
|
|
776a3ecd1f | ||
|
|
1ae67c4549 | ||
|
|
7b369c9107 | ||
|
|
1823ee04ee | ||
|
|
5c0447ee29 | ||
|
|
29eb2cc68a | ||
|
|
1a171ad494 | ||
|
|
c0a17df344 | ||
|
|
e993b37f1e | ||
|
|
e280ca9db8 | ||
|
|
78f9383332 | ||
|
|
a96a4362cd | ||
|
|
1fb6d1b59d | ||
|
|
f94d7d9ea5 | ||
|
|
8eb04de748 | ||
|
|
521d14a6e0 | ||
|
|
e334b781fb | ||
|
|
d286872782 | ||
|
|
db28349703 | ||
|
|
e8ff020543 |
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
fmt:
|
||||
@@ -83,13 +83,13 @@ jobs:
|
||||
rust: 1.61.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.56.0
|
||||
# Minimum Supported Rust Version = 1.56.1
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.56.0
|
||||
rust: 1.56.1
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
|
||||
- name: install python
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -141,3 +141,18 @@ jobs:
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e lint,mypy,doc,py3
|
||||
|
||||
- name: install pypy
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 'pypy${{ matrix.python }}'
|
||||
|
||||
- name: run pypy tests
|
||||
if: ${{ matrix.python }}
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e pypy3
|
||||
|
||||
45
.github/workflows/jsonrpc.yml
vendored
Normal file
45
.github/workflows/jsonrpc.yml
vendored
Normal 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
|
||||
3
.github/workflows/node-package.yml
vendored
3
.github/workflows/node-package.yml
vendored
@@ -120,6 +120,9 @@ jobs:
|
||||
- name: install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
- name: build constants
|
||||
run: |
|
||||
npm run build:core:constants
|
||||
- name: build typescript part
|
||||
run: |
|
||||
npm run build:bindings:ts
|
||||
|
||||
14
.npmignore
14
.npmignore
@@ -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
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -2,9 +2,59 @@
|
||||
|
||||
## 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
|
||||
- configure DeltaChat folder by selecting it, so it is configured even if not LISTed #3371
|
||||
- build PyPy wheels #6683
|
||||
- improve default error if NDN does not provide an error #3456
|
||||
|
||||
### Fixes
|
||||
- mailing list: remove square-brackets only for first name #3452
|
||||
- do not use footers from mailinglists as the contact status #3460
|
||||
- don't ignore KML parsing errors #3473
|
||||
|
||||
|
||||
## 1.87.0
|
||||
|
||||
### Changes
|
||||
- limit the rate of MDN sending #3402
|
||||
- ignore ratelimits for bots #3439
|
||||
- remove `msgs_mdns` references to deleted messages during housekeeping #3387
|
||||
- format message lines starting with `>` as quotes #3434
|
||||
- node: remove `split2` dependency #3418
|
||||
- node: add git installation info to readme #3418
|
||||
- limit the rate of webxdc update sending #3417
|
||||
|
||||
### Fixes
|
||||
- set a default error if NDN does not provide an error #3410
|
||||
- python: avoid exceptions when messages/contacts/chats are compared with `None`
|
||||
- node: wait for the event loop to stop before destroying contexts #3431 #3451
|
||||
- emit configuration errors via event on failure #3433
|
||||
- report configure and imex success/failure after freeing ongoing process #3442
|
||||
|
||||
### API-Changes
|
||||
- python: added `Message.get_status_updates()` #3416
|
||||
- python: added `Message.send_status_update()` #3416
|
||||
- python: added `Message.is_webxdc()` #3416
|
||||
- python: added `Message.is_videochat_invitation()` #3416
|
||||
- python: added support for "videochat" and "webxdc" view types to `Message.new_empty()` #3416
|
||||
|
||||
|
||||
## 1.86.0
|
||||
|
||||
@@ -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
|
||||
|
||||
2056
Cargo.lock
generated
2056
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
44
Cargo.toml
44
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.86.0"
|
||||
version = "1.87.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -9,6 +9,7 @@ rust-version = "1.56"
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
panic = 'abort'
|
||||
opt-level = 1
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
@@ -19,19 +20,19 @@ deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap" }
|
||||
async-native-tls = { version = "0.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", default-features=false, features = ["smtp-transport", "socks5"] }
|
||||
async-std-resolver = "0.21"
|
||||
async-std = { version = "1" }
|
||||
async-tar = { version = "0.4", default-features=false }
|
||||
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
|
||||
trust-dns-resolver = "0.21"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.3"
|
||||
chrono = "0.4"
|
||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||
dirs = { version = "4", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||
encoded-words = "0.2"
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
@@ -47,12 +48,12 @@ num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.12.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pgp = { version = "0.8", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.23"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.7"
|
||||
rand = "0.8"
|
||||
regex = "1.5"
|
||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
@@ -65,32 +66,36 @@ sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4"
|
||||
fast-socks5 = "0.8"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "4.3.3"
|
||||
textwrap = "0.15.0"
|
||||
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
|
||||
async-channel = "1.6.1"
|
||||
futures-lite = "1.12.0"
|
||||
tokio-stream = { version = "0.1.9", features = ["fs"] }
|
||||
reqwest = { version = "0.11.11", features = ["json"] }
|
||||
async_zip = { git = "https://github.com/dignifiedquire/rs-async-zip", branch = "main", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = { version = "0.3.4", features = ["async_std"] }
|
||||
criterion = { version = "0.3.4", features = ["async_tokio"] }
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"deltachat-ffi",
|
||||
"deltachat_derive",
|
||||
"deltachat-jsonrpc"
|
||||
]
|
||||
|
||||
[[example]]
|
||||
@@ -132,5 +137,10 @@ harness = false
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled-sqlcipher-vendored-openssl"]
|
||||
vendored = [
|
||||
"async-native-tls/vendored",
|
||||
"async-smtp/native-tls-vendored",
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"reqwest/native-tls-vendored"
|
||||
]
|
||||
nightly = ["pgp/nightly"]
|
||||
|
||||
@@ -102,9 +102,6 @@ $ cargo build -p deltachat_ffi --release
|
||||
|
||||
## Debugging environment variables
|
||||
|
||||
- `DCC_IMAP_DEBUG`: if set IMAP protocol commands and responses will be
|
||||
printed
|
||||
|
||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
||||
|
||||
- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use async_std::task::block_on;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::Events;
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
let context = Context::new(&dbfile, id, Events::new()).await.unwrap();
|
||||
|
||||
let book = (0..n)
|
||||
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
|
||||
@@ -24,12 +24,16 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
c.bench_function("create 500 contacts", |b| {
|
||||
b.iter(|| block_on(async { address_book_benchmark(black_box(500), black_box(0)).await }))
|
||||
b.to_async(&rt)
|
||||
.iter(|| async { address_book_benchmark(black_box(500), black_box(0)).await })
|
||||
});
|
||||
|
||||
c.bench_function("create 100 contacts and read it 1000 times", |b| {
|
||||
b.iter(|| block_on(async { address_book_benchmark(black_box(100), black_box(1000)).await }))
|
||||
b.to_async(&rt)
|
||||
.iter(|| async { address_book_benchmark(black_box(100), black_box(1000)).await })
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use async_std::path::PathBuf;
|
||||
use async_std::task::block_on;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::accounts::Accounts;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn create_accounts(n: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
|
||||
@@ -18,7 +17,8 @@ async fn create_accounts(n: u32) {
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("create 1 account", |b| {
|
||||
b.iter(|| block_on(async { create_accounts(black_box(1)).await }))
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
b.to_async(&rt).iter(|| create_accounts(black_box(1)))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use async_std::path::Path;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::async_executor::AsyncStdExecutor;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::Events;
|
||||
|
||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
let context = Context::new(dbfile, id, Events::new()).await.unwrap();
|
||||
|
||||
for c in chats.iter().take(10) {
|
||||
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
|
||||
@@ -20,15 +20,19 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
||||
// messages, such as your primary account.
|
||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||
let chats: Vec<_> = async_std::task::block_on(async {
|
||||
let context = Context::new((&path).into(), 100).await.unwrap();
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
let chats: Vec<_> = rt.block_on(async {
|
||||
let context = Context::new(Path::new(&path), 100, Events::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
|
||||
let len = chatlist.len();
|
||||
(0..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
|
||||
});
|
||||
|
||||
c.bench_function("chat::get_chat_msgs (load messages from 10 chats)", |b| {
|
||||
b.to_async(AsyncStdExecutor)
|
||||
b.to_async(&rt)
|
||||
.iter(|| get_chat_msgs_benchmark(black_box(path.as_ref()), black_box(&chats)))
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use criterion::async_executor::AsyncStdExecutor;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::Events;
|
||||
|
||||
async fn get_chat_list_benchmark(context: &Context) {
|
||||
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
@@ -12,10 +14,14 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
||||
// messages, such as your primary account.
|
||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||
let context =
|
||||
async_std::task::block_on(async { Context::new(path.into(), 100).await.unwrap() });
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let context = rt.block_on(async {
|
||||
Context::new(Path::new(&path), 100, Events::new())
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
c.bench_function("chatlist:try_load (Get Chatlist)", |b| {
|
||||
b.to_async(AsyncStdExecutor)
|
||||
b.to_async(&rt)
|
||||
.iter(|| get_chat_list_benchmark(black_box(&context)))
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use async_std::{path::PathBuf, task::block_on};
|
||||
use criterion::{
|
||||
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
|
||||
Criterion,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::{
|
||||
config::Config,
|
||||
context::Context,
|
||||
dc_receive_imf::dc_receive_imf,
|
||||
imex::{imex, ImexMode},
|
||||
receive_imf::receive_imf,
|
||||
Events,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
@@ -31,7 +30,7 @@ Hello {i}",
|
||||
i = i,
|
||||
i_dec = i - 1,
|
||||
);
|
||||
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -42,13 +41,13 @@ async fn create_context() -> Context {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
let context = Context::new(&dbfile, id, Events::new()).await.unwrap();
|
||||
|
||||
let backup: PathBuf = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join("delta-chat-backup.tar")
|
||||
.into();
|
||||
if backup.exists().await {
|
||||
.join("delta-chat-backup.tar");
|
||||
|
||||
if backup.exists() {
|
||||
println!("Importing backup");
|
||||
imex(&context, ImexMode::ImportBackup, &backup, None)
|
||||
.await
|
||||
@@ -71,11 +70,15 @@ async fn create_context() -> Context {
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Receive messages");
|
||||
group.bench_function("Receive 100 simple text msgs", |b| {
|
||||
b.to_async(AsyncStdExecutor).iter_batched(
|
||||
|| block_on(create_context()),
|
||||
|context| recv_all_emails(black_box(context)),
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let context = rt.block_on(create_context());
|
||||
|
||||
b.to_async(&rt).iter(|| {
|
||||
let ctx = context.clone();
|
||||
async move {
|
||||
recv_all_emails(black_box(ctx)).await;
|
||||
}
|
||||
});
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use async_std::task::block_on;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::Events;
|
||||
use std::path::Path;
|
||||
|
||||
async fn search_benchmark(path: impl AsRef<Path>) {
|
||||
let dbfile = path.as_ref();
|
||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
let context = Context::new(dbfile.as_ref(), id, Events::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for _ in 0..10u32 {
|
||||
context.search_msgs(None, "hello").await.unwrap();
|
||||
@@ -17,8 +18,10 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
||||
// messages, such as your primary account.
|
||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
c.bench_function("search hello", |b| {
|
||||
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
|
||||
b.to_async(&rt).iter(|| search_benchmark(black_box(&path)))
|
||||
});
|
||||
} else {
|
||||
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.86.0"
|
||||
version = "1.87.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -16,17 +16,20 @@ 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"
|
||||
serde_json = "1.0"
|
||||
async-std = "1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.7"
|
||||
once_cell = "1.12.0"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
vendored = ["deltachat/vendored"]
|
||||
nightly = ["deltachat/nightly"]
|
||||
jsonrpc = ["deltachat-jsonrpc"]
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
@@ -28,8 +27,9 @@ fn main() {
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
|
||||
.unwrap()
|
||||
.write_all(pkg_config.as_bytes())
|
||||
.unwrap();
|
||||
fs::write(
|
||||
target_path.join("pkgconfig").join("deltachat.pc"),
|
||||
pkg_config.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1791,6 +1791,11 @@ void dc_delete_msgs (dc_context_t* context, const uint3
|
||||
/**
|
||||
* Forward messages to another chat.
|
||||
*
|
||||
* All types of messages can be forwarded,
|
||||
* however, they will be flagged as such (dc_msg_is_forwarded() is set).
|
||||
*
|
||||
* Original sender, info-state and webxdc updates are not forwarded on purpose.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_ids An array of uint32_t containing all message IDs that should be forwarded.
|
||||
@@ -3979,7 +3984,7 @@ int dc_msg_is_sent (const dc_msg_t* msg);
|
||||
*
|
||||
* For privacy reasons, we do not provide the name or the e-mail address of the
|
||||
* original author (in a typical GUI, you select the messages text and click on
|
||||
* "forwared"; you won't expect other data to be send to the new recipient,
|
||||
* "forwarded"; you won't expect other data to be send to the new recipient,
|
||||
* esp. as the new recipient may not be in any relationship to the original author)
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
@@ -5173,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
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![deny(unused, clippy::all)]
|
||||
#![warn(unused, clippy::all)]
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
@@ -15,17 +15,20 @@ extern crate human_panic;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::Write;
|
||||
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 _;
|
||||
use async_std::sync::RwLock;
|
||||
use async_std::task::{block_on, spawn};
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
@@ -38,6 +41,7 @@ use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
mod dc_array;
|
||||
mod lot;
|
||||
@@ -61,6 +65,23 @@ use deltachat::chatlist::Chatlist;
|
||||
/// Struct representing the deltachat context.
|
||||
pub type dc_context_t = Context;
|
||||
|
||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||
|
||||
fn block_on<T>(fut: T) -> T::Output
|
||||
where
|
||||
T: Future,
|
||||
{
|
||||
RT.block_on(fut)
|
||||
}
|
||||
|
||||
fn spawn<T>(fut: T) -> JoinHandle<T::Output>
|
||||
where
|
||||
T: Future + Send + 'static,
|
||||
T::Output: Send + 'static,
|
||||
{
|
||||
RT.spawn(fut)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_new(
|
||||
_os_name: *const libc::c_char,
|
||||
@@ -77,7 +98,7 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
// generate random ID as this functionality is not yet available on the C-api.
|
||||
let id = rand::thread_rng().gen();
|
||||
block_on(Context::new(as_path(dbfile).to_path_buf().into(), id))
|
||||
block_on(Context::new(as_path(dbfile), id, Events::new()))
|
||||
} else {
|
||||
eprintln!("blobdir can not be defined explicitly anymore");
|
||||
return ptr::null_mut();
|
||||
@@ -101,10 +122,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
|
||||
}
|
||||
|
||||
let id = rand::thread_rng().gen();
|
||||
match block_on(Context::new_closed(
|
||||
as_path(dbfile).to_path_buf().into(),
|
||||
id,
|
||||
)) {
|
||||
match block_on(Context::new_closed(as_path(dbfile), id, Events::new())) {
|
||||
Ok(context) => Box::into_raw(Box::new(context)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {:#}", err);
|
||||
@@ -378,7 +396,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
|
||||
let redirect = to_string_lossy(redirect);
|
||||
|
||||
block_on(async move {
|
||||
match oauth2::dc_get_oauth2_url(ctx, &addr, &redirect)
|
||||
match oauth2::get_oauth2_url(ctx, &addr, &redirect)
|
||||
.await
|
||||
.log_err(ctx, "dc_get_oauth2_url failed")
|
||||
{
|
||||
@@ -676,10 +694,13 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
|
||||
}
|
||||
let events = &*events;
|
||||
|
||||
events
|
||||
.recv_sync()
|
||||
.map(|ev| Box::into_raw(Box::new(ev)))
|
||||
.unwrap_or_else(ptr::null_mut)
|
||||
block_on(async move {
|
||||
events
|
||||
.recv()
|
||||
.await
|
||||
.map(|ev| Box::into_raw(Box::new(ev)))
|
||||
.unwrap_or_else(ptr::null_mut)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -719,7 +740,7 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let keypair = key::KeyPair {
|
||||
@@ -2224,7 +2245,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(securejoin::dc_get_securejoin_qr(ctx, chat_id))
|
||||
block_on(securejoin::get_securejoin_qr(ctx, chat_id))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
@@ -2262,7 +2283,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
securejoin::dc_join_securejoin(ctx, &to_string_lossy(qr))
|
||||
securejoin::join_securejoin(ctx, &to_string_lossy(qr))
|
||||
.await
|
||||
.map(|chatid| chatid.to_u32())
|
||||
.log_err(ctx, "failed dc_join_securejoin() call")
|
||||
@@ -2388,7 +2409,7 @@ pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut l
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(ctx.get_last_error()).strdup()
|
||||
ctx.get_last_error().strdup()
|
||||
}
|
||||
|
||||
// dc_array_t
|
||||
@@ -3574,7 +3595,8 @@ pub unsafe extern "C" fn dc_msg_latefiling_mediasize(
|
||||
ffi_msg
|
||||
.message
|
||||
.latefiling_mediasize(ctx, width, height, duration)
|
||||
});
|
||||
})
|
||||
.ok_or_log_msg(ctx, "Cannot set media size");
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4087,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
|
||||
@@ -4100,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 }
|
||||
}
|
||||
}
|
||||
@@ -4120,7 +4142,7 @@ pub unsafe extern "C" fn dc_accounts_new(
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accs = block_on(Accounts::new(as_path(dbfile).to_path_buf().into()));
|
||||
let accs = block_on(Accounts::new(as_path(dbfile).into()));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||
@@ -4292,7 +4314,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
match accounts
|
||||
.migrate_account(async_std::path::PathBuf::from(dbfile))
|
||||
.migrate_account(std::path::PathBuf::from(dbfile))
|
||||
.await
|
||||
{
|
||||
Ok(id) => id,
|
||||
@@ -4375,7 +4397,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
||||
block_on(async move { accounts.write().await.maybe_network_lost().await });
|
||||
}
|
||||
|
||||
pub type dc_accounts_event_emitter_t = deltachat::accounts::EventEmitter;
|
||||
pub type dc_accounts_event_emitter_t = EventEmitter;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
@@ -4412,9 +4434,93 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let emitter = &mut *emitter;
|
||||
|
||||
emitter
|
||||
.recv_sync()
|
||||
block_on(emitter.recv())
|
||||
.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::DeltaChatApiV0;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<DeltaChatApiV0>,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||
account_manager: *mut dc_accounts_t,
|
||||
api_version: *const libc::c_char,
|
||||
) -> *mut dc_jsonrpc_instance_t {
|
||||
if account_manager.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api_version = to_string_lossy(api_version);
|
||||
|
||||
let rpc_api = match api_version.as_str() {
|
||||
"v0" => {
|
||||
deltachat_jsonrpc::api::DeltaChatApiV0::from_arc((*account_manager).inner.clone())
|
||||
}
|
||||
version => {
|
||||
eprintln!(
|
||||
"Error initializing JSON-RPC API: API version {} is not supported.",
|
||||
version
|
||||
);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, rpc_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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ pub(crate) enum CStringError {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use deltachat::dc_tools::{dc_strdup, OsStrExt};
|
||||
/// use deltachat::tools::{dc_strdup, OsStrExt};
|
||||
/// let path = std::path::Path::new("/some/path");
|
||||
/// let path_c = path.to_c_string().unwrap();
|
||||
/// unsafe {
|
||||
|
||||
3
deltachat-jsonrpc/.gitignore
vendored
Normal file
3
deltachat-jsonrpc/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
accounts/
|
||||
|
||||
.cargo
|
||||
39
deltachat-jsonrpc/Cargo.toml
Normal file
39
deltachat-jsonrpc/Cargo.toml
Normal 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
123
deltachat-jsonrpc/README.md
Normal 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
28
deltachat-jsonrpc/TODO.md
Normal 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
|
||||
157
deltachat-jsonrpc/src/api/events.rs
Normal file
157
deltachat-jsonrpc/src/api/events.rs
Normal 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();
|
||||
}
|
||||
683
deltachat-jsonrpc/src/api/mod.rs
Normal file
683
deltachat-jsonrpc/src/api/mod.rs
Normal file
@@ -0,0 +1,683 @@
|
||||
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,
|
||||
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 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 DeltaChatApiV0 {
|
||||
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
||||
}
|
||||
|
||||
impl DeltaChatApiV0 {
|
||||
pub fn new(accounts: Accounts) -> Self {
|
||||
DeltaChatApiV0 {
|
||||
accounts: Arc::new(RwLock::new(accounts)),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
DeltaChatApiV0 { 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 DeltaChatApiV0 {
|
||||
// ---------------------------------------------
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
45
deltachat-jsonrpc/src/api/types/account.rs
Normal file
45
deltachat-jsonrpc/src/api/types/account.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
92
deltachat-jsonrpc/src/api/types/chat.rs
Normal file
92
deltachat-jsonrpc/src/api/types/chat.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
126
deltachat-jsonrpc/src/api/types/chat_list.rs
Normal file
126
deltachat-jsonrpc/src/api/types/chat_list.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
50
deltachat-jsonrpc/src/api/types/contact.rs
Normal file
50
deltachat-jsonrpc/src/api/types/contact.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
202
deltachat-jsonrpc/src/api/types/message.rs
Normal file
202
deltachat-jsonrpc/src/api/types/message.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
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>,
|
||||
}
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
19
deltachat-jsonrpc/src/api/types/mod.rs
Normal file
19
deltachat-jsonrpc/src/api/types/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
22
deltachat-jsonrpc/src/api/types/provider_info.rs
Normal file
22
deltachat-jsonrpc/src/api/types/provider_info.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
60
deltachat-jsonrpc/src/api/types/webxdc.rs
Normal file
60
deltachat-jsonrpc/src/api/types/webxdc.rs
Normal 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),
|
||||
})
|
||||
}
|
||||
}
|
||||
93
deltachat-jsonrpc/src/lib.rs
Normal file
93
deltachat-jsonrpc/src/lib.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
pub mod api;
|
||||
pub use api::events;
|
||||
pub use api::{Accounts, DeltaChatApiV0};
|
||||
pub use yerpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::api::{Accounts, DeltaChatApiV0};
|
||||
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 = DeltaChatApiV0::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 = DeltaChatApiV0::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(())
|
||||
}
|
||||
}
|
||||
54
deltachat-jsonrpc/src/webserver.rs
Normal file
54
deltachat-jsonrpc/src/webserver.rs
Normal 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, DeltaChatApiV0};
|
||||
|
||||
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 = DeltaChatApiV0::new(accounts);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/rpc/v0", 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<DeltaChatApiV0>) -> 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
|
||||
}
|
||||
8
deltachat-jsonrpc/typescript/.gitignore
vendored
Normal file
8
deltachat-jsonrpc/typescript/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
dist
|
||||
test_dist
|
||||
coverage
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
docs
|
||||
accounts
|
||||
6
deltachat-jsonrpc/typescript/.npmignore
Normal file
6
deltachat-jsonrpc/typescript/.npmignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
accounts
|
||||
docs
|
||||
coverage
|
||||
yarn*
|
||||
package-lock.json
|
||||
3
deltachat-jsonrpc/typescript/.prettierignore
Normal file
3
deltachat-jsonrpc/typescript/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
coverage
|
||||
dist
|
||||
generated
|
||||
1
deltachat-jsonrpc/typescript/deltachat.ts
Normal file
1
deltachat-jsonrpc/typescript/deltachat.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./src/lib.js";
|
||||
56
deltachat-jsonrpc/typescript/example.html
Normal file
56
deltachat-jsonrpc/typescript/example.html
Normal 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>
|
||||
109
deltachat-jsonrpc/typescript/example/example.ts
Normal file
109
deltachat-jsonrpc/typescript/example/example.ts
Normal 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> `
|
||||
);
|
||||
} else {
|
||||
write(
|
||||
$head,
|
||||
`<a href="#">
|
||||
${account.id}: (unconfigured)
|
||||
</a> `
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = "";
|
||||
}
|
||||
26
deltachat-jsonrpc/typescript/example/node-add-account.js
Normal file
26
deltachat-jsonrpc/typescript/example/node-add-account.js
Normal 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...")
|
||||
}
|
||||
14
deltachat-jsonrpc/typescript/example/node-demo.js
Normal file
14
deltachat-jsonrpc/typescript/example/node-demo.js
Normal 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...")
|
||||
}
|
||||
327
deltachat-jsonrpc/typescript/generated/client.ts
Normal file
327
deltachat-jsonrpc/typescript/generated/client.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
// 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 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>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
3
deltachat-jsonrpc/typescript/generated/events.ts
Normal file
3
deltachat-jsonrpc/typescript/generated/events.ts
Normal 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");
|
||||
10
deltachat-jsonrpc/typescript/generated/jsonrpc.ts
Normal file
10
deltachat-jsonrpc/typescript/generated/jsonrpc.ts
Normal 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);
|
||||
97
deltachat-jsonrpc/typescript/generated/types.ts
Normal file
97
deltachat-jsonrpc/typescript/generated/types.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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 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 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);};
|
||||
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,(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];
|
||||
51
deltachat-jsonrpc/typescript/package.json
Normal file
51
deltachat-jsonrpc/typescript/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
28
deltachat-jsonrpc/typescript/report_api_coverage.mjs
Normal file
28
deltachat-jsonrpc/typescript/report_api_coverage.mjs
Normal 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")
|
||||
);
|
||||
77
deltachat-jsonrpc/typescript/src/client.ts
Normal file
77
deltachat-jsonrpc/typescript/src/client.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
6
deltachat-jsonrpc/typescript/src/lib.ts
Normal file
6
deltachat-jsonrpc/typescript/src/lib.ts
Normal 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";
|
||||
154
deltachat-jsonrpc/typescript/test/basic.ts
Normal file
154
deltachat-jsonrpc/typescript/test/basic.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
202
deltachat-jsonrpc/typescript/test/online.ts
Normal file
202
deltachat-jsonrpc/typescript/test/online.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
94
deltachat-jsonrpc/typescript/test/test_base.ts
Normal file
94
deltachat-jsonrpc/typescript/test/test_base.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
20
deltachat-jsonrpc/typescript/tsconfig.json
Normal file
20
deltachat-jsonrpc/typescript/tsconfig.json
Normal 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
|
||||
}
|
||||
@@ -11,26 +11,49 @@ Changes to the UIs
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- DONE: We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
- [x] We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- DONE: If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
- [x] If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
||||
- [x] No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
||||
|
||||
- When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
||||
- [ ] When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
||||
|
||||
- When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
||||
- [x] ([#3385](https://github.com/deltachat/deltachat-core-rust/pull/3385)) When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
||||
AND there is a `Chat-Version` header\
|
||||
AND the message is signed correctly
|
||||
AND the From address is (also) in the encrypted (and therefore signed) headers <sup>[[1]](#myfootnote1)</sup>\
|
||||
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
|
||||
|
||||
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
|
||||
|
||||
<a name="myfootnote1">[1]</a>: Without this check, an attacker could replay a message from Alice to Bob. Then Bob's device would do an AEAP transition from Alice's to the attacker's address, allowing for easier phishing.
|
||||
|
||||
<details>
|
||||
<summary>More details about this</summary>
|
||||
Suppose Alice sends a message to Evil (or to a group with both Evil and Bob). Evil then forwards the message to Bob, changing the From and To headers (and if necessary Message-Id) and replacing `addr=alice@example.org;` in the autocrypt header with `addr=evil@example.org;`.
|
||||
|
||||
Then Bob's device sees that there is a message which is signed by Alice's key and comes from Evil's address and would do the AEAP transition, i.e. replace Alice with Evil in all groups and show a message "Alice changed their address from alice@example.org to evil@example.org". Disadvantages for Evil are that Bob's message will be shown on Alice's device, possibly creating confusion/suspicion, and that the usual "Setup changed for..." message will be shown the next time Evil sends a message (because Evil doesn't know Alice's private key).
|
||||
|
||||
Possible mitigations:
|
||||
- if we make the AEAP device message sth. like "Automatically removed alice@example.org and added evil@example.org", then this will create more suspicion, making the phishing harder (we didn't talk about what what the wording should be at all yet).
|
||||
- Add something similar to replay protection to our Autocrypt implementation. This could be done e.g. by adding a second `From` header to the protected headers. If it's present, the receiver then requires it to be the same as the outer `From`, and if it's not present, we don't do AEAP --> **That's what we implemented**
|
||||
|
||||
Note that usually a mail is signed by a key that has a UID matching the from address.
|
||||
|
||||
That's not mandatory for Autocrypt (and in fact, we just keep the old UID when changing the self address, so with AEAP the UID will actually be different than the from address sometimes)
|
||||
|
||||
https://autocrypt.org/level1.html#openpgp-based-key-data says:
|
||||
> The content of the user id packet is only decorative
|
||||
|
||||
</details>
|
||||
|
||||
### Notes:
|
||||
|
||||
- We treat protected and non-protected chats the same
|
||||
@@ -97,3 +120,8 @@ Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
|
||||
Notes during implementing
|
||||
========================
|
||||
|
||||
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.
|
||||
@@ -1,6 +1,12 @@
|
||||
# Webxdc Developer Reference
|
||||
|
||||
(This document may eventually be merged with the [webxdc guidebook](https://deltachat.github.io/webxdc_docs/), where you may currently find other useful information.)
|
||||
This document gives a quick overview about the Webxdc specification,
|
||||
It is meant for both, developing Webxdc apps
|
||||
and developing Webxdc implementations.
|
||||
|
||||
The [Webxdc guidebook](https://deltachat.github.io/webxdc_docs/) shows more detailed information
|
||||
when developing Webxdc apps.
|
||||
|
||||
|
||||
## Webxdc File Format
|
||||
|
||||
@@ -138,9 +144,37 @@ round corners etc. will be added by the implementations as needed.
|
||||
If no icon is set, a default icon will be used.
|
||||
|
||||
|
||||
## Other APIs and Tags Usage Hints
|
||||
|
||||
- `localStorage`, `sessionStorage`, `indexedDB` are okay to be used
|
||||
- `visibilitychange`-events are okay to be used
|
||||
- `window.navigator.language` is okay to be used, on desktop it is the system language
|
||||
- `<a href="localfile.html">` and other internal links are okay to be used
|
||||
- `<a href="mailto:addr@example.org?body=...">`- mailto links are okay to be used
|
||||
- `<meta name="viewport" ...>` usage is okay to be used
|
||||
and useful esp. different webviews have different defaults
|
||||
|
||||
|
||||
### Discouraged Things
|
||||
|
||||
- `document.cookie` is known not to work on desktop and iOS
|
||||
use `localStorage` instead
|
||||
- `unload`-, `beforeunload`- and `pagehide`-events are known not to work on iOS and are flaky on other systems
|
||||
(also partly discouraged by [mozilla](https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event))
|
||||
use `visibilitychange` instead
|
||||
- `<title>` and `document.title` is ignored by Webxdc;
|
||||
use the `name` property from `manifest.toml` instead
|
||||
- newest js features may not work on all webviews,
|
||||
you may want to transpile your code down to an older js version
|
||||
eg. with <https://babeljs.io>
|
||||
- `<a href="https://example.org/foo">` and other external links are blocked by definition;
|
||||
instead, embed content or use `mailto:` link to offer a way for contact
|
||||
- `<input type="file">` is discouraged currently; this may change in future
|
||||
|
||||
|
||||
## Webxdc Examples
|
||||
|
||||
The following example shows an input field and every input is show on all peers.
|
||||
The following example shows an input field and every input is show on all peers.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
@@ -170,30 +204,10 @@ The following example shows an input field and every input is show on all peers
|
||||
</html>
|
||||
```
|
||||
|
||||
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
|
||||
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
|
||||
More examples at [github.com/webxdc](https://github.com/webxdc) and
|
||||
[topic #webxdc](https://github.com/topics/webxdc)
|
||||
|
||||
[github.com/webxdc/hello](https://github.com/webxdc/hello)
|
||||
offers an **Webxdc Tool** that can be used in many browsers without any installation needed.
|
||||
You can also use that repository as a template for your own Webxdc -
|
||||
just clone and start adapting things to your need.
|
||||
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
- [2048](https://github.com/adbenitez/2048.xdc)
|
||||
- [Draw](https://github.com/adbenitez/draw.xdc)
|
||||
- [Poll](https://github.com/r10s/webxdc-poll/)
|
||||
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
|
||||
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc) or in the [webxdc GitHub organization](https://github.com/webxdc)
|
||||
|
||||
|
||||
## Closing Remarks
|
||||
|
||||
- older devices might not have the newest js features in their webview,
|
||||
you may want to transpile your code down to an older js version eg. with https://babeljs.io
|
||||
- viewport and scaling features are implementation specific,
|
||||
if you want to have an explicit behavior, you can add eg.
|
||||
`<meta name="viewport" content="initial-scale=1; user-scalable=no">` to your Webxdc
|
||||
- the `<title>` tag should not be used and its content is usually not displayed;
|
||||
instead, use the `name` property from `manifest.toml`
|
||||
- there are tons of ideas for enhancements of the API and the file format,
|
||||
eg. in the future, we will may define icon- and manifest-files,
|
||||
allow to aggregate the state or add metadata.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
extern crate dirs;
|
||||
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::{
|
||||
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
||||
};
|
||||
@@ -11,8 +12,6 @@ use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::dc_receive_imf::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::download::DownloadState;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
@@ -20,10 +19,11 @@ use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use deltachat::{config, provider};
|
||||
use std::fs;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::fs;
|
||||
|
||||
/// Reset database tables.
|
||||
/// Argument is a bitmask, executing single or multiple actions in one call.
|
||||
@@ -96,10 +96,10 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||
let data = dc_read_file(context, filename).await?;
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = dc_receive_imf(context, &data, false).await {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
if let Err(err) = receive_imf(context, &data, false).await {
|
||||
println!("receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -128,24 +128,20 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
real_spec = rs.unwrap();
|
||||
}
|
||||
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
|
||||
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
|
||||
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
|
||||
read_cnt += 1
|
||||
}
|
||||
} else {
|
||||
/* import a directory */
|
||||
let dir_name = std::path::Path::new(&real_spec);
|
||||
let dir = std::fs::read_dir(dir_name);
|
||||
let dir = fs::read_dir(dir_name).await;
|
||||
if dir.is_err() {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
|
||||
return false;
|
||||
} else {
|
||||
let dir = dir.unwrap();
|
||||
for entry in dir {
|
||||
if entry.is_err() {
|
||||
break;
|
||||
}
|
||||
let entry = entry.unwrap();
|
||||
let mut dir = dir.unwrap();
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
if name.ends_with(".eml") {
|
||||
@@ -191,7 +187,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
DownloadState::Failure => " [⬇ Download failed]",
|
||||
};
|
||||
|
||||
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
|
||||
let temp2 = timestamp_to_str(msg.get_timestamp());
|
||||
let msgtext = msg.get_text();
|
||||
println!(
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{}{} [{}]",
|
||||
@@ -219,6 +215,14 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
msg.get_videochat_url().unwrap_or_default(),
|
||||
msg.get_videochat_type().unwrap_or_default()
|
||||
)
|
||||
} else if msg.get_viewtype() == Viewtype::Webxdc {
|
||||
match msg.get_webxdc_info(context).await {
|
||||
Ok(info) => format!(
|
||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||
info.name, info.icon, info.document, info.summary, info.source_code_url
|
||||
),
|
||||
Err(err) => format!("[get_webxdc_info() failed: {}]", err),
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
@@ -492,7 +496,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let setup_code = create_setup_code(&context);
|
||||
let file_name = blobdir.join("autocrypt-setup-message.html");
|
||||
let file_content = render_setup_file(&context, &setup_code).await?;
|
||||
async_std::fs::write(&file_name, file_content).await?;
|
||||
fs::write(&file_name, file_content).await?;
|
||||
println!(
|
||||
"Setup message written to: {}\nSetup code: {}",
|
||||
file_name.display(),
|
||||
@@ -532,7 +536,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.join("connectivity.html");
|
||||
match context.get_connectivity_html().await {
|
||||
Ok(html) => {
|
||||
fs::write(&file, html)?;
|
||||
fs::write(&file, html).await?;
|
||||
println!("Report written to: {:#?}", file);
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -597,7 +601,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
_ => "",
|
||||
}
|
||||
};
|
||||
let timestr = dc_timestamp_to_str(summary.timestamp);
|
||||
let timestr = timestamp_to_str(summary.timestamp);
|
||||
println!(
|
||||
"{}{}{} [{}]{}",
|
||||
summary
|
||||
@@ -810,7 +814,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!(
|
||||
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} {} {}",
|
||||
location.location_id,
|
||||
dc_timestamp_to_str(location.timestamp),
|
||||
timestamp_to_str(location.timestamp),
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
location.accuracy,
|
||||
@@ -892,7 +896,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "No html-file given.");
|
||||
let path: &Path = arg1.as_ref();
|
||||
let html = &*fs::read(&path)?;
|
||||
let html = &*fs::read(&path).await?;
|
||||
let html = String::from_utf8_lossy(html);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
@@ -1079,7 +1083,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
.unwrap_or_default()
|
||||
.join(format!("msg-{}.html", id.to_u32()));
|
||||
let html = id.get_html(&context).await?.unwrap_or_default();
|
||||
fs::write(&file, html)?;
|
||||
fs::write(&file, html).await?;
|
||||
println!("HTML written to: {:#?}", file);
|
||||
}
|
||||
"listfresh" => {
|
||||
@@ -1233,8 +1237,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
if let Ok(buf) = dc_read_file(&context, &arg1).await {
|
||||
let (width, height) = dc_get_filemeta(&buf)?;
|
||||
if let Ok(buf) = read_file(&context, &arg1).await {
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={}, height={}", width, height);
|
||||
} else {
|
||||
bail!("Command failed.");
|
||||
|
||||
@@ -9,17 +9,18 @@ extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use ansi_term::Color;
|
||||
use anyhow::{bail, Error};
|
||||
use async_std::path::Path;
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::config;
|
||||
use deltachat::context::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::EventType;
|
||||
use deltachat::{EventType, Events};
|
||||
use log::{error, info, warn};
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::config::OutputStreamType;
|
||||
@@ -30,11 +31,11 @@ use rustyline::validate::Validator;
|
||||
use rustyline::{
|
||||
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
|
||||
};
|
||||
use tokio::fs;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use std::fs;
|
||||
|
||||
/// Event Handler
|
||||
fn receive_event(event: EventType) {
|
||||
@@ -298,10 +299,10 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = Context::new(Path::new(&args[1]).to_path_buf(), 0).await?;
|
||||
let context = Context::new(Path::new(&args[1]), 0, Events::new()).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
async_std::task::spawn(async move {
|
||||
tokio::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
receive_event(event.typ);
|
||||
}
|
||||
@@ -316,8 +317,9 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
.output_stream(OutputStreamType::Stdout)
|
||||
.build();
|
||||
let mut selected_chat = ChatId::default();
|
||||
let (reader_s, reader_r) = async_std::channel::bounded(100);
|
||||
let input_loop = async_std::task::spawn_blocking(move || {
|
||||
|
||||
let ctx = context.clone();
|
||||
let input_loop = tokio::task::spawn_blocking(move || {
|
||||
let h = DcHelper {
|
||||
completer: FilenameCompleter::new(),
|
||||
highlighter: MatchingBracketHighlighter::new(),
|
||||
@@ -339,16 +341,30 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
Ok(line) => {
|
||||
// TODO: ignore "set mail_pw"
|
||||
rl.add_history_entry(line.as_str());
|
||||
async_std::task::block_on(reader_s.send(line)).unwrap();
|
||||
let contine = Handle::current().block_on(async {
|
||||
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
|
||||
Ok(ExitResult::Continue) => true,
|
||||
Ok(ExitResult::Exit) => {
|
||||
println!("Exiting ...");
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err);
|
||||
true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !contine {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||
println!("Exiting...");
|
||||
drop(reader_s);
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {}", err);
|
||||
drop(reader_s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -359,15 +375,8 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
Ok::<_, Error>(())
|
||||
});
|
||||
|
||||
while let Ok(line) = reader_r.recv().await {
|
||||
match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await {
|
||||
Ok(ExitResult::Continue) => {}
|
||||
Ok(ExitResult::Exit) => break,
|
||||
Err(err) => println!("Error: {}", err),
|
||||
}
|
||||
}
|
||||
context.stop_io().await;
|
||||
input_loop.await?;
|
||||
input_loop.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -400,7 +409,7 @@ async fn handle_cmd(
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
let oauth2_url =
|
||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
if oauth2_url.is_none() {
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
} else {
|
||||
@@ -417,7 +426,7 @@ async fn handle_cmd(
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
|
||||
let mut qr = get_securejoin_qr(&ctx, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
@@ -437,7 +446,7 @@ async fn handle_cmd(
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
fs::write(&file, svg)?;
|
||||
fs::write(&file, svg).await?;
|
||||
println!("QR code svg written to: {:#?}", file);
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -448,7 +457,7 @@ async fn handle_cmd(
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
dc_join_securejoin(&ctx, arg1).await?;
|
||||
join_securejoin(&ctx, arg1).await?;
|
||||
}
|
||||
}
|
||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||
@@ -458,11 +467,12 @@ async fn handle_cmd(
|
||||
Ok(ExitResult::Continue)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
|
||||
let args = std::env::args().collect();
|
||||
async_std::task::block_on(async move { start(args).await })?;
|
||||
start(args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use deltachat::config;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::EventType;
|
||||
use deltachat::{EventType, Events};
|
||||
|
||||
fn cb(event: EventType) {
|
||||
match event {
|
||||
@@ -29,21 +29,21 @@ fn cb(event: EventType) {
|
||||
}
|
||||
|
||||
/// Run with `RUST_LOG=simple=info cargo run --release --example simple --features repl -- email pw`.
|
||||
#[async_std::main]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pretty_env_logger::try_init_timed().ok();
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
log::info!("creating database {:?}", dbfile);
|
||||
let ctx = Context::new(dbfile.into(), 0)
|
||||
let ctx = Context::new(&dbfile, 0, Events::new())
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
log::info!("info: {:#?}", info);
|
||||
|
||||
let events = ctx.get_event_emitter();
|
||||
let events_spawn = async_std::task::spawn(async move {
|
||||
let events_spawn = tokio::task::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
cb(event.typ);
|
||||
}
|
||||
@@ -80,7 +80,7 @@ async fn main() {
|
||||
}
|
||||
|
||||
// wait for the message to be sent out
|
||||
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
log::info!("fetching chats..");
|
||||
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
|
||||
@@ -96,5 +96,5 @@ async fn main() {
|
||||
ctx.stop_io().await;
|
||||
log::info!("closing");
|
||||
drop(ctx);
|
||||
events_spawn.await;
|
||||
events_spawn.await.unwrap();
|
||||
}
|
||||
|
||||
@@ -60,6 +60,19 @@ building from source or clone this repository and follow this steps:
|
||||
> not inside this folder. (We need this in order to include the rust source
|
||||
> code in the npm package.)
|
||||
|
||||
### Use a git branch in deltachat-desktop
|
||||
|
||||
You can directly install a core branch, but make sure:
|
||||
- that you have typescript in your project dependencies, as it is likely required
|
||||
- you know that there are **no prebuilds** and so core is built during installation which is why it takes so long
|
||||
|
||||
```
|
||||
npm install https://github.com/deltachat/deltachat-core-rust.git#branch
|
||||
```
|
||||
|
||||
If you want prebuilds for a branch that has a core pr, you might find an npm tar.gz package for that branch at <https://download.delta.chat/node/preview/>.
|
||||
The github ci also posts a link to it in the checks for each pr.
|
||||
|
||||
### Use build-from-source in deltachat-desktop
|
||||
|
||||
If you want to use the manually built node bindings in the desktop client (for
|
||||
@@ -104,7 +117,7 @@ $ fnm install 17 --arch x64
|
||||
$ fnm use 17
|
||||
$ node -p process.arch
|
||||
# result should be x64
|
||||
$ cd deltachat-core-rust && rustup target add x86_64-apple-darwin && cd -
|
||||
$ rustup target add x86_64-apple-darwin
|
||||
$ git apply patches/m1_build_use_x86_64.patch
|
||||
$ CARGO_BUILD_TARGET=x86_64-apple-darwin npm run build
|
||||
$ npm run test
|
||||
|
||||
@@ -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, apiVersion: string = "v0") {
|
||||
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, apiVersion, 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)
|
||||
}
|
||||
|
||||
@@ -1,73 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const split = require('split2')
|
||||
|
||||
const data = []
|
||||
const regex = /^#define\s+(\w+)\s+(\w+)/i
|
||||
const header = path.resolve(
|
||||
__dirname,
|
||||
'../../deltachat-ffi/deltachat.h'
|
||||
)
|
||||
const header = path.resolve(__dirname, '../../deltachat-ffi/deltachat.h')
|
||||
|
||||
console.log('Generating constants...')
|
||||
|
||||
fs.createReadStream(header)
|
||||
.pipe(split())
|
||||
.on('data', (line) => {
|
||||
const match = regex.exec(line)
|
||||
if (match) {
|
||||
const key = match[1]
|
||||
const value = parseInt(match[2])
|
||||
if (isNaN(value)) return
|
||||
const header_data = fs.readFileSync(header, 'UTF-8')
|
||||
const regex = /^#define\s+(\w+)\s+(\w+)/gm
|
||||
while (null != (match = regex.exec(header_data))) {
|
||||
const key = match[1]
|
||||
const value = parseInt(match[2])
|
||||
if (!isNaN(value)) {
|
||||
data.push({ key, value })
|
||||
}
|
||||
}
|
||||
|
||||
data.push({ key, value })
|
||||
}
|
||||
delete header_data
|
||||
|
||||
const constants = data
|
||||
.filter(
|
||||
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||
)
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.key < rhs.key) return -1
|
||||
else if (lhs.key > rhs.key) return 1
|
||||
return 0
|
||||
})
|
||||
.on('end', () => {
|
||||
const constants = data
|
||||
.filter(
|
||||
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||
)
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.key < rhs.key) return -1
|
||||
else if (lhs.key > rhs.key) return 1
|
||||
return 0
|
||||
})
|
||||
.map((row) => {
|
||||
return ` ${row.key}: ${row.value}`
|
||||
})
|
||||
.join(',\n')
|
||||
.map((row) => {
|
||||
return ` ${row.key}: ${row.value}`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
const events = data
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.value < rhs.value) return -1
|
||||
else if (lhs.value > rhs.value) return 1
|
||||
return 0
|
||||
})
|
||||
.filter((i) => {
|
||||
return i.key.startsWith('DC_EVENT_')
|
||||
})
|
||||
.map((i) => {
|
||||
return ` ${i.value}: '${i.key}'`
|
||||
})
|
||||
.join(',\n')
|
||||
const events = data
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.value < rhs.value) return -1
|
||||
else if (lhs.value > rhs.value) return 1
|
||||
return 0
|
||||
})
|
||||
.filter((i) => {
|
||||
return i.key.startsWith('DC_EVENT_')
|
||||
})
|
||||
.map((i) => {
|
||||
return ` ${i.value}: '${i.key}'`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
// backwards compat
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../constants.js'),
|
||||
`// Generated!\n\nmodule.exports = {\n${constants}\n}\n`
|
||||
)
|
||||
// backwards compat
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../events.js'),
|
||||
`/* eslint-disable quotes */\n// Generated!\n\nmodule.exports = {\n${events}\n}\n`
|
||||
)
|
||||
// backwards compat
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../constants.js'),
|
||||
`// Generated!\n\nmodule.exports = {\n${constants}\n}\n`
|
||||
)
|
||||
// backwards compat
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../events.js'),
|
||||
`/* eslint-disable quotes */\n// Generated!\n\nmodule.exports = {\n${events}\n}\n`
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../lib/constants.ts'),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, ' =')},\n}\n
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../lib/constants.ts'),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, ' =')},\n}\n
|
||||
// Generated!\n\nexport const EventId2EventName: { [key: number]: string } = {\n${events},\n}\n`
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ const buildArgs = [
|
||||
'build',
|
||||
'--release',
|
||||
'--features',
|
||||
'vendored',
|
||||
'vendored,jsonrpc',
|
||||
'-p',
|
||||
'deltachat_ffi'
|
||||
]
|
||||
|
||||
28
node/segfault.js
Normal file
28
node/segfault.js
Normal 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
50
node/segfault2.js
Normal 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)
|
||||
@@ -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;
|
||||
|
||||
@@ -98,14 +101,6 @@ static void finalize_provider(napi_env env, void* data, void* hint) {
|
||||
}
|
||||
}
|
||||
|
||||
static void finalize_account(napi_env env, void* data, void* hint) {
|
||||
if (data) {
|
||||
dc_accounts_t* dcn_accounts = (dc_accounts_t*)data;
|
||||
//TRACE("cleaning up provider");
|
||||
dc_accounts_unref(dcn_accounts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers.
|
||||
*/
|
||||
@@ -348,7 +343,7 @@ NAPI_METHOD(dcn_start_event_handler) {
|
||||
callback,
|
||||
0,
|
||||
async_resource_name,
|
||||
1,
|
||||
1000, // max_queue_size
|
||||
1,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -371,6 +366,11 @@ NAPI_METHOD(dcn_context_unref) {
|
||||
|
||||
TRACE("Unrefing dc_context");
|
||||
dcn_context->gc = 1;
|
||||
if (dcn_context->event_handler_thread != 0) {
|
||||
dc_stop_io(dcn_context->dc_context);
|
||||
uv_thread_join(&dcn_context->event_handler_thread);
|
||||
dcn_context->event_handler_thread = 0;
|
||||
}
|
||||
dc_context_unref(dcn_context->dc_context);
|
||||
dcn_context->dc_context = NULL;
|
||||
|
||||
@@ -2922,6 +2922,17 @@ NAPI_METHOD(dcn_accounts_unref) {
|
||||
|
||||
TRACE("Unrefing dc_accounts");
|
||||
dcn_accounts->gc = 1;
|
||||
if (dcn_accounts->event_handler_thread != 0) {
|
||||
dc_accounts_stop_io(dcn_accounts->dc_accounts);
|
||||
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;
|
||||
|
||||
@@ -3080,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);
|
||||
@@ -3093,8 +3102,8 @@ static void accounts_event_handler_thread_func(void* arg)
|
||||
}
|
||||
event = dc_accounts_get_next_event(dc_accounts_event_emitter);
|
||||
if (event == NULL) {
|
||||
//TRACE("received NULL event, skipping");
|
||||
continue;
|
||||
TRACE("no more events");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!dcn_accounts->threadsafe_event_handler) {
|
||||
@@ -3171,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) {
|
||||
@@ -3216,7 +3225,7 @@ NAPI_METHOD(dcn_accounts_start_event_handler) {
|
||||
callback,
|
||||
0,
|
||||
async_resource_name,
|
||||
1,
|
||||
1000, // max_queue_size
|
||||
1,
|
||||
NULL,
|
||||
NULL,
|
||||
@@ -3232,6 +3241,125 @@ 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(3);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
NAPI_ARGV_UTF8_MALLOC(api_version, 1);
|
||||
napi_value callback = argv[2];
|
||||
|
||||
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, api_version);
|
||||
|
||||
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() {
|
||||
/**
|
||||
@@ -3502,4 +3630,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);
|
||||
}
|
||||
|
||||
@@ -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) { \
|
||||
|
||||
@@ -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()
|
||||
@@ -128,7 +217,7 @@ describe('Basic offline Tests', function () {
|
||||
|
||||
await expect(
|
||||
context.configure({ addr: 'delta1@delta.localhost' })
|
||||
).to.eventually.be.rejectedWith('Please enter a password.')
|
||||
).to.eventually.be.rejectedWith('Missing (IMAP) password.')
|
||||
await expect(context.configure({ mailPw: 'delta1' })).to.eventually.be
|
||||
.rejected
|
||||
|
||||
@@ -601,8 +690,8 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
strictEqual(chatList.getCount(), 1, 'only one archived')
|
||||
})
|
||||
|
||||
it('Remove qoute from (draft) message', function () {
|
||||
context.addDeviceMessage('test_qoute', 'test')
|
||||
it('Remove quote from (draft) message', function () {
|
||||
context.addDeviceMessage('test_quote', 'test')
|
||||
const msgId = context.getChatMessages(10, 0, 0)[0]
|
||||
const msg = context.messageNew()
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -19,7 +20,6 @@
|
||||
"prebuildify": "^3.0.0",
|
||||
"prebuildify-ci": "^1.0.4",
|
||||
"prettier": "^2.0.5",
|
||||
"split2": "^4.1.0",
|
||||
"typedoc": "^0.17.0",
|
||||
"typescript": "^3.9.10"
|
||||
},
|
||||
@@ -58,8 +58,8 @@
|
||||
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "npm run lint",
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec"
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.86.0"
|
||||
"version": "1.87.0"
|
||||
}
|
||||
3
python/doc/_templates/globaltoc.html
vendored
3
python/doc/_templates/globaltoc.html
vendored
@@ -11,8 +11,7 @@
|
||||
<ul>
|
||||
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
|
||||
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
|
||||
<li><a href="https://web.libera.chat/#deltachat">#deltachat</a></li>
|
||||
</ul>
|
||||
|
||||
<b>#deltachat [freenode]</b>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -18,15 +18,13 @@ if __name__ == "__main__":
|
||||
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||
|
||||
if target == 'release':
|
||||
if target == "release":
|
||||
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
|
||||
cmd.append("--release")
|
||||
|
||||
print("running:", " ".join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll" , shell=True)
|
||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll", shell=True)
|
||||
|
||||
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
||||
subprocess.check_call([
|
||||
sys.executable, "-m", "pip", "install", "-e", "."
|
||||
])
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-e", "."])
|
||||
|
||||
@@ -9,3 +9,6 @@ git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
@@ -1,36 +1,37 @@
|
||||
import setuptools
|
||||
import os
|
||||
import re
|
||||
|
||||
import setuptools
|
||||
|
||||
|
||||
def main():
|
||||
with open("README.rst") as f:
|
||||
long_description = f.read()
|
||||
setuptools.setup(
|
||||
name='deltachat',
|
||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||
name="deltachat",
|
||||
description="Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat",
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imap-tools', 'requests'],
|
||||
author="holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors",
|
||||
install_requires=["cffi>=1.0.0", "pluggy", "imap-tools", "requests"],
|
||||
setup_requires=[
|
||||
'setuptools_scm', # required for compatibility with `python3 setup.py sdist`
|
||||
'pkgconfig',
|
||||
"setuptools_scm", # required for compatibility with `python3 setup.py sdist`
|
||||
"pkgconfig",
|
||||
],
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
entry_points = {
|
||||
'pytest11': [
|
||||
'deltachat.testplugin = deltachat.testplugin',
|
||||
packages=setuptools.find_packages("src"),
|
||||
package_dir={"": "src"},
|
||||
cffi_modules=["src/deltachat/_build.py:ffibuilder"],
|
||||
entry_points={
|
||||
"pytest11": [
|
||||
"deltachat.testplugin = deltachat.testplugin",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Communications :: Email',
|
||||
'Topic :: Software Development :: Libraries',
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Email",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ class Chat(object):
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if other is None:
|
||||
return False
|
||||
return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
|
||||
@@ -22,7 +22,9 @@ class Contact(object):
|
||||
self.account = account
|
||||
self.id = id
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> bool:
|
||||
if other is None:
|
||||
return False
|
||||
return self.account._dc_context == other.account._dc_context and self.id == other.id
|
||||
|
||||
def __ne__(self, other):
|
||||
|
||||
@@ -271,7 +271,8 @@ class EventThread(threading.Thread):
|
||||
data1 = ffi_event.data1
|
||||
if data1 == 0 or data1 == 1000:
|
||||
success = data1 == 1000
|
||||
yield "ac_configure_completed", dict(success=success)
|
||||
comment = ffi_event.data2
|
||||
yield "ac_configure_completed", dict(success=success, comment=comment)
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = account.get_message_by_id(ffi_event.data2)
|
||||
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
||||
|
||||
@@ -38,7 +38,7 @@ class PerAccount:
|
||||
"""log a message related to the account."""
|
||||
|
||||
@account_hookspec
|
||||
def ac_configure_completed(self, success):
|
||||
def ac_configure_completed(self, success, comment):
|
||||
"""Called after a configure process completed."""
|
||||
|
||||
@account_hookspec
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
""" The Message object. """
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from . import const, props
|
||||
from .capi import ffi, lib
|
||||
@@ -26,7 +27,9 @@ class Message(object):
|
||||
msg_id = self.id
|
||||
assert msg_id is not None and msg_id >= 0, repr(msg_id)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> bool:
|
||||
if other is None:
|
||||
return False
|
||||
return self.account == other.account and self.id == other.id
|
||||
|
||||
def __repr__(self):
|
||||
@@ -52,8 +55,8 @@ class Message(object):
|
||||
def new_empty(cls, account, view_type):
|
||||
"""create a non-persistent message.
|
||||
|
||||
:param: view_type is the message type code or one of the strings:
|
||||
"text", "audio", "video", "file", "sticker"
|
||||
:param view_type: the message type code or one of the strings:
|
||||
"text", "audio", "video", "file", "sticker", "videochat", "webxdc"
|
||||
"""
|
||||
if isinstance(view_type, int):
|
||||
view_type_code = view_type
|
||||
@@ -128,6 +131,36 @@ class Message(object):
|
||||
"""mime type of the file (if it exists)"""
|
||||
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||
|
||||
def get_status_updates(self, serial: int = 0) -> list:
|
||||
"""Get the status updates of this webxdc message.
|
||||
|
||||
The status updates may be sent by yourself or by other members.
|
||||
If this message doesn't have a webxdc instance, an empty list is returned.
|
||||
|
||||
:param serial: The last known serial. Pass 0 if there are no known serials to receive all updates.
|
||||
"""
|
||||
return json.loads(
|
||||
from_dc_charpointer(lib.dc_get_webxdc_status_updates(self.account._dc_context, self.id, serial))
|
||||
)
|
||||
|
||||
def send_status_update(self, json_data: Union[str, dict], description: str) -> bool:
|
||||
"""Send an status update for the webxdc instance of this message.
|
||||
|
||||
If the webxdc instance is a draft, the update is not sent immediately.
|
||||
Instead, the updates are collected and sent out in a batch when the instance is actually sent.
|
||||
|
||||
:param json_data: program-readable data, the actual payload.
|
||||
:param description: The user-visible description of JSON data
|
||||
:returns: True on success, False otherwise
|
||||
"""
|
||||
if isinstance(json_data, dict):
|
||||
json_data = json.dumps(json_data, default=str)
|
||||
return bool(
|
||||
lib.dc_send_webxdc_status_update(
|
||||
self.account._dc_context, self.id, as_dc_charpointer(json_data), as_dc_charpointer(description)
|
||||
)
|
||||
)
|
||||
|
||||
def is_system_message(self):
|
||||
"""return True if this message is a system/info message."""
|
||||
return bool(lib.dc_msg_is_info(self._dc_msg))
|
||||
@@ -400,6 +433,14 @@ class Message(object):
|
||||
"""return True if it's a video message."""
|
||||
return self._view_type == const.DC_MSG_VIDEO
|
||||
|
||||
def is_videochat_invitation(self):
|
||||
"""return True if it's a videochat invitation message."""
|
||||
return self._view_type == const.DC_MSG_VIDEOCHAT_INVITATION
|
||||
|
||||
def is_webxdc(self):
|
||||
"""return True if it's a Webxdc message."""
|
||||
return self._view_type == const.DC_MSG_WEBXDC
|
||||
|
||||
def is_file(self):
|
||||
"""return True if it's a file message."""
|
||||
return self._view_type == const.DC_MSG_FILE
|
||||
@@ -419,6 +460,8 @@ _view_type_mapping = {
|
||||
"video": const.DC_MSG_VIDEO,
|
||||
"file": const.DC_MSG_FILE,
|
||||
"sticker": const.DC_MSG_STICKER,
|
||||
"videochat": const.DC_MSG_VIDEOCHAT_INVITATION,
|
||||
"webxdc": const.DC_MSG_WEBXDC,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -241,6 +241,7 @@ def data(request):
|
||||
os.path.normpath(x)
|
||||
for x in [
|
||||
os.path.join(os.path.dirname(request.fspath.strpath), "data"),
|
||||
os.path.join(os.path.dirname(request.fspath.strpath), "..", "..", "test-data"),
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data"),
|
||||
]
|
||||
]
|
||||
@@ -293,8 +294,8 @@ class ACSetup:
|
||||
|
||||
class PendingTracker:
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(this, success):
|
||||
self._configured_events.put((account, success))
|
||||
def ac_configure_completed(this, success: bool, comment: Optional[str]) -> None:
|
||||
self._configured_events.put((account, success, comment))
|
||||
|
||||
account.add_account_plugin(PendingTracker(), name="pending_tracker")
|
||||
self._account2state[account] = self.CONFIGURING
|
||||
@@ -332,9 +333,9 @@ class ACSetup:
|
||||
print("finished, account2state", self._account2state)
|
||||
|
||||
def _pop_config_success(self):
|
||||
acc, success = self._configured_events.get()
|
||||
acc, success, comment = self._configured_events.get()
|
||||
if not success:
|
||||
pytest.fail("configuring online account failed: {}".format(acc))
|
||||
pytest.fail("configuring online account {} failed: {}".format(acc, comment))
|
||||
self._account2state[acc] = self.CONFIGURED
|
||||
return acc
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert len(sys.argv) == 2
|
||||
workspacedir = sys.argv[1]
|
||||
arch = platform.machine()
|
||||
for relpath in os.listdir(workspacedir):
|
||||
if relpath.startswith("deltachat"):
|
||||
p = os.path.join(workspacedir, relpath)
|
||||
@@ -17,7 +15,5 @@ if __name__ == "__main__":
|
||||
p,
|
||||
"-w",
|
||||
workspacedir,
|
||||
"--plat",
|
||||
"manylinux2014_" + arch,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -238,6 +238,70 @@ def test_html_message(acfactory, lp):
|
||||
assert html_text in msg2.html
|
||||
|
||||
|
||||
def test_videochat_invitation_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
text = "You are invited to a video chat, click https://meet.jit.si/WxEGad0gGzX to join."
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
assert not msg2.is_videochat_invitation()
|
||||
|
||||
lp.sec("ac1: prepare and send videochat invitation to ac2")
|
||||
msg1 = Message.new_empty(ac1, "videochat")
|
||||
msg1.set_text(text)
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == text
|
||||
assert msg2.is_videochat_invitation()
|
||||
|
||||
|
||||
def test_webxdc_message(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_webxdc()
|
||||
assert not msg1.send_status_update({"payload": "not an webxdc"}, "invalid")
|
||||
assert not msg1.get_status_updates()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
assert not msg2.is_webxdc()
|
||||
assert not msg1.get_status_updates()
|
||||
|
||||
lp.sec("ac1: prepare and send webxdc instance to ac2")
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
assert msg1.send_status_update({"payload": "test1"}, "some test data")
|
||||
assert msg1.send_status_update({"payload": "test2"}, "more test data")
|
||||
assert len(msg1.get_status_updates()) == 2
|
||||
update1 = msg1.get_status_updates()[0]
|
||||
assert update1["payload"] == "test1"
|
||||
assert len(msg1.get_status_updates(update1["serial"])) == 1
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert msg2.is_webxdc()
|
||||
assert msg2.filename
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
|
||||
@@ -1369,8 +1433,9 @@ def test_set_get_contact_avatar(acfactory, data, lp):
|
||||
|
||||
|
||||
def test_add_remove_member_remote_events(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
ac3_addr = ac3.get_config("addr")
|
||||
# activate local plugin for ac2
|
||||
in_list = queue.Queue()
|
||||
|
||||
@@ -1412,7 +1477,7 @@ def test_add_remove_member_remote_events(acfactory, lp):
|
||||
lp.sec("ac1: add address2")
|
||||
# note that if the above create_chat() would not
|
||||
# happen we would not receive a proper member_added event
|
||||
contact2 = chat.add_contact("devnull@testrun.org")
|
||||
contact2 = chat.add_contact(ac3_addr)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
@@ -1420,7 +1485,7 @@ def test_add_remove_member_remote_events(acfactory, lp):
|
||||
ev = in_list.get()
|
||||
assert ev.action == "added"
|
||||
assert ev.message.get_sender_contact().addr == ac1_addr
|
||||
assert ev.contact.addr == "devnull@testrun.org"
|
||||
assert ev.contact.addr == ac3_addr
|
||||
|
||||
lp.sec("ac1: remove address2")
|
||||
chat.remove_contact(contact2)
|
||||
|
||||
@@ -155,9 +155,11 @@ class TestOfflineContact:
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact3 = None
|
||||
str(contact1)
|
||||
repr(contact1)
|
||||
assert contact1 == contact2
|
||||
assert contact1 != contact3
|
||||
assert contact1.id
|
||||
assert contact1.addr == "some1@example.org"
|
||||
assert contact1.display_name == "some1"
|
||||
@@ -251,10 +253,12 @@ class TestOfflineChat:
|
||||
def test_chat_idempotent(self, chat1, ac1):
|
||||
contact1 = chat1.get_contacts()[0]
|
||||
chat2 = contact1.create_chat()
|
||||
chat3 = None
|
||||
assert chat2.id == chat1.id
|
||||
assert chat2.get_name() == chat1.get_name()
|
||||
assert chat1 == chat2
|
||||
assert not (chat1 != chat2)
|
||||
assert chat1 != chat3
|
||||
|
||||
for ichat in ac1.get_chats():
|
||||
if ichat.id == chat1.id:
|
||||
@@ -406,6 +410,8 @@ class TestOfflineChat:
|
||||
|
||||
def test_message_eq_contains(self, chat1):
|
||||
msg = chat1.send_text("msg1")
|
||||
msg2 = None
|
||||
assert msg != msg2
|
||||
assert msg in chat1.get_messages()
|
||||
assert not (msg not in chat1.get_messages())
|
||||
str(msg)
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestACSetup:
|
||||
monkeypatch.setattr(acc, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(acc)
|
||||
assert pc._account2state[acc] == pc.CONFIGURING
|
||||
pc._configured_events.put((acc, True))
|
||||
pc._configured_events.put((acc, True, None))
|
||||
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
|
||||
pc.wait_one_configured(acc)
|
||||
assert pc._account2state[acc] == pc.CONFIGURED
|
||||
@@ -55,11 +55,11 @@ class TestACSetup:
|
||||
pc.start_configure(ac2)
|
||||
assert pc._account2state[ac1] == pc.CONFIGURING
|
||||
assert pc._account2state[ac2] == pc.CONFIGURING
|
||||
pc._configured_events.put((ac1, True))
|
||||
pc._configured_events.put((ac1, True, None))
|
||||
pc.wait_one_configured(ac1)
|
||||
assert pc._account2state[ac1] == pc.CONFIGURED
|
||||
assert pc._account2state[ac2] == pc.CONFIGURING
|
||||
pc._configured_events.put((ac2, True))
|
||||
pc._configured_events.put((ac2, True, None))
|
||||
pc.bring_online()
|
||||
assert pc._account2state[ac1] == pc.IDLEREADY
|
||||
assert pc._account2state[ac2] == pc.IDLEREADY
|
||||
|
||||
@@ -28,6 +28,10 @@ deps =
|
||||
[testenv:auditwheels]
|
||||
skipsdist = True
|
||||
deps = auditwheel
|
||||
passenv =
|
||||
AUDITWHEEL_ARCH
|
||||
AUDITWHEEL_PLAT
|
||||
AUDITWHEEL_POLICY
|
||||
commands =
|
||||
python tests/auditwheels.py {toxworkdir}/wheelhouse
|
||||
|
||||
@@ -42,8 +46,8 @@ deps =
|
||||
pygments
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
isort --check --profile black src/deltachat examples/ tests/
|
||||
black --check src/deltachat examples/ tests/
|
||||
isort --check setup.py install_python_bindings.py src/deltachat examples/ tests/
|
||||
black --check setup.py install_python_bindings.py src/deltachat examples/ tests/
|
||||
flake8 src/deltachat
|
||||
flake8 tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
@@ -89,4 +93,4 @@ markers =
|
||||
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
ignore = E203, E266, E501, W503
|
||||
ignore = E203, E266, E501, W503
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
edition = "2018"
|
||||
@@ -25,7 +25,7 @@ and an own build machine.
|
||||
|
||||
There is experimental support for triggering a remote Python or Rust test run
|
||||
from your local checkout/branch. You will need to be authorized to login to
|
||||
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
|
||||
the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type::
|
||||
|
||||
scripts/manual_remote_tests.sh rust
|
||||
scripts/manual_remote_tests.sh python
|
||||
|
||||
@@ -78,14 +78,15 @@ jobs:
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: vito/oci-build-task
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/docker-coredeps
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_x86_64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
@@ -177,14 +178,15 @@ jobs:
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: vito/oci-build-task
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/docker-coredeps-arm64
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_aarch64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
@@ -230,3 +232,73 @@ jobs:
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*manylinux201*
|
||||
|
||||
- name: python-musl-x86_64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_x86_64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload musl x86_64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*musllinux_1_1_x86_64*
|
||||
|
||||
8
scripts/coredeps/Dockerfile
Normal file
8
scripts/coredeps/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
ARG BASEIMAGE=quay.io/pypa/manylinux2014_x86_64
|
||||
#ARG BASEIMAGE=quay.io/pypa/musllinux_1_1_x86_64
|
||||
#ARG BASEIMAGE=quay.io/pypa/manylinux2014_aarch64
|
||||
|
||||
FROM $BASEIMAGE
|
||||
RUN pipx install tox
|
||||
COPY install-rust.sh /scripts/
|
||||
RUN /scripts/install-rust.sh
|
||||
20
scripts/coredeps/install-rust.sh
Executable file
20
scripts/coredeps/install-rust.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install Rust
|
||||
#
|
||||
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.61.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$ARCH-unknown-linux-$LIBC"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
|
||||
@@ -1,21 +0,0 @@
|
||||
FROM quay.io/pypa/manylinux2014_aarch64
|
||||
|
||||
# Configure ld.so/ldconfig and pkg-config
|
||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||
|
||||
# Install a recent Perl, needed to install the openssl crate
|
||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||
RUN rm /usr/bin/perl
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||
|
||||
# Install python tools (auditwheels,tox, ...)
|
||||
ADD deps/build_python.sh /builder/build_python.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||
|
||||
# Install Rust
|
||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
OPENSSL_VERSION=1.1.1a
|
||||
OPENSSL_SHA256=fc20130f8b7cbd2fb918b2f14e2f429e109c31ddd0fb38fc5d71d9ffed3f9f41
|
||||
|
||||
curl -O https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
|
||||
echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar xzf openssl-${OPENSSL_VERSION}.tar.gz
|
||||
cd openssl-${OPENSSL_VERSION}
|
||||
./config shared no-ssl2 no-ssl3 -fPIC --prefix=/usr/local
|
||||
|
||||
sed -i "s/^SHLIB_MAJOR=.*/SHLIB_MAJOR=200/" Makefile && \
|
||||
sed -i "s/^SHLIB_MINOR=.*/SHLIB_MINOR=0.0/" Makefile && \
|
||||
sed -i "s/^SHLIB_VERSION_NUMBER=.*/SHLIB_VERSION_NUMBER=200.0.0/" Makefile
|
||||
|
||||
make depend
|
||||
make
|
||||
make install_sw install_ssldirs
|
||||
ldconfig -v | grep ssl
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
PERL_VERSION=5.34.0
|
||||
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
|
||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
||||
cd perl-${PERL_VERSION}
|
||||
|
||||
./Configure -de
|
||||
make
|
||||
make install
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
# Install Rust
|
||||
#
|
||||
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.61.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
|
||||
rustc --version
|
||||
cd ..
|
||||
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
@@ -1,21 +0,0 @@
|
||||
FROM quay.io/pypa/manylinux2014_x86_64
|
||||
|
||||
# Configure ld.so/ldconfig and pkg-config
|
||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||
|
||||
# Install a recent Perl, needed to install the openssl crate
|
||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||
RUN rm /usr/bin/perl
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||
|
||||
# Install python tools (auditwheels,tox, ...)
|
||||
ADD deps/build_python.sh /builder/build_python.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||
|
||||
# Install Rust
|
||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -x
|
||||
|
||||
OPENSSL_VERSION=1.1.1a
|
||||
OPENSSL_SHA256=fc20130f8b7cbd2fb918b2f14e2f429e109c31ddd0fb38fc5d71d9ffed3f9f41
|
||||
|
||||
curl -O https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
|
||||
echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar xzf openssl-${OPENSSL_VERSION}.tar.gz
|
||||
cd openssl-${OPENSSL_VERSION}
|
||||
./config shared no-ssl2 no-ssl3 -fPIC --prefix=/usr/local
|
||||
|
||||
sed -i "s/^SHLIB_MAJOR=.*/SHLIB_MAJOR=200/" Makefile && \
|
||||
sed -i "s/^SHLIB_MINOR=.*/SHLIB_MINOR=0.0/" Makefile && \
|
||||
sed -i "s/^SHLIB_VERSION_NUMBER=.*/SHLIB_VERSION_NUMBER=200.0.0/" Makefile
|
||||
|
||||
make depend
|
||||
make
|
||||
make install_sw install_ssldirs
|
||||
ldconfig -v | grep ssl
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
PERL_VERSION=5.34.0
|
||||
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
|
||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
||||
cd perl-${PERL_VERSION}
|
||||
|
||||
./Configure -de
|
||||
make
|
||||
make install
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x -e
|
||||
|
||||
# we use the python3.7 environment as the base environment
|
||||
/opt/python/cp37-cp37m/bin/pip install tox devpi-client auditwheel
|
||||
|
||||
pushd /usr/bin
|
||||
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/tox
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/devpi
|
||||
ln -s /opt/_internal/cpython-3.7.*/bin/auditwheel
|
||||
|
||||
popd
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user