mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 21:42:10 +03:00
Compare commits
25 Commits
get_info_j
...
integrate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c14e5086d7 | ||
|
|
4779383401 | ||
|
|
47d30ef6d3 | ||
|
|
3a38ffdfe0 | ||
|
|
33d548eccc | ||
|
|
1700af2c8d | ||
|
|
01920a1a00 | ||
|
|
7c67ea0b8a | ||
|
|
0c64701984 | ||
|
|
d2d35fe26b | ||
|
|
a0b4d016d5 | ||
|
|
3ce70ee244 | ||
|
|
c43c9b9107 | ||
|
|
638d2ff932 | ||
|
|
0213bb372f | ||
|
|
d54fa65ff3 | ||
|
|
564d283852 | ||
|
|
8357b3a98c | ||
|
|
177f89f678 | ||
|
|
372425f38f | ||
|
|
53f8274c6f | ||
|
|
65b242aa5c | ||
|
|
bb6d7767b5 | ||
|
|
227a75a5f7 | ||
|
|
8fb46d0b56 |
2
.github/mergeable.yml
vendored
2
.github/mergeable.yml
vendored
@@ -16,7 +16,7 @@ mergeable:
|
||||
required: ['CHANGELOG.md']
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'deltachat-ffi/src/**'
|
||||
file: 'deltachat-ffi/**'
|
||||
required: ['CHANGELOG.md']
|
||||
fail:
|
||||
- do: checks
|
||||
|
||||
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
fmt:
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace --tests --examples --benches --features repl -- -D warnings
|
||||
args: --workspace --tests --examples --benches
|
||||
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
@@ -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:
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
|
||||
- name: install python
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: -p deltachat_ffi --features jsonrpc
|
||||
args: -p deltachat_ffi
|
||||
|
||||
- name: run python tests
|
||||
if: ${{ matrix.python }}
|
||||
@@ -141,18 +141,3 @@ jobs:
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e lint,mypy,doc,py3
|
||||
|
||||
- name: install pypy
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 'pypy${{ matrix.python }}'
|
||||
|
||||
- name: run pypy tests
|
||||
if: ${{ matrix.python }}
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
DCC_RS_TARGET: debug
|
||||
DCC_RS_DEV: ${{ github.workspace }}
|
||||
working-directory: python
|
||||
run: tox -e pypy3
|
||||
|
||||
45
.github/workflows/jsonrpc.yml
vendored
45
.github/workflows/jsonrpc.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: JSON-RPC API Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v1.3.0
|
||||
- name: npm install
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm install
|
||||
- name: Build TypeScript, run Rust tests, generate bindings
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run build
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
- name: Run linter
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run prettier:check
|
||||
66
.github/workflows/jsonrpc_api.yml
vendored
Normal file
66
.github/workflows/jsonrpc_api.yml
vendored
Normal 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
|
||||
93
CHANGELOG.md
93
CHANGELOG.md
@@ -2,95 +2,6 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc api over websocket server (basically a new api next to the cffi) #3463
|
||||
- jsonrpc methods in cffi #3463:
|
||||
- `dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);`
|
||||
- `void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);`
|
||||
- `void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, char* request);`
|
||||
- `char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);`
|
||||
- node: json rpc methods #3463:
|
||||
- `AccountManager.prototype.startJsonRpcHandler(callback: ((response: string) => void)): void`
|
||||
- `AccountManager.prototype.jsonRpcRequest(message: string): void`
|
||||
- remove `dc_chat_get_info_json()` #3523
|
||||
|
||||
### Added
|
||||
- added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463
|
||||
|
||||
### Changes
|
||||
- refactorings #3545 #3551
|
||||
- use [pathlib](https://docs.python.org/3/library/pathlib.html) in provider update script #3543
|
||||
|
||||
### Fixes
|
||||
- improved error handling for account setup from qrcode #3474
|
||||
|
||||
## 1.92.0
|
||||
|
||||
### API-Changes
|
||||
- add `dc_chat_get_mailinglist_addr()` #3520
|
||||
|
||||
|
||||
## 1.91.0
|
||||
|
||||
### Added
|
||||
- python bindings: extra method to get an account running
|
||||
|
||||
### Changes
|
||||
- refactorings #3437
|
||||
|
||||
### Fixes
|
||||
- mark "group image changed" as system message on receiver side #3517
|
||||
|
||||
|
||||
## 1.90.0
|
||||
|
||||
### Changes
|
||||
- handle drafts from mailto links in scanned QR #3492
|
||||
- do not overflow ratelimiter leaky bucket #3496
|
||||
- (AEAP) Add device message after you changed your address #3505
|
||||
- (AEAP) Revert #3491, instead only replace contacts in verified groups #3510
|
||||
- improve python bindings and tests #3502 #3503
|
||||
|
||||
### Fixes
|
||||
- don't squash text parts of NDN into attachments #3497
|
||||
- do not treat non-failed DSNs as NDNs #3506
|
||||
|
||||
|
||||
## 1.89.0
|
||||
|
||||
### Changes
|
||||
|
||||
- (AEAP) When one of your contacts changed their address, they are
|
||||
only replaced in the chat where you got a message from them
|
||||
for now #3491
|
||||
|
||||
### Fixes
|
||||
- replace musl libc name resolution errors with a better message #3485
|
||||
- handle updates for not yet downloaded webxdc instances #3487
|
||||
|
||||
|
||||
## 1.88.0
|
||||
|
||||
### Changes
|
||||
- Implemented "Automatic e-mail address Porting" (AEAP). You can
|
||||
configure a new address in DC now, and when receivers get messages
|
||||
they will automatically recognize your moving to a new address. #3385
|
||||
- switch from `async-std` to `tokio` as the async runtime #3449
|
||||
- upgrade to `pgp@0.8.0` #3467
|
||||
- add IMAP ID extension support #3468
|
||||
- configure DeltaChat folder by selecting it, so it is configured even if not LISTed #3371
|
||||
- build PyPy wheels #6683
|
||||
- improve default error if NDN does not provide an error #3456
|
||||
- increase ratelimit from 3 to 6 messages per 60 seconds #3481
|
||||
|
||||
### Fixes
|
||||
- mailing list: remove square-brackets only for first name #3452
|
||||
- do not use footers from mailinglists as the contact status #3460
|
||||
- don't ignore KML parsing errors #3473
|
||||
|
||||
|
||||
## 1.87.0
|
||||
|
||||
### Changes
|
||||
- limit the rate of MDN sending #3402
|
||||
- ignore ratelimits for bots #3439
|
||||
@@ -98,10 +9,9 @@
|
||||
- format message lines starting with `>` as quotes #3434
|
||||
- node: remove `split2` dependency #3418
|
||||
- node: add git installation info to readme #3418
|
||||
- limit the rate of webxdc update sending #3417
|
||||
|
||||
### Fixes
|
||||
- set a default error if NDN does not provide an error #3410
|
||||
- set a default error if NDN does not provide an error
|
||||
- python: avoid exceptions when messages/contacts/chats are compared with `None`
|
||||
- node: wait for the event loop to stop before destroying contexts #3431 #3451
|
||||
- emit configuration errors via event on failure #3433
|
||||
@@ -114,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
|
||||
|
||||
2186
Cargo.lock
generated
2186
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
49
Cargo.toml
49
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.92.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,23 +19,23 @@ deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
|
||||
trust-dns-resolver = "0.21"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
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"
|
||||
image = { version = "0.24.3", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
@@ -46,18 +45,18 @@ native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.13.0"
|
||||
once_cell = "1.12.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.8", default-features = false }
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.23"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.8"
|
||||
regex = "1.6"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "10", optional = true }
|
||||
rustyline = { version = "9", optional = true }
|
||||
sanitize-filename = "0.4"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -66,30 +65,27 @@ 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.6", 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"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
@@ -137,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"]
|
||||
|
||||
@@ -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 }))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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,
|
||||
dc_receive_imf::dc_receive_imf,
|
||||
imex::{imex, ImexMode},
|
||||
receive_imf::receive_imf,
|
||||
Events,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
@@ -30,7 +32,7 @@ Hello {i}",
|
||||
i = i,
|
||||
i_dec = i - 1,
|
||||
);
|
||||
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.92.0"
|
||||
version = "1.86.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -21,11 +21,10 @@ 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.13.0"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
@@ -27,9 +28,8 @@ fn main() {
|
||||
);
|
||||
|
||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||
fs::write(
|
||||
target_path.join("pkgconfig").join("deltachat.pc"),
|
||||
pkg_config.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
|
||||
.unwrap()
|
||||
.write_all(pkg_config.as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -2292,7 +2292,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID:
|
||||
* scanned fingerprint does not match last seen fingerprint.
|
||||
*
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::text1=Formatted fingerprint
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
|
||||
* the scanned QR code contains a fingerprint but no e-mail address;
|
||||
* suggest the user to establish an encrypted connection first.
|
||||
*
|
||||
@@ -2305,8 +2305,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
|
||||
* e-mail address scanned, optionally, a draft message could be set in
|
||||
* dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT;
|
||||
* e-mail address scanned,
|
||||
* ask the user if they want to start chatting;
|
||||
* if so, call dc_create_chat_by_contact_id().
|
||||
*
|
||||
@@ -3165,6 +3164,24 @@ dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t ch
|
||||
dc_context_t* dc_chatlist_get_context (dc_chatlist_t* chatlist);
|
||||
|
||||
|
||||
/**
|
||||
* Get info summary for a chat, in JSON format.
|
||||
*
|
||||
* The returned JSON string has the following key/values:
|
||||
*
|
||||
* id: chat id
|
||||
* name: chat/group name
|
||||
* color: color of this chat
|
||||
* last-message-from: who sent the last message
|
||||
* last-message-text: message (truncated)
|
||||
* last-message-state: @ref DC_STATE constant
|
||||
* last-message-date:
|
||||
* avatar-path: path-to-blobfile
|
||||
* is_verified: yes/no
|
||||
* @return a UTF8-encoded JSON string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
|
||||
*/
|
||||
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
|
||||
|
||||
/**
|
||||
* @class dc_chat_t
|
||||
*
|
||||
@@ -3239,19 +3256,6 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
|
||||
int dc_chat_get_type (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Returns the address where messages are sent to if the chat is a mailing list.
|
||||
* If you just want to know if a mailing list can be written to,
|
||||
* use dc_chat_can_send() instead.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return The mailing list address. Must be released using dc_str_unref() after usage.
|
||||
* If there is no such address, an empty string is returned, NULL is never returned.
|
||||
*/
|
||||
char* dc_chat_get_mailinglist_addr (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get name of a chat. For one-to-one chats, this is the name of the contact.
|
||||
* For group chats, this is the name given e.g. to dc_create_group_chat() or
|
||||
@@ -5210,14 +5214,14 @@ void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
* @param request JSON-RPC request as string
|
||||
*/
|
||||
void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* request);
|
||||
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, must be freed using dc_str_unref() after usage.
|
||||
* @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().
|
||||
*/
|
||||
@@ -6409,24 +6413,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as status in the connectivity view.
|
||||
#define DC_STR_NOT_CONNECTED 121
|
||||
|
||||
/// %1$s changed their address from %2$s to %3$s"
|
||||
///
|
||||
/// Used as an info message to chats with contacts that changed their address.
|
||||
#define DC_STR_AEAP_ADDR_CHANGED 122
|
||||
|
||||
/// "You changed your email address from %1$s to %2$s.
|
||||
/// If you now send a message to a group, contacts there will automatically
|
||||
/// replace the old with your new address.\n\nIt's highly advised to set up
|
||||
/// your old email provider to forward all emails to your new email address.
|
||||
/// Otherwise you might miss messages of contacts who did not get your new
|
||||
/// address yet." + the link to the AEAP blog post
|
||||
///
|
||||
/// As soon as there is a post about AEAP, the UIs should add it:
|
||||
/// set_stock_translation(123, getString(aeap_explanation) + "\n\n" + AEAP_BLOG_LINK)
|
||||
///
|
||||
/// Used in a device message that explains AEAP.
|
||||
#define DC_STR_AEAP_EXPLANATION_AND_LINK 123
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![warn(unused, clippy::all)]
|
||||
#![deny(unused, clippy::all)]
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
@@ -15,7 +15,6 @@ 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;
|
||||
@@ -23,12 +22,11 @@ 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;
|
||||
@@ -41,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;
|
||||
@@ -65,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,
|
||||
@@ -98,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();
|
||||
@@ -122,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);
|
||||
@@ -396,7 +384,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
|
||||
let redirect = to_string_lossy(redirect);
|
||||
|
||||
block_on(async move {
|
||||
match oauth2::get_oauth2_url(ctx, &addr, &redirect)
|
||||
match oauth2::dc_get_oauth2_url(ctx, &addr, &redirect)
|
||||
.await
|
||||
.log_err(ctx, "dc_get_oauth2_url failed")
|
||||
{
|
||||
@@ -694,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]
|
||||
@@ -740,7 +725,7 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let keypair = key::KeyPair {
|
||||
@@ -2245,7 +2230,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
|
||||
block_on(securejoin::get_securejoin_qr(ctx, chat_id))
|
||||
block_on(securejoin::dc_get_securejoin_qr(ctx, chat_id))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
@@ -2283,7 +2268,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
securejoin::join_securejoin(ctx, &to_string_lossy(qr))
|
||||
securejoin::dc_join_securejoin(ctx, &to_string_lossy(qr))
|
||||
.await
|
||||
.map(|chatid| chatid.to_u32())
|
||||
.log_err(ctx, "failed dc_join_securejoin() call")
|
||||
@@ -2409,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
|
||||
@@ -2782,16 +2767,6 @@ pub unsafe extern "C" fn dc_chat_get_name(chat: *mut dc_chat_t) -> *mut libc::c_
|
||||
ffi_chat.chat.get_name().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_mailinglist_addr(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_mailinglist_addr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat.chat.get_mailinglist_addr().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
||||
if chat.is_null() {
|
||||
@@ -2943,6 +2918,41 @@ pub unsafe extern "C" fn dc_chat_get_remaining_mute_duration(chat: *mut dc_chat_
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_info_json(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_info_json()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
|
||||
Ok(chat) => chat,
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_get_chat_info_json() failed to load chat: {}", err);
|
||||
return "".strdup();
|
||||
}
|
||||
};
|
||||
let info = match chat.get_info(ctx).await {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
error!(
|
||||
ctx,
|
||||
"dc_get_chat_info_json() failed to get chat info: {}", err
|
||||
);
|
||||
return "".strdup();
|
||||
}
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json")
|
||||
.strdup()
|
||||
})
|
||||
}
|
||||
|
||||
// dc_msg_t
|
||||
|
||||
/// FFI struct for [dc_msg_t]
|
||||
@@ -4117,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))),
|
||||
@@ -4289,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,
|
||||
@@ -4409,7 +4419,9 @@ 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)
|
||||
}
|
||||
@@ -4418,11 +4430,11 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
|
||||
mod jsonrpc {
|
||||
use super::*;
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
use deltachat_jsonrpc::yerpc::{MessageHandle, RpcHandle};
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<CommandApi>,
|
||||
receiver: async_std::channel::Receiver<deltachat_jsonrpc::yerpc::Message>,
|
||||
handle: MessageHandle<CommandApi>,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4435,10 +4447,10 @@ mod jsonrpc {
|
||||
}
|
||||
|
||||
let cmd_api =
|
||||
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
|
||||
deltachat_jsonrpc::api::CommandApi::new_from_arc((*account_manager).inner.clone());
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
let (request_handle, receiver) = RpcHandle::new();
|
||||
let handle = MessageHandle::new(request_handle, cmd_api);
|
||||
|
||||
let instance = dc_jsonrpc_instance_t { receiver, handle };
|
||||
|
||||
@@ -4468,8 +4480,8 @@ mod jsonrpc {
|
||||
let api = &*jsonrpc_instance;
|
||||
let handle = &api.handle;
|
||||
let request = to_string_lossy(request);
|
||||
spawn(async move {
|
||||
handle.handle_incoming(&request).await;
|
||||
async_std::task::spawn(async move {
|
||||
handle.handle_message(&request).await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4482,7 +4494,7 @@ mod jsonrpc {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
block_on(api.receiver.recv())
|
||||
async_std::task::block_on(api.receiver.recv())
|
||||
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Addr { .. } => None,
|
||||
Qr::Url { url } => Some(url),
|
||||
Qr::Text { text } => Some(text),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
@@ -79,13 +79,7 @@ impl Lot {
|
||||
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
|
||||
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
|
||||
},
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::Addr {
|
||||
draft: Some(_draft),
|
||||
..
|
||||
} => Meaning::Text1Draft,
|
||||
_ => Meaning::None,
|
||||
},
|
||||
Self::Qr(_qr) => Meaning::None,
|
||||
Self::Error(_err) => Meaning::None,
|
||||
}
|
||||
}
|
||||
@@ -124,7 +118,7 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Addr { contact_id } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
|
||||
@@ -55,7 +55,7 @@ pub(crate) enum CStringError {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use deltachat::tools::{dc_strdup, OsStrExt};
|
||||
/// use deltachat::dc_tools::{dc_strdup, OsStrExt};
|
||||
/// let path = std::path::Path::new("/some/path");
|
||||
/// let path_c = path.to_c_string().unwrap();
|
||||
/// unsafe {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.86.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
default-run = "webserver"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "deltachat-jsonrpc-server"
|
||||
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"] }
|
||||
@@ -22,18 +22,18 @@ log = "0.4"
|
||||
async-channel = { version = "1.6.1" }
|
||||
futures = { version = "0.3.19" }
|
||||
serde_json = "1.0.75"
|
||||
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
|
||||
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
|
||||
tokio = { version = "1.19.2" }
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.5.9", optional = true, features = ["ws"] }
|
||||
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 }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.19.2", features = ["full", "rt-multi-thread"] }
|
||||
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", "axum", "tokio/full", "yerpc/support-axum"]
|
||||
webserver = ["env_logger", "tide", "tide-websockets", "yerpc-tide"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
75
deltachat-jsonrpc/README.MD
Normal file
75
deltachat-jsonrpc/README.MD
Normal 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.
|
||||
@@ -1,123 +0,0 @@
|
||||
# deltachat-jsonrpc
|
||||
|
||||
This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) interface to DeltaChat.
|
||||
|
||||
The JSON-RPC API is exposed in two fashions:
|
||||
|
||||
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
|
||||
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
||||
|
||||
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
|
||||
|
||||
## Usage
|
||||
|
||||
#### Running the WebSocket server
|
||||
|
||||
From within this folder, you can start the WebSocket server with the following command:
|
||||
|
||||
```sh
|
||||
cargo run --features webserver
|
||||
```
|
||||
|
||||
If you want to use the server in a production setup, first build it in release mode:
|
||||
|
||||
```sh
|
||||
cargo build --features webserver --release
|
||||
```
|
||||
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
|
||||
|
||||
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
|
||||
|
||||
The server can be configured with environment variables:
|
||||
|
||||
|variable|default|description|
|
||||
|-|-|-|
|
||||
|`DC_PORT`|`20808`|port to listen on|
|
||||
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
|
||||
|
||||
If you are targetting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
|
||||
|
||||
```sh
|
||||
cross build --features=webserver --target armv7-linux-androideabi --release
|
||||
```
|
||||
|
||||
#### Using the TypeScript/JavaScript client
|
||||
|
||||
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
|
||||
|
||||
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
|
||||
```sh
|
||||
cd typescript
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
|
||||
|
||||
|
||||
```typescript
|
||||
import { DeltaChat } from './deltachat.bundle.js'
|
||||
const dc = new DeltaChat('ws://localhost:20808/ws')
|
||||
const accounts = await dc.rpc.getAllAccounts()
|
||||
console.log('accounts', accounts)
|
||||
```
|
||||
|
||||
A script is included to build autogenerated documentation, which includes all RPC methods:
|
||||
```sh
|
||||
cd typescript
|
||||
npm run docs
|
||||
```
|
||||
Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
|
||||
|
||||
## Development
|
||||
|
||||
#### Running the example app
|
||||
|
||||
We include a small demo web application that talks to the WebSocket server. It can be used for testing. Feel invited to expand this.
|
||||
|
||||
```sh
|
||||
cd typescript
|
||||
npm run build
|
||||
npm run example:build
|
||||
npm run example:start
|
||||
```
|
||||
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
|
||||
|
||||
Run `npm run example:dev` to live-rebuild the example app when files changes.
|
||||
|
||||
### Testing
|
||||
|
||||
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
|
||||
|
||||
#### Rust tests
|
||||
|
||||
To run the Rust test, use this command:
|
||||
|
||||
```
|
||||
cargo test
|
||||
```
|
||||
|
||||
#### TypeScript tests
|
||||
|
||||
```
|
||||
cd typescript
|
||||
npm run test
|
||||
```
|
||||
|
||||
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
|
||||
|
||||
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
|
||||
|
||||
Then, set the `DCC_NEW_TMP_EMAIL` environment variable to your mailadm token before running the tests.
|
||||
|
||||
```
|
||||
DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=yourtoken npm run test
|
||||
```
|
||||
|
||||
#### Test Coverage
|
||||
|
||||
Running `npm run test` will report test coverage. For the coverage to be accurate the online tests need to be run.
|
||||
|
||||
> If you are offline and want to see the coverage results anyway (even though they are inaccurate), you can bypass the errors of the online tests by setting the `COVERAGE_OFFLINE=1` environment variable.
|
||||
|
||||
A summary of the coverage will be reported in the terminal after the test run. Open `coverage/index.html` in a web browser for a detailed report.
|
||||
@@ -1,28 +1,347 @@
|
||||
# TODO
|
||||
## Core system
|
||||
|
||||
- [ ] different test type to simulate two devices: to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer
|
||||
- [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
|
||||
|
||||
## MVP - Websocket server&client
|
||||
|
||||
For kaiOS and other experiments, like a deltachat "web" over network from an android phone.
|
||||
## 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)
|
||||
- [ ] other way blobs can be addressed when using websocket vs. jsonrpc over dc-node
|
||||
- [ ] Web push API? At least some kind of notification hook closure this lib can accept.
|
||||
|
||||
### Other Ideas for the Websocket server
|
||||
## Other Ideas
|
||||
|
||||
- [ ] make sure there can only be one connection at a time to the ws
|
||||
- [ ] 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
|
||||
|
||||
## Desktop Apis
|
||||
|
||||
Incomplete todo for desktop api porting, just some remainders for points that might need more work:
|
||||
|
||||
- [ ] manual start/stop io functions in the api for context and accounts, so "not syncing all accounts" can still be done in desktop -> webserver should then not do start io on all accounts by default
|
||||
## 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
|
||||
@@ -62,9 +62,8 @@ pub fn event_to_json_rpc_notification(event: Event) -> Value {
|
||||
} => (json!(msg_id), json!(status_update_serial)),
|
||||
};
|
||||
|
||||
let id: EventTypeName = event.typ.into();
|
||||
json!({
|
||||
"id": id,
|
||||
"id": event_type_to_string(event.typ),
|
||||
"contextId": event.id,
|
||||
"field1": field1,
|
||||
"field2": field2
|
||||
@@ -104,40 +103,38 @@ pub enum EventTypeName {
|
||||
WebxdcStatusUpdate,
|
||||
}
|
||||
|
||||
impl From<EventType> for EventTypeName {
|
||||
fn from(event: EventType) -> Self {
|
||||
use EventTypeName::*;
|
||||
match event {
|
||||
EventType::Info(_) => Info,
|
||||
EventType::SmtpConnected(_) => SmtpConnected,
|
||||
EventType::ImapConnected(_) => ImapConnected,
|
||||
EventType::SmtpMessageSent(_) => SmtpMessageSent,
|
||||
EventType::ImapMessageDeleted(_) => ImapMessageDeleted,
|
||||
EventType::ImapMessageMoved(_) => ImapMessageMoved,
|
||||
EventType::NewBlobFile(_) => NewBlobFile,
|
||||
EventType::DeletedBlobFile(_) => DeletedBlobFile,
|
||||
EventType::Warning(_) => Warning,
|
||||
EventType::Error(_) => Error,
|
||||
EventType::ErrorSelfNotInGroup(_) => ErrorSelfNotInGroup,
|
||||
EventType::MsgsChanged { .. } => MsgsChanged,
|
||||
EventType::IncomingMsg { .. } => IncomingMsg,
|
||||
EventType::MsgsNoticed(_) => MsgsNoticed,
|
||||
EventType::MsgDelivered { .. } => MsgDelivered,
|
||||
EventType::MsgFailed { .. } => MsgFailed,
|
||||
EventType::MsgRead { .. } => MsgRead,
|
||||
EventType::ChatModified(_) => ChatModified,
|
||||
EventType::ChatEphemeralTimerModified { .. } => ChatEphemeralTimerModified,
|
||||
EventType::ContactsChanged(_) => ContactsChanged,
|
||||
EventType::LocationChanged(_) => LocationChanged,
|
||||
EventType::ConfigureProgress { .. } => ConfigureProgress,
|
||||
EventType::ImexProgress(_) => ImexProgress,
|
||||
EventType::ImexFileWritten(_) => ImexFileWritten,
|
||||
EventType::SecurejoinInviterProgress { .. } => SecurejoinInviterProgress,
|
||||
EventType::SecurejoinJoinerProgress { .. } => SecurejoinJoinerProgress,
|
||||
EventType::ConnectivityChanged => ConnectivityChanged,
|
||||
EventType::SelfavatarChanged => SelfavatarChanged,
|
||||
EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate,
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_std::sync::{Arc, RwLock};
|
||||
use deltachat::{
|
||||
chat::{get_chat_media, get_chat_msgs, ChatId},
|
||||
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,
|
||||
qr,
|
||||
webxdc::StatusUpdateSerial,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
use tokio::sync::RwLock;
|
||||
use yerpc::rpc;
|
||||
|
||||
pub use deltachat::accounts::Accounts;
|
||||
@@ -22,7 +19,6 @@ pub mod events;
|
||||
pub mod types;
|
||||
|
||||
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
|
||||
use crate::api::types::QrObject;
|
||||
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
@@ -30,9 +26,6 @@ use types::chat_list::ChatListEntry;
|
||||
use types::contact::ContactObject;
|
||||
use types::message::MessageObject;
|
||||
use types::provider_info::ProviderInfo;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
use self::types::message::MessageViewtype;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommandApi {
|
||||
@@ -46,8 +39,7 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
pub fn new_from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||
CommandApi { accounts }
|
||||
}
|
||||
|
||||
@@ -191,25 +183,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
|
||||
/// Before this function is called, dc_check_qr() should confirm the type of the
|
||||
/// QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
|
||||
///
|
||||
/// Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
/// e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
|
||||
/// or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
async fn set_config_from_qr(&self, account_id: u32, qr_content: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
qr::set_config_from_qr(&ctx, &qr_content).await
|
||||
}
|
||||
|
||||
async fn check_qr(&self, account_id: u32, qr_content: String) -> Result<QrObject> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = qr::check_qr(&ctx, &qr_content).await?;
|
||||
let qr_object = QrObject::from(qr);
|
||||
Ok(qr_object)
|
||||
}
|
||||
|
||||
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_config(&ctx, &key).await
|
||||
@@ -233,13 +206,7 @@ impl CommandApi {
|
||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
let result = ctx.configure().await;
|
||||
if result.is_err() {
|
||||
if let Ok(true) = ctx.is_configured().await {
|
||||
ctx.start_io().await;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
ctx.configure().await?;
|
||||
ctx.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -251,38 +218,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the message IDs of all _fresh_ messages of any chat.
|
||||
/// Typically used for implementing notification summaries
|
||||
/// or badge counters e.g. on the app icon.
|
||||
/// The list is already sorted and starts with the most recent fresh message.
|
||||
///
|
||||
/// Messages belonging to muted chats or to the contact requests are not returned;
|
||||
/// these messages should not be notified
|
||||
/// and also badge counters should not include these messages.
|
||||
///
|
||||
/// To get the number of fresh messages for a single chat, muted or not,
|
||||
/// use `get_fresh_msg_cnt()`.
|
||||
async fn get_fresh_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx
|
||||
.get_fresh_msgs()
|
||||
.await?
|
||||
.iter()
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get the number of _fresh_ messages in a chat.
|
||||
/// Typically used to implement a badge with a number in the chatlist.
|
||||
///
|
||||
/// If the specified chat is muted,
|
||||
/// the UI should show the badge counter "less obtrusive",
|
||||
/// e.g. using "gray" instead of "red" color.
|
||||
async fn get_fresh_msg_cnt(&self, account_id: u32, chat_id: u32) -> Result<usize> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// autocrypt
|
||||
// ---------------------------------------------
|
||||
@@ -321,7 +256,7 @@ impl CommandApi {
|
||||
query_contact_id.map(ContactId::new),
|
||||
)
|
||||
.await?;
|
||||
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
|
||||
let mut l: Vec<ChatListEntry> = Vec::new();
|
||||
for i in 0..list.len() {
|
||||
l.push(ChatListEntry(
|
||||
list.get_chat_id(i)?.to_u32(),
|
||||
@@ -338,9 +273,8 @@ impl CommandApi {
|
||||
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
|
||||
// todo custom json deserializer for ChatListEntry?
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut result: HashMap<u32, ChatListItemFetchResult> =
|
||||
HashMap::with_capacity(entries.len());
|
||||
for entry in entries.iter() {
|
||||
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 {
|
||||
@@ -365,7 +299,7 @@ impl CommandApi {
|
||||
chat_id: u32,
|
||||
) -> Result<FullChat> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
FullChat::try_from_dc_chat_id(&ctx, chat_id).await
|
||||
FullChat::from_dc_chat_id(&ctx, chat_id).await
|
||||
}
|
||||
|
||||
async fn accept_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
|
||||
@@ -378,21 +312,6 @@ impl CommandApi {
|
||||
ChatId::new(chat_id).block(&ctx).await
|
||||
}
|
||||
|
||||
// for now only text messages, because we only used text messages in desktop thusfar
|
||||
async fn add_device_message(
|
||||
&self,
|
||||
account_id: u32,
|
||||
label: String,
|
||||
text: String,
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(text));
|
||||
let message_id =
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
||||
Ok(message_id.to_u32())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// message list
|
||||
// ---------------------------------------------
|
||||
@@ -448,7 +367,7 @@ impl CommandApi {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
|
||||
ContactObject::try_from_dc_contact(
|
||||
ContactObject::from_dc_contact(
|
||||
&ctx,
|
||||
deltachat::contact::Contact::get_by_id(&ctx, contact_id).await?,
|
||||
)
|
||||
@@ -503,7 +422,7 @@ impl CommandApi {
|
||||
let mut contacts: Vec<ContactObject> = Vec::with_capacity(blocked_ids.len());
|
||||
for id in blocked_ids {
|
||||
contacts.push(
|
||||
ContactObject::try_from_dc_contact(
|
||||
ContactObject::from_dc_contact(
|
||||
&ctx,
|
||||
deltachat::contact::Contact::get_by_id(&ctx, id).await?,
|
||||
)
|
||||
@@ -537,7 +456,7 @@ impl CommandApi {
|
||||
let mut contacts: Vec<ContactObject> = Vec::with_capacity(contact_ids.len());
|
||||
for id in contact_ids {
|
||||
contacts.push(
|
||||
ContactObject::try_from_dc_contact(
|
||||
ContactObject::from_dc_contact(
|
||||
&ctx,
|
||||
deltachat::contact::Contact::get_by_id(&ctx, id).await?,
|
||||
)
|
||||
@@ -558,7 +477,7 @@ impl CommandApi {
|
||||
for id in ids {
|
||||
contacts.insert(
|
||||
id,
|
||||
ContactObject::try_from_dc_contact(
|
||||
ContactObject::from_dc_contact(
|
||||
&ctx,
|
||||
deltachat::contact::Contact::get_by_id(&ctx, ContactId::new(id)).await?,
|
||||
)
|
||||
@@ -567,80 +486,6 @@ impl CommandApi {
|
||||
}
|
||||
Ok(contacts)
|
||||
}
|
||||
// ---------------------------------------------
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Returns all message IDs of the given types in a chat.
|
||||
/// Typically used to show a gallery.
|
||||
///
|
||||
/// The list is already sorted and starts with the oldest message.
|
||||
/// Clients should not try to re-sort the list as this would be an expensive action
|
||||
/// and would result in inconsistencies between clients.
|
||||
async fn chat_get_media(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: u32,
|
||||
message_type: MessageViewtype,
|
||||
or_message_type2: Option<MessageViewtype>,
|
||||
or_message_type3: Option<MessageViewtype>,
|
||||
) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let msg_type = message_type.into();
|
||||
let or_msg_type2 = or_message_type2.map_or(Viewtype::Unknown, |v| v.into());
|
||||
let or_msg_type3 = or_message_type3.map_or(Viewtype::Unknown, |v| v.into());
|
||||
|
||||
let media = get_chat_media(
|
||||
&ctx,
|
||||
ChatId::new(chat_id),
|
||||
msg_type,
|
||||
or_msg_type2,
|
||||
or_msg_type3,
|
||||
)
|
||||
.await?;
|
||||
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// webxdc
|
||||
// ---------------------------------------------
|
||||
|
||||
async fn webxdc_send_status_update(
|
||||
&self,
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
update_str: String,
|
||||
description: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.send_webxdc_status_update(MsgId::new(instance_msg_id), &update_str, &description)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn webxdc_get_status_updates(
|
||||
&self,
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
last_known_serial: u32,
|
||||
) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.get_webxdc_status_updates(
|
||||
MsgId::new(instance_msg_id),
|
||||
StatusUpdateSerial::new(last_known_serial),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get info from a webxdc message
|
||||
async fn message_get_webxdc_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
) -> Result<WebxdcMessageInfo> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// misc prototyping functions
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::color_int_to_hex_string;
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Account {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
//#[serde(rename_all = "camelCase")]
|
||||
Configured {
|
||||
id: u32,
|
||||
display_name: Option<String>,
|
||||
@@ -18,8 +18,9 @@ pub enum Account {
|
||||
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
|
||||
color: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Unconfigured { id: u32 },
|
||||
Unconfigured {
|
||||
id: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Account {
|
||||
|
||||
@@ -11,7 +11,6 @@ use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FullChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
@@ -36,17 +35,17 @@ pub struct FullChat {
|
||||
}
|
||||
|
||||
impl FullChat {
|
||||
pub async fn try_from_dc_chat_id(context: &Context, chat_id: u32) -> Result<Self> {
|
||||
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::with_capacity(contact_ids.len());
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
for contact_id in &contact_ids {
|
||||
contacts.push(
|
||||
ContactObject::try_from_dc_contact(
|
||||
ContactObject::from_dc_contact(
|
||||
context,
|
||||
Contact::load_from_db(context, *contact_id).await?,
|
||||
)
|
||||
|
||||
@@ -42,8 +42,6 @@ pub enum ChatListItemFetchResult {
|
||||
is_pinned: bool,
|
||||
is_muted: bool,
|
||||
is_contact_request: bool,
|
||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||
dm_chat_contact: Option<u32>,
|
||||
},
|
||||
ArchiveLink,
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -88,15 +86,9 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
None => None,
|
||||
};
|
||||
|
||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||
|
||||
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
||||
|
||||
let dm_chat_contact = if chat.get_type() == Chattype::Single {
|
||||
chat_contacts.get(0).map(|contact_id| contact_id.to_u32())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let 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?);
|
||||
@@ -121,6 +113,5 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
dm_chat_contact,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||
#[serde(rename = "Contact")]
|
||||
pub struct ContactObject {
|
||||
address: String,
|
||||
color: String,
|
||||
@@ -23,7 +23,7 @@ pub struct ContactObject {
|
||||
}
|
||||
|
||||
impl ContactObject {
|
||||
pub async fn try_from_dc_contact(
|
||||
pub async fn from_dc_contact(
|
||||
context: &Context,
|
||||
contact: deltachat::contact::Contact,
|
||||
) -> Result<Self> {
|
||||
|
||||
@@ -3,16 +3,14 @@ use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::message::Viewtype;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::contact::ContactObject;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Message", rename_all = "camelCase")]
|
||||
#[serde(rename = "Message")]
|
||||
pub struct MessageObject {
|
||||
id: u32,
|
||||
chat_id: u32,
|
||||
@@ -22,7 +20,7 @@ pub struct MessageObject {
|
||||
text: Option<String>,
|
||||
has_location: bool,
|
||||
has_html: bool,
|
||||
view_type: MessageViewtype,
|
||||
view_type: u32,
|
||||
state: u32,
|
||||
|
||||
timestamp: i64,
|
||||
@@ -66,7 +64,7 @@ impl MessageObject {
|
||||
.map(|m| m.get_id().to_u32());
|
||||
|
||||
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
|
||||
let 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();
|
||||
|
||||
@@ -79,7 +77,10 @@ impl MessageObject {
|
||||
text: message.get_text(),
|
||||
has_location: message.has_location(),
|
||||
has_html: message.has_html(),
|
||||
view_type: message.get_viewtype().into(),
|
||||
view_type: message
|
||||
.get_viewtype()
|
||||
.to_u32()
|
||||
.ok_or_else(|| anyhow!("viewtype conversion to number failed"))?,
|
||||
state: message
|
||||
.get_state()
|
||||
.to_u32()
|
||||
@@ -124,79 +125,3 @@ impl MessageObject {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef)]
|
||||
#[serde(rename = "Viewtype")]
|
||||
pub enum MessageViewtype {
|
||||
Unknown,
|
||||
|
||||
/// Text message.
|
||||
Text,
|
||||
|
||||
/// Image message.
|
||||
/// If the image is an animated GIF, the type `Viewtype.Gif` should be used.
|
||||
Image,
|
||||
|
||||
/// Animated GIF message.
|
||||
Gif,
|
||||
|
||||
/// Message containing a sticker, similar to image.
|
||||
/// If possible, the ui should display the image without borders in a transparent way.
|
||||
/// A click on a sticker will offer to install the sticker set in some future.
|
||||
Sticker,
|
||||
|
||||
/// Message containing an Audio file.
|
||||
Audio,
|
||||
|
||||
/// A voice message that was directly recorded by the user.
|
||||
/// For all other audio messages, the type `Viewtype.Audio` should be used.
|
||||
Voice,
|
||||
|
||||
/// Video messages.
|
||||
Video,
|
||||
|
||||
/// Message containing any file, eg. a PDF.
|
||||
File,
|
||||
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation,
|
||||
|
||||
/// Message is an webxdc instance.
|
||||
Webxdc,
|
||||
}
|
||||
|
||||
impl From<Viewtype> for MessageViewtype {
|
||||
fn from(viewtype: Viewtype) -> Self {
|
||||
match viewtype {
|
||||
Viewtype::Unknown => MessageViewtype::Unknown,
|
||||
Viewtype::Text => MessageViewtype::Text,
|
||||
Viewtype::Image => MessageViewtype::Image,
|
||||
Viewtype::Gif => MessageViewtype::Gif,
|
||||
Viewtype::Sticker => MessageViewtype::Sticker,
|
||||
Viewtype::Audio => MessageViewtype::Audio,
|
||||
Viewtype::Voice => MessageViewtype::Voice,
|
||||
Viewtype::Video => MessageViewtype::Video,
|
||||
Viewtype::File => MessageViewtype::File,
|
||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MessageViewtype> for Viewtype {
|
||||
fn from(viewtype: MessageViewtype) -> Self {
|
||||
match viewtype {
|
||||
MessageViewtype::Unknown => Viewtype::Unknown,
|
||||
MessageViewtype::Text => Viewtype::Text,
|
||||
MessageViewtype::Image => Viewtype::Image,
|
||||
MessageViewtype::Gif => Viewtype::Gif,
|
||||
MessageViewtype::Sticker => Viewtype::Sticker,
|
||||
MessageViewtype::Audio => Viewtype::Audio,
|
||||
MessageViewtype::Voice => Viewtype::Voice,
|
||||
MessageViewtype::Video => Viewtype::Video,
|
||||
MessageViewtype::File => Viewtype::File,
|
||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,229 +1,10 @@
|
||||
use deltachat::qr::Qr;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
pub mod account;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
pub mod contact;
|
||||
pub mod message;
|
||||
pub mod provider_info;
|
||||
pub mod webxdc;
|
||||
|
||||
pub fn color_int_to_hex_string(color: u32) -> String {
|
||||
format!("{:#08x}", color).replace("0x", "#")
|
||||
}
|
||||
|
||||
fn maybe_empty_string_to_option(string: String) -> Option<String> {
|
||||
if string.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum QrObject {
|
||||
AskVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
AskVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
FprOk {
|
||||
contact_id: u32,
|
||||
},
|
||||
FprMismatch {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
FprWithoutAddr {
|
||||
fingerprint: String,
|
||||
},
|
||||
Account {
|
||||
domain: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
Addr {
|
||||
contact_id: u32,
|
||||
draft: Option<String>,
|
||||
},
|
||||
Url {
|
||||
url: String,
|
||||
},
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
WithdrawVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
WithdrawVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
fn from(qr: Qr) -> Self {
|
||||
match qr {
|
||||
Qr::AskVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::AskVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::FprOk { contact_id } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::FprOk { contact_id }
|
||||
}
|
||||
Qr::FprMismatch { contact_id } => {
|
||||
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
|
||||
QrObject::FprMismatch { contact_id }
|
||||
}
|
||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||
Qr::Account { domain } => QrObject::Account { domain },
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
} => QrObject::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
},
|
||||
Qr::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::Addr { contact_id, draft }
|
||||
}
|
||||
Qr::Url { url } => QrObject::Url { url },
|
||||
Qr::Text { text } => QrObject::Text { text },
|
||||
Qr::WithdrawVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::WithdrawVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderInfo {
|
||||
pub before_login_hint: String,
|
||||
pub overview_page: String,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
use deltachat::{
|
||||
context::Context,
|
||||
message::{Message, MsgId},
|
||||
webxdc::WebxdcInfo,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::maybe_empty_string_to_option;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
|
||||
pub struct WebxdcMessageInfo {
|
||||
/// The name of the app.
|
||||
///
|
||||
/// Defaults to the filename if not set in the manifest.
|
||||
name: String,
|
||||
/// App icon file name.
|
||||
/// Defaults to an standard icon if nothing is set in the manifest.
|
||||
///
|
||||
/// To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
|
||||
///
|
||||
/// App icons should should be square,
|
||||
/// the implementations will add round corners etc. as needed.
|
||||
icon: String,
|
||||
/// if the Webxdc represents a document, then this is the name of the document
|
||||
document: Option<String>,
|
||||
/// short string describing the state of the app,
|
||||
/// sth. as "2 votes", "Highscore: 123",
|
||||
/// can be changed by the apps
|
||||
summary: Option<String>,
|
||||
/// URL where the source code of the Webxdc and other information can be found;
|
||||
/// defaults to an empty string.
|
||||
/// Implementations may offer an menu or a button to open this URL.
|
||||
source_code_url: Option<String>,
|
||||
}
|
||||
|
||||
impl WebxdcMessageInfo {
|
||||
pub async fn get_for_message(
|
||||
context: &Context,
|
||||
instance_message_id: MsgId,
|
||||
) -> anyhow::Result<Self> {
|
||||
let message = Message::load_from_db(context, instance_message_id).await?;
|
||||
let WebxdcInfo {
|
||||
name,
|
||||
icon,
|
||||
document,
|
||||
summary,
|
||||
source_code_url,
|
||||
} = message.get_webxdc_info(context).await?;
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
icon,
|
||||
document: maybe_empty_string_to_option(document),
|
||||
summary: maybe_empty_string_to_option(summary),
|
||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,36 @@
|
||||
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::{RpcClient, RpcSession};
|
||||
use yerpc::{MessageHandle, RpcHandle};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[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 api = CommandApi::new(accounts);
|
||||
let cmd_api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
let (client, mut rx) = RpcClient::new();
|
||||
let session = RpcSession::new(client, api);
|
||||
tokio::spawn({
|
||||
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(());
|
||||
@@ -34,7 +41,7 @@ mod tests {
|
||||
{
|
||||
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
||||
session.handle_incoming(request).await;
|
||||
handle.handle_message(request).await;
|
||||
let result = receiver.next().await;
|
||||
println!("{:?}", result);
|
||||
assert_eq!(result, Some(response.to_owned()));
|
||||
@@ -42,7 +49,7 @@ mod tests {
|
||||
{
|
||||
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
|
||||
session.handle_incoming(request).await;
|
||||
handle.handle_message(request).await;
|
||||
let result = receiver.next().await;
|
||||
println!("{:?}", result);
|
||||
assert_eq!(result, Some(response.to_owned()));
|
||||
@@ -50,43 +57,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_batch_set_config() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
let (client, mut rx) = RpcClient::new();
|
||||
let session = RpcSession::new(client, api);
|
||||
tokio::spawn({
|
||||
async move {
|
||||
while let Some(message) = rx.next().await {
|
||||
let message = serde_json::to_string(&message)?;
|
||||
sender.send(message).await?;
|
||||
}
|
||||
let res: Result<(), anyhow::Error> = Ok(());
|
||||
res
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
||||
session.handle_incoming(request).await;
|
||||
let result = receiver.next().await;
|
||||
assert_eq!(result, Some(response.to_owned()));
|
||||
}
|
||||
{
|
||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||
session.handle_incoming(request).await;
|
||||
let result = receiver.next().await;
|
||||
assert_eq!(result, Some(response.to_owned()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,44 @@
|
||||
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use yerpc::axum::handle_ws_rpc;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
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};
|
||||
|
||||
const DEFAULT_PORT: u16 = 20808;
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
#[async_std::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
env_logger::init();
|
||||
log::info!("Starting");
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "./accounts".to_string());
|
||||
let port = std::env::var("DC_PORT")
|
||||
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
|
||||
let accounts = Accounts::new(PathBuf::from("./accounts")).await.unwrap();
|
||||
let state = CommandApi::new(accounts);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/ws", get(handler))
|
||||
.layer(Extension(state.clone()));
|
||||
let mut app = tide::with_state(state.clone());
|
||||
app.at("/ws").get(yerpc_handler(request_handler));
|
||||
|
||||
tokio::spawn(async move {
|
||||
state.accounts.read().await.start_io().await;
|
||||
});
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
log::info!("JSON-RPC WebSocket server listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
state.accounts.read().await.start_io().await;
|
||||
app.listen("127.0.0.1:20808").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
||||
let (client, out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), api.clone());
|
||||
tokio::spawn(async move {
|
||||
let events = api.accounts.read().await.get_event_emitter().await;
|
||||
while let Some(event) = events.recv().await {
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
client.send_notification("event", Some(event)).await.ok();
|
||||
}
|
||||
});
|
||||
handle_ws_rpc(ws, out_receiver, session).await
|
||||
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(())
|
||||
}
|
||||
|
||||
2
deltachat-jsonrpc/typescript/.gitignore
vendored
2
deltachat-jsonrpc/typescript/.gitignore
vendored
@@ -4,5 +4,3 @@ test_dist
|
||||
coverage
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
docs
|
||||
accounts
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
accounts
|
||||
docs
|
||||
coverage
|
||||
yarn*
|
||||
package-lock.json
|
||||
@@ -1,13 +1,13 @@
|
||||
import { DeltaChat, DeltaChatEvent } from "../deltachat.js";
|
||||
|
||||
var SELECTED_ACCOUNT = 0;
|
||||
import { RawClient, RPC } from "./src/lib";
|
||||
import { WebsocketTransport, Request } from "yerpc";
|
||||
|
||||
type DeltaEvent = { id: string; contextId: number; field1: any; field2: any };
|
||||
var selectedAccount = 0;
|
||||
window.addEventListener("DOMContentLoaded", (_event) => {
|
||||
(window as any).selectDeltaAccount = (id: string) => {
|
||||
SELECTED_ACCOUNT = Number(id);
|
||||
selectedAccount = Number(id);
|
||||
window.dispatchEvent(new Event("account-changed"));
|
||||
};
|
||||
console.log('launch run script...')
|
||||
run().catch((err) => console.error("run failed", err));
|
||||
});
|
||||
|
||||
@@ -16,69 +16,67 @@ async function run() {
|
||||
const $side = document.getElementById("side")!;
|
||||
const $head = document.getElementById("header")!;
|
||||
|
||||
const client = new DeltaChat('ws://localhost:20808/ws')
|
||||
const transport = new WebsocketTransport("ws://localhost:20808/ws");
|
||||
const client = new RawClient(transport);
|
||||
|
||||
;(window as any).client = client.rpc;
|
||||
(window as any).client = client;
|
||||
|
||||
client.on("ALL", event => {
|
||||
onIncomingEvent(event)
|
||||
})
|
||||
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() {
|
||||
console.log('load accounts')
|
||||
const accounts = await client.rpc.getAllAccounts();
|
||||
console.log('accounts loaded', accounts)
|
||||
const accounts = await client.getAllAccounts();
|
||||
for (const account of accounts) {
|
||||
if (account.type === "Configured") {
|
||||
write(
|
||||
$head,
|
||||
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
||||
${account.id}: ${account.addr!}
|
||||
${account.addr!}
|
||||
</a> `
|
||||
);
|
||||
} else {
|
||||
write(
|
||||
$head,
|
||||
`<a href="#">
|
||||
${account.id}: (unconfigured)
|
||||
</a> `
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function listChatsForSelectedAccount() {
|
||||
clear($main);
|
||||
const selectedAccount = SELECTED_ACCOUNT
|
||||
const info = await client.rpc.getAccountInfo(selectedAccount);
|
||||
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.rpc.getChatlistEntries(
|
||||
const chats = await client.getChatlistEntries(
|
||||
selectedAccount,
|
||||
0,
|
||||
null,
|
||||
null
|
||||
);
|
||||
for (const [chatId, _messageId] of chats) {
|
||||
const chat = await client.rpc.chatlistGetFullChatById(
|
||||
const chat = await client.chatlistGetFullChatById(
|
||||
selectedAccount,
|
||||
chatId
|
||||
);
|
||||
write($main, `<h3>${chat.name}</h3>`);
|
||||
const messageIds = await client.rpc.messageListGetMessageIds(
|
||||
const messageIds = await client.messageListGetMessageIds(
|
||||
selectedAccount,
|
||||
chatId,
|
||||
0
|
||||
);
|
||||
const messages = await client.rpc.messageGetMessages(
|
||||
const messages = await client.messageGetMessages(
|
||||
selectedAccount,
|
||||
messageIds
|
||||
);
|
||||
@@ -88,12 +86,12 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
function onIncomingEvent(event: DeltaChatEvent) {
|
||||
function onIncomingEvent(event: DeltaEvent, name: string) {
|
||||
write(
|
||||
$side,
|
||||
`
|
||||
<p class="message">
|
||||
[<strong>${event.id}</strong> on account ${event.contextId}]<br>
|
||||
[<strong>${name}</strong> on account ${event.contextId}]<br>
|
||||
<em>f1:</em> ${JSON.stringify(event.field1)}<br>
|
||||
<em>f2:</em> ${JSON.stringify(event.field2)}
|
||||
</p>`
|
||||
@@ -1,26 +0,0 @@
|
||||
import { DeltaChat } from "../dist/deltachat.js";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const delta = new DeltaChat('ws://localhost:20808/ws');
|
||||
delta.on("event", (event) => {
|
||||
console.log("event", event.data);
|
||||
});
|
||||
|
||||
const email = process.argv[2]
|
||||
const password = process.argv[3]
|
||||
if (!email || !password) throw new Error('USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>')
|
||||
console.log(`creating acccount for ${email}`)
|
||||
const id = await delta.rpc.addAccount()
|
||||
console.log(`created account id ${id}`)
|
||||
await delta.rpc.setConfig(id, "addr", email);
|
||||
await delta.rpc.setConfig(id, "mail_pw", password);
|
||||
console.log('configuration updated')
|
||||
await delta.rpc.configure(id)
|
||||
console.log('account configured!')
|
||||
|
||||
const accounts = await delta.rpc.getAllAccounts();
|
||||
console.log("accounts", accounts);
|
||||
console.log("waiting for events...")
|
||||
}
|
||||
@@ -109,24 +109,6 @@ export class RawClient {
|
||||
return (this._transport.request('batch_set_config', [accountId, config] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
*/
|
||||
public setConfigFromQr(accountId: T.U32, qrContent: string): Promise<null> {
|
||||
return (this._transport.request('set_config_from_qr', [accountId, qrContent] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public checkQr(accountId: T.U32, qrContent: string): Promise<T.Qr> {
|
||||
return (this._transport.request('check_qr', [accountId, qrContent] as RPC.Params)) as Promise<T.Qr>;
|
||||
}
|
||||
|
||||
|
||||
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
|
||||
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
|
||||
@@ -152,35 +134,6 @@ export class RawClient {
|
||||
return (this._transport.request('stop_ongoing_process', [accountId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message IDs of all _fresh_ messages of any chat.
|
||||
* Typically used for implementing notification summaries
|
||||
* or badge counters e.g. on the app icon.
|
||||
* The list is already sorted and starts with the most recent fresh message.
|
||||
*
|
||||
* Messages belonging to muted chats or to the contact requests are not returned;
|
||||
* these messages should not be notified
|
||||
* and also badge counters should not include these messages.
|
||||
*
|
||||
* To get the number of fresh messages for a single chat, muted or not,
|
||||
* use `get_fresh_msg_cnt()`.
|
||||
*/
|
||||
public getFreshMsgs(accountId: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_fresh_msgs', [accountId] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of _fresh_ messages in a chat.
|
||||
* Typically used to implement a badge with a number in the chatlist.
|
||||
*
|
||||
* If the specified chat is muted,
|
||||
* the UI should show the badge counter "less obtrusive",
|
||||
* e.g. using "gray" instead of "red" color.
|
||||
*/
|
||||
public getFreshMsgCnt(accountId: T.U32, chatId: T.U32): Promise<T.Usize> {
|
||||
return (this._transport.request('get_fresh_msg_cnt', [accountId, chatId] as RPC.Params)) as Promise<T.Usize>;
|
||||
}
|
||||
|
||||
|
||||
public autocryptInitiateKeyTransfer(accountId: T.U32): Promise<string> {
|
||||
return (this._transport.request('autocrypt_initiate_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
|
||||
@@ -217,11 +170,6 @@ export class RawClient {
|
||||
}
|
||||
|
||||
|
||||
public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise<T.U32> {
|
||||
return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public messageListGetMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('message_list_get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
@@ -292,35 +240,6 @@ export class RawClient {
|
||||
return (this._transport.request('contacts_get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all message IDs of the given types in a chat.
|
||||
* Typically used to show a gallery.
|
||||
*
|
||||
* The list is already sorted and starts with the oldest message.
|
||||
* Clients should not try to re-sort the list as this would be an expensive action
|
||||
* and would result in inconsistencies between clients.
|
||||
*/
|
||||
public chatGetMedia(accountId: T.U32, chatId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
|
||||
public webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
|
||||
return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public webxdcGetStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
|
||||
return (this._transport.request('webxdc_get_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info from a webxdc message
|
||||
*/
|
||||
public messageGetWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
|
||||
return (this._transport.request('message_get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the messageid of the sent message
|
||||
*/
|
||||
|
||||
@@ -1,385 +1,15 @@
|
||||
// AUTO-GENERATED by typescript-type-def
|
||||
|
||||
export type U32 = number;
|
||||
export type Account =
|
||||
| ({ type: "Configured" } & {
|
||||
id: U32;
|
||||
displayName: string | null;
|
||||
addr: string | null;
|
||||
profileImage: string | null;
|
||||
color: string;
|
||||
})
|
||||
| ({ type: "Unconfigured" } & { id: U32 });
|
||||
export type ProviderInfo = {
|
||||
beforeLoginHint: string;
|
||||
overviewPage: string;
|
||||
status: U32;
|
||||
};
|
||||
export type Qr =
|
||||
| ({ type: "askVerifyContact" } & {
|
||||
contact_id: U32;
|
||||
fingerprint: string;
|
||||
invitenumber: string;
|
||||
authcode: string;
|
||||
})
|
||||
| ({ type: "askVerifyGroup" } & {
|
||||
grpname: string;
|
||||
grpid: string;
|
||||
contact_id: U32;
|
||||
fingerprint: string;
|
||||
invitenumber: string;
|
||||
authcode: string;
|
||||
})
|
||||
| ({ type: "fprOk" } & { contact_id: U32 })
|
||||
| ({ type: "fprMismatch" } & { contact_id: U32 | null })
|
||||
| ({ type: "fprWithoutAddr" } & { fingerprint: string })
|
||||
| ({ type: "account" } & { domain: string })
|
||||
| ({ type: "webrtcInstance" } & { domain: string; instance_pattern: string })
|
||||
| ({ type: "addr" } & { contact_id: U32 })
|
||||
| ({ type: "url" } & { url: string })
|
||||
| ({ type: "text" } & { text: string })
|
||||
| ({ type: "withdrawVerifyContact" } & {
|
||||
contact_id: U32;
|
||||
fingerprint: string;
|
||||
invitenumber: string;
|
||||
authcode: string;
|
||||
})
|
||||
| ({ type: "withdrawVerifyGroup" } & {
|
||||
grpname: string;
|
||||
grpid: string;
|
||||
contact_id: U32;
|
||||
fingerprint: string;
|
||||
invitenumber: string;
|
||||
authcode: string;
|
||||
})
|
||||
| ({ type: "reviveVerifyContact" } & {
|
||||
contact_id: U32;
|
||||
fingerprint: string;
|
||||
invitenumber: string;
|
||||
authcode: string;
|
||||
})
|
||||
| ({ type: "reviveVerifyGroup" } & {
|
||||
grpname: string;
|
||||
grpid: string;
|
||||
contact_id: U32;
|
||||
fingerprint: string;
|
||||
invitenumber: string;
|
||||
authcode: string;
|
||||
});
|
||||
export type Usize = number;
|
||||
export type ChatListEntry = [U32, U32];
|
||||
export type I64 = number;
|
||||
export type ChatListItemFetchResult =
|
||||
| ({ type: "ChatListItem" } & {
|
||||
id: U32;
|
||||
name: string;
|
||||
avatarPath: string | null;
|
||||
color: string;
|
||||
lastUpdated: I64 | null;
|
||||
summaryText1: string;
|
||||
summaryText2: string;
|
||||
summaryStatus: U32;
|
||||
isProtected: boolean;
|
||||
isGroup: boolean;
|
||||
freshMessageCounter: Usize;
|
||||
isSelfTalk: boolean;
|
||||
isDeviceTalk: boolean;
|
||||
isSendingLocation: boolean;
|
||||
isSelfInGroup: boolean;
|
||||
isArchived: boolean;
|
||||
isPinned: boolean;
|
||||
isMuted: boolean;
|
||||
isContactRequest: boolean;
|
||||
/**
|
||||
* contact id if this is a dm chat (for view profile entry in context menu)
|
||||
*/
|
||||
dmChatContact: U32 | null;
|
||||
})
|
||||
| { type: "ArchiveLink" }
|
||||
| ({ type: "Error" } & { id: U32; error: string });
|
||||
export type Contact = {
|
||||
address: string;
|
||||
color: string;
|
||||
authName: string;
|
||||
status: string;
|
||||
displayName: string;
|
||||
id: U32;
|
||||
name: string;
|
||||
profileImage: string | null;
|
||||
nameAndAddr: string;
|
||||
isBlocked: boolean;
|
||||
isVerified: boolean;
|
||||
};
|
||||
export type FullChat = {
|
||||
id: U32;
|
||||
name: string;
|
||||
isProtected: boolean;
|
||||
profileImage: string | null;
|
||||
archived: boolean;
|
||||
chatType: U32;
|
||||
isUnpromoted: boolean;
|
||||
isSelfTalk: boolean;
|
||||
contacts: Contact[];
|
||||
contactIds: U32[];
|
||||
color: string;
|
||||
freshMessageCounter: Usize;
|
||||
isContactRequest: boolean;
|
||||
isDeviceChat: boolean;
|
||||
selfInGroup: boolean;
|
||||
isMuted: boolean;
|
||||
ephemeralTimer: U32;
|
||||
canSend: boolean;
|
||||
};
|
||||
export type Viewtype =
|
||||
| "Unknown"
|
||||
/**
|
||||
* Text message.
|
||||
*/
|
||||
| "Text"
|
||||
/**
|
||||
* Image message.
|
||||
* If the image is an animated GIF, the type `Viewtype.Gif` should be used.
|
||||
*/
|
||||
| "Image"
|
||||
/**
|
||||
* Animated GIF message.
|
||||
*/
|
||||
| "Gif"
|
||||
/**
|
||||
* Message containing a sticker, similar to image.
|
||||
* If possible, the ui should display the image without borders in a transparent way.
|
||||
* A click on a sticker will offer to install the sticker set in some future.
|
||||
*/
|
||||
| "Sticker"
|
||||
/**
|
||||
* Message containing an Audio file.
|
||||
*/
|
||||
| "Audio"
|
||||
/**
|
||||
* A voice message that was directly recorded by the user.
|
||||
* For all other audio messages, the type `Viewtype.Audio` should be used.
|
||||
*/
|
||||
| "Voice"
|
||||
/**
|
||||
* Video messages.
|
||||
*/
|
||||
| "Video"
|
||||
/**
|
||||
* Message containing any file, eg. a PDF.
|
||||
*/
|
||||
| "File"
|
||||
/**
|
||||
* Message is an invitation to a videochat.
|
||||
*/
|
||||
| "VideochatInvitation"
|
||||
/**
|
||||
* Message is an webxdc instance.
|
||||
*/
|
||||
| "Webxdc";
|
||||
export type I32 = number;
|
||||
export type U64 = number;
|
||||
export type Message = {
|
||||
id: U32;
|
||||
chatId: U32;
|
||||
fromId: U32;
|
||||
quotedText: string | null;
|
||||
quotedMessageId: U32 | null;
|
||||
text: string | null;
|
||||
hasLocation: boolean;
|
||||
hasHtml: boolean;
|
||||
viewType: Viewtype;
|
||||
state: U32;
|
||||
timestamp: I64;
|
||||
sortTimestamp: I64;
|
||||
receivedTimestamp: I64;
|
||||
hasDeviatingTimestamp: boolean;
|
||||
subject: string;
|
||||
showPadlock: boolean;
|
||||
isSetupmessage: boolean;
|
||||
isInfo: boolean;
|
||||
isForwarded: boolean;
|
||||
duration: I32;
|
||||
dimensionsHeight: I32;
|
||||
dimensionsWidth: I32;
|
||||
videochatType: U32 | null;
|
||||
videochatUrl: string | null;
|
||||
overrideSenderName: string | null;
|
||||
sender: Contact;
|
||||
setupCodeBegin: string | null;
|
||||
file: string | null;
|
||||
fileMime: string | null;
|
||||
fileBytes: U64;
|
||||
fileName: string | null;
|
||||
};
|
||||
export type WebxdcMessageInfo = {
|
||||
/**
|
||||
* The name of the app.
|
||||
*
|
||||
* Defaults to the filename if not set in the manifest.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* App icon file name.
|
||||
* Defaults to an standard icon if nothing is set in the manifest.
|
||||
*
|
||||
* To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
|
||||
*
|
||||
* App icons should should be square,
|
||||
* the implementations will add round corners etc. as needed.
|
||||
*/
|
||||
icon: string;
|
||||
/**
|
||||
* if the Webxdc represents a document, then this is the name of the document
|
||||
*/
|
||||
document: string | null;
|
||||
/**
|
||||
* short string describing the state of the app,
|
||||
* sth. as "2 votes", "Highscore: 123",
|
||||
* can be changed by the apps
|
||||
*/
|
||||
summary: string | null;
|
||||
/**
|
||||
* URL where the source code of the Webxdc and other information can be found;
|
||||
* defaults to an empty string.
|
||||
* Implementations may offer an menu or a button to open this URL.
|
||||
*/
|
||||
sourceCodeUrl: string | null;
|
||||
};
|
||||
export type __AllTyps = [
|
||||
string,
|
||||
boolean,
|
||||
Record<string, string>,
|
||||
U32,
|
||||
U32,
|
||||
null,
|
||||
U32[],
|
||||
U32,
|
||||
null,
|
||||
U32 | null,
|
||||
Account[],
|
||||
U32,
|
||||
Account,
|
||||
U32,
|
||||
string,
|
||||
ProviderInfo | null,
|
||||
U32,
|
||||
boolean,
|
||||
U32,
|
||||
Record<string, string>,
|
||||
U32,
|
||||
string,
|
||||
string | null,
|
||||
null,
|
||||
U32,
|
||||
Record<string, string | null>,
|
||||
null,
|
||||
U32,
|
||||
string,
|
||||
null,
|
||||
U32,
|
||||
string,
|
||||
Qr,
|
||||
U32,
|
||||
string,
|
||||
string | null,
|
||||
U32,
|
||||
string[],
|
||||
Record<string, string | null>,
|
||||
U32,
|
||||
null,
|
||||
U32,
|
||||
null,
|
||||
U32,
|
||||
U32[],
|
||||
U32,
|
||||
U32,
|
||||
Usize,
|
||||
U32,
|
||||
string,
|
||||
U32,
|
||||
U32,
|
||||
string,
|
||||
null,
|
||||
U32,
|
||||
U32 | null,
|
||||
string | null,
|
||||
U32 | null,
|
||||
ChatListEntry[],
|
||||
U32,
|
||||
ChatListEntry[],
|
||||
Record<U32, ChatListItemFetchResult>,
|
||||
U32,
|
||||
U32,
|
||||
FullChat,
|
||||
U32,
|
||||
U32,
|
||||
null,
|
||||
U32,
|
||||
U32,
|
||||
null,
|
||||
U32,
|
||||
string,
|
||||
string,
|
||||
U32,
|
||||
U32,
|
||||
U32,
|
||||
U32,
|
||||
U32[],
|
||||
U32,
|
||||
U32,
|
||||
Message,
|
||||
U32,
|
||||
U32[],
|
||||
Record<U32, Message>,
|
||||
U32,
|
||||
U32,
|
||||
Contact,
|
||||
U32,
|
||||
string,
|
||||
string | null,
|
||||
U32,
|
||||
U32,
|
||||
U32,
|
||||
U32,
|
||||
U32,
|
||||
U32,
|
||||
null,
|
||||
U32,
|
||||
U32,
|
||||
null,
|
||||
U32,
|
||||
Contact[],
|
||||
U32,
|
||||
U32,
|
||||
string | null,
|
||||
U32[],
|
||||
U32,
|
||||
U32,
|
||||
string | null,
|
||||
Contact[],
|
||||
U32,
|
||||
U32[],
|
||||
Record<U32, Contact>,
|
||||
U32,
|
||||
U32,
|
||||
Viewtype,
|
||||
Viewtype | null,
|
||||
Viewtype | null,
|
||||
U32[],
|
||||
U32,
|
||||
U32,
|
||||
string,
|
||||
string,
|
||||
null,
|
||||
U32,
|
||||
U32,
|
||||
U32,
|
||||
string,
|
||||
U32,
|
||||
U32,
|
||||
WebxdcMessageInfo,
|
||||
U32,
|
||||
string,
|
||||
U32,
|
||||
U32
|
||||
];
|
||||
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];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>DeltaChat JSON-RPC example</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
@@ -42,7 +41,6 @@
|
||||
<script type="module" src="dist/example.bundle.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DeltaChat JSON-RPC example</h1>
|
||||
<div class="grid">
|
||||
<div id="header"></div>
|
||||
<div id="main"></div>
|
||||
@@ -1,14 +1,13 @@
|
||||
import { DeltaChat } from "../dist/deltachat.js";
|
||||
import { Deltachat } from "./dist/deltachat.js";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const delta = new DeltaChat();
|
||||
delta.on("event", (event) => {
|
||||
const delta = new Deltachat();
|
||||
delta.addEventListener("event", (event) => {
|
||||
console.log("event", event.data);
|
||||
});
|
||||
|
||||
const accounts = await delta.rpc.getAllAccounts();
|
||||
console.log("accounts", accounts);
|
||||
console.log("waiting for events...")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "deltachat-jsonrpc-client",
|
||||
"name": "@deltachat/jsonrpc-client",
|
||||
"version": "0.1.0",
|
||||
"main": "dist/deltachat.js",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
@@ -9,27 +9,22 @@
|
||||
"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",
|
||||
"build": "run-s generate-bindings build:tsc build:bundle",
|
||||
"build:tsc": "tsc",
|
||||
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||
"example": "run-s build example:build example:start",
|
||||
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
|
||||
"example:start": "http-server .",
|
||||
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
||||
"test": "run-s test:prepare test:run-coverage test:report-coverage",
|
||||
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
|
||||
"test:run": "mocha dist/test",
|
||||
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test",
|
||||
"test:report-coverage": "node report_api_coverage.mjs",
|
||||
"docs": "typedoc --out docs deltachat.ts"
|
||||
"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.3.3"
|
||||
"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",
|
||||
@@ -37,14 +32,9 @@
|
||||
"@types/ws": "^7.2.4",
|
||||
"c8": "^7.10.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esbuild": "^0.14.11",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^9.1.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.6.2",
|
||||
"typedoc": "^0.23.2",
|
||||
"typescript": "^4.5.5",
|
||||
"ws": "^8.5.0"
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { readFileSync } from "fs";
|
||||
// only checks for the coverge of the api functions in bindings.ts for now
|
||||
const generatedFile = "typescript/generated/client.ts";
|
||||
const 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(generatedFile))];
|
||||
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/" + generatedFile + ".html",
|
||||
"./coverage/" + generated_file + ".html",
|
||||
"utf8"
|
||||
);
|
||||
const uncoveredLines = htmlCoverage
|
||||
@@ -22,7 +22,7 @@ console.log(
|
||||
uncoveredFunctions
|
||||
.map((uF) => fnMap.find(({ name }) => name === uF))
|
||||
.map(
|
||||
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
|
||||
({ name, line }) => `.${name.padEnd(40)} (${generated_file}:${line})`
|
||||
)
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EventTypeName } from "../generated/events.js";
|
||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
|
||||
export type DeltaChatEvent = {
|
||||
export type DeltachatEvent = {
|
||||
id: EventTypeName;
|
||||
contextId: number;
|
||||
field1: any;
|
||||
@@ -13,22 +13,21 @@ export type DeltaChatEvent = {
|
||||
};
|
||||
export type Events = Record<
|
||||
EventTypeName | "ALL",
|
||||
(event: DeltaChatEvent) => void
|
||||
(event: DeltachatEvent) => void
|
||||
>;
|
||||
|
||||
export class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>
|
||||
export class BaseDeltachat<
|
||||
Transport extends BaseTransport
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
private contextEmitters: TinyEmitter<Events>[] = [];
|
||||
constructor(public transport: Transport) {
|
||||
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;
|
||||
const event = request.params! as DeltachatEvent;
|
||||
this.emit(event.id, event);
|
||||
this.emit("ALL", event);
|
||||
|
||||
@@ -44,6 +43,8 @@ export class BaseDeltaChat<
|
||||
return await this.rpc.getAllAccounts();
|
||||
}
|
||||
|
||||
private contextEmitters: TinyEmitter<Events>[] = [];
|
||||
|
||||
getContextEvents(account_id: number) {
|
||||
if (this.contextEmitters[account_id]) {
|
||||
return this.contextEmitters[account_id];
|
||||
@@ -61,17 +62,16 @@ export type Opts = {
|
||||
export const DEFAULT_OPTS: Opts = {
|
||||
url: "ws://localhost:20808/ws",
|
||||
};
|
||||
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||
export class Deltachat extends BaseDeltachat<WebsocketTransport> {
|
||||
opts: Opts;
|
||||
close() {
|
||||
this.transport.close();
|
||||
this.transport._socket.close();
|
||||
}
|
||||
constructor(opts?: Opts | string) {
|
||||
constructor(opts: Opts | string | undefined) {
|
||||
if (typeof opts === "string") opts = { url: opts };
|
||||
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
|
||||
else opts = { ...DEFAULT_OPTS };
|
||||
const transport = new WebsocketTransport(opts.url)
|
||||
super(transport);
|
||||
super(new WebsocketTransport(opts.url));
|
||||
this.opts = opts;
|
||||
}
|
||||
}
|
||||
|
||||
1
deltachat-jsonrpc/typescript/test/README.md
Normal file
1
deltachat-jsonrpc/typescript/test/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# tests need to be ported to new API
|
||||
@@ -2,55 +2,59 @@ import { strictEqual } from "assert";
|
||||
import chai, { assert, expect } from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
chai.use(chaiAsPromised);
|
||||
import { DeltaChat } from "../deltachat.js";
|
||||
import { Deltachat } from "../dist/deltachat.js";
|
||||
|
||||
import {
|
||||
RpcServerHandle,
|
||||
startServer,
|
||||
CMD_API_Server_Handle,
|
||||
CMD_API_SERVER_PORT,
|
||||
startCMD_API_Server,
|
||||
} from "./test_base.js";
|
||||
|
||||
describe("basic tests", () => {
|
||||
let serverHandle: RpcServerHandle;
|
||||
let dc: DeltaChat;
|
||||
let server_handle: CMD_API_Server_Handle;
|
||||
let dc: Deltachat;
|
||||
|
||||
before(async () => {
|
||||
serverHandle = await startServer();
|
||||
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(serverHandle.url)
|
||||
// dc.on("ALL", (event) => {
|
||||
|
||||
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 serverHandle.close();
|
||||
await server_handle.close();
|
||||
});
|
||||
|
||||
it("check email address validity", async () => {
|
||||
const validAddresses = [
|
||||
it("check email", async () => {
|
||||
const positive_test_cases = [
|
||||
"email@example.com",
|
||||
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
||||
];
|
||||
const invalidAddresses = ["email@", "example.com", "emai221"];
|
||||
const negative_test_cases = ["email@", "example.com", "emai221"];
|
||||
|
||||
expect(
|
||||
await Promise.all(
|
||||
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||
positive_test_cases.map((email) => dc.rpc.checkEmailValidity(email))
|
||||
)
|
||||
).to.not.contain(false);
|
||||
|
||||
expect(
|
||||
await Promise.all(
|
||||
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||
negative_test_cases.map((email) => dc.rpc.checkEmailValidity(email))
|
||||
)
|
||||
).to.not.contain(true);
|
||||
});
|
||||
|
||||
it("system info", async () => {
|
||||
const systemInfo = await dc.rpc.getSystemInfo();
|
||||
expect(systemInfo).to.contain.keys([
|
||||
const system_info = await dc.rpc.getSystemInfo();
|
||||
expect(system_info).to.contain.keys([
|
||||
"arch",
|
||||
"num_cpus",
|
||||
"deltachat_core_version",
|
||||
@@ -60,7 +64,7 @@ describe("basic tests", () => {
|
||||
|
||||
describe("account managment", () => {
|
||||
it("should create account", async () => {
|
||||
const res = await dc.rpc.addAccount();
|
||||
await dc.rpc.addAccount();
|
||||
assert((await dc.rpc.getAllAccountIds()).length === 1);
|
||||
});
|
||||
|
||||
@@ -79,55 +83,55 @@ describe("basic tests", () => {
|
||||
});
|
||||
|
||||
describe("contact managment", function () {
|
||||
let accountId: number;
|
||||
let acc: number;
|
||||
before(async () => {
|
||||
accountId = await dc.rpc.addAccount();
|
||||
acc = await dc.rpc.addAccount();
|
||||
});
|
||||
it("should block and unblock contact", async function () {
|
||||
it("block and unblock contact", async function () {
|
||||
const contactId = await dc.rpc.contactsCreateContact(
|
||||
accountId,
|
||||
acc,
|
||||
"example@delta.chat",
|
||||
null
|
||||
);
|
||||
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
|
||||
expect((await dc.rpc.contactsGetContact(acc, contactId)).is_blocked).to.be
|
||||
.false;
|
||||
await dc.rpc.contactsBlock(accountId, contactId);
|
||||
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
|
||||
await dc.rpc.contactsBlock(acc, contactId);
|
||||
expect((await dc.rpc.contactsGetContact(acc, contactId)).is_blocked).to.be
|
||||
.true;
|
||||
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(1);
|
||||
await dc.rpc.contactsUnblock(accountId, contactId);
|
||||
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
|
||||
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(accountId)).to.have.length(0);
|
||||
expect(await dc.rpc.contactsGetBlocked(acc)).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("configuration", function () {
|
||||
let accountId: number;
|
||||
let acc: number;
|
||||
before(async () => {
|
||||
accountId = await dc.rpc.addAccount();
|
||||
acc = await dc.rpc.addAccount();
|
||||
});
|
||||
|
||||
it("set and retrive", async function () {
|
||||
await dc.rpc.setConfig(accountId, "addr", "valid@email");
|
||||
assert((await dc.rpc.getConfig(accountId, "addr")) == "valid@email");
|
||||
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(accountId, "invalid_key", "some value")).to.be
|
||||
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(accountId, "invalid_key")).to.be.eventually
|
||||
await expect(dc.rpc.getConfig(acc, "invalid_key")).to.be.eventually
|
||||
.rejected;
|
||||
});
|
||||
it("set and retrive ui.*", async function () {
|
||||
await dc.rpc.setConfig(accountId, "ui.chat_bg", "color:red");
|
||||
assert((await dc.rpc.getConfig(accountId, "ui.chat_bg")) == "color:red");
|
||||
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(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
|
||||
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 () {
|
||||
@@ -135,8 +139,8 @@ describe("basic tests", () => {
|
||||
"ui.chat_bg": "color:green",
|
||||
"ui.enter_key_sends": "true",
|
||||
};
|
||||
await dc.rpc.batchSetConfig(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
|
||||
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 () {
|
||||
@@ -146,8 +150,8 @@ describe("basic tests", () => {
|
||||
addr: "valid2@email",
|
||||
mail_pw: "123456",
|
||||
};
|
||||
await dc.rpc.batchSetConfig(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
|
||||
await dc.rpc.batchSetConfig(acc, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(acc, Object.keys(config));
|
||||
expect(retrieved).to.deep.equal(config);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { assert, expect } from "chai";
|
||||
import { DeltaChat, DeltaChatEvent, EventTypeName } from "../deltachat.js";
|
||||
import { Deltachat, DeltachatEvent, EventTypeName } from "../dist/deltachat.js";
|
||||
import {
|
||||
RpcServerHandle,
|
||||
CMD_API_Server_Handle,
|
||||
CMD_API_SERVER_PORT,
|
||||
createTempUser,
|
||||
startServer,
|
||||
startCMD_API_Server,
|
||||
} from "./test_base.js";
|
||||
|
||||
const EVENT_TIMEOUT = 20000
|
||||
|
||||
describe("online tests", function () {
|
||||
let serverHandle: RpcServerHandle;
|
||||
let dc: DeltaChat;
|
||||
let account1: { email: string; password: string };
|
||||
let server_handle: CMD_API_Server_Handle;
|
||||
let dc: Deltachat;
|
||||
let account: { email: string; password: string };
|
||||
let account2: { email: string; password: string };
|
||||
let accountId1: number, accountId2: number;
|
||||
let acc1: number, acc2: number;
|
||||
|
||||
before(async function () {
|
||||
this.timeout(12000)
|
||||
@@ -30,15 +29,17 @@ describe("online tests", function () {
|
||||
);
|
||||
this.skip();
|
||||
}
|
||||
serverHandle = await startServer();
|
||||
dc = new DeltaChat(serverHandle.url)
|
||||
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);
|
||||
});
|
||||
|
||||
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
||||
if (!account1 || !account1.email || !account1.password) {
|
||||
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"
|
||||
);
|
||||
@@ -56,116 +57,117 @@ describe("online tests", function () {
|
||||
|
||||
after(async () => {
|
||||
dc && dc.close();
|
||||
serverHandle && (await serverHandle.close());
|
||||
server_handle && (await server_handle.close());
|
||||
});
|
||||
|
||||
let accountsConfigured = false;
|
||||
let are_configured = false;
|
||||
|
||||
it("configure test accounts", async function () {
|
||||
this.timeout(40000);
|
||||
this.timeout(20000);
|
||||
|
||||
accountId1 = await dc.rpc.addAccount();
|
||||
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
||||
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
||||
await dc.rpc.configure(accountId1);
|
||||
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);
|
||||
|
||||
accountId2 = await dc.rpc.addAccount();
|
||||
await dc.rpc.batchSetConfig(accountId2, {
|
||||
acc2 = await dc.rpc.addAccount();
|
||||
await dc.rpc.batchSetConfig(acc2, {
|
||||
addr: account2.email,
|
||||
mail_pw: account2.password,
|
||||
});
|
||||
await dc.rpc.configure(accountId2)
|
||||
accountsConfigured = true;
|
||||
|
||||
await Promise.all([configure_promise, dc.rpc.configure(acc2)]);
|
||||
are_configured = true;
|
||||
});
|
||||
|
||||
it("send and recieve text message", async function () {
|
||||
if (!accountsConfigured) {
|
||||
if (!are_configured) {
|
||||
this.skip();
|
||||
}
|
||||
this.timeout(15000);
|
||||
|
||||
const contactId = await dc.rpc.contactsCreateContact(
|
||||
accountId1,
|
||||
acc1,
|
||||
account2.email,
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(acc1, contactId);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
waitForEvent(dc, "MsgsChanged", acc2),
|
||||
waitForEvent(dc, "IncomingMsg", acc2),
|
||||
]);
|
||||
|
||||
await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId);
|
||||
dc.rpc.miscSendTextMessage(acc1, "Hello", chatId);
|
||||
const { field1: chatIdOnAccountB } = await eventPromise;
|
||||
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
|
||||
await dc.rpc.acceptChat(acc2, chatIdOnAccountB);
|
||||
const messageList = await dc.rpc.messageListGetMessageIds(
|
||||
accountId2,
|
||||
acc2,
|
||||
chatIdOnAccountB,
|
||||
0
|
||||
);
|
||||
|
||||
expect(messageList).have.length(1);
|
||||
const message = await dc.rpc.messageGetMessage(accountId2, messageList[0]);
|
||||
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 (!accountsConfigured) {
|
||||
if (!are_configured) {
|
||||
this.skip();
|
||||
}
|
||||
this.timeout(10000);
|
||||
|
||||
// send message from A to B
|
||||
const contactId = await dc.rpc.contactsCreateContact(
|
||||
accountId1,
|
||||
acc1,
|
||||
account2.email,
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(acc1, contactId);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
waitForEvent(dc, "MsgsChanged", acc2),
|
||||
waitForEvent(dc, "IncomingMsg", acc2),
|
||||
]);
|
||||
dc.rpc.miscSendTextMessage(accountId1, "Hello2", chatId);
|
||||
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(accountId2, chatIdOnAccountB);
|
||||
await dc.rpc.acceptChat(acc2, chatIdOnAccountB);
|
||||
const messageList = await dc.rpc.messageListGetMessageIds(
|
||||
accountId2,
|
||||
acc2,
|
||||
chatIdOnAccountB,
|
||||
0
|
||||
);
|
||||
const message = await dc.rpc.messageGetMessage(
|
||||
accountId2,
|
||||
acc2,
|
||||
messageList.reverse()[0]
|
||||
);
|
||||
expect(message.text).equal("Hello2");
|
||||
// Send message back from B to A
|
||||
const eventPromise2 = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId1),
|
||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||
waitForEvent(dc, "MsgsChanged", acc1),
|
||||
waitForEvent(dc, "IncomingMsg", acc1),
|
||||
]);
|
||||
dc.rpc.miscSendTextMessage(accountId2, "super secret message", chatId);
|
||||
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(accountId1, chatId, 0)
|
||||
await dc.rpc.messageListGetMessageIds(acc1, chatId, 0)
|
||||
).reverse()[0];
|
||||
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
|
||||
const message2 = await dc.rpc.messageGetMessage(acc1, messageId);
|
||||
expect(message2.text).equal("super secret message");
|
||||
expect(message2.showPadlock).equal(true);
|
||||
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?.overviewPage).to.equal(
|
||||
expect(info?.overview_page).to.equal(
|
||||
"https://providers.delta.chat/example-com"
|
||||
);
|
||||
expect(info?.status).to.equal(3);
|
||||
@@ -179,24 +181,23 @@ describe("online tests", function () {
|
||||
});
|
||||
});
|
||||
|
||||
type event_data = {
|
||||
contextId: number;
|
||||
id: EventTypeName;
|
||||
[key: string]: any;
|
||||
};
|
||||
async function waitForEvent(
|
||||
dc: DeltaChat,
|
||||
eventType: EventTypeName,
|
||||
accountId: number,
|
||||
timeout: number = EVENT_TIMEOUT
|
||||
): Promise<DeltaChatEvent> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rejectTimeout = setTimeout(
|
||||
() => reject(new Error('Timeout reached before event came in')),
|
||||
timeout
|
||||
)
|
||||
const callback = (event: DeltaChatEvent) => {
|
||||
if (event.contextId == accountId) {
|
||||
dc.off(eventType, callback);
|
||||
clearTimeout(rejectTimeout)
|
||||
resolve(event);
|
||||
dc: 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(eventType, callback);
|
||||
dc.on(event, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,90 +1,28 @@
|
||||
import { tmpdir } from "os";
|
||||
import { join, resolve } from "path";
|
||||
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 */
|
||||
|
||||
export const RPC_SERVER_PORT = 20808;
|
||||
|
||||
export type RpcServerHandle = {
|
||||
url: string,
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
export async function startServer(port: number = RPC_SERVER_PORT): Promise<RpcServerHandle> {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
||||
|
||||
const pathToServerBinary = resolve(join(await getTargetDir(), "debug/deltachat-jsonrpc-server"));
|
||||
console.log('using server binary: ' + pathToServerBinary);
|
||||
|
||||
if (!existsSync(pathToServerBinary)) {
|
||||
throw new Error(
|
||||
"server executable does not exist, you need to build it first" +
|
||||
"\nserver executable not found at " +
|
||||
pathToServerBinary
|
||||
);
|
||||
}
|
||||
|
||||
const server = spawn(pathToServerBinary, {
|
||||
cwd: tmpDir,
|
||||
env: {
|
||||
RUST_LOG: process.env.RUST_LOG || "info",
|
||||
DC_PORT: '' + port
|
||||
},
|
||||
});
|
||||
let shouldClose = false;
|
||||
|
||||
server.on("exit", () => {
|
||||
if (shouldClose) {
|
||||
return;
|
||||
}
|
||||
throw new Error("Server quit");
|
||||
});
|
||||
|
||||
server.stderr.pipe(process.stderr);
|
||||
server.stdout.pipe(process.stdout)
|
||||
|
||||
const url = `ws://localhost:${port}/ws`
|
||||
|
||||
return {
|
||||
url,
|
||||
close: async () => {
|
||||
shouldClose = true;
|
||||
if (!server.kill()) {
|
||||
console.log("server termination failed");
|
||||
}
|
||||
await rm(tmpDir, { recursive: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTempUser(url: string) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"cache-control": "no-cache",
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error('Received invalid response')
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function getTargetDir(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((res, rej) => {
|
||||
exec(
|
||||
"cargo metadata --no-deps --format-version 1",
|
||||
(error, stdout, _stderr) => {
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log("error", error);
|
||||
reject(error);
|
||||
rej(error);
|
||||
} else {
|
||||
try {
|
||||
const json = JSON.parse(stdout);
|
||||
resolve(json.target_directory);
|
||||
res(json.target_directory);
|
||||
} catch (error) {
|
||||
console.log("json error", error);
|
||||
reject(error);
|
||||
rej(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,3 +30,66 @@ function getTargetDir(): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
1
deltachat-jsonrpc/typescript/test/ts_helpers.ts
Normal file
1
deltachat-jsonrpc/typescript/test/ts_helpers.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type unwrapPromise<T> = T extends Promise<infer U> ? U : never;
|
||||
17
deltachat-jsonrpc/typescript/test/tsconfig.json
Normal file
17
deltachat-jsonrpc/typescript/test/tsconfig.json
Normal 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
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"outDir": "dist",
|
||||
"lib": ["ES2017", "dom"],
|
||||
"target": "ES2017",
|
||||
"module": "es2020",
|
||||
"module": "es2015",
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["*.ts", "example/*.ts", "test/*.ts"],
|
||||
"include": ["*.ts"],
|
||||
"compileOnSave": false
|
||||
}
|
||||
|
||||
@@ -11,49 +11,26 @@ Changes to the UIs
|
||||
Changes in the core
|
||||
-------------------
|
||||
|
||||
- [x] We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
- DONE: We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
||||
|
||||
- [x] If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
- DONE: If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
||||
|
||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
||||
|
||||
- The key stays the same.
|
||||
|
||||
- [x] No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
||||
- No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
||||
|
||||
- [ ] When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
||||
- When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
||||
|
||||
- [x] ([#3385](https://github.com/deltachat/deltachat-core-rust/pull/3385)) When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
||||
- When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
||||
AND there is a `Chat-Version` header\
|
||||
AND the message is signed correctly
|
||||
AND the From address is (also) in the encrypted (and therefore signed) headers <sup>[[1]](#myfootnote1)</sup>\
|
||||
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
|
||||
|
||||
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
|
||||
|
||||
<a name="myfootnote1">[1]</a>: Without this check, an attacker could replay a message from Alice to Bob. Then Bob's device would do an AEAP transition from Alice's to the attacker's address, allowing for easier phishing.
|
||||
|
||||
<details>
|
||||
<summary>More details about this</summary>
|
||||
Suppose Alice sends a message to Evil (or to a group with both Evil and Bob). Evil then forwards the message to Bob, changing the From and To headers (and if necessary Message-Id) and replacing `addr=alice@example.org;` in the autocrypt header with `addr=evil@example.org;`.
|
||||
|
||||
Then Bob's device sees that there is a message which is signed by Alice's key and comes from Evil's address and would do the AEAP transition, i.e. replace Alice with Evil in all groups and show a message "Alice changed their address from alice@example.org to evil@example.org". Disadvantages for Evil are that Bob's message will be shown on Alice's device, possibly creating confusion/suspicion, and that the usual "Setup changed for..." message will be shown the next time Evil sends a message (because Evil doesn't know Alice's private key).
|
||||
|
||||
Possible mitigations:
|
||||
- if we make the AEAP device message sth. like "Automatically removed alice@example.org and added evil@example.org", then this will create more suspicion, making the phishing harder (we didn't talk about what what the wording should be at all yet).
|
||||
- Add something similar to replay protection to our Autocrypt implementation. This could be done e.g. by adding a second `From` header to the protected headers. If it's present, the receiver then requires it to be the same as the outer `From`, and if it's not present, we don't do AEAP --> **That's what we implemented**
|
||||
|
||||
Note that usually a mail is signed by a key that has a UID matching the from address.
|
||||
|
||||
That's not mandatory for Autocrypt (and in fact, we just keep the old UID when changing the self address, so with AEAP the UID will actually be different than the from address sometimes)
|
||||
|
||||
https://autocrypt.org/level1.html#openpgp-based-key-data says:
|
||||
> The content of the user id packet is only decorative
|
||||
|
||||
</details>
|
||||
|
||||
### Notes:
|
||||
|
||||
- We treat protected and non-protected chats the same
|
||||
@@ -120,8 +97,3 @@ Other
|
||||
-----
|
||||
|
||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
||||
|
||||
Notes during implementing
|
||||
========================
|
||||
|
||||
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.
|
||||
@@ -1,11 +1,9 @@
|
||||
#![allow(clippy::format_push_string)]
|
||||
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,
|
||||
};
|
||||
@@ -13,6 +11,8 @@ use deltachat::chatlist::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::dc_receive_imf::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::download::DownloadState;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
@@ -20,11 +20,10 @@ use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
use deltachat::{config, provider};
|
||||
use 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.
|
||||
@@ -97,10 +96,10 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
let data = dc_read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = receive_imf(context, &data, false).await {
|
||||
println!("receive_imf errored: {:?}", err);
|
||||
if let Err(err) = dc_receive_imf(context, &data, false).await {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -129,20 +128,24 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
real_spec = rs.unwrap();
|
||||
}
|
||||
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
|
||||
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
|
||||
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
|
||||
read_cnt += 1
|
||||
}
|
||||
} else {
|
||||
/* import a directory */
|
||||
let dir_name = std::path::Path::new(&real_spec);
|
||||
let dir = 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") {
|
||||
@@ -188,7 +191,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
DownloadState::Failure => " [⬇ Download failed]",
|
||||
};
|
||||
|
||||
let temp2 = timestamp_to_str(msg.get_timestamp());
|
||||
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
|
||||
let msgtext = msg.get_text();
|
||||
println!(
|
||||
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{}{} [{}]",
|
||||
@@ -216,14 +219,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
msg.get_videochat_url().unwrap_or_default(),
|
||||
msg.get_videochat_type().unwrap_or_default()
|
||||
)
|
||||
} else if msg.get_viewtype() == Viewtype::Webxdc {
|
||||
match msg.get_webxdc_info(context).await {
|
||||
Ok(info) => format!(
|
||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||
info.name, info.icon, info.document, info.summary, info.source_code_url
|
||||
),
|
||||
Err(err) => format!("[get_webxdc_info() failed: {}]", err),
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
@@ -497,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(),
|
||||
@@ -537,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) => {
|
||||
@@ -602,7 +597,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
_ => "",
|
||||
}
|
||||
};
|
||||
let timestr = timestamp_to_str(summary.timestamp);
|
||||
let timestr = dc_timestamp_to_str(summary.timestamp);
|
||||
println!(
|
||||
"{}{}{} [{}]{}",
|
||||
summary
|
||||
@@ -815,7 +810,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!(
|
||||
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} {} {}",
|
||||
location.location_id,
|
||||
timestamp_to_str(location.timestamp),
|
||||
dc_timestamp_to_str(location.timestamp),
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
location.accuracy,
|
||||
@@ -897,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);
|
||||
@@ -1084,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" => {
|
||||
@@ -1238,8 +1233,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
if let Ok(buf) = read_file(&context, &arg1).await {
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
if let Ok(buf) = dc_read_file(&context, &arg1).await {
|
||||
let (width, height) = dc_get_filemeta(&buf)?;
|
||||
println!("width={}, height={}", width, height);
|
||||
} else {
|
||||
bail!("Command failed.");
|
||||
|
||||
@@ -9,20 +9,20 @@ 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};
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::config::OutputStreamType;
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||
use rustyline::hint::{Hinter, HistoryHinter};
|
||||
@@ -30,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) {
|
||||
@@ -298,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);
|
||||
}
|
||||
@@ -313,17 +313,17 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
.history_ignore_space(true)
|
||||
.completion_type(CompletionType::List)
|
||||
.edit_mode(EditMode::Emacs)
|
||||
.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(),
|
||||
hinter: HistoryHinter {},
|
||||
};
|
||||
let mut rl = Editor::with_config(config)?;
|
||||
let mut rl = Editor::with_config(config);
|
||||
rl.set_helper(Some(h));
|
||||
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
|
||||
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
|
||||
@@ -339,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;
|
||||
}
|
||||
}
|
||||
@@ -373,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(())
|
||||
}
|
||||
@@ -407,7 +400,7 @@ async fn handle_cmd(
|
||||
"oauth2" => {
|
||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||
let oauth2_url =
|
||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
||||
if oauth2_url.is_none() {
|
||||
println!("OAuth2 not available for {}.", &addr);
|
||||
} else {
|
||||
@@ -424,7 +417,7 @@ async fn handle_cmd(
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let mut qr = get_securejoin_qr(&ctx, group).await?;
|
||||
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
@@ -444,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) => {
|
||||
@@ -455,7 +448,7 @@ async fn handle_cmd(
|
||||
"joinqr" => {
|
||||
ctx.start_io().await;
|
||||
if !arg0.is_empty() {
|
||||
join_securejoin(&ctx, arg1).await?;
|
||||
dc_join_securejoin(&ctx, arg1).await?;
|
||||
}
|
||||
}
|
||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||
@@ -465,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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ $ fnm install 17 --arch x64
|
||||
$ fnm use 17
|
||||
$ node -p process.arch
|
||||
# result should be x64
|
||||
$ rustup target add x86_64-apple-darwin
|
||||
$ cd deltachat-core-rust && rustup target add x86_64-apple-darwin && cd -
|
||||
$ git apply patches/m1_build_use_x86_64.patch
|
||||
$ CARGO_BUILD_TARGET=x86_64-apple-darwin npm run build
|
||||
$ npm run test
|
||||
@@ -234,7 +234,7 @@ We have the following scripts for building, testing and coverage:
|
||||
The following steps are needed to make a release:
|
||||
|
||||
1. Wait until `pack-module` github action is completed
|
||||
2. Run `npm publish https://download.delta.chat/node/deltachat-node-1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
|
||||
2. Run `npm publish https://download.delta.chat/node/deltachat-node-v1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -39,10 +39,6 @@ export class Chat {
|
||||
return binding.dcn_chat_get_name(this.dc_chat)
|
||||
}
|
||||
|
||||
getMailinglistAddr(): string {
|
||||
return binding.dcn_chat_get_mailinglist_addr(this.dc_chat)
|
||||
}
|
||||
|
||||
getProfileImage(): string {
|
||||
return binding.dcn_chat_get_profile_image(this.dc_chat)
|
||||
}
|
||||
@@ -96,7 +92,6 @@ export class Chat {
|
||||
color: this.color,
|
||||
id: this.getId(),
|
||||
name: this.getName(),
|
||||
mailinglistAddr: this.getMailinglistAddr(),
|
||||
profileImage: this.getProfileImage(),
|
||||
type: this.getType(),
|
||||
isSelfTalk: this.isSelfTalk(),
|
||||
|
||||
@@ -115,7 +115,7 @@ export class AccountManager extends EventEmitter {
|
||||
debug('Started event handler')
|
||||
}
|
||||
|
||||
startJsonRpcHandler(callback: ((response: string) => void) | null) {
|
||||
startJSONRPCHandler(callback: ((response: string) => void) | null) {
|
||||
if (this.dcn_accounts === null) {
|
||||
throw new Error('dcn_account is null')
|
||||
}
|
||||
@@ -127,14 +127,14 @@ export class AccountManager extends EventEmitter {
|
||||
}
|
||||
|
||||
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this))
|
||||
debug('Started JSON-RPC handler')
|
||||
debug('Started jsonrpc handler')
|
||||
this.jsonRpcStarted = true
|
||||
}
|
||||
|
||||
jsonRpcRequest(message: string) {
|
||||
jsonRPCRequest(message: string) {
|
||||
if (!this.jsonRpcStarted) {
|
||||
throw new Error(
|
||||
'jsonrpc is not active, start it with startJsonRpcHandler first'
|
||||
'jsonrpc is not active, start it with startJSONRPCHandler first'
|
||||
)
|
||||
}
|
||||
binding.dcn_json_rpc_request(this.dcn_accounts, message)
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface ChatJSON {
|
||||
color: string
|
||||
id: number
|
||||
name: string
|
||||
mailinglistAddr: string
|
||||
profileImage: string
|
||||
type: number
|
||||
isSelfTalk: boolean
|
||||
|
||||
@@ -101,6 +101,14 @@ static void finalize_provider(napi_env env, void* data, void* hint) {
|
||||
}
|
||||
}
|
||||
|
||||
static void finalize_account(napi_env env, void* data, void* hint) {
|
||||
if (data) {
|
||||
dc_accounts_t* dcn_accounts = (dc_accounts_t*)data;
|
||||
//TRACE("cleaning up provider");
|
||||
dc_accounts_unref(dcn_accounts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers.
|
||||
*/
|
||||
@@ -1628,18 +1636,6 @@ NAPI_METHOD(dcn_chat_get_name) {
|
||||
NAPI_RETURN_AND_UNREF_STRING(name);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_chat_get_mailinglist_addr) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DC_CHAT();
|
||||
|
||||
//TRACE("calling..");
|
||||
char* addr = dc_chat_get_mailinglist_addr(dc_chat);
|
||||
//TRACE("result %s", name);
|
||||
|
||||
NAPI_RETURN_AND_UNREF_STRING(addr);
|
||||
}
|
||||
|
||||
|
||||
NAPI_METHOD(dcn_chat_get_profile_image) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DC_CHAT();
|
||||
@@ -2942,7 +2938,6 @@ NAPI_METHOD(dcn_accounts_unref) {
|
||||
if (dcn_accounts->jsonrpc_instance) {
|
||||
dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, "{}");
|
||||
uv_thread_join(&dcn_accounts->jsonrpc_thread);
|
||||
dc_jsonrpc_unref(dcn_accounts->jsonrpc_instance);
|
||||
dcn_accounts->jsonrpc_instance = NULL;
|
||||
}
|
||||
dc_accounts_unref(dcn_accounts->dc_accounts);
|
||||
@@ -3192,7 +3187,7 @@ static void call_accounts_js_event_handler(napi_env env, napi_value js_callback,
|
||||
if (status != napi_ok) {
|
||||
napi_throw_error(env, NULL, "Unable to create argv[3] for event_handler arguments");
|
||||
}
|
||||
dc_str_unref(data2_string);
|
||||
free(data2_string);
|
||||
} else {
|
||||
status = napi_create_int32(env, dc_event_get_data2_int(dc_event), &argv[3]);
|
||||
if (status != napi_ok) {
|
||||
@@ -3284,6 +3279,8 @@ static void accounts_jsonrpc_thread_func(void* arg)
|
||||
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);
|
||||
}
|
||||
@@ -3306,7 +3303,7 @@ static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callbac
|
||||
if (status != napi_ok) {
|
||||
napi_throw_error(env, NULL, "Unable to create argv for js jsonrpc_handler arguments");
|
||||
}
|
||||
dc_str_unref(response);
|
||||
free(response);
|
||||
|
||||
TRACE("calling back into js");
|
||||
napi_value result;
|
||||
@@ -3340,11 +3337,11 @@ NAPI_METHOD(dcn_accounts_start_jsonrpc) {
|
||||
callback,
|
||||
0,
|
||||
async_resource_name,
|
||||
1000, // max_queue_size
|
||||
1,
|
||||
1,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
dcn_accounts,
|
||||
call_accounts_js_jsonrpc_handler,
|
||||
&dcn_accounts->threadsafe_jsonrpc_handler));
|
||||
TRACE("done");
|
||||
@@ -3368,7 +3365,6 @@ NAPI_METHOD(dcn_json_rpc_request) {
|
||||
NAPI_ARGV_UTF8_MALLOC(request, 1);
|
||||
dc_jsonrpc_request(dcn_accounts->jsonrpc_instance, request);
|
||||
free(request);
|
||||
NAPI_RETURN_UNDEFINED();
|
||||
}
|
||||
|
||||
|
||||
@@ -3507,7 +3503,6 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_visibility);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_id);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_name);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_mailinglist_addr);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_profile_image);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_type);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_is_self_talk);
|
||||
|
||||
@@ -91,8 +91,8 @@ describe('JSON RPC', function () {
|
||||
const promise = new Promise((res, _rej) => {
|
||||
promise_resolve = res
|
||||
})
|
||||
dc.startJsonRpcHandler(promise_resolve)
|
||||
dc.jsonRpcRequest(
|
||||
dc.startJSONRPCHandler(promise_resolve)
|
||||
dc.jsonRPCRequest(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'get_all_account_ids',
|
||||
@@ -115,13 +115,13 @@ describe('JSON RPC', function () {
|
||||
const { dc } = DeltaChat.newTemporary()
|
||||
|
||||
const promises = {}
|
||||
dc.startJsonRpcHandler((msg) => {
|
||||
dc.startJSONRPCHandler((msg) => {
|
||||
const response = JSON.parse(msg)
|
||||
promises[response.id](response)
|
||||
delete promises[response.id]
|
||||
})
|
||||
const call = (request) => {
|
||||
dc.jsonRpcRequest(JSON.stringify(request))
|
||||
dc.jsonRPCRequest(JSON.stringify(request))
|
||||
return new Promise((res, _rej) => {
|
||||
promises[request.id] = res
|
||||
})
|
||||
|
||||
@@ -61,5 +61,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.92.0"
|
||||
}
|
||||
"version": "1.86.0"
|
||||
}
|
||||
|
||||
@@ -1,77 +1,84 @@
|
||||
=========================
|
||||
DeltaChat Python bindings
|
||||
deltachat python bindings
|
||||
=========================
|
||||
|
||||
This package provides `Python bindings`_ to the `deltachat-core library`_
|
||||
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
|
||||
This package provides bindings to the deltachat-core_ Rust -library
|
||||
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
|
||||
a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||
|
||||
.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust
|
||||
.. _`Python bindings`: https://py.delta.chat/
|
||||
|
||||
Installing pre-built packages (Linux-only)
|
||||
==========================================
|
||||
========================================================
|
||||
|
||||
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
|
||||
without any "build-from-source" steps.
|
||||
Otherwise you need to `compile the Delta Chat bindings yourself`__.
|
||||
|
||||
__ sourceinstall_
|
||||
Otherwise you need to `compile the Delta Chat bindings yourself <#sourceinstall>`_.
|
||||
|
||||
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
|
||||
then create a fresh Python virtual environment and activate it in your shell::
|
||||
|
||||
virtualenv env # or: python -m venv
|
||||
source env/bin/activate
|
||||
virtualenv venv # or: python -m venv
|
||||
source venv/bin/activate
|
||||
|
||||
Afterwards, invoking ``python`` or ``pip install`` only
|
||||
modifies files in your ``env`` directory and leaves
|
||||
modifies files in your ``venv`` directory and leaves
|
||||
your system installation alone.
|
||||
|
||||
For Linux we build wheels for all releases and push them to a python package
|
||||
index. To install the latest release::
|
||||
For Linux, we automatically build wheels for all github PR branches
|
||||
and push them to a python package index. To install the latest
|
||||
github ``master`` branch::
|
||||
|
||||
pip install deltachat
|
||||
pip install --pre -i https://m.devpi.net/dc/master deltachat
|
||||
|
||||
To verify it worked::
|
||||
|
||||
python -c "import deltachat"
|
||||
|
||||
.. note::
|
||||
|
||||
If you can help to automate the building of wheels for Mac or Windows,
|
||||
that'd be much appreciated! please then get
|
||||
`in contact with us <https://delta.chat/en/contribute>`_.
|
||||
|
||||
|
||||
Running tests
|
||||
=============
|
||||
|
||||
Recommended way to run tests is using `tox <https://tox.wiki>`_.
|
||||
After successful binding installation you can install tox
|
||||
and run the tests::
|
||||
After successful binding installation you can install a few more
|
||||
Python packages before running the tests::
|
||||
|
||||
pip install tox
|
||||
tox -e py3
|
||||
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
|
||||
pytest -v tests
|
||||
|
||||
This will run all "offline" tests and skip all functional
|
||||
end-to-end tests that require accounts on real e-mail servers.
|
||||
|
||||
.. _livetests:
|
||||
|
||||
Running "live" tests with temporary accounts
|
||||
--------------------------------------------
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLs created and managed by `mailadm <https://mailadm.readthedocs.io/>`_.
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLS created and managed by [mailadm](https://mailadm.readthedocs.io/en/latest/).
|
||||
|
||||
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this::
|
||||
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this:
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=<URL you got from us>
|
||||
|
||||
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
|
||||
One hour is enough to invoke pytest and run all offline and online tests::
|
||||
One hour is enough to invoke pytest and run all offline and online tests:
|
||||
|
||||
tox -e py3
|
||||
pytest
|
||||
|
||||
# or if you have installed pytest-xdist for parallel test execution
|
||||
pytest -n6
|
||||
|
||||
Each test run creates new accounts.
|
||||
|
||||
|
||||
.. _sourceinstall:
|
||||
|
||||
Installing bindings from source
|
||||
===============================
|
||||
Installing bindings from source (Updated: July 2020)
|
||||
=========================================================
|
||||
|
||||
Install Rust and Cargo first.
|
||||
The easiest is probably to use `rustup <https://rustup.rs/>`_.
|
||||
@@ -90,42 +97,74 @@ E.g. on Debian-based systems `apt install python3 python3-pip
|
||||
python3-venv` should give you a usable python installation.
|
||||
|
||||
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||
virtual environment with dependencies using tox
|
||||
and activate it in your shell::
|
||||
virtual environment and activate it in your shell::
|
||||
|
||||
cd python
|
||||
tox --devenv env
|
||||
source env/bin/activate
|
||||
python3 -m venv venv # or: virtualenv venv
|
||||
source venv/bin/activate
|
||||
|
||||
You should now be able to build the python bindings using the supplied script::
|
||||
|
||||
python3 install_python_bindings.py
|
||||
python install_python_bindings.py
|
||||
|
||||
The core compilation and bindings building might take a while,
|
||||
depending on the speed of your machine.
|
||||
The bindings will be installed in release mode but with debug symbols.
|
||||
The release mode is currently necessary because some tests generate RSA keys
|
||||
which is prohibitively slow in non-release mode.
|
||||
|
||||
|
||||
Code examples
|
||||
=============
|
||||
|
||||
You may look at `examples <https://py.delta.chat/examples.html>`_.
|
||||
|
||||
|
||||
.. _`deltachat-core-rust github repository`: https://github.com/deltachat/deltachat-core-rust
|
||||
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
||||
|
||||
|
||||
Building manylinux based wheels
|
||||
===============================
|
||||
====================================
|
||||
|
||||
Building portable manylinux wheels which come with libdeltachat.so
|
||||
can be done with Docker_ or Podman_.
|
||||
can be done with docker-tooling.
|
||||
|
||||
.. _Docker: https://www.docker.com/
|
||||
.. _Podman: https://podman.io/
|
||||
using docker pull / premade images
|
||||
------------------------------------
|
||||
|
||||
If you want to build your own wheels, build container image first::
|
||||
We publish a build environment under the ``deltachat/coredeps`` tag so
|
||||
that you can pull it from the ``hub.docker.com`` site's "deltachat"
|
||||
organization::
|
||||
|
||||
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
|
||||
$ docker build -t deltachat/coredeps scripts/coredeps
|
||||
|
||||
This will use the ``scripts/coredeps/Dockerfile`` to build
|
||||
container image called ``deltachat/coredeps``. You can afterwards
|
||||
find it with::
|
||||
|
||||
$ docker images
|
||||
$ docker pull deltachat/coredeps
|
||||
|
||||
This docker image can be used to run tests and build Python wheels for all interpreters::
|
||||
|
||||
$ docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v $(pwd):/mnt -w /mnt \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps scripts/run_all.sh
|
||||
|
||||
|
||||
Optionally build your own docker image
|
||||
--------------------------------------
|
||||
|
||||
If you want to build your own custom docker image you can do this::
|
||||
|
||||
$ cd deltachat-core # cd to deltachat-core checkout directory
|
||||
$ docker build -t deltachat/coredeps scripts/docker_coredeps
|
||||
|
||||
This will use the ``scripts/docker_coredeps/Dockerfile`` to build
|
||||
up docker image called ``deltachat/coredeps``. You can afterwards
|
||||
find it with::
|
||||
|
||||
$ docker images
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
On more recent systems running the docker image may crash. You can
|
||||
fix this by adding ``vsyscall=emulate`` to the Linux kernel boot
|
||||
arguments commandline. E.g. on Debian you'd add this to
|
||||
``GRUB_CMDLINE_LINUX_DEFAULT`` in ``/etc/default/grub``.
|
||||
|
||||
7
python/fail_test.py
Normal file
7
python/fail_test.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from __future__ import print_function
|
||||
from deltachat import capi
|
||||
from deltachat.capi import ffi, lib
|
||||
|
||||
if __name__ == "__main__":
|
||||
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
|
||||
lib.dc_stop_io(ctx)
|
||||
@@ -16,7 +16,7 @@ if __name__ == "__main__":
|
||||
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ["DCC_RS_DEV"] = dn
|
||||
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi", "--features", "jsonrpc"]
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||
|
||||
if target == "release":
|
||||
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
|
||||
|
||||
@@ -1,41 +1,7 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
authors = [
|
||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Email",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
]
|
||||
dependencies = [
|
||||
"cffi>=1.0.0",
|
||||
"imap-tools",
|
||||
"pluggy",
|
||||
"requests",
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
|
||||
"Documentation" = "https://py.delta.chat/"
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat.testplugin" = "deltachat.testplugin"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
|
||||
@@ -1,4 +1,40 @@
|
||||
from setuptools import setup
|
||||
import os
|
||||
import re
|
||||
|
||||
import setuptools
|
||||
|
||||
|
||||
def main():
|
||||
with open("README.rst") as f:
|
||||
long_description = f.read()
|
||||
setuptools.setup(
|
||||
name="deltachat",
|
||||
description="Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat",
|
||||
long_description=long_description,
|
||||
author="holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors",
|
||||
install_requires=["cffi>=1.0.0", "pluggy", "imap-tools", "requests"],
|
||||
setup_requires=[
|
||||
"setuptools_scm", # required for compatibility with `python3 setup.py sdist`
|
||||
"pkgconfig",
|
||||
],
|
||||
packages=setuptools.find_packages("src"),
|
||||
package_dir={"": "src"},
|
||||
cffi_modules=["src/deltachat/_build.py:ffibuilder"],
|
||||
entry_points={
|
||||
"pytest11": [
|
||||
"deltachat.testplugin = deltachat.testplugin",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Email",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup(cffi_modules=["src/deltachat/_build.py:ffibuilder"])
|
||||
main()
|
||||
|
||||
@@ -60,7 +60,29 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
|
||||
ac = Account(args.db)
|
||||
|
||||
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)
|
||||
if args.show_ffi:
|
||||
ac.set_config("displayname", "bot")
|
||||
log = events.FFIEventLogger(ac)
|
||||
ac.add_account_plugin(log)
|
||||
|
||||
for plugin in account_plugins or []:
|
||||
print("adding plugin", plugin)
|
||||
ac.add_account_plugin(plugin)
|
||||
|
||||
if not ac.is_configured():
|
||||
assert (
|
||||
args.email and args.password
|
||||
), "you must specify --email and --password once to configure this database/account"
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
ac.set_config("bot", "1")
|
||||
configtracker = ac.configure()
|
||||
configtracker.wait_finish()
|
||||
|
||||
# start IO threads and configure if neccessary
|
||||
ac.start_io()
|
||||
|
||||
print("{}: waiting for message".format(ac.get_config("addr")))
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from .cutil import (
|
||||
from_optional_dc_charpointer,
|
||||
iter_array,
|
||||
)
|
||||
from .events import EventThread, FFIEventLogger
|
||||
from .events import EventThread
|
||||
from .message import Message
|
||||
from .tracker import ConfigureTracker, ImexTracker
|
||||
|
||||
@@ -169,6 +169,8 @@ class Account(object):
|
||||
"""
|
||||
self._check_config_key(name)
|
||||
namebytes = name.encode("utf8")
|
||||
if namebytes == b"addr" and self.is_configured():
|
||||
raise ValueError("can not change 'addr' after account is configured.")
|
||||
if isinstance(value, (int, bool)):
|
||||
value = str(int(value))
|
||||
if value is not None:
|
||||
@@ -596,36 +598,6 @@ class Account(object):
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
|
||||
"""get the account running, configure it if necessary. add plugins if provided.
|
||||
|
||||
:param addr: the email address of the account
|
||||
:param password: the password of the account
|
||||
:param account_plugins: a list of plugins to add
|
||||
:param show_ffi: show low level ffi events
|
||||
"""
|
||||
if show_ffi:
|
||||
self.set_config("displayname", "bot")
|
||||
log = FFIEventLogger(self)
|
||||
self.add_account_plugin(log)
|
||||
|
||||
for plugin in account_plugins or []:
|
||||
print("adding plugin", plugin)
|
||||
self.add_account_plugin(plugin)
|
||||
|
||||
if not self.is_configured():
|
||||
assert addr and password, "you must specify email and password once to configure this database/account"
|
||||
self.set_config("addr", addr)
|
||||
self.set_config("mail_pw", password)
|
||||
self.set_config("mvbox_move", "0")
|
||||
self.set_config("sentbox_watch", "0")
|
||||
self.set_config("bot", "1")
|
||||
configtracker = self.configure()
|
||||
configtracker.wait_finish()
|
||||
|
||||
# start IO threads and configure if neccessary
|
||||
self.start_io()
|
||||
|
||||
def add_account_plugin(self, plugin, name=None):
|
||||
"""add an account plugin which implements one or more of
|
||||
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||
@@ -708,9 +680,8 @@ class Account(object):
|
||||
"""Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
|
||||
:param reconfigure: deprecated, doesn't need to be checked anymore.
|
||||
"""
|
||||
assert self.is_configured() == reconfigure
|
||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||
configtracker = ConfigureTracker(self)
|
||||
|
||||
@@ -171,6 +171,12 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_color(self._dc_chat)
|
||||
|
||||
def get_summary(self):
|
||||
"""return dictionary with summary information."""
|
||||
dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id)
|
||||
s = from_dc_charpointer(dc_res)
|
||||
return json.loads(s)
|
||||
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
"""mutes the chat
|
||||
|
||||
|
||||
13
python/tests/package_wheels.py
Normal file
13
python/tests/package_wheels.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert len(sys.argv) == 2
|
||||
wheelhousedir = sys.argv[1]
|
||||
# pip wheel will build in an isolated tmp dir that does not have git
|
||||
# history so setuptools_scm can not automatically determine a
|
||||
# version there. So pass in the version through an env var.
|
||||
version = subprocess.check_output(["python", "setup.py", "--version"]).strip().split(b"\n")[-1]
|
||||
os.environ["SETUPTOOLS_SCM_PRETEND_VERSION"] = version.decode("ascii")
|
||||
subprocess.check_call(("pip wheel . -w %s" % wheelhousedir).split())
|
||||
@@ -1581,9 +1581,8 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
|
||||
lp.sec("ac2: wait for receiving message from ac1")
|
||||
msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg1.is_system_message() # Member added
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "hi"
|
||||
assert msg1.text == "hi" or msg2.text == "hi"
|
||||
assert msg1.chat.id == msg2.chat.id
|
||||
|
||||
lp.sec("ac2: see if chat now has got the profile image")
|
||||
@@ -1597,8 +1596,6 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
lp.sec("ac2: delete profile image from chat")
|
||||
msg1.chat.remove_profile_image()
|
||||
msg_back = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg_back.text == "Group image deleted by {}.".format(ac2.get_config("addr"))
|
||||
assert msg_back.is_system_message()
|
||||
assert msg_back.chat == chat
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
@@ -1857,7 +1854,7 @@ def test_configure_error_msgs_invalid_server(acfactory):
|
||||
# Can't connect so it probably should say something about "internet"
|
||||
# again, should not repeat itself
|
||||
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
|
||||
# in configure.rs returned false because the error message was changed
|
||||
# in configure/mod.rs returned false because the error message was changed
|
||||
# (i.e. did not contain "could not resolve" anymore)
|
||||
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
|
||||
# Should mention that it can't connect:
|
||||
@@ -2055,47 +2052,6 @@ def test_delete_deltachat_folder(acfactory):
|
||||
assert "DeltaChat" in ac1.direct_imap.list_folders()
|
||||
|
||||
|
||||
def test_aeap_flow_verified(acfactory, lp):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
assert chat2.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("sending first message")
|
||||
msg_out = chat.send_text("old address")
|
||||
|
||||
lp.sec("receiving first message")
|
||||
ac2._evtracker.wait_next_incoming_message() # member added message
|
||||
msg_in_1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
lp.sec("changing email account")
|
||||
ac1.set_config("addr", ac1new.get_config("addr"))
|
||||
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
||||
ac1.stop_io()
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_finish()
|
||||
ac1.start_io()
|
||||
|
||||
lp.sec("sending second message")
|
||||
msg_out = chat.send_text("changed address")
|
||||
|
||||
lp.sec("receiving second message")
|
||||
msg_in_2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in_2.text == msg_out.text
|
||||
assert msg_in_2.chat.id == msg_in_1.chat.id
|
||||
assert msg_in_2.get_sender_contact().addr == ac1new.get_config("addr")
|
||||
assert len(msg_in_2.chat.get_contacts()) == 2
|
||||
assert ac1new.get_config("addr") in [contact.addr for contact in msg_in_2.chat.get_contacts()]
|
||||
|
||||
|
||||
class TestOnlineConfigureFails:
|
||||
def test_invalid_password(self, acfactory):
|
||||
configdict = acfactory.get_next_liveconfig()
|
||||
|
||||
@@ -497,6 +497,12 @@ class TestOfflineChat:
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
def test_set_config_after_configure_is_forbidden(self, ac1):
|
||||
assert ac1.get_config("mail_pw")
|
||||
assert ac1.is_configured()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_config("addr", "123@example.org")
|
||||
|
||||
def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
@@ -9,8 +9,9 @@ envlist =
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_NEW_TMP_EMAIL
|
||||
|
||||
@@ -14,15 +14,18 @@ and an own build machine.
|
||||
- `remote_tests_rust.sh` rsyncs to the build machine and runs
|
||||
`run-rust-test.sh` remotely on the build machine.
|
||||
|
||||
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
|
||||
- `doxygen/Dockerfile` specifies an image that contains
|
||||
the doxygen tool which is used by `run-doxygen.sh`
|
||||
to generate C-docs which are then uploaded
|
||||
via `ci_upload.sh` to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
||||
(and the master branch is linked to https://c.delta.chat proper).
|
||||
|
||||
- `run_all.sh` builds Python wheels
|
||||
|
||||
## Triggering runs on the build machine locally (fast!)
|
||||
|
||||
There is experimental support for triggering a remote Python or Rust test run
|
||||
from your local checkout/branch. You will need to be authorized to login to
|
||||
the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type:
|
||||
the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type::
|
||||
|
||||
scripts/manual_remote_tests.sh rust
|
||||
scripts/manual_remote_tests.sh python
|
||||
@@ -30,18 +33,19 @@ the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type
|
||||
This will **rsync** your current checkout to the remote build machine
|
||||
(no need to commit before) and then run either rust or python tests.
|
||||
|
||||
# coredeps Dockerfile
|
||||
# Outdated files (for later re-use)
|
||||
|
||||
`coredeps/Dockerfile` specifies an image that contains all
|
||||
of Delta Chat's core dependencies. It is used to
|
||||
build python wheels (binary packages for Python).
|
||||
of Delta Chat's core dependencies. It used to run
|
||||
python tests and build wheels (binary packages for Python)
|
||||
|
||||
You can build the docker images yourself locally
|
||||
to avoid the relatively large download:
|
||||
to avoid the relatively large download::
|
||||
|
||||
cd scripts # where all CI things are
|
||||
docker build -t deltachat/coredeps coredeps
|
||||
docker build -t deltachat/coredeps docker-coredeps
|
||||
docker build -t deltachat/doxygen docker-doxygen
|
||||
|
||||
Additionally, you can install qemu and build arm64 docker image on x86\_64 machine:
|
||||
Additionally, you can install qemu and build arm64 docker image:
|
||||
apt-get install qemu binfmt-support qemu-user-static
|
||||
docker build -t deltachat/coredeps-arm64 --build-arg BASEIMAGE=quay.io/pypa/manylinux2014_aarch64 coredeps
|
||||
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64
|
||||
|
||||
@@ -29,19 +29,18 @@ jobs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
source:
|
||||
repository: alpine
|
||||
repository: hrektts/doxygen
|
||||
type: registry-image
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
path: bash
|
||||
args:
|
||||
- -ec
|
||||
- -exc
|
||||
- |
|
||||
apk add --no-cache doxygen git
|
||||
cd deltachat-core-rust
|
||||
scripts/run-doxygen.sh
|
||||
bash scripts/run-doxygen.sh
|
||||
cd ..
|
||||
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
|
||||
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
|
||||
|
||||
- task: upload-c-docs
|
||||
config:
|
||||
@@ -85,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
|
||||
@@ -185,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
|
||||
@@ -233,143 +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*
|
||||
|
||||
- name: python-musl-aarch64
|
||||
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_aarch64
|
||||
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 aarch64 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_aarch64*
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -17,7 +17,7 @@ export RUSTC_BOOTSTRAP=1
|
||||
# [2] https://github.com/mozilla/grcov/issues/595
|
||||
|
||||
export CARGO_INCREMENTAL=0
|
||||
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebuginfo=2"
|
||||
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
export RUSTDOCFLAGS="-Cpanic=abort"
|
||||
cargo clean
|
||||
cargo build
|
||||
|
||||
6
scripts/docker-coredeps-arm64/Dockerfile
Normal file
6
scripts/docker-coredeps-arm64/Dockerfile
Normal 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
|
||||
18
scripts/docker-coredeps-arm64/deps/build_rust.sh
Executable file
18
scripts/docker-coredeps-arm64/deps/build_rust.sh
Executable 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"
|
||||
6
scripts/docker-coredeps/Dockerfile
Normal file
6
scripts/docker-coredeps/Dockerfile
Normal 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
|
||||
18
scripts/docker-coredeps/deps/build_rust.sh
Executable file
18
scripts/docker-coredeps/deps/build_rust.sh
Executable 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"
|
||||
5
scripts/docker-doxygen/Dockerfile
Normal file
5
scripts/docker-doxygen/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM debian:stable
|
||||
|
||||
# this is tagged as deltachat/doxygen
|
||||
|
||||
RUN apt-get update && apt-get install -y doxygen
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
cd deltachat-ffi
|
||||
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build the Delta Chat Core Rust library, Python wheels and docs
|
||||
|
||||
@@ -8,16 +8,21 @@ set -e -x
|
||||
|
||||
# compile core lib
|
||||
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
cargo build --release -p deltachat_ffi
|
||||
# cargo test --all --all-features
|
||||
|
||||
# Statically link against libdeltachat.a.
|
||||
export DCC_RS_DEV="$PWD"
|
||||
export DCC_RS_DEV=$(pwd)
|
||||
export DCC_RS_TARGET=release
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp37-cp37m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
cd python
|
||||
|
||||
TOXWORKDIR=.docker-tox
|
||||
pushd python
|
||||
# prepare a clean tox run
|
||||
rm -rf tests/__pycache__
|
||||
rm -rf src/deltachat/__pycache__
|
||||
@@ -28,13 +33,11 @@ mkdir -p $TOXWORKDIR
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
echo -----------------------
|
||||
echo generating python docs
|
||||
echo -----------------------
|
||||
tox --workdir "$TOXWORKDIR" -e doc
|
||||
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||
|
||||
0
scripts/set_core_version.py
Executable file → Normal file
0
scripts/set_core_version.py
Executable file → Normal file
18
spec.md
18
spec.md
@@ -1,6 +1,6 @@
|
||||
# chat-mail specification
|
||||
|
||||
Version: 0.34.0
|
||||
Version: 0.33.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -474,20 +474,4 @@ as the sending time of the message as indicated by its Date header,
|
||||
or the time of first receipt if that date is in the future or unavailable.
|
||||
|
||||
|
||||
# Transitioning to a new e-mail address (AEAP)
|
||||
|
||||
When receiving a message:
|
||||
- If the key exists, but belongs to another address
|
||||
- AND there is a `Chat-Version` header
|
||||
- AND the message is signed correctly
|
||||
- AND the From address is (also) in the encrypted (and therefore signed) headers
|
||||
- AND the message timestamp is newer than the contact's `lastseen`
|
||||
(to prevent changing the address back when messages arrive out of order)
|
||||
(this condition is not that important
|
||||
since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups,
|
||||
possibly deduplicate the members list,
|
||||
and add a system message to all of these chats.
|
||||
|
||||
Copyright © 2017-2021 Delta Chat contributors.
|
||||
|
||||
115
src/accounts.rs
115
src/accounts.rs
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
pub async fn create(dir: &Path) -> Result<()> {
|
||||
pub async fn create(dir: &PathBuf) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
@@ -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) = {
|
||||
@@ -580,7 +597,7 @@ mod tests {
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 1);
|
||||
|
||||
let id0 = *ids.first().unwrap();
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
|
||||
.await?;
|
||||
@@ -620,7 +637,7 @@ mod tests {
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 3);
|
||||
|
||||
let id0 = *ids.first().unwrap();
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
@@ -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
|
||||
|
||||
348
src/blob.rs
348
src/blob.rs
@@ -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,26 +709,36 @@ 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");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.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!(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);
|
||||
|
||||
async fn file_size(path_buf: &Path) -> u64 {
|
||||
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();
|
||||
file.metadata().await.unwrap().len()
|
||||
}
|
||||
@@ -750,22 +750,25 @@ 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");
|
||||
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(test_utils::AVATAR_900x900_BYTES)
|
||||
.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
|
||||
@@ -776,30 +779,37 @@ 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");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||
File::create(&avatar_src)
|
||||
.await
|
||||
.unwrap()
|
||||
.write_all(avatar_bytes)
|
||||
.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!(
|
||||
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:
|
||||
@@ -819,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");
|
||||
@@ -845,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,
|
||||
@@ -873,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)
|
||||
@@ -924,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);
|
||||
@@ -939,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);
|
||||
@@ -953,17 +961,19 @@ 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?;
|
||||
|
||||
let file = t.get_blobdir().join("anyfile.dat");
|
||||
fs::write(&file, b"bla").await?;
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let prepared_id = chat::prepare_msg(&t, chat_id, &mut msg).await?;
|
||||
@@ -976,14 +986,14 @@ 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?;
|
||||
assert_ne!(t.get_blobdir().to_str(), t.dir.path().to_str());
|
||||
|
||||
let file = t.dir.path().join("anyfile.dat");
|
||||
fs::write(&file, b"bla").await?;
|
||||
File::create(&file).await?.write_all("bla".as_ref()).await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
assert!(chat::prepare_msg(&t, chat_id, &mut msg).await.is_err());
|
||||
|
||||
302
src/chat.rs
302
src/chat.rs
@@ -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};
|
||||
|
||||
@@ -20,6 +20,12 @@ use crate::constants::{
|
||||
};
|
||||
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::ReceivedMsg;
|
||||
use crate::dc_tools::{
|
||||
dc_create_id, dc_create_outgoing_rfc724_mid, dc_create_smeared_timestamp,
|
||||
dc_create_smeared_timestamps, dc_get_abs_path, dc_gm2local_offset, improve_single_line_input,
|
||||
time, IsNoneOrEmpty,
|
||||
};
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::events::EventType;
|
||||
use crate::html::new_html_mimepart;
|
||||
@@ -28,19 +34,14 @@ use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps,
|
||||
get_abs_path, gm2local_offset, improve_single_line_input, time, IsNoneOrEmpty,
|
||||
};
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
use crate::{location, sql};
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum ChatItem {
|
||||
Message {
|
||||
msg_id: MsgId,
|
||||
@@ -232,7 +233,7 @@ impl ChatId {
|
||||
grpname,
|
||||
grpid,
|
||||
create_blocked,
|
||||
create_smeared_timestamp(context).await,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
create_protected,
|
||||
param.unwrap_or_default(),
|
||||
],
|
||||
@@ -434,7 +435,7 @@ impl ChatId {
|
||||
self,
|
||||
&msg_text,
|
||||
cmd,
|
||||
create_smeared_timestamp(context).await,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -1129,16 +1130,11 @@ impl Chat {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns mailing list address where messages are sent to.
|
||||
pub fn get_mailinglist_addr(&self) -> &str {
|
||||
self.param.get(Param::ListPost).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns profile image path for the chat.
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
return Ok(Some(dc_get_abs_path(context, image_rel)));
|
||||
}
|
||||
} else if self.typ == Chattype::Single {
|
||||
let contacts = get_chat_contacts(context, self.id).await?;
|
||||
@@ -1149,7 +1145,7 @@ impl Chat {
|
||||
}
|
||||
} else if self.typ == Chattype::Broadcast {
|
||||
if let Ok(image_rel) = get_broadcast_icon(context).await {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
return Ok(Some(dc_get_abs_path(context, image_rel)));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
@@ -1172,6 +1168,35 @@ impl Chat {
|
||||
Ok(color)
|
||||
}
|
||||
|
||||
/// Returns a struct describing the current state of the chat.
|
||||
///
|
||||
/// This is somewhat experimental, even more so than the rest of
|
||||
/// deltachat, and the data returned is still subject to change.
|
||||
pub async fn get_info(&self, context: &Context) -> Result<ChatInfo> {
|
||||
let draft = match self.id.get_draft(context).await? {
|
||||
Some(message) => message.text.unwrap_or_default(),
|
||||
_ => String::new(),
|
||||
};
|
||||
Ok(ChatInfo {
|
||||
id: self.id,
|
||||
type_: self.typ as u32,
|
||||
name: self.name.clone(),
|
||||
archived: self.visibility == ChatVisibility::Archived,
|
||||
param: self.param.to_string(),
|
||||
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
|
||||
is_sending_locations: self.is_sending_locations,
|
||||
color: self.get_color(context).await?,
|
||||
profile_image: self
|
||||
.get_profile_image(context)
|
||||
.await?
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(std::path::PathBuf::new),
|
||||
draft,
|
||||
is_muted: self.is_muted(),
|
||||
ephemeral_timer: self.id.get_ephemeral_timer(context).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_visibility(&self) -> ChatVisibility {
|
||||
self.visibility
|
||||
}
|
||||
@@ -1246,7 +1271,7 @@ impl Chat {
|
||||
Chattype::Group => Some(self.grpid.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
create_outgoing_rfc724_mid(grpid, &from)
|
||||
dc_create_outgoing_rfc724_mid(grpid, &from)
|
||||
};
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
@@ -1723,7 +1748,7 @@ impl ChatIdBlocked {
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let created_timestamp = create_smeared_timestamp(context).await;
|
||||
let created_timestamp = dc_create_smeared_timestamp(context).await;
|
||||
let chat_id = context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
@@ -1876,7 +1901,7 @@ async fn prepare_msg_common(
|
||||
context,
|
||||
msg,
|
||||
update_msg_id,
|
||||
create_smeared_timestamp(context).await,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
)
|
||||
.await?;
|
||||
msg.chat_id = chat_id;
|
||||
@@ -1970,7 +1995,7 @@ async fn prepare_send_msg(
|
||||
chat_id: ChatId,
|
||||
msg: &mut Message,
|
||||
) -> Result<Option<i64>> {
|
||||
// prepare_msg() leaves the message state to OutPreparing, we
|
||||
// dc_prepare_msg() leaves the message state to OutPreparing, we
|
||||
// only have to change the state to OutPending in this case.
|
||||
// Otherwise we still have to prepare the message, which will set
|
||||
// the state to OutPending.
|
||||
@@ -2154,7 +2179,7 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
|
||||
bail!("webrtc_instance not set");
|
||||
};
|
||||
|
||||
let instance = Message::create_webrtc_instance(&instance, &create_id());
|
||||
let instance = Message::create_webrtc_instance(&instance, &dc_create_id());
|
||||
|
||||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||||
msg.param.set(Param::WebrtcRoom, &instance);
|
||||
@@ -2217,7 +2242,7 @@ pub async fn get_chat_msgs(
|
||||
|
||||
let mut ret = Vec::new();
|
||||
let mut last_day = 0;
|
||||
let cnv_to_local = gm2local_offset();
|
||||
let cnv_to_local = dc_gm2local_offset();
|
||||
|
||||
for (ts, curr_id) in sorted_rows {
|
||||
if (flags & DC_GCM_ADDDAYMARKER) != 0 {
|
||||
@@ -2513,7 +2538,7 @@ pub async fn create_group_chat(
|
||||
let chat_name = improve_single_line_input(chat_name);
|
||||
ensure!(!chat_name.is_empty(), "Invalid chat name");
|
||||
|
||||
let grpid = create_id();
|
||||
let grpid = dc_create_id();
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
@@ -2525,7 +2550,7 @@ pub async fn create_group_chat(
|
||||
Chattype::Group,
|
||||
chat_name,
|
||||
grpid,
|
||||
create_smeared_timestamp(context).await,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
@@ -2572,7 +2597,7 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
|
||||
/// Creates a new broadcast list.
|
||||
pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
||||
let chat_name = find_unused_broadcast_list_name(context).await?;
|
||||
let grpid = create_id();
|
||||
let grpid = dc_create_id();
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
@@ -2583,7 +2608,7 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
||||
Chattype::Broadcast,
|
||||
chat_name,
|
||||
grpid,
|
||||
create_smeared_timestamp(context).await,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
@@ -2757,7 +2782,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
|
||||
Ok(needs_attach)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
@@ -3024,7 +3049,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
|
||||
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len()).await;
|
||||
curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len()).await;
|
||||
let ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
@@ -3219,12 +3244,12 @@ pub async fn add_device_msg_with_importance(
|
||||
if let Some(msg) = msg {
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
msg.try_calc_and_set_dimensions(context).await.ok();
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
chat_id.unarchive_if_not_muted(context).await?;
|
||||
|
||||
let timestamp_sent = create_smeared_timestamp(context).await;
|
||||
let timestamp_sent = dc_create_smeared_timestamp(context).await;
|
||||
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
@@ -3357,7 +3382,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
parent: Option<&Message>,
|
||||
from_id: Option<ContactId>,
|
||||
) -> Result<MsgId> {
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device");
|
||||
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
|
||||
|
||||
let mut param = Params::new();
|
||||
@@ -3434,13 +3459,46 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chatlist::{get_archived_cnt, Chatlist};
|
||||
use crate::chatlist::{dc_get_archived_cnt, Chatlist};
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::contact::Contact;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
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;
|
||||
let info = chat.get_info(&t).await.unwrap();
|
||||
|
||||
// Ensure we can serialize this.
|
||||
println!("{}", serde_json::to_string_pretty(&info).unwrap());
|
||||
|
||||
let expected = r#"
|
||||
{
|
||||
"id": 10,
|
||||
"type": 100,
|
||||
"name": "bob",
|
||||
"archived": false,
|
||||
"param": "",
|
||||
"gossiped_timestamp": 0,
|
||||
"is_sending_locations": false,
|
||||
"color": 35391,
|
||||
"profile_image": "",
|
||||
"draft": "",
|
||||
"is_muted": false,
|
||||
"ephemeral_timer": "Disabled"
|
||||
}
|
||||
"#;
|
||||
|
||||
// Ensure we can deserialize this.
|
||||
let loaded: ChatInfo = serde_json::from_str(expected).unwrap();
|
||||
assert_eq!(info, loaded);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_get_draft_no_draft() {
|
||||
let t = TestContext::new().await;
|
||||
let chat = t.get_self_chat().await;
|
||||
@@ -3448,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.
|
||||
@@ -3464,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;
|
||||
@@ -3478,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?;
|
||||
@@ -3499,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;
|
||||
@@ -3513,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;
|
||||
@@ -3558,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?;
|
||||
@@ -3610,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;
|
||||
@@ -3623,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;
|
||||
@@ -3698,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)
|
||||
@@ -3712,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);
|
||||
|
||||
@@ -3760,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;
|
||||
|
||||
@@ -3775,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;
|
||||
@@ -3802,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;
|
||||
@@ -3831,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();
|
||||
@@ -3855,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;
|
||||
@@ -3886,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;
|
||||
|
||||
@@ -3921,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;
|
||||
|
||||
@@ -3971,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;
|
||||
@@ -3991,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();
|
||||
@@ -4011,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;
|
||||
|
||||
@@ -4031,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();
|
||||
@@ -4048,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);
|
||||
@@ -4080,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;
|
||||
@@ -4183,12 +4241,12 @@ 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;
|
||||
|
||||
async fn msg_from_bob(t: &TestContext, num: u32) -> Result<()> {
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
t,
|
||||
format!(
|
||||
"From: bob@example.net\n\
|
||||
@@ -4211,17 +4269,17 @@ mod tests {
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
chat_id.accept(&t).await?;
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// not muted chat is unarchived on receiving a message
|
||||
msg_from_bob(&t, 2).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 0);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
|
||||
|
||||
// forever muted chat is not unarchived on receiving a message
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
set_muted(&t, chat_id, MuteDuration::Forever).await?;
|
||||
msg_from_bob(&t, 3).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// otherwise muted chat is not unarchived on receiving a message
|
||||
set_muted(
|
||||
@@ -4235,7 +4293,7 @@ mod tests {
|
||||
)
|
||||
.await?;
|
||||
msg_from_bob(&t, 4).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// expired mute will unarchive the chat
|
||||
set_muted(
|
||||
@@ -4249,19 +4307,19 @@ mod tests {
|
||||
)
|
||||
.await?;
|
||||
msg_from_bob(&t, 5).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 0);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
|
||||
|
||||
// no unarchiving on sending to muted chat or on adding info messages to muted chat
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
set_muted(&t, chat_id, MuteDuration::Forever).await?;
|
||||
send_text_msg(&t, chat_id, "out".to_string()).await?;
|
||||
add_info_msg(&t, chat_id, "info", time()).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
|
||||
|
||||
// finally, unarchive on sending to not muted chat
|
||||
set_muted(&t, chat_id, MuteDuration::NotMuted).await?;
|
||||
send_text_msg(&t, chat_id, "out2".to_string()).await?;
|
||||
assert_eq!(get_archived_cnt(&t).await?, 0);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4277,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;
|
||||
|
||||
@@ -4289,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();
|
||||
@@ -4334,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")
|
||||
@@ -4352,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")
|
||||
@@ -4375,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?;
|
||||
@@ -4393,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")
|
||||
@@ -4444,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?;
|
||||
@@ -4461,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?;
|
||||
@@ -4491,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")
|
||||
@@ -4559,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;
|
||||
|
||||
@@ -4602,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;
|
||||
|
||||
@@ -4621,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;
|
||||
@@ -4648,7 +4706,7 @@ mod tests {
|
||||
assert_eq!(msg.match_indices("Gr.").count(), 1);
|
||||
|
||||
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
|
||||
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
|
||||
dc_receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
|
||||
let msg = bob.get_last_msg().await;
|
||||
|
||||
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
@@ -4667,7 +4725,7 @@ mod tests {
|
||||
assert_eq!(msg.match_indices("Chat-").count(), 0);
|
||||
|
||||
// Alice receives this message - she can still detect the group by the `References:`-header
|
||||
receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
|
||||
dc_receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
assert_eq!(msg.text, Some("ho!".to_string()));
|
||||
@@ -4675,12 +4733,12 @@ 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;
|
||||
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -4720,14 +4778,14 @@ 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;
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -4770,11 +4828,11 @@ 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;
|
||||
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -4791,7 +4849,7 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert!(Chat::load_from_db(&t, chat_id).await?.is_contact_request());
|
||||
assert_eq!(get_archived_cnt(&t).await?, 0);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 0);
|
||||
|
||||
// archive request without accepting or blocking
|
||||
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
|
||||
@@ -4800,7 +4858,7 @@ mod tests {
|
||||
assert_eq!(chats.len(), 1);
|
||||
let chat_id = chats.get_chat_id(0)?;
|
||||
assert!(chat_id.is_archived_link());
|
||||
assert_eq!(get_archived_cnt(&t).await?, 1);
|
||||
assert_eq!(dc_get_archived_cnt(&t).await?, 1);
|
||||
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
@@ -4810,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;
|
||||
|
||||
@@ -4821,7 +4879,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Alice receives a classic (non-chat) message from Bob.
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -4855,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?;
|
||||
@@ -4878,7 +4936,7 @@ mod tests {
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
let file = alice.get_blobdir().join(filename);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
File::create(&file).await?.write_all(bytes).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
@@ -4898,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",
|
||||
@@ -4909,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",
|
||||
@@ -4920,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",
|
||||
@@ -4931,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;
|
||||
@@ -4943,7 +5001,7 @@ mod tests {
|
||||
let file_name = "sticker.jpg";
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
let file = alice.get_blobdir().join(file_name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
File::create(&file).await?.write_all(bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Sticker);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
|
||||
@@ -4962,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;
|
||||
@@ -4983,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;
|
||||
|
||||
@@ -5009,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;
|
||||
@@ -5044,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;
|
||||
@@ -5094,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;
|
||||
@@ -5136,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;
|
||||
@@ -5189,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?;
|
||||
@@ -5208,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;
|
||||
@@ -5245,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?;
|
||||
@@ -5269,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?;
|
||||
@@ -5295,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;
|
||||
@@ -5338,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, _) =
|
||||
@@ -5372,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;
|
||||
|
||||
@@ -240,7 +240,7 @@ impl Chatlist {
|
||||
ids
|
||||
};
|
||||
|
||||
if add_archived_link_item && get_archived_cnt(context).await? > 0 {
|
||||
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
|
||||
if ids.is_empty() && flag_add_alldone_hint {
|
||||
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
|
||||
}
|
||||
@@ -355,7 +355,7 @@ impl Chatlist {
|
||||
}
|
||||
|
||||
/// Returns the number of archived chats
|
||||
pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
pub async fn dc_get_archived_cnt(context: &Context) -> Result<usize> {
|
||||
let count = context
|
||||
.sql
|
||||
.count(
|
||||
@@ -371,12 +371,12 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf;
|
||||
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,12 +488,12 @@ 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;
|
||||
|
||||
// receive a one-to-one-message
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -548,12 +548,12 @@ 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;
|
||||
|
||||
// receive a one-to-one-message without authname set
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: bob@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -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")
|
||||
|
||||
@@ -11,7 +11,7 @@ use sha1::{Digest, Sha1};
|
||||
fn str_to_angle(s: &str) -> f64 {
|
||||
let bytes = s.as_bytes();
|
||||
let result = Sha1::digest(bytes);
|
||||
let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
|
||||
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
|
||||
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
|
||||
f64::from(checksum) / 65536.0 * 360.0
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ use crate::blob::BlobObject;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
|
||||
use crate::events::EventType;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -196,7 +196,7 @@ impl Context {
|
||||
let value = match key {
|
||||
Config::Selfavatar => {
|
||||
let rel_path = self.sql.get_raw_config(key).await?;
|
||||
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
rel_path.map(|p| dc_get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
}
|
||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||
@@ -437,8 +437,9 @@ mod tests {
|
||||
use std::string::ToString;
|
||||
|
||||
use crate::constants;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use crate::test_utils::TestContextManager;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
#[test]
|
||||
@@ -453,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();
|
||||
@@ -470,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;
|
||||
|
||||
@@ -492,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;
|
||||
|
||||
@@ -504,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;
|
||||
|
||||
@@ -556,4 +557,68 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_change_primary_self_addr() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Alice sends a message to Bob
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_text(alice_bob_chat.id, "Hi").await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
bob_msg.chat_id.accept(&bob).await?;
|
||||
assert_eq!(bob_msg.text.unwrap(), "Hi");
|
||||
|
||||
// Alice changes her self address and reconfigures
|
||||
// (ensure_secret_key_exists() is called during configure)
|
||||
alice
|
||||
.set_primary_self_addr("alice@someotherdomain.xyz")
|
||||
.await?;
|
||||
crate::e2ee::ensure_secret_key_exists(&alice).await?;
|
||||
|
||||
assert_eq!(
|
||||
alice.get_primary_self_addr().await?,
|
||||
"alice@someotherdomain.xyz"
|
||||
);
|
||||
|
||||
// Bob sends a message to Alice, encrypting to her previous key
|
||||
let sent = bob.send_text(bob_msg.chat_id, "hi back").await;
|
||||
|
||||
// Alice set up message forwarding so that she still receives
|
||||
// the message with her new address
|
||||
let alice_msg = alice.recv_msg(&sent).await;
|
||||
assert_eq!(alice_msg.text, Some("hi back".to_string()));
|
||||
assert_eq!(alice_msg.get_showpadlock(), true);
|
||||
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
|
||||
|
||||
// Even if Bob sends a message to Alice without In-Reply-To,
|
||||
// it's still assigned to the 1:1 chat with Bob and not to
|
||||
// a group (without secondary addresses, an ad-hoc group
|
||||
// would be created)
|
||||
dc_receive_imf(
|
||||
&alice,
|
||||
b"From: bob@example.net
|
||||
To: alice@example.org
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <456@example.com>
|
||||
|
||||
Message w/out In-Reply-To
|
||||
",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
|
||||
assert_eq!(
|
||||
alice_msg.text,
|
||||
Some("Message w/out In-Reply-To".to_string())
|
||||
);
|
||||
assert_eq!(alice_msg.get_showpadlock(), false);
|
||||
assert_eq!(alice_msg.chat_id, alice_bob_chat.id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
118
src/configure.rs
118
src/configure.rs
@@ -6,25 +6,22 @@ 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::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{time, EmailAddress};
|
||||
use crate::imap::Imap;
|
||||
use crate::job;
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::oauth2::dc_get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{time, EmailAddress};
|
||||
use crate::{chat, e2ee, provider};
|
||||
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
@@ -58,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"
|
||||
@@ -103,11 +102,35 @@ impl Context {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let mut param = LoginParam::load_candidate_params(self).await?;
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config(Config::NotifyAboutWrongPw, None).await?;
|
||||
|
||||
on_configure_completed(self, param, old_addr).await?;
|
||||
if let Some(provider) = param.provider {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
if !self.config_exists(def.key).await? {
|
||||
info!(self, "apply config_defaults {}={}", def.key, def.value);
|
||||
self.set_config(def.key, Some(def.value)).await?;
|
||||
} else {
|
||||
info!(
|
||||
self,
|
||||
"skip already set config_defaults {}={}", def.key, def.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(provider.after_login_hint.to_string());
|
||||
if chat::add_device_msg(self, Some("core-provider-info"), Some(&mut msg))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!(self, "cannot add after_login_hint as core-provider-info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
success?;
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
@@ -116,54 +139,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_configure_completed(
|
||||
context: &Context,
|
||||
param: LoginParam,
|
||||
old_addr: Option<String>,
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = param.provider {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
if !context.config_exists(def.key).await? {
|
||||
info!(context, "apply config_defaults {}={}", def.key, def.value);
|
||||
context.set_config(def.key, Some(def.value)).await?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"skip already set config_defaults {}={}", def.key, def.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(provider.after_login_hint.to_string());
|
||||
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!(context, "cannot add after_login_hint as core-provider-info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if let Some(old_addr) = old_addr {
|
||||
if !addr_cmp(&new_addr, &old_addr) {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text =
|
||||
Some(stock_str::aeap_explanation_and_link(context, old_addr, new_addr).await);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.ok_or_log_msg(context, "Cannot add AEAP explanation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
@@ -180,9 +155,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
// IMAP and SMTP or not at all.
|
||||
if param.imap.oauth2 && !socks5_enabled {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
// if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
// if dc_get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
progress!(ctx, 10);
|
||||
if let Some(oauth2_addr) = get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
|
||||
if let Some(oauth2_addr) = dc_get_oauth2_addr(ctx, ¶m.addr, ¶m.imap.password)
|
||||
.await?
|
||||
.and_then(|e| e.parse().ok())
|
||||
{
|
||||
@@ -429,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;
|
||||
}
|
||||
@@ -442,9 +417,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
let create_mvbox = ctx.should_watch_mvbox().await?;
|
||||
|
||||
// Send client ID as soon as possible before doing anything else.
|
||||
imap.determine_capabilities(ctx).await?;
|
||||
|
||||
imap.configure_folders(ctx, create_mvbox).await?;
|
||||
|
||||
imap.select_with_uidvalidity(ctx, "INBOX")
|
||||
@@ -475,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?;
|
||||
|
||||
@@ -577,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
|
||||
{
|
||||
@@ -662,16 +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")
|
||||
|| e.msg
|
||||
.to_lowercase()
|
||||
.contains("failed to lookup address information")
|
||||
}) {
|
||||
if errors
|
||||
.iter()
|
||||
.all(|e| e.msg.to_lowercase().contains("could not resolve"))
|
||||
{
|
||||
return stock_str::error_no_network(context).await;
|
||||
}
|
||||
|
||||
@@ -712,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"))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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;
|
||||
@@ -16,6 +16,7 @@ use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::{dc_get_abs_path, improve_single_line_input, EmailAddress};
|
||||
use crate::events::EventType;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
@@ -24,7 +25,6 @@ use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
@@ -38,7 +38,7 @@ impl ContactId {
|
||||
pub const UNDEFINED: ContactId = ContactId::new(0);
|
||||
/// The owner of the account.
|
||||
///
|
||||
/// The email-address is set by `set_config` using "addr".
|
||||
/// The email-address is set by `dc_set_config` using "addr".
|
||||
pub const SELF: ContactId = ContactId::new(1);
|
||||
pub const INFO: ContactId = ContactId::new(2);
|
||||
pub const DEVICE: ContactId = ContactId::new(5);
|
||||
@@ -142,7 +142,7 @@ pub struct Contact {
|
||||
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
|
||||
addr: String,
|
||||
|
||||
/// Blocked state. Use contact_is_blocked to access this field.
|
||||
/// Blocked state. Use dc_contact_is_blocked to access this field.
|
||||
pub blocked: bool,
|
||||
|
||||
/// Time when the contact was seen last time, Unix time in seconds.
|
||||
@@ -212,13 +212,13 @@ pub enum Origin {
|
||||
/// address is in our address book
|
||||
AddressBook = 0x80000,
|
||||
|
||||
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
|
||||
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
|
||||
SecurejoinInvited = 0x0100_0000,
|
||||
|
||||
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
|
||||
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
|
||||
SecurejoinJoined = 0x0200_0000,
|
||||
|
||||
/// contact added mannually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
|
||||
/// contact added mannually by dc_create_contact(), this should be the largest origin as otherwise the user cannot modify the names
|
||||
ManuallyCreated = 0x0400_0000,
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ impl Contact {
|
||||
/// We assume, the contact name, if any, is entered by the user and is used "as is" therefore,
|
||||
/// normalize() is *not* called for the name. If the contact is blocked, it is unblocked.
|
||||
///
|
||||
/// To add a number of contacts, see `add_address_book()` which is much faster for adding
|
||||
/// To add a number of contacts, see `dc_add_address_book()` which is much faster for adding
|
||||
/// a bunch of addresses.
|
||||
///
|
||||
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
||||
@@ -384,10 +384,10 @@ impl Contact {
|
||||
|
||||
/// Check if an e-mail address belongs to a known and unblocked contact.
|
||||
///
|
||||
/// Known and unblocked contacts will be returned by `get_contacts()`.
|
||||
/// Known and unblocked contacts will be returned by `dc_get_contacts()`.
|
||||
///
|
||||
/// To validate an e-mail address independently of the contact database
|
||||
/// use `may_be_valid_addr()`.
|
||||
/// use `dc_may_be_valid_addr()`.
|
||||
pub async fn lookup_id_by_addr(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
@@ -676,7 +676,7 @@ impl Contact {
|
||||
|
||||
/// Returns known and unblocked contacts.
|
||||
///
|
||||
/// To get information about a single contact, see get_contact().
|
||||
/// To get information about a single contact, see dc_get_contact().
|
||||
///
|
||||
/// `listflags` is a combination of flags:
|
||||
/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
|
||||
@@ -970,11 +970,11 @@ impl Contact {
|
||||
bail!("Could not delete contact with ongoing chats");
|
||||
}
|
||||
|
||||
/// Get a single contact object. For a list, see eg. get_contacts().
|
||||
/// Get a single contact object. For a list, see eg. dc_get_contacts().
|
||||
///
|
||||
/// For contact ContactId::SELF (1), the function returns sth.
|
||||
/// like "Me" in the selected language and the email address
|
||||
/// defined by set_config().
|
||||
/// defined by dc_set_config().
|
||||
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
|
||||
@@ -1063,7 +1063,7 @@ impl Contact {
|
||||
|
||||
/// Get the contact's profile image.
|
||||
/// This is the image set by each remote user on their own
|
||||
/// using set_config(context, "selfavatar", image).
|
||||
/// using dc_set_config(context, "selfavatar", image).
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
if self.id == ContactId::SELF {
|
||||
if let Some(p) = context.get_config(Config::Selfavatar).await? {
|
||||
@@ -1071,7 +1071,7 @@ impl Contact {
|
||||
}
|
||||
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, image_rel)));
|
||||
return Ok(Some(dc_get_abs_path(context, image_rel)));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
@@ -1429,7 +1429,7 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.filter_map(|chunk| {
|
||||
let name = chunk.first()?;
|
||||
let name = chunk.get(0)?;
|
||||
let addr = chunk.get(1)?;
|
||||
Some((*name, *addr))
|
||||
})
|
||||
@@ -1438,12 +1438,15 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_std::fs::File;
|
||||
use async_std::io::WriteExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
#[test]
|
||||
@@ -1505,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;
|
||||
|
||||
@@ -1569,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);
|
||||
@@ -1581,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;
|
||||
@@ -1682,12 +1685,12 @@ 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;
|
||||
|
||||
// first message creates contact and one-to-one-chat without name set
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: f@example.org\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -1716,7 +1719,7 @@ mod tests {
|
||||
assert_eq!(contacts.len(), 1);
|
||||
|
||||
// second message inits the name
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Flobbyfoo <f@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -1744,7 +1747,7 @@ mod tests {
|
||||
assert_eq!(contacts.len(), 1);
|
||||
|
||||
// third message changes the name
|
||||
receive_imf(
|
||||
dc_receive_imf(
|
||||
&t,
|
||||
b"From: Foo Flobby <f@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
@@ -1794,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;
|
||||
|
||||
@@ -1822,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;
|
||||
|
||||
@@ -1873,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;
|
||||
|
||||
@@ -1922,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;
|
||||
|
||||
@@ -1955,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;
|
||||
|
||||
@@ -1992,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;
|
||||
|
||||
@@ -2031,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;
|
||||
|
||||
@@ -2056,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?;
|
||||
@@ -2075,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;
|
||||
|
||||
@@ -2120,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;
|
||||
@@ -2185,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;
|
||||
@@ -2197,7 +2200,10 @@ CCCB 5AA9 F6E1 141C 9431
|
||||
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
|
||||
|
||||
let avatar_src = alice1.get_blobdir().join("avatar.png");
|
||||
tokio::fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES).await?;
|
||||
File::create(&avatar_src)
|
||||
.await?
|
||||
.write_all(test_utils::AVATAR_900x900_BYTES)
|
||||
.await?;
|
||||
|
||||
alice1
|
||||
.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
@@ -2241,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;
|
||||
|
||||
@@ -2260,7 +2266,7 @@ Chat-Version: 1.0
|
||||
Date: Sun, 22 Mar 2020 22:37:55 +0000
|
||||
|
||||
Hi."#;
|
||||
receive_imf(&alice, mime, false).await?;
|
||||
dc_receive_imf(&alice, mime, false).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
let timestamp = msg.get_timestamp();
|
||||
|
||||
108
src/context.rs
108
src/context.rs
@@ -3,18 +3,20 @@
|
||||
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;
|
||||
use crate::constants::DC_VERSION_STR;
|
||||
use crate::contact::Contact;
|
||||
use crate::dc_tools::{duration_to_str, time};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::login_param::LoginParam;
|
||||
@@ -23,7 +25,6 @@ use crate::quota::QuotaInfo;
|
||||
use crate::ratelimit::Ratelimit;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::{duration_to_str, time};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Context {
|
||||
@@ -61,11 +62,6 @@ pub struct InnerContext {
|
||||
/// Set to `None` if quota was never tried to load.
|
||||
pub(crate) quota: RwLock<Option<QuotaInfo>>,
|
||||
|
||||
/// Server ID response if ID capability is supported
|
||||
/// and the server returned non-NIL on the inbox connection.
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc2971>
|
||||
pub(crate) server_id: RwLock<Option<HashMap<String, String>>>,
|
||||
|
||||
pub(crate) last_full_folder_scan: Mutex<Option<Instant>>,
|
||||
|
||||
/// ID for this `Context` in the current process.
|
||||
@@ -79,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.
|
||||
@@ -119,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.
|
||||
@@ -130,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)
|
||||
}
|
||||
|
||||
@@ -176,7 +172,7 @@ impl Context {
|
||||
events: Events,
|
||||
) -> Result<Context> {
|
||||
ensure!(
|
||||
blobdir.is_dir(),
|
||||
blobdir.is_dir().await,
|
||||
"Blobdir does not exist: {}",
|
||||
blobdir.display()
|
||||
);
|
||||
@@ -193,12 +189,11 @@ impl Context {
|
||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||
events,
|
||||
scheduler: RwLock::new(None),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 3.0)), // Allow to send 3 messages immediately, no more than once every 20 seconds.
|
||||
quota: RwLock::new(None),
|
||||
server_id: 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 {
|
||||
@@ -435,11 +430,6 @@ impl Context {
|
||||
res.insert("socks5_enabled", socks5_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2.to_string());
|
||||
|
||||
let server_id = self.server_id.read().await;
|
||||
res.insert("imap_server_id", format!("{:?}", server_id));
|
||||
drop(server_id);
|
||||
|
||||
res.insert("secondary_addrs", secondary_addrs);
|
||||
res.insert(
|
||||
"fetch_existing_msgs",
|
||||
@@ -594,7 +584,7 @@ impl Context {
|
||||
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
do_query(
|
||||
"SELECT m.id AS id
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -618,7 +608,7 @@ impl Context {
|
||||
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
||||
// The limit is documented and UI may add a hint when getting 1000 results.
|
||||
do_query(
|
||||
"SELECT m.id AS id
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -653,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");
|
||||
@@ -680,28 +670,28 @@ mod tests {
|
||||
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
|
||||
};
|
||||
use crate::contact::ContactId;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::dc_tools::dc_create_outgoing_rfc724_mid;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::tools::create_outgoing_rfc724_mid;
|
||||
use anyhow::Context as _;
|
||||
use std::time::Duration;
|
||||
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();
|
||||
@@ -722,13 +712,13 @@ mod tests {
|
||||
\n\
|
||||
hello\n",
|
||||
contact.get_addr(),
|
||||
create_outgoing_rfc724_mid(None, contact.get_addr())
|
||||
dc_create_outgoing_rfc724_mid(None, contact.get_addr())
|
||||
);
|
||||
println!("{}", msg);
|
||||
receive_imf(t, msg.as_bytes(), false).await.unwrap();
|
||||
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;
|
||||
@@ -778,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;
|
||||
@@ -836,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;
|
||||
|
||||
@@ -906,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.
|
||||
@@ -954,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?;
|
||||
@@ -1010,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
|
||||
@@ -1043,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);
|
||||
@@ -1057,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);
|
||||
@@ -1068,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;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user