Compare commits

...

124 Commits

Author SHA1 Message Date
bjoern
e7957b1661 prepare 1.93 (#3555)
* update changelog for 1.93.0

* bump version to 1.93.0
2022-08-18 17:18:41 +02:00
Simon Laux
7d97ab2731 jsonrpc: auto restart io on setConfig for the following keys sentbox_watch, mvbox_move and only_fetch_mvbox (#3542)
Co-authored-by: Jikstra <34889164+Jikstra@users.noreply.github.com>
2022-08-18 16:27:03 +02:00
Robert Schütz
8be5ca6c30 use same encoded-words as deltachat/rust-email (#3549)
* use same encoded-words as deltachat/rust-email

* test `cargo vendor` in CI
2022-08-18 15:36:58 +02:00
bjoern
fdf91b772e globally search for media (#3528)
* get_chat_media() from any chat similar to search_msgs()

* do not return hidden media

this fixes the issue that drafts
and other hidden messages pop up in the gallery.

* add a test for get_chat_media()

* use None instead of ChatId::new(0)

* clarify scope of 'any' in get_chat_media()

* adapt json rpc to changed get_chat_media()

* jsonrpc: chat_get_media turn chat_id into option
and also still allow `0` for dev convenience
(though I'm not totally sure if thats the right decision)

* cargo fmt

Co-authored-by: Simon Laux <mobile.info@simonlaux.de>
2022-08-18 10:27:36 +02:00
Simon Laux
d297aa70bf rebuild json rpc types, to rebase prs to unclutter diff 2022-08-17 19:10:38 +02:00
picoHz
4877f56752 rename strum::EnumProperty to avoid name conflict 2022-08-16 22:23:43 +02:00
link2xt
839021f9eb python: enable certificate checks in cloned accounts 2022-08-15 14:40:33 +00:00
missytake
8533057881 python tests: actually the contacts aren't unified with AEAP
This reverts commit 0aaa33a0d2fd75ff6297ba681ed9de4127bbc995.
2022-08-15 01:07:42 +02:00
missytake
6aa5c60963 tests: ensure that contact of ac1 stays the same 2022-08-15 01:07:42 +02:00
missytake
f548b248eb use a verified group for the AEAP test 2022-08-15 01:07:42 +02:00
missytake
5e06568ac6 python test for AEAP flow 2022-08-15 01:07:42 +02:00
missytake
9707f13a42 deprecated check whether account can be reconfigured 2022-08-15 01:07:42 +02:00
missytake
82d36e604c don't raise an error if addr changes 2022-08-15 01:07:42 +02:00
missytake
c9fc353b60 actually, since AEAP it's fine if the addr changes 2022-08-15 01:07:42 +02:00
link2xt
8b2ece63ab scripts/coverage.sh: enable -Cdebuginfo=2
It is needed to generate .gcno and .gcda files.

Debug info has been disabled in a8c389c3b4
which broke coverage script.
2022-08-14 18:37:12 +00:00
link2xt
14045a6162 ci: error on clippy warnings and check repl 2022-08-14 19:54:32 +02:00
link2xt
c0d1c97490 Simplify provider update script with pathlib 2022-08-13 13:57:44 +00:00
link2xt
50e53f2c82 node: remove unused segfault.js and segfault2.js files 2022-08-08 21:59:04 +00:00
link2xt
95a9f1e2f8 Build with jsonrpc in install_python_bindings.py
Python bindings expect all functions defined in deltachat.h
to be available, even if there is no high-level interface.

scripts/run-python-test.sh doesn't work without this.
2022-08-06 21:00:55 +00:00
link2xt
355f18184c Remove unnecessary context binding to self 2022-08-06 19:50:22 +00:00
link2xt
120a96cd8b Factor decrypt module out of e2ee module 2022-08-06 17:02:56 +00:00
link2xt
64b534fc61 Update rustyline to 10.0.0 2022-08-06 13:33:35 +00:00
Franz Heinzmann
0887acf1bf Integrate JSON-RPC API in core (#3463)
* integrate json-rpc repo
https://github.com/deltachat/deltachat-jsonrpc

* get target dir from cargo

* fix clippy

* use node 16 in ci
use `npm i` instead of `npm ci`
try fix ci script
and fix a doc comment

* fix get_provider_info docs

* refactor function name

* fix formatting
make test  pass
fix clippy

* update .gitignore

* change now returns event names as id
directly, no conversion method or number ids anymore

also longer timeout for requesting test accounts from mailadm

* fix compile after rebase

* add json api to cffi and expose it in dc node

* add some files to npm ignore
that don't need to be in the npm package

* add jsonrpc crate to set_core_version

* add jsonrpc feature flag

* call a jsonrpc function in segfault example

* break loop on empty response

* fix closing segfault
thanks again to link2xt for figguring this out

* activate other tests again

* remove selectAccount  from highlevel client

* put jsonrpc stuff in own module

* disable jsonrpc by default

* add @deltachat/jsonrpc-client
to make sure its dependencies are installed, too
whwn installing dc-node

* commit types.ts
that dc-node has everything it needs to provide @deltachat/jsonrpc-client
without an extra ts compile step

* improve naming

* Changes for tokio compat, upgrade to yerpc 0.3

This also changes the webserver binary to use axum in place of tide.

* Improvements to typescript package

* Improve docs.

* improve docs, fix example

* Fix CFFI for JSON-RPC changes

* use stable toolchain not 1.56.0

* fix ci

* try to fix ci

* remove emtpy file
allow unused code for new_from_arc

* expose anyhow errors
feature name was wrong

* use multi-threaded runtime in JSON-RPC webserver

* improve test setup and code style

* don't wait for IO on webserver start

* Bump yerpc to 0.3.1 with fix for axum server

* update todo document
remove specific api stuff for now,
we now have the an incremental aproach on moving
not the all at-once effort I though it would be

* remove debug logs

* changelog entry about the jsonrpc

* Fix method name casings and cleanups

* Improve JSON-RPC CI, no need to build things multiple times

* Naming consistency: Use DeltaChat not Deltachat

* Improve documentation

* fix docs

* adress dig's comments
- description in cargo.toml
- impl From<EventType> for EventTypeName
- rename `CommandApi::new_from_arc` -> `CommandApi::from_arc`
- pre-allocate if we know the entry count already
- remove unused enumerate
- remove unused serde attribute comment
- rename `FullChat::from_dc_chat_id` -> `FullChat::try_from_dc_chat_id`

* make it more idiomatic:
rename `ContactObject::from_dc_contact -> `ContactObject::try_from_dc_contact`

* apply link2xt's suggestions:
- unref jsonrpc_instance in same thread it was created in
- increase `max_queue_size` from 1 to 1000

* reintroduce segfault test script

* remove unneeded context
thanks to link2xt for pointing that out

* Update deltachat-ffi/deltachat.h

Co-authored-by: bjoern <r10s@b44t.com>

* Update deltachat-ffi/deltachat.h

Co-authored-by: bjoern <r10s@b44t.com>

* make sure to use dc_str_unref instead of free
on cstrings returned/owned by rust

* Increase online test timeouts for CI

* fix the typos
thanks to ralphtheninja for finding them

* restore same configure behaviour as desktop:
make configure restart io with the old configuration if it had one on error

* found another segfault:
this time in batch_set_config

* remove print from test

* make dcn_json_rpc_request return undefined instead of not returning
this might have been the cause for the second segfault

* add set_config_from_qr to jsonrpc

* add `add_device_message` to jsonrpc

* jsonrpc: add `get_fresh_msgs` and `get_fresh_msg_cnt`

* jsonrpc: add dm_chat_contact to ChatListItemFetchResult

* add webxdc methods to jsonrpc:
- `webxdc_send_status_update`
- `webxdc_get_status_updates`
- `message_get_webxdc_info`

* add `chat_get_media` to jsonrpc
also add viewtype wrapper enum and use it in `MessageObject`,
additionally to using it in `chat_get_media`

* use camelCase in all js object properties

* Add check_qr function to jsonrpc

* Fixed clippy errors and formatting

* Fixed formatting

* fix changelog ordering after rebase

* fix compile after merging in master branch

Co-authored-by: Simon Laux <mobile.info@simonlaux.de>
Co-authored-by: Simon Laux <Simon-Laux@users.noreply.github.com>
Co-authored-by: bjoern <r10s@b44t.com>
Co-authored-by: flipsimon <28535045+flipsimon@users.noreply.github.com>
2022-08-04 16:56:37 +00:00
dependabot[bot]
142c02b425 Merge pull request #3532 from deltachat/dependabot/cargo/once_cell-1.13.0 2022-08-02 18:56:27 +00:00
dependabot[bot]
3f6fbdbd21 Merge pull request #3484 from deltachat/dependabot/cargo/openssl-src-111.22.01.1.1q 2022-08-02 18:26:17 +00:00
dependabot[bot]
25ea739e4d Merge pull request #3537 from deltachat/dependabot/cargo/criterion-0.3.6 2022-08-02 18:25:50 +00:00
dependabot[bot]
df1e95ef18 cargo: bump once_cell from 1.12.0 to 1.13.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.12.0 to 1.13.0.
- [Release notes](https://github.com/matklad/once_cell/releases)
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.12.0...v1.13.0)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-02 18:21:48 +00:00
link2xt
5f97cf9de1 mergeable: do not require changelog for deltachat-ffi/Cargo.lock 2022-08-02 18:20:14 +00:00
dependabot[bot]
cb003cb21d Merge pull request #3535 from deltachat/dependabot/cargo/serde-1.0.141 2022-08-02 16:16:51 +00:00
dependabot[bot]
3ce636fa9a Merge pull request #3534 from deltachat/dependabot/cargo/backtrace-0.3.66 2022-08-02 16:16:14 +00:00
dependabot[bot]
39b5c5b946 Merge pull request #3533 from deltachat/dependabot/cargo/anyhow-1.0.59 2022-08-02 16:15:42 +00:00
dependabot[bot]
29a4dd20dd Merge pull request #3536 from deltachat/dependabot/cargo/regex-1.6.0 2022-08-02 16:14:44 +00:00
dependabot[bot]
f38df31509 Merge pull request #3539 from deltachat/dependabot/cargo/tokio-1.20.1 2022-08-02 16:08:12 +00:00
dependabot[bot]
4fc9257f65 Merge pull request #3531 from deltachat/dependabot/cargo/image-0.24.3 2022-08-02 16:06:36 +00:00
dependabot[bot]
31a2f8c878 cargo: bump tokio from 1.19.2 to 1.20.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.19.2 to 1.20.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.19.2...tokio-1.20.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 21:07:57 +00:00
dependabot[bot]
544df73b01 cargo: bump criterion from 0.3.5 to 0.3.6
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.3.5 to 0.3.6.
- [Release notes](https://github.com/bheisler/criterion.rs/releases)
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.3.5...0.3.6)

---
updated-dependencies:
- dependency-name: criterion
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 21:07:41 +00:00
dependabot[bot]
f49dce4f95 cargo: bump regex from 1.5.6 to 1.6.0
Bumps [regex](https://github.com/rust-lang/regex) from 1.5.6 to 1.6.0.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.5.6...1.6.0)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 21:07:34 +00:00
dependabot[bot]
a54ef74c3f cargo: bump serde from 1.0.137 to 1.0.141
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.137 to 1.0.141.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.137...v1.0.141)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 21:07:28 +00:00
dependabot[bot]
750dcb500e cargo: bump backtrace from 0.3.65 to 0.3.66
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.65 to 0.3.66.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.65...0.3.66)

---
updated-dependencies:
- dependency-name: backtrace
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 21:07:20 +00:00
dependabot[bot]
15e598b4a3 cargo: bump anyhow from 1.0.58 to 1.0.59
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.58 to 1.0.59.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.58...1.0.59)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 21:07:12 +00:00
dependabot[bot]
25d255f3a9 cargo: bump image from 0.24.2 to 0.24.3
Bumps [image](https://github.com/image-rs/image) from 0.24.2 to 0.24.3.
- [Release notes](https://github.com/image-rs/image/releases)
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/commits)

---
updated-dependencies:
- dependency-name: image
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 21:06:59 +00:00
Simon Laux
75960e8782 improve error handling for account setup from qrcode closes #3192 (#3474)
* improve error handling for account setup from qrcode
closes #3192

* replace issue id with pr id in changelog

* Update src/qr.rs

Co-authored-by: bjoern <r10s@b44t.com>

* show response when it's invalid in success case, too

* fix changelog entry

Co-authored-by: bjoern <r10s@b44t.com>
2022-07-30 09:57:08 +00:00
bjoern
ab52f8c55d add getMailinglistAddr() to node (#3524)
* add getMailinglistAddr() to node

* set mailinglistAddr in toJson
2022-07-26 19:12:28 +02:00
bjoern
88be304b5b prepare 1.92 (#3525)
* update changelog for 1.92.0

* bump version to 1.92.0
2022-07-26 16:52:32 +02:00
B. Petersen
40fb02a00f Revert "add mailinglistAddr to getJson"
This reverts commit 5a4b12c914.
2022-07-26 14:36:25 +02:00
B. Petersen
5a4b12c914 add mailinglistAddr to getJson 2022-07-26 14:33:01 +02:00
bjoern
bf5edfa3b3 add ffi to get mailinglist post address (#3520)
* add ffi to get mailinglist post address

* Update deltachat-ffi/deltachat.h

Co-authored-by: Hocuri <hocuri@gmx.de>

* adapt tests to check get_mailinglist_addr()

Co-authored-by: Hocuri <hocuri@gmx.de>
2022-07-26 12:50:16 +02:00
link2xt
64dd2f4af6 Prepare release 1.91.0 2022-07-26 09:53:33 +02:00
missytake
52736f2b36 changelog entry: python method to get an account running 2022-07-25 21:56:14 +02:00
missytake
64515786be apparently lint likes long lines more than me 2022-07-25 21:56:14 +02:00
missytake
52a8ec48b7 move account initialization to separate function 2022-07-25 21:56:14 +02:00
link2xt
d72cf3fb43 mimeparser: set is_system_message for "group image changed" messages 2022-07-24 13:05:14 +00:00
link2xt
5920c5c136 Update scripts README
`coredeps` dockerfile is not outdated.

Add `run_all.sh` description.
2022-07-23 16:17:08 +00:00
link2xt
a60da6deac Simplify scripts/run_all.sh
We install `tox` via `pipx` in `Dockerfile`, there is no need to
configure path to `python3.7`.

Also remove use of `pushd`/`popd` and switch from `bash` to `/bin/sh`.
2022-07-23 16:11:01 +00:00
link2xt
aef19cb0e0 Simplify ratelimiting 2022-07-23 14:25:27 +00:00
link2xt
cddd38cdff ci: build python wheels for musl-aarch64 2022-07-17 11:54:53 +00:00
bjoern
9d5bce9b7e release 1.90.0 (#3512)
* update changelog for 1.90.0

* bump version to 1.90.0
2022-07-16 21:54:02 +00:00
Hocuri
9f2100deee (AEAP) Revert #3491, instead only replace contacts in verified groups (#3510)
#3491 introduced a bug that your address is only replaced in the first group you write to, which was rather hard to fix. In order to be able to release something, we agreed to revert it and instead only replace the contacts in verified groups (and in broadcast lists, if the signing key is verified).

Highlights:

* Revert "Only do the AEAP transition in the chat where it happened"

This reverts commit 22f4cd7b79.

* Only do the transition for verified groups (and broadcast lists)

To be exact, only do the transition if the signing key fingerpring is
verified. And only do it in verified groups and broadcast lists

* Slightly adapt string to this change

* Changelog
2022-07-16 21:03:34 +00:00
Hocuri
5f779ca9b2 Add AEAP device message (#3505) 2022-07-15 14:16:12 +00:00
link2xt
9926804f1b ratelimit: do not overflow leaky bucket
This way the time to wait until next message can
be sent is always limited.
2022-07-14 20:03:16 +00:00
link2xt
294d8862e4 Do not treat non-failed DSNs as NDNs 2022-07-14 20:01:45 +00:00
link2xt
d09be1f7e3 python: don't build wheels for dependencies 2022-07-14 14:39:39 +02:00
link2xt
ed5fc820c2 python: move most of setup.py to pyproject.toml 2022-07-14 14:39:39 +02:00
link2xt
248d9600c5 python: remove fail_test.py
It was added in 11fa60d690
2022-07-12 00:46:28 +00:00
link2xt
cfadf20d08 Skip missing Python interpreters in scripts/run_all.sh
musllinux images miss PyPy interpreters,
we want to skip building PyPy wheels for musl
instead of failing the build.

Also remove workaround from CI scripts.
2022-07-10 23:15:51 +00:00
link2xt
32eb016ee7 mimeparser: do not squash NDN text parts into attachments
Text part usually contains an error message that we want to display in
the UI.
2022-07-10 18:18:45 +00:00
link2xt
2e009d1327 Add PR number to changelog 2022-07-10 14:22:29 +00:00
link2xt
797b9fb087 ci: do not modify run_all.sh in-tree
This changes the version of built wheels.
2022-07-10 13:03:53 +00:00
Asiel Díaz Benítez
e5c255e011 Merge pull request #3492 from deltachat/adb/qr-mailto-draft
Detect draft from QR with mailto data
2022-07-09 22:49:04 -04:00
link2xt
39ae44dbf0 rustfmt 2022-07-09 22:27:42 +00:00
link2xt
c9a1ebf257 Collapse match patterns 2022-07-09 22:24:51 +00:00
link2xt
f5c5429fe8 Do not build PyPy wheels on musllinux
musllinux images don't have PyPy installed
2022-07-09 21:35:24 +00:00
adbenitez
8c70393c90 fix other tests 2022-07-09 16:35:00 -04:00
adbenitez
37cb16b95c update documentation 2022-07-09 16:23:31 -04:00
link2xt
fe420ac559 Release 1.89.0 2022-07-09 19:51:36 +00:00
adbenitez
50e1866572 update CHANGELOG.md 2022-07-09 15:51:10 -04:00
link2xt
88402288f9 Update Python bindings README
Wheels are now published to PyPI, recommend it instead of devpi. We
build the wheels only for releases anyway.

Suggest using tox instead of listing all the pytest dependencies to
avoid keeping them up to date in the readme.

We no longer publish `docker/coredeps` images, they cannot be
pulled from a container registry anymore.

Troubleshooting section is outdated, because vsyscall emulation is
only needed for manylinux2010 images, not manylinux2014 or
musllinux_1_1 images.

Mention Podman as an alternative to Docker.

Link to https://py.delta.chat/ instead of only examples.

Remove note about wheels for Mac and Windows. Nobody requests them
anyway.
2022-07-09 19:50:44 +00:00
adbenitez
68a15725d2 fix tests 2022-07-09 15:45:06 -04:00
link2xt
64b4d421c7 Make scripts/set_core_version.py executable 2022-07-09 19:42:03 +00:00
Hocuri
dde5223929 Only do the AEAP transition in the chat where it happened (#3491)
* Only do the AEAP transition in the chat where it happened

* Create a chat with the new contact

* changelog
2022-07-09 21:34:38 +02:00
link2xt
6ae278f735 cargo fmt 2022-07-09 19:25:04 +00:00
link2xt
0b2c3ee163 Use as_deref() instead of unwrapping Option 2022-07-09 19:25:04 +00:00
adbenitez
62a0ce29e9 decode draft 2022-07-09 15:19:39 -04:00
bjoern
91b345abfe handle webxdc updates for not downloaded instances (#3487)
* save webxdc-updates for not yet downloaded messages, that are probably webxdc instances then

* test webxdc updates received while instance is not yet downloaded

* keep msg_id on downloading messages

keeping msg_id on downloading messages
has the advantage that webxdc updates and other references to the msg_id
can be processed as usual.

if a message expands to multiple msg_id,
the last one is kept,
however, this does not affect webxdc at all.

(alternatives may be to update `msgs_status_updates`
but that seems more complicated and even less elegant,
another alternative would be to use different keys (eg. `rfc274_mid`),
but that also seems not to be much easier and would waste space as well.
also both alternatives would need adaption for other foreign keys)

* update CHANGELOG

* do not emit WebxdcStatusUpdate event in case the message is not yet downloaded

* move DELETE/UPDATE to an transaction

* make merge_msg_id() a little less confusing

* use some webxdc-update-param from placeholder

(the placeholder may be updated,
the just downloaded messages is not)

* more precise function name

* test not directly downloading status updates

* test not directly downloading mdn
2022-07-09 18:26:12 +02:00
bjoern
2f3f5a34b4 do not SELECT timestamp if not used (#3493)
* do not `SELECT timestamp` if not used

ordering is by `id` since #2364, selecting `timestamp` is not needed.

(came over this when keeping `id` on downloading in #3487 -
had in mind there was sth. special with ids ...
however, the assumption of #2364 is even more true with #3487 -
before, new (and then maybe much larger) ids were inserted
and could result in wrong search result ordering)

* remove another unused `SELECT timestamp`
2022-07-09 18:25:57 +02:00
link2xt
d9003f344e tox.ini: do not pass through unused TRAVIS environment variable 2022-07-09 13:17:36 +00:00
adbenitez
6b9aac5234 detect draft when scanning QR with mailto link as data 2022-07-09 05:42:39 -04:00
Jikstra
02a3c5d308 Update release instructions for node 2022-07-07 19:22:15 +02:00
link2xt
50c398c2cc Remove bashism in doxygen CI script 2022-07-07 00:24:11 +00:00
link2xt
4a6a08578f Cleanup doxygen CI
Remove unused docker-doxygen Dockerfile.
Switch scripts/run-doxygen.sh from bash to sh.
Use docker.io/alpine image instead of unsupported hrektts/doxygen
2022-07-07 00:31:57 +00:00
link2xt
4f2c68e5a4 Accept more error variants in nicer_configuration_error
musl libc returns "failed to lookup address information: Name does not resolve" in some cases.
2022-07-06 22:58:56 +00:00
dependabot[bot]
a797961a25 cargo: bump openssl-src from 111.21.0+1.1.1p to 111.22.0+1.1.1q
Bumps [openssl-src](https://github.com/alexcrichton/openssl-src-rs) from 111.21.0+1.1.1p to 111.22.0+1.1.1q.
- [Release notes](https://github.com/alexcrichton/openssl-src-rs/releases)
- [Commits](https://github.com/alexcrichton/openssl-src-rs/commits)

---
updated-dependencies:
- dependency-name: openssl-src
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-06 20:17:02 +00:00
link2xt
e149cd7afe Release 1.88.0 2022-07-06 01:46:26 +00:00
B. Petersen
874d103a8d update CHANGELOG 2022-07-05 18:00:55 +02:00
B. Petersen
b85a369341 increase ratelimit
we want to prevent runaway things,
not restrict normal usage too much,
this limit seems to be more reasonable
also for most round-based games.
2022-07-05 18:00:55 +02:00
B. Petersen
522040810d update 163.com in provider database 2022-07-05 18:00:42 +02:00
Hocuri
e60164b5f3 Add AEAP transition (#3385) 2022-07-05 14:20:01 +02:00
dignifiedquire
9f4646e8bd update async-zip to fixed version 2022-07-04 19:14:12 +02:00
B. Petersen
5a9e18ed72 add a test for a .xdc failing with recent zip crate 2022-07-04 19:14:12 +02:00
B. Petersen
d40960bcfd show webxdc information in repl tool 2022-07-04 14:03:05 +02:00
link2xt
ae8e81ceb2 node: remove unused finalize_account function 2022-07-04 08:19:11 +00:00
B. Petersen
a74c850031 add more details to fallback NDN
if the NDN has no specific error text,
but we know the failed recipient address,
add these information the final message.
2022-07-03 23:16:13 +02:00
link2xt
ece5eb065a location: don't ignore KML parsing errors 2022-07-03 20:11:12 +00:00
Hocuri
7598c50dba Turn off hard errors for lints (#3441)
It happened multiple times now that I wanted to quickly execute a test, but because of a warning that had become an error, it didn't execute.

This turns warnings into warnings again; our CI will fail if there is a warning, anyway (because of RUSTFLAGS: -Dwarnings)
2022-07-03 11:05:16 +00:00
link2xt
5078ca6d8e Remove unnecessary as_deref() 2022-07-03 10:20:57 +00:00
link2xt
ddf9f0cd93 Add PyPy support
Run CI against PyPy and build PyPy wheels.
2022-07-03 09:33:35 +00:00
link2xt
75f0537181 ci: update setup-python action 2022-07-03 09:33:22 +00:00
link2xt
c6a47e359f Configure movebox folder by selecting it
Don't use LIST so DeltaChat folder can be configured even if it is
hidden, for example by ACL [1].

[1] https://datatracker.ietf.org/doc/html/rfc4314
2022-07-03 09:29:16 +00:00
link2xt
51aead6b58 Add support for IMAP ID extension 2022-07-03 09:13:56 +00:00
Simon Laux
d738371848 node: fix readme guide for building x64 on M1 mac 2022-07-01 14:19:59 +02:00
Friedel Ziegelmayer
6cabb32aa5 feat: update pgp to 0.8 and rand to 0.8 (#3467)
* feat: update pgp to 0.8 and rand to 0.8

* update changelog
2022-07-01 13:15:37 +02:00
Friedel Ziegelmayer
3e2af8537c refactor: remove dc_ prefix
* refactor: remove `dc_` prefix from mods

* refactor: remove dc_ prefix from functions

* fix: avoid temporary `File`s to avoid race conditions

* test(pgp): fix runtime usage in Lazy

Based on #3462

* fixup: undo some comment changes
2022-07-01 12:20:20 +02:00
link2xt
26e802cf0f Fix trim_split_whitespace clippy lint 2022-06-30 20:56:26 +00:00
link2xt
a467ca22fb Disable new format_push_string clippy lint 2022-06-30 20:47:01 +00:00
bjoern
b376790b78 ignore status footer updates from mailinglists (#3460)
* ignore status/footer updates from mailinglist messages

mailinglist software often modified existing footers
or adds footers on their own.

therefore,therefore, these footers often do not reflect the status/footer set by the user.

* test status footers not updated from mailinglists
2022-06-29 09:32:12 +02:00
bjoern
6d4fecb274 smarter mailinglist's square bracket prefixes (#3452)
some mailinglists have their name in square brackets prepended to the subject.

we pick up that name and remove the square brackets,
as these do not look good as the chat name in delta chat.

if a mailing list has a sequence of square brackets, we use all of them
(this seems to be okayish, at least i do not know of any complains).

however, the removal of square brackets was not nice for sequences,
resulting in `ml topic] [sub topic`.

this pr removes the square brackets only for the first name
and leave the other ones as is.
2022-06-28 10:28:39 +02:00
link2xt
14421c6e00 Move changelog item to the correct section 2022-06-27 12:08:31 +00:00
Friedel Ziegelmayer
290ee20e63 feat: migrate from async-std to tokio 2022-06-27 14:05:21 +02:00
link2xt
997fb4061a Build musl wheels 2022-06-26 23:09:42 +00:00
link2xt
92b38cebe4 Fixup run_all.sh 2022-06-26 23:02:53 +00:00
link2xt
8ebe86d9e9 Release 1.87.0 2022-06-26 22:18:47 +00:00
bjoern
84cabbcb7e limit rate of webxdc updates (#3417)
* more flexible render_webxdc_status_update_object()

* delay webxdc updates when ratelimit is reached

* inject updates messages to the SMTP loop as needed

this avoids starting several tasks
and allows sending updates out after a restart of the app.

* use mutex to prevent race conditions in status updates

* check ratelimiter only before the sending loop; it won't change until messages are actually sent out

* fix typo

* prefer standard type declaration over turbofish syntax

* use UNIQUE and ON CONFLICT for query smtp_status_updates

* combine DELETE+SELECT to one atomic statement

* as all operations on smtp_status_updates are now atomic, a mutex is no longer needed

* test DELETE+RETURNING statement

* simplify calls to can_send()

* comment about ratelimit boolean in send_smtp_messages()
2022-06-26 22:03:14 +02:00
link2xt
f23fa1c9d3 Fix path to coredeps in concourse pipeline 2022-06-26 11:47:05 +00:00
link2xt
5053a22f96 Use single universal coredeps Dockerfile 2022-06-26 11:40:19 +00:00
164 changed files with 9968 additions and 4280 deletions

View File

@@ -16,7 +16,7 @@ mergeable:
required: ['CHANGELOG.md']
- do: dependent
changed:
file: 'deltachat-ffi/**'
file: 'deltachat-ffi/src/**'
required: ['CHANGELOG.md']
fail:
- do: checks

View File

@@ -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
args: --workspace --tests --examples --benches --features repl -- -D warnings
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.0
# Minimum Supported Rust Version = 1.56.1
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.56.0
rust: 1.56.1
python: 3.7
runs-on: ${{ matrix.os }}
steps:
@@ -116,9 +116,14 @@ jobs:
command: test
args: --all
- name: test cargo vendor
uses: actions-rs/cargo@v1
with:
command: vendor
- name: install python
if: ${{ matrix.python }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
@@ -131,7 +136,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: -p deltachat_ffi
args: -p deltachat_ffi --features jsonrpc
- name: run python tests
if: ${{ matrix.python }}
@@ -141,3 +146,18 @@ jobs:
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e lint,mypy,doc,py3
- name: install pypy
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: 'pypy${{ matrix.python }}'
- name: run pypy tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e pypy3

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

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

View File

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

View File

@@ -2,6 +2,106 @@
## Unreleased
### API-Changes
### Changes
### Fixes
## 1.93.0
### API-Changes
- added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463 #3554 #3542
- JSON RPC methods in CFFI #3463:
- `dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);`
- `void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);`
- `void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, char* request);`
- `char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);`
- node: JSON RPC methods #3463:
- `AccountManager.prototype.startJsonRpcHandler(callback: ((response: string) => void)): void`
- `AccountManager.prototype.jsonRpcRequest(message: string): void`
### Changes
- use [pathlib](https://docs.python.org/3/library/pathlib.html) in provider update script #3543
- `dc_get_chat_media()` can return media globally #3528
- node: add `getMailinglistAddr()` #3524
- avoid duplicate encoded-words package and test `cargo vendor` in ci #3549
- python: don't raise an error if addr changes #3530
- improve coverage script #3530
### Fixes
- improved error handling for account setup from qrcode #3474
- python: enable certificate checks in cloned accounts #3443
## 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
@@ -9,9 +109,10 @@
- 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
- set a default error if NDN does not provide an error #3410
- python: avoid exceptions when messages/contacts/chats are compared with `None`
- node: wait for the event loop to stop before destroying contexts #3431 #3451
- emit configuration errors via event on failure #3433
@@ -24,6 +125,7 @@
- python: added `Message.is_videochat_invitation()` #3416
- python: added support for "videochat" and "webxdc" view types to `Message.new_empty()` #3416
## 1.86.0
### API-Changes

View File

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

2256
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.86.0"
version = "1.93.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
@@ -9,6 +9,7 @@ rust-version = "1.56"
[profile.dev]
debug = 0
panic = 'abort'
opt-level = 1
[profile.release]
lto = true
@@ -19,23 +20,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" }
async-native-tls = { version = "0.3" }
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", default-features=false, features = ["smtp-transport", "socks5"] }
async-std-resolver = "0.21"
async-std = { version = "1" }
async-tar = { version = "0.4", default-features=false }
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.21"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
backtrace = "0.3"
base64 = "0.13"
bitflags = "1.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
dirs = { version = "4", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.3", 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"
@@ -45,18 +46,18 @@ native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.12.0"
once_cell = "1.13.0"
percent-encoding = "2.0"
pgp = { version = "0.7", default-features = false }
pgp = { version = "0.8", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.23"
r2d2 = "0.8"
r2d2_sqlite = "0.20"
rand = "0.7"
regex = "1.5"
rand = "0.8"
regex = "1.6"
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustyline = { version = "9", optional = true }
rustyline = { version = "10", optional = true }
sanitize-filename = "0.4"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
@@ -65,32 +66,36 @@ sha2 = "0.10"
smallvec = "1"
strum = "0.24"
strum_macros = "0.24"
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
thiserror = "1"
toml = "0.5"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
fast-socks5 = "0.4"
fast-socks5 = "0.8"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.3.3"
textwrap = "0.15.0"
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
async-channel = "1.6.1"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.9", features = ["fs"] }
reqwest = { version = "0.11.11", features = ["json"] }
async_zip = { git = "https://github.com/dignifiedquire/rs-async-zip", branch = "main", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
async-std = { version = "1", features = ["unstable", "attributes"] }
criterion = { version = "0.3.4", features = ["async_std"] }
criterion = { version = "0.3.6", features = ["async_tokio"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
[workspace]
members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc"
]
[[example]]
@@ -132,5 +137,10 @@ harness = false
default = ["vendored"]
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled-sqlcipher-vendored-openssl"]
vendored = [
"async-native-tls/vendored",
"async-smtp/native-tls-vendored",
"rusqlite/bundled-sqlcipher-vendored-openssl",
"reqwest/native-tls-vendored"
]
nightly = ["pgp/nightly"]

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
use async_std::path::Path;
use std::path::Path;
use criterion::async_executor::AsyncStdExecutor;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
@@ -10,9 +9,7 @@ use deltachat::Events;
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
let id = 100;
let context = Context::new(dbfile.into(), id, Events::new())
.await
.unwrap();
let context = Context::new(dbfile, id, Events::new()).await.unwrap();
for c in chats.iter().take(10) {
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
@@ -23,8 +20,10 @@ fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let chats: Vec<_> = async_std::task::block_on(async {
let context = Context::new((&path).into(), 100, Events::new())
let rt = tokio::runtime::Runtime::new().unwrap();
let chats: Vec<_> = rt.block_on(async {
let context = Context::new(Path::new(&path), 100, Events::new())
.await
.unwrap();
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
@@ -33,7 +32,7 @@ fn criterion_benchmark(c: &mut Criterion) {
});
c.bench_function("chat::get_chat_msgs (load messages from 10 chats)", |b| {
b.to_async(AsyncStdExecutor)
b.to_async(&rt)
.iter(|| get_chat_msgs_benchmark(black_box(path.as_ref()), black_box(&chats)))
});
} else {

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
use async_std::task::block_on;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::Events;
use std::path::Path;
async fn search_benchmark(path: impl AsRef<Path>) {
let dbfile = path.as_ref();
async fn search_benchmark(dbfile: impl AsRef<Path>) {
let id = 100;
let context = Context::new(dbfile.into(), id, Events::new())
let context = Context::new(dbfile.as_ref(), id, Events::new())
.await
.unwrap();
@@ -20,8 +18,10 @@ fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let rt = tokio::runtime::Runtime::new().unwrap();
c.bench_function("search hello", |b| {
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
b.to_async(&rt).iter(|| search_benchmark(black_box(&path)))
});
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");

View File

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

View File

@@ -1,4 +1,3 @@
use std::io::Write;
use std::path::PathBuf;
use std::{env, fs};
@@ -28,8 +27,9 @@ fn main() {
);
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
.unwrap()
.write_all(pkg_config.as_bytes())
.unwrap();
fs::write(
target_path.join("pkgconfig").join("deltachat.pc"),
pkg_config.as_bytes(),
)
.unwrap();
}

View File

@@ -23,7 +23,7 @@ typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
/**
* @mainpage Getting started
@@ -1263,7 +1263,7 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
/**
* Returns all message IDs of the given types in a chat.
* Returns all message IDs of the given types in a given chat or any chat.
* Typically used to show a gallery.
* The result must be dc_array_unref()'d
*
@@ -1273,7 +1273,8 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID to get all messages with media from.
* @param chat_id >0: get messages with media from this chat ID.
* 0: get messages with media from any chat of the currently used account.
* @param msg_type Specify a message type to query here, one of the @ref DC_MSG constants.
* @param msg_type2 Alternative message type to search for. 0 to skip.
* @param msg_type3 Alternative message type to search for. 0 to skip.
@@ -1284,7 +1285,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
/**
* Search next/previous message based on a given message and a list of types.
* The
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
@@ -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::test1=Formatted fingerprint
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::text1=Formatted fingerprint
* the scanned QR code contains a fingerprint but no e-mail address;
* suggest the user to establish an encrypted connection first.
*
@@ -2305,7 +2305,8 @@ 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,
* 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;
* ask the user if they want to start chatting;
* if so, call dc_create_chat_by_contact_id().
*
@@ -3256,6 +3257,19 @@ 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
@@ -5178,6 +5192,55 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
/**
* @class dc_jsonrpc_instance_t
*
* Opaque object for using the json rpc api from the cffi bindings.
*/
/**
* Create the jsonrpc instance that is used to call the jsonrpc.
*
* @memberof dc_accounts_t
* @param account_manager The accounts object as created by dc_accounts_new().
* @return Returns the jsonrpc instance, NULL on errors.
* Must be freed using dc_jsonrpc_unref() after usage.
*
*/
dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);
/**
* Free a jsonrpc instance.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* If NULL is given, nothing is done and an error is logged.
*/
void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* Makes an asynchronous jsonrpc request,
* returns immediately and once the result is ready it can be retrieved via dc_jsonrpc_next_response()
* the jsonrpc specification defines an invocation id that can then be used to match request and response.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param request JSON-RPC request as string
*/
void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* request);
/**
* Get the next json_rpc response, blocks until there is a new event, so call this in a loop from a thread.
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* If NULL is returned, the accounts_t belonging to the jsonrpc instance is unref'd and no more events will come;
* in this case, free the jsonrpc instance using dc_jsonrpc_unref().
*/
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
/**
* @class dc_event_emitter_t
*
@@ -6364,6 +6427,24 @@ 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
/**
* @}
*/

View File

@@ -1,4 +1,4 @@
#![deny(unused, clippy::all)]
#![warn(unused, clippy::all)]
#![allow(
non_camel_case_types,
non_snake_case,
@@ -15,17 +15,20 @@ extern crate human_panic;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt::Write;
use std::future::Future;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use async_std::sync::RwLock;
use async_std::task::{block_on, spawn};
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use num_traits::{FromPrimitive, ToPrimitive};
use once_cell::sync::Lazy;
use rand::Rng;
use tokio::runtime::Runtime;
use tokio::sync::RwLock;
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
@@ -38,6 +41,7 @@ use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use tokio::task::JoinHandle;
mod dc_array;
mod lot;
@@ -61,6 +65,23 @@ use deltachat::chatlist::Chatlist;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
where
T: Future,
{
RT.block_on(fut)
}
fn spawn<T>(fut: T) -> JoinHandle<T::Output>
where
T: Future + Send + 'static,
T::Output: Send + 'static,
{
RT.spawn(fut)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_new(
_os_name: *const libc::c_char,
@@ -77,11 +98,7 @@ pub unsafe extern "C" fn dc_context_new(
let ctx = if blobdir.is_null() || *blobdir == 0 {
// generate random ID as this functionality is not yet available on the C-api.
let id = rand::thread_rng().gen();
block_on(Context::new(
as_path(dbfile).to_path_buf().into(),
id,
Events::new(),
))
block_on(Context::new(as_path(dbfile), id, Events::new()))
} else {
eprintln!("blobdir can not be defined explicitly anymore");
return ptr::null_mut();
@@ -105,11 +122,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(
as_path(dbfile).to_path_buf().into(),
id,
Events::new(),
)) {
match block_on(Context::new_closed(as_path(dbfile), id, Events::new())) {
Ok(context) => Box::into_raw(Box::new(context)),
Err(err) => {
eprintln!("failed to create context: {:#}", err);
@@ -383,7 +396,7 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
let redirect = to_string_lossy(redirect);
block_on(async move {
match oauth2::dc_get_oauth2_url(ctx, &addr, &redirect)
match oauth2::get_oauth2_url(ctx, &addr, &redirect)
.await
.log_err(ctx, "dc_get_oauth2_url failed")
{
@@ -681,10 +694,13 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
}
let events = &*events;
events
.recv_sync()
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
block_on(async move {
events
.recv()
.await
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
})
}
#[no_mangle]
@@ -724,7 +740,7 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
}
let ctx = &*context;
block_on(async move {
let addr = dc_tools::EmailAddress::new(&to_string_lossy(addr))?;
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
let keypair = key::KeyPair {
@@ -1192,6 +1208,11 @@ pub unsafe extern "C" fn dc_get_chat_media(
return ptr::null_mut();
}
let ctx = &*context;
let chat_id = if chat_id == 0 {
None
} else {
Some(ChatId::new(chat_id))
};
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type));
let or_msg_type2 =
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {}", or_msg_type2));
@@ -1200,16 +1221,10 @@ pub unsafe extern "C" fn dc_get_chat_media(
block_on(async move {
Box::into_raw(Box::new(
chat::get_chat_media(
ctx,
ChatId::new(chat_id),
msg_type,
or_msg_type2,
or_msg_type3,
)
.await
.unwrap_or_log_default(ctx, "Failed get_chat_media")
.into(),
chat::get_chat_media(ctx, chat_id, msg_type, or_msg_type2, or_msg_type3)
.await
.unwrap_or_log_default(ctx, "Failed get_chat_media")
.into(),
))
})
}
@@ -2229,7 +2244,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
Some(ChatId::new(chat_id))
};
block_on(securejoin::dc_get_securejoin_qr(ctx, chat_id))
block_on(securejoin::get_securejoin_qr(ctx, chat_id))
.unwrap_or_else(|_| "".to_string())
.strdup()
}
@@ -2267,7 +2282,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
let ctx = &*context;
block_on(async move {
securejoin::dc_join_securejoin(ctx, &to_string_lossy(qr))
securejoin::join_securejoin(ctx, &to_string_lossy(qr))
.await
.map(|chatid| chatid.to_u32())
.log_err(ctx, "failed dc_join_securejoin() call")
@@ -2393,7 +2408,7 @@ pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut l
return "".strdup();
}
let ctx = &*context;
block_on(ctx.get_last_error()).strdup()
ctx.get_last_error().strdup()
}
// dc_array_t
@@ -2766,6 +2781,16 @@ 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() {
@@ -4093,11 +4118,11 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
/// `dc_accounts_t` in multiple threads at once.
pub struct AccountsWrapper {
inner: RwLock<Accounts>,
inner: Arc<RwLock<Accounts>>,
}
impl Deref for AccountsWrapper {
type Target = RwLock<Accounts>;
type Target = Arc<RwLock<Accounts>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -4106,7 +4131,7 @@ impl Deref for AccountsWrapper {
impl AccountsWrapper {
fn new(accounts: Accounts) -> Self {
let inner = RwLock::new(accounts);
let inner = Arc::new(RwLock::new(accounts));
Self { inner }
}
}
@@ -4126,7 +4151,7 @@ pub unsafe extern "C" fn dc_accounts_new(
return ptr::null_mut();
}
let accs = block_on(Accounts::new(as_path(dbfile).to_path_buf().into()));
let accs = block_on(Accounts::new(as_path(dbfile).into()));
match accs {
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
@@ -4298,7 +4323,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
block_on(async move {
let mut accounts = accounts.write().await;
match accounts
.migrate_account(async_std::path::PathBuf::from(dbfile))
.migrate_account(std::path::PathBuf::from(dbfile))
.await
{
Ok(id) => id,
@@ -4418,9 +4443,81 @@ pub unsafe extern "C" fn dc_accounts_get_next_event(
return ptr::null_mut();
}
let emitter = &mut *emitter;
emitter
.recv_sync()
block_on(emitter.recv())
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
}
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use super::*;
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_init(
account_manager: *mut dc_accounts_t,
) -> *mut dc_jsonrpc_instance_t {
if account_manager.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_init()");
return ptr::null_mut();
}
let cmd_api =
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);
let instance = dc_jsonrpc_instance_t { receiver, handle };
Box::into_raw(Box::new(instance))
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_unref(jsonrpc_instance: *mut dc_jsonrpc_instance_t) {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
Box::from_raw(jsonrpc_instance);
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_request(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
request: *const libc::c_char,
) {
if jsonrpc_instance.is_null() || request.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_request()");
return;
}
let api = &*jsonrpc_instance;
let handle = &api.handle;
let request = to_string_lossy(request);
spawn(async move {
handle.handle_incoming(&request).await;
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_next_response(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_next_response()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
block_on(api.receiver.recv())
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
.unwrap_or(ptr::null_mut())
}
}

View File

@@ -51,7 +51,7 @@ impl Lot {
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain),
Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Addr { .. } => None,
Qr::Addr { draft, .. } => draft.as_deref(),
Qr::Url { url } => Some(url),
Qr::Text { text } => Some(text),
Qr::WithdrawVerifyContact { .. } => None,
@@ -79,7 +79,13 @@ impl Lot {
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
},
Self::Qr(_qr) => Meaning::None,
Self::Qr(qr) => match qr {
Qr::Addr {
draft: Some(_draft),
..
} => Meaning::Text1Draft,
_ => Meaning::None,
},
Self::Error(_err) => Meaning::None,
}
}
@@ -118,7 +124,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(),

View File

@@ -55,7 +55,7 @@ pub(crate) enum CStringError {
/// # Example
///
/// ```
/// use deltachat::dc_tools::{dc_strdup, OsStrExt};
/// use deltachat::tools::{dc_strdup, OsStrExt};
/// let path = std::path::Path::new("/some/path");
/// let path_c = path.to_c_string().unwrap();
/// unsafe {

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,98 @@
// 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;"draft":(string|null);})|({"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|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,string,U32,U32];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,26 +11,49 @@ Changes to the UIs
Changes in the core
-------------------
- DONE: We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
- [x] We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
- DONE: If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
- [x] If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
- The key stays the same.
- No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
- [x] No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
- When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
- [ ] When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
- When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
- [x] ([#3385](https://github.com/deltachat/deltachat-core-rust/pull/3385)) When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
AND there is a `Chat-Version` header\
AND the message is signed correctly
AND the From address is (also) in the encrypted (and therefore signed) headers <sup>[[1]](#myfootnote1)</sup>\
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
<a name="myfootnote1">[1]</a>: Without this check, an attacker could replay a message from Alice to Bob. Then Bob's device would do an AEAP transition from Alice's to the attacker's address, allowing for easier phishing.
<details>
<summary>More details about this</summary>
Suppose Alice sends a message to Evil (or to a group with both Evil and Bob). Evil then forwards the message to Bob, changing the From and To headers (and if necessary Message-Id) and replacing `addr=alice@example.org;` in the autocrypt header with `addr=evil@example.org;`.
Then Bob's device sees that there is a message which is signed by Alice's key and comes from Evil's address and would do the AEAP transition, i.e. replace Alice with Evil in all groups and show a message "Alice changed their address from alice@example.org to evil@example.org". Disadvantages for Evil are that Bob's message will be shown on Alice's device, possibly creating confusion/suspicion, and that the usual "Setup changed for..." message will be shown the next time Evil sends a message (because Evil doesn't know Alice's private key).
Possible mitigations:
- if we make the AEAP device message sth. like "Automatically removed alice@example.org and added evil@example.org", then this will create more suspicion, making the phishing harder (we didn't talk about what what the wording should be at all yet).
- Add something similar to replay protection to our Autocrypt implementation. This could be done e.g. by adding a second `From` header to the protected headers. If it's present, the receiver then requires it to be the same as the outer `From`, and if it's not present, we don't do AEAP --> **That's what we implemented**
Note that usually a mail is signed by a key that has a UID matching the from address.
That's not mandatory for Autocrypt (and in fact, we just keep the old UID when changing the self address, so with AEAP the UID will actually be different than the from address sometimes)
https://autocrypt.org/level1.html#openpgp-based-key-data says:
> The content of the user id packet is only decorative
</details>
### Notes:
- We treat protected and non-protected chats the same
@@ -97,3 +120,8 @@ Other
-----
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
Notes during implementing
========================
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.

View File

@@ -1,9 +1,11 @@
#![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,
};
@@ -11,8 +13,6 @@ use deltachat::chatlist::*;
use deltachat::constants::*;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::dc_receive_imf::*;
use deltachat::dc_tools::*;
use deltachat::download::DownloadState;
use deltachat::imex::*;
use deltachat::location;
@@ -20,10 +20,11 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
use deltachat::{config, provider};
use std::fs;
use std::time::{Duration, SystemTime};
use tokio::fs;
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
@@ -96,10 +97,10 @@ async fn reset_tables(context: &Context, bits: i32) {
}
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
let data = dc_read_file(context, filename).await?;
let data = read_file(context, filename).await?;
if let Err(err) = dc_receive_imf(context, &data, false).await {
println!("dc_receive_imf errored: {:?}", err);
if let Err(err) = receive_imf(context, &data, false).await {
println!("receive_imf errored: {:?}", err);
}
Ok(())
}
@@ -128,24 +129,20 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
real_spec = rs.unwrap();
}
if let Some(suffix) = dc_get_filesuffix_lc(&real_spec) {
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
read_cnt += 1
}
} else {
/* import a directory */
let dir_name = std::path::Path::new(&real_spec);
let dir = std::fs::read_dir(dir_name);
let dir = fs::read_dir(dir_name).await;
if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return false;
} else {
let dir = dir.unwrap();
for entry in dir {
if entry.is_err() {
break;
}
let entry = entry.unwrap();
let mut dir = dir.unwrap();
while let Ok(Some(entry)) = dir.next_entry().await {
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
@@ -191,7 +188,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
DownloadState::Failure => " [⬇ Download failed]",
};
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let temp2 = timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{}{} [{}]",
@@ -219,6 +216,14 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else if msg.get_viewtype() == Viewtype::Webxdc {
match msg.get_webxdc_info(context).await {
Ok(info) => format!(
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
info.name, info.icon, info.document, info.summary, info.source_code_url
),
Err(err) => format!("[get_webxdc_info() failed: {}]", err),
}
} else {
"".to_string()
},
@@ -492,7 +497,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let setup_code = create_setup_code(&context);
let file_name = blobdir.join("autocrypt-setup-message.html");
let file_content = render_setup_file(&context, &setup_code).await?;
async_std::fs::write(&file_name, file_content).await?;
fs::write(&file_name, file_content).await?;
println!(
"Setup message written to: {}\nSetup code: {}",
file_name.display(),
@@ -532,7 +537,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.join("connectivity.html");
match context.get_connectivity_html().await {
Ok(html) => {
fs::write(&file, html)?;
fs::write(&file, html).await?;
println!("Report written to: {:#?}", file);
}
Err(err) => {
@@ -597,7 +602,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
_ => "",
}
};
let timestr = dc_timestamp_to_str(summary.timestamp);
let timestr = timestamp_to_str(summary.timestamp);
println!(
"{}{}{} [{}]{}",
summary
@@ -810,7 +815,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!(
"Loc#{}: {}: lat={} lng={} acc={} Chat#{} Contact#{} {} {}",
location.location_id,
dc_timestamp_to_str(location.timestamp),
timestamp_to_str(location.timestamp),
location.latitude,
location.longitude,
location.accuracy,
@@ -892,7 +897,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No html-file given.");
let path: &Path = arg1.as_ref();
let html = &*fs::read(&path)?;
let html = &*fs::read(&path).await?;
let html = String::from_utf8_lossy(html);
let mut msg = Message::new(Viewtype::Text);
@@ -978,11 +983,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let images = chat::get_chat_media(
&context,
sel_chat.as_ref().unwrap().get_id(),
sel_chat.map(|c| c.id),
Viewtype::Image,
Viewtype::Gif,
Viewtype::Video,
@@ -1079,7 +1082,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.unwrap_or_default()
.join(format!("msg-{}.html", id.to_u32()));
let html = id.get_html(&context).await?.unwrap_or_default();
fs::write(&file, html)?;
fs::write(&file, html).await?;
println!("HTML written to: {:#?}", file);
}
"listfresh" => {
@@ -1233,8 +1236,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"fileinfo" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
if let Ok(buf) = dc_read_file(&context, &arg1).await {
let (width, height) = dc_get_filemeta(&buf)?;
if let Ok(buf) = read_file(&context, &arg1).await {
let (width, height) = get_filemeta(&buf)?;
println!("width={}, height={}", width, height);
} else {
bail!("Command failed.");

View File

@@ -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]).to_path_buf(), 0, Events::new()).await?;
let context = Context::new(Path::new(&args[1]), 0, Events::new()).await?;
let events = context.get_event_emitter();
async_std::task::spawn(async move {
tokio::task::spawn(async move {
while let Some(event) = events.recv().await {
receive_event(event.typ);
}
@@ -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 (reader_s, reader_r) = async_std::channel::bounded(100);
let input_loop = async_std::task::spawn_blocking(move || {
let ctx = context.clone();
let input_loop = tokio::task::spawn_blocking(move || {
let h = DcHelper {
completer: FilenameCompleter::new(),
highlighter: MatchingBracketHighlighter::new(),
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,16 +339,30 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str());
async_std::task::block_on(reader_s.send(line)).unwrap();
let contine = Handle::current().block_on(async {
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => true,
Ok(ExitResult::Exit) => {
println!("Exiting ...");
false
}
Err(err) => {
println!("Error: {}", err);
true
}
}
});
if !contine {
break;
}
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
println!("Exiting...");
drop(reader_s);
break;
}
Err(err) => {
println!("Error: {}", err);
drop(reader_s);
break;
}
}
@@ -359,15 +373,8 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
Ok::<_, Error>(())
});
while let Ok(line) = reader_r.recv().await {
match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => {}
Ok(ExitResult::Exit) => break,
Err(err) => println!("Error: {}", err),
}
}
context.stop_io().await;
input_loop.await?;
input_loop.await??;
Ok(())
}
@@ -400,7 +407,7 @@ async fn handle_cmd(
"oauth2" => {
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
let oauth2_url =
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
if oauth2_url.is_none() {
println!("OAuth2 not available for {}.", &addr);
} else {
@@ -417,7 +424,7 @@ async fn handle_cmd(
"getqr" | "getbadqr" => {
ctx.start_io().await;
let group = arg1.parse::<u32>().ok().map(ChatId::new);
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
let mut qr = get_securejoin_qr(&ctx, group).await?;
if !qr.is_empty() {
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
@@ -437,7 +444,7 @@ async fn handle_cmd(
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {
fs::write(&file, svg)?;
fs::write(&file, svg).await?;
println!("QR code svg written to: {:#?}", file);
}
Err(err) => {
@@ -448,7 +455,7 @@ async fn handle_cmd(
"joinqr" => {
ctx.start_io().await;
if !arg0.is_empty() {
dc_join_securejoin(&ctx, arg1).await?;
join_securejoin(&ctx, arg1).await?;
}
}
"exit" | "quit" => return Ok(ExitResult::Exit),
@@ -458,11 +465,12 @@ async fn handle_cmd(
Ok(ExitResult::Continue)
}
fn main() -> Result<(), Error> {
#[tokio::main]
async fn main() -> Result<(), Error> {
let _ = pretty_env_logger::try_init();
let args = std::env::args().collect();
async_std::task::block_on(async move { start(args).await })?;
start(args).await?;
Ok(())
}

View File

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

View File

@@ -117,7 +117,7 @@ $ fnm install 17 --arch x64
$ fnm use 17
$ node -p process.arch
# result should be x64
$ cd deltachat-core-rust && rustup target add x86_64-apple-darwin && cd -
$ rustup target add x86_64-apple-darwin
$ git apply patches/m1_build_use_x86_64.patch
$ CARGO_BUILD_TARGET=x86_64-apple-darwin npm run build
$ npm run test
@@ -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-v1.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-1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
## License

View File

@@ -39,6 +39,10 @@ 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)
}
@@ -92,6 +96,7 @@ export class Chat {
color: this.color,
id: this.getId(),
name: this.getName(),
mailinglistAddr: this.getMailinglistAddr(),
profileImage: this.getProfileImage(),
type: this.getType(),
isSelfTalk: this.isSelfTalk(),

View File

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

View File

@@ -12,6 +12,7 @@ export interface ChatJSON {
color: string
id: number
name: string
mailinglistAddr: string
profileImage: string
type: number
isSelfTalk: boolean

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,84 +1,77 @@
=========================
deltachat python bindings
DeltaChat Python bindings
=========================
This package provides bindings to the deltachat-core_ Rust -library
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
This package provides `Python bindings`_ to the `deltachat-core library`_
which implements IMAP/SMTP/MIME/OpenPGP 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 venv # or: python -m venv
source venv/bin/activate
virtualenv env # or: python -m venv
source env/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``venv`` directory and leaves
modifies files in your ``env`` directory and leaves
your system installation alone.
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::
For Linux we build wheels for all releases and push them to a python package
index. To install the latest release::
pip install --pre -i https://m.devpi.net/dc/master deltachat
pip install 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
=============
After successful binding installation you can install a few more
Python packages before running the tests::
Recommended way to run tests is using `tox <https://tox.wiki>`_.
After successful binding installation you can install tox
and run the tests::
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
pytest -v tests
pip install tox
tox -e py3
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/en/latest/).
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/>`_.
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::
pytest
# or if you have installed pytest-xdist for parallel test execution
pytest -n6
tox -e py3
Each test run creates new accounts.
.. _sourceinstall:
Installing bindings from source (Updated: July 2020)
=========================================================
Installing bindings from source
===============================
Install Rust and Cargo first.
The easiest is probably to use `rustup <https://rustup.rs/>`_.
@@ -97,74 +90,42 @@ 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 and activate it in your shell::
virtual environment with dependencies using tox
and activate it in your shell::
cd python
python3 -m venv venv # or: virtualenv venv
source venv/bin/activate
tox --devenv env
source env/bin/activate
You should now be able to build the python bindings using the supplied script::
python install_python_bindings.py
python3 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-tooling.
can be done with Docker_ or Podman_.
using docker pull / premade images
------------------------------------
.. _Docker: https://www.docker.com/
.. _Podman: https://podman.io/
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::
If you want to build your own wheels, build container image first::
$ docker pull deltachat/coredeps
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
$ docker build -t deltachat/coredeps scripts/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 \
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
This will use the ``scripts/coredeps/Dockerfile`` to build
container image called ``deltachat/coredeps``. You can afterwards
find it with::
$ docker images
This docker image can be used to run tests and build Python wheels for all interpreters::
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``.
$ docker run -e DCC_NEW_TMP_EMAIL \
--rm -it -v $(pwd):/mnt -w /mnt \
deltachat/coredeps scripts/run_all.sh

View File

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

View File

@@ -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"]
cmd = ["cargo", "build", "-p", "deltachat_ffi", "--features", "jsonrpc"]
if target == "release":
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"

View File

@@ -1,7 +1,41 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=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>.*)?$'

View File

@@ -1,40 +1,4 @@
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",
],
)
from setuptools import setup
if __name__ == "__main__":
main()
setup(cffi_modules=["src/deltachat/_build.py:ffibuilder"])

View File

@@ -60,29 +60,7 @@ def run_cmdline(argv=None, account_plugins=None):
ac = Account(args.db)
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()
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)
print("{}: waiting for message".format(ac.get_config("addr")))

View File

@@ -20,7 +20,7 @@ from .cutil import (
from_optional_dc_charpointer,
iter_array,
)
from .events import EventThread
from .events import EventThread, FFIEventLogger
from .message import Message
from .tracker import ConfigureTracker, ImexTracker
@@ -169,8 +169,6 @@ 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:
@@ -598,6 +596,36 @@ 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.
@@ -680,8 +708,9 @@ 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)

View File

@@ -497,6 +497,8 @@ class ACFactory:
configdict = dict(
addr=cloned_from.get_config("addr"),
mail_pw=cloned_from.get_config("mail_pw"),
imap_certificate_checks=cloned_from.get_config("imap_certificate_checks"),
smtp_certificate_checks=cloned_from.get_config("smtp_certificate_checks"),
)
configdict.update(kwargs)
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None

View File

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

View File

@@ -1581,8 +1581,9 @@ 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 msg1.text == "hi" or msg2.text == "hi"
assert msg2.text == "hi"
assert msg1.chat.id == msg2.chat.id
lp.sec("ac2: see if chat now has got the profile image")
@@ -1596,6 +1597,8 @@ 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
@@ -1854,7 +1857,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/mod.rs returned false because the error message was changed
# in configure.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:
@@ -2052,6 +2055,47 @@ 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()

View File

@@ -497,12 +497,6 @@ 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()

View File

@@ -9,9 +9,8 @@ envlist =
[testenv]
commands =
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
python tests/package_wheels.py {toxworkdir}/wheelhouse
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
passenv =
TRAVIS
DCC_RS_DEV
DCC_RS_TARGET
DCC_NEW_TMP_EMAIL

View File

@@ -14,18 +14,15 @@ 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.
- `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-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
- `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
@@ -33,19 +30,18 @@ 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.
# Outdated files (for later re-use)
# coredeps Dockerfile
`coredeps/Dockerfile` specifies an image that contains all
of Delta Chat's core dependencies. It used to run
python tests and build wheels (binary packages for Python)
of Delta Chat's core dependencies. It is used to
build python 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 docker-coredeps
docker build -t deltachat/doxygen docker-doxygen
docker build -t deltachat/coredeps coredeps
Additionally, you can install qemu and build arm64 docker image:
Additionally, you can install qemu and build arm64 docker image on x86\_64 machine:
apt-get install qemu binfmt-support qemu-user-static
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64
docker build -t deltachat/coredeps-arm64 --build-arg BASEIMAGE=quay.io/pypa/manylinux2014_aarch64 coredeps

View File

@@ -29,18 +29,19 @@ jobs:
- name: c-docs
image_resource:
source:
repository: hrektts/doxygen
repository: alpine
type: registry-image
platform: linux
run:
path: bash
path: sh
args:
- -exc
- -ec
- |
apk add --no-cache doxygen git
cd deltachat-core-rust
bash scripts/run-doxygen.sh
scripts/run-doxygen.sh
cd ..
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
- task: upload-c-docs
config:
@@ -84,8 +85,9 @@ jobs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/docker-coredeps
CONTEXT: deltachat-core-rust/scripts/coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_x86_64
platform: linux
caches:
- path: cache
@@ -183,8 +185,9 @@ jobs:
- name: coredeps-image
path: image
params:
CONTEXT: deltachat-core-rust/scripts/docker-coredeps-arm64
CONTEXT: deltachat-core-rust/scripts/coredeps
UNPACK_ROOTFS: "true"
BUILD_ARG_BASEIMAGE: quay.io/pypa/manylinux2014_aarch64
platform: linux
caches:
- path: cache
@@ -230,3 +233,143 @@ 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*

View File

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

View File

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

View File

@@ -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"
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebuginfo=2"
export RUSTDOCFLAGS="-Cpanic=abort"
cargo clean
cargo build

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
FROM debian:stable
# this is tagged as deltachat/doxygen
RUN apt-get update && apt-get install -y doxygen

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env bash
set -ex
#!/bin/sh
set -e
cd deltachat-ffi
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
#
# Build the Delta Chat Core Rust library, Python wheels and docs
@@ -8,21 +8,16 @@ 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__
@@ -33,11 +28,13 @@ mkdir -p $TOXWORKDIR
# Note that the independent remote_tests_python step does all kinds of
# live-testing already.
unset DCC_NEW_TMP_EMAIL
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,auditwheels
popd
# 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
echo -----------------------
echo generating python docs
echo -----------------------
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
tox --workdir "$TOXWORKDIR" -e doc

2
scripts/set_core_version.py Normal file → Executable file
View File

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

18
spec.md
View File

@@ -1,6 +1,6 @@
# chat-mail specification
Version: 0.33.0
Version: 0.34.0
Status: In-progress
Format: [Semantic Line Breaks](https://sembr.org/)
@@ -474,4 +474,20 @@ 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.

View File

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

View File

@@ -4,14 +4,13 @@ use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use async_std::path::{Path, PathBuf};
use async_std::prelude::*;
use async_std::{fs, io};
use std::path::{Path, PathBuf};
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::{
@@ -89,7 +88,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().await {
} else if attempt == 1 && !dir.exists() {
fs::create_dir_all(dir).await.ok_or_log(context);
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
@@ -371,108 +370,81 @@ impl<'a> BlobObject<'a> {
mut img_wh: u32,
max_bytes: Option<usize>,
) -> Result<Option<String>> {
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;
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;
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;
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));
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,
}
}
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;
if do_scale || do_rotate {
if do_rotate {
img = match orientation {
Ok(90) => img.rotate90(),
Ok(180) => img.rotate180(),
Ok(270) => img.rotate270(),
_ => img,
}
}
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)?;
}
std::fs::write(&blob_abs, &encoded)
.context("failed to write recoded blob to file")?;
}
// 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)
Ok(changed_name)
})
}
pub fn get_exif_orientation(&self, context: &Context) -> Result<i32, Error> {
@@ -500,6 +472,35 @@ 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;
@@ -513,7 +514,16 @@ mod tests {
use super::*;
#[async_std::test]
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 fn test_create() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo", b"hello").await.unwrap();
@@ -524,28 +534,28 @@ mod tests {
assert_eq!(blob.to_abs_path(), t.get_blobdir().join("foo"));
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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");
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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");
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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"));
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_suffix() {
let t = TestContext::new().await;
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
@@ -554,16 +564,16 @@ mod tests {
assert_eq!(blob.suffix(), None);
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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().await);
assert!(foo_path.exists());
BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
while let Ok(Some(dirent)) = dir.next_entry().await {
let fname = dirent.file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else {
@@ -574,20 +584,20 @@ mod tests {
}
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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().await);
assert!(foo_path.exists());
BlobObject::create(&t, "foo.tar.gz", b"world")
.await
.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
while let Some(dirent) = dir.next().await {
let fname = dirent.unwrap().file_name();
while let Ok(Some(dirent)) = dir.next_entry().await {
let fname = dirent.file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else {
@@ -599,7 +609,7 @@ mod tests {
}
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_long_names() {
let t = TestContext::new().await;
let s = "1".repeat(150);
@@ -608,7 +618,7 @@ mod tests {
assert!(blobname.len() < 128);
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_and_copy() {
let t = TestContext::new().await;
let src = t.dir.path().join("src");
@@ -623,10 +633,10 @@ mod tests {
.await
.is_err());
let whoops = t.get_blobdir().join("whoops");
assert!(!whoops.exists().await);
assert!(!whoops.exists());
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_from_path() {
let t = TestContext::new().await;
@@ -646,7 +656,7 @@ mod tests {
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_from_name_long() {
let t = TestContext::new().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
@@ -709,36 +719,26 @@ mod tests {
assert!(!stem.contains('?'));
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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");
File::create(&avatar_src)
.await
.unwrap()
.write_all(avatar_bytes)
.await
.unwrap();
fs::write(&avatar_src, avatar_bytes).await.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.jpg");
assert!(!avatar_blob.exists().await);
assert!(!avatar_blob.exists());
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert!(std::fs::metadata(&avatar_blob).unwrap().len() < avatar_bytes.len() as u64);
assert!(avatar_blob.exists());
assert!(fs::metadata(&avatar_blob).await.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()));
let img = image::open(avatar_src).unwrap();
assert_eq!(img.width(), 1000);
assert_eq!(img.height(), 1000);
check_image_size(avatar_src, 1000, 1000);
check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
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 {
async fn file_size(path_buf: &Path) -> u64 {
let file = File::open(path_buf).await.unwrap();
file.metadata().await.unwrap().len()
}
@@ -750,25 +750,22 @@ mod tests {
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
let img = image::open(&avatar_blob).unwrap();
assert!(img.width() > 130);
assert_eq!(img.width(), img.height());
tokio::task::block_in_place(move || {
let img = image::open(&avatar_blob).unwrap();
assert!(img.width() > 130);
assert_eq!(img.width(), img.height());
});
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_in_blobdir() {
let t = TestContext::new().await;
let avatar_src = t.get_blobdir().join("avatar.png");
File::create(&avatar_src)
.await
.unwrap()
.write_all(test_utils::AVATAR_900x900_BYTES)
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
.await
.unwrap();
let img = image::open(&avatar_src).unwrap();
assert_eq!(img.width(), 900);
assert_eq!(img.height(), 900);
check_image_size(&avatar_src, 900, 900);
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
@@ -779,37 +776,30 @@ mod tests {
avatar_src.with_extension("jpg").to_str().unwrap()
);
let img = image::open(avatar_cfg).unwrap();
assert_eq!(img.width(), BALANCED_AVATAR_SIZE);
assert_eq!(img.height(), BALANCED_AVATAR_SIZE);
check_image_size(avatar_cfg, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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");
File::create(&avatar_src)
.await
.unwrap()
.write_all(avatar_bytes)
.await
.unwrap();
fs::write(&avatar_src, avatar_bytes).await.unwrap();
let avatar_blob = t.get_blobdir().join("avatar.png");
assert!(!avatar_blob.exists().await);
assert!(!avatar_blob.exists());
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
.unwrap();
assert!(avatar_blob.exists().await);
assert!(avatar_blob.exists());
assert_eq!(
std::fs::metadata(&avatar_blob).unwrap().len(),
fs::metadata(&avatar_blob).await.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()));
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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:
@@ -829,7 +819,7 @@ mod tests {
.unwrap();
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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");
@@ -855,7 +845,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 = async_std::task::spawn(async move {
let join_handle = tokio::task::spawn(async move {
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes2,
@@ -883,10 +873,10 @@ mod tests {
.unwrap();
assert_correct_rotation(&img_rotated);
join_handle.await;
join_handle.await.unwrap();
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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)
@@ -934,10 +924,10 @@ mod tests {
.await?;
let file = alice.get_blobdir().join("file.jpg");
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);
fs::write(&file, &bytes)
.await
.context("failed to write file")?;
check_image_size(&file, original_width, original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
assert_eq!(blob.get_exif_orientation(&alice).unwrap_or(0), orientation);
@@ -949,9 +939,11 @@ 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);
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);
check_image_size(
alice_msg.get_file(&alice).unwrap(),
compressed_width,
compressed_height,
);
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_width() as u32, compressed_width);
@@ -961,19 +953,17 @@ mod tests {
let blob = BlobObject::new_from_path(&bob, &file).await?;
assert_eq!(blob.get_exif_orientation(&bob).unwrap_or(0), 0);
let img = image::open(file)?;
assert_eq!(img.width() as u32, compressed_width);
assert_eq!(img.height() as u32, compressed_height);
let img = check_image_size(file, compressed_width, compressed_height);
Ok(img)
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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");
File::create(&file).await?.write_all("bla".as_ref()).await?;
fs::write(&file, b"bla").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?;
@@ -986,14 +976,14 @@ mod tests {
Ok(())
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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");
File::create(&file).await?.write_all("bla".as_ref()).await?;
fs::write(&file, b"bla").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());

File diff suppressed because it is too large Load Diff

View File

@@ -240,7 +240,7 @@ impl Chatlist {
ids
};
if add_archived_link_item && dc_get_archived_cnt(context).await? > 0 {
if add_archived_link_item && 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 dc_get_archived_cnt(context: &Context) -> Result<usize> {
pub async fn 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;
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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);
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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());
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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);
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_search_single_chat() -> anyhow::Result<()> {
let t = TestContext::new_alice().await;
// receive a one-to-one-message
dc_receive_imf(
receive_imf(
&t,
b"From: Bob Authname <bob@example.org>\n\
To: alice@example.org\n\
@@ -548,12 +548,12 @@ mod tests {
Ok(())
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
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
dc_receive_imf(
receive_imf(
&t,
b"From: bob@example.org\n\
To: alice@example.org\n\
@@ -610,7 +610,7 @@ mod tests {
Ok(())
}
#[async_std::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_unwrap() {
let t = TestContext::new().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")

View File

@@ -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.get(0).map_or(0, |&x| u16::from(x))
let checksum: u16 = result.first().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
}

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