mirror of
https://github.com/chatmail/core.git
synced 2026-07-23 03:13:06 +03:00
Compare commits
71 Commits
jsonrpc/fi
...
tweak-reac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35791c9d9c | ||
|
|
d93c95e979 | ||
|
|
d8ced9fe87 | ||
|
|
f75aad9a2d | ||
|
|
fd3adb311f | ||
|
|
5cf364b55b | ||
|
|
2e946a008f | ||
|
|
369e1a3ad7 | ||
|
|
d34f9e23fe | ||
|
|
895c723d4e | ||
|
|
c7176d6bc8 | ||
|
|
b2939d3df3 | ||
|
|
54a157a629 | ||
|
|
427adefb42 | ||
|
|
f0dede26a3 | ||
|
|
36f85a6a5a | ||
|
|
137567554d | ||
|
|
72941e51fc | ||
|
|
836c016f97 | ||
|
|
e8ea9b7127 | ||
|
|
f80c78536f | ||
|
|
7877187894 | ||
|
|
a384a57979 | ||
|
|
86e1476dee | ||
|
|
81d0ecd8f6 | ||
|
|
6f329c9e96 | ||
|
|
0e2445c7a0 | ||
|
|
7ed947f598 | ||
|
|
b85f6ea6c3 | ||
|
|
f85f088d65 | ||
|
|
045472deac | ||
|
|
71cad4df58 | ||
|
|
48786522c8 | ||
|
|
0827f1b2f6 | ||
|
|
5973bb8610 | ||
|
|
00ca5132b4 | ||
|
|
191d203b24 | ||
|
|
06f96011d8 | ||
|
|
f2e292c702 | ||
|
|
832deb8e97 | ||
|
|
74a3c57222 | ||
|
|
4a19092db0 | ||
|
|
5c652d913a | ||
|
|
053213f50e | ||
|
|
342e946f49 | ||
|
|
d15ab1355b | ||
|
|
e47f4b8f80 | ||
|
|
637a4eb351 | ||
|
|
a233aeec4a | ||
|
|
d24104154f | ||
|
|
7fa2706d0e | ||
|
|
2a5365d46d | ||
|
|
72cc853420 | ||
|
|
eea111df6f | ||
|
|
130bea9e25 | ||
|
|
754242439a | ||
|
|
3fab9e4cec | ||
|
|
8b6290120e | ||
|
|
564aef2a2a | ||
|
|
5958324550 | ||
|
|
683fc1f081 | ||
|
|
9277b46620 | ||
|
|
261926222b | ||
|
|
35cfefd934 | ||
|
|
aa13374523 | ||
|
|
fd1cd39c7c | ||
|
|
3e6d1d5789 | ||
|
|
185d0bf3a3 | ||
|
|
837f3ce04c | ||
|
|
c661619263 | ||
|
|
ad8d3e2444 |
2
.github/workflows/node-tests.yml
vendored
2
.github/workflows/node-tests.yml
vendored
@@ -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
|
||||
|
||||
64
CHANGELOG.md
64
CHANGELOG.md
@@ -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
125
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -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]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
deltachat-jsonrpc/src/api/types/location.rs
Normal file
47
deltachat-jsonrpc/src/api/types/location.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> `
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;}));
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -47,5 +47,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.95.0"
|
||||
"version": "1.97.0"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
16
src/chat.rs
16
src/chat.rs
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(¶m.addr).context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
12
src/imap.rs
12
src/imap.rs
@@ -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(())
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
462
src/reaction.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
123
src/stock_str.rs
123
src/stock_str.rs
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
38
src/tools.rs
38
src/tools.rs
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user