Compare commits

..

1 Commits

Author SHA1 Message Date
adbenitez
f5ef650b4d python: avoid careless calls to unref functions 2022-06-10 23:13:29 -04:00
114 changed files with 930 additions and 4766 deletions

View File

@@ -1,66 +0,0 @@
name: JSON-RPC API Test
on:
push:
branches: [master]
pull_request:
branches: [master]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.56.0
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v1.3.0
- name: Build
run: cargo build --verbose --features webserver -p deltachat-jsonrpc
- name: Run tests
run: cargo test --verbose --features webserver -p deltachat-jsonrpc
ts_bindings:
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: 1.56.0
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v1.3.0
- name: npm i
run: |
cd deltachat-jsonrpc/typescript
npm i
- name: npm run generate-bindings
run: |
cd deltachat-jsonrpc/typescript
npm run generate-bindings
- name: npm run check ts
run: |
cd deltachat-jsonrpc/typescript
npx tsc --noEmit
- name: run integration tests
run: |
cd deltachat-jsonrpc/typescript
npm run build
cargo build --features webserver
npm run test:integration
- name: run prettier
run: |
cd deltachat-jsonrpc/typescript
npm run prettier:check

View File

@@ -120,9 +120,6 @@ 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

View File

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

View File

@@ -3,19 +3,11 @@
## Unreleased
### 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
### Fixes
- set a default error if NDN does not provide an error
- 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
- python: avoid careless calls to unref functions
### API-Changes
- python: added `Message.get_status_updates()` #3416

View File

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

485
Cargo.lock generated
View File

