Compare commits

..

71 Commits

Author SHA1 Message Date
B. Petersen
35791c9d9c test that reactions are hidden messages 2022-10-18 22:36:48 +02:00
B. Petersen
d93c95e979 test REACTIONS_CHANGED event 2022-10-18 22:09:12 +02:00
link2xt
d8ced9fe87 Update deltachat-ffi/deltachat.h
Co-authored-by: bjoern <r10s@b44t.com>
2022-10-18 22:16:56 +03:00
link2xt
f75aad9a2d Update deltachat-ffi/deltachat.h
Co-authored-by: bjoern <r10s@b44t.com>
2022-10-18 22:16:41 +03:00
link2xt
fd3adb311f Update src/reaction.rs
Co-authored-by: bjoern <r10s@b44t.com>
2022-10-18 22:16:10 +03:00
link2xt
5cf364b55b Simplify saving original chat ID 2022-10-18 19:15:35 +00:00
B. Petersen
2e946a008f pass correct chat_id to REACTIONS_CHANGED event 2022-10-18 17:51:17 +02:00
B. Petersen
369e1a3ad7 do not create chats by reactions 2022-10-18 17:50:46 +02:00
link2xt
d34f9e23fe Implement reactions 2022-10-18 11:22:20 +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
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
55 changed files with 2960 additions and 517 deletions

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

@@ -3,6 +3,67 @@
## Unreleased
### API-Changes
- added reactions support #3644
### 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
- 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:
@@ -10,9 +71,6 @@
- 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
### Fixes
- jsonrpc: fix daymarkers
## 1.95.0
### API-Changes

125
Cargo.lock generated
View File

