Compare commits

..

25 Commits

Author SHA1 Message Date
Simon Laux
c14e5086d7 improve naming 2022-06-28 13:25:41 +02:00
Simon Laux
4779383401 commit types.ts
that dc-node has everything it needs to provide @deltachat/jsonrpc-client
without an extra ts compile step
2022-06-26 02:08:43 +02:00
Simon Laux
47d30ef6d3 add @deltachat/jsonrpc-client
to make sure its dependencies are installed, too
whwn installing dc-node
2022-06-26 01:48:01 +02:00
Simon Laux
3a38ffdfe0 disable jsonrpc by default 2022-06-26 00:06:43 +02:00
Simon Laux
33d548eccc put jsonrpc stuff in own module 2022-06-25 23:56:56 +02:00
Simon Laux
1700af2c8d remove selectAccount from highlevel client 2022-06-25 23:38:13 +02:00
Simon Laux
01920a1a00 activate other tests again 2022-06-25 23:38:13 +02:00
Simon Laux
7c67ea0b8a fix closing segfault
thanks again to link2xt for figguring this out
2022-06-25 23:38:13 +02:00
Simon Laux
0c64701984 break loop on empty response 2022-06-25 23:38:13 +02:00
Simon Laux
d2d35fe26b call a jsonrpc function in segfault example 2022-06-25 23:38:13 +02:00
Simon Laux
a0b4d016d5 add jsonrpc feature flag 2022-06-25 23:38:13 +02:00
Simon Laux
3ce70ee244 add jsonrpc crate to set_core_version 2022-06-25 23:38:13 +02:00
Simon Laux
c43c9b9107 add some files to npm ignore
that don't need to be in the npm package
2022-06-25 23:38:13 +02:00
Simon Laux
638d2ff932 add json api to cffi and expose it in dc node 2022-06-25 23:38:13 +02:00
Simon Laux
0213bb372f cargo.lock changed 2022-06-25 23:36:39 +02:00
Simon Laux
d54fa65ff3 fix compile after rebase 2022-06-25 23:36:39 +02:00
Simon Laux
564d283852 change now returns event names as id
directly, no conversion method or number ids anymore

also longer timeout for requesting test accounts from mailadm
2022-06-25 23:36:39 +02:00
Simon Laux
8357b3a98c update .gitignore 2022-06-25 23:36:39 +02:00
Simon Laux
177f89f678 fix formatting
make test  pass
fix clippy
2022-06-25 23:36:39 +02:00
Simon Laux
372425f38f refactor function name 2022-06-25 23:36:39 +02:00
Simon Laux
53f8274c6f fix get_provider_info docs 2022-06-25 23:36:39 +02:00
Simon Laux
65b242aa5c use node 16 in ci
use `npm i` instead of `npm ci`
try fix ci script
and fix a doc comment
2022-06-25 23:36:39 +02:00
Simon Laux
bb6d7767b5 fix clippy 2022-06-25 23:36:39 +02:00
Simon Laux
227a75a5f7 get target dir from cargo 2022-06-25 23:36:39 +02:00
Simon Laux
8fb46d0b56 integrate json-rpc repo
https://github.com/deltachat/deltachat-jsonrpc
2022-06-25 23:36:39 +02:00
124 changed files with 6092 additions and 2247 deletions

View File

@@ -10,7 +10,7 @@ on:
env:
RUSTFLAGS: -Dwarnings
jobs:
fmt:
@@ -83,13 +83,13 @@ jobs:
rust: 1.61.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.56.1
# Minimum Supported Rust Version = 1.56.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.56.1
rust: 1.56.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:

66
.github/workflows/jsonrpc_api.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
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

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

View File

@@ -2,16 +2,6 @@
## Unreleased
### Changes
- switch from `async-std` to `tokio` as the async runtime #3449
### Fixes
- mailing list: remove square-brackets only for first name #3452
- do not use footers from mailinglists as the contact status #3460
## 1.87.0
### Changes
- limit the rate of MDN sending #3402
- ignore ratelimits for bots #3439
@@ -19,7 +9,6 @@
- format message lines starting with `>` as quotes #3434
- node: remove `split2` dependency #3418
- node: add git installation info to readme #3418
- limit the rate of webxdc update sending #3417
### Fixes
- set a default error if NDN does not provide an error
@@ -35,7 +24,6 @@
- python: added `Message.is_videochat_invitation()` #3416
- python: added support for "videochat" and "webxdc" view types to `Message.new_empty()` #3416
## 1.86.0
### API-Changes

View File

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

1864
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.87.0"
version = "1.86.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
@@ -9,7 +9,6 @@ rust-version = "1.56"
[profile.dev]
debug = 0
panic = 'abort'
opt-level = 1
[profile.release]
lto = true
@@ -20,19 +19,19 @@ deltachat_derive = { path = "./deltachat_derive" }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1"
async-imap = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.21"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
async-imap = { git = "https://github.com/async-email/async-imap" }
async-native-tls = { version = "0.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", default-features=false, features = ["smtp-transport", "socks5"] }
async-std-resolver = "0.21"
async-std = { version = "1" }
async-tar = { version = "0.4", default-features=false }
backtrace = "0.3"
base64 = "0.13"
bitflags = "1.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
dirs = { version = "4", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = "0.2"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
@@ -66,24 +65,22 @@ sha2 = "0.10"
smallvec = "1"
strum = "0.24"
strum_macros = "0.24"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
thiserror = "1"
toml = "0.5"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
fast-socks5 = "0.8"
fast-socks5 = "0.4"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.3.3"
textwrap = "0.15.0"
async-channel = "1.6.1"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.9", features = ["fs"] }
reqwest = { version = "0.11.11", features = ["json"] }
async_zip = { git = "https://github.com/dignifiedquire/rs-async-zip", branch = "main", default-features = false, features = ["deflate"] }
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.3.4", features = ["async_tokio"] }
async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = { version = "0.3.4", features = ["async_std"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
@@ -94,6 +91,7 @@ tempfile = "3"
members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc"
]
[[example]]
@@ -135,10 +133,5 @@ harness = false
default = ["vendored"]
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
vendored = [
"async-native-tls/vendored",
"async-smtp/native-tls-vendored",
"rusqlite/bundled-sqlcipher-vendored-openssl",
"reqwest/native-tls-vendored"
]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled-sqlcipher-vendored-openssl"]
nightly = ["pgp/nightly"]

View File

@@ -1,3 +1,4 @@
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;
@@ -8,7 +9,9 @@ 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, id, Events::new()).await.unwrap();
let context = Context::new(dbfile.into(), id, Events::new())
.await
.unwrap();
let book = (0..n)
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
@@ -24,16 +27,12 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
}
fn criterion_benchmark(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
c.bench_function("create 500 contacts", |b| {
b.to_async(&rt)
.iter(|| async { address_book_benchmark(black_box(500), black_box(0)).await })
b.iter(|| block_on(async { address_book_benchmark(black_box(500), black_box(0)).await }))
});
c.bench_function("create 100 contacts and read it 1000 times", |b| {
b.to_async(&rt)
.iter(|| async { address_book_benchmark(black_box(100), black_box(1000)).await })
b.iter(|| block_on(async { address_book_benchmark(black_box(100), black_box(1000)).await }))
});
}

View File

@@ -1,11 +1,12 @@
use async_std::path::PathBuf;
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::accounts::Accounts;
use std::path::PathBuf;
use tempfile::tempdir;
async fn create_accounts(n: u32) {
let dir = tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let p: PathBuf = dir.path().join("accounts").into();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
@@ -17,8 +18,7 @@ async fn create_accounts(n: u32) {
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("create 1 account", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
b.to_async(&rt).iter(|| create_accounts(black_box(1)))
b.iter(|| block_on(async { create_accounts(black_box(1)).await }))
});
}

View File