@@ -118,12 +118,6 @@ version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
[[package]]
name = "arrayvec"
version = "0.5.2"
@@ -300,27 +294,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "async-session"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f"
dependencies = [
"anyhow",
"async-std",
"async-trait",
"base64 0.12.3",
"bincode",
"blake3",
"chrono",
"hmac 0.8.1",
"kv-log-macro",
"rand 0.7.3",
"serde",
"serde_json",
"sha2 0.9.9",
]
[[package]]
name = "async-smtp"
version = "0.4.0"
@@ -340,20 +313,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "async-sse"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10"
dependencies = [
"async-channel",
"async-std",
"http-types",
"log",
"memchr",
"pin-project-lite 0.1.12",
]
[[package]]
name = "async-std"
version = "1.11.0"
@@ -377,7 +336,7 @@ dependencies = [
"memchr",
"num_cpus",
"once_cell",
"pin-project-lite 0.2.9",
"pin-project-lite",
"pin-utils",
"slab",
"wasm-bindgen-futures",
@@ -428,19 +387,6 @@ dependencies = [
"syn",
]
[[package]]
name = "async-tungstenite"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07b30ef0ea5c20caaa54baea49514a206308989c68be7ecd86c7f956e4da6378"
dependencies = [
"futures-io",
"futures-util",
"log",
"pin-project-lite 0.2.9",
"tungstenite",
]
[[package]]
name = "atomic-waker"
version = "1.0.0"
@@ -512,15 +458,6 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitfield"
version = "0.13.2"
@@ -545,21 +482,6 @@ dependencies = [
"wyz",
]
[[package]]
name = "blake3"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if 0.1.10",
"constant_time_eq",
"crypto-mac 0.8.0",
"digest 0.9.0",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
@@ -685,12 +607,6 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cache-padded"
version = "1.2.0"
@@ -763,7 +679,6 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"serde",
"time 0.1.44",
"winapi",
]
@@ -846,18 +761,6 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "convert_case"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8"
[[package]]
name = "cookie"
version = "0.14.4"
@@ -867,7 +770,7 @@ dependencies = [
"aes-gcm",
"base64 0.13.0",
"hkdf",
"hmac 0.10.1",
"hmac",
"percent-encoding",
"rand 0.8.5",
"sha2 0.9.9",
@@ -1024,16 +927,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "crypto-mac"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "crypto-mac"
version = "0.10.1"
@@ -1104,28 +997,8 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
dependencies = [
"darling_core 0.10.2",
"darling_macro 0.10.2",
]
[[package]]
name = "darling"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
"darling_core 0.13.4",
"darling_macro 0.13.4",
]
[[package]]
name = "darling"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
dependencies = [
"darling_core 0.14.1",
"darling_macro 0.14.1",
"darling_core",
"darling_macro",
]
[[package]]
@@ -1138,35 +1011,7 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim 0.9.3",
"syn",
]
[[package]]
name = "darling_core"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn",
]
[[package]]
name = "darling_core"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"strsim",
"syn",
]
@@ -1176,29 +1021,7 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
dependencies = [
"darling_core 0.10.2",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
"darling_core 0.13.4",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
dependencies = [
"darling_core 0.14.1",
"darling_core",
"quote",
"syn",
]
@@ -1312,28 +1135,6 @@ dependencies = [
"zip",
]
[[package]]
name = "deltachat-jsonrpc"
version = "1.86.0"
dependencies = [
"anyhow",
"async-channel",
"async-std",
"deltachat",
"env_logger 0.9.0",
"futures",
"log",
"num-traits",
"serde",
"serde_json",
"tempfile",
"tide",
"tide-websockets",
"typescript-type-def",
"yerpc",
"yerpc-tide",
]
[[package]]
name = "deltachat_derive"
version = "2.0.0"
@@ -1349,7 +1150,6 @@ dependencies = [
"anyhow",
"async-std",
"deltachat",
"deltachat-jsonrpc",
"human-panic",
"libc",
"num-traits",
@@ -1364,7 +1164,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
dependencies = [
"darling 0.10.2",
"darling",
"derive_builder_core",
"proc-macro2",
"quote",
@@ -1377,7 +1177,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
dependencies = [
"darling 0.10.2",
"darling",
"proc-macro2",
"quote",
"syn",
@@ -1622,34 +1422,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [
"atty",
"humantime 1.3.0",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "env_logger"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"atty",
"humantime 2.1.0",
"log",
"regex",
"termcolor",
]
[[package]]
name = "erased-serde"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad132dd8d0d0b546348d7d86cb3191aad14b34e5f979781fc005c80d4ac67ffd"
dependencies = [
"serde",
]
[[package]]
name = "errno"
version = "0.2.8"
@@ -1750,22 +1528,6 @@ dependencies = [
"windows-sys 0.30.0",
]
[[package]]
name = "femme"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2997b612abb06bc299486c807e68c5fd12e7618e69cf34c5958ca6b575674403"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "filetime"
version = "0.2.16"
@@ -1884,7 +1646,7 @@ dependencies = [
"futures-io",
"memchr",
"parking",
"pin-project-lite 0.2.9",
"pin-project-lite",
"waker-fn",
]
@@ -1924,7 +1686,7 @@ dependencies = [
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite 0.2.9",
"pin-project-lite",
"pin-utils",
"slab",
]
@@ -2051,17 +1813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
dependencies = [
"digest 0.9.0",
"hmac 0.10.1",
]
[[package]]
name = "hmac"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
dependencies = [
"crypto-mac 0.8.0",
"digest 0.9.0",
"hmac",
]
[[package]]
@@ -2070,7 +1822,7 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [
"crypto-mac 0.10.1",
"crypto-mac",
"digest 0.9.0",
]
@@ -2085,17 +1837,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "http"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.2",
]
[[package]]
name = "http-client"
version = "6.5.2"
@@ -2127,7 +1868,7 @@ dependencies = [
"cookie",
"futures-lite",
"infer",
"pin-project-lite 0.2.9",
"pin-project-lite",
"rand 0.7.3",
"serde",
"serde_json",
@@ -2172,12 +1913,6 @@ dependencies = [
"quick-error 1.2.3",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -2227,15 +1962,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
[[package]]
name = "input_buffer"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413"
dependencies = [
"bytes",
]
[[package]]
name = "instant"
version = "0.1.12"
@@ -2428,7 +2154,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if 1.0.0",
"serde",
"value-bag",
]
@@ -2951,12 +2676,6 @@ dependencies = [
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777"
[[package]]
name = "pin-project-lite"
version = "0.2.9"
@@ -3051,7 +2770,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
dependencies = [
"env_logger 0.7.1",
"env_logger",
"log",
]
@@ -3368,12 +3087,6 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "route-recognizer"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e"
[[package]]
name = "rsa"
version = "0.3.0"
@@ -3615,15 +3328,6 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_fmt"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2963a69a2b3918c1dc75a45a18bd3fcd1120e31d3f59deb1b2f9b5d5ffb8baa4"
dependencies = [
"serde",
]
[[package]]
name = "serde_json"
version = "1.0.81"
@@ -3891,7 +3595,7 @@ dependencies = [
"async-channel",
"cfg-if 1.0.0",
"futures-core",
"pin-project-lite 0.2.9",
"pin-project-lite",
]
[[package]]
@@ -3906,12 +3610,6 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.0"
@@ -3954,20 +3652,11 @@ dependencies = [
"log",
"mime_guess",
"once_cell",
"pin-project-lite 0.2.9",
"pin-project-lite",
"serde",
"serde_json",
]
[[package]]
name = "sval"
version = "1.0.0-alpha.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08"
dependencies = [
"serde",
]
[[package]]
name = "syn"
version = "1.0.95"
@@ -4066,47 +3755,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tide"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0"
dependencies = [
"async-h1",
"async-session",
"async-sse",
"async-std",
"async-trait",
"femme",
"futures-util",
"http-client",
"http-types",
"kv-log-macro",
"log",
"pin-project-lite 0.2.9",
"route-recognizer",
"serde",
"serde_json",
]
[[package]]
name = "tide-websockets"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3592c5cb5cb1b7a2ff3a0e5353170c1bb5b104b2f66dd06f73304169b52cc725"
dependencies = [
"async-dup",
"async-std",
"async-tungstenite",
"base64 0.13.0",
"futures-util",
"pin-project",
"serde",
"serde_json",
"sha-1 0.9.8",
"tide",
]
[[package]]
name = "time"
version = "0.1.44"
@@ -4187,7 +3835,7 @@ version = "1.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395"
dependencies = [
"pin-project-lite 0.2.9",
"pin-project-lite",
]
[[package]]
@@ -4251,26 +3899,6 @@ dependencies = [
"cfg-if 0.1.10",
]
[[package]]
name = "tungstenite"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8dada8c1a3aeca77d6b51a4f1314e0f4b8e438b7b1b71e3ddaca8080e4093"
dependencies = [
"base64 0.13.0",
"byteorder",
"bytes",
"http",
"httparse",
"input_buffer",
"log",
"rand 0.8.5",
"sha-1 0.9.8",
"thiserror",
"url",
"utf-8",
]
[[package]]
name = "twofish"
version = "0.5.0"
@@ -4288,28 +3916,6 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "typescript-type-def"
version = "0.5.1"
source = "git+https://github.com/Frando/rust-typescript-type-def?branch=yerpc#e3215df5b594f702a9725d81e1c90696fe9d30dd"
dependencies = [
"serde_json",
"typescript-type-def-derive",
]
[[package]]
name = "typescript-type-def-derive"
version = "0.5.1"
source = "git+https://github.com/Frando/rust-typescript-type-def?branch=yerpc#e3215df5b594f702a9725d81e1c90696fe9d30dd"
dependencies = [
"darling 0.13.4",
"ident_case",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicase"
version = "2.6.0"
@@ -4390,12 +3996,6 @@ dependencies = [
"serde",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.0"
@@ -4428,10 +4028,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55"
dependencies = [
"ctor",
"erased-serde",
"serde",
"serde_fmt",
"sval",
"version_check 0.9.4",
]
@@ -4489,8 +4085,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
dependencies = [
"cfg-if 1.0.0",
"serde",
"serde_json",
"wasm-bindgen-macro",
]
@@ -4724,49 +4318,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "yerpc"
version = "0.1.0"
source = "git+https://github.com/Frando/yerpc#1443c26322d68c6d8593636506edc81411309951"
dependencies = [
"anyhow",
"async-channel",
"async-mutex",
"async-trait",
"futures",
"log",
"serde",
"serde_json",
"typescript-type-def",
"yerpc_derive",
]
[[package]]
name = "yerpc-tide"
version = "0.1.0"
source = "git+https://github.com/Frando/yerpc#1443c26322d68c6d8593636506edc81411309951"
dependencies = [
"anyhow",
"async-std",
"futures",
"serde_json",
"tide",
"tide-websockets",
"yerpc",
]
[[package]]
name = "yerpc_derive"
version = "0.1.0"
source = "git+https://github.com/Frando/yerpc#1443c26322d68c6d8593636506edc81411309951"
dependencies = [
"convert_case",
"darling 0.14.1",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zeroize"
version = "1.3.0"

View File

@@ -28,7 +28,7 @@ async-tar = { version = "0.4", default-features=false }
backtrace = "0.3"
base64 = "0.13"
bitflags = "1.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
chrono = "0.4"
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" }
@@ -91,7 +91,6 @@ tempfile = "3"
members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc"
]
[[example]]

View File

@@ -102,6 +102,9 @@ $ 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

View File

@@ -2,16 +2,13 @@ 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, Events::new())
.await
.unwrap();
let context = Context::new(dbfile.into(), id).await.unwrap();
let book = (0..n)
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))

View File

@@ -6,13 +6,10 @@ 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, Events::new())
.await
.unwrap();
let context = Context::new(dbfile.into(), id).await.unwrap();
for c in chats.iter().take(10) {
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
@@ -24,9 +21,7 @@ fn criterion_benchmark(c: &mut Criterion) {
// 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, Events::new())
.await
.unwrap();
let context = Context::new((&path).into(), 100).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()

View File

@@ -3,7 +3,6 @@ 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();
@@ -13,9 +12,8 @@ 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, Events::new()).await.unwrap()
});
let context =
async_std::task::block_on(async { Context::new(path.into(), 100).await.unwrap() });
c.bench_function("chatlist:try_load (Get Chatlist)", |b| {
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_list_benchmark(black_box(&context)))

View File

@@ -8,7 +8,6 @@ use deltachat::{
context::Context,
dc_receive_imf::dc_receive_imf,
imex::{imex, ImexMode},
Events,
};
use tempfile::tempdir;
@@ -43,9 +42,7 @@ 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, Events::new())
.await
.unwrap();
let context = Context::new(dbfile.into(), id).await.unwrap();
let backup: PathBuf = std::env::current_dir()
.unwrap()

View File

@@ -1,15 +1,12 @@
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();
let id = 100;
let context = Context::new(dbfile.into(), id, Events::new())
.await
.unwrap();
let context = Context::new(dbfile.into(), id).await.unwrap();
for _ in 0..10u32 {
context.search_msgs(None, "hello").await.unwrap();

View File

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

View File

@@ -23,7 +23,7 @@ typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
/**
* @mainpage Getting started
@@ -1791,11 +1791,6 @@ 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.
@@ -3984,7 +3979,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
* "forwarded"; you won't expect other data to be send to the new recipient,
* "forwared"; 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
@@ -5178,55 +5173,6 @@ 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, 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
* If NULL is returned, the accounts_t belonging to the jsonrpc instance is unref'd and no more events will come;
* in this case, free the jsonrpc instance using dc_jsonrpc_unref().
*/
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* @class dc_event_emitter_t
*

View File

@@ -18,7 +18,6 @@ use std::fmt::Write;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
@@ -78,11 +77,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,
Events::new(),
))
block_on(Context::new(as_path(dbfile).to_path_buf().into(), id))
} else {
eprintln!("blobdir can not be defined explicitly anymore");
return ptr::null_mut();
@@ -109,7 +104,6 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
match block_on(Context::new_closed(
as_path(dbfile).to_path_buf().into(),
id,
Events::new(),
)) {
Ok(context) => Box::into_raw(Box::new(context)),
Err(err) => {
@@ -4094,11 +4088,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: Arc<RwLock<Accounts>>,
inner: RwLock<Accounts>,
}
impl Deref for AccountsWrapper {
type Target = Arc<RwLock<Accounts>>;
type Target = RwLock<Accounts>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -4107,7 +4101,7 @@ impl Deref for AccountsWrapper {
impl AccountsWrapper {
fn new(accounts: Accounts) -> Self {
let inner = Arc::new(RwLock::new(accounts));
let inner = RwLock::new(accounts);
Self { inner }
}
}
@@ -4382,7 +4376,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 = EventEmitter;
pub type dc_accounts_event_emitter_t = deltachat::accounts::EventEmitter;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
@@ -4425,77 +4419,3 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
}
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use super::*;
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{MessageHandle, RpcHandle};
pub struct dc_jsonrpc_instance_t {
receiver: async_std::channel::Receiver<deltachat_jsonrpc::yerpc::Message>,
handle: MessageHandle<CommandApi>,
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *mut dc_accounts_t,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
}
let cmd_api =
deltachat_jsonrpc::api::CommandApi::new_from_arc((*account_manager).inner.clone());
let (request_handle, receiver) = RpcHandle::new();
let handle = MessageHandle::new(request_handle, cmd_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };
Box::into_raw(Box::new(instance))
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_unref(jsonrpc_instance: *mut dc_jsonrpc_instance_t) {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
Box::from_raw(jsonrpc_instance);
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_request(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
request: *const libc::c_char,
) {
if jsonrpc_instance.is_null() || request.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_request()");
return;
}
let api = &*jsonrpc_instance;
let handle = &api.handle;
let request = to_string_lossy(request);
async_std::task::spawn(async move {
handle.handle_message(&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;
async_std::task::block_on(api.receiver.recv())
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
}

View File

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

View File

@@ -1,39 +0,0 @@
[package]
name = "deltachat-jsonrpc"
version = "1.86.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
default-run = "webserver"
license = "MPL-2.0"
[[bin]]
name = "webserver"
path = "src/webserver.rs"
required-features = ["webserver"]
[dependencies]
anyhow = "1"
async-std = { version = "1", features = ["attributes"] }
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 = { git = "https://github.com/Frando/yerpc", features = ["anyhow"] }
typescript-type-def = { git = "https://github.com/Frando/rust-typescript-type-def", branch = "yerpc", features = ["json_value"] }
# optional, depended on features
env_logger = { version = "0.9.0", optional = true }
tide = { version = "0.16.0", optional = true }
tide-websockets = { version = "0.4.0", optional = true }
yerpc-tide = { git = "https://github.com/Frando/yerpc", optional = true }
[features]
default = []
webserver = ["env_logger", "tide", "tide-websockets", "yerpc-tide"]
[profile.release]
lto = true

View File

@@ -1,75 +0,0 @@
# deltachat-jsonrpc
## Build Requirements
- Linux or Mac, scrips make use of features like `>` pipes and `&&` (maybe the newer versions of powershell support them, but I didn't try that.)
- rust (installed via rustup)
## Start the webserver
The webserver is an example usage. Goal of it is to be usable both as example and as base for deltachat-kaiOS.
```sh
RUST_LOG=info cargo run --features webserver
```
## Generate Typescript Bindings
```sh
cd typescript
npm i
npm run build
```
## Run the development example
Mac
```sh
alias firefox=/Applications/Firefox.app/Contents/MacOS/firefox
npm run example:build && firefox --devtools $(pwd)/example/browser-example.html
```
Linux:
```sh
npm run example:run
```
## Compiling server for kaiOS or android:
```sh
cross build --features=webserver --target armv7-linux-androideabi --release
```
## Run the tests
### Rust tests
```
cargo test --features=webserver
```
### Typescript
```
cd typescript
npm run test
```
For the online tests to run you need a test account token for a mailadm instance,
you can use docker to spin up a local instance: https://github.com/deltachat/docker-mailadm
> set the env var `DCC_NEW_TMP_EMAIL` to your mailadm token: example:
> `DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=1h_195dksa6544`
If your test fail with server shutdown at the start, then you might have a process from a last run still running probably and you need to kill that process manually to continue.
#### Test Coverage
You can test coverage with `npm run coverage`, but you need to have `DCC_NEW_TMP_EMAIL` set, otherwise the result will be useless because some functions can only be tested with the online tests.
> If you are offline and want to see the coverage results anyway (even though they are NOT correct), you can bypass the error with `COVERAGE_OFFLINE=1 npm run coverage`
Open `coverage/index.html` for a detailed report.
`bindings.ts` is probably the most interesting file for coverage, because it describes the api functions.

View File

@@ -1,347 +0,0 @@
## Core system
- [X] Base structure of JSON API code
- [X] Implement the first methods for testing + the code that should later be generated by the proc macro
- [X] Create the proc macro
- [X] json api
- [X] ts types
- [X] arguments (no args, one argument, multiple args)
- [X] return type
- [X] custom types as type aliases that ts file looks prettier
## Pre - MVP
- [X] Web socket server
- [WIP] Web socket client (ts)
- [X] backend connection state changed events
- [X] Reconnect on connection loss / connection state
- [ ] find a way to type the event emitter callback functions
- [X] Events
## MVP
- [X] mocha integration test for ts api
- [X] basic tests
- [X] advanced / "online tests" (mailadm for burner accounts)
- [ ] coverage for a majority of the API
- [ ] Blobs served
- [ ] Blob upload (for attachments, setting profile-picture, importing backup and so on)
- [ ] Web push API? At least some kind of notification hook closure this lib can accept.
## Other Ideas
- [ ] 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
- [X] GH action for tests (rust and typescript)
- [X] rust test
- [X] rust fmt
- [X] rust clippy
- [X] tsc check
- [X] prettier
- [X] mocha
- [X] scripts to check&fix prettier formatting
## Apis
replicate desktop api feature set:
(this feature set is based on desktop version `1.20`, needs to be updated in the future)
```rs
struct sendMessageParams {
text: Option<String>,
filename: Option<String>, // TODO we need to think about blobs some more
location: Option<(u32,u32)>,
quote_message_id: Option<u32>,
}
struct QrCodeResponse = {
state: u32 // also enum in reality, for simlicity u32 here
id: u32
text1: String
}
impl Api {
// root ---------------------------------------------------------------
// NEEDS_THE_BLOB_QUESTION_ANSWERED_EVENTUALLY
async fn sc_set_profile_picture(&self, new_image: String) -> Result<()> {}
// NEEDS_THE_BLOB_QUESTION_ANSWERED_EVENTUALLY
// 'getProfilePicture' equals to `dc.getContact(C.DC_CONTACT_ID_SELF).getProfileImage()` or `dc.get_config("selfavatar")`
async fn sc_join_secure_join(&self, qrCode: String) -> Result<u32> {}
async fn sc_stop_ongoing_process(&self) -> Result<u32> {}
async fn sc_check_qr_code(&self, qrCode: String) -> Result<QrCodeResponse> {}
// login ----------------------------------------------------
// INFO: login functions need to call stop&start io where applicable
// login.newLogin:
// do instead in frontend:
// 1. call `add_account`
// 2. call `select_account`
// 3. set credentials via set config
// 4. call `sc_configure`
// login.getLogins - is already implemented: `get_all_accounts`
// login.loadAccount - Basically `select_account`
// login.logout -> TODO: unselect account, which isn't implemented in the core yet
// login.forgetAccount -> `remove_account`
// login.getLastLoggedInAccount -> `get_selected_account_id`
// login.updateCredentials -> do instead: set config then call `sc_configure`
// backup -------------------------------------------------------------
// INFO: backup functions need to call stop&start io
// NEEDS_THE_BLOB_QUESTION_ANSWERED_EVENTUALLY
async fn sc_backup_export(&self, out_dir: String) -> Result<()> {}
// NEEDS_THE_BLOB_QUESTION_ANSWERED_EVENTUALLY
async fn sc_backup_import(&self, file: String) -> Result<()> {} // will not return the same as in desktop because this function imports backup to the current context unlike it was in desktop
// chatList -----------------------------------------------------------
// chatList.selectChat - will be removed from desktop
// chatList.getSelectedChatId - will be removed from desktop
// chatList.onChatModified - will be removed from desktop
async fn sc_chatlist_get_general_fresh_message_counter(&self) -> Result<u32> // this method might be used for a favicon badge counter
// contacts ------------------------------------------------------------
async fn sc_contacts_change_nickname(&self, contact_id: u32, new_name: String) -> Result<()>
// contacts.getChatIdByContactId - very similar to sc_contacts_create_chat_by_contact_id
// contacts.getDMChatId - very similar to sc_contacts_create_chat_by_contact_id
async fn sc_contacts_get_encryption_info(&self, contact_id: u32) -> Result<String>
async fn sc_contacts_lookup_contact_id_by_addr(&self, email: String) -> Result<u32>
}
```
```ts
class DeltaRemote {
// chat ---------------------------------------------------------------
call(
fnName: 'chat.getChatMedia',
chatId: number,
msgType1: number,
msgType2: number
): Promise<MessageType[]>
call(fnName: 'chat.getEncryptionInfo', chatId: number): Promise<string>
call(fnName: 'chat.getQrCode', chatId?: number): Promise<string>
call(fnName: 'chat.leaveGroup', chatId: number): Promise<void>
call(fnName: 'chat.setName', chatId: number, name: string): Promise<boolean>
call(
fnName: 'chat.modifyGroup',
chatId: number,
name: string,
image: string,
remove: number[],
add: number[]
): Promise<boolean>
call(
fnName: 'chat.addContactToChat',
chatId: number,
contactId: number
): Promise<boolean>
call(
fnName: 'chat.setProfileImage',
chatId: number,
newImage: string
): Promise<boolean>
call(
fnName: 'chat.setMuteDuration',
chatId: number,
duration: MuteDuration
): Promise<boolean>
call(
fnName: 'chat.createGroupChat',
verified: boolean,
name: string
): Promise<number>
call(fnName: 'chat.delete', chatId: number): Promise<void>
call(
fnName: 'chat.setVisibility',
chatId: number,
visibility:
| C.DC_CERTCK_AUTO
| C.DC_CERTCK_STRICT
| C.DC_CHAT_VISIBILITY_PINNED
): Promise<void>
call(fnName: 'chat.getChatContacts', chatId: number): Promise<number[]>
call(fnName: 'chat.markNoticedChat', chatId: number): Promise<void>
call(fnName: 'chat.getChatEphemeralTimer', chatId: number): Promise<number>
call(
fnName: 'chat.setChatEphemeralTimer',
chatId: number,
ephemeralTimer: number
): Promise<void>
call(fnName: 'chat.sendVideoChatInvitation', chatId: number): Promise<number>
call(
fnName: 'chat.decideOnContactRequest',
messageId: number,
decision:
| C.DC_DECISION_START_CHAT
| C.DC_DECISION_NOT_NOW
| C.DC_DECISION_BLOCK
): Promise<number>
// locations ----------------------------------------------------------
call(
fnName: 'locations.setLocation',
latitude: number,
longitude: number,
accuracy: number
): Promise<void>
call(
fnName: 'locations.getLocations',
chatId: number,
contactId: number,
timestampFrom: number,
timestampTo: number
): Promise<JsonLocations>
// NOTHING HERE that is called directly from the frontend, yet
// messageList --------------------------------------------------------
call(
fnName: 'messageList.sendMessage',
chatId: number,
params: sendMessageParams
): Promise<[number, MessageType | null]>
call(
fnName: 'messageList.sendSticker',
chatId: number,
stickerPath: string
): Promise<void>
call(fnName: 'messageList.deleteMessage', id: number): Promise<void>
call(fnName: 'messageList.getMessageInfo', msgId: number): Promise<string>
call(
fnName: 'messageList.getDraft',
chatId: number
): Promise<MessageType | null>
call(
fnName: 'messageList.setDraft',
chatId: number,
{
text,
file,
quotedMessageId,
}: { text?: string; file?: string; quotedMessageId?: number }
): Promise<void>
call(
fnName: 'messageList.messageIdToJson',
id: number
): Promise<{ msg: null } | MessageType>
call(
fnName: 'messageList.forwardMessage',
msgId: number,
chatId: number
): Promise<void>
call(
fnName: 'messageList.searchMessages',
query: string,
chatId?: number
): Promise<number[]>
call(
fnName: 'messageList.msgIds2SearchResultItems',
msgIds: number[]
): Promise<{ [id: number]: MessageSearchResult }>
call(
fnName: 'messageList.saveMessageHTML2Disk',
messageId: number
): Promise<string>
// settings -----------------------------------------------------------
call(fnName: 'settings.keysImport', directory: string): Promise<void>
call(fnName: 'settings.keysExport', directory: string): Promise<void>
call(
fnName: 'settings.serverFlags',
{
mail_security,
send_security,
}: {
mail_security?: string
send_security?: string
}
): Promise<number | ''>
call(
fnName: 'settings.setDesktopSetting',
key: keyof DesktopSettings,
value: string | number | boolean
): Promise<boolean>
call(fnName: 'settings.getDesktopSettings'): Promise<DesktopSettings>
call(
fnName: 'settings.saveBackgroundImage',
file: string,
isDefaultPicture: boolean
): Promise<string>
call(
fnName: 'settings.estimateAutodeleteCount',
fromServer: boolean,
seconds: number
): Promise<number>
// stickers -----------------------------------------------------------
call(
fnName: 'stickers.getStickers'
): Promise<{
[key: string]: string[]
}> // todo move to extras? because its not directly elated to core
// context ------------------------------------------------------------
call(fnName: 'context.maybeNetwork'): Promise<void>
// burner accounts ------------------------------------------------------------
call(
fnName: 'burnerAccounts.create',
url: string
): Promise<{ email: string; password: string }> // think about how to improve that api - probably use core api instead
// extras -------------------------------------------------------------
call(fnName: 'extras.getLocaleData', locale: string): Promise<LocaleData>
call(fnName: 'extras.setLocale', locale: string): Promise<void>
call(
fnName: 'extras.getActiveTheme'
): Promise<{
theme: Theme
data: string
} | null>
call(fnName: 'extras.setThemeFilePath', address: string): void
call(fnName: 'extras.getAvailableThemes'): Promise<Theme[]>
call(fnName: 'extras.setTheme', address: string): Promise<boolean>
// catchall: ----------------------------------------------------------
call(fnName: string): Promise<any>
call(fnName: string, ...args: any[]): Promise<any> {
return _callDcMethodAsync(fnName, ...args)
}
}
export const DeltaBackend = new DeltaRemote()
```
after that, or while doing it adjust api to be more complete
TODO different test to simulate two devices:
to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer

View File

@@ -1,154 +0,0 @@
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)),
};
json!({
"id": event_type_to_string(event.typ),
"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,
}
fn event_type_to_string(event: EventType) -> EventTypeName {
use EventTypeName::*;
match event {
EventType::Info(_) => Info,
EventType::SmtpConnected(_) => SmtpConnected,
EventType::ImapConnected(_) => ImapConnected,
EventType::SmtpMessageSent(_) => SmtpMessageSent,
EventType::ImapMessageDeleted(_) => ImapMessageDeleted,
EventType::ImapMessageMoved(_) => ImapMessageMoved,
EventType::NewBlobFile(_) => NewBlobFile,
EventType::DeletedBlobFile(_) => DeletedBlobFile,
EventType::Warning(_) => Warning,
EventType::Error(_) => Error,
EventType::ErrorSelfNotInGroup(_) => ErrorSelfNotInGroup,
EventType::MsgsChanged { .. } => MsgsChanged,
EventType::IncomingMsg { .. } => IncomingMsg,
EventType::MsgsNoticed(_) => MsgsNoticed,
EventType::MsgDelivered { .. } => MsgDelivered,
EventType::MsgFailed { .. } => MsgFailed,
EventType::MsgRead { .. } => MsgRead,
EventType::ChatModified(_) => ChatModified,
EventType::ChatEphemeralTimerModified { .. } => ChatEphemeralTimerModified,
EventType::ContactsChanged(_) => ContactsChanged,
EventType::LocationChanged(_) => LocationChanged,
EventType::ConfigureProgress { .. } => ConfigureProgress,
EventType::ImexProgress(_) => ImexProgress,
EventType::ImexFileWritten(_) => ImexFileWritten,
EventType::SecurejoinInviterProgress { .. } => SecurejoinInviterProgress,
EventType::SecurejoinJoinerProgress { .. } => SecurejoinJoinerProgress,
EventType::ConnectivityChanged => ConnectivityChanged,
EventType::SelfavatarChanged => SelfavatarChanged,
EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate,
}
}
#[cfg(test)]
#[test]
fn generate_events_ts_types_definition() {
let events = {
let mut buf = Vec::new();
let options = typescript_type_def::DefinitionFileOptions {
root_namespace: None,
..typescript_type_def::DefinitionFileOptions::default()
};
typescript_type_def::write_definition_file::<_, EventTypeName>(&mut buf, options).unwrap();
String::from_utf8(buf).unwrap()
};
std::fs::write("typescript/generated/events.ts", events).unwrap();
}

View File

@@ -1,536 +0,0 @@
use anyhow::{anyhow, bail, Context, Result};
use async_std::sync::{Arc, RwLock};
use deltachat::{
chat::{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,
};
use std::collections::BTreeMap;
use std::{collections::HashMap, str::FromStr};
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;
#[derive(Clone, Debug)]
pub struct CommandApi {
pub(crate) accounts: Arc<RwLock<Accounts>>,
}
impl CommandApi {
pub fn new(accounts: Accounts) -> Self {
CommandApi {
accounts: Arc::new(RwLock::new(accounts)),
}
}
pub fn new_from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
CommandApi { accounts }
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
let sc = self
.accounts
.read()
.await
.get_account(id)
.await
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
Ok(sc)
}
}
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
// ---------------------------------------------
// Misc top level functions
// ---------------------------------------------
/// Check if an email address is valid.
async fn check_email_validity(&self, email: String) -> bool {
may_be_valid_addr(&email)
}
/// Get general system info.
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
get_info()
}
// ---------------------------------------------
// Account Management
// ---------------------------------------------
async fn add_account(&self) -> Result<u32> {
self.accounts.write().await.add_account().await
}
async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts.write().await.remove_account(account_id).await
}
async fn get_all_account_ids(&self) -> Vec<u32> {
self.accounts.read().await.get_all().await
}
/// Select account id for internally selected state.
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn select_account(&self, id: u32) -> Result<()> {
self.accounts.write().await.select_account(id).await
}
/// Get the selected account id of the internal state..
/// TODO: Likely this is deprecated as all methods take an account id now.
async fn get_selected_account_id(&self) -> Option<u32> {
self.accounts.read().await.get_selected_account_id().await
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
for id in self.accounts.read().await.get_all().await {
let context_option = self.accounts.read().await.get_account(id).await;
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
} else {
println!("account with id {} doesn't exist anymore", id);
}
}
Ok(accounts)
}
// ---------------------------------------------
// Methods that work on individual accounts
// ---------------------------------------------
/// Get top-level info for an account.
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
let context_option = self.accounts.read().await.get_account(account_id).await;
if let Some(ctx) = context_option {
Ok(Account::from_context(&ctx, account_id).await?)
} else {
Err(anyhow!(
"account with id {} doesn't exist anymore",
account_id
))
}
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
async fn get_provider_info(
&self,
account_id: u32,
email: String,
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let socks5_enabled = ctx
.get_config_bool(deltachat::config::Config::Socks5Enabled)
.await?;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await;
Ok(ProviderInfo::from_dc_type(provider_info))
}
/// Checks if the context is already configured.
async fn is_configured(&self, account_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
ctx.is_configured().await
}
/// Get system info for an account.
async fn get_info(&self, account_id: u32) -> Result<BTreeMap<&'static str, String>> {
let ctx = self.get_context(account_id).await?;
ctx.get_info().await
}
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
set_config(&ctx, &key, value.as_deref()).await
}
async fn batch_set_config(
&self,
account_id: u32,
config: HashMap<String, Option<String>>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
for (key, value) in config.into_iter() {
set_config(&ctx, &key, value.as_deref())
.await
.with_context(|| format!("Can't set {} to {:?}", key, value))?;
}
Ok(())
}
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;
ctx.configure().await?;
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(())
}
// ---------------------------------------------
// 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::new();
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::new();
for (_i, entry) in entries.iter().enumerate() {
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::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
}
// ---------------------------------------------
// 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::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::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::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::from_dc_contact(
&ctx,
deltachat::contact::Contact::get_by_id(&ctx, ContactId::new(id)).await?,
)
.await?,
);
}
Ok(contacts)
}
// ---------------------------------------------
// misc prototyping functions
// that might get removed later again
// ---------------------------------------------
/// Returns the messageid of the sent message
async fn misc_send_text_message(
&self,
account_id: u32,
text: String,
chat_id: u32,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some(text));
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
}
// Helper functions (to prevent code duplication)
async fn set_config(
ctx: &deltachat::context::Context,
key: &str,
value: Option<&str>,
) -> Result<(), anyhow::Error> {
if key.starts_with("ui.") {
ctx.set_ui_config(key, value).await
} else {
ctx.set_config(Config::from_str(key).context("unknown key")?, value)
.await
}
}
async fn get_config(
ctx: &deltachat::context::Context,
key: &str,
) -> Result<Option<String>, anyhow::Error> {
if key.starts_with("ui.") {
ctx.get_ui_config(key).await
} else {
ctx.get_config(Config::from_str(key).context("unknown key")?)
.await
}
}