@@ -87,9 +87,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.63"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26fa4d7e3f2eebadf743988fc8aec9fa9a9e82611acafd77c1462ed6262440a"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]]
name = "ascii_utils"
@@ -236,9 +236,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.5.15"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de18bc5f2e9df8f52da03856bf40e29b747de5a84e43aefff90e3dc4a21529b"
checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043"
dependencies = [
"async-trait",
"axum-core",
@@ -270,9 +270,9 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4f44a0e6200e9d11a1cdc989e4b358f6e3d354fbf48478f345a17f4e43f8635"
checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b"
dependencies = [
"async-trait",
"bytes",
@@ -280,6 +280,8 @@ dependencies = [
"http",
"http-body",
"mime",
"tower-layer",
"tower-service",
]
[[package]]
@@ -893,7 +895,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.95.0"
version = "1.97.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -946,13 +948,13 @@ dependencies = [
"serde",
"serde_json",
"sha-1 0.10.0",
"sha2 0.10.3",
"sha2 0.10.6",
"smallvec",
"strum",
"strum_macros",
"tagger",
"tempfile",
"textwrap 0.15.0",
"textwrap 0.15.1",
"thiserror",
"tokio",
"tokio-stream",
@@ -965,13 +967,13 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.95.0"
version = "1.97.0"
dependencies = [
"anyhow",
"async-channel",
"axum",
"deltachat",
"env_logger 0.9.0",
"env_logger 0.9.1",
"futures",
"log",
"num-traits",
@@ -980,6 +982,7 @@ dependencies = [
"tempfile",
"tokio",
"typescript-type-def",
"walkdir",
"yerpc",
]
@@ -993,7 +996,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.95.0"
version = "1.97.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1066,9 +1069,9 @@ dependencies = [
[[package]]
name = "digest"
version = "0.10.3"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
dependencies = [
"block-buffer 0.10.2",
"crypto-common",
@@ -1260,9 +1263,9 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "enum-as-inner"
version = "0.4.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73"
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
dependencies = [
"heck",
"proc-macro2",
@@ -1285,9 +1288,9 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272"
dependencies = [
"atty",
"humantime 2.1.0",
@@ -1827,9 +1830,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.3"
version = "0.24.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964"
checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c"
dependencies = [
"bytemuck",
"byteorder",
@@ -1979,9 +1982,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.132"
version = "0.2.134"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]]
name = "libm"
@@ -2285,9 +2288,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.13.1"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "oorandom"
@@ -2428,9 +2431,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pgp"
@@ -2623,9 +2626,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.43"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
dependencies = [
"unicode-ident",
]
@@ -2878,9 +2881,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.11.11"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92"
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
dependencies = [
"base64 0.13.0",
"bytes",
@@ -2894,10 +2897,10 @@ dependencies = [
"hyper-tls",
"ipnet",
"js-sys",
"lazy_static",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
@@ -2941,7 +2944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b"
dependencies = [
"byteorder",
"digest 0.10.3",
"digest 0.10.5",
"num-bigint-dig",
"num-integer",
"num-iter",
@@ -3105,9 +3108,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.144"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [
"serde_derive",
]
@@ -3124,9 +3127,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.144"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
"proc-macro2",
"quote",
@@ -3177,7 +3180,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.3",
"digest 0.10.5",
]
[[package]]
@@ -3195,13 +3198,13 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.10.3"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899bf02746a2c92bf1053d9327dadb252b01af1f81f90cdb902411f518bc7215"
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.3",
"digest 0.10.5",
]
[[package]]
@@ -3341,9 +3344,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.99"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
dependencies = [
"proc-macro2",
"quote",
@@ -3408,9 +3411,9 @@ dependencies = [
[[package]]
name = "textwrap"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
dependencies = [
"smawk",
"unicode-linebreak",
@@ -3419,18 +3422,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.33"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.33"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [
"proc-macro2",
"quote",
@@ -3475,9 +3478,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.20.1"
version = "1.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581"
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
dependencies = [
"autocfg",
"bytes",
@@ -3485,7 +3488,6 @@ dependencies = [
"memchr",
"mio",
"num_cpus",
"once_cell",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@@ -3517,9 +3519,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9"
checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -3658,9 +3660,9 @@ dependencies = [
[[package]]
name = "trust-dns-proto"
version = "0.21.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d"
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
dependencies = [
"async-trait",
"cfg-if",
@@ -3672,32 +3674,32 @@ dependencies = [
"idna",
"ipnet",
"lazy_static",
"log",
"rand 0.8.5",
"smallvec",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "trust-dns-resolver"
version = "0.21.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558"
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lazy_static",
"log",
"lru-cache",
"parking_lot",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
"trust-dns-proto",
]
@@ -3817,13 +3819,12 @@ checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "url"
version = "2.2.2"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
checksum = "22fe195a4f217c25b25cb5058ced57059824a678474874038dc88d211bf508d3"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.95.0"
version = "1.97.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
@@ -23,7 +23,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"
@@ -36,7 +36,7 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch =
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.24.3", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.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,8 +46,8 @@ native-tls = "0.2"
num_cpus = "1.13"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.13.1"
percent-encoding = "2.0"
once_cell = "1.15.0"
percent-encoding = "2.2"
pgp = { version = "0.8", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.23"
@@ -74,11 +74,11 @@ fast-socks5 = "0.8"
humansize = "1"
qrcodegen = "1.7.0"
tagger = "4.3.3"
textwrap = "0.15.0"
textwrap = "0.15.1"
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.10", 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]

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.95.0"
version = "1.97.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -25,7 +25,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.7"
once_cell = "1.13.1"
once_cell = "1.15.0"
[features]
default = ["vendored"]

View File

@@ -11,16 +11,17 @@ 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_jsonrpc_instance dc_jsonrpc_instance_t;
@@ -991,6 +992,36 @@ 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.
* If there are no reactions, still a structure is returned and dc_reactions_get_contacts() will return an empty array.
* On errors, NULL is returned.
*/
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.
*
@@ -4882,7 +4913,51 @@ 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.
* If there are no reactions, the returned array will be empty.
* On errors, NULL is returned.
*/
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);
/**
@@ -5533,6 +5608,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.

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

@@ -37,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};
@@ -65,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
@@ -98,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();
@@ -122,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);
@@ -487,6 +501,7 @@ 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::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
@@ -531,6 +546,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SelfavatarChanged
| EventType::ErrorSelfNotInGroup(_) => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
| EventType::MsgsNoticed(chat_id)
| EventType::MsgDelivered { chat_id, .. }
@@ -587,6 +603,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| 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, .. }
@@ -626,6 +643,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 { .. }
@@ -937,6 +955,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,
@@ -3977,6 +4037,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 _)
@@ -4441,11 +4540,13 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
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};
pub struct dc_jsonrpc_instance_t {
receiver: OutReceiver,
handle: RpcSession<CommandApi>,
event_thread: JoinHandle<Result<(), anyhow::Error>>,
}
#[no_mangle]
@@ -4461,9 +4562,36 @@ mod jsonrpc {
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 instance = dc_jsonrpc_instance_t { receiver, handle };
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))
}
@@ -4474,8 +4602,8 @@ mod jsonrpc {
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
return;
}
Box::from_raw(jsonrpc_instance);
(*jsonrpc_instance).event_thread.abort();
drop(Box::from_raw(jsonrpc_instance));
}
#[no_mangle]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.95.0"
version = "1.97.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
@@ -24,14 +24,15 @@ futures = { version = "0.3.24" }
serde_json = "1.0.85"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.19.2" }
tokio = { version = "1.21.2" }
# optional dependencies
axum = { version = "0.5.9", optional = true, features = ["ws"] }
env_logger = { version = "0.9.0", optional = true }
axum = { version = "0.5.16", optional = true, features = ["ws"] }
env_logger = { version = "0.9.1", optional = true }
walkdir = "2.3.2"
[dev-dependencies]
tokio = { version = "1.19.2", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.21.2", features = ["full", "rt-multi-thread"] }
[features]

View File

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

View File

@@ -2,24 +2,30 @@ use anyhow::{anyhow, bail, Context, Result};
use deltachat::{
chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat,
remove_contact_from_chat, Chat, ChatId, ChatItem,
remove_contact_from_chat, Chat, ChatId, ChatItem, ProtectionStatus,
},
chatlist::Chatlist,
config::Config,
constants::DC_MSG_ID_DAYMARKER,
contact::{may_be_valid_addr, Contact, ContactId},
contact::{may_be_valid_addr, Contact, ContactId, Origin},
context::get_info,
message::{delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype},
ephemeral::Timer,
imex, location,
message::{
self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype,
},
provider::get_provider_info,
qr,
qr_code_generator::get_securejoin_qr_svg,
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};
use tokio::sync::RwLock;
use tokio::{fs, sync::RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
pub use deltachat::accounts::Accounts;
@@ -39,10 +45,15 @@ use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo;
use self::types::{
chat::{BasicChat, MuteDuration},
message::{MessageNotificationInfo, MessageViewtype},
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
location::JsonrpcLocation,
message::{
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
},
};
use num_traits::FromPrimitive;
#[derive(Clone, Debug)]
pub struct CommandApi {
pub(crate) accounts: Arc<RwLock<Accounts>>,
@@ -129,10 +140,32 @@ impl CommandApi {
Ok(accounts)
}
async fn start_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.start_io().await;
Ok(())
}
async fn stop_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.stop_io().await;
Ok(())
}
// ---------------------------------------------
// Methods that work on individual accounts
// ---------------------------------------------
async fn start_io(&self, id: u32) -> Result<()> {
let ctx = self.get_context(id).await?;
ctx.start_io().await;
Ok(())
}
async fn stop_io(&self, id: u32) -> Result<()> {
let ctx = self.get_context(id).await?;
ctx.stop_io().await;
Ok(())
}
/// Get top-level info for an account.
async fn get_account_info(&self, account_id: u32) -> Result<Account> {
let context_option = self.accounts.read().await.get_account(account_id);
@@ -146,6 +179,21 @@ impl CommandApi {
}
}
/// Get the combined filesize of an account in bytes
async fn get_account_file_size(&self, account_id: u32) -> Result<u64> {
let ctx = self.get_context(account_id).await?;
let dbfile = ctx.get_dbfile().metadata()?.len();
let total_size = WalkDir::new(ctx.get_blobdir())
.max_depth(2)
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.metadata().ok())
.filter(|metadata| metadata.is_file())
.fold(0, |acc, m| acc + m.len());
Ok(dbfile + total_size)
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database.
@@ -234,6 +282,18 @@ impl CommandApi {
Ok(result)
}
async fn set_stock_strings(&self, strings: HashMap<u32, String>) -> Result<()> {
let accounts = self.accounts.read().await;
for (stock_id, stock_message) in strings {
if let Some(stock_id) = StockMessage::from_u32(stock_id) {
accounts
.set_stock_translation(stock_id, stock_message)
.await?;
}
}
Ok(())
}
/// Configures this account with the currently set parameters.
/// Setup the credential config before calling this.
async fn configure(&self, account_id: u32) -> Result<()> {
@@ -257,6 +317,38 @@ impl CommandApi {
Ok(())
}
async fn export_self_keys(
&self,
account_id: u32,
path: String,
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
imex::imex(
&ctx,
imex::ImexMode::ExportSelfKeys,
path.as_ref(),
passphrase,
)
.await
}
async fn import_self_keys(
&self,
account_id: u32,
path: String,
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
imex::imex(
&ctx,
imex::ImexMode::ImportSelfKeys,
path.as_ref(),
passphrase,
)
.await
}
/// 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.
@@ -289,6 +381,20 @@ impl CommandApi {
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
}
/// 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.
async fn estimate_auto_deletion_count(
&self,
account_id: u32,
from_server: bool,
seconds: i64,
) -> Result<usize> {
let ctx = self.get_context(account_id).await?;
message::estimate_deletion_cnt(&ctx, from_server, seconds).await
}
// ---------------------------------------------
// autocrypt
// ---------------------------------------------
@@ -408,9 +514,7 @@ impl CommandApi {
/// 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 dc_remove_contact_from_chat() with
/// chat_id=DC_CONTACT_ID_SELF)
// TODO fix doc comment after adding dc_remove_contact_from_chat
/// To leave a chat explicitly, use leave_group()
async fn delete_chat(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id).delete(&ctx).await
@@ -432,7 +536,7 @@ impl CommandApi {
///
/// 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 dc_join_securejoin()
/// 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;
@@ -442,7 +546,6 @@ impl CommandApi {
/// for details about both protocols.
///
/// return format: `[code, svg]`
// TODO fix doc comment after adding dc_join_securejoin
async fn get_chat_securejoin_qr_code_svg(
&self,
account_id: u32,
@@ -456,6 +559,33 @@ impl CommandApi {
))
}
/// 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.
///
async fn secure_join(&self, account_id: u32, qr: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let chat_id = securejoin::join_securejoin(&ctx, &qr).await?;
Ok(chat_id.to_u32())
}
async fn leave_group(&self, account_id: u32, chat_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
remove_contact_from_chat(&ctx, ChatId::new(chat_id), ContactId::SELF).await
@@ -495,6 +625,153 @@ impl CommandApi {
add_contact_to_chat(&ctx, ChatId::new(chat_id), ContactId::new(contact_id)).await
}
/// 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)
async fn get_chat_contacts(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let contacts = chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await?;
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<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.
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_chat(&ctx, protect, &name)
.await
.map(|id| id.to_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.
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast_list(&ctx)
.await
.map(|id| id.to_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.
async fn set_chat_name(&self, account_id: u32, chat_id: u32, new_name: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
chat::set_chat_name(&ctx, ChatId::new(chat_id), &new_name).await
}
/// 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).
async fn set_chat_profile_image(
&self,
account_id: u32,
chat_id: u32,
image_path: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), image_path.unwrap_or_default())
.await
}
async fn set_chat_visibility(
&self,
account_id: u32,
chat_id: u32,
visibility: JSONRPCChatVisibility,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id)
.set_visibility(&ctx, visibility.into_core_type())
.await
}
async fn set_chat_ephemeral_timer(
&self,
account_id: u32,
chat_id: u32,
timer: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ChatId::new(chat_id)
.set_ephemeral_timer(&ctx, Timer::from_u32(timer))
.await
}
async fn get_chat_ephemeral_timer(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ChatId::new(chat_id)
.get_ephemeral_timer(&ctx)
.await?
.to_u32())
}
// for now only text messages, because we only used text messages in desktop thusfar
async fn add_device_message(
&self,
@@ -605,32 +882,44 @@ impl CommandApi {
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
}
/// get ids of messages for a chat
/// (this api is similar to the cffi api, because it's still used that way by desktop)
///
/// returns list with message ids and daymarkers (DC_MSG_ID_DAYMARKER = 9) if daymarkers are active
async fn message_list_get_message_ids(
&self,
account_id: u32,
chat_id: u32,
flags: u32,
) -> Result<Vec<u32>> {
async fn get_message_ids(&self, account_id: u32, chat_id: u32, flags: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs(&ctx, ChatId::new(chat_id), flags).await?;
Ok(msg
.iter()
.map(|chat_item| match chat_item {
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
.map(|chat_item| -> u32 {
match chat_item {
deltachat::chat::ChatItem::Message { msg_id } => msg_id.to_u32(),
deltachat::chat::ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
}
})
.collect())
}
async fn get_message_list_items(
&self,
account_id: u32,
chat_id: u32,
flags: u32,
) -> Result<Vec<JSONRPCMessageListItem>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs(&ctx, ChatId::new(chat_id), flags).await?;
Ok(msg
.iter()
.map(|chat_item| (*chat_item).into())
.collect::<Vec<JSONRPCMessageListItem>>())
}
async fn message_get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
MessageObject::from_message_id(&ctx, message_id).await
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
MsgId::new(message_id).get_html(&ctx).await
}
async fn message_get_messages(
&self,
account_id: u32,
@@ -675,6 +964,65 @@ impl CommandApi {
get_msg_info(&ctx, MsgId::new(message_id)).await
}
/// 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.
async fn download_full_message(&self, account_id: u32, message_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
MsgId::new(message_id).download_full(&ctx).await
}
/// 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.
async fn search_messages(
&self,
account_id: u32,
query: String,
chat_id: Option<u32>,
) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let messages = ctx.search_msgs(chat_id.map(ChatId::new), &query).await?;
Ok(messages
.iter()
.map(|msg_id| msg_id.to_u32())
.collect::<Vec<u32>>())
}
async fn message_ids_to_search_results(
&self,
account_id: u32,
message_ids: Vec<u32>,
) -> Result<HashMap<u32, MessageSearchResult>> {
let ctx = self.get_context(account_id).await?;
let mut results: HashMap<u32, MessageSearchResult> =
HashMap::with_capacity(message_ids.len());
for id in message_ids {
results.insert(
id,
MessageSearchResult::from_msg_id(&ctx, MsgId::new(id)).await?,
);
}
Ok(results)
}
// ---------------------------------------------
// contact
// ---------------------------------------------
@@ -808,6 +1156,28 @@ impl CommandApi {
Ok(contacts)
}
async fn delete_contact(&self, account_id: u32, contact_id: u32) -> Result<bool> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
Contact::delete(&ctx, contact_id).await?;
Ok(true)
}
async fn change_contact_name(
&self,
account_id: u32,
contact_id: u32,
name: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
let contact = Contact::load_from_db(&ctx, contact_id).await?;
let addr = contact.get_addr();
Contact::create(&ctx, &name, addr).await?;
Ok(())
}
/// 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.
@@ -820,6 +1190,21 @@ impl CommandApi {
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
}
/// 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().
async fn lookup_contact_id_by_addr(
&self,
account_id: u32,
addr: String,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
let contact_id = Contact::lookup_id_by_addr(&ctx, &addr, Origin::IncomingReplyTo).await?;
Ok(contact_id.map(|id| id.to_u32()))
}
// ---------------------------------------------
// chat
// ---------------------------------------------
@@ -900,6 +1285,45 @@ impl CommandApi {
Ok((prev, next))
}
// ---------------------------------------------
// backup
// ---------------------------------------------
async fn export_backup(
&self,
account_id: u32,
destination: String,
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
let result = imex::imex(
&ctx,
imex::ImexMode::ExportBackup,
destination.as_ref(),
passphrase,
)
.await;
ctx.start_io().await;
result
}
async fn import_backup(
&self,
account_id: u32,
path: String,
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
imex::imex(
&ctx,
imex::ImexMode::ImportBackup,
path.as_ref(),
passphrase,
)
.await
}
// ---------------------------------------------
// connectivity
// ---------------------------------------------
@@ -944,6 +1368,32 @@ impl CommandApi {
ctx.get_connectivity_html().await
}
// ---------------------------------------------
// locations
// ---------------------------------------------
async fn get_locations(
&self,
account_id: u32,
chat_id: Option<u32>,
contact_id: Option<u32>,
timestamp_begin: i64,
timestamp_end: i64,
) -> Result<Vec<JsonrpcLocation>> {
let ctx = self.get_context(account_id).await?;
let locations = location::get_range(
&ctx,
chat_id.map(ChatId::new),
contact_id,
timestamp_begin,
timestamp_end,
)
.await?;
Ok(locations.into_iter().map(|l| l.into()).collect())
}
// ---------------------------------------------
// webxdc
// ---------------------------------------------
@@ -1001,6 +1451,21 @@ impl CommandApi {
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
async fn send_sticker(
&self,
account_id: u32,
chat_id: u32,
sticker_path: String,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file(&sticker_path, None);
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
@@ -1023,11 +1488,75 @@ impl CommandApi {
}
}
async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
}
// ---------------------------------------------
// misc prototyping functions
// that might get removed later again
// ---------------------------------------------
async fn misc_get_sticker_folder(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
let sticker_folder_path = account_folder.join("./stickers");
fs::create_dir_all(&sticker_folder_path).await?;
sticker_folder_path
.to_str()
.map(|s| s.to_owned())
.context("path conversion to string failed")
}
/// for desktop, get stickers from stickers folder,
/// grouped by the folder they are in.
async fn misc_get_stickers(&self, account_id: u32) -> Result<HashMap<String, Vec<String>>> {
let ctx = self.get_context(account_id).await?;
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
let sticker_folder_path = account_folder.join("./stickers");
fs::create_dir_all(&sticker_folder_path).await?;
let mut result = HashMap::new();
let mut packs = tokio::fs::read_dir(sticker_folder_path).await?;
while let Some(entry) = packs.next_entry().await? {
if !entry.file_type().await?.is_dir() {
continue;
}
let pack_name = entry.file_name().into_string().unwrap_or_default();
let mut stickers = tokio::fs::read_dir(entry.path()).await?;
let mut sticker_paths = Vec::new();
while let Some(sticker_entry) = stickers.next_entry().await? {
if !sticker_entry.file_type().await?.is_file() {
continue;
}
let sticker_name = sticker_entry.file_name().into_string().unwrap_or_default();
if sticker_name.ends_with(".png") || sticker_name.ends_with(".webp") {
sticker_paths.push(
sticker_entry
.path()
.to_str()
.map(|s| s.to_owned())
.context("path conversion to string failed")?,
);
}
}
if !sticker_paths.is_empty() {
result.insert(pack_name, sticker_paths);
}
}
Ok(result)
}
/// Returns the messageid of the sent message
async fn misc_send_text_message(
&self,

View File

@@ -1,7 +1,7 @@
use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Result};
use deltachat::chat::{self, get_chat_contacts};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};
@@ -193,3 +193,21 @@ impl MuteDuration {
}
}
}
#[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,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

@@ -1,5 +1,7 @@
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;
@@ -79,6 +81,7 @@ enum MessageQuote {
override_sender_name: Option<String>,
image: Option<String>,
is_forwarded: bool,
view_type: MessageViewtype,
},
}
@@ -127,6 +130,7 @@ impl MessageObject {
None
},
is_forwarded: quote.is_forwarded(),
view_type: quote.get_viewtype().into(),
})
}
None => Some(MessageQuote::JustText { text: quoted_text }),
@@ -201,6 +205,9 @@ pub enum MessageViewtype {
/// Text message.
Text,
/// Reaction emoji.
Reaction,
/// Image message.
/// If the image is an animated GIF, the type `Viewtype.Gif` should be used.
Image,
@@ -238,6 +245,7 @@ impl From<Viewtype> for MessageViewtype {
match viewtype {
Viewtype::Unknown => MessageViewtype::Unknown,
Viewtype::Text => MessageViewtype::Text,
Viewtype::Reaction => MessageViewtype::Reaction,
Viewtype::Image => MessageViewtype::Image,
Viewtype::Gif => MessageViewtype::Gif,
Viewtype::Sticker => MessageViewtype::Sticker,
@@ -256,6 +264,7 @@ impl From<MessageViewtype> for Viewtype {
match viewtype {
MessageViewtype::Unknown => Viewtype::Unknown,
MessageViewtype::Text => Viewtype::Text,
MessageViewtype::Reaction => Viewtype::Reaction,
MessageViewtype::Image => Viewtype::Image,
MessageViewtype::Gif => Viewtype::Gif,
MessageViewtype::Sticker => Viewtype::Sticker,
@@ -345,3 +354,68 @@ impl MessageNotificationInfo {
})
}
}
#[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

@@ -6,6 +6,7 @@ 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 webxdc;

View File

@@ -1,4 +1,4 @@
import { DeltaChat, DeltaChatEvent } from "../deltachat.js";
import { DcEvent, DeltaChat } from "../deltachat.js";
var SELECTED_ACCOUNT = 0;
@@ -7,7 +7,7 @@ window.addEventListener("DOMContentLoaded", (_event) => {
SELECTED_ACCOUNT = Number(id);
window.dispatchEvent(new Event("account-changed"));
};
console.log('launch run script...')
console.log("launch run script...");
run().catch((err) => console.error("run failed", err));
});
@@ -16,13 +16,13 @@ async function run() {
const $side = document.getElementById("side")!;
const $head = document.getElementById("header")!;
const client = new DeltaChat('ws://localhost:20808/ws')
const client = new DeltaChat("ws://localhost:20808/ws");
;(window as any).client = client.rpc;
(window as any).client = client.rpc;
client.on("ALL", event => {
onIncomingEvent(event)
})
client.on("ALL", (accountId, event) => {
onIncomingEvent(accountId, event);
});
window.addEventListener("account-changed", async (_event: Event) => {
listChatsForSelectedAccount();
@@ -31,9 +31,9 @@ async function run() {
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
async function loadAccountsInHeader() {
console.log('load accounts')
console.log("load accounts");
const accounts = await client.rpc.getAllAccounts();
console.log('accounts loaded', accounts)
console.log("accounts loaded", accounts);
for (const account of accounts) {
if (account.type === "Configured") {
write(
@@ -48,14 +48,14 @@ async function run() {
`<a href="#">
${account.id}: (unconfigured)
</a>&nbsp;`
)
);
}
}
}
async function listChatsForSelectedAccount() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT
const selectedAccount = SELECTED_ACCOUNT;
const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.type !== "Configured") {
return write($main, "Account is not configured");
@@ -73,7 +73,7 @@ async function run() {
chatId
);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.messageListGetMessageIds(
const messageIds = await client.rpc.getMessageIds(
selectedAccount,
chatId,
0
@@ -88,14 +88,15 @@ async function run() {
}
}
function onIncomingEvent(event: DeltaChatEvent) {
function onIncomingEvent(accountId: number, event: DcEvent) {
write(
$side,
`
<p class="message">
[<strong>${event.id}</strong> on account ${event.contextId}]<br>
<em>f1:</em> ${JSON.stringify(event.field1)}<br>
<em>f2:</em> ${JSON.stringify(event.field2)}
[<strong>${event.type}</strong> on account ${accountId}]<br>
<em>f1:</em> ${JSON.stringify(
Object.assign({}, event, { type: undefined })
)}
</p>`
);
}

View File

@@ -66,6 +66,26 @@ export class RawClient {
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.
*/
@@ -73,6 +93,13 @@ export class RawClient {
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.
*
@@ -135,6 +162,11 @@ export class RawClient {
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.
@@ -150,6 +182,16 @@ export class RawClient {
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
@@ -179,6 +221,16 @@ export class RawClient {
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 autocryptInitiateKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('autocrypt_initiate_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
@@ -240,8 +292,7 @@ export class RawClient {
* 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 dc_remove_contact_from_chat() with
* chat_id=DC_CONTACT_ID_SELF)
* 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>;
@@ -265,7 +316,7 @@ export class RawClient {
*
* 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 dc_join_securejoin()
* 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;
@@ -280,6 +331,33 @@ export class RawClient {
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>;
@@ -311,6 +389,125 @@ export class RawClient {
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>;
@@ -385,14 +582,14 @@ export class RawClient {
return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise<null>;
}
/**
* get ids of messages for a chat
* (this api is similar to the cffi api, because it's still used that way by desktop)
*
* returns list with message ids and daymarkers (DC_MSG_ID_DAYMARKER = 9) if daymarkers are active
*/
public messageListGetMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('message_list_get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
public 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)[]>;
}
@@ -401,6 +598,11 @@ export class RawClient {
}
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 messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
@@ -431,6 +633,46 @@ export class RawClient {
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.
*/
@@ -487,6 +729,16 @@ export class RawClient {
return (this._transport.request('contacts_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
@@ -496,6 +748,17 @@ export class RawClient {
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.
@@ -523,6 +786,16 @@ export class RawClient {
return (this._transport.request('chat_get_neighboring_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
@@ -567,6 +840,11 @@ export class RawClient {
}
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 webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
}
@@ -596,6 +874,11 @@ export class RawClient {
}
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>;
}
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
@@ -607,6 +890,24 @@ export class RawClient {
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
*/

View File

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

@@ -2,11 +2,12 @@
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 ChatListEntry=[U32,U32];
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
@@ -49,8 +50,18 @@ export type BasicChat=
* 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 MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;}));
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.
@@ -96,8 +107,8 @@ export type Viewtype=("Unknown"|
* 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 I32=number;
export type U64=number;
export type WebxdcMessageInfo={
/**
* The name of the app.
@@ -146,5 +157,7 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
* 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 __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,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,Message,U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,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,string,U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],null,U32,U32,U32,string,U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,null,U32,U32,(Message|null),U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
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,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,string,U32,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

@@ -47,5 +47,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.95.0"
"version": "1.97.0"
}

View File

@@ -1,40 +1,57 @@
import * as T from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { EventTypeName } from "../generated/events.js";
import { Event } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "tiny-emitter";
export type DeltaChatEvent = {
id: EventTypeName;
type DCWireEvent<T extends Event> = {
event: T;
contextId: number;
field1: any;
field2: any;
};
export type Events = Record<
EventTypeName | "ALL",
(event: DeltaChatEvent) => void
>;
// 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<Events>[] = [];
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 DeltaChatEvent;
this.emit(event.id, event);
this.emit("ALL", 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.id, event);
this.contextEmitters[event.contextId].emit("ALL", event);
this.contextEmitters[event.contextId].emit(
event.event.type,
event.event as any
);
this.contextEmitters[event.contextId].emit("ALL", event.event);
}
}
});
@@ -70,7 +87,7 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
if (typeof opts === "string") opts = { url: opts };
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
else opts = { ...DEFAULT_OPTS };
const transport = new WebsocketTransport(opts.url)
const transport = new WebsocketTransport(opts.url);
super(transport);
this.opts = opts;
}

View File

@@ -1,12 +1,8 @@
import { assert, expect } from "chai";
import { DeltaChat, DeltaChatEvent, EventTypeName } from "../deltachat.js";
import {
RpcServerHandle,
createTempUser,
startServer,
} from "./test_base.js";
import { DeltaChat, DcEvent } from "../deltachat.js";
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
const EVENT_TIMEOUT = 20000
const EVENT_TIMEOUT = 20000;
describe("online tests", function () {
let serverHandle: RpcServerHandle;
@@ -16,7 +12,7 @@ describe("online tests", function () {
let accountId1: number, accountId2: number;
before(async function () {
this.timeout(12000)
this.timeout(12000);
if (!process.env.DCC_NEW_TMP_EMAIL) {
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
console.error(
@@ -31,10 +27,10 @@ describe("online tests", function () {
this.skip();
}
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.url)
dc = new DeltaChat(serverHandle.url);
dc.on("ALL", ({ id, contextId }) => {
if (id !== "Info") console.log(contextId, id);
dc.on("ALL", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
@@ -74,7 +70,7 @@ describe("online tests", function () {
addr: account2.email,
mail_pw: account2.password,
});
await dc.rpc.configure(accountId2)
await dc.rpc.configure(accountId2);
accountsConfigured = true;
});
@@ -89,16 +85,19 @@ describe("online tests", function () {
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const chatId = await dc.rpc.contactsCreateChatByContactId(
accountId1,
contactId
);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId);
const { field1: chatIdOnAccountB } = await eventPromise;
const { chatId: chatIdOnAccountB } = await eventPromise;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
const messageList = await dc.rpc.getMessageIds(
accountId2,
chatIdOnAccountB,
0
@@ -121,7 +120,10 @@ describe("online tests", function () {
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
const chatId = await dc.rpc.contactsCreateChatByContactId(
accountId1,
contactId
);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
@@ -131,10 +133,10 @@ describe("online tests", function () {
console.log("wait for message from A");
const event = await eventPromise;
const { field1: chatIdOnAccountB } = event;
const { chatId: chatIdOnAccountB } = event;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.messageListGetMessageIds(
const messageList = await dc.rpc.getMessageIds(
accountId2,
chatIdOnAccountB,
0
@@ -154,7 +156,7 @@ describe("online tests", function () {
await eventPromise2;
const messageId = (
await dc.rpc.messageListGetMessageIds(accountId1, chatId, 0)
await dc.rpc.getMessageIds(accountId1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
expect(message2.text).equal("super secret message");
@@ -179,22 +181,22 @@ describe("online tests", function () {
});
});
async function waitForEvent(
async function waitForEvent<T extends DcEvent["type"]>(
dc: DeltaChat,
eventType: EventTypeName,
eventType: T,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<DeltaChatEvent> {
): Promise<Extract<DcEvent, { type: T }>> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error('Timeout reached before event came in')),
() => reject(new Error("Timeout reached before event came in")),
timeout
)
const callback = (event: DeltaChatEvent) => {
if (event.contextId == accountId) {
);
const callback = (contextId: number, event: DcEvent) => {
if (contextId == accountId) {
dc.off(eventType, callback);
clearTimeout(rejectTimeout)
resolve(event);
clearTimeout(rejectTimeout);
resolve(event as any);
}
};
dc.on(eventType, callback);

View File

@@ -20,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::*;
@@ -407,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\
@@ -1121,6 +1123,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,6 +20,7 @@ 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};
@@ -71,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()"));
}
@@ -207,7 +221,7 @@ const CHAT_COMMANDS: [&str; 36] = [
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 8] = [
const MESSAGE_COMMANDS: [&str; 9] = [
"listmsgs",
"msginfo",
"listfresh",
@@ -216,6 +230,7 @@ const MESSAGE_COMMANDS: [&str; 8] = [
"markseen",
"delmsg",
"download",
"react",
];
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
@@ -298,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 {

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

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

View File

@@ -14,6 +14,7 @@ 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',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',

View File

@@ -50,6 +50,7 @@ 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,
@@ -282,6 +283,7 @@ 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',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',

View File

@@ -89,7 +89,11 @@ describe('JSON RPC', function () {
const { dc } = DeltaChat.newTemporary()
let promise_resolve
const promise = new Promise((res, _rej) => {
promise_resolve = res
promise_resolve = (response) => {
// ignore events
const answer = JSON.parse(response)
if (answer['method'] !== 'event') res(answer)
}
})
dc.startJsonRpcHandler(promise_resolve)
dc.jsonRpcRequest(
@@ -106,7 +110,7 @@ describe('JSON RPC', function () {
id: 2,
result: [1],
},
JSON.parse(await promise)
await promise
)
dc.close()
})

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.95.0"
"version": "1.97.0"
}

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 {
@@ -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,6 +73,7 @@ impl Accounts {
config,
accounts,
events,
stockstrings,
})
}
@@ -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);
@@ -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)
}
@@ -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);
}
@@ -446,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();
@@ -522,7 +555,7 @@ mod tests {
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"))
@@ -717,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(())
}
}

View File

@@ -1812,7 +1812,10 @@ pub async fn prepare_msg(context: &Context, chat_id: ChatId, msg: &mut Message)
}
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
if msg.viewtype == Viewtype::Text
|| msg.viewtype == Viewtype::VideochatInvitation
|| msg.viewtype == Viewtype::Reaction
{
// the caller should check if the message text is empty
} else if msg.viewtype.has_file() {
let blob = msg
@@ -2254,7 +2257,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;
}
@@ -3369,6 +3372,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(())
}

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();

View File

@@ -34,7 +34,9 @@ const SEEN_RECENTLY_SECONDS: i64 = 600;
///
/// Some contact IDs are reserved to identify special contacts. This
/// type can represent both the special as well as normal contacts.
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct ContactId(u32);
impl ContactId {
@@ -506,9 +508,9 @@ impl Contact {
let mut update_addr = false;
let mut row_id = 0;
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
if let Some((id, row_name, row_addr, row_origin, row_authname)) = context
.sql
.query_row(
.query_row_optional(
"SELECT id, name, addr, origin, authname \
FROM contacts WHERE addr=? COLLATE NOCASE;",
paramsv![addr.to_string()],
@@ -522,7 +524,7 @@ impl Contact {
Ok((row_id, row_name, row_addr, row_origin, row_authname))
},
)
.await
.await?
{
let update_name = manual && name != row_name;
let update_authname = !manual
@@ -1183,7 +1185,7 @@ impl Contact {
/// Returns false if addr is an invalid address, otherwise true.
pub fn may_be_valid_addr(addr: &str) -> bool {
let res = addr.parse::<EmailAddress>();
let res = EmailAddress::new(addr);
res.is_ok()
}

View File

@@ -23,6 +23,7 @@ use crate::quota::QuotaInfo;
use crate::ratelimit::Ratelimit;
use crate::scheduler::Scheduler;
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::tools::{duration_to_str, time};
#[derive(Clone, Debug)]
@@ -51,7 +52,7 @@ pub struct InnerContext {
pub(crate) oauth2_mutex: Mutex<()>,
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messeges being sent.
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
pub(crate) translated_stockstrings: StockStrings,
pub(crate) events: Events,
pub(crate) scheduler: RwLock<Option<Scheduler>>,
@@ -119,8 +120,13 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
impl Context {
/// Creates new context and opens the database.
pub async fn new(dbfile: &Path, id: u32, events: Events) -> Result<Context> {
let context = Self::new_closed(dbfile, id, events).await?;
pub async fn new(
dbfile: &Path,
id: u32,
events: Events,
stock_strings: StockStrings,
) -> Result<Context> {
let context = Self::new_closed(dbfile, id, events, stock_strings).await?;
// Open the database if is not encrypted.
if context.check_passphrase("".to_string()).await? {
@@ -130,7 +136,12 @@ impl Context {
}
/// Creates new context without opening the database.
pub async fn new_closed(dbfile: &Path, id: u32, events: Events) -> Result<Context> {
pub async fn new_closed(
dbfile: &Path,
id: u32,
events: Events,
stockstrings: StockStrings,
) -> Result<Context> {
let mut blob_fname = OsString::new();
blob_fname.push(dbfile.file_name().unwrap_or_default());
blob_fname.push("-blobs");
@@ -138,7 +149,7 @@ impl Context {
if !blobdir.exists() {
tokio::fs::create_dir_all(&blobdir).await?;
}
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events)?;
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events, stockstrings)?;
Ok(context)
}
@@ -174,6 +185,7 @@ impl Context {
blobdir: PathBuf,
id: u32,
events: Events,
stockstrings: StockStrings,
) -> Result<Context> {
ensure!(
blobdir.is_dir(),
@@ -190,7 +202,7 @@ impl Context {
generating_key_mutex: Mutex::new(()),
oauth2_mutex: Mutex::new(()),
wrong_pw_warning_mutex: Mutex::new(()),
translated_stockstrings: RwLock::new(HashMap::new()),
translated_stockstrings: stockstrings,
events,
scheduler: RwLock::new(None),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
@@ -706,7 +718,7 @@ mod tests {
let tmp = tempfile::tempdir()?;
let dbfile = tmp.path().join("db.sqlite");
tokio::fs::write(&dbfile, b"123").await?;
let res = Context::new(&dbfile, 1, Events::new()).await?;
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
// Broken database is indistinguishable from encrypted one.
assert_eq!(res.is_open().await, false);
@@ -852,7 +864,9 @@ mod tests {
async fn test_blobdir_exists() {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
Context::new(&dbfile, 1, Events::new()).await.unwrap();
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
.await
.unwrap();
let blobdir = tmp.path().join("db.sqlite-blobs");
assert!(blobdir.is_dir());
}
@@ -863,7 +877,7 @@ mod tests {
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("db.sqlite-blobs");
tokio::fs::write(&blobdir, b"123").await.unwrap();
let res = Context::new(&dbfile, 1, Events::new()).await;
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
assert!(res.is_err());
}
@@ -873,7 +887,9 @@ mod tests {
let subdir = tmp.path().join("subdir");
let dbfile = subdir.join("db.sqlite");
let dbfile2 = dbfile.clone();
Context::new(&dbfile, 1, Events::new()).await.unwrap();
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
.await
.unwrap();
assert!(subdir.is_dir());
assert!(dbfile2.is_file());
}
@@ -883,7 +899,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = PathBuf::new();
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new());
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
assert!(res.is_err());
}
@@ -892,7 +908,7 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let dbfile = tmp.path().join("db.sqlite");
let blobdir = tmp.path().join("blobs");
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new());
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
assert!(res.is_err());
}
@@ -1061,7 +1077,7 @@ mod tests {
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new())
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
@@ -1069,7 +1085,7 @@ mod tests {
drop(context);
let id = 2;
let context = Context::new(&dbfile, id, Events::new())
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
.await
.context("failed to create context")?;
assert_eq!(context.is_open().await, false);

View File

@@ -173,6 +173,13 @@ pub enum EventType {
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
chat_id: ChatId,
msg_id: MsgId,
contact_id: ContactId,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///

View File

@@ -2157,8 +2157,8 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
.sql
.execute(
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;",
paramsv![folder, uid_next, uid_next, folder],
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
paramsv![folder, uid_next],
)
.await?;
Ok(())
@@ -2189,8 +2189,8 @@ pub(crate) async fn set_uidvalidity(
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;",
paramsv![folder, uidvalidity, uidvalidity, folder],
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
paramsv![folder, uidvalidity],
)
.await?;
Ok(())
@@ -2212,8 +2212,8 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
.sql
.execute(
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET modseq=? WHERE folder=?;",
paramsv![folder, modseq, modseq, folder],
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
paramsv![folder, modseq],
)
.await?;
Ok(())

View File

@@ -103,6 +103,7 @@ pub mod receive_imf;
pub mod tools;
pub mod accounts;
pub mod reaction;
/// if set imap/incoming and smtp/outgoing MIME messages will be printed
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";

View File

@@ -22,6 +22,7 @@ use crate::imap::markseen_on_imap_table;
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::split_armored_data;
use crate::reaction::get_msg_reactions;
use crate::scheduler::InterruptInfo;
use crate::sql;
use crate::stock_str;
@@ -1082,6 +1083,11 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
ret += "\n";
let reactions = get_msg_reactions(context, msg_id).await?;
if !reactions.is_empty() {
ret += &format!("Reactions: {}\n", reactions);
}
if let Some(error) = msg.error.as_ref() {
ret += &format!("Error: {}", error);
}
@@ -1762,6 +1768,9 @@ pub enum Viewtype {
/// The text of the message is set using dc_msg_set_text() and retrieved with dc_msg_get_text().
Text = 10,
/// RFC 9078 reaction.
Reaction = 11,
/// Image message.
/// If the image is an animated GIF, the type DC_MSG_GIF should be used.
/// File, width and height are set via dc_msg_set_file(), dc_msg_set_dimension
@@ -1821,6 +1830,7 @@ impl Viewtype {
match self {
Viewtype::Unknown => false,
Viewtype::Text => false,
Viewtype::Reaction => false,
Viewtype::Image => true,
Viewtype::Gif => true,
Viewtype::Sticker => true,

View File

@@ -1122,6 +1122,11 @@ impl<'a> MimeFactory<'a> {
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
))
.body(message_text);
if self.msg.viewtype == Viewtype::Reaction {
main_part = main_part.header(("Content-Disposition", "reaction"));
}
let mut parts = Vec::new();
// add HTML-part, this is needed only if a HTML-message from a non-delta-client is forwarded;

View File

@@ -465,7 +465,10 @@ impl MimeMessage {
| Viewtype::Video
| Viewtype::File
| Viewtype::Webxdc => true,
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
Viewtype::Unknown
| Viewtype::Text
| Viewtype::Reaction
| Viewtype::VideochatInvitation => false,
};
if need_drop {
@@ -551,7 +554,10 @@ impl MimeMessage {
}
if prepend_subject && !subject.is_empty() {
let part_with_text = self.parts.iter_mut().find(|part| !part.msg.is_empty());
let part_with_text = self
.parts
.iter_mut()
.find(|part| !part.msg.is_empty() && part.typ != Viewtype::Reaction);
if let Some(mut part) = part_with_text {
part.msg = format!("{} {}", subject, part.msg);
}
@@ -913,6 +919,7 @@ impl MimeMessage {
Ok(any_part_added)
}
/// Returns true if any part was added, false otherwise.
async fn add_single_part_if_known(
&mut self,
context: &Context,
@@ -946,6 +953,26 @@ impl MimeMessage {
warn!(context, "Missing attachment");
return Ok(false);
}
mime::TEXT if msg_type == Viewtype::Reaction => {
// Reaction.
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
Err(err) => {
warn!(context, "Invalid body parsed {:?}", err);
// Note that it's not always an error - might be no data
return Ok(false);
}
};
let part = Part {
typ: Viewtype::Reaction,
mimetype: Some(mime_type),
msg: decoded_data,
..Default::default()
};
self.do_add_single_part(part);
return Ok(true);
}
mime::TEXT | mime::HTML => {
let decoded_data = match mail.get_body() {
Ok(decoded_data) => decoded_data,
@@ -1652,7 +1679,11 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
let viewtype = match mimetype.type_() {
mime::TEXT => {
if !is_attachment_disposition(mail) {
if mail.get_content_disposition().disposition
== DispositionType::Extension("reaction".to_string())
{
Viewtype::Reaction
} else if !is_attachment_disposition(mail) {
match mimetype.subtype() {
mime::PLAIN | mime::HTML => Viewtype::Text,
_ => Viewtype::File,
@@ -3327,7 +3358,40 @@ Message.
assert_eq!(mime_message.parts[0].org_filename, Some(".eml".to_string()));
dbg!(mime_message);
Ok(())
}
/// Tests parsing of MIME message containing RFC 9078 reaction.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let mime_message = MimeMessage::from_bytes(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56789@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
)
.await?;
assert_eq!(mime_message.parts.len(), 1);
assert_eq!(mime_message.parts[0].typ, Viewtype::Reaction);
assert_eq!(
mime_message
.get_header(HeaderDef::InReplyTo)
.and_then(|msgid| parse_message_id(msgid).ok())
.unwrap(),
"12345@example.org"
);
Ok(())
}

462
src/reaction.rs Normal file
View File

@@ -0,0 +1,462 @@
//! # Reactions.
//!
//! Reactions are short messages consisting of emojis sent in reply to
//! messages. Unlike normal messages which are added to the end of the chat,
//! reactions are supposed to be displayed near the original messages.
//!
//! RFC 9078 specifies how reactions are transmitted in MIME messages.
//!
//! Reaction update semantics is not well-defined in RFC 9078, so
//! Delta Chat uses the same semantics as in
//! [XEP-0444](https://xmpp.org/extensions/xep-0444.html) section
//! "3.2 Updating reactions to a message". Received reactions override
//! all previously received reactions from the same user and it is
//! possible to remove all reactions by sending an empty string as a reaction,
//! even though RFC 9078 requires at least one emoji to be sent.
use std::collections::BTreeMap;
use std::fmt;
use anyhow::Result;
use crate::chat::{send_msg, ChatId};
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype};
/// A single reaction consisting of multiple emoji sequences.
///
/// It is guaranteed to have all emojis sorted and deduplicated inside.
#[derive(Debug, Default, Clone)]
pub struct Reaction {
/// Canonical represntation of reaction as a string of space-separated emojis.
reaction: String,
}
// We implement From<&str> instead of std::str::FromStr, because
// FromStr requires error type and reaction parsing never returns an
// error.
impl From<&str> for Reaction {
/// Parses a string containing a reaction.
///
/// Reaction string is separated by spaces or tabs (`WSP` in ABNF),
/// but this function accepts any ASCII whitespace, so even a CRLF at
/// the end of string is acceptable.
///
/// Any short enough string is accepted as a reaction to avoid the
/// complexity of validating emoji sequences ase required by RFC
/// 9078. On the sender side UI is responsible to provide only
/// valid emoji sequences via reaction picker. On the receiver
/// side, abuse of the possibility to use arbitrary strings as
/// reactions is not different from other kinds of spam attacks
/// such as sending large numbers of large messages, and should be
/// dealt with the same way, e.g. by blocking the user.
fn from(reaction: &str) -> Self {
let mut emojis: Vec<&str> = reaction
.split_ascii_whitespace()
.filter(|&emoji| emoji.len() < 30)
.collect();
emojis.sort();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
}
}
impl Reaction {
/// Returns true if reaction contains no emojis.
pub fn is_empty(&self) -> bool {
self.reaction.is_empty()
}
/// Returns a vector of emojis composing a reaction.
pub fn emojis(&self) -> Vec<&str> {
self.reaction.split(' ').collect()
}
/// Returns space-separated string of emojis.
pub fn as_str(&self) -> &str {
&self.reaction
}
/// Appends emojis from another reaction to this reaction.
pub fn add(&self, other: Self) -> Self {
let mut emojis: Vec<&str> = self.emojis();
emojis.append(&mut other.emojis());
emojis.sort();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
}
}
/// Structure representing all reactions to a particular message.
#[derive(Debug)]
pub struct Reactions {
/// Map from a contact to its reaction to message.
reactions: BTreeMap<ContactId, Reaction>,
}
impl Reactions {
/// Returns vector of contacts that reacted to the message.
pub fn contacts(&self) -> Vec<ContactId> {
self.reactions.keys().copied().collect()
}
/// Returns reaction of a given contact to message.
///
/// If contact did not react to message or removed the reaction,
/// this method returns an empty reaction.
pub fn get(&self, contact_id: ContactId) -> Reaction {
self.reactions.get(&contact_id).cloned().unwrap_or_default()
}
/// Returns true if the message has no reactions.
pub fn is_empty(&self) -> bool {
self.reactions.is_empty()
}
}
impl fmt::Display for Reactions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
for reaction in self.reactions.values() {
for emoji in reaction.emojis() {
emoji_frequencies
.entry(emoji.to_string())
.and_modify(|x| *x += 1)
.or_insert(1);
}
}
let mut first = true;
for (emoji, frequency) in emoji_frequencies {
if !first {
write!(f, " ")?;
first = true;
}
write!(f, "{}{}", emoji, frequency)?;
}
Ok(())
}
}
async fn set_msg_id_reaction(
context: &Context,
msg_id: MsgId,
chat_id: ChatId,
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if reaction.is_empty() {
// Simply remove the record instead of setting it to empty string.
context
.sql
.execute(
"DELETE FROM reactions
WHERE msg_id = ?1
AND contact_id = ?2",
paramsv![msg_id, contact_id],
)
.await?;
} else {
context
.sql
.execute(
"INSERT INTO reactions (msg_id, contact_id, reaction)
VALUES (?1, ?2, ?3)
ON CONFLICT(msg_id, contact_id)
DO UPDATE SET reaction=excluded.reaction",
paramsv![msg_id, contact_id, reaction.as_str()],
)
.await?;
}
context.emit_event(EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
});
Ok(())
}
/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
///
/// `reaction` is a string consisting of space-separated emoji. Use
/// empty string to retract a reaction.
pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
let msg = Message::load_from_db(context, msg_id).await?;
let chat_id = msg.chat_id;
let reaction: Reaction = reaction.into();
let mut reaction_msg = Message::new(Viewtype::Reaction);
reaction_msg.text = Some(reaction.as_str().to_string());
set_msg_id_reaction(context, msg_id, msg.chat_id, ContactId::SELF, reaction).await?;
send_msg(context, chat_id, &mut reaction_msg).await
}
/// Adds given reaction to message `msg_id` and sends an update.
///
/// This can be used to implement advanced clients that allow reacting
/// with multiple emojis. For a simple messenger UI, you probably want
/// to use [`send_reaction()`] instead so reacing with a new emoji
/// removes previous emoji at the same time.
pub async fn add_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
let self_reaction = get_self_reaction(context, msg_id).await?;
let reaction = self_reaction.add(Reaction::from(reaction));
send_reaction(context, msg_id, reaction.as_str()).await
}
/// Updates reaction of `contact_id` on the message with `in_reply_to`
/// Message-ID. If no such message is found in the database, reaction
/// is ignored.
///
/// `reaction` is a space-separated string of emojis. It can be empty
/// if contact wants to remove all reactions.
pub(crate) async fn set_msg_reaction(
context: &Context,
in_reply_to: &str,
chat_id: ChatId,
contact_id: ContactId,
reaction: Reaction,
) -> Result<()> {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
set_msg_id_reaction(context, msg_id, chat_id, contact_id, reaction).await
} else {
info!(
context,
"Can't assign reaction to unknown message with Message-ID {}", in_reply_to
);
Ok(())
}
}
/// Get our own reaction for a given message.
async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction> {
let reaction_str: Option<String> = context
.sql
.query_get_value(
"SELECT reaction
FROM reactions
WHERE msg_id=? AND contact_id=?",
paramsv![msg_id, ContactId::SELF],
)
.await?;
Ok(reaction_str
.as_deref()
.map(Reaction::from)
.unwrap_or_default())
}
/// Returns a structure containing all reactions to the message.
pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<Reactions> {
let reactions = context
.sql
.query_map(
"SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
paramsv![msg_id],
|row| {
let contact_id: ContactId = row.get(0)?;
let reaction: String = row.get(1)?;
Ok((contact_id, reaction))
},
|rows| {
let mut reactions = Vec::new();
for row in rows {
let (contact_id, reaction) = row?;
reactions.push((contact_id, Reaction::from(reaction.as_str())));
}
Ok(reactions)
},
)
.await?
.into_iter()
.collect();
Ok(Reactions { reactions })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::get_chat_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::contact::{Contact, Origin};
use crate::message::MessageState;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
#[test]
fn test_parse_reaction() {
// Check that basic set of emojis from RFC 9078 is supported.
assert_eq!(Reaction::from("👍").emojis(), vec!["👍"]);
assert_eq!(Reaction::from("👎").emojis(), vec!["👎"]);
assert_eq!(Reaction::from("😀").emojis(), vec!["😀"]);
assert_eq!(Reaction::from("").emojis(), vec![""]);
assert_eq!(Reaction::from("😢").emojis(), vec!["😢"]);
// Empty string can be used to remove all reactions.
assert!(Reaction::from("").is_empty());
// Short strings can be used as emojis, could be used to add
// support for custom emojis via emoji shortcodes.
assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
// Check that long strings are not valid emojis.
assert!(
Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
);
// Multiple reactions separated by spaces or tabs are supported.
assert_eq!(Reaction::from("👍 ❤").emojis(), vec!["", "👍"]);
assert_eq!(Reaction::from("👍\t").emojis(), vec!["", "👍"]);
// Invalid emojis are removed, but valid emojis are retained.
assert_eq!(
Reaction::from("👍\t:foo: ❤").emojis(),
vec![":foo:", "", "👍"]
);
assert_eq!(Reaction::from("👍\t:foo: ❤").as_str(), ":foo: ❤ 👍");
// Duplicates are removed.
assert_eq!(Reaction::from("👍 👍").emojis(), vec!["👍"]);
}
#[test]
fn test_add_reaction() {
let reaction1 = Reaction::from("👍 😀");
let reaction2 = Reaction::from("");
let reaction_sum = reaction1.add(reaction2);
assert_eq!(reaction_sum.emojis(), vec!["", "👍", "😀"]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config(Config::ShowEmails, Some("2")).await?;
// Alice receives BCC-self copy of a message sent to Bob.
receive_imf(
&alice,
"To: bob@example.net\n\
From: alice@example.org\n\
Date: Today, 29 February 2021 00:00:00 -800\n\
Message-ID: 12345@example.org\n\
Subject: Meeting\n\
\n\
Can we chat at 1pm pacific, today?"
.as_bytes(),
false,
)
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.state, MessageState::OutDelivered);
let reactions = get_msg_reactions(&alice, msg.id).await?;
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 0);
let bob_id = Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated)
.await?
.0;
let bob_reaction = reactions.get(bob_id);
assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
// Alice receives reaction to her message from Bob.
receive_imf(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56789@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
\u{1F44D}"
.as_bytes(),
false,
)
.await?;
let reactions = get_msg_reactions(&alice, msg.id).await?;
assert_eq!(reactions.to_string(), "👍1");
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.get(0), Some(&bob_id));
let bob_reaction = reactions.get(bob_id);
assert_eq!(bob_reaction.is_empty(), false);
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
Ok(())
}
async fn expect_reactions_changed_event(
t: &TestContext,
expected_chat_id: ChatId,
expected_msg_id: MsgId,
expected_contact_id: ContactId,
) -> Result<()> {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::ReactionsChanged { .. }))
.await;
match event {
EventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
} => {
assert_eq!(chat_id, expected_chat_id);
assert_eq!(msg_id, expected_msg_id);
assert_eq!(contact_id, expected_contact_id);
}
_ => unreachable!(),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_alice = alice.create_chat(&bob).await;
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
let bob_msg = bob.recv_msg(&alice_msg).await;
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 1);
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 1);
bob_msg.chat_id.accept(&bob).await?;
send_reaction(&bob, bob_msg.id, "👍").await.unwrap();
expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id, 0).await?.len(), 1);
let bob_reaction_msg = bob.pop_sent_msg().await;
let alice_reaction_msg = alice.recv_msg_opt(&bob_reaction_msg).await.unwrap();
assert_eq!(alice_reaction_msg.chat_id, DC_CHAT_ID_TRASH);
assert_eq!(get_chat_msgs(&alice, chat_alice.id, 0).await?.len(), 1);
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
let contacts = reactions.contacts();
assert_eq!(contacts.len(), 1);
let bob_id = contacts.get(0).unwrap();
let bob_reaction = reactions.get(*bob_id);
assert_eq!(bob_reaction.is_empty(), false);
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
.await?;
Ok(())
}
}

View File

@@ -33,6 +33,7 @@ use crate::mimeparser::{
};
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::sql;
use crate::stock_str;
@@ -430,8 +431,12 @@ async fn add_parts(
};
// incoming non-chat messages may be discarded
let location_kml_is = mime_parser.location_kml.is_some();
let is_location_kml = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
let is_reaction = mime_parser
.parts
.iter()
.any(|part| part.typ == Viewtype::Reaction);
let show_emails =
ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
@@ -450,7 +455,7 @@ async fn add_parts(
ShowEmails::All => allow_creation = !is_mdn,
}
} else {
allow_creation = !is_mdn;
allow_creation = !is_mdn && !is_reaction;
}
// check if the message introduces a new chat:
@@ -689,7 +694,8 @@ async fn add_parts(
state = if seen
|| fetching_existing_messages
|| is_mdn
|| location_kml_is
|| is_reaction
|| is_location_kml
|| securejoin_seen
|| chat_id_blocked == Blocked::Yes
{
@@ -841,14 +847,15 @@ async fn add_parts(
}
}
if is_mdn {
chat_id = Some(DC_CHAT_ID_TRASH);
}
let chat_id = chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
let orig_chat_id = chat_id;
let chat_id = if is_mdn || is_reaction {
DC_CHAT_ID_TRASH
});
} else {
chat_id.unwrap_or_else(|| {
info!(context, "No chat id for message (TRASH)");
DC_CHAT_ID_TRASH
})
};
// Extract ephemeral timer from the message or use the existing timer if the message is not fully downloaded.
let mut ephemeral_timer = if is_partial_download.is_some() {
@@ -1053,6 +1060,17 @@ async fn add_parts(
let conn = context.sql.get_conn().await?;
for part in &mime_parser.parts {
if part.typ == Viewtype::Reaction {
set_msg_reaction(
context,
&mime_in_reply_to,
orig_chat_id.unwrap_or_default(),
from_id,
Reaction::from(part.msg.as_str()),
)
.await?;
}
let mut txt_raw = "".to_string();
let mut stmt = conn.prepare_cached(
r#"
@@ -1113,7 +1131,7 @@ INSERT INTO msgs
// If you change which information is skipped if the message is trashed,
// also change `MsgId::trash()` and `delete_expired_messages()`
let trash = chat_id.is_trash() || (location_kml_is && msg.is_empty());
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
stmt.execute(paramsv![
rfc724_mid,
@@ -4626,7 +4644,7 @@ Reply to all"#,
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_assignment_adhoc() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice.set_config(Config::ShowEmails, Some("2")).await?;
bob.set_config(Config::ShowEmails, Some("2")).await?;

View File

@@ -693,6 +693,7 @@ mod tests {
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::EmailAddress;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_setup_contact() {
@@ -722,7 +723,10 @@ mod tests {
);
let sent = bob.pop_sent_msg().await;
assert_eq!(sent.recipient(), "alice@example.org".parse().unwrap());
assert_eq!(
sent.recipient(),
EmailAddress::new("alice@example.org").unwrap()
);
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
@@ -1087,7 +1091,10 @@ mod tests {
assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 1);
let sent = bob.pop_sent_msg().await;
assert_eq!(sent.recipient(), "alice@example.org".parse().unwrap());
assert_eq!(
sent.recipient(),
EmailAddress::new("alice@example.org").unwrap()
);
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");

View File

@@ -24,7 +24,6 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
.await
.context("failed to check if config table exists")?
{
info!(context, "First time init: creating tables",);
sql.transaction(move |transaction| {
transaction.execute_batch(TABLES)?;
@@ -35,7 +34,8 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
)?;
Ok(())
})
.await?;
.await
.context("Creating tables failed")?;
let mut lock = context.sql.config_cache.write().await;
lock.insert(
@@ -57,7 +57,6 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool
let mut recode_avatar = false;
if dbversion < 1 {
info!(context, "[migration] v1");
sql.execute_migration(
r#"
CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT '');
@@ -67,7 +66,6 @@ CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
.await?;
}
if dbversion < 2 {
info!(context, "[migration] v2");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';",
2,
@@ -75,7 +73,6 @@ CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
.await?;
}
if dbversion < 7 {
info!(context, "[migration] v7");
sql.execute_migration(
"CREATE TABLE keypairs (\
id INTEGER PRIMARY KEY, \
@@ -89,7 +86,6 @@ CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
.await?;
}
if dbversion < 10 {
info!(context, "[migration] v10");
sql.execute_migration(
"CREATE TABLE acpeerstates (\
id INTEGER PRIMARY KEY, \
@@ -104,7 +100,6 @@ CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
.await?;
}
if dbversion < 12 {
info!(context, "[migration] v12");
sql.execute_migration(
r#"
CREATE TABLE msgs_mdns ( msg_id INTEGER, contact_id INTEGER);
@@ -114,7 +109,6 @@ CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);"#,
.await?;
}
if dbversion < 17 {
info!(context, "[migration] v17");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
@@ -128,7 +122,6 @@ CREATE INDEX msgs_index5 ON msgs (starred);"#,
.await?;
}
if dbversion < 18 {
info!(context, "[migration] v18");
sql.execute_migration(
r#"
ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
@@ -138,7 +131,6 @@ ALTER TABLE acpeerstates ADD COLUMN gossip_key;"#,
.await?;
}
if dbversion < 27 {
info!(context, "[migration] v27");
// chat.id=1 and chat.id=2 are the old deaddrops,
// the current ones are defined by chats.blocked=2
sql.execute_migration(
@@ -152,7 +144,6 @@ ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;"#,
.await?;
}
if dbversion < 34 {
info!(context, "[migration] v34");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;
@@ -167,7 +158,6 @@ CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#,
recalc_fingerprints = true;
}
if dbversion < 39 {
info!(context, "[migration] v39");
sql.execute_migration(
r#"
CREATE TABLE tokens (
@@ -185,17 +175,14 @@ CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
.await?;
}
if dbversion < 40 {
info!(context, "[migration] v40");
sql.execute_migration("ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", 40)
.await?;
}
if dbversion < 44 {
info!(context, "[migration] v44");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", 44)
.await?;
}
if dbversion < 46 {
info!(context, "[migration] v46");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
@@ -205,12 +192,10 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
.await?;
}
if dbversion < 47 {
info!(context, "[migration] v47");
sql.execute_migration("ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", 47)
.await?;
}
if dbversion < 48 {
info!(context, "[migration] v48");
// NOTE: move_state is not used anymore
sql.execute_migration(
"ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;",
@@ -219,7 +204,6 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
.await?;
}
if dbversion < 49 {
info!(context, "[migration] v49");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;",
49,
@@ -227,7 +211,6 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
.await?;
}
if dbversion < 50 {
info!(context, "[migration] v50");
// installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly;
// keep this default and use DC_SHOW_EMAILS_NO
// only for new installations
@@ -238,7 +221,6 @@ ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
sql.set_db_version(50).await?;
}
if dbversion < 53 {
info!(context, "[migration] v53");
// the messages containing _only_ locations
// are also added to the database as _hidden_.
sql.execute_migration(
@@ -263,7 +245,6 @@ CREATE INDEX chats_index3 ON chats (locations_send_until);"#,
.await?;
}
if dbversion < 54 {
info!(context, "[migration] v54");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
@@ -273,7 +254,6 @@ CREATE INDEX msgs_index6 ON msgs (location_id);"#,
.await?;
}
if dbversion < 55 {
info!(context, "[migration] v55");
sql.execute_migration(
"ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;",
55,
@@ -281,7 +261,6 @@ CREATE INDEX msgs_index6 ON msgs (location_id);"#,
.await?;
}
if dbversion < 59 {
info!(context, "[migration] v59");
// records in the devmsglabels are kept when the message is deleted.
// so, msg_id may or may not exist.
sql.execute_migration(
@@ -295,7 +274,6 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
}
if dbversion < 60 {
info!(context, "[migration] v60");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;",
60,
@@ -303,7 +281,6 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
.await?;
}
if dbversion < 61 {
info!(context, "[migration] v61");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
61,
@@ -312,7 +289,6 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
update_icons = true;
}
if dbversion < 62 {
info!(context, "[migration] v62");
sql.execute_migration(
"ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
62,
@@ -320,17 +296,14 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
.await?;
}
if dbversion < 63 {
info!(context, "[migration] v63");
sql.execute_migration("UPDATE chats SET grpid='' WHERE type=100", 63)
.await?;
}
if dbversion < 64 {
info!(context, "[migration] v64");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';", 64)
.await?;
}
if dbversion < 65 {
info!(context, "[migration] v65");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER;
@@ -341,12 +314,10 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
.await?;
}
if dbversion < 66 {
info!(context, "[migration] v66");
update_icons = true;
sql.set_db_version(66).await?;
}
if dbversion < 67 {
info!(context, "[migration] v67");
for prefix in &["", "configured_"] {
if let Some(server_flags) = sql
.get_raw_config_int(format!("{}server_flags", prefix))
@@ -373,7 +344,6 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
sql.set_db_version(67).await?;
}
if dbversion < 68 {
info!(context, "[migration] v68");
// the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
sql.execute_migration(
"CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
@@ -382,7 +352,6 @@ ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
.await?;
}
if dbversion < 69 {
info!(context, "[migration] v69");
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;
@@ -394,9 +363,8 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
}
if dbversion < 71 {
info!(context, "[migration] v71");
if let Ok(addr) = context.get_primary_self_addr().await {
if let Ok(domain) = addr.parse::<EmailAddress>().map(|email| email.domain) {
if let Ok(domain) = EmailAddress::new(&addr).map(|email| email.domain) {
context
.set_config(
Config::ConfiguredProvider,
@@ -410,20 +378,16 @@ UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
sql.set_db_version(71).await?;
}
if dbversion < 72 {
info!(context, "[migration] v72");
if !sql.col_exists("msgs", "mime_modified").await? {
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
72,
)
.await?;
}
if dbversion < 72 && !sql.col_exists("msgs", "mime_modified").await? {
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
72,
)
.await?;
}
if dbversion < 73 {
use Config::*;
info!(context, "[migration] v73");
sql.execute(
r#"
CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#,
@@ -459,12 +423,10 @@ paramsv![]
sql.set_db_version(73).await?;
}
if dbversion < 74 {
info!(context, "[migration] v74");
sql.execute_migration("UPDATE contacts SET name='' WHERE name=authname", 74)
.await?;
}
if dbversion < 75 {
info!(context, "[migration] v75");
sql.execute_migration(
"ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
75,
@@ -472,24 +434,20 @@ paramsv![]
.await?;
}
if dbversion < 76 {
info!(context, "[migration] v76");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76)
.await?;
}
if dbversion < 77 {
info!(context, "[migration] v77");
recode_avatar = true;
sql.set_db_version(77).await?;
}
if dbversion < 78 {
// move requests to "Archived Chats",
// this way, the app looks familiar after the contact request upgrade.
info!(context, "[migration] v78");
sql.execute_migration("UPDATE chats SET archived=1 WHERE blocked=2;", 78)
.await?;
}
if dbversion < 79 {
info!(context, "[migration] v79");
sql.execute_migration(
r#"
ALTER TABLE msgs ADD COLUMN download_state INTEGER DEFAULT 0;
@@ -499,7 +457,6 @@ paramsv![]
.await?;
}
if dbversion < 80 {
info!(context, "[migration] v80");
sql.execute_migration(
r#"CREATE TABLE multi_device_sync (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -509,12 +466,10 @@ item TEXT DEFAULT '');"#,
.await?;
}
if dbversion < 81 {
info!(context, "[migration] v81");
sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81)
.await?;
}
if dbversion < 82 {
info!(context, "[migration] v82");
sql.execute_migration(
r#"CREATE TABLE imap (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -547,7 +502,6 @@ DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
.await?;
}
if dbversion < 83 {
info!(context, "[migration] v83");
sql.execute_migration(
"ALTER TABLE imap_sync
ADD COLUMN modseq -- Highest modification sequence
@@ -557,7 +511,6 @@ DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
.await?;
}
if dbversion < 84 {
info!(context, "[migration] v84");
sql.execute_migration(
r#"CREATE TABLE msgs_status_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -570,7 +523,6 @@ CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);"#,
.await?;
}
if dbversion < 85 {
info!(context, "[migration] v85");
sql.execute_migration(
r#"CREATE TABLE smtp (
id INTEGER PRIMARY KEY,
@@ -587,7 +539,6 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 86 {
info!(context, "[migration] v86");
sql.execute_migration(
r#"CREATE TABLE bobstate (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -600,7 +551,6 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 87 {
info!(context, "[migration] v87");
// the index is used to speed up delete_expired_messages()
sql.execute_migration(
"CREATE INDEX IF NOT EXISTS msgs_index8 ON msgs (ephemeral_timestamp);",
@@ -609,12 +559,10 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 88 {
info!(context, "[migration] v88");
sql.execute_migration("DROP TABLE IF EXISTS backup_blobs;", 88)
.await?;
}
if dbversion < 89 {
info!(context, "[migration] v89");
sql.execute_migration(
r#"CREATE TABLE imap_markseen (
id INTEGER,
@@ -625,7 +573,6 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 90 {
info!(context, "[migration] v90");
sql.execute_migration(
r#"CREATE TABLE smtp_mdns (
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN
@@ -638,7 +585,6 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 91 {
info!(context, "[migration] v91");
sql.execute_migration(
r#"CREATE TABLE smtp_status_updates (
msg_id INTEGER NOT NULL UNIQUE, -- msg_id of the webxdc instance with pending updates
@@ -650,6 +596,35 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
)
.await?;
}
if dbversion < 92 {
sql.execute_migration(
r#"CREATE TABLE reactions (
msg_id INTEGER NOT NULL, -- id of the message reacted to
contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
PRIMARY KEY(msg_id, contact_id),
FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
)"#,
92
).await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
.unwrap_or_default();
if new_version != dbversion || !exists_before_update {
let created_db = if exists_before_update {
""
} else {
"Created new database; "
};
info!(
context,
"{}[migration] v{}-v{}", created_db, dbversion, new_version
);
}
Ok((
recalc_fingerprints,

View File

@@ -1,9 +1,14 @@
//! Module to work with translatable stock strings.
use anyhow::{bail, Error};
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::{bail, Result};
use strum::EnumProperty as EnumPropertyTrait;
use strum_macros::EnumProperty;
use tokio::sync::RwLock;
use crate::accounts::Accounts;
use crate::blob::BlobObject;
use crate::chat::{self, Chat, ChatId, ProtectionStatus};
use crate::config::Config;
@@ -14,6 +19,12 @@ use crate::param::Param;
use crate::tools::timestamp_to_str;
use humansize::{file_size_opts, FileSize};
#[derive(Debug, Clone)]
pub struct StockStrings {
/// Map from stock string ID to the translation.
translated_stockstrings: Arc<RwLock<HashMap<usize, String>>>,
}
/// Stock strings
///
/// These identify the string to return in [Context.stock_str]. The
@@ -402,15 +413,54 @@ impl StockMessage {
}
}
impl Default for StockStrings {
fn default() -> Self {
StockStrings::new()
}
}
impl StockStrings {
pub fn new() -> Self {
Self {
translated_stockstrings: Arc::new(RwLock::new(Default::default())),
}
}
async fn translated(&self, id: StockMessage) -> String {
self.translated_stockstrings
.read()
.await
.get(&(id as usize))
.map(AsRef::as_ref)
.unwrap_or_else(|| id.fallback())
.to_string()
}
async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
if stockstring.contains("%1") && !id.fallback().contains("%1") {
bail!(
"translation {} contains invalid %1 placeholder, default is {}",
stockstring,
id.fallback()
);
}
if stockstring.contains("%2") && !id.fallback().contains("%2") {
bail!(
"translation {} contains invalid %2 placeholder, default is {}",
stockstring,
id.fallback()
);
}
self.translated_stockstrings
.write()
.await
.insert(id as usize, stockstring);
Ok(())
}
}
async fn translated(context: &Context, id: StockMessage) -> String {
context
.translated_stockstrings
.read()
.await
.get(&(id as usize))
.map(AsRef::as_ref)
.unwrap_or_else(|| id.fallback())
.to_string()
context.translated_stockstrings.translated(id).await
}
/// Helper trait only meant to be implemented for [`String`].
@@ -1205,29 +1255,10 @@ pub(crate) async fn aeap_explanation_and_link(
impl Context {
/// Set the stock string for the [StockMessage].
///
pub async fn set_stock_translation(
&self,
id: StockMessage,
stockstring: String,
) -> Result<(), Error> {
if stockstring.contains("%1") && !id.fallback().contains("%1") {
bail!(
"translation {} contains invalid %1 placeholder, default is {}",
stockstring,
id.fallback()
);
}
if stockstring.contains("%2") && !id.fallback().contains("%2") {
bail!(
"translation {} contains invalid %2 placeholder, default is {}",
stockstring,
id.fallback()
);
}
pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
self.translated_stockstrings
.write()
.await
.insert(id as usize, stockstring);
.set_stock_translation(id, stockstring)
.await?;
Ok(())
}
@@ -1243,7 +1274,7 @@ impl Context {
}
}
pub(crate) async fn update_device_chats(&self) -> Result<(), Error> {
pub(crate) async fn update_device_chats(&self) -> Result<()> {
if self.get_config_bool(Config::Bot).await? {
return Ok(());
}
@@ -1272,10 +1303,22 @@ impl Context {
}
}
impl Accounts {
/// Set the stock string for the [StockMessage].
///
pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
self.stockstrings
.set_stock_translation(id, stockstring)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use num_traits::ToPrimitive;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::chat::Chat;
use crate::chatlist::Chatlist;
use crate::test_utils::TestContext;
@@ -1386,7 +1429,7 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_quota_exceeding_stock_str() -> anyhow::Result<()> {
async fn test_quota_exceeding_stock_str() -> Result<()> {
let t = TestContext::new().await;
let str = quota_exceeding(&t, 81).await;
assert!(str.contains("81% "));
@@ -1396,7 +1439,7 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_msg_body() -> anyhow::Result<()> {
async fn test_partial_download_msg_body() -> Result<()> {
let t = TestContext::new().await;
let str = partial_download_msg_body(&t, 1024 * 1024).await;
assert_eq!(str, "1 MiB message");
@@ -1441,7 +1484,17 @@ mod tests {
assert_eq!(chats.len(), 0);
// a subsequent call to update_device_chats() must not re-add manally deleted messages or chats
t.update_device_chats().await.ok();
t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
// Reset all device messages. This normally happens due to account export and import.
// Check that update_device_chats() does not add welcome message for imported account.
delete_and_reset_all_device_msgs(&t).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0);
}

View File

@@ -144,7 +144,7 @@ impl Message {
.map(|info| info.name)
.unwrap_or_else(|_| "ErrWebxdcName".to_string())
}
Viewtype::Text | Viewtype::Unknown => {
Viewtype::Text | Viewtype::Reaction | Viewtype::Unknown => {
if self.param.get_cmd() != SystemMessage::LocationOnly {
"".to_string()
} else {

View File

@@ -29,6 +29,7 @@ use crate::key::{self, DcKey, KeyPair, KeyPairUse};
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::stock_str::StockStrings;
use crate::tools::EmailAddress;
#[allow(non_upper_case_globals)]
@@ -277,7 +278,7 @@ impl TestContext {
let mut context_names = CONTEXT_NAMES.write().unwrap();
context_names.insert(id, name);
}
let ctx = Context::new(&dbfile, id, Events::new())
let ctx = Context::new(&dbfile, id, Events::new(), StockStrings::new())
.await
.expect("failed to create context");
@@ -739,7 +740,7 @@ impl SentMessage {
.split(' ')
.next()
.expect("no recipient found");
rcpt.parse().expect("failed to parse email address")
EmailAddress::new(rcpt).expect("failed to parse email address")
}
/// The raw message payload.

View File

@@ -7,7 +7,7 @@ use std::fmt;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::str::from_utf8;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use anyhow::{bail, Error, Result};
@@ -524,23 +524,15 @@ pub struct EmailAddress {
pub domain: String,
}
impl EmailAddress {
pub fn new(input: &str) -> Result<Self> {
input.parse::<EmailAddress>()
}
}
impl fmt::Display for EmailAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}@{}", self.local, self.domain)
}
}
impl FromStr for EmailAddress {
type Err = Error;
impl EmailAddress {
/// Performs a dead-simple parse of an email address.
fn from_str(input: &str) -> Result<EmailAddress> {
pub fn new(input: &str) -> Result<EmailAddress> {
if input.is_empty() {
bail!("empty string is not valid");
}
@@ -945,36 +937,36 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
#[test]
fn test_emailaddress_parse() {
assert_eq!("".parse::<EmailAddress>().is_ok(), false);
assert_eq!(EmailAddress::new("").is_ok(), false);
assert_eq!(
"user@domain.tld".parse::<EmailAddress>().unwrap(),
EmailAddress::new("user@domain.tld").unwrap(),
EmailAddress {
local: "user".into(),
domain: "domain.tld".into(),
}
);
assert_eq!(
"user@localhost".parse::<EmailAddress>().unwrap(),
EmailAddress::new("user@localhost").unwrap(),
EmailAddress {
local: "user".into(),
domain: "localhost".into()
}
);
assert_eq!("uuu".parse::<EmailAddress>().is_ok(), false);
assert_eq!("dd.tt".parse::<EmailAddress>().is_ok(), false);
assert!("tt.dd@uu".parse::<EmailAddress>().is_ok());
assert!("u@d".parse::<EmailAddress>().is_ok());
assert!("u@d.".parse::<EmailAddress>().is_ok());
assert!("u@d.t".parse::<EmailAddress>().is_ok());
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
assert!(EmailAddress::new("tt.dd@uu").is_ok());
assert!(EmailAddress::new("u@d").is_ok());
assert!(EmailAddress::new("u@d.").is_ok());
assert!(EmailAddress::new("u@d.t").is_ok());
assert_eq!(
"u@d.tt".parse::<EmailAddress>().unwrap(),
EmailAddress::new("u@d.tt").unwrap(),
EmailAddress {
local: "u".into(),
domain: "d.tt".into(),
}
);
assert!("u@tt".parse::<EmailAddress>().is_ok());
assert_eq!("@d.tt".parse::<EmailAddress>().is_ok(), false);
assert!(EmailAddress::new("u@tt").is_ok());
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
use crate::chatlist::Chatlist;

View File

@@ -8,6 +8,7 @@ Transport | IMAP v4 ([RFC 3501](https://tools.ietf.org/ht
Proxy | SOCKS5 ([RFC 1928](https://tools.ietf.org/html/rfc1928))
Embedded media | MIME Document Series ([RFC 2045](https://tools.ietf.org/html/rfc2045), [RFC 2046](https://tools.ietf.org/html/rfc2046)), Content-Disposition Header ([RFC 2183](https://tools.ietf.org/html/rfc2183)), Multipart/Related ([RFC 2387](https://tools.ietf.org/html/rfc2387))
Text and Quote encoding | Fixed, Flowed ([RFC 3676](https://tools.ietf.org/html/rfc3676))
Reactions | Reaction: Indicating Summary Reaction to a Message [RFC 9078](https://datatracker.ietf.org/doc/rfc9078/)
Filename encoding | Encoded Words ([RFC 2047](https://tools.ietf.org/html/rfc2047)), Encoded Word Extensions ([RFC 2231](https://tools.ietf.org/html/rfc2231))
Identify server folders | IMAP LIST Extension ([RFC 6154](https://tools.ietf.org/html/rfc6154))
Push | IMAP IDLE ([RFC 2177](https://tools.ietf.org/html/rfc2177))