@@ -1,5 +1,6 @@
use std::path::Path;
use async_std::path::Path;
use criterion::async_executor::AsyncStdExecutor;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
@@ -9,7 +10,9 @@ use deltachat::Events;
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
let id = 100;
let context = Context::new(dbfile, id, Events::new()).await.unwrap();
let context = Context::new(dbfile.into(), id, Events::new())
.await
.unwrap();
for c in chats.iter().take(10) {
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
@@ -20,10 +23,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 rt = tokio::runtime::Runtime::new().unwrap();
let chats: Vec<_> = rt.block_on(async {
let context = Context::new(Path::new(&path), 100, Events::new())
let chats: Vec<_> = async_std::task::block_on(async {
let context = Context::new((&path).into(), 100, Events::new())
.await
.unwrap();
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
@@ -32,7 +33,7 @@ fn criterion_benchmark(c: &mut Criterion) {
});
c.bench_function("chat::get_chat_msgs (load messages from 10 chats)", |b| {
b.to_async(&rt)
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_msgs_benchmark(black_box(path.as_ref()), black_box(&chats)))
});
} else {

View File

@@ -1,5 +1,4 @@
use std::path::Path;
use criterion::async_executor::AsyncStdExecutor;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chatlist::Chatlist;
@@ -14,14 +13,11 @@ fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(async {
Context::new(Path::new(&path), 100, Events::new())
.await
.unwrap()
let context = async_std::task::block_on(async {
Context::new(path.into(), 100, Events::new()).await.unwrap()
});
c.bench_function("chatlist:try_load (Get Chatlist)", |b| {
b.to_async(&rt)
b.to_async(AsyncStdExecutor)
.iter(|| get_chat_list_benchmark(black_box(&context)))
});
} else {

View File

@@ -1,6 +1,8 @@
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use async_std::{path::PathBuf, task::block_on};
use criterion::{
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
Criterion,
};
use deltachat::{
config::Config,
context::Context,
@@ -41,13 +43,15 @@ async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new(&dbfile, id, Events::new()).await.unwrap();
let context = Context::new(dbfile.into(), id, Events::new())
.await
.unwrap();
let backup: PathBuf = std::env::current_dir()
.unwrap()
.join("delta-chat-backup.tar");
if backup.exists() {
.join("delta-chat-backup.tar")
.into();
if backup.exists().await {
println!("Importing backup");
imex(&context, ImexMode::ImportBackup, &backup, None)
.await
@@ -70,15 +74,11 @@ async fn create_context() -> Context {
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("Receive messages");
group.bench_function("Receive 100 simple text msgs", |b| {
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(create_context());
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
recv_all_emails(black_box(ctx)).await;
}
});
b.to_async(AsyncStdExecutor).iter_batched(
|| block_on(create_context()),
|context| recv_all_emails(black_box(context)),
BatchSize::LargeInput,
);
});
group.finish();
}

View File

@@ -1,11 +1,13 @@
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::Events;
use std::path::Path;
async fn search_benchmark(dbfile: impl AsRef<Path>) {
async fn search_benchmark(path: impl AsRef<Path>) {
let dbfile = path.as_ref();
let id = 100;
let context = Context::new(dbfile.as_ref(), id, Events::new())
let context = Context::new(dbfile.into(), id, Events::new())
.await
.unwrap();
@@ -18,10 +20,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 rt = tokio::runtime::Runtime::new().unwrap();
c.bench_function("search hello", |b| {
b.to_async(&rt).iter(|| search_benchmark(black_box(&path)))
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.87.0"
version = "1.86.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -16,18 +16,19 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = "0.2"
human-panic = "1"
num-traits = "0.2"
serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread"] }
async-std = "1"
anyhow = "1"
thiserror = "1"
rand = "0.7"
once_cell = "1.12.0"
[features]
default = ["vendored"]
vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"]
jsonrpc = ["deltachat-jsonrpc"]

View File

@@ -23,7 +23,7 @@ typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
/**
* @mainpage Getting started
@@ -5178,6 +5178,55 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
/**
* @class dc_jsonrpc_instance_t
*
* Opaque object for using the json rpc api from the cffi bindings.
*/
/**
* Create the jsonrpc instance that is used to call the jsonrpc.
*
* @memberof dc_accounts_t
* @param account_manager The accounts object as created by dc_accounts_new().
* @return Returns the jsonrpc instance, NULL on errors.
* Must be freed using dc_jsonrpc_unref() after usage.
*
*/
dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);
/**
* Free a jsonrpc instance.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* If NULL is given, nothing is done and an error is logged.
*/
void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Makes an asynchronous jsonrpc request,
* returns immediately and once the result is ready it can be retrieved via dc_jsonrpc_next_response()
* the jsonrpc specification defines an invocation id that can then be used to match request and response.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param request JSON-RPC request as string
*/
void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, 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

@@ -15,19 +15,18 @@ extern crate human_panic;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt::Write;
use std::future::Future;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use async_std::sync::RwLock;
use async_std::task::{block_on, spawn};
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use num_traits::{FromPrimitive, ToPrimitive};
use once_cell::sync::Lazy;
use rand::Rng;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
@@ -40,7 +39,6 @@ use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use tokio::task::JoinHandle;
mod dc_array;
mod lot;
@@ -64,23 +62,6 @@ use deltachat::chatlist::Chatlist;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
where
T: Future,
{
RT.block_on(fut)
}
fn spawn<T>(fut: T) -> JoinHandle<T::Output>
where
T: Future + Send + 'static,
T::Output: Send + 'static,
{
RT.spawn(fut)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new(
_os_name: *const libc::c_char,
@@ -97,7 +78,11 @@ 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), id, Events::new()))
block_on(Context::new(
as_path(dbfile).to_path_buf().into(),
id,
Events::new(),
))
} else {
eprintln!("blobdir can not be defined explicitly anymore");
return ptr::null_mut();
@@ -121,7 +106,11 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(as_path(dbfile), id, Events::new())) {
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) => {
eprintln!("failed to create context: {:#}", err);
@@ -693,13 +682,10 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
}
let events = &*events;
block_on(async move {
events
.recv()
.await
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
})
events
.recv_sync()
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
}
#[no_mangle]
@@ -2408,7 +2394,7 @@ pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut l
return "".strdup();
}
let ctx = &*context;
ctx.get_last_error().strdup()
block_on(ctx.get_last_error()).strdup()
}
// dc_array_t
@@ -4108,11 +4094,11 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
/// `dc_accounts_t` in multiple threads at once.
pub struct AccountsWrapper {
inner: RwLock<Accounts>,
inner: Arc<RwLock<Accounts>>,
}
impl Deref for AccountsWrapper {
type Target = RwLock<Accounts>;
type Target = Arc<RwLock<Accounts>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -4121,7 +4107,7 @@ impl Deref for AccountsWrapper {
impl AccountsWrapper {
fn new(accounts: Accounts) -> Self {
let inner = RwLock::new(accounts);
let inner = Arc::new(RwLock::new(accounts));
Self { inner }
}
}
@@ -4141,7 +4127,7 @@ pub unsafe extern "C" fn dc_accounts_new(
return ptr::null_mut();
}
let accs = block_on(Accounts::new(as_path(dbfile).into()));
let accs = block_on(Accounts::new(as_path(dbfile).to_path_buf().into()));
match accs {
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
@@ -4313,7 +4299,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
block_on(async move {
let mut accounts = accounts.write().await;
match accounts
.migrate_account(std::path::PathBuf::from(dbfile))
.migrate_account(async_std::path::PathBuf::from(dbfile))
.await
{
Ok(id) => id,
@@ -4433,7 +4419,83 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
return ptr::null_mut();
}
let emitter = &mut *emitter;
block_on(emitter.recv())
emitter
.recv_sync()
.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())
}
}

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

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

View File

@@ -0,0 +1,39 @@
[package]
name = "deltachat-jsonrpc"
version = "1.86.0"
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

@@ -0,0 +1,75 @@
# 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.

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

@@ -0,0 +1,347 @@
## 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

@@ -0,0 +1,154 @@
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

@@ -0,0 +1,536 @@
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

@@ -0,0 +1,46 @@
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

@@ -0,0 +1,91 @@
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

@@ -0,0 +1,117 @@
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

@@ -0,0 +1,50 @@
use anyhow::Result;
use deltachat::contact::VerifiedStatus;
use deltachat::context::Context;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Contact")]
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

@@ -0,0 +1,127 @@
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

@@ -0,0 +1,10 @@
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

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

@@ -0,0 +1,60 @@
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

@@ -0,0 +1,44 @@
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

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

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
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

@@ -0,0 +1,251 @@
// 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

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

View File

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

View File

@@ -0,0 +1,15 @@
// 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

@@ -0,0 +1,54 @@
<!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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,41 @@
{
"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

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,77 @@
import * as T from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { EventTypeName } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "tiny-emitter";
export type DeltachatEvent = {
id: EventTypeName;
contextId: number;
field1: any;
field2: any;
};
export type Events = Record<
EventTypeName | "ALL",
(event: DeltachatEvent) => void
>;
export class BaseDeltachat<
Transport extends BaseTransport
> 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

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

View File

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

View File

@@ -0,0 +1,158 @@
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

@@ -0,0 +1,203 @@
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

@@ -0,0 +1,95 @@
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

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

View File

@@ -0,0 +1,17 @@
{
"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

@@ -0,0 +1,20 @@
{
"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

@@ -1,10 +1,9 @@
extern crate dirs;
use std::path::Path;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Result};
use async_std::path::Path;
use deltachat::chat::{
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
};
@@ -23,7 +22,8 @@ use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::sql;
use deltachat::{config, provider};
use tokio::fs;
use std::fs;
use std::time::{Duration, SystemTime};
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
@@ -135,13 +135,17 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
} else {
/* import a directory */
let dir_name = std::path::Path::new(&real_spec);
let dir = fs::read_dir(dir_name).await;
let dir = std::fs::read_dir(dir_name);
if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return false;
} else {
let mut dir = dir.unwrap();
while let Ok(Some(entry)) = dir.next_entry().await {
let dir = dir.unwrap();
for entry in dir {
if entry.is_err() {
break;
}
let entry = entry.unwrap();
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
@@ -488,7 +492,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let setup_code = create_setup_code(&context);
let file_name = blobdir.join("autocrypt-setup-message.html");
let file_content = render_setup_file(&context, &setup_code).await?;
fs::write(&file_name, file_content).await?;
async_std::fs::write(&file_name, file_content).await?;
println!(
"Setup message written to: {}\nSetup code: {}",
file_name.display(),
@@ -528,7 +532,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.join("connectivity.html");
match context.get_connectivity_html().await {
Ok(html) => {
fs::write(&file, html).await?;
fs::write(&file, html)?;
println!("Report written to: {:#?}", file);
}
Err(err) => {
@@ -888,7 +892,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No html-file given.");
let path: &Path = arg1.as_ref();
let html = &*fs::read(&path).await?;
let html = &*fs::read(&path)?;
let html = String::from_utf8_lossy(html);
let mut msg = Message::new(Viewtype::Text);
@@ -1075,7 +1079,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap_or_default()
.join(format!("msg-{}.html", id.to_u32()));
let html = id.get_html(&context).await?.unwrap_or_default();
fs::write(&file, html).await?;
fs::write(&file, html)?;
println!("HTML written to: {:#?}", file);
}
"listfresh" => {

View File

@@ -9,16 +9,15 @@ extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use ansi_term::Color;
use anyhow::{bail, Error};
use async_std::path::Path;
use deltachat::chat::ChatId;
use deltachat::config;
use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::{EventType, Events};
use log::{error, info, warn};
@@ -31,11 +30,11 @@ use rustyline::validate::Validator;
use rustyline::{
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
};
use tokio::fs;
use tokio::runtime::Handle;
mod cmdline;
use self::cmdline::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use std::fs;
/// Event Handler
fn receive_event(event: EventType) {
@@ -299,10 +298,10 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = Context::new(Path::new(&args[1]), 0, Events::new()).await?;
let context = Context::new(Path::new(&args[1]).to_path_buf(), 0, Events::new()).await?;
let events = context.get_event_emitter();
tokio::task::spawn(async move {
async_std::task::spawn(async move {
while let Some(event) = events.recv().await {
receive_event(event.typ);
}
@@ -317,9 +316,8 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
.output_stream(OutputStreamType::Stdout)
.build();
let mut selected_chat = ChatId::default();
let ctx = context.clone();
let input_loop = tokio::task::spawn_blocking(move || {
let (reader_s, reader_r) = async_std::channel::bounded(100);
let input_loop = async_std::task::spawn_blocking(move || {
let h = DcHelper {
completer: FilenameCompleter::new(),
highlighter: MatchingBracketHighlighter::new(),
@@ -341,30 +339,16 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str());
let contine = Handle::current().block_on(async {
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => true,
Ok(ExitResult::Exit) => {
println!("Exiting ...");
false
}
Err(err) => {
println!("Error: {}", err);
true
}
}
});
if !contine {
break;
}
async_std::task::block_on(reader_s.send(line)).unwrap();
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
drop(reader_s);
break;
}
Err(err) => {
println!("Error: {}", err);
drop(reader_s);
break;
}
}
@@ -375,8 +359,15 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok::<_, Error>(())
});
while let Ok(line) = reader_r.recv().await {
match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => {}
Ok(ExitResult::Exit) => break,
Err(err) => println!("Error: {}", err),
}
}
context.stop_io().await;
input_loop.await??;
input_loop.await?;
Ok(())
}
@@ -446,7 +437,7 @@ async fn handle_cmd(
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {
fs::write(&file, svg).await?;
fs::write(&file, svg)?;
println!("QR code svg written to: {:#?}", file);
}
Err(err) => {
@@ -467,12 +458,11 @@ async fn handle_cmd(
Ok(ExitResult::Continue)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
fn main() -> Result<(), Error> {
let _ = pretty_env_logger::try_init();
let args = std::env::args().collect();
start(args).await?;
async_std::task::block_on(async move { start(args).await })?;
Ok(())
}

View File

@@ -29,21 +29,21 @@ fn cb(event: EventType) {
}
/// Run with `RUST_LOG=simple=info cargo run --release --example simple --features repl -- email pw`.
#[tokio::main]
#[async_std::main]
async fn main() {
pretty_env_logger::try_init_timed().ok();
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
log::info!("creating database {:?}", dbfile);
let ctx = Context::new(&dbfile, 0, Events::new())
let ctx = Context::new(dbfile.into(), 0, Events::new())
.await
.expect("Failed to create context");
let info = ctx.get_info().await;
log::info!("info: {:#?}", info);
let events = ctx.get_event_emitter();
let events_spawn = tokio::task::spawn(async move {
let events_spawn = async_std::task::spawn(async move {
while let Some(event) = events.recv().await {
cb(event.typ);
}
@@ -80,7 +80,7 @@ async fn main() {
}
// wait for the message to be sent out
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
log::info!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
@@ -96,5 +96,5 @@ async fn main() {
ctx.stop_io().await;
log::info!("closing");
drop(ctx);
events_spawn.await.unwrap();
events_spawn.await;
}

View File

@@ -19,10 +19,11 @@ interface NativeAccount {}
export class AccountManager extends EventEmitter {
dcn_accounts: NativeAccount
accountDir: string
jsonRpcStarted = false
constructor(cwd: string, os = 'deltachat-node') {
debug('DeltaChat constructor')
super()
debug('DeltaChat constructor')
this.accountDir = cwd
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
@@ -114,6 +115,31 @@ export class AccountManager extends EventEmitter {
debug('Started event handler')
}
startJSONRPCHandler(callback: ((response: string) => void) | null) {
if (this.dcn_accounts === null) {
throw new Error('dcn_account is null')
}
if (!callback) {
throw new Error('no callback set')
}
if (this.jsonRpcStarted) {
throw new Error('jsonrpc was started already')
}
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this))
debug('Started 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

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

View File

@@ -34,6 +34,9 @@ typedef struct dcn_accounts_t {
dc_accounts_t* dc_accounts;
napi_threadsafe_function threadsafe_event_handler;
uv_thread_t event_handler_thread;
napi_threadsafe_function threadsafe_jsonrpc_handler;
uv_thread_t jsonrpc_thread;
dc_jsonrpc_instance_t* jsonrpc_instance;
int gc;
} dcn_accounts_t;
@@ -2932,6 +2935,11 @@ NAPI_METHOD(dcn_accounts_unref) {
uv_thread_join(&dcn_accounts->event_handler_thread);
dcn_accounts->event_handler_thread = 0;
}
if (dcn_accounts->jsonrpc_instance) {
dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, "{}");
uv_thread_join(&dcn_accounts->jsonrpc_thread);
dcn_accounts->jsonrpc_instance = NULL;
}
dc_accounts_unref(dcn_accounts->dc_accounts);
dcn_accounts->dc_accounts = NULL;
@@ -3090,8 +3098,6 @@ static void accounts_event_handler_thread_func(void* arg)
{
dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg;
TRACE("event_handler_thread_func starting");
dc_accounts_event_emitter_t * dc_accounts_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
@@ -3242,6 +3248,125 @@ NAPI_METHOD(dcn_accounts_start_event_handler) {
NAPI_RETURN_UNDEFINED();
}
// JSON RPC
static void accounts_jsonrpc_thread_func(void* arg)
{
dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg;
TRACE("accounts_jsonrpc_thread_func starting");
char* response;
while (true) {
response = dc_jsonrpc_next_response(dcn_accounts->jsonrpc_instance);
if (response == NULL) {
// done or broken
break;
}
if (!dcn_accounts->threadsafe_jsonrpc_handler) {
TRACE("threadsafe_jsonrpc_handler not set, bailing");
break;
}
// Don't process events if we're being garbage collected!
if (dcn_accounts->gc == 1) {
TRACE("dc_accounts has been destroyed, bailing");
break;
}
napi_status status = napi_call_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, response, napi_tsfn_blocking);
if (status == napi_closing) {
TRACE("JS function got released, bailing");
break;
}
}
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() {
/**
@@ -3512,4 +3637,9 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_webxdc_status_update);
NAPI_EXPORT_FUNCTION(dcn_get_webxdc_status_updates);
NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_blob);
/** jsonrpc **/
NAPI_EXPORT_FUNCTION(dcn_accounts_start_jsonrpc);
NAPI_EXPORT_FUNCTION(dcn_json_rpc_request);
}

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
{
"dependencies": {
"@deltachat/jsonrpc-client": "file:deltachat-jsonrpc/typescript",
"debug": "^4.1.1",
"napi-macros": "^2.0.0",
"node-gyp-build": "^4.1.0"
@@ -60,5 +61,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
},
"types": "node/dist/index.d.ts",
"version": "1.87.0"
}
"version": "1.86.0"
}

View File

@@ -84,9 +84,8 @@ jobs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/coredeps
CONTEXT: deltachat-core-rust/scripts/docker-coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_x86_64
platform: linux
caches:
- path: cache
@@ -184,9 +183,8 @@ jobs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/coredeps
CONTEXT: deltachat-core-rust/scripts/docker-coredeps-arm64
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_aarch64
platform: linux
caches:
- path: cache
@@ -232,73 +230,3 @@ jobs:
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*manylinux201*
- name: python-musl-x86_64
plan:
- get: deltachat-core-rust
- get: deltachat-core-rust-release
trigger: true
# Build manylinux image with additional dependencies
- task: build-coredeps
privileged: true
config:
inputs:
# Building the latest, not tagged coredeps
- name: deltachat-core-rust
image_resource:
source:
repository: concourse/oci-build-task
type: registry-image
outputs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_x86_64
platform: linux
caches:
- path: cache
run:
path: build
# Use built image to build python wheels
- task: build-wheels
image: coredeps-image
config:
inputs:
- name: deltachat-core-rust-release
path: .
outputs:
- name: py-wheels
path: ./python/.docker-tox/wheelhouse/
platform: linux
run:
path: bash
args:
- -exc
- |
scripts/run_all.sh
# Upload musl x86_64 wheels
- task: upload-wheels
config:
inputs:
- name: py-wheels
image_resource:
type: registry-image
source:
repository: debian
platform: linux
run:
path: sh
args:
- -ec
- |
apt-get update -y
apt-get install -y --no-install-recommends python3-pip python3-setuptools
pip3 install devpi
devpi use https://m.devpi.net/dc/master
devpi login ((devpi.login)) --password ((devpi.password))
devpi upload py-wheels/*musllinux_1_1_x86_64*

View File

@@ -1,8 +0,0 @@
ARG BASEIMAGE=quay.io/pypa/manylinux2014_x86_64
#ARG BASEIMAGE=quay.io/pypa/musllinux_1_1_x86_64
#ARG BASEIMAGE=quay.io/pypa/manylinux2014_aarch64
FROM $BASEIMAGE
RUN pipx install tox
COPY install-rust.sh /scripts/
RUN /scripts/install-rust.sh

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Install Rust
#
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.61.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$ARCH-unknown-linux-$LIBC"
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$ARCH-unknown-linux-$LIBC"

View File

@@ -0,0 +1,6 @@
FROM quay.io/pypa/manylinux2014_aarch64
RUN pipx install tox
# Install Rust
ADD deps/build_rust.sh /builder/build_rust.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e -x
# Install Rust
#
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.61.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

@@ -0,0 +1,6 @@
FROM quay.io/pypa/manylinux2014_x86_64
RUN pipx install tox
# Install Rust
ADD deps/build_rust.sh /builder/build_rust.sh
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e -x
# Install Rust
#
# Path from https://forge.rust-lang.org/infra/other-installation-methods.html
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.61.0
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
./install.sh --prefix=/usr --components=rustc,cargo,"rust-std-$(uname -m)-unknown-linux-gnu"
rustc --version
cd ..
rm -fr "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"

View File

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

View File

@@ -1,12 +1,13 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use async_std::fs;
use async_std::path::PathBuf;
use uuid::Uuid;
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use uuid::Uuid;
use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
@@ -25,7 +26,7 @@ pub struct Accounts {
impl Accounts {
/// Loads or creates an accounts folder at the given `dir`.
pub async fn new(dir: PathBuf) -> Result<Self> {
if !dir.exists() {
if !dir.exists().await {
Accounts::create(&dir).await?;
}
@@ -46,10 +47,14 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
pub async fn open(dir: PathBuf) -> Result<Self> {
ensure!(dir.exists(), "directory does not exist");
ensure!(dir.exists().await, "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
ensure!(
config_file.exists().await,
"{:?} does not exist",
config_file
);
let config = Config::from_file(config_file)
.await
@@ -101,7 +106,7 @@ impl Accounts {
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new(
&account_config.dbfile(),
account_config.dbfile().into(),
account_config.id,
self.events.clone(),
)
@@ -116,7 +121,7 @@ impl Accounts {
let account_config = self.config.new_account(&self.dir).await?;
let ctx = Context::new_closed(
&account_config.dbfile(),
account_config.dbfile().into(),
account_config.id,
self.events.clone(),
)
@@ -143,7 +148,7 @@ impl Accounts {
loop {
counter += 1;
if let Err(err) = fs::remove_dir_all(&cfg.dir)
if let Err(err) = fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
.await
.context("failed to remove account data")
{
@@ -152,7 +157,7 @@ impl Accounts {
}
// Wait 1 second and try again.
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
} else {
break;
}
@@ -168,8 +173,16 @@ impl Accounts {
let blobdir = Context::derive_blobdir(&dbfile);
let walfile = Context::derive_walfile(&dbfile);
ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
ensure!(
dbfile.exists().await,
"no database found: {}",
dbfile.display()
);
ensure!(
blobdir.exists().await,
"no blobdir found: {}",
blobdir.display()
);
let old_id = self.config.get_selected_account().await;
@@ -180,7 +193,7 @@ impl Accounts {
.await
.context("failed to create new account")?;
let new_dbfile = account_config.dbfile();
let new_dbfile = account_config.dbfile().into();
let new_blobdir = Context::derive_blobdir(&new_dbfile);
let new_walfile = Context::derive_walfile(&new_dbfile);
@@ -194,7 +207,7 @@ impl Accounts {
fs::rename(&blobdir, &new_blobdir)
.await
.context("failed to rename blobdir")?;
if walfile.exists() {
if walfile.exists().await {
fs::rename(&walfile, &new_walfile)
.await
.context("failed to rename walfile")?;
@@ -204,13 +217,13 @@ 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, self.events.clone()).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
}
Err(err) => {
// remove temp account
fs::remove_dir_all(std::path::PathBuf::from(&account_config.dir))
fs::remove_dir_all(async_std::path::PathBuf::from(&account_config.dir))
.await
.context("failed to remove account data")?;
@@ -308,7 +321,7 @@ struct InnerConfig {
}
impl Config {
pub async fn new(dir: &Path) -> Result<Self> {
pub async fn new(dir: &PathBuf) -> Result<Self> {
let inner = InnerConfig {
accounts: Vec::new(),
selected_account: 0,
@@ -342,14 +355,18 @@ impl Config {
pub async fn load_accounts(&self, events: &Events) -> Result<BTreeMap<u32, Context>> {
let mut accounts = BTreeMap::new();
for account_config in &self.inner.accounts {
let ctx = Context::new(&account_config.dbfile(), 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,
events.clone(),
)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile()
)
})?;
accounts.insert(account_config.id, ctx);
}
@@ -358,7 +375,7 @@ impl Config {
}
/// Create a new account in the given root directory.
async fn new_account(&mut self, dir: &Path) -> Result<AccountConfig> {
async fn new_account(&mut self, dir: &PathBuf) -> Result<AccountConfig> {
let id = {
let id = self.inner.next_id;
let uuid = Uuid::new_v4();
@@ -366,7 +383,7 @@ impl Config {
self.inner.accounts.push(AccountConfig {
id,
dir: target_dir,
dir: target_dir.into(),
uuid,
});
self.inner.next_id += 1;
@@ -447,10 +464,10 @@ impl AccountConfig {
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_account_new_open() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts1");
let p: PathBuf = dir.path().join("accounts1").into();
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
accounts1.add_account().await.unwrap();
@@ -465,10 +482,10 @@ mod tests {
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_account_new_add_remove() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let p: PathBuf = dir.path().join("accounts").into();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
@@ -492,10 +509,10 @@ mod tests {
assert_eq!(accounts.accounts.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_accounts_remove_last() -> Result<()> {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts");
let p: PathBuf = dir.path().join("accounts").into();
let mut accounts = Accounts::new(p.clone()).await?;
assert!(accounts.get_selected_account().await.is_none());
@@ -513,17 +530,17 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_migrate_account() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let p: PathBuf = dir.path().join("accounts").into();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
let extern_dbfile: PathBuf = dir.path().join("other");
let ctx = Context::new(&extern_dbfile, 0, Events::new())
let extern_dbfile: PathBuf = dir.path().join("other").into();
let ctx = Context::new(extern_dbfile.clone(), 0, Events::new())
.await
.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
@@ -550,10 +567,10 @@ mod tests {
}
/// Tests that accounts are sorted by ID.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_accounts_sorted() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let p: PathBuf = dir.path().join("accounts").into();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
@@ -568,10 +585,10 @@ mod tests {
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_accounts_ids_unique_increasing_and_persisted() -> Result<()> {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts");
let p: PathBuf = dir.path().join("accounts").into();
let dummy_accounts = 10;
let (id0, id1, id2) = {
@@ -650,10 +667,10 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_no_accounts_event_emitter() -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new(p.clone()).await?;
@@ -665,7 +682,7 @@ mod tests {
// Test that event emitter does not return `None` immediately.
let duration = std::time::Duration::from_millis(1);
assert!(tokio::time::timeout(duration, event_emitter.recv())
assert!(async_std::future::timeout(duration, event_emitter.recv())
.await
.is_err());
@@ -676,10 +693,10 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_encrypted_account() -> Result<()> {
let dir = tempfile::tempdir().context("failed to create tempdir")?;
let p: PathBuf = dir.path().join("accounts");
let p: PathBuf = dir.path().join("accounts").into();
let mut accounts = Accounts::new(p.clone())
.await

View File

@@ -4,13 +4,14 @@ use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::{format_err, Context as _, Error, Result};
use image::{DynamicImage, ImageFormat};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
use crate::config::Config;
use crate::constants::{
@@ -88,7 +89,7 @@ impl<'a> BlobObject<'a> {
Err(err) => {
if attempt >= MAX_ATTEMPT {
return Err(err).context("failed to create file");
} else if attempt == 1 && !dir.exists() {
} else if attempt == 1 && !dir.exists().await {
fs::create_dir_all(dir).await.ok_or_log(context);
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
@@ -370,81 +371,108 @@ impl<'a> BlobObject<'a> {
mut img_wh: u32,
max_bytes: Option<usize>,
) -> Result<Option<String>> {
tokio::task::block_in_place(move || {
let mut img = image::open(&blob_abs).context("image recode failure")?;
let orientation = self.get_exif_orientation(context);
let mut encoded = Vec::new();
let mut changed_name = None;
let mut img = image::open(&blob_abs).context("image recode failure")?;
let orientation = self.get_exif_orientation(context);
let mut encoded = Vec::new();
let mut changed_name = None;
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
let do_scale =
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
if do_scale || do_rotate {
if do_rotate {
img = match orientation {
Ok(90) => img.rotate90(),
Ok(180) => img.rotate180(),
Ok(270) => img.rotate270(),
_ => img,
}
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
max_bytes: Option<usize>,
encoded: &mut Vec<u8>,
) -> anyhow::Result<bool> {
if let Some(max_bytes) = max_bytes {
encode_img(img, encoded)?;
if encoded.len() > max_bytes {
info!(
context,
"image size {}B ({}x{}px) exceeds {}B, need to scale down",
encoded.len(),
img.width(),
img.height(),
max_bytes,
);
return Ok(true);
}
}
Ok(false)
}
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
if do_scale {
if !exceeds_width {
// The image is already smaller than img_wh, but exceeds max_bytes
// We can directly start with trying to scale down to 2/3 of its current width
img_wh = max(img.width(), img.height()) * 2 / 3
}
let do_scale =
exceeds_width || encoded_img_exceeds_bytes(context, &img, max_bytes, &mut encoded)?;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
loop {
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B",
max_bytes.unwrap_or_default()
));
}
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
context,
"Final scaled-down image size: {}B ({}px)",
encoded.len(),
img_wh
);
break;
}
}
if do_scale || do_rotate {
if do_rotate {
img = match orientation {
Ok(90) => img.rotate90(),
Ok(180) => img.rotate180(),
Ok(270) => img.rotate270(),
_ => img,
}
// The file format is JPEG now, we may have to change the file extension
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
blob_abs = blob_abs.with_extension("jpg");
let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
changed_name = Some(format!("$BLOBDIR/{}", file_name));
}
if encoded.is_empty() {
encode_img(&img, &mut encoded)?;
}
std::fs::write(&blob_abs, &encoded)
.context("failed to write recoded blob to file")?;
}
Ok(changed_name)
})
if do_scale {
if !exceeds_width {
// The image is already smaller than img_wh, but exceeds max_bytes
// We can directly start with trying to scale down to 2/3 of its current width
img_wh = max(img.width(), img.height()) * 2 / 3
}
loop {
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(context, &new_img, max_bytes, &mut encoded)? {
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B",
max_bytes.unwrap_or_default()
));
}
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
context,
"Final scaled-down image size: {}B ({}px)",
encoded.len(),
img_wh
);
break;
}
}
}
// The file format is JPEG now, we may have to change the file extension
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
blob_abs = blob_abs.with_extension("jpg");
let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
changed_name = Some(format!("$BLOBDIR/{}", file_name));
}
if encoded.is_empty() {
encode_img(&img, &mut encoded)?;
}
fs::write(&blob_abs, &encoded)
.await
.context("failed to write recoded blob to file")?;
}
Ok(changed_name)
}
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
@@ -472,35 +500,6 @@ impl<'a> fmt::Display for BlobObject<'a> {
}
}
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
max_bytes: Option<usize>,
encoded: &mut Vec<u8>,
) -> anyhow::Result<bool> {
if let Some(max_bytes) = max_bytes {
encode_img(img, encoded)?;
if encoded.len() > max_bytes {
info!(
context,
"image size {}B ({}x{}px) exceeds {}B, need to scale down",
encoded.len(),
img.width(),
img.height(),
max_bytes,
);
return Ok(true);
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use fs::File;
@@ -514,16 +513,7 @@ mod tests {
use super::*;
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
tokio::task::block_in_place(move || {
let img = image::open(path).expect("failed to open image");
assert_eq!(img.width(), width, "invalid width");
assert_eq!(img.height(), height, "invalid height");
img
})
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo", b"hello").await.unwrap();
@@ -534,28 +524,28 @@ mod tests {
assert_eq!(blob.to_abs_path(), t.get_blobdir().join("foo"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_lowercase_ext() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.TXT", b"hello").await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_as_file_name() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
assert_eq!(blob.as_file_name(), "foo.txt");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_as_rel_path() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_suffix() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
@@ -564,16 +554,16 @@ mod tests {
assert_eq!(blob.suffix(), None);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_dup() {
let t = TestContext::new().await;
BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
let foo_path = t.get_blobdir().join("foo.txt");
assert!(foo_path.exists());
assert!(foo_path.exists().await);
BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
while let Ok(Some(dirent)) = dir.next_entry().await {
let fname = dirent.file_name();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else {
@@ -584,20 +574,20 @@ mod tests {
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_double_ext_preserved() {
let t = TestContext::new().await;
BlobObject::create(&t, "foo.tar.gz", b"hello")
.await
.unwrap();
let foo_path = t.get_blobdir().join("foo.tar.gz");
assert!(foo_path.exists());
assert!(foo_path.exists().await);
BlobObject::create(&t, "foo.tar.gz", b"world")
.await
.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
while let Ok(Some(dirent)) = dir.next_entry().await {
let fname = dirent.file_name();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else {
@@ -609,7 +599,7 @@ mod tests {
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_long_names() {
let t = TestContext::new().await;
let s = "1".repeat(150);
@@ -618,7 +608,7 @@ mod tests {
assert!(blobname.len() < 128);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_and_copy() {
let t = TestContext::new().await;
let src = t.dir.path().join("src");
@@ -633,10 +623,10 @@ mod tests {
.await
.is_err());
let whoops = t.get_blobdir().join("whoops");
assert!(!whoops.exists());
assert!(!whoops.exists().await);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_from_path() {
let t = TestContext::new().await;
@@ -656,7 +646,7 @@ mod tests {
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_from_name_long() {
let t = TestContext::new().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
@@ -719,7 +709,7 @@ mod tests {
assert!(!stem.contains('?'));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.jpg");
@@ -731,17 +721,22 @@ mod tests {
.await
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists());
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists());
assert!(tokio::fs::metadata(&avatar_blob).await.unwrap().len() < avatar_bytes.len() as u64);
assert!(avatar_blob.exists().await);
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
check_image_size(avatar_src, 1000, 1000);
check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), 1000);
assert_eq!(img.height(), 1000);
let img = image::open(&avatar_blob).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
async fn file_size(path_buf: &PathBuf) -> u64 {
let file = File::open(path_buf).await.unwrap();
@@ -755,14 +750,12 @@ mod tests {
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
tokio::task::block_in_place(move || {
let img = image::open(&avatar_blob).unwrap();
assert!(img.width() > 130);
assert_eq!(img.width(), img.height());
});
let img = image::open(&avatar_blob).unwrap();
assert!(img.width() > 130);
assert_eq!(img.width(), img.height());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.get_blobdir().join("avatar.png");
@@ -773,7 +766,9 @@ mod tests {
.await
.unwrap();
check_image_size(&avatar_src, 900, 900);
let img = image::open(&avatar_src).unwrap();
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
@@ -784,10 +779,12 @@ mod tests {
avatar_src.with_extension("jpg").to_str().unwrap()
);
check_image_size(avatar_cfg, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
let img = image::open(avatar_cfg).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_selfavatar_copy_without_recode() {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.png");
@@ -799,20 +796,20 @@ mod tests {
.await
.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists());
assert!(!avatar_blob.exists().await);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists());
assert!(avatar_blob.exists().await);
assert_eq!(
tokio::fs::metadata(&avatar_blob).await.unwrap().len(),
std::fs::metadata(&avatar_blob).unwrap().len(),
avatar_bytes.len() as u64
);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_recode_image_1() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
@@ -832,7 +829,7 @@ mod tests {
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_recode_image_2() {
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
@@ -858,7 +855,7 @@ mod tests {
// Do this in parallel to speed up the test a bit
// (it still takes very long though)
let bytes2 = bytes.clone();
let join_handle = tokio::task::spawn(async move {
let join_handle = async_std::task::spawn(async move {
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes2,
@@ -886,10 +883,10 @@ mod tests {
.unwrap();
assert_correct_rotation(&img_rotated);
join_handle.await.unwrap();
join_handle.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_recode_image_3() {
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
@@ -937,10 +934,10 @@ mod tests {
.await?;
let file = alice.get_blobdir().join("file.jpg");
fs::write(&file, &bytes)
.await
.context("failed to write file")?;
check_image_size(&file, original_width, original_height);
File::create(&file).await?.write_all(bytes).await?;
let img = image::open(&file)?;
assert_eq!(img.width(), original_width);
assert_eq!(img.height(), original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
@@ -952,11 +949,9 @@ mod tests {
let alice_msg = alice.get_last_msg().await;
assert_eq!(alice_msg.get_width() as u32, compressed_width);
assert_eq!(alice_msg.get_height() as u32, compressed_height);
check_image_size(
alice_msg.get_file(&alice).unwrap(),
compressed_width,
compressed_height,
);
let img = image::open(alice_msg.get_file(&alice).unwrap())?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_width() as u32, compressed_width);
@@ -966,11 +961,13 @@ mod tests {
let blob = BlobObject::new_from_path(&bob, &file).await?;
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
let img = check_image_size(file, compressed_width, compressed_height);
let img = image::open(file)?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
Ok(img)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_increation_in_blobdir() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
@@ -989,7 +986,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_increation_not_blobdir() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;

View File

@@ -2,11 +2,11 @@
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
use async_std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
@@ -3464,10 +3464,10 @@ mod tests {
use crate::contact::Contact;
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use async_std::fs::File;
use async_std::prelude::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_chat_info() {
let t = TestContext::new().await;
let chat = t.create_chat_with_contact("bob", "bob@example.com").await;
@@ -3498,7 +3498,7 @@ mod tests {
assert_eq!(info, loaded);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_draft_no_draft() {
let t = TestContext::new().await;
let chat = t.get_self_chat().await;
@@ -3506,14 +3506,14 @@ mod tests {
assert!(draft.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_draft_special_chat_id() {
let t = TestContext::new().await;
let draft = DC_CHAT_ID_LAST_SPECIAL.get_draft(&t).await.unwrap();
assert!(draft.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_draft_no_chat() {
// This is a weird case, maybe this should be an error but we
// do not get this info from the database currently.
@@ -3522,7 +3522,7 @@ mod tests {
assert!(draft.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_draft() {
let t = TestContext::new().await;
let chat_id = &t.get_self_chat().await.id;
@@ -3536,7 +3536,7 @@ mod tests {
assert_eq!(msg_text, draft_text);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_delete_draft() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "abc").await?;
@@ -3557,7 +3557,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_forwarding_draft_failing() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
@@ -3571,7 +3571,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_draft_stable_ids() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = &t.get_self_chat().await.id;
@@ -3616,7 +3616,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_change_quotes_on_reused_message_object() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
@@ -3668,7 +3668,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_add_contact_to_chat_ex_add_self() {
// Adding self to a contact should succeed, even though it's pointless.
let t = TestContext::new_alice().await;
@@ -3681,7 +3681,7 @@ mod tests {
assert_eq!(added, false);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_modify_chat_multi_device() -> Result<()> {
let a1 = TestContext::new_alice().await;
let a2 = TestContext::new_alice().await;
@@ -3756,7 +3756,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_modify_chat_disordered() -> Result<()> {
// Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy
// (sleep() is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then)
@@ -3770,47 +3770,47 @@ mod tests {
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
let add1 = alice.pop_sent_msg().await;
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
let add2 = alice.pop_sent_msg().await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
let add3 = alice.pop_sent_msg().await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
let remove1 = alice.pop_sent_msg().await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
let remove2 = alice.pop_sent_msg().await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
// Bob receives the add and deletion messages out of order
let bob = TestContext::new_bob().await;
bob.recv_msg(&add1).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
bob.recv_msg(&add3).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
let bob_chat_id = bob.recv_msg(&add2).await.chat_id;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 4);
bob.recv_msg(&remove2).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
bob.recv_msg(&remove1).await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
@@ -3818,7 +3818,7 @@ mod tests {
}
/// Test that group updates are robust to lost messages and eventual out of order arrival.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_modify_chat_lost() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -3833,11 +3833,11 @@ mod tests {
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let add = alice.pop_sent_msg().await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
let remove1 = alice.pop_sent_msg().await;
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
async_std::task::sleep(std::time::Duration::from_millis(1100)).await;
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
let remove2 = alice.pop_sent_msg().await;
@@ -3860,7 +3860,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_leave_group() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -3889,7 +3889,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_add_remove_contact_for_single() {
let ctx = TestContext::new_alice().await;
let bob = Contact::create(&ctx, "", "bob@f.br").await.unwrap();
@@ -3913,7 +3913,7 @@ mod tests {
assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_self_talk() -> Result<()> {
let t = TestContext::new_alice().await;
let chat = &t.get_self_chat().await;
@@ -3944,7 +3944,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_add_device_msg_unlabelled() {
let t = TestContext::new().await;
@@ -3979,7 +3979,7 @@ mod tests {
assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_add_device_msg_labelled() -> Result<()> {
let t = TestContext::new().await;
@@ -4029,7 +4029,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_add_device_msg_label_only() {
let t = TestContext::new().await;
let res = add_device_msg(&t, Some(""), None).await;
@@ -4049,7 +4049,7 @@ mod tests {
assert!(!msg_id.as_ref().unwrap().is_unset());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_was_device_msg_ever_added() {
let t = TestContext::new().await;
add_device_msg(&t, Some("some-label"), None).await.ok();
@@ -4069,7 +4069,7 @@ mod tests {
assert!(was_device_msg_ever_added(&t, "").await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_delete_device_chat() {
let t = TestContext::new().await;
@@ -4089,7 +4089,7 @@ mod tests {
assert_eq!(chatlist_len(&t, 0).await, 0)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_device_chat_cannot_sent() {
let t = TestContext::new().await;
t.update_device_chats().await.unwrap();
@@ -4106,7 +4106,7 @@ mod tests {
assert!(forward_msgs(&t, &[msg_id], device_chat_id).await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_delete_and_reset_all_device_msgs() {
let t = TestContext::new().await;
let mut msg = Message::new(Viewtype::Text);
@@ -4138,7 +4138,7 @@ mod tests {
.len()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_archive() {
// create two chats
let t = TestContext::new().await;
@@ -4241,7 +4241,7 @@ mod tests {
assert_eq!(chatlist_len(&t, DC_GCL_ARCHIVED_ONLY).await, 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_unarchive_if_muted() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -4335,7 +4335,7 @@ mod tests {
result
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_pinned() {
let t = TestContext::new().await;
@@ -4347,9 +4347,9 @@ mod tests {
.await
.unwrap()
.chat_id;
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
let chat_id2 = t.get_self_chat().await.id;
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
@@ -4392,7 +4392,7 @@ mod tests {
assert_eq!(chatlist, vec![chat_id3, chat_id2, chat_id1]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_set_chat_name() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
@@ -4410,7 +4410,7 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_same_chat_twice() {
let context = TestContext::new().await;
let contact1 = Contact::create(&context.ctx, "bob", "bob@mail.de")
@@ -4433,7 +4433,7 @@ mod tests {
assert_eq!(chat2.name, chat.name);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_shall_attach_selfavatar() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
@@ -4451,7 +4451,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_set_mute_duration() {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
@@ -4502,7 +4502,7 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_add_info_msg() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
@@ -4519,7 +4519,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_add_info_msg_with_cmd() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
@@ -4549,7 +4549,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_set_protection() {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
@@ -4617,7 +4617,7 @@ mod tests {
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_lookup_by_contact_id() {
let ctx = TestContext::new_alice().await;
@@ -4660,7 +4660,7 @@ mod tests {
assert!(found.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_lookup_self_by_contact_id() {
let ctx = TestContext::new_alice().await;
@@ -4679,7 +4679,7 @@ mod tests {
assert_eq!(chat.blocked, Blocked::Not);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_group_with_removed_message_id() -> Result<()> {
// Alice creates a group with Bob, sends a message to bob
let alice = TestContext::new_alice().await;
@@ -4733,7 +4733,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_marknoticed_chat() -> Result<()> {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
@@ -4778,7 +4778,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_contact_request_fresh_messages() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -4828,7 +4828,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_contact_request_archive() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -4868,7 +4868,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_classic_email_chat() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -4913,7 +4913,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_chat_get_color() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
@@ -4956,7 +4956,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_sticker_png() -> Result<()> {
test_sticker(
"sticker.png",
@@ -4967,7 +4967,7 @@ mod tests {
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_sticker_jpeg() -> Result<()> {
test_sticker(
"sticker.jpg",
@@ -4978,7 +4978,7 @@ mod tests {
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_sticker_gif() -> Result<()> {
test_sticker(
"sticker.gif",
@@ -4989,7 +4989,7 @@ mod tests {
.await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_sticker_forward() -> Result<()> {
// create chats
let alice = TestContext::new_alice().await;
@@ -5020,7 +5020,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_forward() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -5041,7 +5041,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_forward_info_msg() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -5067,7 +5067,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_forward_quote() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -5102,7 +5102,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_forward_group() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -5152,7 +5152,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_only_minimal_data_are_forwarded() -> Result<()> {
// send a message from Alice to a group with Bob
let alice = TestContext::new_alice().await;
@@ -5194,7 +5194,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_resend_own_message() -> Result<()> {
// Alice creates group with Bob and sends an initial message
let alice = TestContext::new_alice().await;
@@ -5247,7 +5247,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_resend_foreign_message_fails() -> Result<()> {
let alice = TestContext::new_alice().await;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
@@ -5266,7 +5266,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_resend_opportunistically_encryption() -> Result<()> {
// Alice creates group with Bob and sends an initial message
let alice = TestContext::new_alice().await;
@@ -5303,7 +5303,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_resend_info_message_fails() -> Result<()> {
let alice = TestContext::new_alice().await;
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
@@ -5327,7 +5327,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_can_send_group() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = Contact::create(&alice, "", "bob@f.br").await?;
@@ -5353,7 +5353,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_broadcast() -> Result<()> {
// create two context, send two messages so both know the other
let alice = TestContext::new_alice().await;
@@ -5396,7 +5396,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_for_contact_with_blocked() -> Result<()> {
let t = TestContext::new().await;
let (contact_id, _) =
@@ -5430,7 +5430,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_chat_get_encryption_info() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;

View File

@@ -376,7 +376,7 @@ mod tests {
use crate::stock_str::StockMessage;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_try_load() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
@@ -432,7 +432,7 @@ mod tests {
assert_eq!(chats.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new().await;
t.update_device_chats().await.unwrap();
@@ -457,7 +457,7 @@ mod tests {
.is_self_talk());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_search_special_chat_names() {
let t = TestContext::new().await;
t.update_device_chats().await.unwrap();
@@ -488,7 +488,7 @@ mod tests {
assert_eq!(chats.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_search_single_chat() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
@@ -548,7 +548,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
@@ -610,7 +610,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")

View File

@@ -454,7 +454,7 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
@@ -471,7 +471,7 @@ mod tests {
assert_eq!(media_quality, constants::MediaQuality::Worse);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_ui_config() -> Result<()> {
let t = TestContext::new().await;
@@ -493,7 +493,7 @@ mod tests {
}
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_set_config_bool() -> Result<()> {
let t = TestContext::new().await;
@@ -505,7 +505,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_self_addrs() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -558,7 +558,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_change_primary_self_addr() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;

View File

@@ -6,10 +6,9 @@ mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use futures::FutureExt;
use futures_lite::FutureExt as _;
use async_std::prelude::*;
use async_std::task;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use tokio::task;
use crate::config::Config;
use crate::context::Context;
@@ -56,6 +55,8 @@ impl Context {
/// Configures this account with the currently set parameters.
pub async fn configure(&self) -> Result<()> {
use futures::future::FutureExt;
ensure!(
self.scheduler.read().await.is_none(),
"cannot configure, already running"
@@ -403,7 +404,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 850);
// Wait for SMTP configuration
match smtp_config_task.await.unwrap() {
match smtp_config_task.await {
Ok(smtp_param) => {
param.smtp = smtp_param;
}
@@ -446,7 +447,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
ctx.interrupt_inbox(InterruptInfo::new(false)).await;
progress!(ctx, 940);
update_device_chats_handle.await??;
update_device_chats_handle.await?;
ctx.sql.set_raw_config_bool("configured", true).await?;
@@ -548,7 +549,7 @@ async fn try_imap_one_param(
);
info!(context, "Trying: {}", inf);
let (_s, r) = async_channel::bounded(1);
let (_s, r) = async_std::channel::bounded(1);
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r).await
{
@@ -633,13 +634,10 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
return "no error".to_string();
};
if errors.iter().all(|e| {
e.msg.to_lowercase().contains("could not resolve")
|| e.msg
.to_lowercase()
.contains("temporary failure in name resolution")
|| e.msg.to_lowercase().contains("name or service not known")
}) {
if errors
.iter()
.all(|e| e.msg.to_lowercase().contains("could not resolve"))
{
return stock_str::error_no_network(context).await;
}
@@ -680,7 +678,7 @@ mod tests {
use crate::config::Config;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_no_panic_on_bad_credentials() {
let t = TestContext::new().await;
t.set_config(Config::Addr, Some("probably@unexistant.addr"))

View File

@@ -1,7 +1,8 @@
use anyhow::{anyhow, format_err};
use crate::context::Context;
use anyhow::format_err;
use anyhow::Context as _;
pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
match read_url_inner(context, url).await {
Ok(s) => {
@@ -15,27 +16,24 @@ pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
}
}
pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result<String> {
let client = reqwest::Client::new();
let mut url = url.to_string();
pub async fn read_url_inner(context: &Context, mut url: &str) -> anyhow::Result<String> {
let mut _temp; // For the borrow checker
// Follow up to 10 http-redirects
for _i in 0..10 {
let response = client.get(&url).send().await?;
let mut response = surf::get(url).send().await.map_err(|e| e.into_inner())?;
if response.status().is_redirection() {
let headers = response.headers();
let header = headers
.get_all("location")
.iter()
_temp = response
.header("location")
.context("Redirection doesn't have a target location")?
.last()
.ok_or_else(|| anyhow!("Redirection doesn't have a target location"))?
.to_str()?;
info!(context, "Following redirect to {}", header);
url = header.to_string();
.to_string();
info!(context, "Following redirect to {}", _temp);
url = &_temp;
continue;
}
return response.text().await.map_err(Into::into);
return response.body_string().await.map_err(|e| e.into_inner());
}
Err(format_err!("Followed 10 redirections"))

View File

@@ -2,9 +2,9 @@
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::PathBuf;
use anyhow::{bail, ensure, Context as _, Result};
use async_std::path::PathBuf;
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use regex::Regex;
@@ -1438,8 +1438,8 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
#[cfg(test)]
mod tests {
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use async_std::fs::File;
use async_std::io::WriteExt;
use super::*;
@@ -1508,7 +1508,7 @@ mod tests {
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_contacts() -> Result<()> {
let context = TestContext::new().await;
@@ -1572,7 +1572,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_is_self_addr() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.is_self_addr("me@me.org").await?, false);
@@ -1584,7 +1584,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
let t = TestContext::new().await;
@@ -1685,7 +1685,7 @@ mod tests {
assert!(!contact.is_blocked());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_contact_name_changes() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -1797,7 +1797,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_delete() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -1825,7 +1825,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_remote_authnames() {
let t = TestContext::new().await;
@@ -1876,7 +1876,7 @@ mod tests {
assert_eq!(contact.get_display_name(), "bob3");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_remote_authnames_create_empty() {
let t = TestContext::new().await;
@@ -1925,7 +1925,7 @@ mod tests {
///
/// In the past, "Not Bob" name was stuck until "Bob" changed the name to "Not Bob" and back in
/// the "From:" field or user set the name to empty string manually.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_remote_authnames_update_to() -> Result<()> {
let t = TestContext::new().await;
@@ -1958,7 +1958,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_remote_authnames_edit_empty() {
let t = TestContext::new().await;
@@ -1995,7 +1995,7 @@ mod tests {
assert!(addr_cmp(" mailto:AA@AA.ORG", "Aa@Aa.orG"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_name_in_address() {
let t = TestContext::new().await;
@@ -2034,7 +2034,7 @@ mod tests {
.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_lookup_id_by_addr() {
let t = TestContext::new().await;
@@ -2059,7 +2059,7 @@ mod tests {
assert_eq!(id, Some(ContactId::SELF));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_contact_get_color() -> Result<()> {
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
@@ -2078,7 +2078,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_contact_get_encrinfo() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -2123,7 +2123,7 @@ CCCB 5AA9 F6E1 141C 9431
/// Tests that status is synchronized when sending encrypted BCC-self messages and not
/// synchronized when the message is not encrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_synchronize_status() -> Result<()> {
// Alice has two devices.
let alice1 = TestContext::new_alice().await;
@@ -2188,7 +2188,7 @@ CCCB 5AA9 F6E1 141C 9431
}
/// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_selfavatar_changed_event() -> Result<()> {
// Alice has two devices.
let alice1 = TestContext::new_alice().await;
@@ -2247,7 +2247,7 @@ CCCB 5AA9 F6E1 141C 9431
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_last_seen() -> Result<()> {
let alice = TestContext::new_alice().await;

View File

@@ -3,13 +3,14 @@
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use anyhow::{ensure, Result};
use async_channel::{self as channel, Receiver, Sender};
use tokio::sync::{Mutex, RwLock};
use async_std::{
channel::{self, Receiver, Sender},
path::{Path, PathBuf},
sync::{Arc, Mutex, RwLock},
};
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
@@ -74,7 +75,7 @@ pub struct InnerContext {
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: std::sync::RwLock<String>,
pub(crate) last_error: RwLock<String>,
}
/// The state of ongoing process.
@@ -114,7 +115,7 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
impl Context {
/// Creates new context and opens the database.
pub async fn new(dbfile: &Path, id: u32, events: Events) -> Result<Context> {
pub async fn new(dbfile: PathBuf, id: u32, events: Events) -> Result<Context> {
let context = Self::new_closed(dbfile, id, events).await?;
// Open the database if is not encrypted.
@@ -125,15 +126,15 @@ impl Context {
}
/// Creates new context without opening the database.
pub async fn new_closed(dbfile: &Path, id: u32, events: Events) -> Result<Context> {
pub async fn new_closed(dbfile: PathBuf, id: u32, events: Events) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
let blobdir = dbfile.with_file_name(blob_fname);
if !blobdir.exists() {
tokio::fs::create_dir_all(&blobdir).await?;
if !blobdir.exists().await {
async_std::fs::create_dir_all(&blobdir).await?;
}
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events).await?;
let context = Context::with_blobdir(dbfile, blobdir, id, events).await?;
Ok(context)
}
@@ -171,7 +172,7 @@ impl Context {
events: Events,
) -> Result<Context> {
ensure!(
blobdir.is_dir(),
blobdir.is_dir().await,
"Blobdir does not exist: {}",
blobdir.display()
);
@@ -192,7 +193,7 @@ impl Context {
quota: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()),
last_error: RwLock::new("".to_string()),
};
let ctx = Context {
@@ -642,14 +643,14 @@ impl Context {
Ok(mvbox.as_deref() == Some(folder_name))
}
pub(crate) fn derive_blobdir(dbfile: &Path) -> PathBuf {
pub(crate) fn derive_blobdir(dbfile: &PathBuf) -> PathBuf {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
dbfile.with_file_name(blob_fname)
}
pub(crate) fn derive_walfile(dbfile: &Path) -> PathBuf {
pub(crate) fn derive_walfile(dbfile: &PathBuf) -> PathBuf {
let mut wal_fname = OsString::new();
wal_fname.push(dbfile.file_name().unwrap_or_default());
wal_fname.push("-wal");
@@ -678,19 +679,19 @@ mod tests {
use strum::IntoEnumIterator;
use tempfile::tempdir;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_wrong_db() -> Result<()> {
let tmp = tempfile::tempdir()?;
let dbfile = tmp.path().join("db.sqlite");
tokio::fs::write(&dbfile, b"123").await?;
let res = Context::new(&dbfile, 1, Events::new()).await?;
std::fs::write(&dbfile, b"123")?;
let res = Context::new(dbfile.into(), 1, Events::new()).await?;
// Broken database is indistinguishable from encrypted one.
assert_eq!(res.is_open().await, false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_fresh_msgs() {
let t = TestContext::new().await;
let fresh = t.get_fresh_msgs().await.unwrap();
@@ -717,7 +718,7 @@ mod tests {
dc_receive_imf(t, msg.as_bytes(), false).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_fresh_msgs_and_muted_chats() {
// receive various mails in 3 chats
let t = TestContext::new_alice().await;
@@ -767,7 +768,7 @@ mod tests {
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 9); // claire is counted again
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_fresh_msgs_and_muted_until() {
let t = TestContext::new_alice().await;
let bob = t.create_chat_with_contact("", "bob@g.it").await;
@@ -825,61 +826,61 @@ mod tests {
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(&dbfile, 1, Events::new()).await.unwrap();
Context::new(dbfile.into(), 1, Events::new()).await.unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_wrong_blogdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
tokio::fs::write(&blobdir, b"123").await.unwrap();
let res = Context::new(&dbfile, 1, Events::new()).await;
std::fs::write(&blobdir, b"123").unwrap();
let res = Context::new(dbfile.into(), 1, Events::new()).await;
assert!(res.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_sqlite_parent_not_exists() {
let tmp = tempfile::tempdir().unwrap();
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(&dbfile, 1, Events::new()).await.unwrap();
Context::new(dbfile.into(), 1, Events::new()).await.unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_with_empty_blobdir() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new()).await;
let res = Context::with_blobdir(dbfile.into(), blobdir, 1, Events::new()).await;
assert!(res.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_with_blobdir_not_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new()).await;
let res = Context::with_blobdir(dbfile.into(), blobdir.into(), 1, Events::new()).await;
assert!(res.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn no_crashes_on_context_deref() {
let t = TestContext::new().await;
std::mem::drop(t);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_info() {
let t = TestContext::new().await;
@@ -895,7 +896,7 @@ mod tests {
assert_eq!(info.get("level").unwrap(), "awesome");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_info_completeness() {
// For easier debugging,
// get_info() shall return all important information configurable by the Config-values.
@@ -943,7 +944,7 @@ mod tests {
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let self_talk = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
@@ -999,7 +1000,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_limit_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat = alice
@@ -1032,13 +1033,13 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_check_passphrase() -> Result<()> {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new())
let context = Context::new_closed(dbfile.clone().into(), id, Events::new())
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
@@ -1046,7 +1047,7 @@ mod tests {
drop(context);
let id = 2;
let context = Context::new(&dbfile, id, Events::new())
let context = Context::new(dbfile.into(), id, Events::new())
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);
@@ -1057,7 +1058,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_ongoing() -> Result<()> {
let context = TestContext::new().await;

View File

@@ -280,9 +280,7 @@ pub(crate) async fn dc_receive_imf_inner(
// Always update the status, even if there is no footer, to allow removing the status.
//
// Ignore MDNs though, as they never contain the signature even if user has set it.
// Ignore footers from mailinglists as they are often created or modified by the mailinglist software.
if mime_parser.mdn_reports.is_empty()
&& !mime_parser.is_mailinglist_message()
&& is_partial_download.is_none()
&& from_id != ContactId::UNDEFINED
&& context
@@ -1741,14 +1739,13 @@ async fn create_or_lookup_mailinglist(
}
}
// additional names in square brackets in the subject are preferred
// if we have an additional name square brackets in the subject, we prefer that
// (as that part is much more visible, we assume, that names is shorter and comes more to the point,
// than the sometimes longer part from ListId)
let subject = mime_parser.get_subject().unwrap_or_default();
static SUBJECT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
static SUBJECT: Lazy<Regex> = Lazy::new(|| Regex::new(r"^.{0,5}\[(.*.)\]").unwrap());
if let Some(cap) = SUBJECT.captures(&subject) {
name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
name = cap[1].to_string();
}
// if we do not have a name yet and `From` indicates, that this is a notification list,
@@ -2206,8 +2203,8 @@ async fn add_or_lookup_contact_by_addr(
#[cfg(test)]
mod tests {
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;
use async_std::fs::{self, File};
use async_std::io::WriteExt;
use super::*;
@@ -2219,7 +2216,7 @@ mod tests {
use crate::message::Message;
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_grpid_simple() {
let context = TestContext::new().await;
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
@@ -2237,7 +2234,7 @@ mod tests {
assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_grpid_from_multiple() {
let context = TestContext::new().await;
let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
@@ -2286,7 +2283,7 @@ mod tests {
\n\
hello\n";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_adhoc_group_show_chats_only() {
let t = TestContext::new_alice().await;
assert_eq!(t.get_config_int(Config::ShowEmails).await.unwrap(), 0);
@@ -2309,7 +2306,7 @@ mod tests {
assert_eq!(chats.len(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_adhoc_group_show_accepted_contact_unknown() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
@@ -2320,7 +2317,7 @@ mod tests {
assert_eq!(chats.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_adhoc_group_show_accepted_contact_known() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
@@ -2333,7 +2330,7 @@ mod tests {
assert_eq!(chats.len(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_adhoc_group_show_accepted_contact_accepted() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("1")).await.unwrap();
@@ -2371,7 +2368,7 @@ mod tests {
assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_adhoc_group_show_all() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -2390,7 +2387,7 @@ mod tests {
assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_read_receipt_and_unarchive() -> Result<()> {
// create alice's account
let t = TestContext::new_alice().await;
@@ -2504,7 +2501,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_no_from() {
// if there is no from given, from_id stays 0 which is just fine. These messages
// are very rare, however, we have to add them to the database
@@ -2536,7 +2533,7 @@ mod tests {
assert!(chats.get_msg_id(0).is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_escaped_from() {
let t = TestContext::new_alice().await;
let contact_id = Contact::create(&t, "foobar", "foobar@example.com")
@@ -2569,7 +2566,7 @@ mod tests {
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_escaped_recipients() {
let t = TestContext::new_alice().await;
Contact::create(&t, "foobar", "foobar@example.com")
@@ -2611,7 +2608,7 @@ mod tests {
assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_cc_to_contact() {
let t = TestContext::new_alice().await;
Contact::create(&t, "foobar", "foobar@example.com")
@@ -2646,7 +2643,7 @@ mod tests {
assert_eq!(contact.get_display_name(), "Carl");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_ndn_tiscali() {
test_parse_ndn(
"alice@tiscali.it",
@@ -2658,7 +2655,7 @@ mod tests {
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_ndn_testrun() {
test_parse_ndn(
"alice@testrun.org",
@@ -2670,7 +2667,7 @@ mod tests {
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_ndn_yahoo() {
test_parse_ndn(
"alice@yahoo.com",
@@ -2682,7 +2679,7 @@ mod tests {
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_ndn_gmail() {
test_parse_ndn(
"alice@gmail.com",
@@ -2694,7 +2691,7 @@ mod tests {
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_ndn_gmx() {
test_parse_ndn(
"alice@gmx.com",
@@ -2706,7 +2703,7 @@ mod tests {
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_ndn_posteo() {
test_parse_ndn(
"alice@posteo.org",
@@ -2718,7 +2715,7 @@ mod tests {
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_ndn_testrun_2() {
test_parse_ndn(
"alice@example.org",
@@ -2784,7 +2781,7 @@ mod tests {
assert_eq!(msg.error(), error_msg.map(|error| error.to_string()));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_ndn_group_msg() -> Result<()> {
let t = TestContext::new().await;
t.configure_addr("alice@gmail.com").await;
@@ -2844,7 +2841,7 @@ mod tests {
Message::load_from_db(context, msg_id).await.unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_html_only_mail() {
let t = TestContext::new_alice().await;
let msg = load_imf_email(&t, include_bytes!("../test-data/message/wrong-html.eml")).await;
@@ -2877,7 +2874,7 @@ mod tests {
\n\
hello back\n";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_github_mailing_list() -> Result<()> {
let t = TestContext::new_alice().await;
t.ctx.set_config(Config::ShowEmails, Some("2")).await?;
@@ -2944,7 +2941,7 @@ mod tests {
\n\
body 4\n";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_classic_mailing_list() -> Result<()> {
let t = TestContext::new_alice().await;
t.ctx
@@ -2990,7 +2987,7 @@ Hello mailinglist!\r\n"
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_other_device_writes_to_mailinglist() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
@@ -3040,7 +3037,7 @@ Hello mailinglist!\r\n"
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_block_mailing_list() {
let t = TestContext::new_alice().await;
t.ctx
@@ -3074,7 +3071,7 @@ Hello mailinglist!\r\n"
assert_eq!(msgs.len(), 2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_mailing_list_decide_block_then_unblock() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3107,7 +3104,7 @@ Hello mailinglist!\r\n"
assert_eq!(msgs.len(), 2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_mailing_list_decide_not_now() {
let t = TestContext::new_alice().await;
t.ctx
@@ -3140,7 +3137,7 @@ Hello mailinglist!\r\n"
assert!(chat.is_contact_request());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_mailing_list_decide_accept() {
let t = TestContext::new_alice().await;
t.ctx
@@ -3168,32 +3165,7 @@ Hello mailinglist!\r\n"
assert!(chat.can_send(&t.ctx).await.unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mailing_list_multiple_names_in_subject() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
dc_receive_imf(
&t,
b"From: Foo Bar <foo@bar.org>\n\
To: deltachat/deltachat-core-rust <deltachat-core-rust@noreply.github.com>\n\
Subject: [ola list] [foo][bar] just a subject\n\
Message-ID: <3333@example.org>\n\
List-ID: \"looong description of 'ola list', with foo, bar\" <delta.codespeak.net>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
\n\
hello\n",
false,
)
.await
.unwrap();
let msg = t.get_last_msg().await;
let chat_id = msg.get_chat_id();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.name, "ola list [foo][bar]");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_majordomo_mailing_list() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3242,7 +3214,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_mailchimp_mailing_list() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3272,7 +3244,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.name, "Atlas Obscura");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_dhl_mailing_list() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3297,7 +3269,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.name, "DHL Paket");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_dpd_mailing_list() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3322,7 +3294,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.name, "DPD");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_xt_local_mailing_list() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
@@ -3352,7 +3324,7 @@ Hello mailinglist!\r\n"
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_xing_mailing_list() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
@@ -3373,7 +3345,7 @@ Hello mailinglist!\r\n"
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_ttline_mailing_list() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
@@ -3394,7 +3366,7 @@ Hello mailinglist!\r\n"
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_mailing_list_with_mimepart_footer() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3425,7 +3397,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.name, "Intern");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_mailing_list_with_mimepart_footer_signed() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3450,7 +3422,7 @@ Hello mailinglist!\r\n"
/// Test that the changes from apply_mailinglist_changes() are also applied
/// if the message is assigned to the chat by In-Reply-To
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_apply_mailinglist_changes_assigned_by_reply() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3492,7 +3464,7 @@ Hello mailinglist!\r\n"
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_dont_show_tokens_in_contacts_list() {
check_dont_show_in_contacts_list(
"reply+OGHVYCLVBEGATYBICAXBIRQATABUOTUCERABERAHNO@reply.github.com",
@@ -3500,7 +3472,7 @@ Hello mailinglist!\r\n"
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_dont_show_noreply_in_contacts_list() {
check_dont_show_in_contacts_list("noreply@github.com").await;
}
@@ -3538,7 +3510,7 @@ YEAAAAAA!.
assert!(contacts.is_empty()); // The contact should not have been added to the db
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_pdf_filename_simple() {
let t = TestContext::new_alice().await;
let msg = load_imf_email(
@@ -3551,7 +3523,7 @@ YEAAAAAA!.
assert_eq!(msg.param.get(Param::File).unwrap(), "$BLOBDIR/simple.pdf");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_pdf_filename_continuation() {
// test filenames split across multiple header lines, see rfc 2231
let t = TestContext::new_alice().await;
@@ -3578,7 +3550,7 @@ YEAAAAAA!.
/// or mua may use multipart/related not correctly -
/// so this test is in competition with parse_thunderbird_html_embedded_image()
/// that wants the image to be kept in the chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_many_images() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -3599,7 +3571,7 @@ YEAAAAAA!.
/// Test that classical MUA messages are assigned to group chats based on the `In-Reply-To`
/// header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_in_reply_to() {
let t = TestContext::new().await;
t.configure_addr("bob@example.com").await;
@@ -3664,7 +3636,7 @@ YEAAAAAA!.
/// Test that classical MUA messages are assigned to group chats
/// based on the `In-Reply-To` header for two-member groups.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_in_reply_to_two_member_group() {
let t = TestContext::new().await;
t.configure_addr("bob@example.com").await;
@@ -3766,7 +3738,7 @@ YEAAAAAA!.
assert_eq!(msg.get_text().unwrap(), "private reply");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_save_mime_headers_off() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -3781,7 +3753,7 @@ YEAAAAAA!.
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_save_mime_headers_on() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config_bool(Config::SaveMimeHeaders, true).await?;
@@ -3952,7 +3924,7 @@ YEAAAAAA!.
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_alias_support_answer_from_nondc() {
// Bob, the other supporter, answers with a classic MUA.
let bob_answer = b"To: support@example.org, claire@example.org\n\
@@ -3972,7 +3944,7 @@ YEAAAAAA!.
check_alias_reply(bob_answer, false, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_alias_answer_from_dc() {
// Bob, the other supporter, answers with Delta Chat.
let bob_answer = b"To: support@example.org, claire@example.org\n\
@@ -3996,7 +3968,7 @@ YEAAAAAA!.
check_alias_reply(bob_answer, false, false).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_dont_assign_to_trash_by_parent() {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await.unwrap();
@@ -4044,7 +4016,7 @@ YEAAAAAA!.
assert_eq!(msg.text.unwrap(), "Reply");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_dont_show_all_outgoing_msgs_in_self_chat() {
// Regression test for <https://github.com/deltachat/deltachat-android/issues/1940>:
// Some servers add a `Bcc: <Self>` header, which caused all outgoing messages to
@@ -4071,7 +4043,7 @@ Message content",
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_outgoing_classic_mail_creates_chat() {
let alice = TestContext::new_alice().await;
@@ -4101,7 +4073,7 @@ Message content",
assert_eq!(msg.get_text().unwrap(), "Subj Message content");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_duplicate_message() -> Result<()> {
// Test that duplicate messages are ignored based on the Message-ID
let alice = TestContext::new_alice().await;
@@ -4160,80 +4132,7 @@ Second signature";
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ignore_footer_status_from_mailinglist() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
let bob_id = Contact::add_or_lookup(&t, "", "bob@example.net", Origin::IncomingUnknownCc)
.await?
.0;
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "");
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
dc_receive_imf(
&t,
b"From: Bob <bob@example.net>
To: Alice <alice@example.org>
Message-ID: <1@example.org>
Subject: first message
body 1
--
Original signature",
false,
)
.await?;
let one2one_chat_id = t.get_last_msg().await.chat_id;
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "Original signature");
dc_receive_imf(
&t,
b"From: Bob <bob@example.net>
Sender: ml@example.net
To: Alice <alice@example.org>
Message-ID: <2@example.net>
Precedence: list
Subject: second message
body 2
--
The modified signature
--
Tap here to unsubscribe ...",
false,
)
.await?;
let ml_chat_id = t.get_last_msg().await.chat_id;
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "Original signature");
dc_receive_imf(
&t,
b"From: Bob <bob@example.net>
To: Alice <alice@example.org>
Message-ID: <3@example.org>
Subject: third message
body 3
--
Original signature updated",
false,
)
.await?;
let bob = Contact::load_from_db(&t, bob_id).await?;
assert_eq!(bob.get_status(), "Original signature updated");
assert_eq!(get_chat_msgs(&t, one2one_chat_id, 0).await?.len(), 2);
assert_eq!(get_chat_msgs(&t, ml_chat_id, 0).await?.len(), 1);
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 2);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_chat_assignment_private_classical_reply() {
for outgoing_is_classical in &[true, false] {
let t = TestContext::new_alice().await;
@@ -4317,7 +4216,7 @@ Private reply"#,
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_chat_assignment_private_chat_reply() {
for (outgoing_is_classical, outgoing_has_multiple_recipients) in
&[(true, true), (false, true), (false, false)]
@@ -4413,7 +4312,7 @@ Sent with my Delta Chat Messenger: https://delta.chat
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_chat_assignment_nonprivate_classical_reply() {
for outgoing_is_classical in &[true, false] {
let t = TestContext::new_alice().await;
@@ -4522,7 +4421,7 @@ Reply to all"#,
/// messages have the same recipient lists and only differ in the subject and message contents.
/// The messages can be properly assigned to chats only using the In-Reply-To or References
/// headers.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_chat_assignment_adhoc() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_alice().await;
@@ -4602,7 +4501,7 @@ Second thread."#;
}
/// Test that read receipts don't create chats.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_read_receipts_dont_create_chats() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -4635,7 +4534,7 @@ Second thread."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_gmx_forwarded_msg() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
@@ -4655,7 +4554,7 @@ Second thread."#;
}
/// Tests that user is notified about new incoming contact requests.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_incoming_contact_request() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -4680,7 +4579,7 @@ Second thread."#;
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_parent_message() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
@@ -4743,7 +4642,7 @@ Message with references."#;
}
/// Test a message with RFC 1847 encapsulation as created by Thunderbird.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_rfc1847_encapsulation() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -4769,7 +4668,7 @@ Message with references."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_invalid_to_address() -> Result<()> {
let alice = TestContext::new_alice().await;
@@ -4781,7 +4680,7 @@ Message with references."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_reply_from_different_addr() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;
@@ -4846,7 +4745,7 @@ Reply from different address
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_long_filenames() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;
@@ -4901,7 +4800,7 @@ Reply from different address
}
/// Tests that contact request is accepted automatically on outgoing message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_accept_outgoing() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice1 = tcm.alice().await;
@@ -4946,7 +4845,7 @@ Reply from different address
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_outgoing_private_reply_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice1 = tcm.alice().await;
@@ -5023,7 +4922,7 @@ Reply from different address
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_no_private_reply_to_blocked_account() -> Result<()> {
let mut tcm = TestContextManager::new().await;
let alice = tcm.alice().await;

View File

@@ -5,19 +5,20 @@ use core::cmp::{max, min};
use std::borrow::Cow;
use std::fmt;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::str::from_utf8;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use anyhow::{bail, Error, Result};
use chrono::{Local, TimeZone};
use futures::StreamExt;
use mailparse::dateparse;
use mailparse::headers::Headers;
use mailparse::MailHeaderMap;
use rand::{thread_rng, Rng};
use tokio::{fs, io};
use crate::chat::{add_device_msg, add_device_msg_with_importance};
use crate::constants::{DC_ELLIPSIS, DC_OUTDATED_WARNING_DAYS};
@@ -277,7 +278,7 @@ pub fn dc_get_filemeta(buf: &[u8]) -> Result<(u32, u32), Error> {
///
/// If `path` starts with "$BLOBDIR", replaces it with the blobdir path.
/// Otherwise, returns path as is.
pub(crate) fn dc_get_abs_path(context: &Context, path: impl AsRef<Path>) -> PathBuf {
pub(crate) fn dc_get_abs_path<P: AsRef<Path>>(context: &Context, path: P) -> PathBuf {
let p: &Path = path.as_ref();
if let Ok(p) = p.strip_prefix("$BLOBDIR") {
context.get_blobdir().join(p)
@@ -296,10 +297,10 @@ pub(crate) async fn dc_get_filebytes(context: &Context, path: impl AsRef<Path>)
pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) -> bool {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
if !path_abs.exists().await {
return false;
}
if !path_abs.is_file() {
if !path_abs.is_file().await {
warn!(
context,
"refusing to delete non-file \"{}\".",
@@ -322,9 +323,8 @@ pub(crate) async fn dc_delete_file(context: &Context, path: impl AsRef<Path>) ->
}
pub async fn dc_delete_files_in_dir(context: &Context, path: impl AsRef<Path>) {
match tokio::fs::read_dir(path).await {
Ok(read_dir) => {
let mut read_dir = tokio_stream::wrappers::ReadDirStream::new(read_dir);
match async_std::fs::read_dir(path).await {
Ok(mut read_dir) => {
while let Some(entry) = read_dir.next().await {
match entry {
Ok(file) => {
@@ -344,7 +344,7 @@ pub(crate) async fn dc_create_folder(
path: impl AsRef<Path>,
) -> Result<(), io::Error> {
let path_abs = dc_get_abs_path(context, &path);
if !path_abs.exists() {
if !path_abs.exists().await {
match fs::create_dir_all(path_abs).await {
Ok(_) => Ok(()),
Err(err) => {
@@ -655,7 +655,7 @@ mod tests {
assert_eq!(hop_info, expected)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_parse_receive_headers_integration() {
let raw = include_bytes!("../test-data/message/mail_with_cc.txt");
let expected = r"State: Fresh
@@ -873,7 +873,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_file_handling() {
let t = TestContext::new().await;
let context = &t;
@@ -889,8 +889,8 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
assert!(dc_write_file(context, "$BLOBDIR/foobar", b"content")
.await
.is_ok());
assert!(dc_file_exist!(context, "$BLOBDIR/foobar"));
assert!(!dc_file_exist!(context, "$BLOBDIR/foobarx"));
assert!(dc_file_exist!(context, "$BLOBDIR/foobar").await);
assert!(!dc_file_exist!(context, "$BLOBDIR/foobarx").await);
assert_eq!(dc_get_filebytes(context, "$BLOBDIR/foobar").await, 7);
let abs_path = context
@@ -899,23 +899,23 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
.to_string_lossy()
.to_string();
assert!(dc_file_exist!(context, &abs_path));
assert!(dc_file_exist!(context, &abs_path).await);
assert!(dc_delete_file(context, "$BLOBDIR/foobar").await);
assert!(dc_create_folder(context, "$BLOBDIR/foobar-folder")
.await
.is_ok());
assert!(dc_file_exist!(context, "$BLOBDIR/foobar-folder"));
assert!(dc_file_exist!(context, "$BLOBDIR/foobar-folder").await);
assert!(!dc_delete_file(context, "$BLOBDIR/foobar-folder").await);
let fn0 = "$BLOBDIR/data.data";
assert!(dc_write_file(context, &fn0, b"content").await.is_ok());
assert!(dc_delete_file(context, &fn0).await);
assert!(!dc_file_exist!(context, &fn0));
assert!(!dc_file_exist!(context, &fn0).await);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_smeared_timestamp() {
let t = TestContext::new().await;
assert_ne!(
@@ -931,7 +931,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_smeared_timestamps() {
let t = TestContext::new().await;
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
@@ -1001,7 +1001,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
assert_eq!(improve_single_line_input("\r\nahte\n\r"), "ahte");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_maybe_warn_on_bad_time() {
let t = TestContext::new().await;
let timestamp_now = time();
@@ -1064,7 +1064,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
assert_eq!(msgs.len(), 2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_maybe_warn_on_outdated() {
let t = TestContext::new().await;
let timestamp_now: i64 = time();

View File

@@ -382,7 +382,7 @@ mod tests {
assert_eq!(txt.trim(), "two\nlines");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_quote_div() {
let input = include_str!("../test-data/message/gmx-quote-body.eml");
let dehtml = dehtml(input).unwrap();

View File

@@ -280,7 +280,7 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_download_limit() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -303,7 +303,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_update_download_state() -> Result<()> {
let t = TestContext::new_alice().await;
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
@@ -328,7 +328,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_partial_receive_imf() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -376,7 +376,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_partial_download_and_ephemeral() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = t

View File

@@ -422,7 +422,7 @@ mod tests {
mod ensure_secret_key_exists {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_prexisting() {
let t = TestContext::new_alice().await;
assert_eq!(
@@ -431,7 +431,7 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_not_configured() {
let t = TestContext::new().await;
assert!(ensure_secret_key_exists(&t).await.is_err());
@@ -480,7 +480,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
assert_eq!(has_decrypted_pgp_armor(data), false);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_encrypted_no_autocrypt() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -588,7 +588,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
vec![(Some(peerstate), addr)]
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_should_encrypt() {
let t = TestContext::new_alice().await;
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
@@ -615,7 +615,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_mixed_up_mime() -> Result<()> {
// "Mixed Up" mail as received when sending an encrypted
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP

View File

@@ -62,9 +62,9 @@ use std::str::FromStr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{ensure, Context as _, Result};
use async_channel::Receiver;
use async_std::channel::Receiver;
use async_std::future::timeout;
use serde::{Deserialize, Serialize};
use tokio::time::timeout;
use crate::chat::{send_msg, ChatId};
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
@@ -581,7 +581,7 @@ mod tests {
dc_tools::IsNoneOrEmpty,
};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_stock_ephemeral_messages() {
let context = TestContext::new().await;
@@ -711,7 +711,7 @@ mod tests {
}
/// Test enabling and disabling ephemeral timer remotely.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_ephemeral_enable_disable() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -743,7 +743,7 @@ mod tests {
}
/// Test that timer is enabled even if the message explicitly enabling the timer is lost.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_ephemeral_enable_lost() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -785,7 +785,7 @@ mod tests {
/// Test that Alice replying to the chat without a timer at the same time as Bob enables the
/// timer does not result in disabling the timer on the Bob's side.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_ephemeral_timer_rollback() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -859,7 +859,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_ephemeral_delete_msgs() -> Result<()> {
let t = TestContext::new_alice().await;
let self_chat = t.get_self_chat().await;
@@ -985,7 +985,7 @@ mod tests {
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_delete_expired_imap_messages() -> Result<()> {
let t = TestContext::new_alice().await;
const HOUR: i64 = 60 * 60;
@@ -1096,7 +1096,7 @@ mod tests {
}
// Regression test for a bug in the timer rollback protection.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_ephemeral_timer_references() -> Result<()> {
let alice = TestContext::new_alice().await;

View File

@@ -1,8 +1,7 @@
//! # Events specification.
use std::path::PathBuf;
use async_channel::{self as channel, Receiver, Sender, TrySendError};
use async_std::channel::{self, Receiver, Sender, TrySendError};
use async_std::path::PathBuf;
use crate::chat::ChatId;
use crate::contact::ContactId;
@@ -62,18 +61,23 @@ impl Events {
///
/// [`Context`]: crate::context::Context
/// [`Context::get_event_emitter`]: crate::context::Context::get_event_emitter
/// [`Stream`]: futures::stream::Stream
/// [`Stream`]: async_std::stream::Stream
#[derive(Debug, Clone)]
pub struct EventEmitter(Receiver<Event>);
impl EventEmitter {
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
pub fn recv_sync(&self) -> Option<Event> {
async_std::task::block_on(self.recv())
}
/// Async recv of an event. Return `None` if the `Sender` has been droped.
pub async fn recv(&self) -> Option<Event> {
self.0.recv().await.ok()
}
}
impl futures::stream::Stream for EventEmitter {
impl async_std::stream::Stream for EventEmitter {
type Item = Event;
fn poll_next(

View File

@@ -203,7 +203,7 @@ mod tests {
assert_eq!(format_flowed_quote(quote), expected);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_send_quotes() -> anyhow::Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;

View File

@@ -284,7 +284,7 @@ mod tests {
use crate::message::{MessengerMessage, Viewtype};
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_htmlparse_plain_unspecified() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
@@ -300,7 +300,7 @@ This message does not have Content-Type nor Subject.<br/>
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_htmlparse_plain_iso88591() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
@@ -316,7 +316,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_htmlparse_plain_flowed() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
@@ -336,7 +336,7 @@ and will be wrapped as usual.<br/>
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_htmlparse_alt_plain() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
@@ -355,7 +355,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_htmlparse_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_html.eml");
@@ -373,7 +373,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_htmlparse_alt_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
@@ -388,7 +388,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_htmlparse_alt_plain_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
@@ -405,7 +405,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_htmlparse_apple_cid_jpg() {
// load raw mime html-data with related image-part (cid:)
// and make sure, Content-Id has angle-brackets that are removed correctly.
@@ -424,14 +424,14 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(!parser.html.contains("cid:"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_html_invalid_msgid() {
let t = TestContext::new().await;
let msg_id = MsgId::new(100);
assert!(msg_id.get_html(&t).await.is_err())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_html_forwarding() {
// alice receives a non-delta html-message
let alice = TestContext::new_alice().await;
@@ -478,7 +478,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(html.contains("this is <b>html</b>"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_html_forwarding_encrypted() {
// Alice receives a non-delta html-message
// (`ShowEmails=1` lets Alice actually receive non-delta messages for known contacts,
@@ -515,7 +515,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(html.contains("this is <b>html</b>"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_set_html() {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
@@ -547,7 +547,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(html.contains("<b>html</b> text"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_cp1252_html() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config(Config::ShowEmails, Some("2")).await?;

View File

@@ -11,11 +11,11 @@ use std::{
};
use anyhow::{bail, format_err, Context as _, Result};
use async_channel::Receiver;
use async_imap::types::{
Fetch, Flag, Mailbox, Name, NameAttribute, Quota, QuotaRoot, UnsolicitedResponse,
};
use futures::StreamExt;
use async_std::channel::Receiver;
use async_std::prelude::*;
use num_traits::FromPrimitive;
use crate::chat::{self, ChatId, ChatIdBlocked};
@@ -1922,7 +1922,7 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
fn get_folder_meaning(folder_name: &Name) -> FolderMeaning {
for attr in folder_name.attributes() {
if let NameAttribute::Extension(ref label) = attr {
if let NameAttribute::Custom(ref label) = attr {
match label.as_ref() {
"\\Trash" => return FolderMeaning::Other,
"\\Sent" => return FolderMeaning::Sent,
@@ -2388,7 +2388,7 @@ mod tests {
assert_eq!(get_folder_meaning_by_name("SPAM"), FolderMeaning::Spam);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_set_uid_next_validity() {
let t = TestContext::new_alice().await;
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
@@ -2570,7 +2570,7 @@ mod tests {
("Spam", true, true, "DeltaChat"),
];
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_target_folder_incoming_accepted() -> Result<()> {
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
check_target_folder_combination(
@@ -2587,7 +2587,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_target_folder_incoming_request() -> Result<()> {
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_REQUEST {
check_target_folder_combination(
@@ -2604,7 +2604,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_target_folder_outgoing() -> Result<()> {
// Test outgoing emails
for (folder, mvbox_move, chat_msg, expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
@@ -2622,7 +2622,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_target_folder_setupmsg() -> Result<()> {
// Test setupmessages
for (folder, mvbox_move, chat_msg, _expected_destination) in COMBINATIONS_ACCEPTED_CHAT {
@@ -2640,7 +2640,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_imap_search_command() -> Result<()> {
let t = TestContext::new_alice().await;
assert_eq!(

View File

@@ -8,7 +8,7 @@ use anyhow::{Context as _, Result};
use async_imap::Client as ImapClient;
use async_smtp::ServerAddress;
use tokio::net::{self, TcpStream};
use async_std::net::{self, TcpStream};
use super::session::Session;
use crate::login_param::{dc_build_tls, Socks5Config};

View File

@@ -2,7 +2,7 @@ use super::Imap;
use anyhow::{bail, Context as _, Result};
use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt;
use async_std::prelude::*;
use std::time::{Duration, SystemTime};
use crate::{context::Context, scheduler::InterruptInfo};
@@ -87,7 +87,9 @@ impl Imap {
}
}
let session = tokio::time::timeout(Duration::from_secs(15), handle.done())
let session = handle
.done()
.timeout(Duration::from_secs(15))
.await?
.context("IMAP IDLE protocol timed out")?;
self.session = Some(Session { inner: session });
@@ -119,7 +121,7 @@ impl Imap {
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
let mut interval = tokio::time::interval(Duration::from_secs(60));
let mut interval = async_std::stream::interval(Duration::from_secs(60));
enum Event {
Tick,
@@ -129,7 +131,7 @@ impl Imap {
let info = loop {
use futures::future::FutureExt;
match interval
.tick()
.next()
.map(|_| Event::Tick)
.race(
self.idle_interrupt

View File

@@ -1,13 +1,14 @@
use std::{collections::BTreeMap, time::Instant};
use anyhow::{Context as _, Result};
use futures::stream::StreamExt;
use crate::config::Config;
use crate::imap::Imap;
use crate::log::LogExt;
use crate::{context::Context, imap::FolderMeaning};
use async_std::stream::StreamExt;
use super::{get_folder_meaning, get_folder_meaning_by_name};
impl Imap {
@@ -103,7 +104,7 @@ impl Imap {
let list = session
.list(Some(""), Some("*"))
.await?
.filter_map(|f| async { f.ok_or_log_msg(context, "list_folders() can't get folder") });
.filter_map(|f| f.ok_or_log_msg(context, "list_folders() can't get folder"));
Ok(list.collect().await)
}
}

View File

@@ -2,8 +2,8 @@ use std::ops::{Deref, DerefMut};
use async_imap::Session as ImapSession;
use async_native_tls::TlsStream;
use async_std::net::TcpStream;
use fast_socks5::client::Socks5Stream;
use tokio::net::TcpStream;
#[derive(Debug)]
pub(crate) struct Session {
@@ -11,7 +11,7 @@ pub(crate) struct Session {
}
pub(crate) trait SessionStream:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + std::fmt::Debug
async_std::io::Read + async_std::io::Write + Unpin + Send + Sync + std::fmt::Debug
{
}

View File

@@ -2,15 +2,16 @@
use std::any::Any;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use ::pgp::types::KeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use futures::{StreamExt, TryStreamExt};
use futures_lite::FutureExt;
use async_std::{
fs::{self, File},
path::{Path, PathBuf},
prelude::*,
};
use async_tar::Archive;
use rand::{thread_rng, Rng};
use tokio::fs::{self, File};
use tokio_tar::Archive;
use crate::blob::BlobObject;
use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
@@ -108,22 +109,24 @@ pub async fn imex(
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
let mut dir_iter = tokio::fs::read_dir(dir_name).await?;
let mut dir_iter = async_std::fs::read_dir(dir_name).await?;
let mut newest_backup_name = "".to_string();
let mut newest_backup_path: Option<PathBuf> = None;
while let Ok(Some(dirent)) = dir_iter.next_entry().await {
let path = dirent.path();
let name = dirent.file_name();
let name: String = name.to_string_lossy().into();
if name.starts_with("delta-chat")
&& name.ends_with(".tar")
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
// We just use string comparison to determine which backup is newer.
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar
newest_backup_path = Some(path);
newest_backup_name = name;
while let Some(dirent) = dir_iter.next().await {
if let Ok(dirent) = dirent {
let path = dirent.path();
let name = dirent.file_name();
let name: String = name.to_string_lossy().into();
if name.starts_with("delta-chat")
&& name.ends_with(".tar")
&& (newest_backup_name.is_empty() || name > newest_backup_name)
{
// We just use string comparison to determine which backup is newer.
// This works fine because the filenames have the form ...delta-chat-backup-2020-07-24-00.tar
newest_backup_path = Some(path);
newest_backup_name = name;
}
}
}
@@ -174,7 +177,7 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
info!(context, "Wait for setup message being sent ...",);
while !context.shall_stop_ongoing().await {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
if let Ok(msg) = Message::load_from_db(context, msg_id).await {
if msg.is_sent() {
info!(context, "... setup message sent.",);
@@ -443,7 +446,7 @@ async fn import_backup(
context.sql.config_cache.write().await.clear();
let mut archive = Archive::new(backup_file);
let archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
let mut last_progress = 0;
@@ -474,7 +477,7 @@ async fn import_backup(
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
f.unpack_in(context.get_blobdir()).await?;
let from_path = context.get_blobdir().join(f.path()?);
if from_path.is_file() {
if from_path.is_file().await {
if let Some(name) = from_path.file_name() {
fs::rename(&from_path, context.get_blobdir().join(name)).await?;
} else {
@@ -496,7 +499,10 @@ async fn import_backup(
/// Returns Ok((temp_db_path, temp_path, dest_path)) on success. Unencrypted database can be
/// written to temp_db_path. The backup can then be written to temp_path. If the backup succeeded,
/// it can be renamed to dest_path. This guarantees that the backup is complete.
fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, PathBuf, PathBuf)> {
async fn get_next_backup_path(
folder: &Path,
backup_time: i64,
) -> Result<(PathBuf, PathBuf, PathBuf)> {
let folder = PathBuf::from(folder);
let stem = chrono::NaiveDateTime::from_timestamp(backup_time, 0)
// Don't change this file name format, in has_backup() we use string comparison to determine which backup is newer:
@@ -514,7 +520,7 @@ fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, Pat
let mut destfile = folder.clone();
destfile.push(format!("{}-{:02}.tar", stem, i));
if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
if !tempdbfile.exists().await && !tempfile.exists().await && !destfile.exists().await {
return Ok((tempdbfile, tempfile, destfile));
}
}
@@ -524,7 +530,7 @@ fn get_next_backup_path(folder: &Path, backup_time: i64) -> Result<(PathBuf, Pat
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time();
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now)?;
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, now).await?;
let _d1 = DeleteOnDrop(temp_db_path.clone());
let _d2 = DeleteOnDrop(temp_path.clone());
@@ -578,8 +584,7 @@ impl Drop for DeleteOnDrop {
fn drop(&mut self) {
let file = self.0.clone();
// Not using dc_delete_file() here because it would send a DeletedBlobFile event
// Hack to avoid panic in nested runtime calls of tokio
std::fs::remove_file(file).ok();
async_std::task::block_on(fs::remove_file(file)).ok();
}
}
@@ -590,21 +595,19 @@ async fn export_backup_inner(
) -> Result<()> {
let file = File::create(temp_path).await?;
let mut builder = tokio_tar::Builder::new(file);
let mut builder = async_tar::Builder::new(file);
builder
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
.await?;
let read_dir: Vec<_> =
tokio_stream::wrappers::ReadDirStream::new(fs::read_dir(context.get_blobdir()).await?)
.try_collect()
.await?;
let read_dir: Vec<_> = fs::read_dir(context.get_blobdir()).await?.collect().await;
let count = read_dir.len();
let mut written_files = 0;
let mut last_progress = 0;
for entry in read_dir.into_iter() {
let entry = entry?;
let name = entry.file_name();
if !entry.file_type().await?.is_file() {
warn!(
@@ -645,9 +648,9 @@ async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
let mut imported_cnt = 0;
let dir_name = dir.to_string_lossy();
let mut dir_handle = tokio::fs::read_dir(&dir).await?;
while let Ok(Some(entry)) = dir_handle.next_entry().await {
let entry_fn = entry.file_name();
let mut dir_handle = async_std::fs::read_dir(&dir).await?;
while let Some(entry) = dir_handle.next().await {
let entry_fn = entry?.file_name();
let name_f = entry_fn.to_string_lossy();
let path_plus_name = dir.join(&entry_fn);
match dc_get_filesuffix_lc(&name_f) {
@@ -797,7 +800,7 @@ mod tests {
use ::pgp::armor::BlockType;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_render_setup_file() {
let t = TestContext::new_alice().await;
let msg = render_setup_file(&t, "hello").await.unwrap();
@@ -814,7 +817,7 @@ mod tests {
assert!(msg.contains("-----END PGP MESSAGE-----\n"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_render_setup_file_newline_replace() {
let t = TestContext::new_alice().await;
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
@@ -825,7 +828,7 @@ mod tests {
assert!(msg.contains("<p>hello<br>there</p>"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_create_setup_code() {
let t = TestContext::new().await;
let setupcode = create_setup_code(&t);
@@ -840,7 +843,7 @@ mod tests {
assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_export_public_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().public;
@@ -850,12 +853,12 @@ mod tests {
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{}/public-key-default.asc", blobdir);
let bytes = tokio::fs::read(&filename).await.unwrap();
let bytes = async_std::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_export_private_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().secret;
@@ -865,12 +868,12 @@ mod tests {
.is_ok());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{}/private-key-default.asc", blobdir);
let bytes = tokio::fs::read(&filename).await.unwrap();
let bytes = async_std::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_export_and_import_key() {
let context = TestContext::new_alice().await;
let blobdir = context.ctx.get_blobdir();
@@ -884,7 +887,7 @@ mod tests {
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_export_and_import_backup() -> Result<()> {
let backup_dir = tempfile::tempdir().unwrap();
@@ -893,21 +896,26 @@ mod tests {
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
assert!(has_backup(&context2, backup_dir.path().as_ref())
.await
.is_err());
// export from context1
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_ok()
);
assert!(imex(
&context1,
ImexMode::ExportBackup,
backup_dir.path().as_ref(),
None,
)
.await
.is_ok());
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// import to context2
let backup = has_backup(&context2, backup_dir.path()).await?;
let backup = has_backup(&context2, backup_dir.path().as_ref()).await?;
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
assert!(imex(
@@ -953,7 +961,7 @@ mod tests {
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt");
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_split_and_decrypt() {
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
@@ -976,20 +984,20 @@ mod tests {
assert!(headers.get(HEADER_SETUPCODE).is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_key_transfer() -> Result<()> {
let alice = TestContext::new_alice().await;
let alice_clone = alice.clone();
let key_transfer_task = tokio::task::spawn(async move {
let key_transfer_task = async_std::task::spawn(async move {
let ctx = alice_clone;
initiate_key_transfer(&ctx).await
});
// Wait for the message to be added to the queue.
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
async_std::task::sleep(std::time::Duration::from_secs(1)).await;
let sent = alice.pop_sent_msg().await;
let setup_code = key_transfer_task.await??;
let setup_code = key_transfer_task.await?;
// Alice sets up a second device.
let alice2 = TestContext::new().await;

View File

@@ -448,7 +448,7 @@ mod tests {
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_load_next_job_two() -> Result<()> {
// We want to ensure that loading jobs skips over jobs which
// fails to load from the database instead of failing to load
@@ -464,7 +464,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_load_next_job_one() -> Result<()> {
let t = TestContext::new().await;

View File

@@ -11,7 +11,6 @@ use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use tokio::runtime::Handle;
use crate::config::Config;
use crate::constants::KeyGenType;
@@ -220,10 +219,9 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
.unwrap_or_default();
info!(context, "Generating keypair with type {}", keytype);
let keypair = Handle::current()
.spawn_blocking(move || crate::pgp::create_keypair(addr, keytype))
.await??;
let keypair =
async_std::task::spawn_blocking(move || crate::pgp::create_keypair(addr, keytype))
.await?;
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
info!(
context,
@@ -399,8 +397,8 @@ mod tests {
use super::*;
use crate::test_utils::{alice_keypair, TestContext};
use async_std::sync::Arc;
use once_cell::sync::Lazy;
use std::sync::Arc;
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
@@ -522,7 +520,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(key, key2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_load_self_existing() {
let alice = alice_keypair();
let t = TestContext::new_alice().await;
@@ -532,7 +530,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(alice.secret, seckey);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_load_self_generate_public() {
let t = TestContext::new().await;
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
@@ -542,7 +540,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert!(key.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_load_self_generate_secret() {
let t = TestContext::new().await;
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
@@ -552,7 +550,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert!(key.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_load_self_generate_concurrent() {
use std::thread;
@@ -562,19 +560,11 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
.unwrap();
let thr0 = {
let ctx = t.clone();
thread::spawn(move || {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(SignedPublicKey::load_self(&ctx))
})
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx)))
};
let thr1 = {
let ctx = t.clone();
thread::spawn(move || {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(SignedPublicKey::load_self(&ctx))
})
thread::spawn(move || async_std::task::block_on(SignedPublicKey::load_self(&ctx)))
};
let res0 = thr0.join().unwrap();
let res1 = thr1.join().unwrap();
@@ -587,7 +577,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
assert_eq!(pubkey.primary_key, KEYPAIR.public.primary_key);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_save_self_key_twice() {
// Saving the same key twice should result in only one row in
// the keypairs table.

View File

@@ -76,7 +76,7 @@ mod tests {
assert_eq!(sec_ring.keys(), [alice.secret]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_keyring_load_self() {
// new_self() implies load_self()
let t = TestContext::new_alice().await;

View File

@@ -1,6 +1,5 @@
//! # Delta Chat Core Library.
#![recursion_limit = "256"]
#![forbid(unsafe_code)]
#![deny(
unused,

View File

@@ -1,12 +1,12 @@
//! Location handling.
use std::convert::TryFrom;
use std::time::Duration;
use anyhow::{ensure, Context as _, Result};
use async_channel::Receiver;
use async_std::channel::Receiver;
use async_std::future::timeout;
use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use tokio::time::timeout;
use std::time::Duration;
use crate::chat::{self, ChatId};
use crate::contact::ContactId;
@@ -731,7 +731,7 @@ mod tests {
use crate::dc_receive_imf::dc_receive_imf;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_kml_parse() {
let context = TestContext::new().await;
@@ -763,7 +763,7 @@ mod tests {
assert_eq!(locations_ref[1].timestamp, 1544739072);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_message_kml() {
let context = TestContext::new().await;
let timestamp = 1598490000;
@@ -791,7 +791,7 @@ mod tests {
}
/// Tests that location.kml is hidden.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn receive_location_kml() -> Result<()> {
let alice = TestContext::new_alice().await;

View File

@@ -1,6 +1,7 @@
//! # Logging.
use crate::context::Context;
use async_std::task::block_on;
#[macro_export]
macro_rules! info {
@@ -48,13 +49,15 @@ impl Context {
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
pub fn set_last_error(&self, error: &str) {
let mut last_error = self.last_error.write().unwrap();
*last_error = error.to_string();
block_on(async move {
let mut last_error = self.last_error.write().await;
*last_error = error.to_string();
});
}
/// Get last error string.
pub fn get_last_error(&self) -> String {
let last_error = &*self.last_error.read().unwrap();
pub async fn get_last_error(&self) -> String {
let last_error = &*self.last_error.read().await;
last_error.clone()
}
}
@@ -156,24 +159,24 @@ mod tests {
use crate::test_utils::TestContext;
use anyhow::Result;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_get_last_error() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_last_error(), "");
assert_eq!(t.get_last_error().await, "");
error!(t, "foo-error");
assert_eq!(t.get_last_error(), "foo-error");
assert_eq!(t.get_last_error().await, "foo-error");
warn!(t, "foo-warning");
assert_eq!(t.get_last_error(), "foo-error");
assert_eq!(t.get_last_error().await, "foo-error");
info!(t, "foo-info");
assert_eq!(t.get_last_error(), "foo-error");
assert_eq!(t.get_last_error().await, "foo-error");
error!(t, "bar-error");
error!(t, "baz-error");
assert_eq!(t.get_last_error(), "baz-error");
assert_eq!(t.get_last_error().await, "baz-error");
Ok(())
}

View File

@@ -4,16 +4,18 @@ use std::borrow::Cow;
use std::fmt;
use std::time::Duration;
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::provider::{get_provider_by_id, Provider};
use crate::{context::Context, provider::Socket};
use anyhow::{ensure, Result};
use async_std::io;
use async_std::net::TcpStream;
use async_native_tls::Certificate;
pub use async_smtp::ServerAddress;
use fast_socks5::client::Socks5Stream;
use once_cell::sync::Lazy;
use tokio::{io, net::TcpStream};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::provider::{get_provider_by_id, Provider};
use crate::{context::Context, provider::Socket};
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
#[repr(u32)]
@@ -422,7 +424,7 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_save_load_login_param() -> Result<()> {
let t = TestContext::new().await;
@@ -458,7 +460,7 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[async_std::test]
async fn test_build_tls() -> Result<()> {
// we are using some additional root certificates.
// make sure, they do not break construction of TlsConnector

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