View File

@@ -1,46 +0,0 @@
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,
},
Unconfigured {
id: u32,
},
}
impl Account {
pub async fn from_context(ctx: &deltachat::context::Context, id: u32) -> Result<Self> {
if ctx.is_configured().await? {
let display_name = ctx.get_config(Config::Displayname).await?;
let addr = ctx.get_config(Config::Addr).await?;
let profile_image = ctx.get_config(Config::Selfavatar).await?;
let color = color_int_to_hex_string(
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
);
Ok(Account::Configured {
id,
display_name,
addr,
profile_image,
color,
})
} else {
Ok(Account::Unconfigured { id })
}
}
}

View File

@@ -1,91 +0,0 @@
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)]
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 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::new();
for contact_id in &contact_ids {
contacts.push(
ContactObject::from_dc_contact(
context,
Contact::load_from_db(context, *contact_id).await?,
)
.await?,
)
}
let profile_image = match chat.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let color = color_int_to_hex_string(chat.get_color(context).await?);
let fresh_message_counter = rust_chat_id.get_fresh_msg_cnt(context).await?;
let ephemeral_timer = rust_chat_id.get_ephemeral_timer(context).await?.to_u32();
let can_send = chat.can_send(context).await?;
Ok(FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == deltachat::chat::ChatVisibility::Archived,
chat_type: chat
.get_type()
.to_u32()
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
contacts,
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
color,
fresh_message_counter,
is_contact_request: chat.is_contact_request(),
is_device_chat: chat.is_device_talk(),
self_in_group: contact_ids.contains(&ContactId::SELF),
is_muted: chat.is_muted(),
ephemeral_timer,
can_send,
})
}
}

View File

@@ -1,117 +0,0 @@
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,
},
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 self_in_group = get_chat_contacts(ctx, chat_id)
.await?
.contains(&ContactId::SELF);
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(),
})
}

View File

@@ -1,50 +0,0 @@
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")]
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 from_dc_contact(
context: &Context,
contact: deltachat::contact::Contact,
) -> Result<Self> {
let profile_image = match contact.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
Ok(ContactObject {
address: contact.get_addr().to_owned(),
color: color_int_to_hex_string(contact.get_color()),
auth_name: contact.get_authname().to_owned(),
status: contact.get_status().to_owned(),
display_name: contact.get_display_name().to_owned(),
id: contact.id.to_u32(),
name: contact.get_name().to_owned(),
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_verified,
})
}
}

View File

@@ -1,127 +0,0 @@
use anyhow::{anyhow, Result};
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::message::Message;
use deltachat::message::MsgId;
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::contact::ContactObject;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Message")]
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: u32,
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::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()
.to_u32()
.ok_or_else(|| anyhow!("viewtype conversion to number failed"))?,
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(),
})
}
}

View File

@@ -1,10 +0,0 @@
pub mod account;
pub mod chat;
pub mod chat_list;
pub mod contact;
pub mod message;
pub mod provider_info;
pub fn color_int_to_hex_string(color: u32) -> String {
format!("{:#08x}", color).replace("0x", "#")
}

View File

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

View File

@@ -1,60 +0,0 @@
pub mod api;
pub use api::events;
pub use yerpc;
#[cfg(test)]
mod tests {
use super::api::{Accounts, CommandApi};
use async_channel::unbounded;
use async_std::task;
use futures::StreamExt;
use tempfile::TempDir;
use yerpc::{MessageHandle, RpcHandle};
#[async_std::test]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
// println!("{}", "");
let tmp_dir = TempDir::new().unwrap().path().into();
println!("tmp_dir: {:?}", tmp_dir);
let accounts = Accounts::new(tmp_dir).await?;
let cmd_api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
let (request_handle, mut rx) = RpcHandle::new();
let session = cmd_api;
let handle = MessageHandle::new(request_handle, session);
task::spawn({
async move {
while let Some(message) = rx.next().await {
let message = serde_json::to_string(&message)?;
// Abort serialization on error.
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}"#;
handle.handle_message(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]}"#;
handle.handle_message(request).await;
let result = receiver.next().await;
println!("{:?}", result);
assert_eq!(result, Some(response.to_owned()));
}
Ok(())
}
}

