Compare commits

..

239 Commits

Author SHA1 Message Date
link2xt
d29b0baa25 Prepare core release 1.99.0 2022-11-06 17:41:47 +00:00
Hocuri
ffd57772e9 Add DC_EVENT_INCOMING_MSG_BUNCH event (#3643)
* Add DC_EVENT_INCOMING_MSG event

* Fix lots of compile errors

* Docs

* Changelog

* Fix python tests

Adding DC_EVENT_INCOMING_MSG_BUNCH made the python tests fail because they use `get_matching("DC_EVENT_INCOMING_MSG")`, which also matches DC_EVENT_INCOMING_MSG_BUNCH, so the tests got confused.

This fixes `get_matching()` to only match whole event names.

* Also fix test_ac_setup_message_twice()

The built regex was ^EVENT_NAME1|EVENT_NAME2$, which becomes parsed as
"^EVENT_NAME1" OR "EVENT_NAME2$". Introduce a group (parentheses) to fix
this.

* desktop will use DC_EVENT_INCOMING_MSG_BUNCH,
so I would not call it experimental anymore

* add generated node constants

* msg_ids in the event as Vec<u32>
number[] in js land

this is way more convinient than a json encoded string.

* Apply suggestions from code review

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

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>
2022-11-06 17:17:48 +00:00
link2xt
e648e4fb29 DKIM-Checking: Don't disallow keychanges for now (#3728) 2022-11-06 16:18:21 +00:00
Hocuri
ecab62a56b Fix flaky test_block_mailing_list() (#3733)
Seems like consume_events() didn't work properly, i.e. in some cases it
didn't see the latest events and failed to consume them. So, the
IncomingMsg event from receiving DC_MAILINGLIST stayed in the events
channel, which made this fail:

```rust
        // Check that no notification is displayed for blocked mailing list message.
        while let Ok(event) = t.evtracker.try_recv() {
            assert!(!matches!(event.typ, EventType::IncomingMsg { .. }));
        }
```

Fix it by explicitly waiting for the first IncomingMsg event.
2022-11-05 14:27:39 +00:00
Hocuri
e21ea739d9 Ignore now-failing test 2022-11-05 10:10:50 +01:00
Hocuri
659bb08389 Also don't disallow going back to cleartext 2022-11-05 10:10:35 +01:00
Hocuri
f8da264e2b changelog 2022-11-05 01:47:27 +00:00
Hocuri
db84317be0 DKIM-Checking: Don't disallowing keychanges for now
To get back to a releaseable state - the info stays accessible in the
Message-info.

We can re-enable it as soon as it has been tested thoroughly.
2022-11-05 01:47:03 +00:00
link2xt
e93dc33ef8 Do not allow peerstate reset if DKIM check failed
The problem was that a message without Autocrypt key or with a wrong
signature resets peerstate regardless of what DKIM check says. I
inserted sleep(1.1) to make sure reset always happens and make the bug
reproducible, then fixed it by forbidding reset if DKIM check fails.

https://github.com/deltachat/deltachat-core-rust/pull/3731
2022-11-05 01:44:37 +00:00
link2xt
cb1a4291d0 Accept ToString instead of AsRef<str> in Params.set() (#3732) 2022-11-05 02:17:29 +01:00
link2xt
1a745b24d7 Merge branch 'unwrap-mailinglistaddr-in-cffi'
https://github.com/deltachat/deltachat-core-rust/pull/3706
2022-11-05 01:14:11 +00:00
link2xt
7b7ce30fe3 Changelog 2022-11-05 01:13:35 +00:00
link2xt
9bc525f579 Fix mailinglist test 2022-11-04 23:30:56 +00:00
Simon Laux
3fcbc03759 fix formatting and most of the test 2022-11-04 23:27:01 +00:00
Simon Laux
1bd53de1f7 unwrap mailinglist addr option in cffi
so rust api and jsonrpc return the option
2022-11-04 23:27:01 +00:00
link2xt
037739c634 mimeparser: do not allow key reset if DKIM check failed 2022-11-04 21:04:24 +00:00
Floris Bruynooghe
3150d2b94b Introduce a ContextBuilder struct (#3698)
The way to create a Context is now rather burdensome, users have to
create and import a bunch of things just to get a Context.  So let's
introduce a builder.

Notice that the builder can only produce an open context, if the
context can not be opened it is dropped.  This is on purpose, the
Context itself can become RAII again at some point by doing this.
Only the FFI needs to have the concept of an open and a closed
Context.
2022-11-04 20:23:06 +00:00
link2xt
91ab10084a Make error reproducible with sleep() 2022-11-04 19:45:29 +00:00
link2xt
96d2a7f0bf Assert that encryption preference is still mutual 2022-11-04 18:57:18 +00:00
link2xt
053c9372cb peerstate: use named columns for SELECT statements
This ensures wrong column is not accidentally used.
2022-11-04 18:55:32 +00:00
Simon Laux
0bb231ad00 jsonrpc: add SystemMessageType to Message and cffi: add missing DC_INFO_ constants (#3707)
* jsonrpc: add `SystemMessageType` to `Message`
and cffi: add missing `DC_INFO_` constants

* Update deltachat-ffi/deltachat.h

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

* regenerate js constants

Co-authored-by: bjoern <r10s@b44t.com>
2022-11-04 19:48:53 +01:00
link2xt
15db5adc7e Merge branch 'flub/unoptimised-debug'
https://github.com/deltachat/deltachat-core-rust/pull/3699
2022-11-04 13:14:35 +00:00
link2xt
146478e450 Optimize debug builds, but not tests 2022-11-04 12:33:08 +00:00
link2xt
97192a8055 Set RUST_MIN_STACK for Python tests 2022-11-04 12:32:22 +00:00
link2xt
772514940c Set RUST_MIN_STACK in JSON-RPC tests 2022-11-04 12:32:22 +00:00
Floris Bruynooghe
38efde6c98 Seems the npm stuff manages to avoid the cargo config file 2022-11-04 12:32:22 +00:00
Floris Bruynooghe
056b8ba1e8 link tokio issue, remove opt-level line
opt-level relies on the default instead
2022-11-04 12:32:22 +00:00
Floris Bruynooghe
ccd4d46391 Do not use optimised debug builds
Optimised debug builds result in an extremely slow code-build-test
cycle.  The reason we do this is because we have a few tests which end
up overflowing the stack.  This increases the stack instead.
2022-11-04 12:32:22 +00:00
link2xt
e3bf8265c4 Recently seen loop 2022-11-03 20:18:14 +00:00
Hocuri
f4ee86282e Fix clippy warnings (#3726) 2022-11-03 15:44:35 +00:00
dependabot[bot]
7b66eb8b9c Merge pull request #3719 from deltachat/dependabot/cargo/uuid-1.2.1 2022-11-03 08:47:32 +00:00
Simon Laux
a7861c2ea5 fix readme (#3725)
Update README.md
2022-11-02 19:48:37 +01:00
dependabot[bot]
163678dfe3 Merge pull request #3715 from deltachat/dependabot/cargo/axum-0.5.17 2022-11-02 14:18:04 +00:00
dependabot[bot]
e32b1341f8 Merge pull request #3717 from deltachat/dependabot/cargo/anyhow-1.0.66 2022-11-02 13:18:16 +00:00
dependabot[bot]
f579ad79a2 Merge pull request #3710 from deltachat/dependabot/cargo/syn-1.0.103 2022-11-02 13:06:06 +00:00
dependabot[bot]
a6d1b8b975 cargo: bump anyhow from 1.0.65 to 1.0.66
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.65 to 1.0.66.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.65...1.0.66)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 12:21:14 +00:00
dependabot[bot]
890c5596a9 cargo: bump syn from 1.0.102 to 1.0.103
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.102 to 1.0.103.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.102...1.0.103)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 12:21:13 +00:00
dependabot[bot]
7797580c7f Merge pull request #3712 from deltachat/dependabot/cargo/futures-0.3.25 2022-11-02 12:19:35 +00:00
dependabot[bot]
7f4abc5285 cargo: bump axum from 0.5.16 to 0.5.17
Bumps [axum](https://github.com/tokio-rs/axum) from 0.5.16 to 0.5.17.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.5.16...axum-v0.5.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 07:43:44 +00:00
dependabot[bot]
e559c467c6 cargo: bump futures from 0.3.24 to 0.3.25
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.24 to 0.3.25.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.24...0.3.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 07:43:44 +00:00
dependabot[bot]
916915d430 cargo: bump tokio-stream from 0.1.10 to 0.1.11
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.10 to 0.1.11.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.10...tokio-stream-0.1.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:41:33 +01:00
dependabot[bot]
46d3889b63 cargo: bump base64 from 0.13.0 to 0.13.1
Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.13.0 to 0.13.1.
- [Release notes](https://github.com/marshallpierce/rust-base64/releases)
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.13.0...v0.13.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:40:50 +01:00
dependabot[bot]
e39dc25e2a cargo: bump libc from 0.2.134 to 0.2.137
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.134 to 0.2.137.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.134...0.2.137)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:40:16 +01:00
dependabot[bot]
1a022d8905 cargo: bump serde from 1.0.145 to 1.0.147
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.145 to 1.0.147.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.145...v1.0.147)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:38:28 +01:00
dependabot[bot]
3dc8888c6a cargo: bump kamadak-exif from 0.5.4 to 0.5.5
Bumps [kamadak-exif](https://github.com/kamadak/exif-rs) from 0.5.4 to 0.5.5.
- [Release notes](https://github.com/kamadak/exif-rs/releases)
- [Changelog](https://github.com/kamadak/exif-rs/blob/master/NEWS)
- [Commits](https://github.com/kamadak/exif-rs/compare/0.5.4...0.5.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:38:10 +01:00
dependabot[bot]
3ff3046b68 Merge pull request #3718 from deltachat/dependabot/cargo/native-tls-0.2.11 2022-11-02 02:13:47 +00:00
dependabot[bot]
910a5dd96a Merge pull request #3720 from deltachat/dependabot/cargo/smallvec-1.10.0 2022-11-02 02:12:59 +00:00
dependabot[bot]
6b521c4c43 Merge pull request #3724 from deltachat/dependabot/cargo/url-2.3.1 2022-11-02 02:12:15 +00:00
dependabot[bot]
fe909fbe92 cargo: bump native-tls from 0.2.10 to 0.2.11
Bumps [native-tls](https://github.com/sfackler/rust-native-tls) from 0.2.10 to 0.2.11.
- [Release notes](https://github.com/sfackler/rust-native-tls/releases)
- [Changelog](https://github.com/sfackler/rust-native-tls/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sfackler/rust-native-tls/compare/v0.2.10...v0.2.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 23:51:39 +00:00
dependabot[bot]
98c9139f3a Merge pull request #3713 from deltachat/dependabot/cargo/serde_json-1.0.87 2022-11-01 23:50:19 +00:00
dependabot[bot]
03d9ee31ec Merge pull request #3711 from deltachat/dependabot/cargo/once_cell-1.16.0 2022-11-01 23:50:02 +00:00
dependabot[bot]
ef1cc56439 cargo: bump smallvec from 1.9.0 to 1.10.0
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.9.0...v1.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 23:15:10 +00:00
link2xt
47fcdef88c cargo update -p mio
mio 0.8.5 does not use libc epoll_create1() function on Android anymore,
so it will be possible to revert e29b6f9974
in the next Android release with the new core.
2022-11-01 23:12:45 +00:00
dependabot[bot]
a1c2260f9b cargo: bump url from 2.3.0 to 2.3.1
Bumps [url](https://github.com/servo/rust-url) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.3.0...v2.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 21:05:01 +00:00
dependabot[bot]
a4b01b83e7 cargo: bump uuid from 1.1.2 to 1.2.1
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.1.2 to 1.2.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.1.2...1.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 21:04:32 +00:00
dependabot[bot]
b9e8edb3ef cargo: bump serde_json from 1.0.85 to 1.0.87
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.85 to 1.0.87.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.85...v1.0.87)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 21:03:43 +00:00
dependabot[bot]
b95861c378 cargo: bump once_cell from 1.15.0 to 1.16.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.15.0 to 1.16.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.15.0...v1.16.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-11-01 21:03:25 +00:00
Simon Laux
758ae185ba jsonrpc: change method naming (#3678)
* jsonrpc: change method naming

* re-gen types

* cargo fmt
2022-10-31 15:41:40 +00:00
Simon Laux
f60d9a51d4 update bindings section in readme (#3696)
* update bindings section in readme

* Update README.md

Co-authored-by: Floris Bruynooghe <flub@devork.be>

* Update README.md

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

Co-authored-by: Floris Bruynooghe <flub@devork.be>
Co-authored-by: bjoern <r10s@b44t.com>
2022-10-31 15:12:55 +00:00
link2xt
d81579730e Allow sender timestamp to be in the future
This can happen due to unsynchronized clocks or
when "smeared" timestamp is used as the sender sends
multiple messages without delay.
2022-10-30 10:26:22 +00:00
Hocuri
b1c6c40fa7 Check DKIM Authentication-Results (#3583)
Fix #3507

Note that this is not intended for a release at this point! We first have to test whether it runs stable enough. If we want to make a release while we are not confident enough in authres-checking, then we have to disable it.

BTW, most of the 3000 new lines are in `test_data/messages/dkimchecks...`, not the actual code

da3a4b94 adds the results to the Message info. It currently does this by adding them to `hop_info`. Maybe we should rename `hop_info` to `extra_info` or something; this has the disadvantage that we can't rename the sql column name though.

Follow-ups for this could be:
- In `update_authservid_candidates()`: Implement the rest of the algorithm @hpk42 and me thought about. What's missing is remembering how sure we are that these are the right authserv-ids. Esp., when receiving a message sent from another account at the same domain, we can be quite sure that the authserv-ids in there are the ones of our email server. This will make authres-checking work with buzon.uy, disroot.org, yandex.ru, mailo.com, and riseup.net.
- Think about how we present this to the user - e.g. currently the only change is that we don't accept key changes, which will mean that the small lock on the message is not shown.
- And it will mean that we can fully enable AEAP, after revisiting the security implications of this, and assuming everyone (esp. @link2xt who pointed out the problems in the first place) feels comfortable with it.
2022-10-28 12:15:37 +02:00
bjoern
d8bc3769a5 allow searching for unaccepted requests (#3694)
* let search_msgs() return unaccepted requests

unaccepted chat requests are shown in the chatlist,
it should be returned by search_msgs() an by the other search functions as well.

form the view of the user, the search acts like a filter,
so there is no reason to hide things additionally.

also, the user may remember a word in a chat request,
maybe even an archived one (there is no need to accept a request before archiving)
that one wants to search later on.

* test searching for unaccepted requests

* simplyfy expression; `c.blocked!=1` is also what is used in similar statements
2022-10-25 22:22:31 +02:00
Floris Bruynooghe
a73fbf7232 Update textwrap dependency
The current version is unsatisfyable if you use deltachat as a
dependency itself.
2022-10-24 22:09:41 +02:00
link2xt
b6b2f453a0 Prepare 1.98.0 2022-10-24 16:09:24 +00:00
link2xt
aa14015919 sql: every Result is anyhow::Result 2022-10-23 11:25:27 +00:00
Simon Laux
7551c84c4f jsonrpc: move qr/uri type to dedicated file (#3687)
#skip-changelog
2022-10-23 08:41:32 +00:00
link2xt
434e53e922 Use UPSERT to insert into msgs table
This way no temporary rows are created and it is easier to maintain
because UPDATE statement is right below the INSERT statement,
unlike `merge_messages` function which is easy to forget about.
2022-10-22 21:34:56 +00:00
link2xt
b5d238f7f4 Keep reactions when downloading partially downloaded message 2022-10-22 15:02:05 +00:00
link2xt
e5c9fea52d Implement reactions
Co-Authored-By: bjoern <r10s@b44t.com>
Co-Authored-By: Simon Laux <mobile.info@simonlaux.de>
2022-10-22 09:59:43 +00:00
Simon Laux
cd15a0e966 jsonrpc: typescript client: export constants under C enum, similar to how its exported from deltachat-node (#3681)
* jsonrpc: typescript client: export constants
under `C` enum,
similar to how its exported from `deltachat-node`

* add pr number to changelog

* fix tests

* fix changelog entry position
2022-10-21 17:51:38 +00:00
dependabot[bot]
895c723d4e Merge pull request #3626 from deltachat/dependabot/cargo/trust-dns-resolver-0.22.0 2022-10-18 09:41:10 +00:00
dependabot[bot]
c7176d6bc8 Merge pull request #3628 from deltachat/dependabot/cargo/percent-encoding-2.2.0 2022-10-18 09:29:54 +00:00
link2xt
b2939d3df3 imap: simplify UPSERT queries on imap_sync
Use `excluded` and remove noop `WHERE` query.

See <https://www.sqlite.org/lang_UPSERT.html> for official SQLite documentation.
2022-10-16 16:34:13 +00:00
link2xt
54a157a629 Prepare 1.97.0 release (#3668) 2022-10-16 15:08:55 +02:00
Simon Laux
427adefb42 jsonrpc: add miscGetStickerFolder and miscGetStickers (#3672)
* jsonrpc: add `miscGetStickerFolder` and `miscGetStickers`

* add pr number to changelog

* refactor

* fix clippy
2022-10-16 14:53:06 +02:00
link2xt
f0dede26a3 cargo fmt 2022-10-16 11:30:01 +00:00
Simon Laux
36f85a6a5a fix nodejs jsonrpc smoke tests (#3674)
the solution was to ignore events
2022-10-15 23:03:54 +02:00
Simon Laux
137567554d set timeout for node ci tests to 10min (#3675)
* set timeout for node ci tests to 10min

set timeout for node ci tests to 10min for the test step,
macOS takes 12min for the whole workflow with cached core build,
so 10min just for the test step should be plenty.

* don't forget to set the limit on windows, too
2022-10-15 22:58:48 +02:00
Simon Laux
72941e51fc exit node test when it failed (#3673) 2022-10-15 22:23:58 +02:00
Simon Laux
836c016f97 jsonrpc: add getMessageHtml (#3671)
* add getMessageHtml function

* add changelog entry
2022-10-15 20:47:31 +02:00
Simon Laux
e8ea9b7127 jsonrpc/events: commit type I forgot to commit (#3670)
commit line I forgot to commit
2022-10-15 20:35:30 +02:00
link2xt
f80c78536f fix unused result error 2022-10-15 13:11:53 +00:00
Hocuri
7877187894 Join all migration messages into one (#3665) 2022-10-15 07:06:40 +00:00
Simon Laux
a384a57979 cffi:jsonrpc: send events (#3662)
* jsonrpc in cffi also sends events now

* add pr id to changelog

* jsonrpc: new format for events and better typescript autocompletion (#3663)

* jsonrpc: new format for events and better typescript autocompletion

* adjust doc comments
2022-10-14 22:46:43 +00:00
Simon Laux
86e1476dee jsonrpc: add viewType to quoted message(MessageQuote type) in Message object type (#3651)
* jsonrpc: add `viewType` to quoted message(`MessageQuote` type) in `Message` object type

* add pr number to changelog
2022-10-13 15:41:54 +00:00
Jikstra
81d0ecd8f6 jsonrpc: more methods and some fixes (#3653)
* jsonrpc: Implement join_securejoin(), contacts_delete(),
contacts_change_name(), send_sticker()

* Add missing fn

* cargo fmt

* add missing &self

* Make it compile

* fixup return type, clippy and doc comment

* generate types and add 2 functions

* change naming

* changelog entry

* jsonrpc add start and stop io functions

* fix getMessageListItems

* normalize daymarker timestamps for MessageListItem

* jsonrpc: exportBackup and importBackup

* don't multiply timestamp anymore

* update types.ts

Co-authored-by: Simon Laux <mobile.info@simonlaux.de>
2022-10-13 12:56:10 +02:00
link2xt
6f329c9e96 Fix unix timestamp used for daymarker 2022-10-12 18:38:03 +00:00
Hocuri
0e2445c7a0 Move the actual logic of email parsing to EmailAddress::new() (#3656)
Very small PR; Motivation: Easier navigation using Go-To-definition.

Because, using go-to-definition of rust-analyzer on parse() doesn't take you to the actual parse() implementation but its trait definiton. On the other hand, it's very easy to find EmailAddress::new().
2022-10-12 20:37:17 +02:00
Hocuri
7ed947f598 Remove forgotten dbg! 2022-10-12 10:49:34 +02:00
link2xt
b85f6ea6c3 contact: do not ignore SQL errors in add_or_lookup 2022-10-11 16:42:17 +00:00
dependabot[bot]
f85f088d65 cargo: bump percent-encoding from 2.1.0 to 2.2.0
Bumps [percent-encoding](https://github.com/servo/rust-url) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/percent-encoding-v2.1.0...v2.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 12:33:13 +00:00
dependabot[bot]
045472deac Merge pull request #3625 from deltachat/dependabot/cargo/tokio-stream-0.1.10 2022-10-11 12:15:16 +00:00
dependabot[bot]
71cad4df58 Merge pull request #3632 from deltachat/dependabot/cargo/tokio-1.21.2 2022-10-11 12:13:52 +00:00
dependabot[bot]
48786522c8 Merge pull request #3622 from deltachat/dependabot/cargo/thiserror-1.0.37 2022-10-11 10:46:18 +00:00
dependabot[bot]
0827f1b2f6 cargo: bump tokio-stream from 0.1.9 to 0.1.10
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.9 to 0.1.10.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.9...tokio-stream-0.1.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 08:16:26 +00:00
dependabot[bot]
5973bb8610 cargo: bump thiserror from 1.0.33 to 1.0.37
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.33 to 1.0.37.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.33...1.0.37)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 08:16:17 +00:00
dependabot[bot]
00ca5132b4 cargo: bump tokio from 1.20.1 to 1.21.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.20.1 to 1.21.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.20.1...tokio-1.21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 08:16:16 +00:00
dependabot[bot]
191d203b24 Merge pull request #3623 from deltachat/dependabot/cargo/serde-1.0.145 2022-10-11 08:14:15 +00:00
dependabot[bot]
06f96011d8 Merge pull request #3635 from deltachat/dependabot/cargo/textwrap-0.15.1 2022-10-11 08:13:26 +00:00
dependabot[bot]
f2e292c702 Merge pull request #3654 from deltachat/dependabot/cargo/syn-1.0.102 2022-10-11 08:13:09 +00:00
dependabot[bot]
832deb8e97 Merge pull request #3631 from deltachat/dependabot/cargo/once_cell-1.15.0 2022-10-11 08:12:48 +00:00
dependabot[bot]
74a3c57222 cargo: bump syn from 1.0.99 to 1.0.102
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.99 to 1.0.102.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.99...1.0.102)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 05:40:18 +00:00
dependabot[bot]
4a19092db0 Merge pull request #3636 from deltachat/dependabot/cargo/axum-0.5.16 2022-10-11 05:38:48 +00:00
dependabot[bot]
5c652d913a cargo: bump serde from 1.0.144 to 1.0.145
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.144 to 1.0.145.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.144...v1.0.145)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 01:13:33 +00:00
dependabot[bot]
053213f50e cargo: bump axum from 0.5.15 to 0.5.16
Bumps [axum](https://github.com/tokio-rs/axum) from 0.5.15 to 0.5.16.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.5.15...axum-v0.5.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 01:13:03 +00:00
dependabot[bot]
342e946f49 Merge pull request #3634 from deltachat/dependabot/cargo/sha2-0.10.6 2022-10-11 01:12:07 +00:00
dependabot[bot]
d15ab1355b cargo: bump once_cell from 1.13.1 to 1.15.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.13.1 to 1.15.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.13.1...v1.15.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-10-11 01:11:41 +00:00
dependabot[bot]
e47f4b8f80 Merge pull request #3600 from deltachat/dependabot/cargo/axum-core-0.2.8 2022-10-11 01:11:33 +00:00
dependabot[bot]
637a4eb351 cargo: bump textwrap from 0.15.0 to 0.15.1
Bumps [textwrap](https://github.com/mgeisler/textwrap) from 0.15.0 to 0.15.1.
- [Release notes](https://github.com/mgeisler/textwrap/releases)
- [Changelog](https://github.com/mgeisler/textwrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mgeisler/textwrap/compare/0.15.0...0.15.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-11 01:11:25 +00:00
dependabot[bot]
a233aeec4a Merge pull request #3633 from deltachat/dependabot/cargo/image-0.24.4 2022-10-11 01:11:08 +00:00
dependabot[bot]
d24104154f Merge pull request #3624 from deltachat/dependabot/cargo/libc-0.2.134 2022-10-11 01:10:31 +00:00
dependabot[bot]
7fa2706d0e Merge pull request #3621 from deltachat/dependabot/cargo/url-2.3.0 2022-10-11 01:09:37 +00:00
dependabot[bot]
2a5365d46d Merge pull request #3637 from deltachat/dependabot/cargo/env_logger-0.9.1 2022-10-11 01:07:43 +00:00
dependabot[bot]
72cc853420 Merge pull request #3638 from deltachat/dependabot/cargo/reqwest-0.11.12 2022-10-11 01:07:14 +00:00
dependabot[bot]
eea111df6f Merge pull request #3639 from deltachat/dependabot/cargo/anyhow-1.0.65 2022-10-11 00:02:54 +00:00
Simon Laux
130bea9e25 jsonrpc: better way to access messagelist (#3652)
remove function `messageListGetMessageIds()`,
it is replaced by `getMessageIds()` and `getMessageListEntries()`
the latter returns a new `MessageListItem` type,
which is the now prefered way of using the message list.
2022-10-10 18:56:59 +00:00
dependabot[bot]
754242439a cargo: bump env_logger from 0.9.0 to 0.9.1
Bumps [env_logger](https://github.com/env-logger-rs/env_logger) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/env-logger-rs/env_logger/releases)
- [Changelog](https://github.com/env-logger-rs/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/env-logger-rs/env_logger/compare/v0.9.0...v0.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-10 15:16:03 +00:00
Simon Laux
3fab9e4cec more jsonrpc porting (#3645)
* Port setChatVisbility to jsonrpc

* jsonrpc add functions

- setChatEphemeralTimer
- getChatEphemeralTimer
and changelog

* add pr number to changelog

* jsonrpc: getLocations function

* Port imex() to jsonrpc

* autogenerate types

* jsonrpc: add `getAccountFileSize()`

* jsonrpc: `estimateAutodeleteCount`

* jsonrpc: setStockStrings

* Refactor imex into exportSelfKeys and importSelfKeys

* generate typings

* rustformat

* fix clippy

* update changelog

Co-authored-by: jikstra <jikstra@disroot.org>
2022-10-10 15:14:17 +00:00
link2xt
8b6290120e Create bob with new_bob(), not new_alice() 2022-10-09 22:51:37 +00:00
dependabot[bot]
564aef2a2a cargo: bump sha2 from 0.10.3 to 0.10.6
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.3 to 0.10.6.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.3...sha2-v0.10.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-06 18:11:58 +00:00
dependabot[bot]
5958324550 cargo: bump anyhow from 1.0.63 to 1.0.65
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.63 to 1.0.65.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.63...1.0.65)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-06 18:08:41 +00:00
Simon Laux
683fc1f081 jsonrpc: add more functions (#3641)
* jsonrpc: add more functions

  - `getChatContacts()`
  - `createGroupChat()`
  - `createBroadcastList()`
  - `setChatName()`
  - `setChatProfileImage()`
  - `downloadFullMessage()`
  - `lookupContactIdByAddr()`
  - `sendVideochatInvitation()`

* jsonrpc: add searchMessages

* jsonrpc add messageIdsToSearchResults function
and `MessageSearchResult` type

* fix return type of message_ids_to_search_results
2022-10-04 14:37:48 +02:00
link2xt
9277b46620 Suppress welcome device messages after account import
Add dummy `devmsglabels` entries on import to avoid welcome messages
being added when user runs reconfiguration on imported account.
2022-10-02 21:55:27 +00:00
link2xt
261926222b Use anyhow::Result in stock_str.rs 2022-10-02 20:49:45 +00:00
link2xt
35cfefd934 Share stock strings across accounts
All contexts created by the same account manager
share stock string translations. Setting translation on
a single context automatically sets translations for all other
accounts, so it is enough to set translations on the active account.
2022-10-02 19:12:04 +00:00
dependabot[bot]
aa13374523 cargo: bump reqwest from 0.11.11 to 0.11.12
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.11 to 0.11.12.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.11...v0.11.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-01 21:03:51 +00:00
dependabot[bot]
fd1cd39c7c cargo: bump image from 0.24.3 to 0.24.4
Bumps [image](https://github.com/image-rs/image) from 0.24.3 to 0.24.4.
- [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/compare/v0.24.3...v0.24.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-01 21:03:20 +00:00
dependabot[bot]
3e6d1d5789 cargo: bump trust-dns-resolver from 0.21.2 to 0.22.0
Bumps [trust-dns-resolver](https://github.com/bluejekyll/trust-dns) from 0.21.2 to 0.22.0.
- [Release notes](https://github.com/bluejekyll/trust-dns/releases)
- [Changelog](https://github.com/bluejekyll/trust-dns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluejekyll/trust-dns/compare/v0.21.2...v0.22.0)

---
updated-dependencies:
- dependency-name: trust-dns-resolver
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-01 21:02:36 +00:00
dependabot[bot]
185d0bf3a3 cargo: bump libc from 0.2.132 to 0.2.134
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.132 to 0.2.134.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.132...0.2.134)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-01 21:02:24 +00:00
dependabot[bot]
837f3ce04c cargo: bump url from 2.2.2 to 2.3.0
Bumps [url](https://github.com/servo/rust-url) from 2.2.2 to 2.3.0.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.2.2...v2.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-01 21:02:07 +00:00
Simon Laux
c661619263 prepare 1.96.0 (#3619)
after commit, on master make sure to:

   git tag -a 1.96.0
   git push origin 1.96.0
   git tag -a py-1.96.0
   git push origin py-1.96.0
2022-10-01 22:16:24 +02:00
Simon Laux
b2f7a7bb2e jsonrpc js client: ci upload and new name (#3618)
* jsonrpc js client: ci upload and new name

* Update jsonrpc-client-npm-package.yml

* Update jsonrpc-client-npm-package.yml

* Update jsonrpc-client-npm-package.yml

* change details message

* make sure to generate dist directory
2022-10-01 20:43:45 +02:00
bjoern
1965866813 prepare 1.95 (#3617)
* update changelog for 1.95.0

* bump version to 1.95.0
2022-09-30 12:43:44 +02:00
Simon Laux
110f56777d implement dclogin scheme (#3541)
* start implementing dclogin scheme

* fix formatting

* add test for usename+extension@host cases

* add test with all advanced options

* add changelog

* jsonrpc api and regenerate node constants

* Update src/qr/dclogin_scheme.rs

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

* apply Hocuris comments from code review

* fix clippy

* Use .eq_ignore_ascii_case()

* rename internal function apply_from_login_qr
to configure_from_login_qr

* fix error message

* cargo fmt

* remove test todo comment

Co-authored-by: Hocuri <hocuri@gmx.de>
2022-09-29 18:32:17 +02:00
Simon Laux
37c6001b6c jsonrpc: add chat_get_neighboring_media function (#3610)
* jsonrpc: add `chat_get_neighboring_media` function

* add number to changelog
2022-09-27 12:42:32 +02:00
Simon Laux
9a9c91e591 add method for desktop to get notification relevant information for a message (#3614)
* add method for desktop to get notification relevant information for a message

* add pr number to changelog

* rename MessageNotificationData to MessageNotificationInfo
2022-09-25 21:29:46 +02:00
link2xt
17276179e7 python: fix test_getinfo after bcc_self default change 2022-09-25 10:29:48 +00:00
link2xt
335d780f3e Fix Python test for bcc_self 2022-09-25 10:09:26 +00:00
link2xt
450d113993 Fix Rust 1.64 clippy warnings and tests 2022-09-25 03:17:00 +00:00
bjoern
cd6d181bbc enable BccSelf by default (#3612)
* enable `BccSelf` by default

enabling `BccSelf` improves user experience as
it is easier to set up another device
and ppl will also see "all" messages in other user agents directly.

for uncounted user problems, after diving into the issue,
the resulting device was "turn on BccSelf".
disabled `BccSelf` was probably the the number one single reason
of user problems.

main drawback of the change are potentially double notifications
when using a shared account and having another mail app on the same device.
however, we meanwhile do not recommend shared accounts at all,
the issue is also fixable by the other mail apps (as done by K-9)
and could be even regarded as a feature (you can decide which app to use for ansering).
but at the end the drawback is probably much smaller than the issues reported above.

* adapt tests to `BccSelf` enabled

* update CHANGELOG
2022-09-24 21:54:06 +02:00
Simon Laux
c92c6a24a0 jsonrpc: add mailingListAddress property to FullChat (#3607)
* jsonrpc: add `mailingListAddress` property to `FullChat`

* add pr number to changelog
2022-09-19 22:32:34 +02:00
link2xt
ffe7216194 Emit per-message events for expired messages
Instead of emitting single MsgsChanged event
with zero chat and msg IDs, emit one event per message.

Also emit WebxdcInstanceDeleted event if expired message
contains a webxdc.
2022-09-17 21:23:47 +00:00
link2xt
474eb7cbc8 Fix changelog PR reference 2022-09-17 17:51:33 +00:00
link2xt
252b528f40 Update "How messages are deleted" documentation
"IMAP folder and UID information" is no longer stored
in the `msgs` table since creation of the `imap` table.
As a result `msgs` table entries do not contain UID information in the
first place.

This commit updates documentation to reflect this change and also
points to `prune_tombstones()` procedure which actually deletes `msgs`
rows.
2022-09-17 17:18:53 +00:00
Simon Laux
a4357712bf fix changelog entry
was inserted at wrong place
2022-09-17 16:16:35 +02:00
Simon Laux
62afd3d4c3 truncate incoming messages by lines (#3480)
* truncate incoming messages by lines,
because many linebreaks seem to cause the chat open delay on deltachat-ios

* run cargo fmt

* remove DC_DESIRED_TEXT_LINES_THRESHOLD
and use Strings instead of Cow<str>

* remove usage of clippy::indexing_slicing in truncate_by_lines (#3596)

* adjust comments

* Fix truncate_by_lines tests

* Reword indexing/slicing error

* Remove unnecessary conditional

* Fix a typo in the comment

Co-authored-by: link2xt <link2xt@testrun.org>
2022-09-17 16:15:33 +02:00
link2xt
569628a202 Release 1.94.0 2022-09-16 23:02:05 +00:00
link2xt
c75bc66560 Make initiate_key_transfer() non-blocking 2022-09-16 22:03:25 +00:00
dependabot[bot]
ad8d3e2444 cargo: bump axum-core from 0.2.7 to 0.2.8
Bumps [axum-core](https://github.com/tokio-rs/axum) from 0.2.7 to 0.2.8.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-core-v0.2.7...axum-core-v0.2.8)

---
updated-dependencies:
- dependency-name: axum-core
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-15 03:29:08 +00:00
Simon Laux
56cd875fbd update node constants, follow up to #3592 (#3593)
update node constants, this was forgotten in #3592
not a big deal as these are generated on building node,
so this is just a simple commit of these files after running `npm run build`
2022-09-12 19:39:41 +02:00
bjoern
06873ed04f simplify return value of flush_status_updates() (#3597)
this was probably forgotten by recent refactorings in this area.
2022-09-11 23:21:00 +02:00
link2xt
5b518e588f Remove StockMessage.action_by_contact() (#3518)
Use separate strings for "... by me" and "... by contact"
to make them easier to translate.
2022-09-11 20:38:34 +02:00
Simon Laux
d3f2db2326 jsonrpc: add more functions, mostly message related (#3590)
* add more functions, see changelog for details

* add pr number to changelog

* clarify doc comment

* clarify usage of BasicChat
and adjust properties acordingly

r10s is right it should only contain what we need of the expensive calls

* fix doc typos

* run cargo fmt

* jsonrpc: add connectivity functions

* fix typo

* fix typo

* Add get_contact_encryption_info and get_connectivity_html

Fix get_connectivity_html and get_encrinfo futures not being Send. See https://github.com/rust-lang/rust/issues/101650 for more information.

Co-authored-by: jikstra <jikstra@disroot.org>

* Update CHANGELOG

* Update typescript files

* remove todo from changelog

Co-authored-by: jikstra <jikstra@disroot.org>
2022-09-11 17:48:42 +00:00
Sebastian Klähn
e619d9690d Webxdc delete event (#3592)
webxdc delete event
2022-09-11 13:53:55 +02:00
link2xt
40dc182295 python: move get_dc_event_name() to events 2022-09-10 21:22:38 +00:00
link2xt
3cf1aad551 node: npm run build:core:constants 2022-09-10 19:55:51 +00:00
Sebastian Klähn
7cff681234 Only apply webxdc updates for members of group (#3568)
Don't accept wexdc updates from members not in group
2022-09-09 18:42:35 +00:00
Simon Laux
54b10106bd jsonrpc: add deleteMessages() and getMessageInfo() (#3587)
* jsonrpc: add `deleteMessages()` and `getMessageInfo()`

* add pr numbers to changelog
2022-09-07 22:22:37 +02:00
Simon Laux
484aa54ed6 jsonrpc: add webxdc_info property to Message (#3588)
* jsonrpc: add `webxdc_info` property to `Message`

* add pr number to changelog
2022-09-07 22:14:13 +02:00
Simon Laux
417bddfa16 add more functions to jsonrpc (#3586)
* jsonrpc: add functions:
  - `deleteChat()`
  - `getChatEncryptionInfo()`
  - `getChatSecurejoinQrCodeSvg()`
  - `leaveGroup()`
  - `removeContactFromChat()`
  - `addContactToChat()`
 also fix doc a little

* Update deltachat-jsonrpc/src/api/mod.rs

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

Co-authored-by: bjoern <r10s@b44t.com>
2022-09-07 17:45:15 +02:00
Simon Laux
1e4e799f2e add pr id to changelog entry 2022-09-07 11:02:38 +02:00
Simon Laux
33b18e3014 jsonrpc: add was_seen_recently property
to `ChatListItemFetchResult`, `FullChat` and `Contact`
2022-09-07 11:02:38 +02:00
Simon Laux
96ce8eb851 jsonrpc: add is_broadcast property
to `ChatListItemFetchResult`
2022-09-07 11:02:38 +02:00
Simon Laux
8de5e964e0 add pr id to changelog entry 2022-09-07 10:40:18 +02:00
Simon Laux
43150195d4 fix set_core_version.py script to also update version in deltachat-jsonrpc/typescript/package.json 2022-09-07 10:40:18 +02:00
Sebastian Klähn
065c7af9a0 Fix typos & add documentation (#3569)
* fix typo

* tips & typos

* improve doc string

* update documentation

* fmt

* fix typo
2022-09-06 11:15:49 +02:00
bjoern
949370ad63 restricted webxdc internet access (#3516)
* add request_internet_access manifest option

* test request_internet_access

* update CHANGELOG

* force warning when internet access is enabled

if internet access is enabled,
show a warning instead of the normal summary
(the internet access is currently mainly to test out integrations
as maps for video chat; the summary is dispensable in the cases currently)

* adapt json-rpc's WebxdcMessageInfo
2022-09-05 11:45:34 +02:00
link2xt
4b91a88bc9 Do not prepend subject to mailing list chat messages (#3253) 2022-09-05 08:12:33 +00:00
dependabot[bot]
d04a0c8f2f cargo: bump libc from 0.2.129 to 0.2.132 (#3580)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.129 to 0.2.132.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.129...0.2.132)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 12:43:50 +02:00
dependabot[bot]
6f6618d46f cargo: bump futures from 0.3.21 to 0.3.24 (#3575)
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.21 to 0.3.24.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.21...0.3.24)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 12:43:02 +02:00
dependabot[bot]
90eb39439a cargo: bump chrono from 0.4.21 to 0.4.22 (#3576)
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.21 to 0.4.22.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/v0.4.22/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.21...v0.4.22)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 12:01:12 +02:00
dependabot[bot]
9813265ef4 cargo: bump sha2 from 0.10.2 to 0.10.3 (#3577)
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.2 to 0.10.3.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.2...sha2-v0.10.3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 11:59:46 +02:00
dependabot[bot]
8acbf9babf cargo: bump once_cell from 1.13.0 to 1.13.1 (#3579)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.13.0 to 1.13.1.
- [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.13.0...v1.13.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 11:58:47 +02:00
dependabot[bot]
97d7c20549 cargo: bump fast-socks5 from 0.8.0 to 0.8.1 (#3582)
Bumps [fast-socks5](https://github.com/dizda/fast-socks5) from 0.8.0 to 0.8.1.
- [Release notes](https://github.com/dizda/fast-socks5/releases)
- [Commits](https://github.com/dizda/fast-socks5/compare/v0.8.0...v0.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 11:56:36 +02:00
dependabot[bot]
4eddd7f616 cargo: bump thiserror from 1.0.32 to 1.0.33 (#3572)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.32 to 1.0.33.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.32...1.0.33)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 11:16:09 +02:00
dependabot[bot]
217217bd2b cargo: bump serde_json from 1.0.83 to 1.0.85 (#3573)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.83 to 1.0.85.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.83...v1.0.85)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 11:15:42 +02:00
dependabot[bot]
37eeb55a5b cargo: bump serde from 1.0.143 to 1.0.144 (#3581)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.143 to 1.0.144.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.143...v1.0.144)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 11:14:22 +02:00
dependabot[bot]
d32da8e4d5 cargo: bump anyhow from 1.0.61 to 1.0.63 (#3578)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.61 to 1.0.63.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.61...1.0.63)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-02 11:13:59 +02:00
bjoern
da3052fdc2 document new bot behavior wrt accepting chats (#3571) 2022-09-01 18:59:32 +02:00
Sebastian Klähn
97982ef93a Auto-accept contact requests for bots (#3567)
Auto accept contact request for bots
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-09-01 14:58:47 +02:00
bjoern
25bff21edd fix doc, dc_accounts_get_next_event() is gone (#3565) 2022-08-29 16:26:38 +02:00
bjoern
b2b22c8b85 order contact lists by "last seen" instead of name/address (#3562)
order all contact lists by last_seen instead of name or address
2022-08-29 13:55:00 +02:00
link2xt
5663b1c539 Add defines for backward compatibility 2022-08-28 21:19:53 +00:00
link2xt
c68a4543db Remove dc_accounts_event_emitter_t
Use dc_event_emitter_t instead.
2022-08-28 20:47:41 +00:00
link2xt
88dc8a389a Update Cargo.lock 2022-08-28 20:47:27 +00:00
link2xt
e7b6c689ce Keep deltachat-jsonrpc version in sync 2022-08-28 17:30:20 +00:00
Hocuri
6ec9b0a0b0 Show attached .eml files as such (#3561)
Co-authored-by: bjoern <r10s@b44t.com>
2022-08-27 14:36:45 +00:00
bjoern
682ec563c8 add was_seen_recently() (#3560)
* add dc_contact_was_seen_recently()

* add wasSeenRecently() to node
2022-08-26 19:21:44 +02:00
B. Petersen
8eb4d9bf08 fix some typos 2022-08-26 09:28:58 +02:00
link2xt
977a8cf33f python: remove unused setup.cfg 2022-08-22 00:11:01 +00:00
link2xt
db496b82fb Add --features jsonrpc to scripts/run_all.sh
Python wheels cannot be built without it
as they expect everything defined in deltachat.h
to be in the C library.
2022-08-22 00:10:30 +00:00
link2xt
9d578884f9 Test that no notifications are displayed for blocked mailing lists 2022-08-20 14:12:30 +00:00
link2xt
84ae27744a test_utils: add EventTracker.conusme_events() 2022-08-20 14:12:22 +00:00
link2xt
54df44d930 test_utils: implement Deref and DerefMut for EventTracker 2022-08-20 14:12:05 +00:00
link2xt
f79b16c244 Do not emit notifications for blocked chats 2022-08-20 13:56:09 +00:00
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
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
457 changed files with 15828 additions and 1812 deletions

11
.cargo/config.toml Normal file
View File

@@ -0,0 +1,11 @@
[env]
# In unoptimised builds tokio tends to use a lot of stack space when
# creating some complicated futures, tokio has an open issue for this:
# https://github.com/tokio-rs/tokio/issues/2055. Some of our tests
# manage to not fit in the default 2MiB stack anymore due to this, so
# while the issue is not resolved we want to work around this.
# Because compiling optimised builds takes a very long time we prefer
# to avoid that. Setting this environment variable ensures that when
# invoking `cargo test` threads are allowed to have a large enough
# stack size without needing to use an optimised build.
RUST_MIN_STACK = "8388608"

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

@@ -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
@@ -116,6 +116,11 @@ 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@v4
@@ -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 }}

View File

@@ -0,0 +1,83 @@
name: 'jsonrpc js client build'
on:
pull_request:
push:
tags:
- '*'
- '!py-*'
jobs:
pack-module:
name: 'Package @deltachat/jsonrpc-client and upload to download.delta.chat'
runs-on: ubuntu-18.04
steps:
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: get tag
id: tag
uses: dawidd6/action-get-tag@v1
continue-on-error: true
- name: Get Pullrequest ID
id: prepare
run: |
tag=${{ steps.tag.outputs.tag }}
if [ -z "$tag" ]; then
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
else
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
echo "No preview will be uploaded this time, but the $tag release"
fi
- name: System info
run: |
npm --version
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
- name: install dependencies without running scripts
run: |
cd deltachat-jsonrpc/typescript
npm install --ignore-scripts
- name: package
shell: bash
run: |
cd deltachat-jsonrpc/typescript
npm run build:tsc
npm pack .
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v1
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
# Upload to download.delta.chat/node/preview/
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
if: ${{ ! steps.tag.outputs.tag }}
id: upload-preview
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
continue-on-error: true
- name: "Post links to details"
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
env:
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
# Upload to download.delta.chat/node/
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
if: ${{ steps.tag.outputs.tag }}
id: upload
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"

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

@@ -0,0 +1,46 @@
name: JSON-RPC API Test
on:
push:
branches: [master]
pull_request:
branches: [master]
env:
CARGO_TERM_COLOR: always
RUST_MIN_STACK: "8388608"
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

@@ -52,6 +52,7 @@ jobs:
npm install --verbose
- name: Test
timeout-minutes: 10
if: runner.os != 'Windows'
run: |
cd node
@@ -59,6 +60,7 @@ jobs:
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: Run tests on Windows, except lint
timeout-minutes: 10
if: runner.os == 'Windows'
run: |
cd node

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,13 +2,241 @@
## Unreleased
### Added
### API-Changes
### Changes
### Fixes
## 1.99.0
### API-Changes
- breaking jsonrpc: changed function naming
- `autocryptInitiateKeyTransfer` -> `initiateAutocryptKeyTransfer`
- `autocryptContinueKeyTransfer` -> `continueAutocryptKeyTransfer`
- `chatlistGetFullChatById` -> `getFullChatById`
- `messageGetMessage` -> `getMessage`
- `messageGetMessages` -> `getMessages`
- `messageGetNotificationInfo` -> `getMessageNotificationInfo`
- `contactsGetContact` -> `getContact`
- `contactsCreateContact` -> `createContact`
- `contactsCreateChatByContactId` -> `createChatByContactId`
- `contactsBlock` -> `blockContact`
- `contactsUnblock` -> `unblockContact`
- `contactsGetBlocked` -> `getBlockedContacts`
- `contactsGetContactIds` -> `getContactIds`
- `contactsGetContacts` -> `getContacts`
- `contactsGetContactsByIds` -> `getContactsByIds`
- `chatGetMedia` -> `getChatMedia`
- `chatGetNeighboringMedia` -> `getNeighboringChatMedia`
- `webxdcSendStatusUpdate` -> `sendWebxdcStatusUpdate`
- `webxdcGetStatusUpdates` -> `getWebxdcStatusUpdates`
- `messageGetWebxdcInfo` -> `getWebxdcInfo`
- jsonrpc: changed method signature
- `miscSendTextMessage(accountId, text, chatId)` -> `miscSendTextMessage(accountId, chatId, text)`
- jsonrpc: add `SystemMessageType` to `Message`
- cffi: add missing `DC_INFO_` constants
- Add DC_EVENT_INCOMING_MSG_BUNCH event #3643
- Python bindings: Make get_matching() only match the
whole event name, e.g. events.get_matching("DC_EVENT_INCOMING_MSG")
won't match DC_EVENT_INCOMING_MSG_BUNCH anymore #3643
- Rust: Introduce a ContextBuilder #3698
### Changes
- allow sender timestamp to be in the future, but not too much
- Disable the new "Authentication-Results/DKIM checking" security feature
until we have tested it a bit #3728
- refactorings #3706
### Fixes
- `dc_search_msgs()` returns unaccepted requests #3694
- emit "contacts changed" event when the contact is no longer "seen recently" #3703
- do not allow peerstate reset if DKIM check failed #3731
## 1.98.0
### API-Changes
- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681
- added reactions support #3644
- jsonrpc: reactions: added reactions to `Message` type and the `sendReaction()` method #3686
### Changes
- simplify `UPSERT` queries #3676
### Fixes
## 1.97.0
### API-Changes
- jsonrpc: add function: #3641, #3645, #3653
- `getChatContacts()`
- `createGroupChat()`
- `createBroadcastList()`
- `setChatName()`
- `setChatProfileImage()`
- `downloadFullMessage()`
- `lookupContactIdByAddr()`
- `sendVideochatInvitation()`
- `searchMessages()`
- `messageIdsToSearchResults()`
- `setChatVisibility()`
- `getChatEphemeralTimer()`
- `setChatEphemeralTimer()`
- `getLocations()`
- `getAccountFileSize()`
- `estimateAutoDeletionCount()`
- `setStockStrings()`
- `exportSelfKeys()`
- `importSelfKeys()`
- `sendSticker()`
- `changeContactName()`
- `deleteContact()`
- `joinSecurejoin()`
- `stopIoForAllAccounts()`
- `startIoForAllAccounts()`
- `startIo()`
- `stopIo()`
- `exportBackup()`
- `importBackup()`
- `getMessageHtml()` #3671
- `miscGetStickerFolder` and `miscGetStickers` #3672
- breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list.
- jsonrpc: add type: #3641, #3645
- `MessageSearchResult`
- `Location`
- jsonrpc: add `viewType` to quoted message(`MessageQuote` type) in `Message` object type #3651
### Changes
- Look at Authentication-Results. Don't accept Autocrypt key changes
if they come with negative authentiation results while this contact
sent emails with positive authentication results in the past. #3583
- jsonrpc in cffi also sends events now #3662
- jsonrpc: new format for events and better typescript autocompletion
- Join all "[migration] vXX" log messages into one
### Fixes
- share stock string translations across accounts created by the same account manager #3640
- suppress welcome device messages after account import #3642
- fix unix timestamp used for daymarker #3660
## 1.96.0
### Changes
- jsonrpc js client:
- Change package name from `deltachat-jsonrpc-client` to `@deltachat/jsonrpc-client`
- remove relative file dependency to it from `deltachat-node` (because it did not work anyway and broke the nix build of desktop)
- ci: add github ci action to upload it to our download server automaticaly on realease
## 1.95.0
### API-Changes
- jsonrpc: add `mailingListAddress` property to `FullChat` #3607
- jsonrpc: add `MessageNotificationInfo` & `messageGetNotificationInfo()` #3614
- jsonrpc: add `chat_get_neighboring_media` function #3610
### Changes
- added `dclogin:` scheme to allow configuration from a qr code
(data inside qrcode, contrary to `dcaccount:` which points to an API to create an account) #3541
- truncate incoming messages by lines instead of just length #3480
- emit separate `DC_EVENT_MSGS_CHANGED` for each expired message,
and `DC_EVENT_WEBXDC_INSTANCE_DELETED` when a message contains a webxdc #3605
- enable `bcc_self` by default #3612
## 1.94.0
### API-Changes
- breaking change: replace `dc_accounts_event_emitter_t` with `dc_event_emitter_t` #3422
Type `dc_accounts_event_emitter_t` is removed.
`dc_accounts_get_event_emitter()` returns `dc_event_emitter_t` now, so
`dc_get_next_event()` should be used instead of `dc_accounts_get_next_event`
and `dc_event_emitter_unref()` should be used instead of
`dc_accounts_event_emitter_unref`.
- add `dc_contact_was_seen_recently()` #3560
- Fix `get_connectivity_html` and `get_encrinfo` futures not being Send. See rust-lang/rust#101650 for more information
- jsonrpc: add functions: #3586, #3587, #3590
- `deleteChat()`
- `getChatEncryptionInfo()`
- `getChatSecurejoinQrCodeSvg()`
- `leaveGroup()`
- `removeContactFromChat()`
- `addContactToChat()`
- `deleteMessages()`
- `getMessageInfo()`
- `getBasicChatInfo()`
- `marknoticedChat()`
- `getFirstUnreadMessageOfChat()`
- `markseenMsgs()`
- `forwardMessages()`
- `removeDraft()`
- `getDraft()`
- `miscSendMsg()`
- `miscSetDraft()`
- `maybeNetwork()`
- `getConnectivity()`
- `getContactEncryptionInfo()`
- `getConnectivityHtml()`
- jsonrpc: add `is_broadcast` property to `ChatListItemFetchResult` #3584
- jsonrpc: add `was_seen_recently` property to `ChatListItemFetchResult`, `FullChat` and `Contact` #3584
- jsonrpc: add `webxdc_info` property to `Message` #3588
- python: move `get_dc_event_name()` from `deltachat` to `deltachat.events` #3564
- jsonrpc: add `webxdc_info`, `parent_id` and `download_state` property to `Message` #3588, #3590
- jsonrpc: add `BasicChat` object as a leaner alternative to `FullChat` #3590
- jsonrpc: add `last_seen` property to `Contact` #3590
- breaking! jsonrpc: replace `Message.quoted_text` and `Message.quoted_message_id` with `Message.quote` #3590
- add separate stock strings for actions done by contacts to make them easier to translate #3518
- `dc_initiate_key_transfer()` is non-blocking now. #3553
UIs don't need to display a button to cancel sending Autocrypt Setup Message with
`dc_stop_ongoing_process()` anymore.
### Changes
- order contact lists by "last seen";
this affects `dc_get_chat_contacts()`, `dc_get_contacts()` and `dc_get_blocked_contacts()` #3562
- add `internet_access` flag to `dc_msg_get_webxdc_info()` #3516
- `DC_EVENT_WEBXDC_INSTANCE_DELETED` is emitted when a message containing a webxdc gets deleted #3592
### Fixes
- do not emit notifications for blocked chats #3557
- Show attached .eml files correctly #3561
- Auto accept contact requests if `Config::Bot` is set for a client #3567
- Don't prepend the subject to chat messages in mailinglists
- fix `set_core_version.py` script to also update version in `deltachat-jsonrpc/typescript/package.json` #3585
- Reject webxcd-updates from contacts who are not group members #3568
## 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

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

1018
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.92.0"
version = "1.99.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
@@ -11,6 +11,9 @@ debug = 0
panic = 'abort'
opt-level = 1
[profile.test]
opt-level = 0
[profile.release]
lto = true
panic = 'abort'
@@ -23,7 +26,7 @@ anyhow = "1"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.21"
trust-dns-resolver = "0.22"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
backtrace = "0.3"
@@ -32,11 +35,11 @@ bitflags = "1.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
dirs = { version = "4", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = "0.2"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
@@ -46,18 +49,18 @@ native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.12.0"
percent-encoding = "2.0"
once_cell = "1.16.0"
percent-encoding = "2.2"
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.8"
regex = "1.5"
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"] }
@@ -74,16 +77,16 @@ fast-socks5 = "0.8"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.3.3"
textwrap = "0.15.0"
textwrap = "0.16.0"
async-channel = "1.6.1"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.9", features = ["fs"] }
reqwest = { version = "0.11.11", features = ["json"] }
tokio-stream = { version = "0.1.11", features = ["fs"] }
reqwest = { version = "0.11.12", 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"
criterion = { version = "0.3.4", features = ["async_tokio"] }
criterion = { version = "0.3.6", features = ["async_tokio"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
@@ -95,6 +98,7 @@ tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"]
members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc"
]
[[example]]

View File

@@ -125,10 +125,12 @@ $ cargo test -- --ignored
Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js** \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- **Node.js**
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Go**[^1] \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library
@@ -140,3 +142,5 @@ or its language bindings:
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
- several **Bots**
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.

View File

@@ -1,6 +1,7 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use tempfile::tempdir;
@@ -8,7 +9,9 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new(&dbfile, id, Events::new()).await.unwrap();
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
.await
.unwrap();
let book = (0..n)
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))

View File

@@ -5,11 +5,14 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
let id = 100;
let context = Context::new(dbfile, id, Events::new()).await.unwrap();
let context = Context::new(dbfile, id, Events::new(), StockStrings::new())
.await
.unwrap();
for c in chats.iter().take(10) {
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
@@ -23,7 +26,7 @@ fn criterion_benchmark(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let chats: Vec<_> = rt.block_on(async {
let context = Context::new(Path::new(&path), 100, Events::new())
let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
.await
.unwrap();
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();

View File

@@ -4,6 +4,7 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
async fn get_chat_list_benchmark(context: &Context) {
@@ -16,7 +17,7 @@ fn criterion_benchmark(c: &mut Criterion) {
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(async {
Context::new(Path::new(&path), 100, Events::new())
Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
.await
.unwrap()
});

View File

@@ -6,6 +6,7 @@ use deltachat::{
context::Context,
imex::{imex, ImexMode},
receive_imf::receive_imf,
stock_str::StockStrings,
Events,
};
use tempfile::tempdir;
@@ -41,7 +42,9 @@ async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new(&dbfile, id, Events::new()).await.unwrap();
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
.await
.unwrap();
let backup: PathBuf = std::env::current_dir()
.unwrap()

View File

@@ -1,11 +1,12 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use std::path::Path;
async fn search_benchmark(dbfile: impl AsRef<Path>) {
let id = 100;
let context = Context::new(dbfile.as_ref(), id, Events::new())
let context = Context::new(dbfile.as_ref(), id, Events::new(), StockStrings::new())
.await
.unwrap();

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.92.0"
version = "1.99.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -16,6 +16,7 @@ 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"
@@ -24,10 +25,11 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.7"
once_cell = "1.12.0"
once_cell = "1.16.0"
[features]
default = ["vendored"]
vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"]
jsonrpc = ["deltachat-jsonrpc"]

View File

@@ -11,19 +11,22 @@ extern "C" {
#endif
typedef struct _dc_context dc_context_t;
typedef struct _dc_accounts dc_accounts_t;
typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_context dc_context_t;
typedef struct _dc_accounts dc_accounts_t;
typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_reactions dc_reactions_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
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;
// Alias for backwards compatibility, use dc_event_emitter_t instead.
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
/**
* @mainpage Getting started
@@ -391,7 +394,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages.
* adds Auto-Submitted header to outgoing messages
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
@@ -465,10 +469,10 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
/**
* Set configuration values from a QR code.
* 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.
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN 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
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
*
* @memberof dc_context_t
@@ -988,6 +992,34 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id ID of the message you react to.
* @param reaction A string consisting of emojis separated by spaces.
* @return The ID of the message sent out or 0 for errors.
*/
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
/**
* Get a structure with reactions to the message.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The message ID to get reactions for.
* @return A structure with all reactions to the message.
*/
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
/**
* A webxdc instance sends a status update to its other members.
*
@@ -1263,7 +1295,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 +1305,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 +1317,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.
*
@@ -1385,6 +1417,9 @@ void dc_block_chat (dc_context_t* context, uint32_t ch
*
* Use it to accept "contact request" chats as indicated by dc_chat_is_contact_request().
*
* If the dc_set_config()-option `bot` is set,
* all chats are accepted automatically and calling this function has no effect.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to accept.
@@ -2168,11 +2203,10 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
* ~~~
*
* After that, this function should be called to send the Autocrypt Setup Message.
* The function creates the setup message and waits until it is really sent.
* As this may take a while, it is recommended to start the function in a separate thread;
* to interrupt it, you can use dc_stop_ongoing_process().
* The function creates the setup message and adds it to outgoing message queue.
* The message is sent asynchronously.
*
* After everything succeeded, the required setup code is returned in the following format:
* The required setup code is returned in the following format:
*
* ~~~
* 1234-1234-1234-1234-1234-1234-1234-1234-1234
@@ -2238,8 +2272,8 @@ int dc_continue_key_transfer (dc_context_t* context, uint32_t ms
* The ongoing process will return ASAP then, however, it may
* still take a moment.
*
* Typical ongoing processes are started by dc_configure(),
* dc_initiate_key_transfer() or dc_imex(). As there is always at most only
* Typical ongoing processes are started by dc_configure()
* or dc_imex(). As there is always at most only
* one onging process at the same time, there is no need to define _which_ process to exit.
*
* @memberof dc_context_t
@@ -2265,6 +2299,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname
#define DC_QR_REVIVE_VERIFYCONTACT 510
#define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname
#define DC_QR_LOGIN 520 // text1=email_address
/**
* Check a scanned QR code.
@@ -2337,6 +2372,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask the user if they want to revive the withdrawn group-invite code;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_LOGIN with dc_lot_t::text1=email_address:
* ask the user if they want to login with the email_address,
* if so, call dc_set_config_from_qr() and then dc_configure().
*
* @memberof dc_context_t
* @param context The context object.
* @param qr The text of the scanned QR code.
@@ -2510,9 +2549,9 @@ int dc_set_location (dc_context_t* context, double latit
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
* 0 for "all up to now".
* @return An array of locations, NULL is never returned.
* The array is sorted decending;
* The array is sorted descending;
* the first entry in the array is the location with the newest timestamp.
* Note that this is only realated to the recent postion of the user
* Note that this is only related to the recent position of the user
* if dc_array_is_independent() returns 0.
* The returned array must be freed using dc_array_unref().
*
@@ -2836,7 +2875,7 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
*
* The library will emit various @ref DC_EVENT events as "new message", "message read" etc.
* To get these events, you have to create an event emitter using this function
* and call dc_accounts_get_next_event() on the emitter.
* and call dc_get_next_event() on the emitter.
*
* This is similar to dc_get_event_emitter(), which, however,
* must not be called for accounts handled by the account manager.
@@ -2844,13 +2883,13 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
* @memberof dc_accounts_t
* @param accounts The account manager as created by dc_accounts_new().
* @return Returns the event emitter, NULL on errors.
* Must be freed using dc_accounts_event_emitter_unref() after usage.
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per account manager.
* Having more than one event emitter running at the same time on the same account manager
* will result in events randomly delivered to the one or to the other.
*/
dc_accounts_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
/**
@@ -3757,6 +3796,11 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* 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.
* - internet_access:
* true if the Webxdc should get full internet access, including Webrtc.
* currently, this is only true for encrypted Webxdc's in the self chat
* that have requested internet access in the manifest.
* this is useful for development and maybe for internal integrations at some point.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
@@ -4053,9 +4097,19 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
// DC_INFO* uses the same values as SystemMessage in rust-land
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_UNKNOWN 0
#define DC_INFO_GROUP_NAME_CHANGED 2
#define DC_INFO_GROUP_IMAGE_CHANGED 3
#define DC_INFO_MEMBER_ADDED_TO_GROUP 4
#define DC_INFO_MEMBER_REMOVED_FROM_GROUP 5
#define DC_INFO_AUTOCRYPT_SETUP_MESSAGE 6
#define DC_INFO_SECURE_JOIN_MESSAGE 7
#define DC_INFO_LOCATIONSTREAMING_ENABLED 8
#define DC_INFO_LOCATION_ONLY 9
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
/**
* Check if a message is still in creation. A message is in creation between
@@ -4638,6 +4692,22 @@ char* dc_contact_get_status (const dc_contact_t* contact);
*/
int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
/**
* Check if the contact was seen recently.
*
* The UI may highlight these contacts,
* eg. draw a little green dot on the avatars of the users recently seen.
* DC_CONTACT_ID_SELF and other special contact IDs are defined as never seen recently (they should not get a dot).
* To get the time a contact was seen, use dc_contact_get_last_seen().
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 1=contact seen recently, 0=contact not seen recently.
*/
int dc_contact_was_seen_recently (const dc_contact_t* contact);
/**
* Check if a contact is blocked.
*
@@ -4851,7 +4921,49 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
* @param lot The lot object.
* @return The timestamp as defined by the creator of the object. 0 if there is not timestamp or on errors.
*/
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* @class dc_reactions_t
*
* An object representing all reactions for a single message.
*/
/**
* Returns array of contacts which reacted to the given message.
*
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
*/
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
/**
* Returns a string containing space-separated reactions of a single contact.
*
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @param contact_id ID of the contact.
* @return Space-separated list of emoji sequences, which could be empty.
* Returned string should not be modified and should be freed
* with dc_str_unref() after usage.
*/
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
/**
* Frees an object containing message reactions.
*
* Reactions objects are created by dc_get_msg_reactions().
*
* @memberof dc_reactions_t
* @param reactions The object to free.
* If NULL is given, nothing is done.
*/
void dc_reactions_unref (dc_reactions_t* reactions);
/**
@@ -5192,13 +5304,61 @@ 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
*
* Opaque object that is used to get events from a single context.
* You can get an event emitter from a context using dc_get_event_emitter().
* If you are using the dc_accounts_t account manager,
* dc_accounts_event_emitter_t must be used instead.
* You can get an event emitter from a context using dc_get_event_emitter()
* or dc_accounts_get_event_emitter().
*/
/**
@@ -5214,6 +5374,8 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
// Alias for backwards compatibility, use dc_get_next_event instead.
#define dc_accounts_get_next_event dc_get_next_event
/**
* Free a context event emitter object.
@@ -5224,39 +5386,8 @@ dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
*/
void dc_event_emitter_unref(dc_event_emitter_t* emitter);
/**
* @class dc_accounts_event_emitter_t
*
* Opaque object that is used to get events from the dc_accounts_t account manager.
* You get an event emitter from the account manager using dc_accounts_get_event_emitter().
* If you are not using the dc_accounts_t account manager but just a single dc_context_t object,
* dc_event_emitter_t must be used instead.
*/
/**
* Get the next event from an accounts event emitter object.
*
* @memberof dc_accounts_event_emitter_t
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
* @return An event as an dc_event_t object.
* You can query the event for information using dc_event_get_id(), dc_event_get_data1_int() and so on;
* if you are done with the event, you have to free the event using dc_event_unref().
* If NULL is returned, the contexts belonging to the event emitter are unref'd and no more events will come;
* in this case, free the event emitter using dc_accounts_event_emitter_unref().
*/
dc_event_t* dc_accounts_get_next_event (dc_accounts_event_emitter_t* emitter);
/**
* Free an accounts event emitter object.
*
* @memberof dc_accounts_event_emitter_t
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
* If NULL is given, nothing is done and an error is logged.
*/
void dc_accounts_event_emitter_unref(dc_accounts_event_emitter_t* emitter);
// Alias for backwards compatibility, use dc_event_emtitter_unref instead.
#define dc_accounts_event_emitter_unref dc_event_emitter_unref
/**
* @class dc_event_t
@@ -5328,7 +5459,7 @@ char* dc_event_get_data2_str(dc_event_t* event);
* To get the context object belonging to the event, use dc_accounts_get_account().
*
* @memberof dc_event_t
* @param event The event object as returned from dc_accounts_get_next_event().
* @param event The event object as returned from dc_get_next_event().
* @return The account ID belonging to the event, 0 for account manager errors.
*/
uint32_t dc_event_get_account_id(dc_event_t* event);
@@ -5483,6 +5614,15 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_MSGS_CHANGED 2000
/**
* Message reactions changed.
*
* @param data1 (int) chat_id ID of the chat affected by the changes.
* @param data2 (int) msg_id ID of the message for which reactions were changed.
*/
#define DC_EVENT_REACTIONS_CHANGED 2001
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
@@ -5494,6 +5634,17 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_INCOMING_MSG 2005
/**
* Downloading a bunch of messages just finished. This is an experimental
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
*
* @param data1 0
* @param data2 (char*) msg_ids, a json object with the message ids.
*/
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
/**
* Messages were marked noticed or seen.
@@ -5678,7 +5829,15 @@ void dc_event_unref(dc_event_t* event);
* @param data1 (int) msg_id
* @param data2 (int) status_update_serial - must not be used by UI implementations.
*/
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
/**
* Message deleted which contained a webxdc instance.
*
* @param data1 (int) msg_id
*/
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
/**
@@ -5912,28 +6071,38 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages for group name changes.
/// - %1$s will be replaced by the old group name
/// - %2$s will be replaced by the new group name
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGRPNAME 15
/// "Group image changed."
///
/// Used in status messages for group images changes.
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGRPIMGCHANGED 16
/// "Member %1$s added."
///
/// Used in status messages for added members.
/// - %1$s will be replaced by the name of the added member
///
/// @deprecated 2022-09-10
#define DC_STR_MSGADDMEMBER 17
/// "Member %1$s removed."
///
/// Used in status messages for removed members.
/// - %1$s will be replaced by the name of the removed member
///
/// @deprecated 2022-09-10
#define DC_STR_MSGDELMEMBER 18
/// "Group left."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGROUPLEFT 19
/// "GIF"
@@ -5980,9 +6149,7 @@ void dc_event_unref(dc_event_t* event);
/// - %1$s will be replaced by the subject of the displayed message
#define DC_STR_READRCPT_MAILBODY 32
/// "Group image deleted."
///
/// Used in status messages for deleted group images.
/// @deprecated Deprecated, this string is no longer needed.
#define DC_STR_MSGGRPIMGDELETED 33
/// "End-to-end encryption preferred."
@@ -6035,6 +6202,8 @@ void dc_event_unref(dc_event_t* event);
/// - %1$s will be replaced by an action
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
/// - %2$s will be replaced by the name of the user taking that action
///
/// @deprecated 2022-09-10
#define DC_STR_MSGACTIONBYUSER 62
/// "%1$s by me"
@@ -6042,6 +6211,8 @@ void dc_event_unref(dc_event_t* event);
/// Used to concretize actions.
/// - %1$s will be replaced by an action
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
///
/// @deprecated 2022-09-10
#define DC_STR_MSGACTIONBYME 63
/// "Location streaming enabled."
@@ -6105,6 +6276,8 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is disabled."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_DISABLED 75
/// "Message deletion timer is set to %1$s s."
@@ -6112,26 +6285,36 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages when the other constants
/// (#DC_STR_EPHEMERAL_MINUTE, #DC_STR_EPHEMERAL_HOUR and so on) do not match the timer.
/// - %1$s will be replaced by the number of seconds the timer is set to
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_SECONDS 76
/// "Message deletion timer is set to 1 minute."
///
/// Used in status messages.
///
/// @deperecated 2022-09-10
#define DC_STR_EPHEMERAL_MINUTE 77
/// "Message deletion timer is set to 1 hour."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_HOUR 78
/// "Message deletion timer is set to 1 day."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_DAY 79
/// "Message deletion timer is set to 1 week."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_WEEK 80
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
@@ -6172,12 +6355,11 @@ void dc_event_unref(dc_event_t* event);
/// "Chat protection enabled."
///
/// Used in status messages.
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
#define DC_STR_PROTECTION_ENABLED 88
/// "Chat protection disabled."
///
/// Used in status messages.
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
#define DC_STR_PROTECTION_DISABLED 89
/// "Reply"
@@ -6199,29 +6381,37 @@ void dc_event_unref(dc_event_t* event);
/// "Message deletion timer is set to %1$s minutes."
///
/// Used in status messages.
//
/// `%1$s` will be replaced by the number of minutes (alwasy >1) the timer is set to.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_MINUTES and DC_STR_MSG_EPHEMERAL_TIMER_MINUTES_BY.
#define DC_STR_EPHEMERAL_MINUTES 93
/// "Message deletion timer is set to %1$s hours."
///
/// Used in status messages.
//
///
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_HOURS and DC_STR_MSG_EPHEMERAL_TIMER_HOURS_BY.
#define DC_STR_EPHEMERAL_HOURS 94
/// "Message deletion timer is set to %1$s days."
///
/// Used in status messages.
//
///
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_DAYS and DC_STR_MSG_EPHEMERAL_TIMER_DAYS_BY.
#define DC_STR_EPHEMERAL_DAYS 95
/// "Message deletion timer is set to %1$s weeks."
///
/// Used in status messages.
//
///
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_WEEKS and DC_STR_MSG_EPHEMERAL_TIMER_WEEKS_BY.
#define DC_STR_EPHEMERAL_WEEKS 96
/// "Forwarded"
@@ -6378,7 +6568,7 @@ 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"
/// "%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
@@ -6396,6 +6586,246 @@ void dc_event_unref(dc_event_t* event);
/// Used in a device message that explains AEAP.
#define DC_STR_AEAP_EXPLANATION_AND_LINK 123
/// "You changed group name from \"%1$s\" to \"%2$s\"."
///
/// `%1$s` will be replaced by the old group name.
/// `%2$s` will be replaced by the new group name.
#define DC_STR_GROUP_NAME_CHANGED_BY_YOU 124
/// "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."
///
/// `%1$s` will be replaced by the old group name.
/// `%2$s` will be replaced by the new group name.
/// `%3$s` will be replaced by name and address of the contact who did the action.
#define DC_STR_GROUP_NAME_CHANGED_BY_OTHER 125
/// "You changed the group image."
#define DC_STR_GROUP_IMAGE_CHANGED_BY_YOU 126
/// "Group image changed by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact who did the action.
#define DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER 127
/// "You added member %1$s."
///
/// Used in status messages.
#define DC_STR_ADD_MEMBER_BY_YOU 128
/// "Member %1$s added by %2$s."
///
/// `%1$s` will be replaced by name and address of the contact added to the group.
/// `%2$s` will be replaced by name and address of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_ADD_MEMBER_BY_OTHER 129
/// "You removed member %1$s."
///
/// `%1$s` will be replaced by name and address of the contact removed from the group.
///
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_YOU 130
/// "Member %1$s removed by %2$s."
///
/// `%1$s` will be replaced by name and address of the contact removed from the group.
/// `%2$s` will be replaced by name and address of the contact who did the action.
///
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
/// "You left the group."
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_YOU 132
/// "Group left by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_OTHER 133
/// "You deleted the group image."
///
/// Used in status messages.
#define DC_STR_GROUP_IMAGE_DELETED_BY_YOU 134
/// "Group image deleted by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_GROUP_IMAGE_DELETED_BY_OTHER 135
/// "You enabled location streaming."
///
/// Used in status messages.
#define DC_STR_LOCATION_ENABLED_BY_YOU 136
/// "Location streaming enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_LOCATION_ENABLED_BY_OTHER 137
/// "You disabled message deletion timer."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU 138
/// "Message deletion timer is disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER 139
/// "You set message deletion timer to %1$s s."
///
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU 140
/// "Message deletion timer is set to %1$s s by %2$s."
///
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER 141
/// "You set message deletion timer to 1 minute."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU 142
/// "Message deletion timer is set to 1 minute by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER 143
/// "You set message deletion timer to 1 hour."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU 144
/// "Message deletion timer is set to 1 hour by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER 145
/// "You set message deletion timer to 1 day."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU 146
/// "Message deletion timer is set to 1 day by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER 147
/// "You set message deletion timer to 1 week."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU 148
/// "Message deletion timer is set to 1 week by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER 149
/// "You set message deletion timer to %1$s minutes."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU 150
/// "Message deletion timer is set to %1$s minutes by %2$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER 151
/// "You set message deletion timer to %1$s hours."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU 152
/// "Message deletion timer is set to %1$s hours by %2$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER 153
/// "You set message deletion timer to %1$s days."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU 154
/// "Message deletion timer is set to %1$s days by %2$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER 155
/// "You set message deletion timer to %1$s weeks."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU 156
/// "Message deletion timer is set to %1$s weeks by %2$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You enabled chat protection."
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
/// "Chat protection enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
/// "You disabled chat protection."
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
/// "Chat protection disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/**
* @}
*/

View File

@@ -1,5 +1,6 @@
use crate::chat::ChatItem;
use crate::constants::DC_MSG_ID_DAYMARKER;
use crate::contact::ContactId;
use crate::location::Location;
use crate::message::MsgId;
@@ -7,6 +8,7 @@ use crate::message::MsgId;
#[derive(Debug, Clone)]
pub enum dc_array_t {
MsgIds(Vec<MsgId>),
ContactIds(Vec<ContactId>),
Chat(Vec<ChatItem>),
Locations(Vec<Location>),
Uint(Vec<u32>),
@@ -16,6 +18,7 @@ impl dc_array_t {
pub(crate) fn get_id(&self, index: usize) -> u32 {
match self {
Self::MsgIds(array) => array[index].to_u32(),
Self::ContactIds(array) => array[index].to_u32(),
Self::Chat(array) => match array[index] {
ChatItem::Message { msg_id } => msg_id.to_u32(),
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
@@ -28,6 +31,7 @@ impl dc_array_t {
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
match self {
Self::MsgIds(_) => None,
Self::ContactIds(_) => None,
Self::Chat(array) => array.get(index).and_then(|item| match item {
ChatItem::Message { .. } => None,
ChatItem::DayMarker { timestamp } => Some(*timestamp),
@@ -40,6 +44,7 @@ impl dc_array_t {
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
match self {
Self::MsgIds(_) => None,
Self::ContactIds(_) => None,
Self::Chat(_) => None,
Self::Locations(array) => array
.get(index)
@@ -60,6 +65,7 @@ impl dc_array_t {
pub(crate) fn len(&self) -> usize {
match self {
Self::MsgIds(array) => array.len(),
Self::ContactIds(array) => array.len(),
Self::Chat(array) => array.len(),
Self::Locations(array) => array.len(),
Self::Uint(array) => array.len(),
@@ -83,6 +89,12 @@ impl From<Vec<MsgId>> for dc_array_t {
}
}
impl From<Vec<ContactId>> for dc_array_t {
fn from(array: Vec<ContactId>) -> Self {
dc_array_t::ContactIds(array)
}
}
impl From<Vec<ChatItem>> for dc_array_t {
fn from(array: Vec<ChatItem>) -> Self {
dc_array_t::Chat(array)

View File

@@ -19,6 +19,7 @@ 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 _;
@@ -36,7 +37,9 @@ use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -64,6 +67,8 @@ use deltachat::chatlist::Chatlist;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
pub type dc_reactions_t = Reactions;
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
@@ -97,7 +102,12 @@ pub unsafe extern "C" fn dc_context_new(
let ctx = if blobdir.is_null() || *blobdir == 0 {
// generate random ID as this functionality is not yet available on the C-api.
let id = rand::thread_rng().gen();
block_on(Context::new(as_path(dbfile), id, Events::new()))
block_on(Context::new(
as_path(dbfile),
id,
Events::new(),
StockStrings::new(),
))
} else {
eprintln!("blobdir can not be defined explicitly anymore");
return ptr::null_mut();
@@ -121,7 +131,12 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(as_path(dbfile), id, Events::new())) {
match block_on(Context::new_closed(
as_path(dbfile),
id,
Events::new(),
StockStrings::new(),
)) {
Ok(context) => Box::into_raw(Box::new(context)),
Err(err) => {
eprintln!("failed to create context: {:#}", err);
@@ -168,7 +183,7 @@ pub unsafe extern "C" fn dc_context_unref(context: *mut dc_context_t) {
eprintln!("ignoring careless call to dc_context_unref()");
return;
}
Box::from_raw(context);
drop(Box::from_raw(context));
}
#[no_mangle]
@@ -462,7 +477,7 @@ pub unsafe extern "C" fn dc_event_unref(a: *mut dc_event_t) {
return;
}
Box::from_raw(a);
drop(Box::from_raw(a));
}
#[no_mangle]
@@ -486,7 +501,9 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::Error(_) => 400,
EventType::ErrorSelfNotInGroup(_) => 410,
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
@@ -503,6 +520,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ConnectivityChanged => 2100,
EventType::SelfavatarChanged => 2110,
EventType::WebxdcStatusUpdate { .. } => 2120,
EventType::WebxdcInstanceDeleted { .. } => 2121,
}
}
@@ -527,8 +545,10 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::Error(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_) => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
| EventType::MsgsNoticed(chat_id)
| EventType::MsgDelivered { chat_id, .. }
@@ -549,6 +569,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
contact_id.to_u32() as libc::c_int
}
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
}
}
@@ -580,9 +601,12 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ImexFileWritten(_)
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::WebxdcInstanceDeleted { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
| EventType::MsgDelivered { msg_id, .. }
| EventType::MsgFailed { msg_id, .. }
@@ -622,6 +646,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
data2.into_raw()
}
EventType::MsgsChanged { .. }
| EventType::ReactionsChanged { .. }
| EventType::IncomingMsg { .. }
| EventType::MsgsNoticed(_)
| EventType::MsgDelivered { .. }
@@ -636,6 +661,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::WebxdcStatusUpdate { .. }
| EventType::WebxdcInstanceDeleted { .. }
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
@@ -648,6 +674,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
.unwrap_or_default()
.to_c_string()
.unwrap_or_default()
.into_raw(),
}
}
@@ -682,7 +713,7 @@ pub unsafe extern "C" fn dc_event_emitter_unref(emitter: *mut dc_event_emitter_t
return;
}
Box::from_raw(emitter);
drop(Box::from_raw(emitter));
}
#[no_mangle]
@@ -932,6 +963,48 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_reaction(
context: *mut dc_context_t,
msg_id: u32,
reaction: *const libc::c_char,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_reaction()");
return 0;
}
let ctx = &*context;
block_on(async move {
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send reaction")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_reactions(
context: *mut dc_context_t,
msg_id: u32,
) -> *mut dc_reactions_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_msg_reactions()");
return ptr::null_mut();
}
let ctx = &*context;
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
.log_err(ctx, "failed dc_get_msg_reactions() call")
{
reactions
} else {
return ptr::null_mut();
};
Box::into_raw(Box::new(reactions))
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
@@ -1207,6 +1280,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));
@@ -1215,16 +1293,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(),
))
})
}
@@ -2422,7 +2494,7 @@ pub unsafe extern "C" fn dc_array_unref(a: *mut dc_array::dc_array_t) {
return;
}
Box::from_raw(a);
drop(Box::from_raw(a));
}
#[no_mangle]
@@ -2603,7 +2675,7 @@ pub unsafe extern "C" fn dc_chatlist_unref(chatlist: *mut dc_chatlist_t) {
eprintln!("ignoring careless call to dc_chatlist_unref()");
return;
}
Box::from_raw(chatlist);
drop(Box::from_raw(chatlist));
}
#[no_mangle]
@@ -2748,7 +2820,7 @@ pub unsafe extern "C" fn dc_chat_unref(chat: *mut dc_chat_t) {
return;
}
Box::from_raw(chat);
drop(Box::from_raw(chat));
}
#[no_mangle]
@@ -2788,7 +2860,11 @@ pub unsafe extern "C" fn dc_chat_get_mailinglist_addr(chat: *mut dc_chat_t) -> *
return "".strdup();
}
let ffi_chat = &*chat;
ffi_chat.chat.get_mailinglist_addr().strdup()
ffi_chat
.chat
.get_mailinglist_addr()
.unwrap_or_default()
.strdup()
}
#[no_mangle]
@@ -3018,7 +3094,7 @@ pub unsafe extern "C" fn dc_msg_unref(msg: *mut dc_msg_t) {
return;
}
Box::from_raw(msg);
drop(Box::from_raw(msg));
}
#[no_mangle]
@@ -3740,7 +3816,7 @@ pub unsafe extern "C" fn dc_contact_unref(contact: *mut dc_contact_t) {
eprintln!("ignoring careless call to dc_contact_unref()");
return;
}
Box::from_raw(contact);
drop(Box::from_raw(contact));
}
#[no_mangle]
@@ -3859,6 +3935,16 @@ pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) ->
ffi_contact.contact.last_seen()
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_was_seen_recently(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_was_seen_recently()");
return 0;
}
let ffi_contact = &*contact;
ffi_contact.contact.was_seen_recently() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
@@ -3894,7 +3980,7 @@ pub unsafe extern "C" fn dc_lot_unref(lot: *mut dc_lot_t) {
return;
}
Box::from_raw(lot);
drop(Box::from_raw(lot));
}
#[no_mangle]
@@ -3963,6 +4049,45 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
lot.get_timestamp()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_contacts(
reactions: *mut dc_reactions_t,
) -> *mut dc_array::dc_array_t {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
return ptr::null_mut();
}
let reactions = &*reactions;
let array: dc_array_t = reactions.contacts().into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
reactions: *mut dc_reactions_t,
contact_id: u32,
) -> *mut libc::c_char {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
return ptr::null_mut();
}
let reactions = &*reactions;
reactions.get(ContactId::new(contact_id)).as_str().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_unref()");
return;
}
drop(Box::from_raw(reactions));
}
#[no_mangle]
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
@@ -4118,11 +4243,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
@@ -4131,7 +4256,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 }
}
}
@@ -4186,7 +4311,8 @@ pub unsafe extern "C" fn dc_accounts_get_account(
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.get_account(id).await })
block_on(accounts.read())
.get_account(id)
.map(|ctx| Box::into_raw(Box::new(ctx)))
.unwrap_or_else(std::ptr::null_mut)
}
@@ -4201,7 +4327,8 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.get_selected_account().await })
block_on(accounts.read())
.get_selected_account()
.map(|ctx| Box::into_raw(Box::new(ctx)))
.unwrap_or_else(std::ptr::null_mut)
}
@@ -4346,7 +4473,7 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
}
let accounts = &*accounts;
let list = block_on(async move { accounts.read().await.get_all().await });
let list = block_on(accounts.read()).get_all();
let array: dc_array_t = list.into();
Box::into_raw(Box::new(array))
@@ -4406,44 +4533,120 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
block_on(async move { accounts.write().await.maybe_network_lost().await });
}
pub type dc_accounts_event_emitter_t = EventEmitter;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
accounts: *mut dc_accounts_t,
) -> *mut dc_accounts_event_emitter_t {
) -> *mut dc_event_emitter_t {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
return ptr::null_mut();
}
let accounts = &*accounts;
let emitter = block_on(async move { accounts.read().await.get_event_emitter().await });
let emitter = block_on(accounts.read()).get_event_emitter();
Box::into_raw(Box::new(emitter))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_event_emitter_unref(
emitter: *mut dc_accounts_event_emitter_t,
) {
if emitter.is_null() {
eprintln!("ignoring careless call to dc_accounts_event_emitter_unref()");
return;
}
let _ = Box::from_raw(emitter);
}
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use super::*;
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_next_event(
emitter: *mut dc_accounts_event_emitter_t,
) -> *mut dc_event_t {
if emitter.is_null() {
eprintln!("ignoring careless call to dc_accounts_get_next_event()");
return ptr::null_mut();
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
event_thread: JoinHandle<Result<(), anyhow::Error>>,
}
#[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 request_handle2 = request_handle.clone();
let handle = RpcSession::new(request_handle, cmd_api);
let events = block_on({
async {
let am = (*account_manager).inner.clone();
let ev = am.read().await.get_event_emitter();
drop(am);
ev
}
});
let event_thread = spawn({
async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
request_handle2
.send_notification("event", Some(event))
.await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
}
});
let instance = dc_jsonrpc_instance_t {
receiver,
handle,
event_thread,
};
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;
}
(*jsonrpc_instance).event_thread.abort();
drop(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())
}
let emitter = &mut *emitter;
block_on(emitter.recv())
.map(|ev| Box::into_raw(Box::new(ev)))
.unwrap_or_else(ptr::null_mut)
}

View File

@@ -58,6 +58,7 @@ impl Lot {
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
Qr::ReviveVerifyContact { .. } => None,
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
Qr::Login { address, .. } => Some(address),
},
Self::Error(err) => Some(err),
}
@@ -108,6 +109,7 @@ impl Lot {
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
Qr::Login { .. } => LotState::QrLogin,
},
Self::Error(_err) => LotState::QrError,
}
@@ -131,6 +133,7 @@ impl Lot {
Qr::WithdrawVerifyGroup { .. } => Default::default(),
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
Qr::ReviveVerifyGroup { .. } => Default::default(),
Qr::Login { .. } => Default::default(),
},
Self::Error(_) => Default::default(),
}
@@ -195,6 +198,9 @@ pub enum LotState {
/// text1=groupname
QrReviveVerifyGroup = 512,
/// text1=email_address
QrLogin = 520,
// Message States
MsgInFresh = 10,
MsgInNoticed = 13,

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

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

View File

@@ -0,0 +1,40 @@
[package]
name = "deltachat-jsonrpc"
version = "1.99.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.25" }
serde_json = "1.0.87"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.21.2" }
# optional dependencies
axum = { version = "0.5.17", optional = true, features = ["ws"] }
env_logger = { version = "0.9.1", optional = true }
walkdir = "2.3.2"
[dev-dependencies]
tokio = { version = "1.21.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,402 @@
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 id: JSONRPCEventType = event.typ.into();
json!({
"event": id,
"contextId": event.id,
})
}
#[derive(Serialize, TypeDef)]
#[serde(tag = "type", rename = "Event")]
pub enum JSONRPCEventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Info {
msg: String,
},
/// Emitted when SMTP connection is established and login was successful.
SmtpConnected {
msg: String,
},
/// Emitted when IMAP connection is established and login was successful.
ImapConnected {
msg: String,
},
/// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent {
msg: String,
},
/// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted {
msg: String,
},
/// Emitted when an IMAP message has been moved
ImapMessageMoved {
msg: String,
},
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile {
file: String,
},
/// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile {
file: String,
},
/// The library-user should write a warning string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Warning {
msg: String,
},
/// The library-user should report an error to the end-user.
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
///
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
Error {
msg: String,
},
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// setChatName(), setChatProfileImage(),
/// addContactToChat(), removeContactFromChat(),
/// and messages sending functions.
ErrorSelfNotInGroup {
msg: String,
},
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
#[serde(rename_all = "camelCase")]
MsgsChanged {
chat_id: u32,
msg_id: u32,
},
/// Reactions for the message changed.
#[serde(rename_all = "camelCase")]
ReactionsChanged {
chat_id: u32,
msg_id: u32,
contact_id: u32,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
#[serde(rename_all = "camelCase")]
IncomingMsg {
chat_id: u32,
msg_id: u32,
},
/// Downloading a bunch of messages just finished. This is an experimental
/// event to allow the UI to only show one notification per message bunch,
/// instead of cluttering the user with many notifications.
///
/// msg_ids contains the message ids.
#[serde(rename_all = "camelCase")]
IncomingMsgBunch {
msg_ids: Vec<u32>,
},
/// Messages were seen or noticed.
/// chat id is always set.
#[serde(rename_all = "camelCase")]
MsgsNoticed {
chat_id: u32,
},
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgDelivered {
chat_id: u32,
msg_id: u32,
},
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgFailed {
chat_id: u32,
msg_id: u32,
},
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgRead {
chat_id: u32,
msg_id: u32,
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See setChatName(), setChatProfileImage(), addContactToChat()
/// and removeContactFromChat().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
#[serde(rename_all = "camelCase")]
ChatModified {
chat_id: u32,
},
/// Chat ephemeral timer changed.
#[serde(rename_all = "camelCase")]
ChatEphemeralTimerModified {
chat_id: u32,
timer: u32,
},
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
#[serde(rename_all = "camelCase")]
ContactsChanged {
contact_id: Option<u32>,
},
/// Location of one or more contact has changed.
///
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// this parameter is set to `None`.
#[serde(rename_all = "camelCase")]
LocationChanged {
contact_id: Option<u32>,
},
/// Inform about the configuration progress started by configure().
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
/// Inform about the import/export progress started by imex().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
#[serde(rename_all = "camelCase")]
ImexProgress {
progress: usize,
},
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data2 0
#[serde(rename_all = "camelCase")]
ImexFileWritten {
path: String,
},
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by getChatSecurejoinQrCodeSvg().
///
/// @param data1 (int) ID of the contact that wants to join.
/// @param data2 (int) Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
#[serde(rename_all = "camelCase")]
SecurejoinInviterProgress {
contact_id: u32,
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
/// The events are typically sent while secureJoin(), which
/// may take some time, is executed.
/// @param data1 (int) ID of the inviting contact.
/// @param data2 (int) Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
#[serde(rename_all = "camelCase")]
SecurejoinJoinerProgress {
contact_id: u32,
progress: usize,
},
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
/// and possibly the connectivtiy HTML; see getConnectivity() and
/// getConnectivityHtml() for details.
ConnectivityChanged,
SelfavatarChanged,
#[serde(rename_all = "camelCase")]
WebxdcStatusUpdate {
msg_id: u32,
status_update_serial: u32,
},
/// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted {
msg_id: u32,
},
}
impl From<EventType> for JSONRPCEventType {
fn from(event: EventType) -> Self {
use JSONRPCEventType::*;
match event {
EventType::Info(msg) => Info { msg },
EventType::SmtpConnected(msg) => SmtpConnected { msg },
EventType::ImapConnected(msg) => ImapConnected { msg },
EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
EventType::NewBlobFile(file) => NewBlobFile { file },
EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
EventType::Warning(msg) => Warning { msg },
EventType::Error(msg) => Error { msg },
EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => ReactionsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},
EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::MsgRead { chat_id, msg_id } => MsgRead {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::ChatModified(chat_id) => ChatModified {
chat_id: chat_id.to_u32(),
},
EventType::ChatEphemeralTimerModified { chat_id, timer } => {
ChatEphemeralTimerModified {
chat_id: chat_id.to_u32(),
timer: timer.to_u32(),
}
}
EventType::ContactsChanged(contact) => ContactsChanged {
contact_id: contact.map(|c| c.to_u32()),
},
EventType::LocationChanged(contact) => LocationChanged {
contact_id: contact.map(|c| c.to_u32()),
},
EventType::ConfigureProgress { progress, comment } => {
ConfigureProgress { progress, comment }
}
EventType::ImexProgress(progress) => ImexProgress { progress },
EventType::ImexFileWritten(path) => ImexFileWritten {
path: path.to_str().unwrap_or_default().to_owned(),
},
EventType::SecurejoinInviterProgress {
contact_id,
progress,
} => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(),
progress,
},
EventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => SecurejoinJoinerProgress {
contact_id: contact_id.to_u32(),
progress,
},
EventType::ConnectivityChanged => ConnectivityChanged,
EventType::SelfavatarChanged => SelfavatarChanged,
EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
} => WebxdcStatusUpdate {
msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(),
},
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
}
}
}
#[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::<_, JSONRPCEventType>(&mut buf, options)
.unwrap();
String::from_utf8(buf).unwrap()
};
std::fs::write("typescript/generated/events.ts", events).unwrap();
}

File diff suppressed because it is too large Load Diff

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,213 @@
use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Result};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};
use deltachat::context::Context;
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, 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,
was_seen_recently: bool,
mailing_list_address: Option<String>,
}
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?;
let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.get(0) {
Some(contact) => Contact::load_from_db(context, *contact)
.await?
.was_seen_recently(),
None => false,
}
} else {
false
};
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
Ok(FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == 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,
was_seen_recently,
mailing_list_address,
})
}
}
/// cheaper version of fullchat, omits:
/// - contacts
/// - contact_ids
/// - fresh_message_counter
/// - ephemeral_timer
/// - self_in_group
/// - was_seen_recently
/// - can_send
///
/// used when you only need the basic metadata of a chat like type, name, profile picture
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct BasicChat {
id: u32,
name: String,
is_protected: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
chat_type: u32,
is_unpromoted: bool,
is_self_talk: bool,
color: String,
is_contact_request: bool,
is_device_chat: bool,
is_muted: bool,
}
impl BasicChat {
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 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?);
Ok(BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == 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(),
color,
is_contact_request: chat.is_contact_request(),
is_device_chat: chat.is_device_talk(),
is_muted: chat.is_muted(),
})
}
}
#[derive(Clone, Serialize, Deserialize, TypeDef)]
pub enum MuteDuration {
NotMuted,
Forever,
Until(i64),
}
impl MuteDuration {
pub fn try_into_core_type(self) -> Result<chat::MuteDuration> {
match self {
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
MuteDuration::Until(n) => {
if n <= 0 {
bail!("failed to read mute duration")
}
Ok(SystemTime::now()
.checked_add(Duration::from_secs(n as u64))
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
}
}
}
}
#[derive(Clone, Serialize, Deserialize, TypeDef)]
#[serde(rename = "ChatVisibility")]
pub enum JSONRPCChatVisibility {
Normal,
Archived,
Pinned,
}
impl JSONRPCChatVisibility {
pub fn into_core_type(self) -> ChatVisibility {
match self {
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
}
}
}

View File

@@ -0,0 +1,141 @@
use anyhow::Result;
use deltachat::constants::*;
use deltachat::contact::{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,
/// true when chat is a broadcastlist
is_broadcast: bool,
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
was_seen_recently: bool,
},
ArchiveLink,
#[serde(rename_all = "camelCase")]
Error {
id: u32,
error: String,
},
}
pub(crate) async fn get_chat_list_item_by_id(
ctx: &deltachat::context::Context,
entry: &ChatListEntry,
) -> Result<ChatListItemFetchResult> {
let chat_id = ChatId::new(entry.0);
let last_msgid = match entry.1 {
0 => None,
_ => Some(MsgId::new(entry.1)),
};
if chat_id.is_archived_link() {
return Ok(ChatListItemFetchResult::ArchiveLink);
}
let chat = Chat::load_from_db(ctx, chat_id).await?;
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
let summary_text2 = summary.text.to_owned();
let visibility = chat.get_visibility();
let avatar_path = chat
.get_profile_image(ctx)
.await?
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
let last_updated = match last_msgid {
Some(id) => {
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
Some(last_message.get_timestamp() * 1000)
}
None => None,
};
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
let self_in_group = chat_contacts.contains(&ContactId::SELF);
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
let contact = chat_contacts.get(0);
let was_seen_recently = match contact {
Some(contact) => Contact::load_from_db(ctx, *contact)
.await?
.was_seen_recently(),
None => false,
};
(
contact.map(|contact_id| contact_id.to_u32()),
was_seen_recently,
)
} else {
(None, false)
};
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(),
is_broadcast: chat.get_type() == Chattype::Broadcast,
dm_chat_contact,
was_seen_recently,
})
}

View File

@@ -0,0 +1,55 @@
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,
/// the contact's last seen timestamp
last_seen: i64,
was_seen_recently: 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,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
})
}
}

View File

@@ -0,0 +1,47 @@
use deltachat::location::Location;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Location", rename_all = "camelCase")]
pub struct JsonrpcLocation {
pub location_id: u32,
pub is_independent: bool,
pub latitude: f64,
pub longitude: f64,
pub accuracy: f64,
pub timestamp: i64,
pub contact_id: u32,
pub msg_id: u32,
pub chat_id: u32,
pub marker: Option<String>,
}
impl From<Location> for JsonrpcLocation {
fn from(location: Location) -> Self {
let Location {
location_id,
independent,
latitude,
longitude,
accuracy,
timestamp,
contact_id,
msg_id,
chat_id,
marker,
} = location;
Self {
location_id,
is_independent: independent != 0,
latitude,
longitude,
accuracy,
timestamp,
contact_id: contact_id.to_u32(),
msg_id,
chat_id: chat_id.to_u32(),
marker,
}
}
}

View File

@@ -0,0 +1,487 @@
use anyhow::{anyhow, Result};
use deltachat::chat::Chat;
use deltachat::chat::ChatItem;
use deltachat::constants::Chattype;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::download;
use deltachat::message::Message;
use deltachat::message::MsgId;
use deltachat::message::Viewtype;
use deltachat::reaction::get_msg_reactions;
use num_traits::cast::ToPrimitive;
use serde::Deserialize;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef)]
#[serde(rename = "Message", rename_all = "camelCase")]
pub struct MessageObject {
id: u32,
chat_id: u32,
from_id: u32,
quote: Option<MessageQuote>,
parent_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,
/// when is_info is true this describes what type of system message it is
system_message_type: SystemMessageType,
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>,
webxdc_info: Option<WebxdcMessageInfo>,
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
}
#[derive(Serialize, TypeDef)]
#[serde(tag = "kind")]
enum MessageQuote {
JustText {
text: String,
},
#[serde(rename_all = "camelCase")]
WithMessage {
text: String,
message_id: u32,
author_display_name: String,
author_display_color: String,
override_sender_name: Option<String>,
image: Option<String>,
is_forwarded: bool,
view_type: MessageViewtype,
},
}
impl MessageObject {
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
let msg_id = MsgId::new(message_id);
Self::from_msg_id(context, msg_id).await
}
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
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();
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
} else {
None
};
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
let download_state = message.download_state().into();
let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? {
Some(quote) => {
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
Some(MessageQuote::WithMessage {
text: quoted_text,
message_id: quote.get_id().to_u32(),
author_display_name: quote_author.get_display_name().to_owned(),
author_display_color: color_int_to_hex_string(quote_author.get_color()),
override_sender_name: quote.get_override_sender_name(),
image: if quote.get_viewtype() == Viewtype::Image
|| quote.get_viewtype() == Viewtype::Gif
{
match quote.get_file(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
}
} else {
None
},
is_forwarded: quote.is_forwarded(),
view_type: quote.get_viewtype().into(),
})
}
None => Some(MessageQuote::JustText { text: quoted_text }),
}
} else {
None
};
let reactions = get_msg_reactions(context, msg_id).await?;
let reactions = if reactions.is_empty() {
None
} else {
Some(reactions.into())
};
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
quote,
parent_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(),
system_message_type: message.get_info_type().into(),
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(),
webxdc_info,
download_state,
reactions,
})
}
}
#[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,
}
}
}
#[derive(Serialize, TypeDef)]
pub enum DownloadState {
Done,
Available,
Failure,
InProgress,
}
impl From<download::DownloadState> for DownloadState {
fn from(state: download::DownloadState) -> Self {
match state {
download::DownloadState::Done => DownloadState::Done,
download::DownloadState::Available => DownloadState::Available,
download::DownloadState::Failure => DownloadState::Failure,
download::DownloadState::InProgress => DownloadState::InProgress,
}
}
}
#[derive(Serialize, TypeDef)]
pub enum SystemMessageType {
Unknown,
GroupNameChanged,
GroupImageChanged,
MemberAddedToGroup,
MemberRemovedFromGroup,
AutocryptSetupMessage,
SecurejoinMessage,
LocationStreamingEnabled,
LocationOnly,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
// Chat protection state changed
ChatProtectionEnabled,
ChatProtectionDisabled,
/// Self-sent-message that contains only json used for multi-device-sync;
/// if possible, we attach that to other messages as for locations.
MultiDeviceSync,
// Sync message that contains a json payload
// sent to the other webxdc instances
// These messages are not shown in the chat.
WebxdcStatusUpdate,
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
WebxdcInfoMessage,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
fn from(system_message_type: deltachat::mimeparser::SystemMessage) -> Self {
use deltachat::mimeparser::SystemMessage;
match system_message_type {
SystemMessage::Unknown => SystemMessageType::Unknown,
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
SystemMessage::AutocryptSetupMessage => SystemMessageType::AutocryptSetupMessage,
SystemMessage::SecurejoinMessage => SystemMessageType::SecurejoinMessage,
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
}
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageNotificationInfo {
id: u32,
chat_id: u32,
account_id: u32,
image: Option<String>,
image_mime_type: Option<String>,
chat_name: String,
chat_profile_image: Option<String>,
/// also known as summary_text1
summary_prefix: Option<String>,
/// also known as summary_text2
summary_text: String,
}
impl MessageNotificationInfo {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
let image = if matches!(
message.get_viewtype(),
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
) {
message
.get_file(context)
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
.unwrap_or_default()
} else {
None
};
let chat_profile_image = chat
.get_profile_image(context)
.await?
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
.unwrap_or_default();
let summary = message.get_summary(context, Some(&chat)).await?;
Ok(MessageNotificationInfo {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
account_id: context.get_id(),
image,
image_mime_type: message.get_filemime(),
chat_name: chat.name,
chat_profile_image,
summary_prefix: summary.prefix.map(|s| s.to_string()),
summary_text: summary.text,
})
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageSearchResult {
id: u32,
author_profile_image: Option<String>,
author_name: String,
author_color: String,
chat_name: Option<String>,
message: String,
timestamp: i64,
}
impl MessageSearchResult {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
let profile_image = match sender.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
Ok(Self {
id: msg_id.to_u32(),
author_profile_image: profile_image,
author_name: sender.get_display_name().to_owned(),
author_color: color_int_to_hex_string(sender.get_color()),
chat_name: if chat.get_type() == Chattype::Single {
Some(chat.get_name().to_owned())
} else {
None
},
message: message.get_text().unwrap_or_default(),
timestamp: message.get_timestamp(),
})
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
pub enum JSONRPCMessageListItem {
Message {
msg_id: u32,
},
/// Day marker, separating messages that correspond to different
/// days according to local time.
DayMarker {
/// Marker timestamp, for day markers, in unix milliseconds
timestamp: i64,
},
}
impl From<ChatItem> for JSONRPCMessageListItem {
fn from(item: ChatItem) -> Self {
match item {
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
msg_id: msg_id.to_u32(),
},
ChatItem::DayMarker { timestamp } => JSONRPCMessageListItem::DayMarker { timestamp },
}
}
}

View File

@@ -0,0 +1,22 @@
pub mod account;
pub mod chat;
pub mod chat_list;
pub mod contact;
pub mod location;
pub mod message;
pub mod provider_info;
pub mod qr;
pub mod reactions;
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)
}
}

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,213 @@
use deltachat::qr::Qr;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[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,
},
Login {
address: 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,
}
}
Qr::Login { address, .. } => QrObject::Login { address },
}
}
}

View File

@@ -0,0 +1,47 @@
use std::collections::BTreeMap;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
/// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef)]
#[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count
reactions: BTreeMap<String, u32>,
}
impl From<Reactions> for JSONRPCReactions {
fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id);
if reaction.is_empty() {
continue;
}
let emojis: Vec<String> = reaction
.emojis()
.into_iter()
.map(|emoji| emoji.to_owned())
.collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
for emoji in emojis {
if let Some(x) = unique_reactions.get_mut(&emoji) {
*x += 1;
} else {
unique_reactions.insert(emoji, 1);
}
}
}
JSONRPCReactions {
reactions_by_contact,
reactions: unique_reactions,
}
}
}

View File

@@ -0,0 +1,64 @@
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>,
/// True if full internet access should be granted to the app.
internet_access: bool,
}
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,
internet_access,
} = 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),
internet_access,
})
}
}

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();
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,110 @@
import { DcEvent, DeltaChat } 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", (accountId, event) => {
onIncomingEvent(accountId, 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.getFullChatById(
selectedAccount,
chatId
);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.getMessageIds(
selectedAccount,
chatId,
0
);
const messages = await client.rpc.getMessages(
selectedAccount,
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
write($main, `<p>${message.text}</p>`);
}
}
}
function onIncomingEvent(accountId: number, event: DcEvent) {
write(
$side,
`
<p class="message">
[<strong>${event.type}</strong> on account ${accountId}]<br>
<em>f1:</em> ${JSON.stringify(
Object.assign({}, event, { type: undefined })
)}
</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,941 @@
// 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)[]>;
}
public startIoForAllAccounts(): Promise<null> {
return (this._transport.request('start_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
}
public stopIoForAllAccounts(): Promise<null> {
return (this._transport.request('stop_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
}
public startIo(id: T.U32): Promise<null> {
return (this._transport.request('start_io', [id] as RPC.Params)) as Promise<null>;
}
public stopIo(id: T.U32): Promise<null> {
return (this._transport.request('stop_io', [id] as RPC.Params)) as Promise<null>;
}
/**
* 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>;
}
/**
* Get the combined filesize of an account in bytes
*/
public getAccountFileSize(accountId: T.U32): Promise<T.U64> {
return (this._transport.request('get_account_file_size', [accountId] as RPC.Params)) as Promise<T.U64>;
}
/**
* 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, `checkQr()` should confirm the type of the
* QR code is `account` or `webrtcInstance`.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
*/
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)>>;
}
public setStockStrings(strings: Record<T.U32,string>): Promise<null> {
return (this._transport.request('set_stock_strings', [strings] as RPC.Params)) as Promise<null>;
}
/**
* Configures this account with the currently set parameters.
* Setup the credential config before calling this.
*/
public configure(accountId: T.U32): Promise<null> {
return (this._transport.request('configure', [accountId] as RPC.Params)) as Promise<null>;
}
/**
* Signal an ongoing process to stop.
*/
public stopOngoingProcess(accountId: T.U32): Promise<null> {
return (this._transport.request('stop_ongoing_process', [accountId] as RPC.Params)) as Promise<null>;
}
public exportSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('export_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
}
public importSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('import_self_keys', [accountId, path, passphrase] 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>;
}
/**
* Estimate the number of messages that will be deleted
* by the set_config()-options `delete_device_after` or `delete_server_after`.
* This is typically used to show the estimated impact to the user
* before actually enabling deletion of old messages.
*/
public estimateAutoDeletionCount(accountId: T.U32, fromServer: boolean, seconds: T.I64): Promise<T.Usize> {
return (this._transport.request('estimate_auto_deletion_count', [accountId, fromServer, seconds] as RPC.Params)) as Promise<T.Usize>;
}
public initiateAutocryptKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('initiate_autocrypt_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
}
public continueAutocryptKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
return (this._transport.request('continue_autocrypt_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 getFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
return (this._transport.request('get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
}
/**
* get basic info about a chat,
* use chatlist_get_full_chat_by_id() instead if you need more information
*/
public getBasicChatInfo(accountId: T.U32, chatId: T.U32): Promise<T.BasicChat> {
return (this._transport.request('get_basic_chat_info', [accountId, chatId] as RPC.Params)) as Promise<T.BasicChat>;
}
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>;
}
/**
* Delete a chat.
*
* Messages are deleted from the device and the chat database entry is deleted.
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
*
* Things that are _not done_ implicitly:
*
* - Messages are **not deleted from the server**.
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
* and the user may create the chat again.
* - **Groups are not left** - this would
* be unexpected as (1) deleting a normal chat also does not prevent new mails
* from arriving, (2) leaving a group requires sending a message to
* all group members - especially for groups not used for a longer time, this is
* really unexpected when deletion results in contacting all members again,
* (3) only leaving groups is also a valid usecase.
*
* To leave a chat explicitly, use leave_group()
*/
public deleteChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('delete_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Get encryption info for a chat.
* Get a multi-line encryption info, containing encryption preferences of all members.
* Can be used to find out why messages sent to group are not encrypted.
*
* returns Multi-line text
*/
public getChatEncryptionInfo(accountId: T.U32, chatId: T.U32): Promise<string> {
return (this._transport.request('get_chat_encryption_info', [accountId, chatId] as RPC.Params)) as Promise<string>;
}
/**
* Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to `checkQr()` then;
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
* an out-of-band-verification can be joined using `secure_join()`
*
* chat_id: If set to a group-chat-id,
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If not set, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* return format: `[code, svg]`
*/
public getChatSecurejoinQrCodeSvg(accountId: T.U32, chatId: (T.U32|null)): Promise<[string,string]> {
return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>;
}
/**
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with `get_chat_securejoin_qr_code_svg()`.
* This function is typically called when `check_qr()` returns
* type=AskVerifyContact or type=AskVerifyGroup.
*
* The function returns immediately and the handshake runs in background,
* sending and receiving several messages.
* During the handshake, info messages are added to the chat,
* showing progress, success or errors.
*
* Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* **qr**: The text of the scanned QR code. Typically, the same string as given
* to `check_qr()`.
*
* **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
*
*/
public secureJoin(accountId: T.U32, qr: string): Promise<T.U32> {
return (this._transport.request('secure_join', [accountId, qr] as RPC.Params)) as Promise<T.U32>;
}
public leaveGroup(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('leave_group', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Remove a member from a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public removeContactFromChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('remove_contact_from_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
}
/**
* Add a member to a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* If the group has group protection enabled, only verified contacts can be added to the group.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public addContactToChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('add_contact_to_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
}
/**
* Get the contact IDs belonging to a chat.
*
* - for normal chats, the function always returns exactly one contact,
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
*
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
* explicitly as it may happen that oneself gets removed from a still existing
* group
*
* - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
*
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
* for now, the UI should not show the list for mailing lists.
* (we do not know all members and there is not always a global mailing list address,
* so we could return only SELF or the known members; this is not decided yet)
*/
public getChatContacts(accountId: T.U32, chatId: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_chat_contacts', [accountId, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Create a new group chat.
*
* After creation,
* the group has one member with the ID DC_CONTACT_ID_SELF
* and is in _unpromoted_ state.
* This means, you can add or remove members, change the name,
* the group image and so on without messages being sent to all group members.
*
* This changes as soon as the first message is sent to the group members
* and the group becomes _promoted_.
* After that, all changes are synced with all group members
* by sending status message.
*
* To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
* This may be useful if you want to show some help for just created groups.
*
* @param protect If set to 1 the function creates group with protection initially enabled.
* Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled.
*/
public createGroupChat(accountId: T.U32, name: string, protect: boolean): Promise<T.U32> {
return (this._transport.request('create_group_chat', [accountId, name, protect] as RPC.Params)) as Promise<T.U32>;
}
/**
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
*
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in the broadcast list
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
*/
public createBroadcastList(accountId: T.U32): Promise<T.U32> {
return (this._transport.request('create_broadcast_list', [accountId] as RPC.Params)) as Promise<T.U32>;
}
/**
* Set group name.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public setChatName(accountId: T.U32, chatId: T.U32, newName: string): Promise<null> {
return (this._transport.request('set_chat_name', [accountId, chatId, newName] as RPC.Params)) as Promise<null>;
}
/**
* Set group profile image.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* To find out the profile image of a chat, use dc_chat_get_profile_image()
*
* @param image_path Full path of the image to use as the group image. The image will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* If you pass null here, the group image is deleted (for promoted groups, all members are informed about
* this change anyway).
*/
public setChatProfileImage(accountId: T.U32, chatId: T.U32, imagePath: (string|null)): Promise<null> {
return (this._transport.request('set_chat_profile_image', [accountId, chatId, imagePath] as RPC.Params)) as Promise<null>;
}
public setChatVisibility(accountId: T.U32, chatId: T.U32, visibility: T.ChatVisibility): Promise<null> {
return (this._transport.request('set_chat_visibility', [accountId, chatId, visibility] as RPC.Params)) as Promise<null>;
}
public setChatEphemeralTimer(accountId: T.U32, chatId: T.U32, timer: T.U32): Promise<null> {
return (this._transport.request('set_chat_ephemeral_timer', [accountId, chatId, timer] as RPC.Params)) as Promise<null>;
}
public getChatEphemeralTimer(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('get_chat_ephemeral_timer', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
}
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>;
}
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
* but are still waiting for being marked as "seen" using markseen_msgs()
* (IMAP/MDNs is not done for noticed messages).
*
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also markseen_msgs().
*/
public marknoticedChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('marknoticed_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public getFirstUnreadMessageOfChat(accountId: T.U32, chatId: T.U32): Promise<(T.U32|null)> {
return (this._transport.request('get_first_unread_message_of_chat', [accountId, chatId] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Set mute duration of a chat.
*
* The UI can then call is_chat_muted() when receiving a new message
* to decide whether it should trigger an notification.
*
* Muted chats should not sound or vibrate
* and should not show a visual notification in the system area.
* Moreover, muted chats should be excluded from global badge counter
* (get_fresh_msgs() skips muted chats therefore)
* and the in-app, per-chat badge counter should use a less obtrusive color.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*/
public setChatMuteDuration(accountId: T.U32, chatId: T.U32, duration: T.MuteDuration): Promise<null> {
return (this._transport.request('set_chat_mute_duration', [accountId, chatId, duration] as RPC.Params)) as Promise<null>;
}
/**
* Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
*
* This is available as a standalone function outside of fullchat, because it might be only needed for notification
*/
public isChatMuted(accountId: T.U32, chatId: T.U32): Promise<boolean> {
return (this._transport.request('is_chat_muted', [accountId, chatId] as RPC.Params)) as Promise<boolean>;
}
/**
* Mark messages as presented to the user.
* Typically, UIs call this function on scrolling through the message list,
* when the messages are presented at least for a little moment.
* The concrete action depends on the type of the chat and on the users settings
* (dc_msgs_presented() may be a better name therefore, but well. :)
*
* - For normal chats, the IMAP state is updated, MDN is sent
* (if set_config()-options `mdns_enabled` is set)
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
*
* - For contact requests, no IMAP or MDNs is done
* and the internal state is not changed therefore.
* See also marknoticed_chat().
*
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*/
public markseenMsgs(accountId: T.U32, msgIds: (T.U32)[]): Promise<null> {
return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise<null>;
}
public getMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
}
public getMessageListItems(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.MessageListItem)[]> {
return (this._transport.request('get_message_list_items', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.MessageListItem)[]>;
}
public getMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
return (this._transport.request('get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
}
public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> {
return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>;
}
public getMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
/**
* Fetch info desktop needs for creating a notification for a message
*/
public getMessageNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
return (this._transport.request('get_message_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
}
/**
* Delete messages. The messages are deleted on the current device and
* on the IMAP server.
*/
public deleteMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<null> {
return (this._transport.request('delete_messages', [accountId, messageIds] as RPC.Params)) as Promise<null>;
}
/**
* Get an informational text for a single message. The text is multiline and may
* contain e.g. the raw text of the message.
*
* The max. text returned is typically longer (about 100000 characters) than the
* max. text returned by dc_msg_get_text() (about 30000 characters).
*/
public getMessageInfo(accountId: T.U32, messageId: T.U32): Promise<string> {
return (this._transport.request('get_message_info', [accountId, messageId] as RPC.Params)) as Promise<string>;
}
/**
* Asks the core to start downloading a message fully.
* This function is typically called when the user hits the "Download" button
* that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`
*
* On success, the @ref DC_MSG "view type of the message" may change
* or the message may be replaced completely by one or more messages with other message IDs.
* That may happen e.g. in cases where the message was encrypted
* and the type could not be determined without fully downloading.
* Downloaded content can be accessed as usual after download.
*
* To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
*/
public downloadFullMessage(accountId: T.U32, messageId: T.U32): Promise<null> {
return (this._transport.request('download_full_message', [accountId, messageId] as RPC.Params)) as Promise<null>;
}
/**
* Search messages containing the given query string.
* Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
*
* Global chat results are typically displayed using dc_msg_get_summary(), chat
* search results may just hilite the corresponding messages and present a
* prev/next button.
*
* For global search, result is limited to 1000 messages,
* this allows incremental search done fast.
* So, when getting exactly 1000 results, the result may be truncated;
* the UIs may display sth. as "1000+ messages found" in this case.
* Chat search (if a chat_id is set) is not limited.
*/
public searchMessages(accountId: T.U32, query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> {
return (this._transport.request('search_messages', [accountId, query, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
}
public messageIdsToSearchResults(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.MessageSearchResult>> {
return (this._transport.request('message_ids_to_search_results', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.MessageSearchResult>>;
}
/**
* Get a single contact options by ID.
*/
public getContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
return (this._transport.request('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 createContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
return (this._transport.request('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 createChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
return (this._transport.request('create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
}
public blockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('block_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public unblockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('unblock_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public getBlockedContacts(accountId: T.U32): Promise<(T.Contact)[]> {
return (this._transport.request('get_blocked_contacts', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public getContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return (this._transport.request('get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get a list of contacts.
* (formerly called getContacts2 in desktop)
*/
public getContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return (this._transport.request('get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public getContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return (this._transport.request('get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
}
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<boolean> {
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<boolean>;
}
public changeContactName(accountId: T.U32, contactId: T.U32, name: string): Promise<null> {
return (this._transport.request('change_contact_name', [accountId, contactId, name] as RPC.Params)) as Promise<null>;
}
/**
* Get encryption info for a contact.
* Get a multi-line encryption info, containing your fingerprint and the
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
*/
public getContactEncryptionInfo(accountId: T.U32, contactId: T.U32): Promise<string> {
return (this._transport.request('get_contact_encryption_info', [accountId, contactId] as RPC.Params)) as Promise<string>;
}
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* To get a list of all known and unblocked contacts, use contacts_get_contacts().
*
* To validate an e-mail address independently of the contact database
* use check_email_validity().
*/
public lookupContactIdByAddr(accountId: T.U32, addr: string): Promise<(T.U32|null)> {
return (this._transport.request('lookup_contact_id_by_addr', [accountId, addr] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* 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 getChatMedia(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('get_chat_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Search next/previous message based on a given message and a list of types.
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* one combined call for getting chat::get_next_media for both directions
* the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
*/
public getNeighboringChatMedia(accountId: T.U32, msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
return (this._transport.request('get_neighboring_chat_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
}
public exportBackup(accountId: T.U32, destination: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('export_backup', [accountId, destination, passphrase] as RPC.Params)) as Promise<null>;
}
public importBackup(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('import_backup', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
}
/**
* Indicate that the network likely has come back.
* or just that the network conditions might have changed
*/
public maybeNetwork(): Promise<null> {
return (this._transport.request('maybe_network', [] as RPC.Params)) as Promise<null>;
}
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
*
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*/
public getConnectivity(accountId: T.U32): Promise<T.U32> {
return (this._transport.request('get_connectivity', [accountId] as RPC.Params)) as Promise<T.U32>;
}
/**
* Get an overview of the current connectivity, and possibly more statistics.
* Meant to give the user more insight about the current status than
* the basic connectivity info returned by get_connectivity(); show this
* e.g., if the user taps on said basic connectivity info.
*
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* This comes as an HTML from the core so that we can easily improve it
* and the improvement instantly reaches all UIs.
*/
public getConnectivityHtml(accountId: T.U32): Promise<string> {
return (this._transport.request('get_connectivity_html', [accountId] as RPC.Params)) as Promise<string>;
}
public getLocations(accountId: T.U32, chatId: (T.U32|null), contactId: (T.U32|null), timestampBegin: T.I64, timestampEnd: T.I64): Promise<(T.Location)[]> {
return (this._transport.request('get_locations', [accountId, chatId, contactId, timestampBegin, timestampEnd] as RPC.Params)) as Promise<(T.Location)[]>;
}
public sendWebxdcStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('send_webxdc_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
}
public getWebxdcStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return (this._transport.request('get_webxdc_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
}
/**
* Get info from a webxdc message
*/
public getWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return (this._transport.request('get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
}
/**
* Forward messages to another chat.
*
* All types of messages can be forwarded,
* however, they will be flagged as such (dc_msg_is_forwarded() is set).
*
* Original sender, info-state and webxdc updates are not forwarded on purpose.
*/
public forwardMessages(accountId: T.U32, messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
return (this._transport.request('forward_messages', [accountId, messageIds, chatId] as RPC.Params)) as Promise<null>;
}
public sendSticker(accountId: T.U32, chatId: T.U32, stickerPath: string): Promise<T.U32> {
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
}
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*/
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
}
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Get draft for a chat, if any.
*/
public getDraft(accountId: T.U32, chatId: T.U32): Promise<(T.Message|null)> {
return (this._transport.request('get_draft', [accountId, chatId] as RPC.Params)) as Promise<(T.Message|null)>;
}
public sendVideochatInvitation(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
}
public miscGetStickerFolder(accountId: T.U32): Promise<string> {
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
}
/**
* for desktop, get stickers from stickers folder,
* grouped by the folder they are in.
*/
public miscGetStickers(accountId: T.U32): Promise<Record<string,(string)[]>> {
return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise<Record<string,(string)[]>>;
}
/**
* Returns the messageid of the sent message
*/
public miscSendTextMessage(accountId: T.U32, chatId: T.U32, text: string): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, chatId, text] as RPC.Params)) as Promise<T.U32>;
}
public miscSendMsg(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> {
return (this._transport.request('misc_send_msg', [accountId, chatId, text, file, location, quotedMessageId] as RPC.Params)) as Promise<[T.U32,T.Message]>;
}
public miscSetDraft(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
return (this._transport.request('misc_set_draft', [accountId, chatId, text, file, quotedMessageId] as RPC.Params)) as Promise<null>;
}
}

View File

@@ -0,0 +1,199 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
DC_CHAT_ID_ALLDONE_HINT = 7,
DC_CHAT_ID_ARCHIVED_LINK = 6,
DC_CHAT_ID_LAST_SPECIAL = 9,
DC_CHAT_ID_TRASH = 3,
DC_CHAT_TYPE_BROADCAST = 160,
DC_CHAT_TYPE_GROUP = 120,
DC_CHAT_TYPE_MAILINGLIST = 140,
DC_CHAT_TYPE_SINGLE = 100,
DC_CHAT_TYPE_UNDEFINED = 0,
DC_CONNECTIVITY_CONNECTED = 4000,
DC_CONNECTIVITY_CONNECTING = 2000,
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
DC_CONNECTIVITY_WORKING = 3000,
DC_CONTACT_ID_DEVICE = 5,
DC_CONTACT_ID_INFO = 2,
DC_CONTACT_ID_LAST_SPECIAL = 9,
DC_CONTACT_ID_SELF = 1,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
DC_GCL_ARCHIVED_ONLY = 1,
DC_GCL_FOR_FORWARDING = 8,
DC_GCL_NO_SPECIALS = 2,
DC_GCL_VERIFIED_ONLY = 1,
DC_GCM_ADDDAYMARKER = 1,
DC_GCM_INFO_ONLY = 2,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
DC_MEDIA_QUALITY_WORSE = 1,
DC_MSG_ID_DAYMARKER = 9,
DC_MSG_ID_LAST_SPECIAL = 9,
DC_MSG_ID_MARKER1 = 1,
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
DC_SHOW_EMAILS_ALL = 2,
DC_SHOW_EMAILS_OFF = 0,
DC_SOCKET_AUTO = 0,
DC_SOCKET_PLAIN = 3,
DC_SOCKET_SSL = 1,
DC_SOCKET_STARTTLS = 2,
DC_STATE_IN_FRESH = 10,
DC_STATE_IN_NOTICED = 13,
DC_STATE_IN_SEEN = 16,
DC_STATE_OUT_DELIVERED = 26,
DC_STATE_OUT_DRAFT = 19,
DC_STATE_OUT_FAILED = 24,
DC_STATE_OUT_MDN_RCVD = 28,
DC_STATE_OUT_PENDING = 20,
DC_STATE_OUT_PREPARING = 18,
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ADD_MEMBER_BY_OTHER = 129,
DC_STR_ADD_MEMBER_BY_YOU = 128,
DC_STR_AEAP_ADDR_CHANGED = 122,
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
DC_STR_DEVICE_MESSAGES = 68,
DC_STR_DEVICE_MESSAGES_HINT = 70,
DC_STR_DOWNLOAD_AVAILABILITY = 100,
DC_STR_DRAFT = 3,
DC_STR_E2E_AVAILABLE = 25,
DC_STR_E2E_PREFERRED = 34,
DC_STR_ENCRYPTEDMSG = 24,
DC_STR_ENCR_NONE = 28,
DC_STR_ENCR_TRANSP = 27,
DC_STR_EPHEMERAL_DAY = 79,
DC_STR_EPHEMERAL_DAYS = 95,
DC_STR_EPHEMERAL_DISABLED = 75,
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
DC_STR_EPHEMERAL_HOUR = 78,
DC_STR_EPHEMERAL_HOURS = 94,
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
DC_STR_ERROR_NO_NETWORK = 87,
DC_STR_FAILED_SENDING_TO = 74,
DC_STR_FILE = 12,
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
DC_STR_GROUP_LEFT_BY_OTHER = 133,
DC_STR_GROUP_LEFT_BY_YOU = 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
DC_STR_MSGDELMEMBER = 18,
DC_STR_MSGGROUPLEFT = 19,
DC_STR_MSGGRPIMGCHANGED = 16,
DC_STR_MSGGRPIMGDELETED = 33,
DC_STR_MSGGRPNAME = 15,
DC_STR_MSGLOCATIONDISABLED = 65,
DC_STR_MSGLOCATIONENABLED = 64,
DC_STR_NOMESSAGES = 1,
DC_STR_NOT_CONNECTED = 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
DC_STR_ONE_MOMENT = 106,
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
DC_STR_SELF = 2,
DC_STR_SELF_DELETED_MSG_BODY = 91,
DC_STR_SENDING = 110,
DC_STR_SERVER_TURNED_OFF = 92,
DC_STR_SETUP_CONTACT_QR_DESC = 119,
DC_STR_STICKER = 67,
DC_STR_STORAGE_ON_DOMAIN = 105,
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
DC_STR_SYNC_MSG_BODY = 102,
DC_STR_SYNC_MSG_SUBJECT = 101,
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
DC_STR_UPDATING = 109,
DC_STR_VIDEO = 10,
DC_STR_VIDEOCHAT_INVITATION = 82,
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
DC_VIDEOCHATTYPE_JITSI = 2,
DC_VIDEOCHATTYPE_UNKNOWN = 0,
}

View File

@@ -0,0 +1,214 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Usize=number;
export type Event=(({
/**
* The library-user may write an informational string to the log.
*
* This event should *not* be reported to the end-user using a popup or something like
* that.
*/
"type":"Info";}&{"msg":string;})|({
/**
* Emitted when SMTP connection is established and login was successful.
*/
"type":"SmtpConnected";}&{"msg":string;})|({
/**
* Emitted when IMAP connection is established and login was successful.
*/
"type":"ImapConnected";}&{"msg":string;})|({
/**
* Emitted when a message was successfully sent to the SMTP server.
*/
"type":"SmtpMessageSent";}&{"msg":string;})|({
/**
* Emitted when an IMAP message has been marked as deleted
*/
"type":"ImapMessageDeleted";}&{"msg":string;})|({
/**
* Emitted when an IMAP message has been moved
*/
"type":"ImapMessageMoved";}&{"msg":string;})|({
/**
* Emitted when an new file in the $BLOBDIR was created
*/
"type":"NewBlobFile";}&{"file":string;})|({
/**
* Emitted when an file in the $BLOBDIR was deleted
*/
"type":"DeletedBlobFile";}&{"file":string;})|({
/**
* The library-user should write a warning string to the log.
*
* This event should *not* be reported to the end-user using a popup or something like
* that.
*/
"type":"Warning";}&{"msg":string;})|({
/**
* The library-user should report an error to the end-user.
*
* As most things are asynchronous, things may go wrong at any time and the user
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
*
* However, for ongoing processes (eg. configure())
* or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
* it might be better to delay showing these events until the function has really
* failed (returned false). It should be sufficient to report only the *last* error
* in a messasge box then.
*/
"type":"Error";}&{"msg":string;})|({
/**
* An action cannot be performed because the user is not in the group.
* Reported eg. after a call to
* setChatName(), setChatProfileImage(),
* addContactToChat(), removeContactFromChat(),
* and messages sending functions.
*/
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
/**
* Messages or chats changed. One or more messages or chats changed for various
* reasons in the database:
* - Messages sent, received or removed
* - Chats created, deleted or archived
* - A draft has been set
*
* `chatId` is set if only a single chat is affected by the changes, otherwise 0.
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
*/
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
/**
* Reactions for the message changed.
*/
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
*
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
*/
"type":"IncomingMsg";}&{"chatId":U32;"msgId":U32;})|({
/**
* Downloading a bunch of messages just finished. This is an experimental
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
*
* msg_ids contains the message ids.
*/
"type":"IncomingMsgBunch";}&{"msgIds":(U32)[];})|({
/**
* Messages were seen or noticed.
* chat id is always set.
*/
"type":"MsgsNoticed";}&{"chatId":U32;})|({
/**
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
* DC_STATE_OUT_DELIVERED, see `Message.state`.
*/
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_FAILED, see `Message.state`.
*/
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_MDN_RCVD, see `Message.state`.
*/
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
* See setChatName(), setChatProfileImage(), addContactToChat()
* and removeContactFromChat().
*
* This event does not include ephemeral timer modification, which
* is a separate event.
*/
"type":"ChatModified";}&{"chatId":U32;})|({
/**
* Chat ephemeral timer changed.
*/
"type":"ChatEphemeralTimerModified";}&{"chatId":U32;"timer":U32;})|({
/**
* Contact(s) created, renamed, blocked or deleted.
*
* @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
*/
"type":"ContactsChanged";}&{"contactId":(U32|null);})|({
/**
* Location of one or more contact has changed.
*
* @param data1 (u32) contact_id of the contact for which the location has changed.
* If the locations of several contacts have been changed,
* this parameter is set to `None`.
*/
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
/**
* Inform about the configuration progress started by configure().
*/
"type":"ConfigureProgress";}&{
/**
* Progress.
*
* 0=error, 1-999=progress in permille, 1000=success and done
*/
"progress":Usize;
/**
* Progress comment or error, something to display to the user.
*/
"comment":(string|null);})|({
/**
* Inform about the import/export progress started by imex().
*
* @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
*/
"type":"ImexProgress";}&{"progress":Usize;})|({
/**
* A file has been exported. A file has been written by imex().
* This event may be sent multiple times by a single call to imex().
*
* A typical purpose for a handler of this event may be to make the file public to some system
* services.
*
* @param data2 0
*/
"type":"ImexFileWritten";}&{"path":string;})|({
/**
* Progress information of a secure-join handshake from the view of the inviter
* (Alice, the person who shows the QR code).
*
* These events are typically sent after a joiner has scanned the QR code
* generated by getChatSecurejoinQrCodeSvg().
*
* @param data1 (int) ID of the contact that wants to join.
* @param data2 (int) Progress as:
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
* 1000=Protocol finished for this contact.
*/
"type":"SecurejoinInviterProgress";}&{"contactId":U32;"progress":Usize;})|({
/**
* Progress information of a secure-join handshake from the view of the joiner
* (Bob, the person who scans the QR code).
* The events are typically sent while secureJoin(), which
* may take some time, is executed.
* @param data1 (int) ID of the inviting contact.
* @param data2 (int) Progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
* (Bob has verified alice and waits until Alice does the same for him)
*/
"type":"SecurejoinJoinerProgress";}&{"contactId":U32;"progress":Usize;})|{
/**
* The connectivity to the server changed.
* This means that you should refresh the connectivity view
* and possibly the connectivtiy HTML; see getConnectivity() and
* getConnectivityHtml() for details.
*/
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
/**
* Inform that a message containing a webxdc instance has been deleted
*/
"type":"WebxdcInstanceDeleted";}&{"msgId":U32;}));

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,198 @@
// 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 U64=number;
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;})|({"type":"login";}&{"address":string;}));
export type Usize=number;
export type I64=number;
export type ChatListEntry=[U32,U32];
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;
/**
* true when chat is a broadcastlist
*/
"isBroadcast":boolean;
/**
* contact id if this is a dm chat (for view profile entry in context menu)
*/
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"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;
/**
* the contact's last seen timestamp
*/
"lastSeen":I64;"wasSeenRecently":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;"wasSeenRecently":boolean;"mailingListAddress":(string|null);};
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
export type BasicChat=
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
/**
* Day marker, separating messages that correspond to different
* days according to local time.
*/
"kind":"dayMarker";}&{
/**
* Marker timestamp, for day markers, in unix milliseconds
*/
"timestamp":I64;}));
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 MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;"viewType":Viewtype;}));
export type SystemMessageType=("Unknown"|"GroupNameChanged"|"GroupImageChanged"|"MemberAddedToGroup"|"MemberRemovedFromGroup"|"AutocryptSetupMessage"|"SecurejoinMessage"|"LocationStreamingEnabled"|"LocationOnly"|
/**
* Chat ephemeral message timer is changed.
*/
"EphemeralTimerChanged"|"ChatProtectionEnabled"|"ChatProtectionDisabled"|
/**
* Self-sent-message that contains only json used for multi-device-sync;
* if possible, we attach that to other messages as for locations.
*/
"MultiDeviceSync"|"WebxdcStatusUpdate"|
/**
* Webxdc info added with `info` set in `send_webxdc_status_update()`.
*/
"WebxdcInfoMessage");
export type I32=number;
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);
/**
* True if full internet access should be granted to the app.
*/
"internetAccess":boolean;};
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
/**
* Structure representing all reactions to a particular message.
*/
export type Reactions=
/**
* Structure representing all reactions to a particular message.
*/
{
/**
* Map from a contact to it's reaction to message.
*/
"reactionsByContact":Record<U32,(string)[]>;
/**
* Unique reactions and their count
*/
"reactions":Record<string,U32>;};
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(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;
/**
* when is_info is true this describes what type of system message it is
*/
"systemMessageType":SystemMessageType;"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);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
/**
* also known as summary_text1
*/
"summaryPrefix":(string|null);
/**
* also known as summary_text2
*/
"summaryText":string;};
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type F64=number;
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,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)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,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,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,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,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];

View File

@@ -0,0 +1,52 @@
{
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"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"
},
"license": "MPL-2.0",
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"scripts": {
"build": "run-s generate-bindings extract-constants build:tsc build:bundle",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"build:tsc": "tsc",
"docs": "typedoc --out docs deltachat.ts",
"example": "run-s build example:build example:start",
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"example:start": "http-server .",
"extract-constants": "node ./scripts/generate-constants.js",
"generate-bindings": "cargo test",
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
"test:report-coverage": "node report_api_coverage.mjs",
"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"
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.99.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,54 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const data = [];
const header = resolve(__dirname, "../../../deltachat-ffi/deltachat.h");
console.log("Generating constants...");
const header_data = readFileSync(header, "UTF-8");
const regex = /^#define\s+(\w+)\s+(\w+)/gm;
let match;
while (null != (match = regex.exec(header_data))) {
const key = match[1];
const value = parseInt(match[2]);
if (!isNaN(value)) {
data.push({ key, value });
}
}
const constants = data
.filter(
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
)
.sort((lhs, rhs) => {
if (lhs.key < rhs.key) return -1;
else if (lhs.key > rhs.key) return 1;
return 0;
})
.filter(({ key }) => {
// filter out what we don't need it
return !(
key.startsWith("DC_EVENT_") ||
key.startsWith("DC_IMEX_") ||
key.startsWith("DC_CHAT_VISIBILITY") ||
key.startsWith("DC_DOWNLOAD") ||
key.startsWith("DC_INFO_") ||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
key.startsWith("DC_QR_")
);
})
.map((row) => {
return ` ${row.key}: ${row.value}`;
})
.join(",\n");
writeFileSync(
resolve(__dirname, "../generated/constants.ts"),
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
);

View File

@@ -0,0 +1,94 @@
import * as T from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { Event } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "tiny-emitter";
type DCWireEvent<T extends Event> = {
event: T;
contextId: number;
};
// export type Events = Record<
// Event["type"] | "ALL",
// (event: DeltaChatEvent<Event>) => void
// >;
type Events = { ALL: (accountId: number, event: Event) => void } & {
[Property in Event["type"]]: (
accountId: number,
event: Extract<Event, { type: Property }>
) => void;
};
type ContextEvents = { ALL: (event: Event) => void } & {
[Property in Event["type"]]: (
event: Extract<Event, { type: Property }>
) => void;
};
export type DcEvent = Event;
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>
export class BaseDeltaChat<
Transport extends BaseTransport<any>
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: TinyEmitter<ContextEvents>[] = [];
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 DCWireEvent<Event>;
this.emit(event.event.type, event.contextId, event.event as any);
this.emit("ALL", event.contextId, event.event as any);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
event.event as any
);
this.contextEmitters[event.contextId].emit("ALL", event.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,7 @@
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";
export { C } from "../generated/constants.js";

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.createContact(
accountId,
"example@delta.chat",
null
);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
.false;
await dc.rpc.blockContact(accountId, contactId);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
.true;
expect(await dc.rpc.getBlockedContacts(accountId)).to.have.length(1);
await dc.rpc.unblockContact(accountId, contactId);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
.false;
expect(await dc.rpc.getBlockedContacts(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,198 @@
import { assert, expect } from "chai";
import { DeltaChat, DcEvent } 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", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);
});
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.createContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
const { chatId: chatIdOnAccountB } = await eventPromise;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.getMessageIds(
accountId2,
chatIdOnAccountB,
0
);
expect(messageList).have.length(1);
const message = await dc.rpc.getMessage(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.createContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
// wait for message from A
console.log("wait for message from A");
const event = await eventPromise;
const { chatId: chatIdOnAccountB } = event;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.getMessageIds(
accountId2,
chatIdOnAccountB,
0
);
const message = await dc.rpc.getMessage(
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, chatId, "super secret message");
// Check if answer arives at A and if it is encrypted
await eventPromise2;
const messageId = (
await dc.rpc.getMessageIds(accountId1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.getMessage(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<T extends DcEvent["type"]>(
dc: DeltaChat,
eventType: T,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<Extract<DcEvent, { type: T }>> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")),
timeout
);
const callback = (contextId: number, event: DcEvent) => {
if (contextId == accountId) {
dc.off(eventType, callback);
clearTimeout(rejectTimeout);
resolve(event as any);
}
};
dc.on(eventType, callback);
});
}

View File

@@ -0,0 +1,95 @@
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,
RUST_MIN_STACK: "8388608"
},
});
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

@@ -1,3 +1,4 @@
#![allow(clippy::format_push_string)]
extern crate dirs;
use std::path::Path;
@@ -19,6 +20,7 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
use deltachat::receive_imf::*;
use deltachat::sql;
use deltachat::tools::*;
@@ -406,6 +408,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
resend <msg-id>\n\
markseen <msg-id>\n\
delmsg <msg-id>\n\
react <msg-id> [<reaction>]\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
listverified [<query>]\n\
@@ -551,7 +554,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
sql::housekeeping(&context).await.ok_or_log(&context);
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
let listflags = if arg0 == "listarchived" {
DC_GCL_ARCHIVED_ONLY
} else {
0
};
let time_start = std::time::SystemTime::now();
let chatlist = Chatlist::try_load(
&context,
@@ -982,11 +989,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,
@@ -1122,6 +1127,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ids[0] = MsgId::new(arg1.parse()?);
message::delete_msgs(&context, &ids).await?;
}
"react" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let msg_id = MsgId::new(arg1.parse()?);
let reaction = arg2;
send_reaction(&context, msg_id, reaction).await?;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
&context,

View File

@@ -20,10 +20,10 @@ use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::stock_str::StockStrings;
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};
@@ -72,6 +72,19 @@ fn receive_event(event: EventType) {
))
);
}
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => {
info!(
"{}",
yellow.paint(format!(
"Received REACTIONS_CHANGED(chat_id={}, msg_id={}, contact_id={})",
chat_id, msg_id, contact_id
))
);
}
EventType::ContactsChanged(_) => {
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
}
@@ -208,7 +221,7 @@ const CHAT_COMMANDS: [&str; 36] = [
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
const MESSAGE_COMMANDS: [&str; 9] = [
"listmsgs",
"msginfo",
"listfresh",
@@ -217,6 +230,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
"markseen",
"delmsg",
"download",
"react",
];
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
@@ -299,7 +313,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = Context::new(Path::new(&args[1]), 0, Events::new()).await?;
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
let events = context.get_event_emitter();
tokio::task::spawn(async move {
@@ -314,7 +328,6 @@ 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();
@@ -325,7 +338,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
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);
@@ -433,7 +446,7 @@ async fn handle_cmd(
}
println!("{}", qr);
let output = Command::new("qrencode")
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
.expect("failed to execute process");
io::stdout().write_all(&output.stdout).unwrap();

View File

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

View File

@@ -42,6 +42,7 @@ module.exports = {
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
DC_EVENT_IMEX_PROGRESS: 2051,
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
@@ -50,12 +51,14 @@ module.exports = {
DC_EVENT_MSG_FAILED: 2012,
DC_EVENT_MSG_READ: 2015,
DC_EVENT_NEW_BLOB_FILE: 150,
DC_EVENT_REACTIONS_CHANGED: 2001,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
DC_EVENT_SELFAVATAR_CHANGED: 2110,
DC_EVENT_SMTP_CONNECTED: 101,
DC_EVENT_SMTP_MESSAGE_SENT: 103,
DC_EVENT_WARNING: 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
DC_GCL_ADD_ALLDONE_HINT: 4,
DC_GCL_ADD_SELF: 2,
@@ -69,8 +72,19 @@ module.exports = {
DC_IMEX_EXPORT_SELF_KEYS: 1,
DC_IMEX_IMPORT_BACKUP: 12,
DC_IMEX_IMPORT_SELF_KEYS: 2,
DC_INFO_AUTOCRYPT_SETUP_MESSAGE: 6,
DC_INFO_EPHEMERAL_TIMER_CHANGED: 10,
DC_INFO_GROUP_IMAGE_CHANGED: 3,
DC_INFO_GROUP_NAME_CHANGED: 2,
DC_INFO_LOCATIONSTREAMING_ENABLED: 8,
DC_INFO_LOCATION_ONLY: 9,
DC_INFO_MEMBER_ADDED_TO_GROUP: 4,
DC_INFO_MEMBER_REMOVED_FROM_GROUP: 5,
DC_INFO_PROTECTION_DISABLED: 12,
DC_INFO_PROTECTION_ENABLED: 11,
DC_INFO_SECURE_JOIN_MESSAGE: 7,
DC_INFO_UNKNOWN: 0,
DC_INFO_WEBXDC_INFO_MESSAGE: 32,
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,
@@ -102,6 +116,7 @@ module.exports = {
DC_QR_FPR_MISMATCH: 220,
DC_QR_FPR_OK: 210,
DC_QR_FPR_WITHOUT_ADDR: 230,
DC_QR_LOGIN: 520,
DC_QR_REVIVE_VERIFYCONTACT: 510,
DC_QR_REVIVE_VERIFYGROUP: 512,
DC_QR_TEXT: 330,
@@ -128,6 +143,10 @@ module.exports = {
DC_STATE_UNDEFINED: 0,
DC_STR_AC_SETUP_MSG_BODY: 43,
DC_STR_AC_SETUP_MSG_SUBJECT: 42,
DC_STR_ADD_MEMBER_BY_OTHER: 129,
DC_STR_ADD_MEMBER_BY_YOU: 128,
DC_STR_AEAP_ADDR_CHANGED: 122,
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
DC_STR_ARCHIVEDCHATS: 40,
DC_STR_AUDIO: 11,
DC_STR_BAD_TIME_MSG_BODY: 85,
@@ -158,6 +177,26 @@ module.exports = {
DC_STR_EPHEMERAL_MINUTE: 77,
DC_STR_EPHEMERAL_MINUTES: 93,
DC_STR_EPHEMERAL_SECONDS: 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER: 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU: 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER: 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU: 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER: 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU: 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER: 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU: 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER: 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU: 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER: 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU: 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER: 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU: 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER: 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU: 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER: 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU: 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER: 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU: 156,
DC_STR_EPHEMERAL_WEEK: 80,
DC_STR_EPHEMERAL_WEEKS: 96,
DC_STR_ERROR: 112,
@@ -167,10 +206,20 @@ module.exports = {
DC_STR_FINGERPRINTS: 30,
DC_STR_FORWARDED: 97,
DC_STR_GIF: 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER: 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU: 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER: 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU: 134,
DC_STR_GROUP_LEFT_BY_OTHER: 133,
DC_STR_GROUP_LEFT_BY_YOU: 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER: 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU: 124,
DC_STR_IMAGE: 9,
DC_STR_INCOMING_MESSAGES: 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
DC_STR_LOCATION: 66,
DC_STR_LOCATION_ENABLED_BY_OTHER: 137,
DC_STR_LOCATION_ENABLED_BY_YOU: 136,
DC_STR_MESSAGES: 114,
DC_STR_MSGACTIONBYME: 63,
DC_STR_MSGACTIONBYUSER: 62,
@@ -190,10 +239,16 @@ module.exports = {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_PROTECTION_DISABLED: 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
DC_STR_PROTECTION_ENABLED: 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
DC_STR_REMOVE_MEMBER_BY_YOU: 130,
DC_STR_REPLY_NOUN: 90,
DC_STR_SAVED_MESSAGES: 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,

View File

@@ -14,7 +14,9 @@ module.exports = {
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
@@ -30,5 +32,6 @@ module.exports = {
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE'
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED'
}

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

@@ -42,6 +42,7 @@ export enum C {
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
DC_EVENT_IMEX_PROGRESS = 2051,
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
@@ -50,12 +51,14 @@ export enum C {
DC_EVENT_MSG_FAILED = 2012,
DC_EVENT_MSG_READ = 2015,
DC_EVENT_NEW_BLOB_FILE = 150,
DC_EVENT_REACTIONS_CHANGED = 2001,
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
DC_EVENT_SELFAVATAR_CHANGED = 2110,
DC_EVENT_SMTP_CONNECTED = 101,
DC_EVENT_SMTP_MESSAGE_SENT = 103,
DC_EVENT_WARNING = 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
@@ -69,8 +72,19 @@ export enum C {
DC_IMEX_EXPORT_SELF_KEYS = 1,
DC_IMEX_IMPORT_BACKUP = 12,
DC_IMEX_IMPORT_SELF_KEYS = 2,
DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6,
DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
DC_INFO_GROUP_IMAGE_CHANGED = 3,
DC_INFO_GROUP_NAME_CHANGED = 2,
DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
DC_INFO_LOCATION_ONLY = 9,
DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5,
DC_INFO_PROTECTION_DISABLED = 12,
DC_INFO_PROTECTION_ENABLED = 11,
DC_INFO_SECURE_JOIN_MESSAGE = 7,
DC_INFO_UNKNOWN = 0,
DC_INFO_WEBXDC_INFO_MESSAGE = 32,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
@@ -102,6 +116,7 @@ export enum C {
DC_QR_FPR_MISMATCH = 220,
DC_QR_FPR_OK = 210,
DC_QR_FPR_WITHOUT_ADDR = 230,
DC_QR_LOGIN = 520,
DC_QR_REVIVE_VERIFYCONTACT = 510,
DC_QR_REVIVE_VERIFYGROUP = 512,
DC_QR_TEXT = 330,
@@ -128,6 +143,10 @@ export enum C {
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ADD_MEMBER_BY_OTHER = 129,
DC_STR_ADD_MEMBER_BY_YOU = 128,
DC_STR_AEAP_ADDR_CHANGED = 122,
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
@@ -158,6 +177,26 @@ export enum C {
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
@@ -167,10 +206,20 @@ export enum C {
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
DC_STR_GROUP_LEFT_BY_OTHER = 133,
DC_STR_GROUP_LEFT_BY_YOU = 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
@@ -190,10 +239,16 @@ export enum C {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
@@ -240,7 +295,9 @@ export const EventId2EventName: { [key: number]: string } = {
400: 'DC_EVENT_ERROR',
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',
@@ -257,4 +314,5 @@ export const EventId2EventName: { [key: number]: string } = {
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
}

View File

@@ -72,6 +72,10 @@ export class Contact {
return binding.dcn_contact_get_last_seen(this.dc_contact)
}
wasSeenRecently() {
return Boolean(binding.dcn_contact_was_seen_recently(this.dc_contact))
}
getName(): string {
return binding.dcn_contact_get_name(this.dc_contact)
}

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

@@ -12,11 +12,12 @@ const GITHUB_API_URL =
const file_url = process.env['URL']
const GITHUB_TOKEN = process.env['GITHUB_TOKEN']
const context = process.env['MSG_CONTEXT']
const STATUS_DATA = {
state: 'success',
description: '⏩ Click on "Details" to download →',
context: 'Download the node-bindings.tar.gz',
context: context || 'Download the node-bindings.tar.gz',
target_url: base_url + file_url,
}

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;
@@ -1625,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();
@@ -1915,6 +1930,13 @@ NAPI_METHOD(dcn_contact_get_last_seen) {
NAPI_RETURN_INT64(timestamp);
}
NAPI_METHOD(dcn_contact_was_seen_recently) {
NAPI_ARGV(1);
NAPI_DC_CONTACT();
int seen_recently = dc_contact_was_seen_recently(dc_contact);
NAPI_RETURN_UINT32(seen_recently);
}
NAPI_METHOD(dcn_contact_is_blocked) {
NAPI_ARGV(1);
NAPI_DC_CONTACT();
@@ -2924,6 +2946,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;
@@ -3082,18 +3110,16 @@ 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);
dc_event_emitter_t * dc_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
dc_event_t* event;
while (true) {
if (dc_accounts_event_emitter == NULL) {
if (dc_event_emitter == NULL) {
TRACE("event emitter is null, bailing");
break;
}
event = dc_accounts_get_next_event(dc_accounts_event_emitter);
event = dc_get_next_event(dc_event_emitter);
if (event == NULL) {
TRACE("no more events");
break;
@@ -3119,7 +3145,7 @@ static void accounts_event_handler_thread_func(void* arg)
}
}
dc_accounts_event_emitter_unref(dc_accounts_event_emitter);
dc_event_emitter_unref(dc_event_emitter);
TRACE("event_handler_thread_func ended");
@@ -3173,7 +3199,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) {
@@ -3234,6 +3260,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() {
/**
@@ -3370,6 +3514,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);
@@ -3504,4 +3649,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,99 @@ 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 = (response) => {
// ignore events
const answer = JSON.parse(response)
if (answer['method'] !== 'event') res(answer)
}
})
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],
},
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

@@ -57,8 +57,8 @@
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
"test": "npm run test:lint && npm run test:mocha",
"test:lint": "npm run lint",
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.92.0"
"version": "1.99.0"
}

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,4 +0,0 @@
[devpi:upload]
formats = sdist.tgz
no-vcs = 1

View File

@@ -2,7 +2,7 @@ import sys
from pkg_resources import DistributionNotFound, get_distribution
from . import capi, const, events, hookspec # noqa
from . import capi, events, hookspec # noqa
from .account import Account, get_core_info # noqa
from .capi import ffi # noqa
from .chat import Chat # noqa
@@ -17,14 +17,6 @@ except DistributionNotFound:
__version__ = "0.0.0.dev0-unknown"
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _DC_EVENTNAME_MAP:
for name in dir(const):
if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[getattr(const, name)] = name
return _DC_EVENTNAME_MAP[integer]
def register_global_plugin(plugin):
"""Register a global plugin which implements one or more
of the :class:`deltachat.hookspec.Global` hooks.

View File

@@ -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:
@@ -710,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

@@ -8,14 +8,21 @@ import traceback
from contextlib import contextmanager
from queue import Empty, Queue
import deltachat
from . import const
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
if not _DC_EVENTNAME_MAP:
for name in dir(const):
if name.startswith("DC_EVENT_"):
_DC_EVENTNAME_MAP[getattr(const, name)] = name
return _DC_EVENTNAME_MAP[integer]
class FFIEvent:
def __init__(self, name: str, data1, data2):
self.name = name
@@ -98,7 +105,7 @@ class FFIEventTracker:
yield self.get(timeout=timeout, check_error=check_error)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
rex = re.compile("(?:{}).*".format(event_name_regex))
rex = re.compile("^(?:{})$".format(event_name_regex))
for ev in self.iter_events(timeout=timeout, check_error=check_error):
if rex.match(ev.name):
return ev
@@ -239,7 +246,7 @@ class EventThread(threading.Thread):
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper
# function which provides us signature info of an event call
evt_name = deltachat.get_dc_event_name(evt)
evt_name = get_dc_event_name(evt)
if lib.dc_event_has_string_data(evt):
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
else:

View File

@@ -507,6 +507,8 @@ def parse_system_add_remove(text):
returns a (action, affected, actor) triple"""
# You removed member a@b.
# You added member a@b.
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
@@ -518,6 +520,10 @@ def parse_system_add_remove(text):
if m:
affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor)
m = re.match(r"you (removed|added) member (.+)", text)
if m:
action, affected = m.groups()
return action, extract_addr(affected), "me"
if text.startswith("group left by "):
addr = extract_addr(text[13:])
if addr:

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

@@ -2055,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

@@ -80,7 +80,7 @@ class TestOfflineAccountBasic:
d = ac1.get_info()
assert d["arch"]
assert d["number_of_chats"] == "0"
assert d["bcc_self"] == "0"
assert d["bcc_self"] == "1"
def test_is_not_configured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
@@ -118,7 +118,7 @@ class TestOfflineAccountBasic:
def test_has_bccself(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
assert ac1.get_config("bcc_self") == "0"
assert ac1.get_config("bcc_self") == "1"
def test_selfcontact_if_unconfigured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
@@ -301,7 +301,7 @@ class TestOfflineChat:
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1._evtracker.consume_events()
with pytest.raises(ValueError):
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
@@ -317,7 +317,7 @@ class TestOfflineChat:
chat.send_text("Now we have a group for homework")
assert chat.is_promoted()
chat.set_name("Homework")
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
@pytest.mark.parametrize("verified", [True, False])
def test_group_chat_qr(self, acfactory, ac1, verified):
@@ -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

@@ -10,6 +10,9 @@ envlist =
commands =
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
passenv =
DCC_RS_DEV
DCC_RS_TARGET

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

@@ -8,7 +8,7 @@ set -e -x
# compile core lib
cargo build --release -p deltachat_ffi
cargo build --release -p deltachat_ffi --features jsonrpc
# Statically link against libdeltachat.a.
export DCC_RS_DEV="$PWD"

View File

@@ -42,15 +42,15 @@ def replace_toml_version(relpath, newversion):
def read_json_version(relpath):
p = pathlib.Path("package.json")
p = pathlib.Path(relpath)
assert p.exists()
with open(p, "r") as f:
json_data = json.loads(f.read())
return json_data["version"]
def update_package_json(newversion):
p = pathlib.Path("package.json")
def update_package_json(relpath, newversion):
p = pathlib.Path(relpath)
assert p.exists()
with open(p, "r") as f:
json_data = json.loads(f.read())
@@ -63,14 +63,16 @@ 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"]
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
try:
opts = parser.parse_args()
except SystemExit:
print()
for x in toml_list:
print("{}: {}".format(x, read_toml_version(x)))
print("package.json:", str(read_json_version("package.json")))
for x in json_list:
print("{}: {}".format(x, read_json_version(x)))
print()
raise SystemExit("need argument: new version, example: 1.25.0")
@@ -91,9 +93,12 @@ def main():
else:
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
replace_toml_version("Cargo.toml", newversion)
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
update_package_json(newversion)
for toml_filename in toml_list:
replace_toml_version(toml_filename, newversion)
for json_filename in json_list:
update_package_json(json_filename, newversion)
print("running cargo check")
subprocess.call(["cargo", "check"])

10
spec.md
View File

@@ -450,6 +450,16 @@ This allows the receiver to show the time without knowing the file format.
Chat-Duration: 10000
# Reactions
Messengers MAY implement [RFC 9078](https://tools.ietf.org/html/rfc9078) reactions.
Received reaction should be interpreted as overwriting all previous reactions
received from the same contact.
This semantics is compatible to [XEP-0444](https://xmpp.org/extensions/xep-0444.html).
As an extension to RFC 9078, it is allowed to send empty reaction message,
in which case all previously sent reactions are retracted.
# Miscellaneous
Messengers SHOULD use the header `In-Reply-To` as usual.

View File

@@ -10,6 +10,7 @@ use uuid::Uuid;
use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::stock_str::StockStrings;
/// Account manager, that can handle multiple accounts in a single place.
#[derive(Debug)]
@@ -20,6 +21,12 @@ pub struct Accounts {
/// Event channel to emit account manager errors.
events: Events,
/// Stock string translations shared by all created contexts.
///
/// This way changing a translation for one context automatically
/// changes it for all other contexts.
pub(crate) stockstrings: StockStrings,
}
impl Accounts {
@@ -33,7 +40,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")?;
@@ -55,8 +62,9 @@ impl Accounts {
.await
.context("failed to load accounts config")?;
let events = Events::new();
let stockstrings = StockStrings::new();
let accounts = config
.load_accounts(&events)
.load_accounts(&events, &stockstrings)
.await
.context("failed to load accounts")?;
@@ -65,23 +73,24 @@ impl Accounts {
config,
accounts,
events,
stockstrings,
})
}
/// Get an account by its `id`:
pub async fn get_account(&self, id: u32) -> Option<Context> {
pub fn get_account(&self, id: u32) -> Option<Context> {
self.accounts.get(&id).cloned()
}
/// Get the currently selected account.
pub async fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account().await;
pub fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account();
self.accounts.get(&id).cloned()
}
/// Returns the currently selected account's id or None if no account is selected.
pub async fn get_selected_account_id(&self) -> Option<u32> {
match self.config.get_selected_account().await {
pub fn get_selected_account_id(&self) -> Option<u32> {
match self.config.get_selected_account() {
0 => None,
id => Some(id),
}
@@ -104,6 +113,7 @@ impl Accounts {
&account_config.dbfile(),
account_config.id,
self.events.clone(),
self.stockstrings.clone(),
)
.await?;
self.accounts.insert(account_config.id, ctx);
@@ -119,6 +129,7 @@ impl Accounts {
&account_config.dbfile(),
account_config.id,
self.events.clone(),
self.stockstrings.clone(),
)
.await?;
self.accounts.insert(account_config.id, ctx);
@@ -135,7 +146,7 @@ impl Accounts {
ctx.stop_io().await;
drop(ctx);
if let Some(cfg) = self.config.get_account(id).await {
if let Some(cfg) = self.config.get_account(id) {
// Spend up to 1 minute trying to remove the files.
// Files may remain locked up to 30 seconds due to r2d2 bug:
// https://github.com/sfackler/r2d2/issues/99
@@ -171,7 +182,7 @@ impl Accounts {
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;
let old_id = self.config.get_selected_account();
// create new account
let account_config = self
@@ -204,7 +215,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(),
self.stockstrings.clone(),
)
.await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
}
@@ -225,7 +242,7 @@ impl Accounts {
}
/// Get a list of all account ids.
pub async fn get_all(&self) -> Vec<u32> {
pub fn get_all(&self) -> Vec<u32> {
self.accounts.keys().copied().collect()
}
@@ -281,7 +298,7 @@ impl Accounts {
}
/// Returns event emitter.
pub async fn get_event_emitter(&self) -> EventEmitter {
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
}
@@ -339,17 +356,31 @@ impl Config {
Ok(Config { file, inner })
}
pub async fn load_accounts(&self, events: &Events) -> Result<BTreeMap<u32, Context>> {
/// Loads all accounts defined in the configuration file.
///
/// Created contexts share the same event channel and stock string
/// translations.
pub async fn load_accounts(
&self,
events: &Events,
stockstrings: &StockStrings,
) -> Result<BTreeMap<u32, Context>> {
let mut accounts = BTreeMap::new();
for account_config in &self.inner.accounts {
let ctx = Context::new(&account_config.dbfile(), account_config.id, events.clone())
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile()
)
})?;
let ctx = Context::new(
&account_config.dbfile(),
account_config.id,
events.clone(),
stockstrings.clone(),
)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile()
)
})?;
accounts.insert(account_config.id, ctx);
}
@@ -380,7 +411,6 @@ impl Config {
.context("failed to select just added account")?;
let cfg = self
.get_account(id)
.await
.context("failed to get just added account")?;
Ok(cfg)
}
@@ -402,11 +432,11 @@ impl Config {
self.sync().await
}
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
fn get_account(&self, id: u32) -> Option<AccountConfig> {
self.inner.accounts.iter().find(|e| e.id == id).cloned()
}
pub async fn get_selected_account(&self) -> u32 {
pub fn get_selected_account(&self) -> u32 {
self.inner.selected_account
}
@@ -447,6 +477,8 @@ impl AccountConfig {
mod tests {
use super::*;
use crate::stock_str::{self, StockMessage};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_new_open() {
let dir = tempfile::tempdir().unwrap();
@@ -458,7 +490,7 @@ mod tests {
let accounts2 = Accounts::open(p).await.unwrap();
assert_eq!(accounts1.accounts.len(), 1);
assert_eq!(accounts1.config.get_selected_account().await, 1);
assert_eq!(accounts1.config.get_selected_account(), 1);
assert_eq!(accounts1.dir, accounts2.dir);
assert_eq!(accounts1.config, accounts2.config,);
@@ -472,23 +504,23 @@ mod tests {
let mut accounts = Accounts::new(p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
assert_eq!(accounts.config.get_selected_account(), 0);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 1);
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
assert_eq!(accounts.config.get_selected_account(), 1);
let id = accounts.add_account().await.unwrap();
assert_eq!(id, 2);
assert_eq!(accounts.config.get_selected_account().await, id);
assert_eq!(accounts.config.get_selected_account(), id);
assert_eq!(accounts.accounts.len(), 2);
accounts.select_account(1).await.unwrap();
assert_eq!(accounts.config.get_selected_account().await, 1);
assert_eq!(accounts.config.get_selected_account(), 1);
accounts.remove_account(1).await.unwrap();
assert_eq!(accounts.config.get_selected_account().await, 2);
assert_eq!(accounts.config.get_selected_account(), 2);
assert_eq!(accounts.accounts.len(), 1);
}
@@ -498,17 +530,17 @@ mod tests {
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await?;
assert!(accounts.get_selected_account().await.is_none());
assert_eq!(accounts.config.get_selected_account().await, 0);
assert!(accounts.get_selected_account().is_none());
assert_eq!(accounts.config.get_selected_account(), 0);
let id = accounts.add_account().await?;
assert!(accounts.get_selected_account().await.is_some());
assert!(accounts.get_selected_account().is_some());
assert_eq!(id, 1);
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, id);
assert_eq!(accounts.config.get_selected_account(), id);
accounts.remove_account(id).await?;
assert!(accounts.get_selected_account().await.is_none());
assert!(accounts.get_selected_account().is_none());
Ok(())
}
@@ -520,10 +552,10 @@ mod tests {
let mut accounts = Accounts::new(p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account().await, 0);
assert_eq!(accounts.config.get_selected_account(), 0);
let extern_dbfile: PathBuf = dir.path().join("other");
let ctx = Context::new(&extern_dbfile, 0, Events::new())
let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
.await
.unwrap();
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
@@ -537,9 +569,9 @@ mod tests {
.await
.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account().await, 1);
assert_eq!(accounts.config.get_selected_account(), 1);
let ctx = accounts.get_selected_account().await.unwrap();
let ctx = accounts.get_selected_account().unwrap();
assert_eq!(
"me@mail.com",
ctx.get_config(crate::config::Config::Addr)
@@ -562,7 +594,7 @@ mod tests {
assert_eq!(id, expected_id);
}
let ids = accounts.get_all().await;
let ids = accounts.get_all();
for (i, expected_id) in (1..10).enumerate() {
assert_eq!(ids.get(i), Some(&expected_id));
}
@@ -577,16 +609,16 @@ mod tests {
let (id0, id1, id2) = {
let mut accounts = Accounts::new(p.clone()).await?;
accounts.add_account().await?;
let ids = accounts.get_all().await;
let ids = accounts.get_all();
assert_eq!(ids.len(), 1);
let id0 = *ids.get(0).unwrap();
let ctx = accounts.get_account(id0).await.unwrap();
let id0 = *ids.first().unwrap();
let ctx = accounts.get_account(id0).unwrap();
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
.await?;
let id1 = accounts.add_account().await?;
let ctx = accounts.get_account(id1).await.unwrap();
let ctx = accounts.get_account(id1).unwrap();
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
.await?;
@@ -597,7 +629,7 @@ mod tests {
}
let id2 = accounts.add_account().await?;
let ctx = accounts.get_account(id2).await.unwrap();
let ctx = accounts.get_account(id2).unwrap();
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
.await?;
@@ -611,31 +643,31 @@ mod tests {
let (id0_reopened, id1_reopened, id2_reopened) = {
let accounts = Accounts::new(p.clone()).await?;
let ctx = accounts.get_selected_account().await.unwrap();
let ctx = accounts.get_selected_account().unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("two@example.org".to_string())
);
let ids = accounts.get_all().await;
let ids = accounts.get_all();
assert_eq!(ids.len(), 3);
let id0 = *ids.get(0).unwrap();
let ctx = accounts.get_account(id0).await.unwrap();
let id0 = *ids.first().unwrap();
let ctx = accounts.get_account(id0).unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("one@example.org".to_string())
);
let id1 = *ids.get(1).unwrap();
let t = accounts.get_account(id1).await.unwrap();
let t = accounts.get_account(id1).unwrap();
assert_eq!(
t.get_config(crate::config::Config::Addr).await?,
Some("two@example.org".to_string())
);
let id2 = *ids.get(2).unwrap();
let ctx = accounts.get_account(id2).await.unwrap();
let ctx = accounts.get_account(id2).unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
Some("three@example.org".to_string())
@@ -661,7 +693,7 @@ mod tests {
assert_eq!(accounts.accounts.len(), 0);
// Create event emitter.
let event_emitter = accounts.get_event_emitter().await;
let event_emitter = accounts.get_event_emitter();
// Test that event emitter does not return `None` immediately.
let duration = std::time::Duration::from_millis(1);
@@ -692,7 +724,6 @@ mod tests {
.context("failed to add closed account")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.id, account_id);
let passphrase_set_success = account
@@ -707,7 +738,6 @@ mod tests {
.context("failed to create second accounts manager")?;
let account = accounts
.get_selected_account()
.await
.context("failed to get account")?;
assert_eq!(account.is_open().await, false);
@@ -720,4 +750,28 @@ mod tests {
Ok(())
}
/// Tests that accounts share stock string translations.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_accounts_share_translations() -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await?;
accounts.add_account().await?;
accounts.add_account().await?;
let account1 = accounts.get_account(1).context("failed to get account 1")?;
let account2 = accounts.get_account(2).context("failed to get account 2")?;
assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
account1
.set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
.await?;
assert_eq!(stock_str::no_messages(&account1).await, "foobar");
assert_eq!(stock_str::no_messages(&account2).await, "foobar");
Ok(())
}
}

753
src/authres.rs Normal file
View File

@@ -0,0 +1,753 @@
//! Parsing and handling of the Authentication-Results header.
//! See the comment on [`handle_authres`] for more.
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fmt;
use anyhow::Result;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
use once_cell::sync::Lazy;
use crate::config::Config;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::tools::time;
use crate::tools::EmailAddress;
/// `authres` is short for the Authentication-Results header, defined in
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
/// about whether DKIM and SPF passed.
///
/// To mitigate From forgery, we remember for each sending domain whether it is known
/// to have valid DKIM. If an email from such a domain comes with invalid DKIM,
/// we don't allow changing the autocrypt key.
///
/// See <https://github.com/deltachat/deltachat-core-rust/issues/3507>.
pub(crate) async fn handle_authres(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DkimResults> {
let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
warn!(context, "invalid email {:#}", e);
// This email is invalid, but don't return an error, we still want to
// add a stub to the database so that it's not downloaded again
return Ok(DkimResults::default());
}
};
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
update_authservid_candidates(context, &authres).await?;
compute_dkim_results(context, authres, &from_domain, message_time).await
}
#[derive(Default, Debug)]
pub(crate) struct DkimResults {
/// Whether DKIM passed for this particular e-mail.
pub dkim_passed: bool,
/// Whether DKIM is known to work for e-mails coming from the sender's domain,
/// i.e. whether we expect DKIM to work.
pub dkim_should_work: bool,
/// Whether changing the public Autocrypt key should be allowed.
/// This is false if we expected DKIM to work (dkim_works=true),
/// but it failed now (dkim_passed=false).
pub allow_keychange: bool,
}
impl fmt::Display for DkimResults {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"DKIM Results: Passed={}, Works={}, Allow_Keychange={}",
self.dkim_passed, self.dkim_should_work, self.allow_keychange
)?;
if !self.allow_keychange {
write!(fmt, " KEYCHANGES NOT ALLOWED!!!!")?;
}
Ok(())
}
}
type AuthservId = String;
#[derive(Debug, PartialEq)]
enum DkimResult {
/// The header explicitly said that DKIM passed
Passed,
/// The header explicitly said that DKIM failed
Failed,
/// The header didn't say anything about DKIM; this might mean that it wasn't
/// checked, but it might also mean that it failed. This is because some providers
/// (e.g. ik.me, mail.ru, posteo.de) don't add `dkim=none` to their
/// Authentication-Results if there was no DKIM.
Nothing,
}
type ParsedAuthresHeaders = Vec<(AuthservId, DkimResult)>;
fn parse_authres_headers(
headers: &mailparse::headers::Headers<'_>,
from_domain: &str,
) -> ParsedAuthresHeaders {
let mut res = Vec::new();
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
let header_value = remove_comments(&header_value);
if let Some(mut authserv_id) = header_value.split(';').next() {
if authserv_id.contains(char::is_whitespace) || authserv_id.is_empty() {
// Outlook violates the RFC by not adding an authserv-id at all, which we notice
// because there is whitespace in the first identifier before the ';'.
// Authentication-Results-parsing still works securely because they remove incoming
// Authentication-Results headers.
// We just use an arbitrary authserv-id, it will work for Outlook, and in general,
// with providers not implementing the RFC correctly, someone can trick us
// into thinking that an incoming email is DKIM-correct, anyway.
// The most important thing here is that we have some valid `authserv_id`.
authserv_id = "invalidAuthservId";
}
let dkim_passed = parse_one_authres_header(&header_value, from_domain);
res.push((authserv_id.to_string(), dkim_passed));
}
}
res
}
/// The headers can contain comments that look like this:
/// ```text
/// Authentication-Results: (this is a comment) gmx.net; (another; comment) dkim=pass;
/// ```
fn remove_comments(header: &str) -> Cow<'_, str> {
// In Pomsky, this is:
// "(" Codepoint* lazy ")"
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
static RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
RE.replace_all(header, " ")
}
/// Parses a single Authentication-Results header, like:
///
/// ```text
/// Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
/// ```
fn parse_one_authres_header(header_value: &str, from_domain: &str) -> DkimResult {
if let Some((before_dkim_part, dkim_to_end)) = header_value.split_once("dkim=") {
// Check that the character right before `dkim=` is a space or a tab
// so that we wouldn't e.g. mistake `notdkim=pass` for `dkim=pass`
if before_dkim_part.ends_with(' ') || before_dkim_part.ends_with('\t') {
let dkim_part = dkim_to_end.split(';').next().unwrap_or_default();
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
if let Some(&"pass") = dkim_parts.first() {
// DKIM headers contain a header.d or header.i field
// that says which domain signed. We have to check ourselves
// that this is the same domain as in the From header.
let header_d: &str = &format!("header.d={}", &from_domain);
let header_i: &str = &format!("header.i=@{}", &from_domain);
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
// We have found a `dkim=pass` header!
return DkimResult::Passed;
}
} else {
// dkim=fail, dkim=none, ...
return DkimResult::Failed;
}
}
}
DkimResult::Nothing
}
/// ## About authserv-ids
///
/// After having checked DKIM, our email server adds an Authentication-Results header.
///
/// Now, an attacker could just add an Authentication-Results header that says dkim=pass
/// in order to make us think that DKIM was correct in their From-forged email.
///
/// In order to prevent this, each email server adds its authserv-id to the
/// Authentication-Results header, e.g. Testrun's authserv-id is `testrun.org`, Gmail's
/// is `mx.google.com`. When Testrun gets a mail delivered from outside, it will then
/// remove any Authentication-Results headers whose authserv-id is also `testrun.org`.
///
/// We need to somehow find out the authserv-id(s) of our email server, so that
/// we can use the Authentication-Results with the right authserv-id.
///
/// ## What this function does
///
/// When receiving an email, this function is called and updates the candidates for
/// our server's authserv-id, i.e. what we think our server's authserv-id is.
///
/// Usually, every incoming email has Authentication-Results with our server's
/// authserv-id, so, the intersection of the existing authserv-ids and the incoming
/// authserv-ids for our server's authserv-id is a good guess for our server's
/// authserv-id. When this intersection is empty, we assume that the authserv-id has
/// changed and start over with the new authserv-ids.
///
/// See [`handle_authres`].
async fn update_authservid_candidates(
context: &Context,
authres: &ParsedAuthresHeaders,
) -> Result<()> {
let mut new_ids: BTreeSet<&str> = authres
.iter()
.map(|(authserv_id, _dkim_passed)| authserv_id.as_str())
.collect();
if new_ids.is_empty() {
// The incoming message doesn't contain any authentication results, maybe it's a
// self-sent or a mailer-daemon message
return Ok(());
}
let old_config = context.get_config(Config::AuthservIdCandidates).await?;
let old_ids = parse_authservid_candidates_config(&old_config);
let intersection: BTreeSet<&str> = old_ids.intersection(&new_ids).copied().collect();
if !intersection.is_empty() {
new_ids = intersection;
}
// If there were no AuthservIdCandidates previously, just start with
// the ones from the incoming email
if old_ids != new_ids {
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
context
.set_config(Config::AuthservIdCandidates, Some(&new_config))
.await?;
// Updating the authservid candidates may mean that we now consider
// emails as "failed" which "passed" previously, so we need to
// reset our expectation which DKIMs work.
clear_dkim_works(context).await?
}
Ok(())
}
/// Use the parsed authres and the authservid candidates to compute whether DKIM passed
/// and whether a keychange should be allowed.
///
/// We track in the `sending_domains` table whether we get positive Authentication-Results
/// for mails from a contact (meaning that their provider properly authenticates against
/// our provider).
///
/// Once a contact is known to come with positive Authentication-Resutls (dkim: pass),
/// we don't accept Autocrypt key changes if they come with negative Authentication-Results.
async fn compute_dkim_results(
context: &Context,
mut authres: ParsedAuthresHeaders,
from_domain: &str,
message_time: i64,
) -> Result<DkimResults> {
let mut dkim_passed = false;
let ids_config = context.get_config(Config::AuthservIdCandidates).await?;
let ids = parse_authservid_candidates_config(&ids_config);
// Remove all foreign authentication results
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
if authres.is_empty() {
// If the authentication results are empty, then our provider doesn't add them
// and an attacker could just add their own Authentication-Results, making us
// think that DKIM passed. So, in this case, we can as well assume that DKIM passed.
dkim_passed = true;
} else {
for (_authserv_id, current_dkim_passed) in authres {
match current_dkim_passed {
DkimResult::Passed => {
dkim_passed = true;
break;
}
DkimResult::Failed => {
dkim_passed = false;
break;
}
DkimResult::Nothing => {
// Continue looking for an Authentication-Results header
}
}
}
}
let last_working_timestamp = dkim_works_timestamp(context, from_domain).await?;
let mut dkim_should_work = dkim_should_work(last_working_timestamp)?;
if message_time > last_working_timestamp && dkim_passed {
set_dkim_works_timestamp(context, from_domain, message_time).await?;
dkim_should_work = true;
}
Ok(DkimResults {
dkim_passed,
dkim_should_work,
allow_keychange: dkim_passed || !dkim_should_work,
})
}
/// Whether DKIM in emails from this domain should be considered to work.
fn dkim_should_work(last_working_timestamp: i64) -> Result<bool> {
// When we get an email with valid DKIM-Authentication-Results,
// then we assume that DKIM works for 30 days from this time on.
let should_work_until = last_working_timestamp + 3600 * 24 * 30;
let dkim_ever_worked = last_working_timestamp > 0;
// We're using time() here and not the time when the message
// claims to have been sent (passed around as `message_time`)
// because otherwise an attacker could just put a time way
// in the future into the `Date` header and then we would
// assume that DKIM doesn't have to be valid anymore.
let dkim_should_work_now = should_work_until > time();
Ok(dkim_ever_worked && dkim_should_work_now)
}
async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i64, anyhow::Error> {
let last_working_timestamp: i64 = context
.sql
.query_get_value(
"SELECT dkim_works FROM sending_domains WHERE domain=?",
paramsv![from_domain],
)
.await?
.unwrap_or(0);
Ok(last_working_timestamp)
}
async fn set_dkim_works_timestamp(
context: &Context,
from_domain: &str,
timestamp: i64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
paramsv![from_domain, timestamp],
)
.await?;
Ok(())
}
async fn clear_dkim_works(context: &Context) -> Result<()> {
context
.sql
.execute("DELETE FROM sending_domains", paramsv![])
.await?;
Ok(())
}
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
config
.as_deref()
.map(|c| c.split_whitespace().collect())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncReadExt;
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
use crate::securejoin::join_securejoin;
use crate::test_utils;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
#[test]
fn test_remove_comments() {
let header = "Authentication-Results: mx3.messagingengine.com;
dkim=pass (1024-bit rsa key sha256) header.d=riseup.net;"
.to_string();
assert_eq!(
remove_comments(&header),
"Authentication-Results: mx3.messagingengine.com;
dkim=pass header.d=riseup.net;"
);
let header = ") aaa (".to_string();
assert_eq!(remove_comments(&header), ") aaa (");
let header = "((something weird) no comment".to_string();
assert_eq!(remove_comments(&header), " no comment");
let header = "🎉(🎉(🎉))🎉(".to_string();
assert_eq!(remove_comments(&header), "🎉 )🎉(");
// Comments are allowed to include whitespace
let header = "(com\n\t\r\nment) no comment (comment)".to_string();
assert_eq!(remove_comments(&header), " no comment ");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_authentication_results() -> Result<()> {
let t = TestContext::new().await;
t.configure_addr("alice@gmx.net").await;
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Passed),
("gmx.net".to_string(), DkimResult::Nothing)
]
);
let bytes = b"Authentication-Results: gmx.net; notdkim=pass header.i=@slack.com
Authentication-Results: gmx.net; notdkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Nothing),
("gmx.net".to_string(), DkimResult::Nothing)
]
);
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(actual, vec![("gmx.net".to_string(), DkimResult::Nothing)],);
// Weird Authentication-Results from Outlook without an authserv-id
let bytes = b"Authentication-Results: spf=pass (sender IP is 40.92.73.85)
smtp.mailfrom=hotmail.com; dkim=pass (signature was verified)
header.d=hotmail.com;dmarc=pass action=none
header.from=hotmail.com;compauth=pass reason=100";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "hotmail.com");
// At this point, the most important thing to test is that there are no
// authserv-ids with whitespace in them.
assert_eq!(
actual,
vec![("invalidAuthservId".to_string(), DkimResult::Passed)]
);
let bytes = b"Authentication-Results: gmx.net; dkim=none header.i=@slack.com
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Failed),
("gmx.net".to_string(), DkimResult::Passed)
]
);
// ';' in comments
let bytes = b"Authentication-Results: mx1.riseup.net;
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.a=rsa-sha256 header.s=mail header.b=avNJu6sw;
dkim-atps=neutral";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "yandex.ru");
assert_eq!(
actual,
vec![("mx1.riseup.net".to_string(), DkimResult::Passed)]
);
let bytes = br#"Authentication-Results: box.hispanilandia.net;
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
dkim-atps=neutral
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#;
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "disroot.org");
assert_eq!(
actual,
vec![
("box.hispanilandia.net".to_string(), DkimResult::Failed),
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_authservid_candidates() -> Result<()> {
let t = TestContext::new_alice().await;
update_authservid_candidates_test(&t, &["mx3.messagingengine.com"]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx3.messagingengine.com");
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
// A message without any Authentication-Results headers shouldn't remove all
// candidates since it could be a mailer-daemon message or so
update_authservid_candidates_test(&t, &[]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
update_authservid_candidates_test(&t, &["mx4.messagingengine.com", "someotherdomain.com"])
.await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
Ok(())
}
/// Calls update_authservid_candidates(), meant for using in a test.
///
/// update_authservid_candidates() only looks at the keys of its
/// `authentication_results` parameter. So, this function takes `incoming_ids`
/// and adds some AuthenticationResults to get the HashMap we need.
async fn update_authservid_candidates_test(context: &Context, incoming_ids: &[&str]) {
let v = incoming_ids
.iter()
.map(|id| (id.to_string(), DkimResult::Passed))
.collect();
update_authservid_candidates(context, &v).await.unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_realworld_authentication_results() -> Result<()> {
let mut test_failed = false;
let dir = tools::read_dir("test-data/message/dkimchecks-2022-09-28/".as_ref())
.await
.unwrap();
let mut bytes = Vec::new();
for entry in dir {
if !entry.file_type().await.unwrap().is_dir() {
continue;
}
let self_addr = entry.file_name().into_string().unwrap();
let self_domain = EmailAddress::new(&self_addr).unwrap().domain;
let authres_parsing_works = [
"ik.me",
"web.de",
"posteo.de",
"gmail.com",
"hotmail.com",
"mail.ru",
"aol.com",
"yahoo.com",
"icloud.com",
"fastmail.com",
"mail.de",
"outlook.com",
"gmx.de",
"testrun.org",
]
.contains(&self_domain.as_str());
let t = TestContext::new().await;
t.configure_addr(&self_addr).await;
if !authres_parsing_works {
println!("========= Receiving as {} =========", &self_addr);
}
// Simulate receiving all emails once, so that we have the correct authserv-ids
let mut dir = tools::read_dir(&entry.path()).await.unwrap();
// The ordering in which the emails are received can matter;
// the test _should_ pass for every ordering.
dir.sort_by_key(|d| d.file_name());
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::thread_rng());
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers)[0].addr;
let res = handle_authres(&t, &mail, from, time()).await?;
assert!(res.allow_keychange);
}
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers)[0].addr;
let res = handle_authres(&t, &mail, from, time()).await?;
if !res.allow_keychange {
println!(
"!!!!!! FAILURE Receiving {:?}, keychange is not allowed !!!!!!",
entry.path()
);
test_failed = true;
}
let from_domain = EmailAddress::new(from).unwrap().domain;
assert_eq!(
res.dkim_should_work,
dkim_should_work(dkim_works_timestamp(&t, &from_domain).await?)?
);
assert_eq!(res.dkim_passed, res.dkim_should_work);
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
// These are (fictional) forged emails where the attacker added a fake
// Authentication-Results before sending the email
&& from != "forged-authres-added@example.com"
// Other forged emails
&& !from.starts_with("forged");
if res.dkim_passed != expected_result {
if authres_parsing_works {
println!(
"!!!!!! FAILURE Receiving {:?}, order {:#?} wrong result: !!!!!!",
entry.path(),
dir.iter().map(|e| e.file_name()).collect::<Vec<_>>()
);
test_failed = true;
}
println!("From {}: {}", from_domain, res.dkim_passed);
}
}
}
assert!(!test_failed);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres() {
let t = TestContext::new().await;
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
// return an Err because this would prevent the message from being added
// to the database and downloaded again and again
let bytes = b"Authentication-Results: dkim=";
let mail = mailparse::parse_mail(bytes).unwrap();
handle_authres(&t, &mail, "invalidfrom.com", time())
.await
.unwrap();
}
#[ignore = "Disallowing keychanges is disabled for now"]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Bob sends Alice a message, so she gets his key
tcm.send_recv_accept(&bob, &alice, "Hi").await;
// We don't need bob anymore, let's make sure it's not accidentally used
drop(bob);
// Assume Alice receives an email from bob@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&alice, "example.net", time()).await?;
// And Alice knows her server's authserv-id
alice
.set_config(Config::AuthservIdCandidates, Some("example.org"))
.await?;
tcm.section("An attacker, bob2, sends a from-forged email to Alice!");
// Sleep to make sure key reset is ignored because of DKIM failure
// and not because reordering is suspected.
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let bob2 = tcm.unconfigured().await;
bob2.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob2).await?;
let chat = bob2.create_chat(&alice).await;
let mut sent = bob2
.send_text(chat.id, "Please send me lots of money")
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
let received = alice.recv_msg(&sent).await;
// Assert that the error tells the user about the problem
assert!(received.error.unwrap().contains("DKIM failed"));
let bob_state = Peerstate::from_addr(&alice, "bob@example.net")
.await?
.unwrap();
// Encryption preference is still mutual.
assert_eq!(bob_state.prefer_encrypt, EncryptPreference::Mutual);
// Also check that the keypair was not changed
assert_eq!(
bob_state.public_key.unwrap(),
test_utils::bob_keypair().public
);
// Since Alice didn't change the key, Bob can't read her message
let received = tcm
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
.await;
assert!(!received.text.as_ref().unwrap().contains("1234"));
assert!(received.error.is_some());
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
tcm.section("To fix the key problems, Bob scans Alice's QR code.");
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
join_securejoin(&bob2.ctx, &qr).await.unwrap();
loop {
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
alice.recv_msg(&sent).await;
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
bob2.recv_msg(&sent).await;
} else {
break;
}
}
// Unfortunately, securejoin currently doesn't work with authres-checking,
// so these checks would fail:
// let contact_bob = alice.add_or_lookup_contact(&bob2).await;
// assert_eq!(
// contact_bob.is_verified(&alice.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// let contact_alice = bob2.add_or_lookup_contact(&alice).await;
// assert_eq!(
// contact_alice.is_verified(&bob2.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// // Bob can read Alice's messages again
// let received = tcm
// .try_send_recv(&alice, &bob2, "Can you read this again?")
// .await;
// assert_eq!(received.text.as_ref().unwrap(), "Can you read this again?");
// assert!(received.error.is_none());
Ok(())
}
}

View File

@@ -326,10 +326,7 @@ impl<'a> BlobObject<'a> {
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
if let Some(new_name) = self
.recode_to_size(context, blob_abs, img_wh, Some(20_000))
.await?
{
if let Some(new_name) = self.recode_to_size(context, blob_abs, img_wh, Some(20_000))? {
self.name = new_name;
}
Ok(())
@@ -352,8 +349,7 @@ impl<'a> BlobObject<'a> {
};
if self
.recode_to_size(context, blob_abs, img_wh, None)
.await?
.recode_to_size(context, blob_abs, img_wh, None)?
.is_some()
{
return Err(format_err!(
@@ -363,7 +359,7 @@ impl<'a> BlobObject<'a> {
Ok(())
}
async fn recode_to_size(
fn recode_to_size(
&self,
context: &Context,
mut blob_abs: PathBuf,
@@ -738,7 +734,7 @@ mod tests {
check_image_size(avatar_src, 1000, 1000);
check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, 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()
}
@@ -746,7 +742,6 @@ mod tests {
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
.await
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);

View File

@@ -40,7 +40,7 @@ use crate::webxdc::WEBXDC_SUFFIX;
use crate::{location, sql};
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ChatItem {
Message {
msg_id: MsgId,
@@ -1130,8 +1130,8 @@ impl Chat {
}
/// Returns mailing list address where messages are sent to.
pub fn get_mailinglist_addr(&self) -> &str {
self.param.get(Param::ListPost).unwrap_or_default()
pub fn get_mailinglist_addr(&self) -> Option<&str> {
self.param.get(Param::ListPost)
}
/// Returns profile image path for the chat.
@@ -2254,7 +2254,7 @@ pub async fn get_chat_msgs(
let curr_day = curr_local_timestamp / 86400;
if curr_day != last_day {
ret.push(ChatItem::DayMarker {
timestamp: curr_day,
timestamp: curr_day * 86400, // Convert day back to Unix timestamp
});
last_day = curr_day;
}
@@ -2423,7 +2423,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
pub async fn get_chat_media(
context: &Context,
chat_id: ChatId,
chat_id: Option<ChatId>,
msg_type: Viewtype,
msg_type2: Viewtype,
msg_type3: Viewtype,
@@ -2434,11 +2434,13 @@ pub async fn get_chat_media(
.query_map(
"SELECT id
FROM msgs
WHERE chat_id=?
WHERE (1=? OR chat_id=?)
AND (type=? OR type=? OR type=?)
AND hidden=0
ORDER BY timestamp, id;",
paramsv![
chat_id,
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
msg_type,
if msg_type2 != Viewtype::Unknown {
msg_type2
@@ -2479,7 +2481,7 @@ pub async fn get_next_media(
if let Ok(msg) = Message::load_from_db(context, curr_msg_id).await {
let list: Vec<MsgId> = get_chat_media(
context,
msg.chat_id,
Some(msg.chat_id),
if msg_type != Viewtype::Unknown {
msg_type
} else {
@@ -2523,7 +2525,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
LEFT JOIN contacts c
ON c.id=cc.contact_id
WHERE cc.chat_id=?
ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;",
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
paramsv![chat_id],
|row| row.get::<_, ContactId>(0),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
@@ -2786,7 +2788,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
Ok(needs_attach)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MuteDuration {
NotMuted,
Forever,
@@ -2861,7 +2863,7 @@ pub async fn remove_contact_from_chat(
let mut success = false;
/* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
if !chat.is_self_in_chat(context).await? {
@@ -3367,6 +3369,15 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
.sql
.execute("DELETE FROM devmsglabels;", paramsv![])
.await?;
// Insert labels for welcome messages to avoid them being readded on reconfiguration.
context
.sql
.execute(
r#"INSERT INTO devmsglabels (label) VALUES ("core-welcome-image"), ("core-welcome")"#,
paramsv![],
)
.await?;
context.set_config(Config::QuotaExceeding, None).await?;
Ok(())
}
@@ -3691,11 +3702,11 @@ mod tests {
// create group and sync it to the second device
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
a1.send_text(a1_chat_id, "ho!").await;
let sent = a1.send_text(a1_chat_id, "ho!").await;
let a1_msg = a1.get_last_msg().await;
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
let a2_msg = a2.recv_msg(&sent).await;
let a2_chat_id = a2_msg.chat_id;
let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?;
@@ -4552,26 +4563,24 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_protection() {
async fn test_set_protection() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
.await
.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
t.set_config_bool(Config::BccSelf, false).await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await
.unwrap();
.await?;
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
assert_eq!(msgs.len(), 1);
let msg = t.get_last_msg_in(chat_id).await;
@@ -4582,10 +4591,9 @@ mod tests {
// disable protection again, still unpromoted
chat_id
.set_protection(&t, ProtectionStatus::Unprotected)
.await
.unwrap();
.await?;
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
@@ -4595,21 +4603,20 @@ mod tests {
assert_eq!(msg.get_state(), MessageState::InNoticed);
// send a message, this switches to promoted state
send_text_msg(&t, chat_id, "hi!".to_string()).await.unwrap();
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(!chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
assert_eq!(msgs.len(), 3);
// enable protection on promoted chat, the info-message is sent via send_msg() this time
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await
.unwrap();
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(!chat.is_unpromoted());
@@ -4617,6 +4624,8 @@ mod tests {
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -5454,8 +5463,8 @@ mod tests {
assert_eq!(
chat_id.get_encryption_info(&alice).await?,
"No encryption:\n\
bob@example.net\n\
fiona@example.net"
fiona@example.net\n\
bob@example.net"
);
let direct_chat = bob.create_chat(&alice).await;
@@ -5486,4 +5495,158 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_chat_media() -> Result<()> {
let t = TestContext::new_alice().await;
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "bar").await?;
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Unknown
)
.await?
.len(),
0
);
async fn send_media(
t: &TestContext,
chat_id: ChatId,
msg_type: Viewtype,
name: &str,
bytes: &[u8],
) -> Result<MsgId> {
let file = t.get_blobdir().join(name);
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(msg_type);
msg.set_file(file.to_str().unwrap(), None);
send_msg(t, chat_id, &mut msg).await
}
send_media(
&t,
chat_id1,
Viewtype::Image,
"a.jpg",
include_bytes!("../test-data/image/rectangle200x180-rotated.jpg"),
)
.await?;
send_media(
&t,
chat_id1,
Viewtype::Sticker,
"b.png",
include_bytes!("../test-data/image/avatar64x64.png"),
)
.await?;
send_media(
&t,
chat_id2,
Viewtype::Image,
"c.jpg",
include_bytes!("../test-data/image/avatar64x64.png"),
)
.await?;
send_media(
&t,
chat_id2,
Viewtype::Webxdc,
"d.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc"),
)
.await?;
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Image,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?
.len(),
1
);
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Sticker,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?
.len(),
1
);
assert_eq!(
get_chat_media(
&t,
Some(chat_id1),
Viewtype::Sticker,
Viewtype::Image,
Viewtype::Unknown,
)
.await?
.len(),
2
);
assert_eq!(
get_chat_media(
&t,
Some(chat_id2),
Viewtype::Webxdc,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?
.len(),
1
);
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Unknown,
Viewtype::Unknown,
)
.await?
.len(),
2
);
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Unknown,
)
.await?
.len(),
3
);
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Webxdc,
)
.await?
.len(),
4
);
Ok(())
}
}

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
}

View File

@@ -1,7 +1,7 @@
//! # Key-value configuration management.
use anyhow::{ensure, Context as _, Result};
use strum::{EnumProperty, IntoEnumIterator};
use strum::{EnumProperty as EnumPropertyTrait, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use crate::blob::BlobObject;
@@ -55,7 +55,7 @@ pub enum Config {
Selfstatus,
Selfavatar,
#[strum(props(default = "0"))]
#[strum(props(default = "1"))]
BccSelf,
#[strum(props(default = "1"))]
@@ -184,6 +184,12 @@ pub enum Config {
/// In a future versions, this switch may be removed.
#[strum(props(default = "0"))]
SendSyncMsgs,
/// Space-separated list of all the authserv-ids which we believe
/// may be the one of our email server.
///
/// See `crate::authres::update_authservid_candidates`.
AuthservIdCandidates,
}
impl Context {
@@ -198,7 +204,7 @@ impl Context {
let rel_path = self.sql.get_raw_config(key).await?;
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key).await?,

View File

@@ -196,7 +196,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
// no oauth? - just continue it's no error
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
let parsed = EmailAddress::new(&param.addr).context("Bad email-address")?;
let param_domain = parsed.domain;
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
@@ -579,8 +579,7 @@ async fn try_imap_one_param(
let (_s, r) = async_channel::bounded(1);
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r).await
{
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
Err(err) => {
info!(context, "failure: {}", err);
return Err(ConfigurationError {

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