View File

@@ -1,44 +0,0 @@
use async_std::path::PathBuf;
use async_std::task;
use tide::Request;
use yerpc::RpcHandle;
use yerpc_tide::yerpc_handler;
mod api;
use api::events::event_to_json_rpc_notification;
use api::{Accounts, CommandApi};
#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
env_logger::init();
log::info!("Starting");
let accounts = Accounts::new(PathBuf::from("./accounts")).await.unwrap();
let state = CommandApi::new(accounts);
let mut app = tide::with_state(state.clone());
app.at("/ws").get(yerpc_handler(request_handler));
state.accounts.read().await.start_io().await;
app.listen("127.0.0.1:20808").await?;
Ok(())
}
async fn request_handler(
request: Request<CommandApi>,
rpc: RpcHandle,
) -> anyhow::Result<CommandApi> {
let state = request.state().clone();
task::spawn(event_loop(state.clone(), rpc));
Ok(state)
}
async fn event_loop(state: CommandApi, rpc: RpcHandle) -> anyhow::Result<()> {
let events = state.accounts.read().await.get_event_emitter().await;
while let Some(event) = events.recv().await {
// log::debug!("event {:?}", event);
let event = event_to_json_rpc_notification(event);
rpc.notify("event", Some(event)).await?;
}
Ok(())
}

View File

@@ -1,6 +0,0 @@
node_modules
dist
test_dist
coverage
yarn.lock
package-lock.json

View File

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

View File

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

View File

@@ -1,107 +0,0 @@
import { RawClient, RPC } from "./src/lib";
import { WebsocketTransport, Request } from "yerpc";
type DeltaEvent = { id: string; contextId: number; field1: any; field2: any };
var selectedAccount = 0;
window.addEventListener("DOMContentLoaded", (_event) => {
(window as any).selectDeltaAccount = (id: string) => {
selectedAccount = Number(id);
window.dispatchEvent(new Event("account-changed"));
};
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 transport = new WebsocketTransport("ws://localhost:20808/ws");
const client = new RawClient(transport);
(window as any).client = client;
transport.on("request", (request: Request) => {
const method = request.method;
if (method === "event") {
const params = request.params! as DeltaEvent;
onIncomingEvent(params, params.id);
}
});
window.addEventListener("account-changed", async (_event: Event) => {
await client.selectAccount(selectedAccount);
listChatsForSelectedAccount();
});
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
async function loadAccountsInHeader() {
const accounts = await client.getAllAccounts();
for (const account of accounts) {
if (account.type === "Configured") {
write(
$head,
`<a href="#" onclick="selectDeltaAccount(${account.id})">
${account.addr!}
</a>&nbsp;`
);
}
}
}
async function listChatsForSelectedAccount() {
clear($main);
const selectedAccount = await client.getSelectedAccountId();
if (!selectedAccount) return write($main, "No account selected");
const info = await client.getAccountInfo(selectedAccount);
if (info.type !== "Configured") {
return write($main, "Account is not configured");
}
write($main, `<h2>${info.addr!}</h2>`);
const chats = await client.getChatlistEntries(
selectedAccount,
0,
null,
null
);
for (const [chatId, _messageId] of chats) {
const chat = await client.chatlistGetFullChatById(
selectedAccount,
chatId
);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.messageListGetMessageIds(
selectedAccount,
chatId,
0
);
const messages = await client.messageGetMessages(
selectedAccount,
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
write($main, `<p>${message.text}</p>`);
}
}
}
function onIncomingEvent(event: DeltaEvent, name: string) {
write(
$side,
`
<p class="message">
[<strong>${name}</strong> on account ${event.contextId}]<br>
<em>f1:</em> ${JSON.stringify(event.field1)}<br>
<em>f2:</em> ${JSON.stringify(event.field2)}
</p>`
);
}
}
function write(el: HTMLElement, html: string) {
el.innerHTML += html;
}
function clear(el: HTMLElement) {
el.innerHTML = "";
}

View File

@@ -1,251 +0,0 @@
// 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>;
}
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>;
}
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 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 the messageid of the sent message
*/
public miscSendTextMessage(accountId: T.U32, text: string, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, text, chatId] as RPC.Params)) as Promise<T.U32>;
}
}

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Account=(({"type":"Configured";}&{"id":U32;"display_name":(string|null);"addr":(string|null);"profile_image":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
export type ProviderInfo={"before_login_hint":string;"overview_page":string;"status":U32;};
export type ChatListEntry=[U32,U32];
export type I64=number;
export type Usize=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;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type Contact={"address":string;"color":string;"auth_name":string;"status":string;"display_name":string;"id":U32;"name":string;"profile_image":(string|null);"name_and_addr":string;"is_blocked":boolean;"is_verified":boolean;};
export type FullChat={"id":U32;"name":string;"is_protected":boolean;"profile_image":(string|null);"archived":boolean;"chat_type":U32;"is_unpromoted":boolean;"is_self_talk":boolean;"contacts":(Contact)[];"contact_ids":(U32)[];"color":string;"fresh_message_counter":Usize;"is_contact_request":boolean;"is_device_chat":boolean;"self_in_group":boolean;"is_muted":boolean;"ephemeral_timer":U32;"can_send":boolean;};
export type I32=number;
export type U64=number;
export type Message={"id":U32;"chat_id":U32;"from_id":U32;"quoted_text":(string|null);"quoted_message_id":(U32|null);"text":(string|null);"has_location":boolean;"has_html":boolean;"view_type":U32;"state":U32;"timestamp":I64;"sort_timestamp":I64;"received_timestamp":I64;"has_deviating_timestamp":boolean;"subject":string;"show_padlock":boolean;"is_setupmessage":boolean;"is_info":boolean;"is_forwarded":boolean;"duration":I32;"dimensions_height":I32;"dimensions_width":I32;"videochat_type":(U32|null);"videochat_url":(string|null);"override_sender_name":(string|null);"sender":Contact;"setup_code_begin":(string|null);"file":(string|null);"file_mime":(string|null);"file_bytes":U64;"file_name":(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,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,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,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,string,U32,U32];

View File

@@ -1,54 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<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>
<div class="grid">
<div id="header"></div>
<div id="main"></div>
<div id="side"><h2>log</h2></div>
</div>
<p>
Tip: open the dev console and use the client with
<code>window.client</code>
</p>
</body>
</html>

View File

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

View File

@@ -1,41 +0,0 @@
{
"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",
"build": "npm run generate-bindings && tsc",
"bundle": "npm run build && esbuild --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"generate-bindings": "cargo test",
"example:build": "tsc && esbuild --bundle dist/example.js --outfile=dist/example.bundle.js",
"example:dev": "esbuild example.ts --bundle --outdir=dist --servedir=.",
"coverage": "tsc -b test && COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include \"dist/*\" -r text -r html -r json mocha test_dist && node report_api_coverage.mjs",
"test": "rm -rf dist && npm run build && npm run coverage && npm run prettier:check"
},
"dependencies": {
"isomorphic-ws": "^4.0.1",
"tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git",
"yerpc": "^0.2.3"
},
"devDependencies": {
"prettier": "^2.6.2",
"chai-as-promised": "^7.1.1",
"@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",
"esbuild": "^0.14.11",
"mocha": "^9.1.1",
"node-fetch": "^2.6.1",
"typescript": "^4.5.5",
"ws": "^8.5.0"
}
}

View File

@@ -1,28 +0,0 @@
import { readFileSync } from "fs";
// only checks for the coverge of the api functions in bindings.ts for now
const generated_file = "typescript/generated/client.ts";
const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
const jsonCoverage =
json[Object.keys(json).find((k) => k.includes(generated_file))];
const fnMap = Object.keys(jsonCoverage.fnMap).map(
(key) => jsonCoverage.fnMap[key]
);
const htmlCoverage = readFileSync(
"./coverage/" + generated_file + ".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)} (${generated_file}:${line})`
)
.join("\n")
);

View File

@@ -1,77 +0,0 @@
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
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
constructor(protected 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();
}
private contextEmitters: TinyEmitter<Events>[] = [];
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._socket.close();
}
constructor(opts: Opts | string | undefined) {
if (typeof opts === "string") opts = { url: opts };
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
else opts = { ...DEFAULT_OPTS };
super(new WebsocketTransport(opts.url));
this.opts = opts;
}
}

View File

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

View File

@@ -1 +0,0 @@
# tests need to be ported to new API

View File

@@ -1,158 +0,0 @@
import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { Deltachat } from "../dist/deltachat.js";
import {
CMD_API_Server_Handle,
CMD_API_SERVER_PORT,
startCMD_API_Server,
} from "./test_base.js";
describe("basic tests", () => {
let server_handle: CMD_API_Server_Handle;
let dc: Deltachat;
before(async () => {
server_handle = await startCMD_API_Server(CMD_API_SERVER_PORT);
// make sure server is up by the time we continue
await new Promise((res) => setTimeout(res, 100));
dc = new Deltachat({
url: "ws://localhost:" + CMD_API_SERVER_PORT + "/ws",
});
dc.on("ALL", (event) => {
//console.log("event", event);
});
});
after(async () => {
dc && dc.close();
await server_handle.close();
});
it("check email", async () => {
const positive_test_cases = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
];
const negative_test_cases = ["email@", "example.com", "emai221"];
expect(
await Promise.all(
positive_test_cases.map((email) => dc.rpc.checkEmailValidity(email))
)
).to.not.contain(false);
expect(
await Promise.all(
negative_test_cases.map((email) => dc.rpc.checkEmailValidity(email))
)
).to.not.contain(true);
});
it("system info", async () => {
const system_info = await dc.rpc.getSystemInfo();
expect(system_info).to.contain.keys([
"arch",
"num_cpus",
"deltachat_core_version",
"sqlite_version",
]);
});
describe("account managment", () => {
it("should create account", async () => {
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 acc: number;
before(async () => {
acc = await dc.rpc.addAccount();
});
it("block and unblock contact", async function () {
const contactId = await dc.rpc.contactsCreateContact(
acc,
"example@delta.chat",
null
);
expect((await dc.rpc.contactsGetContact(acc, contactId)).is_blocked).to.be
.false;
await dc.rpc.contactsBlock(acc, contactId);
expect((await dc.rpc.contactsGetContact(acc, contactId)).is_blocked).to.be
.true;
expect(await dc.rpc.contactsGetBlocked(acc)).to.have.length(1);
await dc.rpc.contactsUnblock(acc, contactId);
expect((await dc.rpc.contactsGetContact(acc, contactId)).is_blocked).to.be
.false;
expect(await dc.rpc.contactsGetBlocked(acc)).to.have.length(0);
});
});
describe("configuration", function () {
let acc: number;
before(async () => {
acc = await dc.rpc.addAccount();
});
it("set and retrive", async function () {
await dc.rpc.setConfig(acc, "addr", "valid@email");
assert((await dc.rpc.getConfig(acc, "addr")) == "valid@email");
});
it("set invalid key should throw", async function () {
await expect(dc.rpc.setConfig(acc, "invalid_key", "some value")).to.be
.eventually.rejected;
});
it("get invalid key should throw", async function () {
await expect(dc.rpc.getConfig(acc, "invalid_key")).to.be.eventually
.rejected;
});
it("set and retrive ui.*", async function () {
await dc.rpc.setConfig(acc, "ui.chat_bg", "color:red");
assert((await dc.rpc.getConfig(acc, "ui.chat_bg")) == "color:red");
});
it("set and retrive (batch)", async function () {
const config = { addr: "valid@email", mail_pw: "1234" };
await dc.rpc.batchSetConfig(acc, config);
const retrieved = await dc.rpc.batchGetConfig(acc, 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(acc, config);
const retrieved = await dc.rpc.batchGetConfig(acc, 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(acc, config);
const retrieved = await dc.rpc.batchGetConfig(acc, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
});
});

View File

@@ -1,203 +0,0 @@
import { assert, expect } from "chai";
import { Deltachat, DeltachatEvent, EventTypeName } from "../dist/deltachat.js";
import {
CMD_API_Server_Handle,
CMD_API_SERVER_PORT,
createTempUser,
startCMD_API_Server,
} from "./test_base.js";
describe("online tests", function () {
let server_handle: CMD_API_Server_Handle;
let dc: Deltachat;
let account: { email: string; password: string };
let account2: { email: string; password: string };
let acc1: number, acc2: 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();
}
server_handle = await startCMD_API_Server(CMD_API_SERVER_PORT);
dc = new Deltachat({
url: "ws://localhost:" + CMD_API_SERVER_PORT + "/ws",
});
dc.on("ALL", ({ id, contextId }) => {
if (id !== "Info") console.log(contextId, id);
});
account = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account || !account.email || !account.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();
server_handle && (await server_handle.close());
});
let are_configured = false;
it("configure test accounts", async function () {
this.timeout(20000);
acc1 = await dc.rpc.addAccount();
await dc.rpc.setConfig(acc1, "addr", account.email);
await dc.rpc.setConfig(acc1, "mail_pw", account.password);
let configure_promise = dc.rpc.configure(acc1);
acc2 = await dc.rpc.addAccount();
await dc.rpc.batchSetConfig(acc2, {
addr: account2.email,
mail_pw: account2.password,
});
await Promise.all([configure_promise, dc.rpc.configure(acc2)]);
are_configured = true;
});
it("send and recieve text message", async function () {
if (!are_configured) {
this.skip();
}
this.timeout(15000);
const contactId = await dc.rpc.contactsCreateContact(
acc1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(acc1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", acc2),
waitForEvent(dc, "IncomingMsg", acc2),
]);
dc.rpc.miscSendTextMessage(acc1, "Hello", chatId);
const { field1: chatIdOnAccountB } = await eventPromise;
await dc.rpc.acceptChat(acc2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
acc2,
chatIdOnAccountB,
0
);
expect(messageList).have.length(1);
const message = await dc.rpc.messageGetMessage(acc2, messageList[0]);
expect(message.text).equal("Hello");
});
it("send and recieve text message roundtrip, encrypted on answer onwards", async function () {
if (!are_configured) {
this.skip();
}
this.timeout(10000);
// send message from A to B
const contactId = await dc.rpc.contactsCreateContact(
acc1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(acc1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", acc2),
waitForEvent(dc, "IncomingMsg", acc2),
]);
dc.rpc.miscSendTextMessage(acc1, "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(acc2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
acc2,
chatIdOnAccountB,
0
);
const message = await dc.rpc.messageGetMessage(
acc2,
messageList.reverse()[0]
);
expect(message.text).equal("Hello2");
// Send message back from B to A
const eventPromise2 = Promise.race([
waitForEvent(dc, "MsgsChanged", acc1),
waitForEvent(dc, "IncomingMsg", acc1),
]);
dc.rpc.miscSendTextMessage(acc2, "super secret message", chatId);
// Check if answer arives at A and if it is encrypted
await eventPromise2;
const messageId = (
await dc.rpc.messageListGetMessageIds(acc1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.messageGetMessage(acc1, messageId);
expect(message2.text).equal("super secret message");
expect(message2.show_padlock).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?.overview_page).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);
});
});
type event_data = {
contextId: number;
id: EventTypeName;
[key: string]: any;
};
async function waitForEvent(
dc: Deltachat,
event: EventTypeName,
accountId: number
): Promise<event_data> {
return new Promise((res, rej) => {
const callback = (ev: DeltachatEvent) => {
if (ev.contextId == accountId) {
dc.off(event, callback);
res(ev);
}
};
dc.on(event, callback);
});
}

View File

@@ -1,95 +0,0 @@
import { tmpdir } from "os";
import { join } from "path";
import { mkdtemp, rm } from "fs/promises";
import { existsSync } from "fs";
import { spawn, exec } from "child_process";
import { unwrapPromise } from "./ts_helpers.js";
import fetch from "node-fetch";
/* port is not configurable yet */
function getTargetDir(): Promise<string> {
return new Promise((res, rej) => {
exec(
"cargo metadata --no-deps --format-version 1",
(error, stdout, stderr) => {
if (error) {
console.log("error", error);
rej(error);
} else {
try {
const json = JSON.parse(stdout);
res(json.target_directory);
} catch (error) {
console.log("json error", error);
rej(error);
}
}
}
);
});
}
export const CMD_API_SERVER_PORT = 20808;
export async function startCMD_API_Server(port: typeof CMD_API_SERVER_PORT) {
const tmp_dir = await mkdtemp(join(tmpdir(), "test_prefix"));
const path_of_server = join(await getTargetDir(), "debug/webserver");
console.log(path_of_server);
if (!existsSync(path_of_server)) {
throw new Error(
"server executable does not exist, you need to build it first" +
"\nserver executable not found at " +
path_of_server
);
}
const server = spawn(path_of_server, {
cwd: tmp_dir,
env: {
RUST_LOG: "info",
},
});
let should_close = false;
server.on("exit", () => {
if (should_close) {
return;
}
throw new Error("Server quit");
});
server.stderr.pipe(process.stderr);
//server.stdout.pipe(process.stdout)
return {
close: async () => {
should_close = true;
if (!server.kill(9)) {
console.log("server termination failed");
}
await rm(tmp_dir, { recursive: true });
},
};
}
export type CMD_API_Server_Handle = unwrapPromise<
ReturnType<typeof startCMD_API_Server>
>;
export async function createTempUser(url: string) {
async function postData(url = "") {
// Default options are marked with *
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
headers: {
"cache-control": "no-cache",
},
});
return response.json(); // parses JSON response into native JavaScript objects
}
return await postData(url);
}

View File

@@ -1 +0,0 @@
export type unwrapPromise<T> = T extends Promise<infer U> ? U : never;

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"rootDir": ".",
"outDir": "../test_dist",
"target": "ES2020",
"module": "es2020",
"moduleResolution": "node",
"declaration": false,
"esModuleInterop": true,
"noImplicitAny": true,
"isolatedModules": true,
"strictNullChecks": true,
"strict": true,
"sourceMap": true
},
"compileOnSave": true
}

View File

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

View File

@@ -148,9 +148,9 @@ If no icon is set, a default icon will be used.
- `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
- `window.navigator.language` is okay to be used, on desktop, this is currently always "en-GB"
- `<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
- `<a href="mailto:addr@example.org?body=...">`-links are okay to be used
- `<meta name="viewport" ...>` usage is okay to be used
and useful esp. different webviews have different defaults
@@ -174,7 +174,7 @@ If no icon is set, a default icon will be used.
## 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>

View File

@@ -19,7 +19,7 @@ use deltachat::config;
use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::securejoin::*;
use deltachat::{EventType, Events};
use deltachat::EventType;
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::config::OutputStreamType;
@@ -298,7 +298,7 @@ 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, Events::new()).await?;
let context = Context::new(Path::new(&args[1]).to_path_buf(), 0).await?;
let events = context.get_event_emitter();
async_std::task::spawn(async move {

View File

@@ -6,7 +6,7 @@ use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::message::Message;
use deltachat::{EventType, Events};
use deltachat::EventType;
fn cb(event: EventType) {
match event {
@@ -36,7 +36,7 @@ async fn main() {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
log::info!("creating database {:?}", dbfile);
let ctx = Context::new(dbfile.into(), 0, Events::new())
let ctx = Context::new(dbfile.into(), 0)
.await
.expect("Failed to create context");
let info = ctx.get_info().await;

View File

@@ -60,19 +60,6 @@ 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

View File

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

View File

@@ -1,65 +1,73 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const split = require('split2')
const data = []
const header = path.resolve(__dirname, '../../deltachat-ffi/deltachat.h')
const regex = /^#define\s+(\w+)\s+(\w+)/i
const header = path.resolve(
__dirname,
'../../deltachat-ffi/deltachat.h'
)
console.log('Generating constants...')
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 })
}
}
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
delete header_data
data.push({ key, value })
}
})
.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')
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')
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`
)
)
})

View File

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

View File

@@ -34,9 +34,6 @@ 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;
@@ -351,7 +348,7 @@ NAPI_METHOD(dcn_start_event_handler) {
callback,
0,
async_resource_name,
1000, // max_queue_size
1,
1,
NULL,
NULL,
@@ -374,11 +371,6 @@ 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;
@@ -2930,16 +2922,6 @@ 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);
dcn_accounts->jsonrpc_instance = NULL;
}
dc_accounts_unref(dcn_accounts->dc_accounts);
dcn_accounts->dc_accounts = NULL;
@@ -3098,6 +3080,8 @@ 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);
@@ -3109,8 +3093,8 @@ static void accounts_event_handler_thread_func(void* arg)
}
event = dc_accounts_get_next_event(dc_accounts_event_emitter);
if (event == NULL) {
TRACE("no more events");
break;
//TRACE("received NULL event, skipping");
continue;
}
if (!dcn_accounts->threadsafe_event_handler) {
@@ -3232,7 +3216,7 @@ NAPI_METHOD(dcn_accounts_start_event_handler) {
callback,
0,
async_resource_name,
1000, // max_queue_size
1,
1,
NULL,
NULL,
@@ -3248,125 +3232,6 @@ 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;
}
}
dc_jsonrpc_unref(dcn_accounts->jsonrpc_instance);
dcn_accounts->jsonrpc_instance = NULL;
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");
}
free(response);
TRACE("calling back into js");
napi_value result;
status = napi_call_function(
env,
global,
js_callback,
1,
argv,
&result);
if (status != napi_ok) {
TRACE("Unable to call jsonrpc_handler callback2");
const napi_extended_error_info* error_result;
NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result));
}
}
NAPI_METHOD(dcn_accounts_start_jsonrpc) {
NAPI_ARGV(2);
NAPI_DCN_ACCOUNTS();
napi_value callback = argv[1];
TRACE("calling..");
napi_value async_resource_name;
NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_accounts_jsonrpc_callback", NAPI_AUTO_LENGTH, &async_resource_name));
TRACE("creating threadsafe function..");
NAPI_STATUS_THROWS(napi_create_threadsafe_function(
env,
callback,
0,
async_resource_name,
1,
1,
NULL,
NULL,
dcn_accounts,
call_accounts_js_jsonrpc_handler,
&dcn_accounts->threadsafe_jsonrpc_handler));
TRACE("done");
dcn_accounts->gc = 0;
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts);
TRACE("creating uv thread..");
uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts);
NAPI_RETURN_UNDEFINED();
}
NAPI_METHOD(dcn_json_rpc_request) {
NAPI_ARGV(2);
NAPI_DCN_ACCOUNTS();
if (!dcn_accounts->jsonrpc_instance) {
const char* msg = "dcn_accounts->jsonrpc_instance is null, have you called dcn_accounts_start_jsonrpc()?";
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg));
}
NAPI_ARGV_UTF8_MALLOC(request, 1);
dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, request);
free(request);
}
NAPI_INIT() {
/**
@@ -3637,9 +3502,4 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_webxdc_status_update);
NAPI_EXPORT_FUNCTION(dcn_get_webxdc_status_updates);
NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_blob);
/** jsonrpc **/
NAPI_EXPORT_FUNCTION(dcn_accounts_start_jsonrpc);
NAPI_EXPORT_FUNCTION(dcn_json_rpc_request);
}

View File

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

View File

@@ -2,7 +2,7 @@
import DeltaChat, { Message } from '../dist'
import binding from '../binding'
import { deepEqual, deepStrictEqual, strictEqual } from 'assert'
import { strictEqual } from 'assert'
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { EventId2EventName, C } from '../dist/constants'
@@ -84,95 +84,6 @@ 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()
@@ -217,7 +128,7 @@ describe('Basic offline Tests', function () {
await expect(
context.configure({ addr: 'delta1@delta.localhost' })
).to.eventually.be.rejectedWith('Missing (IMAP) password.')
).to.eventually.be.rejectedWith('Please enter a password.')
await expect(context.configure({ mailPw: 'delta1' })).to.eventually.be
.rejected
@@ -690,8 +601,8 @@ describe('Offline Tests with unconfigured account', function () {
strictEqual(chatList.getCount(), 1, 'only one archived')
})
it('Remove quote from (draft) message', function () {
context.addDeviceMessage('test_quote', 'test')
it('Remove qoute from (draft) message', function () {
context.addDeviceMessage('test_qoute', 'test')
const msgId = context.getChatMessages(10, 0, 0)[0]
const msg = context.messageNew()

View File

@@ -1,6 +1,5 @@
{
"dependencies": {
"@deltachat/jsonrpc-client": "file:deltachat-jsonrpc/typescript",
"debug": "^4.1.1",
"napi-macros": "^2.0.0",
"node-gyp-build": "^4.1.0"
@@ -20,6 +19,7 @@
"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 --bail"
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec"
},
"types": "node/dist/index.d.ts",
"version": "1.86.0"
}
}

View File

@@ -11,7 +11,8 @@
<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>

View File

@@ -18,13 +18,15 @@ 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", "."
])

View File

@@ -1,37 +1,36 @@
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',
],
)

View File

@@ -19,6 +19,7 @@ from .cutil import (
from_dc_charpointer,
from_optional_dc_charpointer,
iter_array,
safe_unref,
)
from .events import EventThread
from .message import Message
@@ -38,7 +39,7 @@ def get_core_info():
return get_dc_info_as_dict(
ffi.gc(
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
lib.dc_context_unref,
safe_unref(lib.dc_context_unref),
)
)
@@ -84,7 +85,7 @@ class Account(object):
self._dc_context = ffi.gc(
lib.dc_context_new_closed(db_path) if closed else lib.dc_context_new(ffi.NULL, db_path, ffi.NULL),
lib.dc_context_unref,
safe_unref(lib.dc_context_unref),
)
if self._dc_context == ffi.NULL:
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
@@ -342,7 +343,7 @@ class Account(object):
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
dc_array = ffi.gc(lib.dc_get_blocked_contacts(self._dc_context), lib.dc_array_unref)
dc_array = ffi.gc(lib.dc_get_blocked_contacts(self._dc_context), safe_unref(lib.dc_array_unref))
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts(
@@ -365,12 +366,12 @@ class Account(object):
flags |= const.DC_GCL_VERIFIED_ONLY
if with_self:
flags |= const.DC_GCL_ADD_SELF
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query), lib.dc_array_unref)
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query), safe_unref(lib.dc_array_unref))
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self) -> Generator[Message, None, None]:
"""yield all fresh messages from all chats."""
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), safe_unref(lib.dc_array_unref))
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat(self, obj) -> Chat:
@@ -404,7 +405,7 @@ class Account(object):
:returns: a list of :class:`deltachat.chat.Chat` objects.
"""
dc_chatlist = ffi.gc(lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0), lib.dc_chatlist_unref)
dc_chatlist = ffi.gc(lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0), safe_unref(lib.dc_chatlist_unref))
assert dc_chatlist != ffi.NULL
chatlist = []
@@ -545,7 +546,7 @@ class Account(object):
def check_qr(self, qr):
"""check qr code and return :class:`ScannedQRCode` instance representing the result"""
res = ffi.gc(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref)
res = ffi.gc(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), safe_unref(lib.dc_lot_unref))
lot = DCLot(res)
if lot.state() == const.DC_QR_ERROR:
raise ValueError("invalid or unknown QR code: {}".format(lot.text1()))

View File

@@ -14,6 +14,7 @@ from .cutil import (
from_dc_charpointer,
from_optional_dc_charpointer,
iter_array,
safe_unref,
)
from .message import Message
@@ -44,7 +45,7 @@ class Chat(object):
@property
def _dc_chat(self):
return ffi.gc(lib.dc_get_chat(self.account._dc_context, self.id), lib.dc_chat_unref)
return ffi.gc(lib.dc_get_chat(self.account._dc_context, self.id), safe_unref(lib.dc_chat_unref))
def delete(self) -> None:
"""Delete this chat and all its messages.
@@ -416,7 +417,7 @@ class Chat(object):
"""
dc_array = ffi.gc(
lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0),
lib.dc_array_unref,
safe_unref(lib.dc_array_unref),
)
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
@@ -469,7 +470,7 @@ class Chat(object):
dc_array = ffi.gc(
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
lib.dc_array_unref,
safe_unref(lib.dc_array_unref),
)
return list(iter_array(dc_array, lambda id: Contact(self.account, id)))
@@ -477,7 +478,7 @@ class Chat(object):
"""return number of contacts in this chat."""
dc_array = ffi.gc(
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
lib.dc_array_unref,
safe_unref(lib.dc_array_unref),
)
return lib.dc_array_get_cnt(dc_array)

View File

@@ -6,6 +6,14 @@ from .capi import ffi, lib
T = TypeVar("T")
def safe_unref(unref: Callable) -> Callable:
def wrapper(obj) -> None:
if obj != ffi.NULL:
unref(obj)
return wrapper
def as_dc_charpointer(obj):
if obj == ffi.NULL or obj is None:
return ffi.NULL

View File

@@ -11,7 +11,7 @@ from queue import Empty, Queue
import deltachat
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .cutil import from_optional_dc_charpointer, safe_unref
from .hookspec import account_hookimpl
from .message import map_system_message
@@ -229,7 +229,7 @@ class EventThread(threading.Thread):
def _inner_run(self):
event_emitter = ffi.gc(
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
safe_unref(lib.dc_event_emitter_unref),
)
while not self._marked_for_shutdown:
event = lib.dc_get_next_event(event_emitter)
@@ -271,8 +271,7 @@ class EventThread(threading.Thread):
data1 = ffi_event.data1
if data1 == 0 or data1 == 1000:
success = data1 == 1000
comment = ffi_event.data2
yield "ac_configure_completed", dict(success=success, comment=comment)
yield "ac_configure_completed", dict(success=success)
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))

View File

@@ -38,7 +38,7 @@ class PerAccount:
"""log a message related to the account."""
@account_hookspec
def ac_configure_completed(self, success, comment):
def ac_configure_completed(self, success):
"""Called after a configure process completed."""
@account_hookspec

View File

@@ -8,7 +8,12 @@ from typing import Optional, Union
from . import const, props
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
from .cutil import (
as_dc_charpointer,
from_dc_charpointer,
from_optional_dc_charpointer,
safe_unref,
)
class Message(object):
@@ -49,7 +54,7 @@ class Message(object):
@classmethod
def from_db(cls, account, id):
assert id > 0
return cls(account, ffi.gc(lib.dc_get_msg(account._dc_context, id), lib.dc_msg_unref))
return cls(account, ffi.gc(lib.dc_get_msg(account._dc_context, id), safe_unref(lib.dc_msg_unref)))
@classmethod
def new_empty(cls, account, view_type):
@@ -64,7 +69,7 @@ class Message(object):
view_type_code = get_viewtype_code_from_name(view_type)
return Message(
account,
ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref),
ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), safe_unref(lib.dc_msg_unref)),
)
def create_chat(self):
@@ -278,7 +283,7 @@ class Message(object):
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
if mime_headers:
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
s = ffi.string(ffi.gc(mime_headers, safe_unref(lib.dc_str_unref)))
if isinstance(s, bytes):
return email.message_from_bytes(s)
return email.message_from_string(s)
@@ -337,7 +342,7 @@ class Message(object):
dc_msg = self._dc_msg
else:
# load message from db to get a fresh/current state
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), safe_unref(lib.dc_msg_unref))
return lib.dc_msg_get_state(dc_msg)
def is_in_fresh(self):

View File

@@ -1,7 +1,7 @@
"""Provider info class."""
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer
from .cutil import as_dc_charpointer, from_dc_charpointer, safe_unref
class ProviderNotFoundError(Exception):
@@ -17,7 +17,7 @@ class Provider(object):
def __init__(self, account, addr) -> None:
provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)),
lib.dc_provider_unref,
safe_unref(lib.dc_provider_unref),
)
if provider == ffi.NULL:
raise ProviderNotFoundError("Provider not found")

View File

@@ -294,8 +294,8 @@ class ACSetup:
class PendingTracker:
@account_hookimpl
def ac_configure_completed(this, success: bool, comment: Optional[str]) -> None:
self._configured_events.put((account, success, comment))
def ac_configure_completed(this, success):
self._configured_events.put((account, success))
account.add_account_plugin(PendingTracker(), name="pending_tracker")
self._account2state[account] = self.CONFIGURING
@@ -333,9 +333,9 @@ class ACSetup:
print("finished, account2state", self._account2state)
def _pop_config_success(self):
acc, success, comment = self._configured_events.get()
acc, success = self._configured_events.get()
if not success:
pytest.fail("configuring online account {} failed: {}".format(acc, comment))
pytest.fail("configuring online account failed: {}".format(acc))
self._account2state[acc] = self.CONFIGURED
return acc

View File

@@ -1,10 +1,12 @@
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)
@@ -15,5 +17,7 @@ if __name__ == "__main__":
p,
"-w",
workspacedir,
"--plat",
"manylinux2014_" + arch,
]
)

View File

@@ -1433,9 +1433,8 @@ def test_set_get_contact_avatar(acfactory, data, lp):
def test_add_remove_member_remote_events(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
ac3_addr = ac3.get_config("addr")
# activate local plugin for ac2
in_list = queue.Queue()
@@ -1477,7 +1476,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(ac3_addr)
contact2 = chat.add_contact("devnull@testrun.org")
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
@@ -1485,7 +1484,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 == ac3_addr
assert ev.contact.addr == "devnull@testrun.org"
lp.sec("ac1: remove address2")
chat.remove_contact(contact2)

View File

@@ -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, None))
pc._configured_events.put((acc, True))
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, None))
pc._configured_events.put((ac1, True))
pc.wait_one_configured(ac1)
assert pc._account2state[ac1] == pc.CONFIGURED
assert pc._account2state[ac2] == pc.CONFIGURING
pc._configured_events.put((ac2, True, None))
pc._configured_events.put((ac2, True))
pc.bring_online()
assert pc._account2state[ac1] == pc.IDLEREADY
assert pc._account2state[ac2] == pc.IDLEREADY

View File

@@ -28,10 +28,6 @@ deps =
[testenv:auditwheels]
skipsdist = True
deps = auditwheel
passenv =
AUDITWHEEL_ARCH
AUDITWHEEL_PLAT
AUDITWHEEL_POLICY
commands =
python tests/auditwheels.py {toxworkdir}/wheelhouse
@@ -46,8 +42,8 @@ deps =
pygments
restructuredtext_lint
commands =
isort --check setup.py install_python_bindings.py src/deltachat examples/ tests/
black --check setup.py install_python_bindings.py src/deltachat examples/ tests/
isort --check src/deltachat examples/ tests/
black --check src/deltachat examples/ tests/
flake8 src/deltachat
flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst
@@ -93,4 +89,4 @@ markers =
[flake8]
max-line-length = 120
ignore = E203, E266, E501, W503
ignore = E203, E266, E501, W503

1
rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
edition = "2018"

View File

@@ -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 Libera Chat) to type::
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
scripts/manual_remote_tests.sh rust
scripts/manual_remote_tests.sh python

View File

@@ -78,7 +78,7 @@ jobs:
- name: deltachat-core-rust
image_resource:
source:
repository: concourse/oci-build-task
repository: vito/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
@@ -177,7 +177,7 @@ jobs:
- name: deltachat-core-rust
image_resource:
source:
repository: concourse/oci-build-task
repository: vito/oci-build-task
type: registry-image
outputs:
- name: coredeps-image

View File

@@ -1,5 +1,20 @@
FROM quay.io/pypa/manylinux2014_aarch64
RUN pipx install tox
# 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

View File

@@ -0,0 +1,21 @@
#!/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

View File

@@ -0,0 +1,12 @@
#!/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

View File

@@ -0,0 +1,14 @@
#!/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

View File

@@ -1,5 +1,20 @@
FROM quay.io/pypa/manylinux2014_x86_64
RUN pipx install tox
# 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

View File

@@ -0,0 +1,21 @@
#!/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

View File

@@ -0,0 +1,12 @@
#!/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

View File

@@ -0,0 +1,14 @@
#!/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

View File

@@ -22,4 +22,4 @@ export PYTHONDONTWRITEBYTECODE=1
# run python tests (tox invokes pytest to run tests in python/tests)
#TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc
tox -e lint
tox -e doc,py3
tox -e doc,py37

View File

@@ -5,6 +5,7 @@
set -e -x
# Perform clean build of core and install.
export TOXWORKDIR=.docker-tox
# compile core lib
@@ -20,8 +21,17 @@ export DCC_RS_TARGET=release
# needed by tox below.
export PATH=$PATH:/opt/python/cp37-cp37m/bin
export PYTHONDONTWRITEBYTECODE=1
pushd /bin
rm -f python3.7
ln -s /opt/python/cp37-cp37m/bin/python3.7
rm -f python3.8
ln -s /opt/python/cp38-cp38/bin/python3.8
rm -f python3.9
ln -s /opt/python/cp39-cp39/bin/python3.9
rm -f python3.10
ln -s /opt/python/cp310-cp310/bin/python3.10
popd
TOXWORKDIR=.docker-tox
pushd python
# prepare a clean tox run
rm -rf tests/__pycache__

View File

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

View File

@@ -2,15 +2,18 @@
use std::collections::BTreeMap;
use async_std::channel::{self, Receiver, Sender};
use async_std::fs;
use async_std::path::PathBuf;
use async_std::prelude::*;
use async_std::sync::{Arc, RwLock};
use uuid::Uuid;
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::events::{Event, EventType, Events};
/// Account manager, that can handle multiple accounts in a single place.
#[derive(Debug)]
@@ -18,6 +21,7 @@ pub struct Accounts {
dir: PathBuf,
config: Config,
accounts: BTreeMap<u32, Context>,
emitter: EventEmitter,
/// Event channel to emit account manager errors.
events: Events,
@@ -59,16 +63,28 @@ impl Accounts {
let config = Config::from_file(config_file)
.await
.context("failed to load accounts config")?;
let events = Events::new();
let accounts = config
.load_accounts(&events)
.load_accounts()
.await
.context("failed to load accounts")?;
let emitter = EventEmitter::new();
let events = Events::default();
emitter.sender.send(events.get_emitter()).await?;
for account in accounts.values() {
emitter.add_account(account).await.with_context(|| {
format!("failed to add account {} to event emitter", account.id)
})?;
}
Ok(Self {
dir,
config,
accounts,
emitter,
events,
})
}
@@ -105,12 +121,8 @@ impl Accounts {
pub async fn add_account(&mut self) -> Result<u32> {
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new(
account_config.dbfile().into(),
account_config.id,
self.events.clone(),
)
.await?;
let ctx = Context::new(account_config.dbfile().into(), account_config.id).await?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -120,12 +132,8 @@ impl Accounts {
pub async fn add_closed_account(&mut self) -> Result<u32> {
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new_closed(
account_config.dbfile().into(),
account_config.id,
self.events.clone(),
)
.await?;
let ctx = Context::new_closed(account_config.dbfile().into(), account_config.id).await?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -217,7 +225,8 @@ impl Accounts {
match res {
Ok(_) => {
let ctx = Context::new(new_dbfile, account_config.id, self.events.clone()).await?;
let ctx = Context::new(new_dbfile, account_config.id).await?;
self.emitter.add_account(&ctx).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
}
@@ -268,9 +277,6 @@ impl Accounts {
}
pub async fn stop_io(&self) {
// Sending an event here wakes up event loop even
// if there are no accounts.
info!(self, "Stopping IO for all accounts");
for account in self.accounts.values() {
account.stop_io().await;
}
@@ -293,9 +299,74 @@ impl Accounts {
self.events.emit(Event { id: 0, typ: event })
}
/// Returns event emitter.
/// Returns unified event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
self.emitter.clone()
}
}
/// Unified event emitter for multiple accounts.
#[derive(Debug, Clone)]
pub struct EventEmitter {
/// Aggregate stream of events from all accounts.
stream: Arc<RwLock<futures::stream::SelectAll<crate::events::EventEmitter>>>,
/// Sender for the channel where new account emitters will be pushed.
sender: Sender<crate::events::EventEmitter>,
/// Receiver for the channel where new account emitters will be pushed.
receiver: Receiver<crate::events::EventEmitter>,
}
impl EventEmitter {
pub fn new() -> Self {
let (sender, receiver) = channel::unbounded();
Self {
stream: Arc::new(RwLock::new(futures::stream::SelectAll::new())),
sender,
receiver,
}
}
/// Blocking recv of an event. Return `None` if all `Sender`s have been droped.
pub fn recv_sync(&mut self) -> Option<Event> {
async_std::task::block_on(self.recv()).unwrap_or_default()
}
/// Async recv of an event. Return `None` if all `Sender`s have been dropped.
pub async fn recv(&mut self) -> Result<Option<Event>> {
let mut stream = self.stream.write().await;
loop {
match futures::future::select(self.receiver.recv(), stream.next()).await {
futures::future::Either::Left((emitter, _)) => {
stream.push(emitter?);
}
futures::future::Either::Right((ev, _)) => return Ok(ev),
}
}
}
/// Add event emitter of a new account to the aggregate event emitter.
pub async fn add_account(&self, context: &Context) -> Result<()> {
self.sender.send(context.get_event_emitter()).await?;
Ok(())
}
}
impl Default for EventEmitter {
fn default() -> Self {
Self::new()
}
}
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::pin::Pin::new(&mut self).poll_next(cx)
}
}
@@ -352,21 +423,17 @@ impl Config {
Ok(Config { file, inner })
}
pub async fn load_accounts(&self, events: &Events) -> Result<BTreeMap<u32, Context>> {
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
let mut accounts = BTreeMap::new();
for account_config in &self.inner.accounts {
let ctx = Context::new(
account_config.dbfile().into(),
account_config.id,
events.clone(),
)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile()
)
})?;
let ctx = Context::new(account_config.dbfile().into(), account_config.id)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile()
)
})?;
accounts.insert(account_config.id, ctx);
}
@@ -540,9 +607,7 @@ mod tests {
assert_eq!(accounts.config.get_selected_account().await, 0);
let extern_dbfile: PathBuf = dir.path().join("other").into();
let ctx = Context::new(extern_dbfile.clone(), 0, Events::new())
.await
.unwrap();
let ctx = Context::new(extern_dbfile.clone(), 0).await.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
.await
.unwrap();
@@ -678,7 +743,7 @@ mod tests {
assert_eq!(accounts.accounts.len(), 0);
// Create event emitter.
let event_emitter = accounts.get_event_emitter().await;
let mut event_emitter = accounts.get_event_emitter().await;
// Test that event emitter does not return `None` immediately.
let duration = std::time::Duration::from_millis(1);
@@ -688,7 +753,7 @@ mod tests {
// When account manager is dropped, event emitter is exhausted.
drop(accounts);
assert_eq!(event_emitter.recv().await, None);
assert_eq!(event_emitter.recv().await?, None);
Ok(())
}

View File

@@ -9,9 +9,10 @@ use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::{format_err, Context as _, Error, Result};
use anyhow::{format_err, Context as _, Error};
use image::{DynamicImage, ImageFormat};
use num_traits::FromPrimitive;
use thiserror::Error;
use crate::config::Config;
use crate::constants::{
@@ -43,15 +44,31 @@ impl<'a> BlobObject<'a> {
/// name, followed by a random number and followed by a possible
/// extension. The `data` will be written into the file without
/// race-conditions.
///
/// # Errors
///
/// [BlobError::CreateFailure] is used when the file could not
/// be created. You can expect [BlobError.cause] to contain an
/// underlying error.
///
/// [BlobError::WriteFailure] is used when the file could not
/// be written to. You can expect [BlobError.cause] to contain an
/// underlying error.
pub async fn create(
context: &'a Context,
suggested_name: &str,
data: &[u8],
) -> Result<BlobObject<'a>> {
) -> std::result::Result<BlobObject<'a>, BlobError> {
let blobdir = context.get_blobdir();
let (stem, ext) = BlobObject::sanitise_name(suggested_name);
let (name, mut file) = BlobObject::create_new_file(context, blobdir, &stem, &ext).await?;
file.write_all(data).await.context("file write failure")?;
file.write_all(data)
.await
.map_err(|err| BlobError::WriteFailure {
blobdir: blobdir.to_path_buf(),
blobname: name.clone(),
cause: err.into(),
})?;
// workaround a bug in async-std
// (the executor does not handle blocking operation in Drop correctly,
@@ -72,7 +89,7 @@ impl<'a> BlobObject<'a> {
dir: &Path,
stem: &str,
ext: &str,
) -> Result<(String, fs::File)> {
) -> Result<(String, fs::File), BlobError> {
const MAX_ATTEMPT: u32 = 16;
let mut attempt = 0;
let mut name = format!("{}{}", stem, ext);
@@ -88,7 +105,11 @@ impl<'a> BlobObject<'a> {
Ok(file) => return Ok((name, file)),
Err(err) => {
if attempt >= MAX_ATTEMPT {
return Err(err).context("failed to create file");
return Err(BlobError::CreateFailure {
blobdir: dir.to_path_buf(),
blobname: name,
cause: err,
});
} else if attempt == 1 && !dir.exists().await {
fs::create_dir_all(dir).await.ok_or_log(context);
} else {
@@ -105,19 +126,40 @@ impl<'a> BlobObject<'a> {
/// but also copies an existing file into it. This is done in a
/// in way which avoids race-conditions when multiple files are
/// concurrently created.
pub async fn create_and_copy(context: &'a Context, src: &Path) -> Result<BlobObject<'a>> {
///
/// # Errors
///
/// In addition to the errors in [BlobObject::create] the
/// [BlobError::CopyFailure] is used when the data can not be
/// copied.
pub async fn create_and_copy(
context: &'a Context,
src: &Path,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let mut src_file = fs::File::open(src)
.await
.with_context(|| format!("failed to open file {}", src.display()))?;
.map_err(|err| BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: String::from(""),
src: src.to_path_buf(),
cause: err,
})?;
let (stem, ext) = BlobObject::sanitise_name(&src.to_string_lossy());
let (name, mut dst_file) =
BlobObject::create_new_file(context, context.get_blobdir(), &stem, &ext).await?;
let name_for_err = name.clone();
if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
// Attempt to remove the failed file, swallow errors resulting from that.
let path = context.get_blobdir().join(&name_for_err);
fs::remove_file(path).await.ok();
return Err(err).context("failed to copy file");
{
// Attempt to remove the failed file, swallow errors resulting from that.
let path = context.get_blobdir().join(&name_for_err);
fs::remove_file(path).await.ok();
}
return Err(BlobError::CopyFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: name_for_err,
src: src.to_path_buf(),
cause: err,
});
}
// workaround, see create() for details
@@ -142,7 +184,16 @@ impl<'a> BlobObject<'a> {
///
/// Paths into the blob directory may be either defined by an absolute path
/// or by the relative prefix `$BLOBDIR`.
pub async fn new_from_path(context: &'a Context, src: &Path) -> Result<BlobObject<'a>> {
///
/// # Errors
///
/// This merely delegates to the [BlobObject::create_and_copy] and
/// the [BlobObject::from_path] methods. See those for possible
/// errors.
pub async fn new_from_path(
context: &'a Context,
src: &Path,
) -> std::result::Result<BlobObject<'a>, BlobError> {
if src.starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src)
} else if src.starts_with("$BLOBDIR/") {
@@ -158,14 +209,32 @@ impl<'a> BlobObject<'a> {
/// must use a valid blob name. That is after sanitisation the
/// name must still be the same, that means it must be valid UTF-8
/// and not have any special characters in it.
pub fn from_path(context: &'a Context, path: &Path) -> Result<BlobObject<'a>> {
let rel_path = path
.strip_prefix(context.get_blobdir())
.context("wrong blobdir")?;
///
/// # Errors
///
/// [BlobError::WrongBlobdir] is used if the path is not in
/// the blob directory.
///
/// [BlobError::WrongName] is used if the file name does not
/// remain identical after sanitisation.
pub fn from_path(
context: &'a Context,
path: &Path,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let rel_path =
path.strip_prefix(context.get_blobdir())
.map_err(|_| BlobError::WrongBlobdir {
blobdir: context.get_blobdir().to_path_buf(),
src: path.to_path_buf(),
})?;
if !BlobObject::is_acceptible_blob_name(rel_path) {
return Err(format_err!("wrong name"));
return Err(BlobError::WrongName {
blobname: path.to_path_buf(),
});
}
let name = rel_path.to_str().context("wrong name")?;
let name = rel_path.to_str().ok_or_else(|| BlobError::WrongName {
blobname: path.to_path_buf(),
})?;
BlobObject::from_name(context, name.to_string())
}
@@ -175,13 +244,24 @@ impl<'a> BlobObject<'a> {
/// prefixed, as returned by [BlobObject::as_name]. This is how
/// you want to create a [BlobObject] for a filename read from the
/// database.
pub fn from_name(context: &'a Context, name: String) -> Result<BlobObject<'a>> {
///
/// # Errors
///
/// [BlobError::WrongName] is used if the name is not a valid
/// blobname, i.e. if [BlobObject::sanitise_name] does modify the
/// provided name.
pub fn from_name(
context: &'a Context,
name: String,
) -> std::result::Result<BlobObject<'a>, BlobError> {
let name: String = match name.starts_with("$BLOBDIR/") {
true => name.splitn(2, '/').last().unwrap().to_string(),
false => name,
};
if !BlobObject::is_acceptible_blob_name(&name) {
return Err(format_err!("not an acceptable blob name: {}", &name));
return Err(BlobError::WrongName {
blobname: PathBuf::from(name),
});
}
Ok(BlobObject {
blobdir: context.get_blobdir(),
@@ -314,7 +394,7 @@ impl<'a> BlobObject<'a> {
true
}
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
let img_wh =
@@ -336,7 +416,7 @@ impl<'a> BlobObject<'a> {
Ok(())
}
pub async fn recode_to_image_size(&self, context: &Context) -> Result<()> {
pub async fn recode_to_image_size(&self, context: &Context) -> Result<(), BlobError> {
let blob_abs = self.to_abs_path();
if message::guess_msgtype_from_suffix(Path::new(&blob_abs))
!= Some((Viewtype::Image, "image/jpeg"))
@@ -359,7 +439,8 @@ impl<'a> BlobObject<'a> {
{
return Err(format_err!(
"Internal error: recode_to_size(..., None) shouldn't change the name of the image"
));
)
.into());
}
Ok(())
}
@@ -370,8 +451,12 @@ impl<'a> BlobObject<'a> {
mut blob_abs: PathBuf,
mut img_wh: u32,
max_bytes: Option<usize>,
) -> Result<Option<String>> {
let mut img = image::open(&blob_abs).context("image recode failure")?;
) -> Result<Option<String>, BlobError> {
let mut img = image::open(&blob_abs).map_err(|err| BlobError::RecodeFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err,
})?;
let orientation = self.get_exif_orientation(context);
let mut encoded = Vec::new();
let mut changed_name = None;
@@ -435,7 +520,8 @@ impl<'a> BlobObject<'a> {
return Err(format_err!(
"Failed to scale image to below {}B",
max_bytes.unwrap_or_default()
));
)
.into());
}
img_wh = img_wh * 2 / 3;
@@ -469,7 +555,11 @@ impl<'a> BlobObject<'a> {
fs::write(&blob_abs, &encoded)
.await
.context("failed to write recoded blob to file")?;
.map_err(|err| BlobError::WriteFailure {
blobdir: context.get_blobdir().to_path_buf(),
blobname: blob_abs.to_str().unwrap_or_default().to_string(),
cause: err.into(),
})?;
}
Ok(changed_name)
@@ -500,6 +590,46 @@ impl<'a> fmt::Display for BlobObject<'a> {
}
}
/// Errors for the [BlobObject].
#[derive(Debug, Error)]
pub enum BlobError {
#[error("Failed to create blob {blobname} in {}", .blobdir.display())]
CreateFailure {
blobdir: PathBuf,
blobname: String,
#[source]
cause: std::io::Error,
},
#[error("Failed to write data to blob {blobname} in {}", .blobdir.display())]
WriteFailure {
blobdir: PathBuf,
blobname: String,
#[source]
cause: anyhow::Error,
},
#[error("Failed to copy data from {} to blob {blobname} in {}", .src.display(), .blobdir.display())]
CopyFailure {
blobdir: PathBuf,
blobname: String,
src: PathBuf,
#[source]
cause: std::io::Error,
},
#[error("Failed to recode to blob {blobname} in {}", .blobdir.display())]
RecodeFailure {
blobdir: PathBuf,
blobname: String,
#[source]
cause: image::ImageError,
},
#[error("File path {} is not in {}", .src.display(), .blobdir.display())]
WrongBlobdir { blobdir: PathBuf, src: PathBuf },
#[error("Blob has a badname {}", .blobname.display())]
WrongName { blobname: PathBuf },
#[error("{0}")]
Other(#[from] anyhow::Error),
}
#[cfg(test)]
mod tests {
use fs::File;

View File

@@ -11,7 +11,7 @@ use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::blob::{BlobError, BlobObject};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
@@ -3022,8 +3022,15 @@ pub async fn set_chat_profile_image(
msg.param.remove(Param::Arg);
msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
} else {
let mut image_blob =
BlobObject::new_from_path(context, Path::new(new_image.as_ref())).await?;
let mut image_blob = match BlobObject::from_path(context, Path::new(new_image.as_ref())) {
Ok(blob) => Ok(blob),
Err(err) => match err {
BlobError::WrongBlobdir { .. } => {
BlobObject::create_and_copy(context, Path::new(new_image.as_ref())).await
}
_ => Err(err),
},
}?;
image_blob.recode_to_avatar_size(context).await?;
chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name());
@@ -5041,32 +5048,6 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_forward_info_msg() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a").await?;
send_text_msg(&t, chat_id1, "msg one".to_string()).await?;
let bob_id = Contact::create(&t, "", "bob@example.net").await?;
add_contact_to_chat(&t, chat_id1, bob_id).await?;
let msg1 = t.get_last_msg_in(chat_id1).await;
assert!(msg1.is_info());
assert!(msg1.get_text().unwrap().contains("bob@example.net"));
let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?;
assert_eq!(get_chat_msgs(&t, chat_id2, 0).await?.len(), 0);
forward_msgs(&t, &[msg1.id], chat_id2).await?;
let msg2 = t.get_last_msg_in(chat_id2).await;
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state
assert_eq!(msg2.get_info_type(), SystemMessage::Unknown);
assert_ne!(msg2.from_id, ContactId::INFO);
assert_ne!(msg2.to_id, ContactId::INFO);
assert_eq!(msg2.get_text().unwrap(), msg1.get_text().unwrap());
assert!(msg2.is_forwarded());
Ok(())
}
#[async_std::test]
async fn test_forward_quote() -> Result<()> {
let alice = TestContext::new_alice().await;

View File

@@ -11,6 +11,7 @@ use async_std::task;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use crate::config::Config;
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::dc_tools::{time, EmailAddress};
use crate::imap::Imap;
@@ -77,24 +78,6 @@ impl Context {
self.free_ongoing().await;
if let Err(err) = res.as_ref() {
progress!(
self,
0,
Some(
stock_str::configuration_failed(
self,
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
format!("{:#}", err),
)
.await
)
);
} else {
progress!(self, 1000);
}
res
}
@@ -132,16 +115,58 @@ impl Context {
}
}
success?;
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
.await?;
Ok(())
match success {
Ok(_) => {
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
.await?;
progress!(self, 1000);
Ok(())
}
Err(err) => {
progress!(
self,
0,
Some(
stock_str::configuration_failed(
self,
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
format!("{:#}", err),
)
.await
)
);
Err(err)
}
}
}
}
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 1);
// Check basic settings.
ensure!(!param.addr.is_empty(), "Please enter an email address.");
// Only check for IMAP password, SMTP password is an "advanced" setting.
ensure!(!param.imap.password.is_empty(), "Please enter a password.");
if param.smtp.password.is_empty() {
param.smtp.password = param.imap.password.clone()
}
// Normalize authentication flags.
let oauth2 = match param.server_flags & DC_LP_AUTH_FLAGS as i32 {
DC_LP_AUTH_OAUTH2 => true,
DC_LP_AUTH_NORMAL => false,
_ => false,
};
param.server_flags &= !(DC_LP_AUTH_FLAGS as i32);
param.server_flags |= if oauth2 {
DC_LP_AUTH_OAUTH2 as i32
} else {
DC_LP_AUTH_NORMAL as i32
};
let socks5_config = param.socks5_config.clone();
let socks5_enabled = socks5_config.is_some();
@@ -151,9 +176,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Step 1: Load the parameters and check email-address and password
// Do oauth2 only if socks5 is disabled. As soon as we have a http library that can do
// socks5 requests, this can work with socks5 too. OAuth is always set either for both
// IMAP and SMTP or not at all.
if param.imap.oauth2 && !socks5_enabled {
// socks5 requests, this can work with socks5 too
if oauth2 && !socks5_enabled {
// the used oauth2 addr may differ, check this.
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
@@ -334,6 +358,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
&smtp_param,
&socks5_config,
&smtp_addr,
oauth2,
provider_strict_tls,
&mut smtp,
)
@@ -381,6 +406,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
&param.imap,
&param.socks5_config,
&param.addr,
oauth2,
provider_strict_tls,
)
.await
@@ -536,22 +562,26 @@ async fn try_imap_one_param(
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
) -> Result<Imap, ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
param.oauth2
param.user, param.server, param.port, param.security, param.certificate_checks, oauth2
);
info!(context, "Trying: {}", inf);
let (_s, r) = async_std::channel::bounded(1);
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r).await
let mut imap = match Imap::new(
param,
socks5_config.clone(),
addr,
oauth2,
provider_strict_tls,
r,
)
.await
{
Err(err) => {
info!(context, "failure: {}", err);
@@ -583,6 +613,7 @@ async fn try_smtp_one_param(
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
oauth2: bool,
provider_strict_tls: bool,
smtp: &mut Smtp,
) -> Result<(), ConfigurationError> {
@@ -593,7 +624,7 @@ async fn try_smtp_one_param(
param.port,
param.security,
param.certificate_checks,
param.oauth2,
oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
} else {
@@ -603,7 +634,14 @@ async fn try_smtp_one_param(
info!(context, "Trying: {}", inf);
if let Err(err) = smtp
.connect(context, param, socks5_config, addr, provider_strict_tls)
.connect(
context,
param,
socks5_config,
addr,
oauth2,
provider_strict_tls,
)
.await
{
info!(context, "failure: {}", err);

View File

@@ -3,7 +3,7 @@
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::time::{Duration, Instant, SystemTime};
use std::time::{Instant, SystemTime};
use anyhow::{ensure, Result};
use async_std::{
@@ -22,7 +22,6 @@ use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
use crate::ratelimit::Ratelimit;
use crate::scheduler::Scheduler;
use crate::sql::Sql;
@@ -56,7 +55,6 @@ pub struct InnerContext {
pub(crate) events: Events,
pub(crate) scheduler: RwLock<Option<Scheduler>>,
pub(crate) ratelimit: RwLock<Ratelimit>,
/// Recently loaded quota information, if any.
/// Set to `None` if quota was never tried to load.
@@ -115,8 +113,8 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
impl Context {
/// Creates new context and opens the database.
pub async fn new(dbfile: PathBuf, id: u32, events: Events) -> Result<Context> {
let context = Self::new_closed(dbfile, id, events).await?;
pub async fn new(dbfile: PathBuf, id: u32) -> Result<Context> {
let context = Self::new_closed(dbfile, id).await?;
// Open the database if is not encrypted.
if context.check_passphrase("".to_string()).await? {
@@ -126,7 +124,7 @@ impl Context {
}
/// Creates new context without opening the database.
pub async fn new_closed(dbfile: PathBuf, id: u32, events: Events) -> Result<Context> {
pub async fn new_closed(dbfile: PathBuf, id: u32) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -134,7 +132,7 @@ impl Context {
if !blobdir.exists().await {
async_std::fs::create_dir_all(&blobdir).await?;
}
let context = Context::with_blobdir(dbfile, blobdir, id, events).await?;
let context = Context::with_blobdir(dbfile, blobdir, id).await?;
Ok(context)
}
@@ -169,7 +167,6 @@ impl Context {
dbfile: PathBuf,
blobdir: PathBuf,
id: u32,
events: Events,
) -> Result<Context> {
ensure!(
blobdir.is_dir().await,
@@ -187,9 +184,8 @@ impl Context {
oauth2_mutex: Mutex::new(()),
wrong_pw_warning_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
events,
events: Events::default(),
scheduler: RwLock::new(None),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 3.0)), // Allow to send 3 messages immediately, no more than once every 20 seconds.
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
@@ -343,7 +339,7 @@ impl Context {
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let unset = "0";
let l = LoginParam::load_candidate_params_unchecked(self).await?;
let l = LoginParam::load_candidate_params(self).await?;
let l2 = LoginParam::load_configured_params(self).await?;
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let displayname = self.get_config(Config::Displayname).await?;
@@ -684,7 +680,7 @@ mod tests {
let tmp = tempfile::tempdir()?;
let dbfile = tmp.path().join("db.sqlite");
std::fs::write(&dbfile, b"123")?;
let res = Context::new(dbfile.into(), 1, Events::new()).await?;
let res = Context::new(dbfile.into(), 1).await?;
// Broken database is indistinguishable from encrypted one.
assert_eq!(res.is_open().await, false);
@@ -830,7 +826,7 @@ mod tests {
async fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(dbfile.into(), 1, Events::new()).await.unwrap();
Context::new(dbfile.into(), 1).await.unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
@@ -841,7 +837,7 @@ mod tests {
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(dbfile.into(), 1, Events::new()).await;
let res = Context::new(dbfile.into(), 1).await;
assert!(res.is_err());
}
@@ -851,7 +847,7 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(dbfile.into(), 1, Events::new()).await.unwrap();
Context::new(dbfile.into(), 1).await.unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -861,7 +857,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(dbfile.into(), blobdir, 1, Events::new()).await;
let res = Context::with_blobdir(dbfile.into(), blobdir, 1).await;
assert!(res.is_err());
}
@@ -870,7 +866,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(dbfile.into(), blobdir.into(), 1, Events::new()).await;
let res = Context::with_blobdir(dbfile.into(), blobdir.into(), 1).await;
assert!(res.is_err());
}
@@ -1039,7 +1035,7 @@ mod tests {
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(dbfile.clone().into(), id, Events::new())
let context = Context::new_closed(dbfile.clone().into(), id)
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
@@ -1047,7 +1043,7 @@ mod tests {
drop(context);
let id = 2;
let context = Context::new(dbfile.into(), id, Events::new())
let context = Context::new(dbfile.into(), id)
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);

View File

@@ -287,7 +287,7 @@ pub fn dehtml_manually(buf: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::simplify::{simplify, SimplifiedText};
use crate::simplify::simplify;
#[test]
fn test_dehtml() {
@@ -311,7 +311,7 @@ mod tests {
("<!some invalid html code>\n<b>some text</b>", "some text"),
];
for (input, output) in cases {
assert_eq!(simplify(dehtml(input).unwrap(), true).text, output);
assert_eq!(simplify(dehtml(input).unwrap(), true).0, output);
}
let none_cases = vec!["<html> </html>", ""];
for input in none_cases {
@@ -387,16 +387,10 @@ mod tests {
let input = include_str!("../test-data/message/gmx-quote-body.eml");
let dehtml = dehtml(input).unwrap();
println!("{}", dehtml);
let SimplifiedText {
text,
is_forwarded,
is_cut,
top_quote,
footer,
} = simplify(dehtml, false);
assert_eq!(text, "Test");
assert_eq!(is_forwarded, false);
assert_eq!(is_cut, false);
let (msg, forwarded, cut, top_quote, footer) = simplify(dehtml, false);
assert_eq!(msg, "Test");
assert_eq!(forwarded, false);
assert_eq!(cut, false);
assert_eq!(top_quote.as_deref(), Some("test"));
assert_eq!(footer, None);
}

View File

@@ -9,8 +9,7 @@ use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateSerial;
/// Event channel.
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Events {
receiver: Receiver<Event>,
sender: Sender<Event>,
@@ -18,17 +17,13 @@ pub struct Events {
impl Default for Events {
fn default() -> Self {
Self::new()
}
}
impl Events {
pub fn new() -> Self {
let (sender, receiver) = channel::bounded(1_000);
Self { receiver, sender }
}
}
impl Events {
pub fn emit(&self, event: Event) {
match self.sender.try_send(event) {
Ok(()) => {}

View File

@@ -51,26 +51,14 @@ fn format_line_flowed(line: &str, prefix: &str) -> String {
result + &buffer
}
/// Returns text formatted according to RFC 3767 (format=flowed).
///
/// This function accepts text separated by LF, but returns text
/// separated by CRLF.
///
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
/// SHOULD be set to "no" when sending.
pub(crate) fn format_flowed(text: &str) -> String {
fn format_flowed_prefix(text: &str, prefix: &str) -> String {
let mut result = String::new();
for line in text.split('\n') {
if !result.is_empty() {
result += "\r\n";
}
let line_no_prefix = line.strip_prefix('>');
let is_quote = line_no_prefix.is_some();
let line = line_no_prefix.unwrap_or(line).trim();
let prefix = if is_quote { "> " } else { "" };
let line = line.trim_end();
if prefix.len() + line.len() > 78 {
result += &format_line_flowed(line, prefix);
} else {
@@ -82,23 +70,23 @@ pub(crate) fn format_flowed(text: &str) -> String {
result += line;
}
}
result
}
/// Returns text formatted according to RFC 3767 (format=flowed).
///
/// This function accepts text separated by LF, but returns text
/// separated by CRLF.
///
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
/// SHOULD be set to "no" when sending.
pub fn format_flowed(text: &str) -> String {
format_flowed_prefix(text, "")
}
/// Same as format_flowed(), but adds "> " prefix to each line.
pub fn format_flowed_quote(text: &str) -> String {
let mut result = String::new();
for line in text.split('\n') {
if !result.is_empty() {
result += "\n";
}
result += "> ";
result += line;
}
format_flowed(&result)
format_flowed_prefix(text, "> ")
}
/// Joins lines in format=flowed text.
@@ -141,7 +129,6 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::TestContext;
#[test]
fn test_format_flowed() {
@@ -157,18 +144,18 @@ mod tests {
client and enter the setup code presented on the generating device.";
assert_eq!(format_flowed(text), expected);
let text = "> A quote";
assert_eq!(format_flowed(text), "> A quote");
let text = "> Not a quote";
assert_eq!(format_flowed(text), " > Not a quote");
// Test space stuffing of wrapped lines
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
> \n\
> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\
> clients.\r\n\
> \r\n\
> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
> client and enter the setup code presented on the generating device.";
let expected = "\x20> This is the Autocrypt Setup Message used to transfer your key between \r\n\
clients.\r\n\
\x20>\r\n\
\x20> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
client and enter the setup code presented on the generating device.";
assert_eq!(format_flowed(text), expected);
}
@@ -188,10 +175,6 @@ mod tests {
let expected = "> this is a quoted line";
assert_eq!(format_flowed_quote(quote), expected);
let quote = "first quoted line\nsecond quoted line";
let expected = "> first quoted line\r\n> second quoted line";
assert_eq!(format_flowed_quote(quote), expected);
let quote = "> foo bar baz";
let expected = "> > foo bar baz";
assert_eq!(format_flowed_quote(quote), expected);
@@ -202,25 +185,4 @@ mod tests {
> unwrapped on the receiver";
assert_eq!(format_flowed_quote(quote), expected);
}
#[async_std::test]
async fn test_send_quotes() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat = alice.create_chat(&bob).await;
let sent = alice.send_text(chat.id, "> First quote").await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some("> First quote"));
assert!(received.quoted_text().is_none());
assert!(received.quoted_message(&bob).await?.is_none());
let sent = alice.send_text(chat.id, "> Second quote").await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some("> Second quote"));
assert!(received.quoted_text().is_none());
assert!(received.quoted_message(&bob).await?.is_none());
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More