mirror of
https://github.com/chatmail/core.git
synced 2026-04-14 03:57:19 +03:00
Compare commits
554 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc2b111f5d | ||
|
|
913d2c45b3 | ||
|
|
e32d676a08 | ||
|
|
9812d5ba75 | ||
|
|
bc7568e39b | ||
|
|
11bf1c45d2 | ||
|
|
122c23ad4e | ||
|
|
a0bde4699e | ||
|
|
ac01a4a771 | ||
|
|
51f2a8d59e | ||
|
|
f208c31cdf | ||
|
|
acd7a1d17e | ||
|
|
db6d451c90 | ||
|
|
4b3a6445fb | ||
|
|
aa3ef5011b | ||
|
|
1d3072c287 | ||
|
|
4fb59177fa | ||
|
|
d841bcb41e | ||
|
|
d205bc410b | ||
|
|
0d573ac037 | ||
|
|
a55e33fbc7 | ||
|
|
839b0e94af | ||
|
|
f2e600dc55 | ||
|
|
61fd0d400f | ||
|
|
7424d06416 | ||
|
|
aa71fbe04c | ||
|
|
c5cadd9991 | ||
|
|
c92554dc1f | ||
|
|
94c6d1dea4 | ||
|
|
d27d0ef476 | ||
|
|
d3f75360fa | ||
|
|
06a6cc48d2 | ||
|
|
b13f2709be | ||
|
|
1b824705fd | ||
|
|
6f22ce2722 | ||
|
|
5e58bf7575 | ||
|
|
85d7c1f942 | ||
|
|
df4fd82140 | ||
|
|
65b970a191 | ||
|
|
5e13b4c736 | ||
|
|
864833d232 | ||
|
|
3d07db6e62 | ||
|
|
9e88764a8a | ||
|
|
e70b879182 | ||
|
|
00d296e1ff | ||
|
|
e07f1a8b9c | ||
|
|
02b9085147 | ||
|
|
07fa9c35ee | ||
|
|
7db7c0aab1 | ||
|
|
30b23df816 | ||
|
|
4efd0d1ef7 | ||
|
|
f14880146a | ||
|
|
3a72188548 | ||
|
|
351f28361d | ||
|
|
c5b78741d6 | ||
|
|
57871bbaf8 | ||
|
|
287256693c | ||
|
|
d660f55a99 | ||
|
|
f1ca689f99 | ||
|
|
796b0d7752 | ||
|
|
2ea5c86a5a | ||
|
|
50b250cf78 | ||
|
|
3c03370589 | ||
|
|
8f41aed917 | ||
|
|
19be12a25d | ||
|
|
6a121b87eb | ||
|
|
420c0ed9b0 | ||
|
|
e05bb03db6 | ||
|
|
73fcb97eef | ||
|
|
8acf391ffe | ||
|
|
aacea2de25 | ||
|
|
b713e8cd94 | ||
|
|
b7be0b7bf6 | ||
|
|
2cb8b53256 | ||
|
|
a592a470cf | ||
|
|
c4d07ab99e | ||
|
|
eddd5a0d25 | ||
|
|
0f43d5d8f4 | ||
|
|
2e6d3aebae | ||
|
|
650995dc41 | ||
|
|
283a1f1653 | ||
|
|
d33909a054 | ||
|
|
129be3aa27 | ||
|
|
8a88479d8f | ||
|
|
5711f2fe3a | ||
|
|
46922d4d9d | ||
|
|
75fe4e106a | ||
|
|
7c60ac863e | ||
|
|
fa9bd7f144 | ||
|
|
22e5bf8571 | ||
|
|
c8ba516e83 | ||
|
|
4b021f509c | ||
|
|
bd1e06cfa7 | ||
|
|
11e5a00366 | ||
|
|
5fdecdcc16 | ||
|
|
77b899813c | ||
|
|
7843e0ed29 | ||
|
|
a036c86857 | ||
|
|
e535a6f859 | ||
|
|
5384d5f75d | ||
|
|
c569696fff | ||
|
|
a6732f5a5c | ||
|
|
9978f89b1b | ||
|
|
dbca15e5ef | ||
|
|
91649effa6 | ||
|
|
672ff58e3c | ||
|
|
a85b7ceb9c | ||
|
|
943ec19de4 | ||
|
|
733da91c5c | ||
|
|
d899cc730a | ||
|
|
5872b64265 | ||
|
|
5d8035f741 | ||
|
|
3d183336f5 | ||
|
|
9c931c22cc | ||
|
|
78a0d7501b | ||
|
|
638da904e7 | ||
|
|
fe0c9958a6 | ||
|
|
c469fcb435 | ||
|
|
02db6bcb8e | ||
|
|
4b74c9d85f | ||
|
|
040ac0ffe3 | ||
|
|
bfef129dbf | ||
|
|
486ea3a358 | ||
|
|
624ae86913 | ||
|
|
b47b96d5d6 | ||
|
|
f6b5c5d150 | ||
|
|
9cc65c615c | ||
|
|
d6845bd5e9 | ||
|
|
0b908db272 | ||
|
|
841ed43f11 | ||
|
|
60cd6f56be | ||
|
|
060fd55249 | ||
|
|
38c7f7300e | ||
|
|
f7a705c6da | ||
|
|
f497e4dd12 | ||
|
|
0a63083df7 | ||
|
|
5a6efdff44 | ||
|
|
7efb5a269c | ||
|
|
1caf672904 | ||
|
|
7743072411 | ||
|
|
c461c4f02e | ||
|
|
5b597f3a95 | ||
|
|
b69488685f | ||
|
|
afb01e3e90 | ||
|
|
7ff14dc26b | ||
|
|
0c33064193 | ||
|
|
61d77584e8 | ||
|
|
37ca9d7319 | ||
|
|
2c136f6355 | ||
|
|
52dcc7e350 | ||
|
|
ff6488371c | ||
|
|
0782b5abdd | ||
|
|
2e2ba96d75 | ||
|
|
853e38e054 | ||
|
|
418dfbf994 | ||
|
|
533a872118 | ||
|
|
2ae854e8ea | ||
|
|
3969383857 | ||
|
|
e4ebb91712 | ||
|
|
eb3c1b3c25 | ||
|
|
c257482838 | ||
|
|
0a46e64971 | ||
|
|
845420cf17 | ||
|
|
96ea0db88e | ||
|
|
d99c735e12 | ||
|
|
d48f4100e9 | ||
|
|
7e73d5fdac | ||
|
|
152cdfe9bc | ||
|
|
a9eedafbcb | ||
|
|
5baf191483 | ||
|
|
2d2e703884 | ||
|
|
026450ddf3 | ||
|
|
5646782d23 | ||
|
|
dd1c2e836b | ||
|
|
be73076e9e | ||
|
|
9d47be0d8a | ||
|
|
fcf3dbbad4 | ||
|
|
d344cc3bdd | ||
|
|
93e181b2da | ||
|
|
3867808927 | ||
|
|
c7c3b9ca90 | ||
|
|
54cfc21e28 | ||
|
|
f01514dba4 | ||
|
|
ee5723416e | ||
|
|
aab8ef2726 | ||
|
|
84c1ffd7cc | ||
|
|
273158a337 | ||
|
|
099f0e2d18 | ||
|
|
2dd85afdc2 | ||
|
|
cdeca9ed9d | ||
|
|
af77c0c987 | ||
|
|
f912bc78e6 | ||
|
|
137ee9334c | ||
|
|
36e5e964e5 | ||
|
|
495337743a | ||
|
|
775edab7b1 | ||
|
|
fe9fa17005 | ||
|
|
ef12a76a9e | ||
|
|
6b3de9d7da | ||
|
|
3599e4be16 | ||
|
|
8dc844e194 | ||
|
|
104c60840a | ||
|
|
f2cb098148 | ||
|
|
30b998eca3 | ||
|
|
b5133fe8c8 | ||
|
|
0d0f556f21 | ||
|
|
0e365395bf | ||
|
|
08ec133aac | ||
|
|
7d7391887a | ||
|
|
e7d4ccffe2 | ||
|
|
8538a3c148 | ||
|
|
cb4b992204 | ||
|
|
af4d54ab50 | ||
|
|
1faff84905 | ||
|
|
62fde21d9a | ||
|
|
6f3729a00f | ||
|
|
fbf66ba02b | ||
|
|
ed74f4d1d9 | ||
|
|
a268946f8d | ||
|
|
7432c6de84 | ||
|
|
7fe9342d0d | ||
|
|
a0e89e4d4e | ||
|
|
0c3a476449 | ||
|
|
de517c15ff | ||
|
|
b83d5b0dbf | ||
|
|
27924a259f | ||
|
|
530256b1bf | ||
|
|
23d15d7485 | ||
|
|
3c38d2e105 | ||
|
|
a53ffcf5e3 | ||
|
|
22366cf246 | ||
|
|
ddc2b86875 | ||
|
|
9e966615f2 | ||
|
|
3335fc727d | ||
|
|
00d7b38e02 | ||
|
|
2a8a98c432 | ||
|
|
13841491d4 | ||
|
|
2137c05cd6 | ||
|
|
6519630d46 | ||
|
|
7c6d6a4b12 | ||
|
|
745b33f174 | ||
|
|
153188db20 | ||
|
|
4a2ebd0c81 | ||
|
|
e701709645 | ||
|
|
1ca835f34d | ||
|
|
1c021ae5ca | ||
|
|
479a4c2880 | ||
|
|
5ce44ade17 | ||
|
|
f03ffa7641 | ||
|
|
b44185948d | ||
|
|
6b4532a08e | ||
|
|
86ad5506e3 | ||
|
|
6513349c09 | ||
|
|
92685189aa | ||
|
|
3b76622cf1 | ||
|
|
c5a524d3c6 | ||
|
|
17eb85b9cd | ||
|
|
3c688360fb | ||
|
|
9f220768c2 | ||
|
|
fd183c6ee5 | ||
|
|
9788fb16e8 | ||
|
|
39ed587959 | ||
|
|
c4327a0558 | ||
|
|
1b92d18777 | ||
|
|
a67503ae4a | ||
|
|
c54f39bea0 | ||
|
|
ff3138fa43 | ||
|
|
09d46942ca | ||
|
|
84e365d263 | ||
|
|
b31bcf5561 | ||
|
|
da50d682e1 | ||
|
|
094d310f5c | ||
|
|
642eaf92d7 | ||
|
|
76c032a2c4 | ||
|
|
a74b04d175 | ||
|
|
c9448feafc | ||
|
|
8314f3e30c | ||
|
|
935da2db49 | ||
|
|
b5e95fa1ef | ||
|
|
b60d8356cb | ||
|
|
ee7a7a2f9d | ||
|
|
b5eb824346 | ||
|
|
41867b89a0 | ||
|
|
7e7aa7aba0 | ||
|
|
fd1dab7c7b | ||
|
|
a69f9f01b3 | ||
|
|
c808ed1368 | ||
|
|
21be85071a | ||
|
|
a30c6ae1f7 | ||
|
|
0324884124 | ||
|
|
ad225b12c2 | ||
|
|
0dd5e5ab7d | ||
|
|
490f41cda8 | ||
|
|
c163438eaf | ||
|
|
ef925b0948 | ||
|
|
0fceb270ca | ||
|
|
4ec5d12213 | ||
|
|
d9c0e47581 | ||
|
|
8ec4a8ad46 | ||
|
|
40d355209b | ||
|
|
354702fcab | ||
|
|
bfc7ae1eff | ||
|
|
cccefe15b3 | ||
|
|
bb4236ffed | ||
|
|
14d57e780b | ||
|
|
76a43c8de6 | ||
|
|
b807435c42 | ||
|
|
3b040fd4b5 | ||
|
|
b9b9ed197e | ||
|
|
03523ab589 | ||
|
|
c4efe59a12 | ||
|
|
d46f53a004 | ||
|
|
5fb5fd4318 | ||
|
|
a3cb58484f | ||
|
|
04fd2cdcab | ||
|
|
a710c034e4 | ||
|
|
bd651d9ef3 | ||
|
|
7f3e8f9796 | ||
|
|
837311abce | ||
|
|
c596ee0256 | ||
|
|
5815d8f1dd | ||
|
|
2675e7b2e1 | ||
|
|
8f400dda85 | ||
|
|
2a605b93cd | ||
|
|
e4d65b2f3b | ||
|
|
87a45e88dc | ||
|
|
d6d90db957 | ||
|
|
eb669afb8f | ||
|
|
d1cf80001e | ||
|
|
307d11f503 | ||
|
|
73f527e772 | ||
|
|
5143ebece1 | ||
|
|
9996c2db80 | ||
|
|
0f26da4028 | ||
|
|
a3dd37b011 | ||
|
|
6b11b0ea8d | ||
|
|
faad7d5843 | ||
|
|
ef0d6d0c90 | ||
|
|
bd83fb3d38 | ||
|
|
f84e603318 | ||
|
|
d77459e4fc | ||
|
|
2c14bd353f | ||
|
|
0860508a1d | ||
|
|
f81daa16b3 | ||
|
|
436b00e3cb | ||
|
|
4d52aa8b7f | ||
|
|
c2d5488663 | ||
|
|
cc51d51a78 | ||
|
|
7f1068e37e | ||
|
|
81777fac47 | ||
|
|
9a6147b643 | ||
|
|
a2dacc333c | ||
|
|
088008a030 | ||
|
|
a198e9fce8 | ||
|
|
3f087e5fb1 | ||
|
|
5beb4a5f27 | ||
|
|
ba7eaca762 | ||
|
|
d31f897f9e | ||
|
|
e60598bafd | ||
|
|
df29767fc7 | ||
|
|
e58a1a2aad | ||
|
|
74f98e2b79 | ||
|
|
c4cfde3c4c | ||
|
|
5792d7b18d | ||
|
|
5fa7cff468 | ||
|
|
a76a2715ad | ||
|
|
2d2a61f7df | ||
|
|
9f963c0b61 | ||
|
|
69595a6bb4 | ||
|
|
bbac5a499a | ||
|
|
1b241b62f3 | ||
|
|
1f36595d19 | ||
|
|
e8c0f85016 | ||
|
|
2dbddef5e9 | ||
|
|
4a34ae5cdc | ||
|
|
b2ad958340 | ||
|
|
53217d5eb8 | ||
|
|
7a5dca2645 | ||
|
|
170cbb6635 | ||
|
|
ee2fffb52b | ||
|
|
68b62392bf | ||
|
|
222e1ce4a6 | ||
|
|
ac198b17bf | ||
|
|
4ed9c04e9b | ||
|
|
ce44312ac0 | ||
|
|
71104e9312 | ||
|
|
ced5f51482 | ||
|
|
c400491c07 | ||
|
|
72a1406b86 | ||
|
|
11e13d1873 | ||
|
|
6607b7fd62 | ||
|
|
8d862b5ad3 | ||
|
|
d40ec88b94 | ||
|
|
a82eb7def6 | ||
|
|
92e8b80da8 | ||
|
|
76a84ec9b1 | ||
|
|
7109692791 | ||
|
|
7ad3c70b68 | ||
|
|
0b20f69959 | ||
|
|
be0ebc7847 | ||
|
|
b5e2ded47a | ||
|
|
8953c2a7de | ||
|
|
13f58e0ca5 | ||
|
|
f436e915d3 | ||
|
|
72bfae9448 | ||
|
|
6aaed3b524 | ||
|
|
501f41fca1 | ||
|
|
06d80e5da3 | ||
|
|
8ddc05923b | ||
|
|
9cbc9bf2bc | ||
|
|
5489b49cc1 | ||
|
|
f6f4ccc6ea | ||
|
|
a5d14b377d | ||
|
|
3b91815240 | ||
|
|
aa30afbeda | ||
|
|
bdc2c8f456 | ||
|
|
37831f82a4 | ||
|
|
4049d3451a | ||
|
|
6614864d78 | ||
|
|
b771311593 | ||
|
|
78fe2beefb | ||
|
|
6a3902d90d | ||
|
|
d412887bf4 | ||
|
|
9c2526bbdd | ||
|
|
889b947792 | ||
|
|
0a0e7156e0 | ||
|
|
24a06d175e | ||
|
|
980bab3040 | ||
|
|
b6dceb4271 | ||
|
|
87a57cd63b | ||
|
|
25b8a482bc | ||
|
|
d7dd563df4 | ||
|
|
6d720b793d | ||
|
|
6cc3e0a19a | ||
|
|
380116d107 | ||
|
|
216b295f52 | ||
|
|
388980ed6c | ||
|
|
2a2983ace0 | ||
|
|
a7f56e164e | ||
|
|
db4183596c | ||
|
|
2b06e672de | ||
|
|
e596664753 | ||
|
|
79d1c96db4 | ||
|
|
cc7c235556 | ||
|
|
56960882ce | ||
|
|
b11c2c6cc5 | ||
|
|
12e0a1962d | ||
|
|
f379bea669 | ||
|
|
bf674151cc | ||
|
|
c11cb5fb3e | ||
|
|
941208cc64 | ||
|
|
9f3cbdc873 | ||
|
|
90c30879b1 | ||
|
|
0ca1318118 | ||
|
|
0be639b244 | ||
|
|
48b4cfc247 | ||
|
|
a4037b8278 | ||
|
|
30405056e3 | ||
|
|
0fbab7147a | ||
|
|
de57ef5ac7 | ||
|
|
f48a047fe0 | ||
|
|
8ba08432c5 | ||
|
|
bf34bd3a62 | ||
|
|
21845ca5ea | ||
|
|
768ef772bb | ||
|
|
69842c18f7 | ||
|
|
42a7cd3eea | ||
|
|
b7e5b906d1 | ||
|
|
ad271fac80 | ||
|
|
70ad323c9a | ||
|
|
27bf4c37a7 | ||
|
|
1cc31c1038 | ||
|
|
adb0dd43a7 | ||
|
|
d29538beb0 | ||
|
|
b99e4649a4 | ||
|
|
68daa3550e | ||
|
|
9d65282710 | ||
|
|
d8f3368b3c | ||
|
|
5755fe7bef | ||
|
|
4f071e3b31 | ||
|
|
f4dfc79808 | ||
|
|
518d5bc4c7 | ||
|
|
0e1f62a38d | ||
|
|
af4b59fe0a | ||
|
|
8c3c0484ed | ||
|
|
97828234dd | ||
|
|
20e64c71f8 | ||
|
|
2214d140c3 | ||
|
|
907d3efcd0 | ||
|
|
9573e02c32 | ||
|
|
8cb699290a | ||
|
|
31d7b4f9ce | ||
|
|
2e5ad3f3a0 | ||
|
|
5d3d5d23a1 | ||
|
|
469ff799ad | ||
|
|
18f2a09b35 | ||
|
|
81f6aec1a0 | ||
|
|
ff60605a7f | ||
|
|
7010e80336 | ||
|
|
5f790c1dbc | ||
|
|
8c5d8477fb | ||
|
|
10fe6929b0 | ||
|
|
6fc0000c8a | ||
|
|
e84a5589df | ||
|
|
e7d9ff12ec | ||
|
|
607f5959ab | ||
|
|
11546a1ce9 | ||
|
|
ee671836ca | ||
|
|
dd77d32446 | ||
|
|
b32fb05ab8 | ||
|
|
918d87dcb6 | ||
|
|
98ae05ee59 | ||
|
|
cff5c064a6 | ||
|
|
e9cef4b0ba | ||
|
|
7f2c8ff53d | ||
|
|
46d6b81058 | ||
|
|
6d59fb49aa | ||
|
|
97602f3fd7 | ||
|
|
f17987743e | ||
|
|
5767cce178 | ||
|
|
20a4bb1a88 | ||
|
|
578f29f215 | ||
|
|
6c9643e39e | ||
|
|
502ae7fd9f | ||
|
|
8cb527342a | ||
|
|
964c943dd9 | ||
|
|
a971ad1f85 | ||
|
|
e66b9de922 | ||
|
|
5db202169b | ||
|
|
b292b191ff | ||
|
|
450ff411ec | ||
|
|
8de92e54eb | ||
|
|
d0844c3e62 | ||
|
|
37d61e41ca | ||
|
|
0c7dad961d | ||
|
|
36f1fc4f9d | ||
|
|
517cb821fb | ||
|
|
ef6c3f8476 | ||
|
|
f84f0d5ad9 | ||
|
|
d8e98279c4 | ||
|
|
424ac606d8 | ||
|
|
2f35d9a013 | ||
|
|
e5259176c9 | ||
|
|
c370195698 | ||
|
|
0ba0bd3d77 | ||
|
|
d23a7b8523 | ||
|
|
935f503bc7 | ||
|
|
a0f0a8e021 | ||
|
|
6290ed8752 | ||
|
|
a38f0ba09e | ||
|
|
191624f334 | ||
|
|
5292a49bb1 | ||
|
|
22f01a2699 | ||
|
|
95238b6e17 |
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -7,3 +7,10 @@ updates:
|
||||
commit-message:
|
||||
prefix: "chore(cargo)"
|
||||
open-pull-requests-limit: 50
|
||||
|
||||
# Keep GitHub Actions up to date.
|
||||
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.78.0
|
||||
RUSTUP_TOOLCHAIN: 1.82.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -40,6 +40,18 @@ jobs:
|
||||
- name: Check
|
||||
run: cargo check --workspace --all-targets --all-features
|
||||
|
||||
npm_constants:
|
||||
name: Check if node constants are up to date
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- name: Rebuild constants
|
||||
run: npm run build:core:constants
|
||||
- name: Check that constants are not changed
|
||||
run: git diff --exit-code
|
||||
|
||||
cargo_deny:
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
@@ -47,7 +59,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
arguments: --all-features --workspace
|
||||
command: check
|
||||
@@ -83,11 +95,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.82.0
|
||||
- os: windows-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.82.0
|
||||
- os: macos-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.82.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
@@ -113,9 +125,6 @@ jobs:
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
|
||||
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Doc-Tests
|
||||
@@ -202,9 +211,9 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
@@ -254,11 +263,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: macos-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
- os: windows-latest
|
||||
python: 3.12
|
||||
python: 3.13
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
|
||||
2
.github/workflows/deltachat-rpc-server.yml
vendored
2
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -401,6 +401,6 @@ jobs:
|
||||
working-directory: deltachat-rpc-server/npm-package
|
||||
run: |
|
||||
ls -lah platform_package
|
||||
for platform in *.tgz; do npm publish --provenance "$platform"; done
|
||||
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1.1.1
|
||||
uses: dependabot/fetch-metadata@v2.2.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve a PR
|
||||
|
||||
80
.github/workflows/jsonrpc-client-npm-package.yml
vendored
80
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -1,82 +1,38 @@
|
||||
name: "jsonrpc js client build"
|
||||
name: "Publish @deltachat/jsonrpc-client"
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "!py-*"
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
pack-module:
|
||||
name: "Package @deltachat/jsonrpc-client and upload to download.delta.chat"
|
||||
name: "Publish @deltachat/jsonrpc-client"
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Install tree
|
||||
run: sudo apt install tree
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
continue-on-error: true
|
||||
- name: Get Pull Request ID
|
||||
id: prepare
|
||||
run: |
|
||||
tag=${{ steps.tag.outputs.tag }}
|
||||
if [ -z "$tag" ]; then
|
||||
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
|
||||
echo "No preview will be uploaded this time, but the $tag release"
|
||||
fi
|
||||
- name: System info
|
||||
run: |
|
||||
npm --version
|
||||
node --version
|
||||
echo $DELTACHAT_JSONRPC_TAR_GZ
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install dependencies without running scripts
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install --ignore-scripts
|
||||
|
||||
- name: Package
|
||||
shell: bash
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: |
|
||||
npm run build
|
||||
npm pack .
|
||||
ls -lah
|
||||
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: deltachat-jsonrpc-client.tgz
|
||||
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
# Upload to download.delta.chat/node/preview/
|
||||
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
|
||||
if: ${{ ! steps.tag.outputs.tag }}
|
||||
id: upload-preview
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||
continue-on-error: true
|
||||
- name: Post links to details
|
||||
if: steps.upload-preview.outcome == 'success'
|
||||
run: node ./node/scripts/postLinksToDetails.js
|
||||
|
||||
- name: Publish
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||
env:
|
||||
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
|
||||
# Upload to download.delta.chat/node/
|
||||
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
|
||||
if: ${{ steps.tag.outputs.tag }}
|
||||
id: upload
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
2
.github/workflows/node-docs.yml
vendored
2
.github/workflows/node-docs.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
mv docs js
|
||||
|
||||
- name: Upload
|
||||
uses: horochx/deploy-via-scp@v1.0.1
|
||||
uses: horochx/deploy-via-scp@1.1.0
|
||||
with:
|
||||
user: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.KEY }}
|
||||
|
||||
2
.github/workflows/upload-docs.yml
vendored
2
.github/workflows/upload-docs.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
show-progress: false
|
||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: npm install
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,7 +34,6 @@ deltachat-ffi/xml
|
||||
coverage/
|
||||
.DS_Store
|
||||
.vscode
|
||||
.vscode/launch.json
|
||||
python/accounts.txt
|
||||
python/all-testaccounts.txt
|
||||
tmp/
|
||||
@@ -51,4 +50,4 @@ result
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
.direnv
|
||||
|
||||
1000
CHANGELOG.md
1000
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ add_custom_command(
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
|
||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --features jsonrpc
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +32,66 @@ on the contributing page: <https://github.com/deltachat/deltachat-core-rust/cont
|
||||
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
||||
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
||||
|
||||
### SQL
|
||||
|
||||
Multi-line SQL statements should be formatted using string literals,
|
||||
for example
|
||||
```
|
||||
sql.execute(
|
||||
"CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT DEFAULT '' NOT NULL -- message text
|
||||
) STRICT",
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
|
||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||
or [`indoc!](https://docs.rs/indoc).
|
||||
Do not escape newlines like this:
|
||||
```
|
||||
sql.execute(
|
||||
"CREATE TABLE messages ( \
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, \
|
||||
text TEXT DEFAULT '' NOT NULL \
|
||||
) STRICT",
|
||||
)
|
||||
.await?;
|
||||
```
|
||||
Escaping newlines
|
||||
is prone to errors like this if space before backslash is missing:
|
||||
```
|
||||
"SELECT foo\
|
||||
FROM bar"
|
||||
```
|
||||
Literal above results in `SELECT fooFROM bar` string.
|
||||
This style also does not allow using `--` comments.
|
||||
|
||||
---
|
||||
|
||||
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
|
||||
to make SQLite check column types.
|
||||
|
||||
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
|
||||
This avoids reuse of the row IDs and can avoid dangerous bugs
|
||||
like forwarding wrong message because the message was deleted
|
||||
and another message took its row ID.
|
||||
|
||||
Declare all new columns as `NOT NULL`
|
||||
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
|
||||
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
|
||||
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
|
||||
Use `HAVING COUNT(*) > 0` clause
|
||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
||||
|
||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
||||
an older version. Also don't change the column type, consider adding a new column with another name
|
||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
||||
keyword doesn't help here.
|
||||
|
||||
### Commit messages
|
||||
|
||||
Commit messages follow the [Conventional Commits] notation.
|
||||
We use [git-cliff] to generate the changelog from commit messages before the release.
|
||||
|
||||
|
||||
3300
Cargo.lock
generated
3300
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
128
Cargo.toml
128
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.138.4"
|
||||
version = "1.148.4"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -34,94 +34,93 @@ strip = true
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
deltachat-time = { path = "./deltachat-time" }
|
||||
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
|
||||
deltachat-contact-tools = { workspace = true }
|
||||
format-flowed = { path = "./format-flowed" }
|
||||
ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.0"
|
||||
async-channel = "2.2.1"
|
||||
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
|
||||
async-broadcast = "0.7.1"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
base64 = { workspace = true }
|
||||
brotli = { version = "6", default-features=false, features = ["std"] }
|
||||
chrono = { workspace = true }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.9"
|
||||
fd-lock = "4"
|
||||
futures = "0.3"
|
||||
futures-lite = "2.3.0"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
hickory-resolver = "=0.25.0-alpha.2"
|
||||
http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.9"
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh = { version = "0.4.2", default-features = false }
|
||||
iroh-gossip = { version = "0.25.0", default-features = false, features = ["net"] }
|
||||
iroh-net = { version = "0.25.0", default-features = false }
|
||||
kamadak-exif = "0.5.3"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
libc = { workspace = true }
|
||||
mailparse = "0.15"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.11", default-features = false }
|
||||
pretty_env_logger = { version = "0.5", optional = true }
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.13.2", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.31"
|
||||
quick-xml = "0.36"
|
||||
quoted_printable = "0.5"
|
||||
rand = "0.8"
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { version = "0.12.2", features = ["json"] }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
rustls-pki-types = "1.9.0"
|
||||
rustls = { version = "0.23.13", default-features = false }
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.21.0", default-features = false, features = ["aead-cipher-2022"] }
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.1"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||
tokio-rustls = { version = "0.26.0", default-features = false }
|
||||
tokio-stream = { version = "0.1.16", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.9"
|
||||
tokio-util = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
# Pin OpenSSL to 3.1 releases.
|
||||
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
|
||||
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
|
||||
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
|
||||
# According to <https://www.openssl.org/policies/releasestrat.html>
|
||||
# 3.1 branch will be supported until 2025-03-14.
|
||||
openssl-src = "~300.1"
|
||||
webpki-roots = "0.26.6"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
pretty_assertions = "1.4.1"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
tempfile = { workspace = true }
|
||||
testdir = "0.9.0"
|
||||
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
@@ -166,16 +165,45 @@ harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
async-channel = "2.3.1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.38", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.30"
|
||||
futures-lite = "2.3.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
nu-ansi-term = "0.46"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.18.0"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.31"
|
||||
chrono = { version = "0.4.38", default-features=false, features = ["alloc", "clock", "std"] }
|
||||
rusqlite = "0.32"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.13.0"
|
||||
thiserror = "1"
|
||||
|
||||
# 1.38 is the latest version before `mio` dependency update
|
||||
# that broke compilation with Android NDK r23c and r24.
|
||||
# Version 1.39.0 cannot be compiled using these NDKs,
|
||||
# see issue <https://github.com/tokio-rs/tokio/issues/6748>
|
||||
# for details.
|
||||
tokio = "~1.38.1"
|
||||
|
||||
tokio-util = "0.7.11"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.2"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
vendored = [
|
||||
"async-native-tls/vendored",
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"reqwest/native-tls-vendored"
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl"
|
||||
]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
@@ -30,13 +30,13 @@ $ curl https://sh.rustup.rs -sSf | sh
|
||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||
|
||||
```
|
||||
$ cargo run -p deltachat-repl -- ~/deltachat-db
|
||||
$ cargo run --locked -p deltachat-repl -- ~/deltachat-db
|
||||
```
|
||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||
|
||||
Optionally, install `deltachat-repl` binary with
|
||||
```
|
||||
$ cargo install --path deltachat-repl/
|
||||
$ cargo install --locked --path deltachat-repl/
|
||||
```
|
||||
and run as
|
||||
```
|
||||
|
||||
17
RELEASE.md
17
RELEASE.md
@@ -4,21 +4,18 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
|
||||
|
||||
2. Run `npm run build:core:constants` in the root of the repository
|
||||
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
|
||||
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||
|
||||
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||
|
||||
4. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||
3. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
|
||||
|
||||
5. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
|
||||
6. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||
|
||||
7. Tag the release: `git tag -a v1.116.0`.
|
||||
6. Tag the release: `git tag -a v1.116.0`.
|
||||
|
||||
8. Push the release tag: `git push origin v1.116.0`.
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
|
||||
12
assets/qr_overlay_delta.svg-part
Executable file
12
assets/qr_overlay_delta.svg-part
Executable file
@@ -0,0 +1,12 @@
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
d="m 24.015419,1.2870249 c -12.549421,0 -22.7283936,10.1789711 -22.7283936,22.7283931 0,12.549422 10.1789726,22.728395 22.7283936,22.728395 14.337742,-0.342877 9.614352,-4.702705 23.697556,0.969161 -7.545453,-13.001555 -1.082973,-13.32964 -0.969161,-23.697556 0,-12.549422 -10.178973,-22.7283931 -22.728395,-22.7283931 z" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
d="M 23.982249,5.3106163 C 13.645822,5.4364005 5.2618355,13.92999 5.2618355,24.275753 c 0,10.345764 8.3839865,18.635301 18.7204135,18.509516 9.827724,-0.03951 7.516769,-5.489695 18.380082,-0.443187 -5.950849,-9.296115 0.201753,-10.533667 0.340336,-18.521947 0,-10.345766 -8.383989,-18.6353031 -18.720418,-18.5095187 z" />
|
||||
<g
|
||||
style="fill:#ffffff"
|
||||
transform="scale(1.1342891,0.88160947)">
|
||||
<path
|
||||
d="m 21.360141,23.513382 q -1.218487,-1.364705 -3.387392,-3.265543 -2.388233,-2.095797 -3.216804,-3.289913 -0.828571,-1.218486 -0.828571,-2.6563 0,-2.144536 1.998318,-3.363022 1.998317,-1.2428565 5.215121,-1.2428565 3.216804,0 5.605037,1.0966375 2.412603,1.096638 2.412603,3.021846 0,0.92605 -0.584873,1.535293 -0.584874,0.609243 -1.364705,0.609243 -1.121008,0 -2.631931,-1.681511 -1.535292,-1.705881 -2.60756,-2.388233 -1.047898,-0.706722 -2.461343,-0.706722 -1.803359,0 -2.973106,0.804201 -1.145377,0.804201 -1.145377,2.047057 0,1.169747 0.950419,2.193275 0.950419,1.023529 4.898315,3.728568 4.215963,2.899998 5.946213,4.532769 1.75462,1.632772 2.851258,3.972265 1.096638,2.339494 1.096638,4.947055 0,4.581508 -3.241174,8.090749 -3.216804,3.484871 -7.530245,3.484871 -3.923526,0 -6.628566,-2.802519 -2.705039,-2.802518 -2.705039,-7.481506 0,-4.508399 2.973106,-7.530245 2.997477,-3.021846 7.359658,-3.655459 z m 1.072268,1.121008 q -6.994112,1.145377 -6.994112,9.601672 0,4.36218 1.730251,6.774783 1.75462,2.412603 4.069744,2.412603 2.412603,0 3.972265,-2.315124 1.559663,-2.339493 1.559663,-6.311759 0,-5.751255 -4.337811,-10.162175 z" />
|
||||
</g>
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::context::Context;
|
||||
|
||||
@@ -12,7 +12,7 @@ anyhow = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||
chrono = { workspace = true }
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
clippy::bool_assert_comparison,
|
||||
clippy::manual_split_once,
|
||||
clippy::format_push_string,
|
||||
clippy::bool_to_int_with_if
|
||||
clippy::bool_to_int_with_if,
|
||||
clippy::manual_range_contains
|
||||
)]
|
||||
|
||||
use std::fmt;
|
||||
@@ -35,23 +36,30 @@ use chrono::{DateTime, NaiveDateTime};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
// TODOs to clean up:
|
||||
// - Check if sanitizing is done correctly everywhere
|
||||
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A Contact, as represented in a VCard.
|
||||
pub struct VcardContact {
|
||||
/// The email address, vcard property `email`
|
||||
pub addr: String,
|
||||
/// The contact's display name, vcard property `fn`
|
||||
pub display_name: String,
|
||||
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
|
||||
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
|
||||
pub authname: String,
|
||||
/// The contact's public PGP key in Base64, vcard property `key`
|
||||
pub key: Option<String>,
|
||||
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
||||
pub profile_image: Option<String>,
|
||||
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
||||
pub timestamp: Result<u64>,
|
||||
pub timestamp: Result<i64>,
|
||||
}
|
||||
|
||||
impl VcardContact {
|
||||
/// Returns the contact's display name.
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self.authname.is_empty() {
|
||||
false => &self.authname,
|
||||
true => &self.addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a vCard containing given contacts.
|
||||
@@ -60,7 +68,6 @@ pub struct VcardContact {
|
||||
pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
fn format_timestamp(c: &VcardContact) -> Option<String> {
|
||||
let timestamp = *c.timestamp.as_ref().ok()?;
|
||||
let timestamp: i64 = timestamp.try_into().ok()?;
|
||||
let datetime = DateTime::from_timestamp(timestamp, 0)?;
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
@@ -68,10 +75,7 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
let addr = &c.addr;
|
||||
let display_name = match c.display_name.is_empty() {
|
||||
false => &c.display_name,
|
||||
true => &c.addr,
|
||||
};
|
||||
let display_name = c.display_name();
|
||||
res += &format!(
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
@@ -93,7 +97,7 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
}
|
||||
|
||||
/// Parses `VcardContact`s from a given `&str`.
|
||||
pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
||||
let start_of_s = s.get(..prefix.len())?;
|
||||
|
||||
@@ -108,7 +112,9 @@ pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||
|
||||
// TODO this doesn't handle the case where there are quotes around a colon
|
||||
// Note: This doesn't handle the case where there are quotes around a colon,
|
||||
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
|
||||
// This could be improved in the future, but for now, the parsing is good enough.
|
||||
let (params, value) = remainder.split_once(':')?;
|
||||
// In the example from above, `params` is now `;TYPE=work`
|
||||
// and `value` is now `alice@example.com`
|
||||
@@ -125,7 +131,7 @@ pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
fn parse_datetime(datetime: &str) -> Result<u64> {
|
||||
fn parse_datetime(datetime: &str) -> Result<i64> {
|
||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
||||
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
|
||||
// ISO.8601, but fails to parse any of the examples given.
|
||||
@@ -144,10 +150,14 @@ pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
Err(_) => return Err(e.into()),
|
||||
},
|
||||
};
|
||||
Ok(timestamp.try_into()?)
|
||||
Ok(timestamp)
|
||||
}
|
||||
|
||||
let mut lines = vcard.lines().peekable();
|
||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||
|
||||
let mut lines = unfolded_lines.lines().peekable();
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
while lines.peek().is_some() {
|
||||
@@ -164,7 +174,15 @@ pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
let mut photo = None;
|
||||
let mut datetime = None;
|
||||
|
||||
for line in lines.by_ref() {
|
||||
for mut line in lines.by_ref() {
|
||||
if let Some(remainder) = remove_prefix(line, "item1.") {
|
||||
// Remove the group name, if the group is called "item1".
|
||||
// If necessary, we can improve this to also remove groups that are called something different that "item1".
|
||||
//
|
||||
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
|
||||
line = remainder;
|
||||
}
|
||||
|
||||
if let Some(email) = vcard_property(line, "email") {
|
||||
addr.get_or_insert(email);
|
||||
} else if let Some(name) = vcard_property(line, "fn") {
|
||||
@@ -172,11 +190,15 @@ pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
|
||||
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
|
||||
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
|
||||
.or_else(|| remove_prefix(line, "KEY;PREF=1:data:application/pgp-keys;base64,"))
|
||||
{
|
||||
key.get_or_insert(k);
|
||||
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
|
||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
|
||||
{
|
||||
photo.get_or_insert(p);
|
||||
@@ -187,11 +209,11 @@ pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
}
|
||||
}
|
||||
|
||||
let (display_name, addr) =
|
||||
let (authname, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
|
||||
contacts.push(VcardContact {
|
||||
display_name,
|
||||
authname,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
@@ -201,7 +223,7 @@ pub fn parse_vcard(vcard: &str) -> Result<Vec<VcardContact>> {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(contacts)
|
||||
contacts
|
||||
}
|
||||
|
||||
/// Valid contact address.
|
||||
@@ -249,27 +271,27 @@ impl rusqlite::types::ToSql for ContactAddress {
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the name and address
|
||||
/// Takes a name and an address and sanitizes them:
|
||||
/// - Extracts a name from the addr if the addr is in form "Alice <alice@example.org>"
|
||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
||||
/// - Removes the name if it is equal to the address by setting it to ""
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
|
||||
captures.get(1).map_or("", |m| m.as_str())
|
||||
} else {
|
||||
strip_rtlo_characters(name)
|
||||
name
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
strip_rtlo_characters(&normalize_name(name)),
|
||||
addr.to_string(),
|
||||
)
|
||||
(name, addr.to_string())
|
||||
};
|
||||
let mut name = normalize_name(&name);
|
||||
let mut name = sanitize_name(name);
|
||||
|
||||
// If the 'display name' is just the address, remove it:
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||
@@ -281,31 +303,77 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
(name, addr)
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
/// Sanitizes a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
/// - Removes newlines and trims the string
|
||||
/// - Removes quotes (come from some bad MUA implementations)
|
||||
/// - Removes potentially-malicious bidi characters
|
||||
pub fn sanitize_name(name: &str) -> String {
|
||||
let name = sanitize_single_line(name);
|
||||
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
match name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => name
|
||||
.get(1..name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
_ => name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitizes user input
|
||||
///
|
||||
/// - Removes newlines and trims the string
|
||||
/// - Removes potentially-malicious bidi characters
|
||||
pub fn sanitize_single_line(input: &str) -> String {
|
||||
sanitize_bidi_characters(input.replace(['\n', '\r'], " ").trim())
|
||||
}
|
||||
|
||||
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
|
||||
/// This method strips all occurrences of the RTLO Unicode character.
|
||||
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
|
||||
pub fn strip_rtlo_characters(input_str: &str) -> String {
|
||||
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
|
||||
const ISOLATE_CHARACTERS: [char; 3] = ['\u{2066}', '\u{2067}', '\u{2068}'];
|
||||
const POP_ISOLATE_CHARACTER: char = '\u{2069}';
|
||||
/// Some control unicode characters can influence whether adjacent text is shown from
|
||||
/// left to right or from right to left.
|
||||
///
|
||||
/// Since user input is not supposed to influence how adjacent text looks,
|
||||
/// this function removes some of these characters.
|
||||
///
|
||||
/// Also see https://github.com/deltachat/deltachat-core-rust/issues/3479.
|
||||
pub fn sanitize_bidi_characters(input_str: &str) -> String {
|
||||
// RTLO_CHARACTERS are apparently rarely used in practice.
|
||||
// They can impact all following text, so, better remove them all:
|
||||
let input_str = input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "");
|
||||
|
||||
// If the ISOLATE characters are not ended with a POP DIRECTIONAL ISOLATE character,
|
||||
// we regard the input as potentially malicious and simply remove all ISOLATE characters.
|
||||
// See https://en.wikipedia.org/wiki/Bidirectional_text#Unicode_bidi_support
|
||||
// and https://www.w3.org/International/questions/qa-bidi-unicode-controls.en
|
||||
// for an explanation about ISOLATE characters.
|
||||
fn isolate_characters_are_valid(input_str: &str) -> bool {
|
||||
let mut isolate_character_nesting: i32 = 0;
|
||||
for char in input_str.chars() {
|
||||
if ISOLATE_CHARACTERS.contains(&char) {
|
||||
isolate_character_nesting += 1;
|
||||
} else if char == POP_ISOLATE_CHARACTER {
|
||||
isolate_character_nesting -= 1;
|
||||
}
|
||||
|
||||
// According to Wikipedia, 125 levels are allowed:
|
||||
// https://en.wikipedia.org/wiki/Unicode_control_characters
|
||||
// (although, in practice, we could also significantly lower this number)
|
||||
if isolate_character_nesting < 0 || isolate_character_nesting > 125 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
isolate_character_nesting == 0
|
||||
}
|
||||
|
||||
if isolate_characters_are_valid(&input_str) {
|
||||
input_str
|
||||
} else {
|
||||
input_str.replace(
|
||||
|char| ISOLATE_CHARACTERS.contains(&char) || POP_ISOLATE_CHARACTER == char,
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
@@ -430,17 +498,16 @@ EMAIL;PREF=1:bobzzz@freenet.de
|
||||
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||
END:VCARD
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Mueller".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
|
||||
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
|
||||
assert_eq!(contacts[1].display_name, "".to_string());
|
||||
assert_eq!(contacts[1].authname, "".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
assert!(contacts[1].timestamp.is_err());
|
||||
@@ -461,11 +528,10 @@ KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
||||
REV:20240418T184242Z
|
||||
|
||||
END:VCARD",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
||||
@@ -478,27 +544,48 @@ END:VCARD",
|
||||
let contacts = [
|
||||
VcardContact {
|
||||
addr: "alice@example.org".to_string(),
|
||||
display_name: "Alice Wonderland".to_string(),
|
||||
authname: "Alice Wonderland".to_string(),
|
||||
key: Some("[base64-data]".to_string()),
|
||||
profile_image: Some("image in Base64".to_string()),
|
||||
timestamp: Ok(1713465762),
|
||||
},
|
||||
VcardContact {
|
||||
addr: "bob@example.com".to_string(),
|
||||
display_name: "".to_string(),
|
||||
authname: "".to_string(),
|
||||
key: None,
|
||||
profile_image: None,
|
||||
timestamp: Ok(0),
|
||||
},
|
||||
];
|
||||
let items = [
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:alice@example.org\n\
|
||||
FN:Alice Wonderland\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\n\
|
||||
REV:20240418T184242Z\n\
|
||||
END:VCARD\n",
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
EMAIL:bob@example.com\n\
|
||||
FN:bob@example.com\n\
|
||||
REV:19700101T000000Z\n\
|
||||
END:VCARD\n",
|
||||
];
|
||||
let mut expected = "".to_string();
|
||||
for len in 0..=contacts.len() {
|
||||
let contacts = &contacts[0..len];
|
||||
let vcard = make_vcard(contacts);
|
||||
let parsed = parse_vcard(&vcard).unwrap();
|
||||
if len > 0 {
|
||||
expected += items[len - 1];
|
||||
}
|
||||
assert_eq!(vcard, expected);
|
||||
let parsed = parse_vcard(&vcard);
|
||||
assert_eq!(parsed.len(), contacts.len());
|
||||
for i in 0..parsed.len() {
|
||||
assert_eq!(parsed[i].addr, contacts[i].addr);
|
||||
assert_eq!(parsed[i].display_name, contacts[i].display_name);
|
||||
assert_eq!(parsed[i].authname, contacts[i].authname);
|
||||
assert_eq!(parsed[i].key, contacts[i].key);
|
||||
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
|
||||
assert_eq!(
|
||||
@@ -572,16 +659,15 @@ FN:Alice
|
||||
EMAIL;HOME:alice@example.org
|
||||
END:VCARD
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Bob".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
|
||||
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[1].display_name, "Alice".to_string());
|
||||
assert_eq!(contacts[1].authname, "Alice".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
|
||||
@@ -597,19 +683,128 @@ END:VCARD
|
||||
EMAIL;TYPE=work:alice@example.org\n\
|
||||
REV:20240418T184242\n\
|
||||
END:VCARD",
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(
|
||||
*contacts[0].timestamp.as_ref().unwrap(),
|
||||
chrono::offset::Local
|
||||
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
|
||||
.unwrap()
|
||||
.timestamp()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_with_base64_avatar() {
|
||||
// This is not an actual base64-encoded avatar, it's just to test the parsing.
|
||||
// This one is Android-like.
|
||||
let vcard0 = "BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
EMAIL;HOME:bob@example.org
|
||||
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
|
||||
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
|
||||
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
|
||||
|
||||
END:VCARD
|
||||
";
|
||||
// This one is DOS-like.
|
||||
let vcard1 = vcard0.replace('\n', "\r\n");
|
||||
for vcard in [vcard0, vcard1.as_str()] {
|
||||
let contacts = parse_vcard(vcard);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protonmail_vcard() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN;PREF=1:Alice Wonderland
|
||||
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
|
||||
ITEM1.EMAIL;PREF=1:alice@example.org
|
||||
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
ITEM1.X-PM-ENCRYPT:true
|
||||
ITEM1.X-PM-SIGN:true
|
||||
END:VCARD",
|
||||
);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_name() {
|
||||
assert_eq!(&sanitize_name(" hello world "), "hello world");
|
||||
assert_eq!(&sanitize_name("<"), "<");
|
||||
assert_eq!(&sanitize_name(">"), ">");
|
||||
assert_eq!(&sanitize_name("'"), "'");
|
||||
assert_eq!(&sanitize_name("\""), "\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_single_line() {
|
||||
assert_eq!(sanitize_single_line("Hi\naiae "), "Hi aiae");
|
||||
assert_eq!(sanitize_single_line("\r\nahte\n\r"), "ahte");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_bidi_characters() {
|
||||
// Legit inputs:
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2067}ting Delta Chat\u{2069}"),
|
||||
"Tes\u{2067}ting Delta Chat\u{2069}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2067}ting \u{2068} Delta Chat\u{2069}\u{2069}"),
|
||||
"Tes\u{2067}ting \u{2068} Delta Chat\u{2069}\u{2069}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2067}ting\u{2069} Delta Chat\u{2067}\u{2069}"),
|
||||
"Tes\u{2067}ting\u{2069} Delta Chat\u{2067}\u{2069}"
|
||||
);
|
||||
|
||||
// Potentially-malicious inputs:
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{202C}ting Delta Chat"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Testing Delta Chat\u{2069}"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2067}ting Delta Chat"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2069}ting Delta Chat\u{2067}"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2068}ting Delta Chat"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.138.4"
|
||||
version = "1.148.4"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -14,21 +14,21 @@ name = "deltachat"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
deltachat = { path = "../", default-features = false }
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
||||
libc = "0.2"
|
||||
deltachat = { workspace = true, default-features = false }
|
||||
deltachat-jsonrpc = { workspace = true, optional = true }
|
||||
libc = { workspace = true }
|
||||
human-panic = { version = "2", default-features = false }
|
||||
num-traits = "0.2"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.8"
|
||||
once_cell = "1.18.0"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||
num-traits = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
yerpc = { workspace = true, features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
vendored = ["deltachat/vendored"]
|
||||
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
|
||||
jsonrpc = ["dep:deltachat-jsonrpc"]
|
||||
|
||||
|
||||
@@ -403,13 +403,10 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `send_port` = SMTP-port, guessed if left out
|
||||
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
|
||||
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
|
||||
* - `socks5_enabled` = SOCKS5 enabled
|
||||
* - `socks5_host` = SOCKS5 proxy server host
|
||||
* - `socks5_port` = SOCKS5 proxy server port
|
||||
* - `socks5_user` = SOCKS5 proxy username
|
||||
* - `socks5_password` = SOCKS5 proxy password
|
||||
* - `proxy_enabled` = Proxy enabled. Disabled by default.
|
||||
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
|
||||
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
|
||||
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
|
||||
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
|
||||
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
|
||||
* - `selfavatar` = File containing avatar. Will immediately be copied to the
|
||||
@@ -420,9 +417,10 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* and also recoded to a reasonable size.
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts (default)
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
|
||||
* 1=send a copy of outgoing messages to self.
|
||||
* 1=send and request read receipts
|
||||
* default=send and request read receipts, only send but not reuqest if `bot` is set
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self,
|
||||
* 1=send a copy of outgoing messages to self (default).
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
|
||||
@@ -481,8 +479,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `bot` = Set to "1" if this is a bot.
|
||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||
* adds Auto-Submitted header to outgoing messages,
|
||||
* accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
|
||||
* and does not cut large incoming text messages.
|
||||
* accepts contact requests automatically (calling dc_accept_chat() is not needed),
|
||||
* does not cut large incoming text messages,
|
||||
* handles existing messages the same way as new ones if `fetch_existing_msgs=1`.
|
||||
* - `last_msg_id` = database ID of the last message processed by the bot.
|
||||
* This ID and IDs below it are guaranteed not to be returned
|
||||
* by dc_get_next_msgs() and dc_wait_next_msgs().
|
||||
@@ -493,8 +492,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* For most bots calling `dc_markseen_msgs()` is the
|
||||
* recommended way to update this value
|
||||
* even for self-sent messages.
|
||||
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
||||
* 0=do not fetch existing messages on configure.
|
||||
* - `fetch_existing_msgs` = 0=do not fetch existing messages on configure (default),
|
||||
* 1=fetch most recent existing messages on configure.
|
||||
* In both cases, existing recipients are added to the contact database.
|
||||
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
|
||||
* 0=use IMAP IDLE if the server supports it.
|
||||
@@ -518,11 +517,21 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
|
||||
* until `dc_accept_chat()` is called.
|
||||
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
* In contrast to `dc_set_chat_mute_duration()`,
|
||||
* fresh message and badge counters are not changed by this setting,
|
||||
* but should be tuned down where appropriate.
|
||||
* - `private_tag` = Optional tag as "Work", "Family".
|
||||
* Meant to help profile owner to differ between profiles with similar names.
|
||||
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
||||
* The prefix should be followed by the system and maybe subsystem,
|
||||
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
||||
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
|
||||
* however, are not handled by the core otherwise.
|
||||
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
|
||||
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
|
||||
* 1 = WebXDC realtime API is enabled.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -857,13 +866,10 @@ void dc_maybe_network (dc_context_t* context);
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param addr The e-mail address of the user. This must match the
|
||||
* configured_addr setting of the context as well as the UID of the key.
|
||||
* @param public_data Ignored, actual public key is extracted from secret_data.
|
||||
* @param secret_data ASCII armored secret key.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
int dc_preconfigure_keypair (dc_context_t* context, const char *addr, const char *public_data, const char *secret_data);
|
||||
int dc_preconfigure_keypair (dc_context_t* context, const char *secret_data);
|
||||
|
||||
|
||||
// handle chatlists
|
||||
@@ -1543,30 +1549,6 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
|
||||
dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t chat_id, int msg_type, int msg_type2, int msg_type3);
|
||||
|
||||
|
||||
/**
|
||||
* Search next/previous message based on a given message and a list of types.
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param msg_id The ID of the current message from which the next or previous message should be searched.
|
||||
* @param dir 1=get the next message, -1=get the previous one.
|
||||
* @param msg_type The message type to search for.
|
||||
* If 0, the message type from curr_msg_id is used.
|
||||
* @param msg_type2 Alternative message type to search for. 0 to skip.
|
||||
* @param msg_type3 Alternative message type to search for. 0 to skip.
|
||||
* @return Returns the message ID that should be played next.
|
||||
* The returned message is in the same chat as the given one
|
||||
* and has one of the given types.
|
||||
* Typically, this result is passed again to dc_get_next_media()
|
||||
* later on the next swipe.
|
||||
* If there is not next/previous message, the function returns 0.
|
||||
*/
|
||||
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
|
||||
|
||||
|
||||
/**
|
||||
* Set chat visibility to pinned, archived or normal.
|
||||
*
|
||||
@@ -2495,7 +2477,9 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_BACKUP 251
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
#define DC_QR_URL 332 // text1=URL
|
||||
@@ -2541,6 +2525,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
*
|
||||
* - DC_QR_BACKUP:
|
||||
* - DC_QR_BACKUP2:
|
||||
* ask the user if they want to set up a new device.
|
||||
* If so, pass the qr-code to dc_receive_backup().
|
||||
*
|
||||
@@ -2548,6 +2533,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_PROXY with dc_lot_t::text1=address:
|
||||
* ask the user if they want to use the given proxy.
|
||||
* if so, call dc_set_config_from_qr() and restart I/O.
|
||||
*
|
||||
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
|
||||
* e-mail address scanned, optionally, a draft message could be set in
|
||||
* dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT;
|
||||
@@ -2622,6 +2611,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
* Get QR code image from the QR code text generated by dc_get_securejoin_qr().
|
||||
* See dc_get_securejoin_qr() for details about the contained QR code.
|
||||
*
|
||||
* @deprecated 2024-10 use dc_create_qr_svg(dc_get_securejoin_qr()) instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id group-chat-id for secure-join or 0 for setup-contact,
|
||||
@@ -2802,6 +2792,22 @@ dc_array_t* dc_get_locations (dc_context_t* context, uint32_t cha
|
||||
void dc_delete_all_locations (dc_context_t* context);
|
||||
|
||||
|
||||
// misc
|
||||
|
||||
/**
|
||||
* Create a QR code from any input data.
|
||||
*
|
||||
* The QR code is returned as a square SVG image.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param payload The content for the QR code.
|
||||
* @return SVG image with the QR code.
|
||||
* On errors, an empty string is returned.
|
||||
* The returned string must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_create_qr_svg (const char* payload);
|
||||
|
||||
|
||||
/**
|
||||
* Get last error string.
|
||||
*
|
||||
@@ -2890,6 +2896,7 @@ char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider);
|
||||
* This works like dc_backup_provider_qr() but returns the text of a rendered
|
||||
* SVG image containing the QR code.
|
||||
*
|
||||
* @deprecated 2024-10 use dc_create_qr_svg(dc_backup_provider_get_qr()) instead.
|
||||
* @memberof dc_backup_provider_t
|
||||
* @param backup_provider The backup provider object as created by
|
||||
* dc_backup_provider_new().
|
||||
@@ -2929,7 +2936,7 @@ void dc_backup_provider_unref (dc_backup_provider_t* backup_provider);
|
||||
* Gets a backup offered by a dc_backup_provider_t object on another device.
|
||||
*
|
||||
* This function is called on a device that scanned the QR code offered by
|
||||
* dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a
|
||||
* dc_backup_provider_get_qr(). Typically this is a
|
||||
* different device than that which provides the backup.
|
||||
*
|
||||
* This call will block while the backup is being transferred and only
|
||||
@@ -5480,6 +5487,11 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
*/
|
||||
#define DC_MSG_WEBXDC 80
|
||||
|
||||
/**
|
||||
* Message containing shared contacts represented as a vCard (virtual contact file)
|
||||
* with email addresses and possibly other fields.
|
||||
*/
|
||||
#define DC_MSG_VCARD 90
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -6039,6 +6051,21 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_REACTIONS_CHANGED 2001
|
||||
|
||||
|
||||
/**
|
||||
* A reaction to one's own sent message received.
|
||||
* Typically, the UI will show a notification for that.
|
||||
*
|
||||
* In addition to this event, DC_EVENT_REACTIONS_CHANGED is emitted.
|
||||
*
|
||||
* @param data1 (int) contact_id ID of the contact sending this reaction.
|
||||
* @param data2 (int) msg_id + (char*) reaction.
|
||||
* ID of the message for which a reaction was received in dc_event_get_data2_int(),
|
||||
* and the reaction as dc_event_get_data2_str().
|
||||
* string must be passed to dc_str_unref() afterwards.
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_REACTION 2002
|
||||
|
||||
|
||||
/**
|
||||
* There is a fresh message. Typically, the user will show an notification
|
||||
* when receiving this message.
|
||||
@@ -6256,7 +6283,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* webxdc status update received.
|
||||
* Webxdc status update received.
|
||||
* To get the received status update, use dc_get_webxdc_status_updates() with
|
||||
* `serial` set to the last known update
|
||||
* (in case of special bots, `status_update_serial` from `data2`
|
||||
@@ -6279,6 +6306,27 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
|
||||
|
||||
/**
|
||||
* Data received over an ephemeral peer channel.
|
||||
*
|
||||
* @param data1 (int) msg_id
|
||||
* @param data2 (int) + (char*) binary data.
|
||||
* length is returned as integer with dc_event_get_data2_int()
|
||||
* and binary data is returned as dc_event_get_data2_str().
|
||||
* Binary data must be passed to dc_str_unref() afterwards.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
|
||||
|
||||
/**
|
||||
* Advertisement for ephemeral peer channel communication received.
|
||||
* This can be used by bots to initiate peer-to-peer communication from their side.
|
||||
* @param data1 (int) msg_id
|
||||
* @param data2 0
|
||||
*/
|
||||
|
||||
#define DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT 2151
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
*
|
||||
@@ -6307,6 +6355,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
|
||||
|
||||
|
||||
/**
|
||||
* Inform that some events have been skipped due to event channel overflow.
|
||||
*
|
||||
* @param data1 (int) number of events that have been skipped
|
||||
*/
|
||||
#define DC_EVENT_CHANNEL_OVERFLOW 2400
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -6614,12 +6670,16 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "Message opened"
|
||||
///
|
||||
/// Used in subjects of outgoing read receipts.
|
||||
///
|
||||
/// @deprecated Deprecated 2024-07-26
|
||||
#define DC_STR_READRCPT 31
|
||||
|
||||
/// "The message '%1$s' you sent was displayed on the screen of the recipient."
|
||||
///
|
||||
/// Used as message text of outgoing read receipts.
|
||||
/// - %1$s will be replaced by the subject of the displayed message
|
||||
///
|
||||
/// @deprecated Deprecated 2024-06-23
|
||||
#define DC_STR_READRCPT_MAILBODY 32
|
||||
|
||||
/// @deprecated Deprecated, this string is no longer needed.
|
||||
@@ -7338,6 +7398,9 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as info message.
|
||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
#![warn(unused, clippy::all)]
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
@@ -29,7 +30,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
@@ -540,6 +541,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ErrorSelfNotInGroup(_) => 410,
|
||||
EventType::MsgsChanged { .. } => 2000,
|
||||
EventType::ReactionsChanged { .. } => 2001,
|
||||
EventType::IncomingReaction { .. } => 2002,
|
||||
EventType::IncomingMsg { .. } => 2005,
|
||||
EventType::IncomingMsgBunch { .. } => 2006,
|
||||
EventType::MsgsNoticed { .. } => 2008,
|
||||
@@ -561,9 +563,15 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ConfigSynced { .. } => 2111,
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||
EventType::WebxdcRealtimeData { .. } => 2150,
|
||||
EventType::WebxdcRealtimeAdvertisementReceived { .. } => 2151,
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +602,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone => 0,
|
||||
EventType::ChatlistChanged => 0,
|
||||
EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
@@ -616,11 +625,17 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
|
||||
contact_id.to_u32() as libc::c_int
|
||||
}
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { msg_id, .. }
|
||||
| EventType::WebxdcStatusUpdate { msg_id, .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
|
||||
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
EventType::EventChannelOverflow { n } => *n as libc::c_int,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,10 +674,13 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ConfigSynced { .. } => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingReaction { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
@@ -675,6 +693,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
status_update_serial,
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,7 +747,9 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged => ptr::null_mut(),
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -741,6 +765,19 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
let data2 = key.to_string().to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::WebxdcRealtimeData { data, .. } => {
|
||||
let ptr = libc::malloc(data.len());
|
||||
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
EventType::IncomingReaction { reaction, .. } => reaction
|
||||
.as_str()
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,8 +859,6 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
_public_data: *const libc::c_char,
|
||||
secret_data: *const libc::c_char,
|
||||
) -> i32 {
|
||||
if context.is_null() {
|
||||
@@ -831,9 +866,8 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let addr = to_string_lossy(addr);
|
||||
let secret_data = to_string_lossy(secret_data);
|
||||
block_on(preconfigure_keypair(ctx, &addr, &secret_data))
|
||||
block_on(preconfigure_keypair(ctx, &secret_data))
|
||||
.context("Failed to save keypair")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
@@ -1433,48 +1467,6 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(deprecated)]
|
||||
pub unsafe extern "C" fn dc_get_next_media(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
dir: libc::c_int,
|
||||
msg_type: libc::c_int,
|
||||
or_msg_type2: libc::c_int,
|
||||
or_msg_type3: libc::c_int,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_next_media()");
|
||||
return 0;
|
||||
}
|
||||
let direction = if dir < 0 {
|
||||
chat::Direction::Backward
|
||||
} else {
|
||||
chat::Direction::Forward
|
||||
};
|
||||
|
||||
let ctx = &*context;
|
||||
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {msg_type}"));
|
||||
let or_msg_type2 =
|
||||
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {or_msg_type2}"));
|
||||
let or_msg_type3 =
|
||||
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {or_msg_type3}"));
|
||||
|
||||
block_on(async move {
|
||||
chat::get_next_media(
|
||||
ctx,
|
||||
MsgId::new(msg_id),
|
||||
direction,
|
||||
msg_type,
|
||||
or_msg_type2,
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||
context: *mut dc_context_t,
|
||||
@@ -2602,6 +2594,18 @@ pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
|
||||
if payload.is_null() {
|
||||
eprintln!("ignoring careless call to dc_create_qr_svg()");
|
||||
return "".strdup();
|
||||
}
|
||||
|
||||
create_qr_svg(&to_string_lossy(payload))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_last_error(context: *mut dc_context_t) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
@@ -4351,7 +4355,7 @@ pub unsafe extern "C" fn dc_backup_provider_wait(provider: *mut dc_backup_provid
|
||||
let ctx = &*ffi_provider.context;
|
||||
let provider = &mut ffi_provider.provider;
|
||||
block_on(provider)
|
||||
.context("Failed to await BackupProvider")
|
||||
.context("Failed to await backup provider")
|
||||
.log_err(ctx)
|
||||
.set_last_error(ctx)
|
||||
.ok();
|
||||
@@ -4405,7 +4409,7 @@ trait ResultExt<T, E> {
|
||||
/// Like `log_err()`, but:
|
||||
/// - returns the default value instead of an Err value.
|
||||
/// - emits an error instead of a warning for an [Err] result. This means
|
||||
/// that the error will be shown to the user in a small pop-up.
|
||||
/// that the error will be shown to the user in a small pop-up.
|
||||
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
|
||||
}
|
||||
|
||||
@@ -4524,19 +4528,16 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
let addr = to_string_lossy(addr);
|
||||
|
||||
let ctx = &*context;
|
||||
let socks5_enabled = block_on(async move {
|
||||
ctx.get_config_bool(config::Config::Socks5Enabled)
|
||||
.await
|
||||
.context("Can't get config")
|
||||
.log_err(ctx)
|
||||
});
|
||||
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
|
||||
.context("Can't get config")
|
||||
.log_err(ctx);
|
||||
|
||||
match socks5_enabled {
|
||||
Ok(socks5_enabled) => {
|
||||
match proxy_enabled {
|
||||
Ok(proxy_enabled) => {
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
socks5_enabled,
|
||||
proxy_enabled,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -34,33 +34,34 @@ pub enum Meaning {
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
pub fn get_text1(&self) -> Option<&str> {
|
||||
pub fn get_text1(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => None,
|
||||
Some(SummaryPrefix::Draft(text)) => Some(text),
|
||||
Some(SummaryPrefix::Username(username)) => Some(username),
|
||||
Some(SummaryPrefix::Me(text)) => Some(text),
|
||||
Some(SummaryPrefix::Draft(text)) => Some(Cow::Borrowed(text)),
|
||||
Some(SummaryPrefix::Username(username)) => Some(Cow::Borrowed(username)),
|
||||
Some(SummaryPrefix::Me(text)) => Some(Cow::Borrowed(text)),
|
||||
},
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { .. } => None,
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::FprOk { .. } => None,
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::Backup { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Url { url } => Some(url),
|
||||
Qr::Text { text } => Some(text),
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
||||
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::Login { address, .. } => Some(address),
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
||||
},
|
||||
Self::Error(err) => Some(err),
|
||||
Self::Error(err) => Some(Cow::Borrowed(err)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +102,9 @@ impl Lot {
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup { .. } => LotState::QrBackup,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Proxy { .. } => LotState::QrProxy,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
Qr::Text { .. } => LotState::QrText,
|
||||
@@ -126,8 +128,9 @@ impl Lot {
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Proxy { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
@@ -177,9 +180,14 @@ pub enum LotState {
|
||||
|
||||
QrBackup = 251,
|
||||
|
||||
QrBackup2 = 252,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// text1=address, text2=protocol
|
||||
QrProxy = 271,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.138.4"
|
||||
version = "1.148.4"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
@@ -13,29 +13,30 @@ path = "src/webserver.rs"
|
||||
required-features = ["webserver"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
deltachat = { path = ".." }
|
||||
num-traits = "0.2"
|
||||
schemars = "0.8.19"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.10.1"
|
||||
log = "0.4"
|
||||
async-channel = { version = "2.2.1" }
|
||||
futures = { version = "0.3.30" }
|
||||
serde_json = "1"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||
tokio = { version = "1.37.0" }
|
||||
sanitize-filename = "0.5"
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true }
|
||||
deltachat-contact-tools = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
schemars = "0.8.21"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tempfile = { workspace = true }
|
||||
log = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.12", features = ["json_value"] }
|
||||
tokio = { workspace = true }
|
||||
sanitize-filename = { workspace = true }
|
||||
walkdir = "2.5.0"
|
||||
base64 = "0.22"
|
||||
base64 = { workspace = true }
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.11.3", optional = true }
|
||||
env_logger = { version = "0.11.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::str;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
@@ -17,12 +18,14 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use deltachat::context::get_info;
|
||||
use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::get_msg_read_receipts;
|
||||
use deltachat::message::{
|
||||
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use deltachat::peer_channels::{
|
||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||
};
|
||||
use deltachat::provider::get_provider_info;
|
||||
use deltachat::qr::{self, Qr};
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
@@ -31,6 +34,7 @@ use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use deltachat::{imex, info};
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
@@ -42,7 +46,7 @@ pub mod types;
|
||||
use num_traits::FromPrimitive;
|
||||
use types::account::Account;
|
||||
use types::chat::FullChat;
|
||||
use types::contact::ContactObject;
|
||||
use types::contact::{ContactObject, VcardContact};
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||
@@ -184,6 +188,16 @@ impl CommandApi {
|
||||
self.accounts.write().await.add_account().await
|
||||
}
|
||||
|
||||
/// Imports/migrated an existing account from a database path into this account manager.
|
||||
/// Returns the ID of new account.
|
||||
async fn migrate_account(&self, path_to_db: String) -> Result<u32> {
|
||||
self.accounts
|
||||
.write()
|
||||
.await
|
||||
.migrate_account(std::path::PathBuf::from(path_to_db))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_account(&self, account_id: u32) -> Result<()> {
|
||||
self.accounts
|
||||
.write()
|
||||
@@ -307,12 +321,12 @@ impl CommandApi {
|
||||
) -> Result<Option<ProviderInfo>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let socks5_enabled = ctx
|
||||
.get_config_bool(deltachat::config::Config::Socks5Enabled)
|
||||
let proxy_enabled = ctx
|
||||
.get_config_bool(deltachat::config::Config::ProxyEnabled)
|
||||
.await?;
|
||||
|
||||
let provider_info =
|
||||
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await;
|
||||
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await;
|
||||
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||
}
|
||||
|
||||
@@ -328,6 +342,11 @@ impl CommandApi {
|
||||
ctx.get_info().await
|
||||
}
|
||||
|
||||
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
|
||||
}
|
||||
|
||||
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(ctx.draft_self_report().await?.to_u32())
|
||||
@@ -688,7 +707,22 @@ impl CommandApi {
|
||||
ChatId::new(chat_id).get_encryption_info(&ctx).await
|
||||
}
|
||||
|
||||
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
|
||||
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
||||
///
|
||||
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
|
||||
/// If `chat_id` is unset, setup contact QR code is returned.
|
||||
async fn get_chat_securejoin_qr_code(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: Option<u32>,
|
||||
) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat = chat_id.map(ChatId::new);
|
||||
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
|
||||
Ok(qr)
|
||||
}
|
||||
|
||||
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
|
||||
/// The QR code is compatible to the OPENPGP4FPR format
|
||||
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||
///
|
||||
@@ -710,10 +744,9 @@ impl CommandApi {
|
||||
) -> Result<(String, String)> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat = chat_id.map(ChatId::new);
|
||||
Ok((
|
||||
securejoin::get_securejoin_qr(&ctx, chat).await?,
|
||||
get_securejoin_qr_svg(&ctx, chat).await?,
|
||||
))
|
||||
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
|
||||
let svg = get_securejoin_qr_svg(&ctx, chat).await?;
|
||||
Ok((qr, svg))
|
||||
}
|
||||
|
||||
/// Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
@@ -1426,6 +1459,51 @@ impl CommandApi {
|
||||
Ok(contact_id.map(|id| id.to_u32()))
|
||||
}
|
||||
|
||||
/// Parses a vCard file located at the given path. Returns contacts in their original order.
|
||||
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
|
||||
let vcard = fs::read(Path::new(&path)).await?;
|
||||
let vcard = str::from_utf8(&vcard)?;
|
||||
Ok(deltachat_contact_tools::parse_vcard(vcard)
|
||||
.into_iter()
|
||||
.map(|c| c.into())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Imports contacts from a vCard file located at the given path.
|
||||
///
|
||||
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
|
||||
async fn import_vcard(&self, account_id: u32, path: String) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let vcard = tokio::fs::read(Path::new(&path)).await?;
|
||||
let vcard = str::from_utf8(&vcard)?;
|
||||
Ok(deltachat::contact::import_vcard(&ctx, vcard)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|c| c.to_u32())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns a vCard containing contacts with the given ids.
|
||||
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
|
||||
deltachat::contact::make_vcard(&ctx, &contacts).await
|
||||
}
|
||||
|
||||
/// Sets vCard containing the given contacts to the message draft.
|
||||
async fn set_draft_vcard(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
contacts: Vec<u32>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
|
||||
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
msg.make_vcard(&ctx, &contacts).await?;
|
||||
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
@@ -1474,55 +1552,6 @@ impl CommandApi {
|
||||
Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect())
|
||||
}
|
||||
|
||||
/// Search next/previous message based on a given message and a list of types.
|
||||
/// Typically used to implement the "next" and "previous" buttons
|
||||
/// in a gallery or in a media player.
|
||||
///
|
||||
/// one combined call for getting chat::get_next_media for both directions
|
||||
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||
///
|
||||
/// Deprecated 2023-10-03, use `get_chat_media` method
|
||||
/// and navigate the returned array instead.
|
||||
#[allow(deprecated)]
|
||||
async fn get_neighboring_chat_media(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
message_type: MessageViewtype,
|
||||
or_message_type2: Option<MessageViewtype>,
|
||||
or_message_type3: Option<MessageViewtype>,
|
||||
) -> Result<(Option<u32>, Option<u32>)> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let msg_type: Viewtype = message_type.into();
|
||||
let msg_type2: Viewtype = or_message_type2.map(|v| v.into()).unwrap_or_default();
|
||||
let msg_type3: Viewtype = or_message_type3.map(|v| v.into()).unwrap_or_default();
|
||||
|
||||
let prev = chat::get_next_media(
|
||||
&ctx,
|
||||
MsgId::new(msg_id),
|
||||
chat::Direction::Backward,
|
||||
msg_type,
|
||||
msg_type2,
|
||||
msg_type3,
|
||||
)
|
||||
.await?
|
||||
.map(|id| id.to_u32());
|
||||
|
||||
let next = chat::get_next_media(
|
||||
&ctx,
|
||||
MsgId::new(msg_id),
|
||||
chat::Direction::Forward,
|
||||
msg_type,
|
||||
msg_type2,
|
||||
msg_type3,
|
||||
)
|
||||
.await?
|
||||
.map(|id| id.to_u32());
|
||||
|
||||
Ok((prev, next))
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// backup
|
||||
// ---------------------------------------------
|
||||
@@ -1594,10 +1623,10 @@ impl CommandApi {
|
||||
///
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
/// but will fail after 60 seconds to avoid deadlocks.
|
||||
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(60),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
@@ -1613,13 +1642,13 @@ impl CommandApi {
|
||||
///
|
||||
/// This call will block until the QR code is ready,
|
||||
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||
/// but will fail after 10 seconds to avoid deadlocks.
|
||||
/// but will fail after 60 seconds to avoid deadlocks.
|
||||
///
|
||||
/// Returns the QR code rendered as an SVG image.
|
||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let qr = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(60),
|
||||
self.inner_get_backup_qr(account_id),
|
||||
)
|
||||
.await
|
||||
@@ -1730,6 +1759,37 @@ impl CommandApi {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_webxdc_realtime_data(
|
||||
&self,
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
data: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
send_webxdc_realtime_data(&ctx, MsgId::new(instance_msg_id), data).await
|
||||
}
|
||||
|
||||
async fn send_webxdc_realtime_advertisement(
|
||||
&self,
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
if let Some(fut) = fut {
|
||||
tokio::spawn(async move {
|
||||
fut.await.ok();
|
||||
info!(ctx, "send_webxdc_realtime_advertisement done")
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await
|
||||
}
|
||||
|
||||
async fn get_webxdc_status_updates(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1886,9 +1946,13 @@ impl CommandApi {
|
||||
|
||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut message = data.create_message(&ctx).await?;
|
||||
let mut message = data
|
||||
.create_message(&ctx)
|
||||
.await
|
||||
.context("Failed to create message")?;
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||
.await?
|
||||
.await
|
||||
.context("Failed to send created message")?
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ pub enum Account {
|
||||
// size: u32,
|
||||
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
|
||||
color: String,
|
||||
/// Optional tag as "Work", "Family".
|
||||
/// Meant to help profile owner to differ between profiles with similar names.
|
||||
private_tag: Option<String>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Unconfigured { id: u32 },
|
||||
@@ -31,12 +34,14 @@ impl Account {
|
||||
let color = color_int_to_hex_string(
|
||||
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
|
||||
);
|
||||
let private_tag = ctx.get_config(Config::PrivateTag).await?;
|
||||
Ok(Account::Configured {
|
||||
id,
|
||||
display_name,
|
||||
addr,
|
||||
profile_image,
|
||||
color,
|
||||
private_tag,
|
||||
})
|
||||
} else {
|
||||
Ok(Account::Unconfigured { id })
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct FullChat {
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
// subtitle - will be moved to frontend because it uses translation functions
|
||||
chat_type: u32,
|
||||
is_unpromoted: bool,
|
||||
@@ -104,6 +105,7 @@ impl FullChat {
|
||||
is_protected: chat.is_protected(),
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
is_unpromoted: chat.is_unpromoted(),
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
@@ -153,6 +155,7 @@ pub struct BasicChat {
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
pinned: bool,
|
||||
chat_type: u32,
|
||||
is_unpromoted: bool,
|
||||
is_self_talk: bool,
|
||||
@@ -180,6 +183,7 @@ impl BasicChat {
|
||||
is_protected: chat.is_protected(),
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||
is_unpromoted: chat.is_unpromoted(),
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use deltachat::color;
|
||||
use deltachat::context::Context;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
@@ -18,6 +19,7 @@ pub struct ContactObject {
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact can be added to verified groups.
|
||||
///
|
||||
@@ -78,6 +80,7 @@ impl ContactObject {
|
||||
profile_image, //BLOBS
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
is_verified,
|
||||
is_profile_verified,
|
||||
verifier_id,
|
||||
@@ -87,3 +90,35 @@ impl ContactObject {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VcardContact {
|
||||
/// Email address.
|
||||
addr: String,
|
||||
/// The contact's name, or the email address if no name was given.
|
||||
display_name: String,
|
||||
/// Public PGP key in Base64.
|
||||
key: Option<String>,
|
||||
/// Profile image in Base64.
|
||||
profile_image: Option<String>,
|
||||
/// Contact color as hex string.
|
||||
color: String,
|
||||
/// Last update timestamp.
|
||||
timestamp: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
|
||||
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
|
||||
let display_name = vc.display_name().to_string();
|
||||
let color = color::str_to_color(&vc.addr.to_lowercase());
|
||||
Self {
|
||||
addr: vc.addr,
|
||||
display_name,
|
||||
key: vc.key,
|
||||
profile_image: vc.profile_image,
|
||||
color: color_int_to_hex_string(color),
|
||||
timestamp: vc.timestamp.ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,14 @@ pub enum EventType {
|
||||
contact_id: u32,
|
||||
},
|
||||
|
||||
/// Incoming reaction, should be notified.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingReaction {
|
||||
contact_id: u32,
|
||||
msg_id: u32,
|
||||
reaction: String,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
@@ -240,6 +248,15 @@ pub enum EventType {
|
||||
status_update_serial: u32,
|
||||
},
|
||||
|
||||
/// Data received over an ephemeral peer channel.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
|
||||
|
||||
/// Advertisement received over an ephemeral peer channel.
|
||||
/// This can be used by bots to initiate peer-to-peer communication from their side.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcRealtimeAdvertisementReceived { msg_id: u32 },
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcInstanceDeleted { msg_id: u32 },
|
||||
@@ -259,6 +276,9 @@ pub enum EventType {
|
||||
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatlistItemChanged { chat_id: Option<u32> },
|
||||
|
||||
/// Inform than some events have been skipped due to event channel overflow.
|
||||
EventChannelOverflow { n: u64 },
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -290,6 +310,15 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
CoreEventType::IncomingReaction {
|
||||
contact_id,
|
||||
msg_id,
|
||||
reaction,
|
||||
} => IncomingReaction {
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
reaction: reaction.as_str().to_string(),
|
||||
},
|
||||
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
@@ -362,6 +391,15 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
status_update_serial: status_update_serial.to_u32(),
|
||||
},
|
||||
CoreEventType::WebxdcRealtimeData { msg_id, data } => WebxdcRealtimeData {
|
||||
msg_id: msg_id.to_u32(),
|
||||
data,
|
||||
},
|
||||
CoreEventType::WebxdcRealtimeAdvertisementReceived { msg_id } => {
|
||||
WebxdcRealtimeAdvertisementReceived {
|
||||
msg_id: msg_id.to_u32(),
|
||||
}
|
||||
}
|
||||
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
@@ -370,6 +408,7 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.map(|id| id.to_u32()),
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::api::VcardContact;
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat::chat::Chat;
|
||||
use deltachat::chat::ChatItem;
|
||||
@@ -87,6 +88,8 @@ pub struct MessageObject {
|
||||
download_state: DownloadState,
|
||||
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
|
||||
vcard_contact: Option<VcardContact>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
@@ -173,6 +176,13 @@ impl MessageObject {
|
||||
Some(reactions.into())
|
||||
};
|
||||
|
||||
let vcard_contacts: Vec<VcardContact> = message
|
||||
.vcard_contacts(context)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok(MessageObject {
|
||||
id: msg_id.to_u32(),
|
||||
chat_id: message.get_chat_id().to_u32(),
|
||||
@@ -232,6 +242,8 @@ impl MessageObject {
|
||||
download_state,
|
||||
|
||||
reactions,
|
||||
|
||||
vcard_contact: vcard_contacts.first().cloned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -274,6 +286,11 @@ pub enum MessageViewtype {
|
||||
|
||||
/// Message is an webxdc instance.
|
||||
Webxdc,
|
||||
|
||||
/// Message containing shared contacts represented as a vCard (virtual contact file)
|
||||
/// with email addresses and possibly other fields.
|
||||
/// Use `parse_vcard()` to retrieve them.
|
||||
Vcard,
|
||||
}
|
||||
|
||||
impl From<Viewtype> for MessageViewtype {
|
||||
@@ -290,6 +307,7 @@ impl From<Viewtype> for MessageViewtype {
|
||||
Viewtype::File => MessageViewtype::File,
|
||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +326,7 @@ impl From<MessageViewtype> for Viewtype {
|
||||
MessageViewtype::File => Viewtype::File,
|
||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,6 +391,9 @@ pub enum SystemMessageType {
|
||||
|
||||
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
||||
WebxdcInfoMessage,
|
||||
|
||||
/// This message contains a users iroh node address.
|
||||
IrohNodeAddr,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
@@ -394,6 +416,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
||||
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
||||
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
|
||||
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
||||
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
||||
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||
}
|
||||
@@ -554,7 +577,9 @@ pub struct MessageData {
|
||||
pub file: Option<String>,
|
||||
pub location: Option<(f64, f64)>,
|
||||
pub override_sender_name: Option<String>,
|
||||
/// Quoted message id. Takes preference over `quoted_text` (see below).
|
||||
pub quoted_message_id: Option<u32>,
|
||||
pub quoted_text: Option<String>,
|
||||
}
|
||||
|
||||
impl MessageData {
|
||||
@@ -580,16 +605,16 @@ impl MessageData {
|
||||
message.set_location(latitude, longitude);
|
||||
}
|
||||
if let Some(id) = self.quoted_message_id {
|
||||
let quoted_message = Message::load_from_db(context, MsgId::new(id))
|
||||
.await
|
||||
.context("Failed to load quoted message")?;
|
||||
message
|
||||
.set_quote(
|
||||
context,
|
||||
Some(
|
||||
&Message::load_from_db(context, MsgId::new(id))
|
||||
.await
|
||||
.context("message to quote could not be loaded")?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
.set_quote(context, Some("ed_message))
|
||||
.await
|
||||
.context("Failed to set quote")?;
|
||||
} else if let Some(text) = self.quoted_text {
|
||||
let protect = false;
|
||||
message.set_quote_text(Some((text, protect)));
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
@@ -612,7 +637,7 @@ pub struct MessageInfo {
|
||||
error: Option<String>,
|
||||
rfc724_mid: String,
|
||||
server_urls: Vec<String>,
|
||||
hop_info: Option<String>,
|
||||
hop_info: String,
|
||||
}
|
||||
|
||||
impl MessageInfo {
|
||||
|
||||
@@ -32,13 +32,20 @@ pub enum QrObject {
|
||||
Account {
|
||||
domain: String,
|
||||
},
|
||||
Backup {
|
||||
ticket: String,
|
||||
Backup2 {
|
||||
auth_token: String,
|
||||
|
||||
node_addr: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
Proxy {
|
||||
url: String,
|
||||
host: String,
|
||||
port: u16,
|
||||
},
|
||||
Addr {
|
||||
contact_id: u32,
|
||||
draft: Option<String>,
|
||||
@@ -129,8 +136,13 @@ impl From<Qr> for QrObject {
|
||||
}
|
||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||
Qr::Account { domain } => QrObject::Account { domain },
|
||||
Qr::Backup { ticket } => QrObject::Backup {
|
||||
ticket: ticket.to_string(),
|
||||
Qr::Backup2 {
|
||||
ref node_addr,
|
||||
auth_token,
|
||||
} => QrObject::Backup2 {
|
||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
||||
|
||||
auth_token,
|
||||
},
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
@@ -139,6 +151,7 @@ impl From<Qr> for QrObject {
|
||||
domain,
|
||||
instance_pattern,
|
||||
},
|
||||
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
||||
Qr::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::Addr { contact_id, draft }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
pub mod api;
|
||||
pub use yerpc;
|
||||
|
||||
@@ -82,7 +83,7 @@ mod tests {
|
||||
assert_eq!(result, response.to_owned());
|
||||
}
|
||||
{
|
||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
|
||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
|
||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||
session.handle_incoming(request).await;
|
||||
let result = receiver.recv().await?;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"dependencies": {
|
||||
"@deltachat/tiny-emitter": "3.0.0",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"yerpc": "^0.4.3"
|
||||
"yerpc": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.21",
|
||||
@@ -32,6 +32,10 @@
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/deltachat.js",
|
||||
"name": "@deltachat/jsonrpc-client",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
|
||||
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||
@@ -54,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.138.4"
|
||||
"version": "1.148.4"
|
||||
}
|
||||
|
||||
@@ -86,10 +86,7 @@ describe("online tests", function () {
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
]);
|
||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
||||
|
||||
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
|
||||
const { chatId: chatIdOnAccountB } = await eventPromise;
|
||||
@@ -119,10 +116,7 @@ describe("online tests", function () {
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
]);
|
||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
||||
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
|
||||
// wait for message from A
|
||||
console.log("wait for message from A");
|
||||
@@ -143,10 +137,7 @@ describe("online tests", function () {
|
||||
);
|
||||
expect(message.text).equal("Hello2");
|
||||
// Send message back from B to A
|
||||
const eventPromise2 = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId1),
|
||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||
]);
|
||||
const eventPromise2 = waitForEvent(dc, "IncomingMsg", accountId1);
|
||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||
// Check if answer arrives at A and if it is encrypted
|
||||
await eventPromise2;
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.138.4"
|
||||
version = "1.148.4"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true, features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.21"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.31"
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "14"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -19,8 +19,10 @@ use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::mimeparser::SystemMessage;
|
||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::qr_code_generator::create_qr_svg;
|
||||
use deltachat::reaction::send_reaction;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
@@ -338,7 +340,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
receive-backup <qr>\n\
|
||||
export-keys\n\
|
||||
import-keys\n\
|
||||
export-setup\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
@@ -355,6 +356,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
configure\n\
|
||||
connect\n\
|
||||
disconnect\n\
|
||||
fetch\n\
|
||||
connectivity\n\
|
||||
maybenetwork\n\
|
||||
housekeeping\n\
|
||||
@@ -424,6 +426,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
checkqr <qr-content>\n\
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
createqrsvg <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
@@ -486,8 +489,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"send-backup" => {
|
||||
let provider = BackupProvider::prepare(&context).await?;
|
||||
let qr = provider.qr();
|
||||
println!("QR code: {}", format_backup(&qr)?);
|
||||
let qr = format_backup(&provider.qr())?;
|
||||
println!("QR code: {}", qr);
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
provider.await?;
|
||||
}
|
||||
"receive-backup" => {
|
||||
@@ -503,17 +507,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"import-keys" => {
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
||||
}
|
||||
"export-setup" => {
|
||||
let setup_code = create_setup_code(&context);
|
||||
let file_name = blobdir.join("autocrypt-setup-message.html");
|
||||
let file_content = render_setup_file(&context, &setup_code).await?;
|
||||
fs::write(&file_name, file_content).await?;
|
||||
println!(
|
||||
"Setup message written to: {}\nSetup code: {}",
|
||||
file_name.display(),
|
||||
&setup_code,
|
||||
);
|
||||
}
|
||||
"poke" => {
|
||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||
}
|
||||
@@ -642,6 +635,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("{cnt} chats");
|
||||
println!("{time_needed:?} to create this list");
|
||||
}
|
||||
"start-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
bail!("missing msgid");
|
||||
}
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
let res = send_webxdc_realtime_advertisement(&context, msg_id).await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
println!("waiting for peer channel join");
|
||||
res.await?;
|
||||
}
|
||||
println!("joined peer channel");
|
||||
}
|
||||
"send-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
bail!("missing msgid");
|
||||
}
|
||||
if arg2.is_empty() {
|
||||
bail!("no message");
|
||||
}
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
send_webxdc_realtime_data(&context, msg_id, arg2.as_bytes().to_vec()).await?;
|
||||
println!("sent realtime message");
|
||||
}
|
||||
"chat" => {
|
||||
if sel_chat.is_none() && arg1.is_empty() {
|
||||
bail!("Argument [chat-id] is missing.");
|
||||
@@ -1234,12 +1251,19 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
"createqrsvg" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||
let svg = create_qr_svg(arg1)?;
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
fs::write(&file, svg).await?;
|
||||
println!("{file:#?} written.");
|
||||
}
|
||||
"providerinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||
let socks5_enabled = context
|
||||
.get_config_bool(config::Config::Socks5Enabled)
|
||||
let proxy_enabled = context
|
||||
.get_config_bool(config::Config::ProxyEnabled)
|
||||
.await?;
|
||||
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
|
||||
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
|
||||
Some(info) => {
|
||||
println!("Information for provider belonging to {arg1}:");
|
||||
println!("status: {}", info.status as u32);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
//! This is a CLI program and a little testing frame. This file must not be
|
||||
//! included when using Delta Chat Core as a library.
|
||||
//!
|
||||
@@ -8,10 +9,7 @@
|
||||
extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::process::Command;
|
||||
|
||||
use ansi_term::Color;
|
||||
use anyhow::{bail, Error};
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::config;
|
||||
@@ -21,6 +19,7 @@ use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::EventType;
|
||||
use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||
@@ -31,6 +30,7 @@ use rustyline::{
|
||||
};
|
||||
use tokio::fs;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
@@ -150,7 +150,7 @@ impl Completer for DcHelper {
|
||||
}
|
||||
}
|
||||
|
||||
const IMEX_COMMANDS: [&str; 14] = [
|
||||
const IMEX_COMMANDS: [&str; 13] = [
|
||||
"initiate-key-transfer",
|
||||
"get-setupcodebegin",
|
||||
"continue-key-transfer",
|
||||
@@ -161,13 +161,12 @@ const IMEX_COMMANDS: [&str; 14] = [
|
||||
"receive-backup",
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"export-setup",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
];
|
||||
|
||||
const DB_COMMANDS: [&str; 10] = [
|
||||
const DB_COMMANDS: [&str; 11] = [
|
||||
"info",
|
||||
"set",
|
||||
"get",
|
||||
@@ -175,6 +174,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"configure",
|
||||
"connect",
|
||||
"disconnect",
|
||||
"fetch",
|
||||
"connectivity",
|
||||
"maybenetwork",
|
||||
"housekeeping",
|
||||
@@ -240,12 +240,13 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"createqrsvg",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
"exit",
|
||||
@@ -416,6 +417,9 @@ async fn handle_cmd(
|
||||
"disconnect" => {
|
||||
ctx.stop_io().await;
|
||||
}
|
||||
"fetch" => {
|
||||
ctx.background_fetch().await?;
|
||||
}
|
||||
"configure" => {
|
||||
ctx.configure().await?;
|
||||
}
|
||||
@@ -445,12 +449,7 @@ async fn handle_cmd(
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{qr}");
|
||||
let output = Command::new("qrencode")
|
||||
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
}
|
||||
}
|
||||
"getqrsvg" => {
|
||||
@@ -482,9 +481,10 @@ async fn handle_cmd(
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
pretty_env_logger::formatted_timed_builder()
|
||||
.parse_default_env()
|
||||
.filter_module("deltachat_repl", log::LevelFilter::Info)
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::from_default_env().add_directive("deltachat_repl=info".parse()?),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = std::env::args().collect();
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.138.4"
|
||||
version = "1.148.4"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -13,10 +13,13 @@ classifiers = [
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
|
||||
@@ -250,12 +250,16 @@ class Account:
|
||||
"""
|
||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
"""Get Setup-Contact QR Code text and SVG data.
|
||||
def get_qr_code(self) -> str:
|
||||
"""Get Setup-Contact QR Code text.
|
||||
|
||||
this data needs to be transferred to another Delta Chat account
|
||||
This data needs to be transferred to another Delta Chat account
|
||||
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
||||
"""
|
||||
return self._rpc.get_chat_securejoin_qr_code(self.id, None)
|
||||
|
||||
def get_qr_code_svg(self) -> tuple[str, str]:
|
||||
"""Get Setup-Contact QR code text and SVG."""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
@@ -297,6 +301,12 @@ class Account:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
|
||||
Consumes all events before the next incoming message event."""
|
||||
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
||||
|
||||
def wait_for_securejoin_inviter_success(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
from dataclasses import dataclass
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
@@ -95,7 +96,11 @@ class Chat:
|
||||
"""Return encryption info for this chat."""
|
||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
def get_qr_code(self) -> str:
|
||||
"""Get Join-Group QR code text."""
|
||||
return self._rpc.get_chat_securejoin_qr_code(self.account.id, self.id)
|
||||
|
||||
def get_qr_code_svg(self) -> tuple[str, str]:
|
||||
"""Get Join-Group QR code text and SVG data."""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
|
||||
@@ -265,3 +270,11 @@ class Chat:
|
||||
location["message"] = Message(self.account, location.msg_id)
|
||||
locations.append(location)
|
||||
return locations
|
||||
|
||||
def send_contact(self, contact: Contact):
|
||||
"""Send contact to the chat."""
|
||||
vcard = contact.make_vcard()
|
||||
with NamedTemporaryFile(suffix=".vcard") as f:
|
||||
f.write(vcard.encode())
|
||||
f.flush()
|
||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
||||
|
||||
@@ -62,6 +62,8 @@ class EventType(str, Enum):
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
CONFIG_SYNCED = "ConfigSynced"
|
||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
@@ -114,6 +116,7 @@ class ViewType(str, Enum):
|
||||
FILE = "File"
|
||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||
WEBXDC = "Webxdc"
|
||||
VCARD = "Vcard"
|
||||
|
||||
|
||||
class SystemMessageType(str, Enum):
|
||||
|
||||
@@ -60,3 +60,6 @@ class Contact:
|
||||
self.account,
|
||||
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||
)
|
||||
|
||||
def make_vcard(self) -> str:
|
||||
return self._rpc.make_vcard(self.account.id, [self.id])
|
||||
|
||||
@@ -9,18 +9,19 @@ import io
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from . import Account, const
|
||||
if TYPE_CHECKING:
|
||||
from . import Account
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
@@ -35,28 +36,15 @@ class DirectImap:
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
# Assume the testing server supports TLS on port 993.
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
port = 993
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
host = user.rsplit("@")[-1]
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
if security == const.SocketSecurity.PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.SocketSecurity.STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .const import EventType
|
||||
from .contact import Contact
|
||||
|
||||
@@ -70,3 +70,11 @@ class Message:
|
||||
event = self.account.wait_for_event()
|
||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||
break
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_advertisement(self):
|
||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_data(self, data) -> None:
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
@@ -114,13 +114,13 @@ class ACFactory:
|
||||
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def rpc(tmp_path) -> AsyncGenerator:
|
||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||
with rpc_server:
|
||||
yield rpc_server
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
@@ -126,8 +126,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
@@ -135,13 +134,15 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||
)
|
||||
|
||||
msg_id = alice.wait_for_incoming_msg_event().msg_id
|
||||
|
||||
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
|
||||
message = alice.wait_for_incoming_msg()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.download_state == const.DownloadState.AVAILABLE
|
||||
|
||||
alice.clear_all_events()
|
||||
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
|
||||
alice._rpc.download_full_message(alice.id, msg_id)
|
||||
|
||||
snapshot = message.get_snapshot()
|
||||
chat_id = snapshot.chat_id
|
||||
alice._rpc.download_full_message(alice.id, message.id)
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
@@ -177,8 +178,7 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
|
||||
alice_chat_bob.send_text("hello")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
bob_chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
|
||||
@@ -189,8 +189,7 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
# make sure alice_second_device already received the message
|
||||
alice_second_device.wait_for_incoming_msg_event()
|
||||
|
||||
event = alice.wait_for_incoming_msg_event()
|
||||
msg = alice.get_message_by_id(event.msg_id)
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
alice_second_device.clear_all_events()
|
||||
msg.mark_seen()
|
||||
|
||||
@@ -211,6 +210,7 @@ def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
|
||||
alice_second_device.clear_all_events()
|
||||
alice_chat_bob.pin()
|
||||
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().pinned
|
||||
|
||||
alice_second_device.clear_all_events()
|
||||
alice_chat_bob.mute()
|
||||
|
||||
238
deltachat-rpc-client/tests/test_iroh_webxdc.py
Normal file
238
deltachat-rpc-client/tests/test_iroh_webxdc.py
Normal file
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Testing webxdc iroh connectivity
|
||||
|
||||
If you want to debug iroh at rust-trace/log level set
|
||||
|
||||
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def path_to_webxdc(request):
|
||||
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
|
||||
assert p.exists()
|
||||
return str(p)
|
||||
|
||||
|
||||
def log(msg):
|
||||
print()
|
||||
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
|
||||
print()
|
||||
|
||||
|
||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
|
||||
assert ac1.get_config("webxdc_realtime_enabled") == "1"
|
||||
assert ac2.get_config("webxdc_realtime_enabled") == "1"
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "play"
|
||||
|
||||
# send iroh announcements simultaneously
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
return ac1_webxdc_msg, ac2_webxdc_msg
|
||||
|
||||
|
||||
def setup_thread_send_realtime_data(msg, data):
|
||||
def thread_run():
|
||||
for _i in range(10):
|
||||
msg.send_webxdc_realtime_data(data)
|
||||
time.sleep(1)
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
|
||||
def wait_receive_realtime_data(msg_data_list):
|
||||
account = msg_data_list[0][0].account
|
||||
msg_data_list = msg_data_list[:]
|
||||
|
||||
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
|
||||
while msg_data_list:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
for i, (msg, data) in enumerate(msg_data_list):
|
||||
if msg.id == event.msg_id:
|
||||
assert list(data) == event.data
|
||||
log(f"msg {msg.id}: got correct realtime data {data}")
|
||||
del msg_data_list[i]
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection sequentially."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||
assert snapshot.text == "play"
|
||||
|
||||
# send iroh announcements sequentially
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||
|
||||
log("waiting for incoming message on ac2")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping1"
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||
|
||||
log("waiting for incoming message on ac1")
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping2"
|
||||
|
||||
log("sending realtime data ac1 -> ac2")
|
||||
# Test that 128 KB of data can be sent in a single message.
|
||||
data = os.urandom(128000)
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||
|
||||
log("ac2: waiting for realtime data")
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert event.data == list(data)
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
|
||||
|
||||
|
||||
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
|
||||
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
|
||||
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
|
||||
|
||||
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
|
||||
|
||||
|
||||
def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
"""Test that messages are received only once."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
|
||||
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
ac2_webxdc_msg.get_snapshot().chat.accept()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "webxdc"
|
||||
|
||||
# Issue a "send" call in parallel with sending advertisement.
|
||||
# Previously due to a bug this caused subscribing to the channel twice.
|
||||
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
def thread_run():
|
||||
for i in range(10):
|
||||
data = str(i).encode()
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||
time.sleep(1)
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
n = int(bytes(event.data).decode())
|
||||
break
|
||||
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert int(bytes(event.data).decode()) > n
|
||||
break
|
||||
|
||||
|
||||
def test_no_reordering(acfactory, path_to_webxdc):
|
||||
"""Test that sending a lot of realtime messages does not result in reordering."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, b"hello")
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, b"hello")])
|
||||
|
||||
for i in range(200):
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data([i])
|
||||
|
||||
for i in range(200):
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA and bytes(event.data) != b"hello":
|
||||
if event.data[0] == i:
|
||||
break
|
||||
pytest.fail("Reordering detected")
|
||||
|
||||
|
||||
def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
"""Test that realtime advertisement is assigned to the correct message after chatting."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
|
||||
|
||||
ac1_ac2_chat.send_text("Hello!")
|
||||
ac2_hello_msg = ac2.wait_for_incoming_msg()
|
||||
ac2_hello_msg_snapshot = ac2_hello_msg.get_snapshot()
|
||||
assert ac2_hello_msg_snapshot.text == "Hello!"
|
||||
ac2_hello_msg_snapshot.chat.accept()
|
||||
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
while 1:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
break
|
||||
@@ -1,13 +1,15 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
@@ -30,23 +32,45 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
bob2.export_self_keys(tmp_path)
|
||||
|
||||
logging.info("Bob imports a key")
|
||||
bob.import_self_keys(tmp_path / "private-key-default.asc")
|
||||
bob.import_self_keys(tmp_path)
|
||||
|
||||
assert bob.get_config("key_id") == "2"
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert not bob_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protect", [True, False])
|
||||
def test_qr_securejoin(acfactory, protect):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
_, _, domain = alice.get_config("addr").rpartition("@")
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=protect)
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
|
||||
alice.set_config("displayname", "Alice")
|
||||
|
||||
# Test that display name is used
|
||||
# in SVG and no address is visible.
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
assert domain not in svg
|
||||
assert "Alice" in svg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protect", [True, False])
|
||||
def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
# Setup second device for Alice
|
||||
# to test observing securejoin protocol.
|
||||
alice.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group", protect=protect)
|
||||
assert alice_chat.get_basic_snapshot().is_protected == protect
|
||||
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
logging.info("Bob joins the group")
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
# Check that at least some of the handshake messages are deleted.
|
||||
@@ -74,6 +98,21 @@ def test_qr_securejoin(acfactory, protect):
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
# Start second Alice device.
|
||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||
alice2.start_io()
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||
assert alice2_contact_bob_snapshot.is_verified
|
||||
|
||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||
logging.info("Fiona joins the group via alice2")
|
||||
alice.stop_io()
|
||||
fiona.secure_join(qr_code)
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
@@ -91,7 +130,7 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
@@ -106,7 +145,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
alice, bob, charlie = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("Bob and Charlie setup contact with Alice")
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
|
||||
bob.secure_join(qr_code)
|
||||
charlie.secure_join(qr_code)
|
||||
@@ -168,13 +207,13 @@ def test_setup_contact_resetup(acfactory) -> None:
|
||||
"""Tests that setup contact works after Alice resets the device and changes the key."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
alice = acfactory.resetup_account(alice)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -188,7 +227,7 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
qr_code = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -205,7 +244,7 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
qr_code = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -252,7 +291,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
qr_code = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -269,7 +308,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
qr_code = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -287,7 +326,6 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
|
||||
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
@@ -297,6 +335,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
|
||||
event = ac2.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
chat_id = event.chat_id
|
||||
@@ -336,7 +376,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
|
||||
logging.info("ac3: verify with ac2")
|
||||
qr_code, _svg = ac2.get_qr_code()
|
||||
qr_code = ac2.get_qr_code()
|
||||
ac3.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_inviter_success()
|
||||
|
||||
@@ -346,7 +386,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
|
||||
logging.info("ac1: create verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group", protect=True)
|
||||
qr_code, _svg = ch1.get_qr_code()
|
||||
qr_code = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
@@ -359,7 +399,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
break
|
||||
|
||||
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||
qr_code, _svg = ch1.get_qr_code()
|
||||
qr_code = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.remove()
|
||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
@@ -381,7 +421,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
break
|
||||
|
||||
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||
qr_code, _svg = vg.get_qr_code()
|
||||
qr_code = vg.get_qr_code()
|
||||
ac4.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
while 1:
|
||||
@@ -402,7 +442,7 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group("Group for joining", protect=True)
|
||||
qr_code, _svg = ac1_chat.get_qr_code()
|
||||
qr_code = ac1_chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
@@ -420,15 +460,19 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
|
||||
def test_aeap_flow_verified(acfactory):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
# ac1new is only used to get a new address.
|
||||
ac1new = acfactory.new_preconfigured_account()
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
qr_code = chat.get_qr_code()
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
logging.info("sending first message")
|
||||
msg_out = chat.send_text("old address").get_snapshot()
|
||||
@@ -464,12 +508,12 @@ def test_gossip_verification(acfactory) -> None:
|
||||
alice, bob, carol = acfactory.get_online_accounts(3)
|
||||
|
||||
# Bob verifies Alice.
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Bob verifies Carol.
|
||||
qr_code, _svg = carol.get_qr_code()
|
||||
qr_code = carol.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -520,16 +564,17 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
ac3_chat = ac3.create_group("Verified group", protect=True)
|
||||
|
||||
# ac1 joins ac3 group.
|
||||
ac3_qr_code, _svg = ac3_chat.get_qr_code()
|
||||
ac3_qr_code = ac3_chat.get_qr_code()
|
||||
ac1.secure_join(ac3_qr_code)
|
||||
ac1.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
ac1_qr_code, _svg = snapshot.chat.get_qr_code()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 verifies ac1
|
||||
qr_code, _svg = ac1.get_qr_code()
|
||||
qr_code = ac1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -540,17 +585,29 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
# ac1 resetups the account.
|
||||
ac1 = acfactory.resetup_account(ac1)
|
||||
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
# Loop sending message from ac1 to ac2
|
||||
# until ac2 accepts new ac1 key.
|
||||
#
|
||||
# This may not happen immediately because resetup of ac1
|
||||
# rewinds "smeared timestamp" so Date: header for messages
|
||||
# sent by new ac1 are in the past compared to the last Date:
|
||||
# header sent by old ac1.
|
||||
while True:
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
|
||||
# ac2 receives a message.
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
# ac2 receives a message.
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
logging.info("ac2 received Hello!")
|
||||
|
||||
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
|
||||
if not ac2_contact_ac1.get_snapshot().is_verified:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# ac1 goes offline.
|
||||
ac1.remove()
|
||||
@@ -589,7 +646,7 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
assert alice_chat.get_basic_snapshot().is_protected
|
||||
logging.info("Bob joins verified group")
|
||||
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -612,7 +669,8 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
logging.info("Bob scanned withdrawn QR code")
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
|
||||
if (
|
||||
event.kind == EventType.WARNING
|
||||
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
|
||||
):
|
||||
break
|
||||
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
|
||||
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import base64
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.direct_imap import DirectImap
|
||||
@@ -68,6 +71,38 @@ def test_configure_starttls(acfactory) -> None:
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
domain = account.get_config("addr").rsplit("@")[-1]
|
||||
ip_address = socket.gethostbyname(domain)
|
||||
|
||||
# This should fail TLS check.
|
||||
account.set_config("mail_server", ip_address)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.configure()
|
||||
|
||||
|
||||
def test_configure_alternative_port(acfactory) -> None:
|
||||
"""Test that configuration with alternative port 443 works."""
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
account.set_config("mail_port", "443")
|
||||
account.set_config("send_port", "443")
|
||||
|
||||
account.configure()
|
||||
|
||||
|
||||
def test_configure_username(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
addr = account.get_config("addr")
|
||||
account.set_config("mail_user", addr)
|
||||
account.configure()
|
||||
|
||||
assert account.get_config("configured_mail_user") == addr
|
||||
|
||||
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -103,12 +138,12 @@ def test_account(acfactory) -> None:
|
||||
assert alice.get_chatlist(snapshot=True)
|
||||
assert alice.get_qr_code()
|
||||
assert alice.get_fresh_messages()
|
||||
assert alice.get_next_messages()
|
||||
|
||||
# Test sending empty message.
|
||||
assert len(bob.wait_next_messages()) == 0
|
||||
alice_chat_bob.send_text("")
|
||||
messages = bob.wait_next_messages()
|
||||
assert bob.get_next_messages() == messages
|
||||
assert len(messages) == 1
|
||||
message = messages[0]
|
||||
snapshot = message.get_snapshot()
|
||||
@@ -398,7 +433,7 @@ def test_provider_info(rpc) -> None:
|
||||
assert provider_info["id"] == "gmail"
|
||||
|
||||
# Disable MX record resolution.
|
||||
rpc.set_config(account_id, "socks5_enabled", "1")
|
||||
rpc.set_config(account_id, "proxy_enabled", "1")
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info is None
|
||||
|
||||
@@ -613,3 +648,31 @@ def test_markseen_contact_request(acfactory, tmp_path):
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
break
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
def test_get_http_response(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
|
||||
assert http_response["mimetype"] == "text/html"
|
||||
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
|
||||
|
||||
|
||||
def test_configured_imap_certificate_checks(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
|
||||
|
||||
# Certificate checks should be configured (not None)
|
||||
assert configured_certificate_checks
|
||||
|
||||
# 0 is the value old Delta Chat core versions used
|
||||
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
||||
# and configuration failed to use strict TLS checks
|
||||
# so it switched strict TLS checks off.
|
||||
#
|
||||
# New versions of Delta Chat are not disabling TLS checks
|
||||
# unless users explicitly disables them
|
||||
# or provider database says provider has invalid certificates.
|
||||
#
|
||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||
# This test is a regression test to prevent this happening again.
|
||||
assert configured_certificate_checks != "0"
|
||||
|
||||
15
deltachat-rpc-client/tests/test_vcard.py
Normal file
15
deltachat-rpc-client/tests/test_vcard.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def test_vcard(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
||||
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_contact(alice_contact_charlie)
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.vcard_contact
|
||||
assert snapshot.vcard_contact.addr == "charlie@example.org"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.138.4"
|
||||
version = "1.148.4"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -10,18 +10,18 @@ keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
categories = ["cryptography", "std", "email"]
|
||||
|
||||
[dependencies]
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = "..", default-features = false }
|
||||
deltachat-jsonrpc = { workspace = true }
|
||||
deltachat = { workspace = true }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.11.3" }
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.37.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.9"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
anyhow = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["io-std"] }
|
||||
tokio-util = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
3
deltachat-rpc-server/npm-package/.gitignore
vendored
3
deltachat-rpc-server/npm-package/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
platform_package
|
||||
*.tgz
|
||||
*.tgz
|
||||
package-lock.json
|
||||
@@ -7,7 +7,7 @@ This simplifies cross-compilation and even reduces binary size (no CFFI layer an
|
||||
|
||||
## Usage
|
||||
|
||||
> The **minimum** nodejs version for this package is `20.11`
|
||||
> The **minimum** nodejs version for this package is `16`
|
||||
|
||||
```
|
||||
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
|
||||
@@ -20,7 +20,9 @@ import { C } from "@deltachat/jsonrpc-client";
|
||||
async function main() {
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close()
|
||||
}
|
||||
main()
|
||||
```
|
||||
|
||||
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
|
||||
@@ -44,12 +46,11 @@ references:
|
||||
When you import this package it searches for the rpc server in the following locations and order:
|
||||
|
||||
1. `DELTA_CHAT_RPC_SERVER` environment variable
|
||||
2. in PATH
|
||||
- unless `DELTA_CHAT_SKIP_PATH=1` is specified
|
||||
- searches in .cargo/bin directory first
|
||||
- but there an additional version check is performed
|
||||
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
|
||||
3. prebuilds in npm packages
|
||||
|
||||
so by default it uses the prebuilds.
|
||||
|
||||
## How do you built this package in CI
|
||||
|
||||
- To build platform packages, run the `build_platform_package.py` script:
|
||||
|
||||
11
deltachat-rpc-server/npm-package/index.d.ts
vendored
11
deltachat-rpc-server/npm-package/index.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||
|
||||
export interface SearchOptions {
|
||||
/** whether to disable looking for deltachat-rpc-server inside of $PATH */
|
||||
skipSearchInPath: boolean;
|
||||
/** whether take deltachat-rpc-server inside of $PATH*/
|
||||
takeVersionFromPATH: boolean;
|
||||
|
||||
/** whether to disable the DELTA_CHAT_RPC_SERVER environment variable */
|
||||
disableEnvPath: boolean;
|
||||
@@ -20,17 +20,20 @@ export function getRPCServerPath(
|
||||
|
||||
|
||||
export type DeltaChatOverJsonRpcServer = StdioDeltaChat & {
|
||||
shutdown: () => Promise<void>;
|
||||
readonly pathToServerBinary: string;
|
||||
};
|
||||
|
||||
export interface StartOptions {
|
||||
/** whether to disable outputting stderr to the parent process's stderr */
|
||||
muteStdErr: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param directory directory for accounts folder
|
||||
* @param options
|
||||
*/
|
||||
export function startDeltaChat(directory: string, options?: Partial<SearchOptions> ): Promise<DeltaChatOverJsonRpcServer>
|
||||
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): Promise<DeltaChatOverJsonRpcServer>
|
||||
|
||||
|
||||
export namespace FnTypes {
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
//@ts-check
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { stat, readdir } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import { stat } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { join, basename } from "node:path";
|
||||
import process from "node:process";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
ENV_VAR_NAME,
|
||||
PATH_EXECUTABLE_NAME,
|
||||
SKIP_SEARCH_IN_PATH,
|
||||
} from "./src/const.js";
|
||||
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
|
||||
import {
|
||||
ENV_VAR_LOCATION_NOT_FOUND,
|
||||
FAILED_TO_START_SERVER_EXECUTABLE,
|
||||
@@ -17,15 +11,8 @@ import {
|
||||
NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR,
|
||||
} from "./src/errors.js";
|
||||
|
||||
// Because this is not compiled by typescript, esm needs this stuff (` with { type: "json" };`,
|
||||
// nodejs still complains about it being experimental, but deno also uses it, so treefit bets taht it will become standard)
|
||||
import package_json from "./package.json" with { type: "json" };
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
// exports
|
||||
// - [ ] a raw starter that has a stdin/out handle thingie like desktop uses
|
||||
// - [X] a function that already wraps the stdio handle from above into the deltachat jsonrpc bindings
|
||||
|
||||
function findRPCServerInNodeModules() {
|
||||
const arch = os.arch();
|
||||
const operating_system = process.platform;
|
||||
@@ -35,7 +22,12 @@ function findRPCServerInNodeModules() {
|
||||
return resolve(package_name);
|
||||
} catch (error) {
|
||||
console.debug("findRpcServerInNodeModules", error);
|
||||
if (Object.keys(package_json.optionalDependencies).includes(package_name)) {
|
||||
const require = createRequire(import.meta.url);
|
||||
if (
|
||||
Object.keys(require("./package.json").optionalDependencies).includes(
|
||||
package_name
|
||||
)
|
||||
) {
|
||||
throw new Error(NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name));
|
||||
} else {
|
||||
throw new Error(NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR());
|
||||
@@ -44,11 +36,12 @@ function findRPCServerInNodeModules() {
|
||||
}
|
||||
|
||||
/** @type {import("./index").FnTypes.getRPCServerPath} */
|
||||
export async function getRPCServerPath(
|
||||
options = { skipSearchInPath: false, disableEnvPath: false }
|
||||
) {
|
||||
// @TODO: improve confusing naming of these options
|
||||
const { skipSearchInPath, disableEnvPath } = options;
|
||||
export async function getRPCServerPath(options = {}) {
|
||||
const { takeVersionFromPATH, disableEnvPath } = {
|
||||
takeVersionFromPATH: false,
|
||||
disableEnvPath: false,
|
||||
...options,
|
||||
};
|
||||
// 1. check if it is set as env var
|
||||
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
|
||||
try {
|
||||
@@ -63,36 +56,9 @@ export async function getRPCServerPath(
|
||||
return process.env[ENV_VAR_NAME];
|
||||
}
|
||||
|
||||
// 2. check if it can be found in PATH
|
||||
if (!process.env[SKIP_SEARCH_IN_PATH] && !skipSearchInPath) {
|
||||
const exec = promisify(execFile);
|
||||
|
||||
const { stdout: executable } =
|
||||
os.platform() !== "win32"
|
||||
? await exec("command", ["-v", PATH_EXECUTABLE_NAME])
|
||||
: await exec("where", [PATH_EXECUTABLE_NAME]);
|
||||
|
||||
// by just trying to execute it and then use "command -v deltachat-rpc-server" (unix) or "where deltachat-rpc-server" (windows) to get the path to the executable
|
||||
if (executable.length > 1) {
|
||||
// test if it is the right version
|
||||
try {
|
||||
// for some unknown reason it is in stderr and not in stdout
|
||||
const { stderr } = await promisify(execFile)(executable, ["--version"]);
|
||||
const version = stderr.slice(0, stderr.indexOf("\n"));
|
||||
if (package_json.version !== version) {
|
||||
throw new Error(
|
||||
`version mismatch: (npm package: ${package_json.version}) (installed ${PATH_EXECUTABLE_NAME} version: ${version})`
|
||||
);
|
||||
} else {
|
||||
return executable;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Found executable in PATH, but there was an error: " + error
|
||||
);
|
||||
console.error("So falling back to using prebuild...");
|
||||
}
|
||||
}
|
||||
// 2. check if PATH should be used
|
||||
if (takeVersionFromPATH) {
|
||||
return PATH_EXECUTABLE_NAME;
|
||||
}
|
||||
// 3. check for prebuilds
|
||||
|
||||
@@ -102,13 +68,14 @@ export async function getRPCServerPath(
|
||||
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||
|
||||
/** @type {import("./index").FnTypes.startDeltaChat} */
|
||||
export async function startDeltaChat(directory, options) {
|
||||
export async function startDeltaChat(directory, options = {}) {
|
||||
const pathToServerBinary = await getRPCServerPath(options);
|
||||
const server = spawn(pathToServerBinary, {
|
||||
env: {
|
||||
RUST_LOG: process.env.RUST_LOG || "info",
|
||||
RUST_LOG: process.env.RUST_LOG,
|
||||
DC_ACCOUNTS_PATH: directory,
|
||||
},
|
||||
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
@@ -123,13 +90,11 @@ export async function startDeltaChat(directory, options) {
|
||||
throw new Error("Server quit");
|
||||
});
|
||||
|
||||
server.stderr.pipe(process.stderr);
|
||||
|
||||
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
|
||||
//@ts-expect-error
|
||||
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
|
||||
|
||||
dc.shutdown = async () => {
|
||||
dc.close = () => {
|
||||
shouldClose = true;
|
||||
if (!server.kill()) {
|
||||
console.log("server termination failed");
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
"peerDependencies": {
|
||||
"@deltachat/jsonrpc-client": "*"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "node scripts/update_optional_dependencies_and_version.js"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.138.4"
|
||||
"version": "1.148.4"
|
||||
}
|
||||
|
||||
@@ -29,3 +29,6 @@ subprocess.run(["python", "scripts/build_platform_package.py", host_target], cap
|
||||
|
||||
# run update_optional_dependencies_and_version.js to adjust the package / make it installable locally
|
||||
subprocess.run(["node", "scripts/update_optional_dependencies_and_version.js", "--local"], capture_output=False, check=True)
|
||||
|
||||
# typescript / npm local package installing/linking needs that this package has it's own node_modules folder
|
||||
subprocess.run(["npm", "i"], capture_output=False, check=True)
|
||||
|
||||
@@ -2,7 +2,7 @@ def convert_cpu_arch_to_npm_cpu_arch(arch):
|
||||
if arch == "x86_64":
|
||||
return "x64"
|
||||
if arch == "i686":
|
||||
return "i32"
|
||||
return "ia32"
|
||||
if arch == "aarch64":
|
||||
return "arm64"
|
||||
if arch == "armv7" or arch == "arm":
|
||||
|
||||
@@ -13,14 +13,21 @@ def write_package_json(platform_path, rust_target, my_binary_name):
|
||||
tomlfile = open("../../Cargo.toml", 'rb')
|
||||
version = tomllib.load(tomlfile)['package']['version']
|
||||
|
||||
package_json = dict({
|
||||
"name": "@deltachat/stdio-rpc-server-" + convert_os_to_npm_os(os) + "-" + convert_cpu_arch_to_npm_cpu_arch(cpu_arch),
|
||||
package_json = {
|
||||
"name": "@deltachat/stdio-rpc-server-"
|
||||
+ convert_os_to_npm_os(os)
|
||||
+ "-"
|
||||
+ convert_cpu_arch_to_npm_cpu_arch(cpu_arch),
|
||||
"version": version,
|
||||
"os": [convert_os_to_npm_os(os)],
|
||||
"cpu": [convert_cpu_arch_to_npm_cpu_arch(cpu_arch)],
|
||||
"main": my_binary_name,
|
||||
"license": "MPL-2.0"
|
||||
})
|
||||
"license": "MPL-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git",
|
||||
},
|
||||
}
|
||||
|
||||
file = open(platform_path + "/package.json", 'w')
|
||||
file.write(json.dumps(package_json, indent=4))
|
||||
|
||||
@@ -54,4 +54,11 @@ for (const { folder_name, package_name } of platform_package_names) {
|
||||
: version;
|
||||
}
|
||||
|
||||
if (is_local) {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
|
||||
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
|
||||
} else {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
|
||||
}
|
||||
|
||||
await fs.writeFile("./package.json", JSON.stringify(package_json, null, 4));
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
export const PATH_EXECUTABLE_NAME = 'deltachat-rpc-server'
|
||||
|
||||
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"
|
||||
export const SKIP_SEARCH_IN_PATH = "DELTA_CHAT_SKIP_PATH"
|
||||
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"
|
||||
@@ -1,3 +1,4 @@
|
||||
#![recursion_limit = "256"]
|
||||
//! Delta Chat core RPC server.
|
||||
//!
|
||||
//! It speaks JSON Lines over stdio.
|
||||
@@ -10,6 +11,7 @@ use deltachat::constants::DC_VERSION_STR;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use yerpc::RpcServer as _;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
@@ -27,6 +29,9 @@ async fn main() {
|
||||
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
||||
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
|
||||
// until the user presses enter."
|
||||
if let Err(error) = &r {
|
||||
log::error!("Fatal error: {error:#}.")
|
||||
}
|
||||
std::process::exit(if r.is_ok() { 0 } else { 1 });
|
||||
}
|
||||
|
||||
@@ -59,7 +64,13 @@ async fn main_impl() -> Result<()> {
|
||||
#[cfg(target_family = "unix")]
|
||||
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interferring with JSON-RPC using stdout.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
|
||||
42
deny.toml
42
deny.toml
@@ -1,7 +1,6 @@
|
||||
[advisories]
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0071",
|
||||
"RUSTSEC-2022-0093",
|
||||
|
||||
# Timing attack on RSA.
|
||||
# Delta Chat does not use RSA for new keys
|
||||
@@ -10,9 +9,6 @@ ignore = [
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
|
||||
"RUSTSEC-2023-0071",
|
||||
|
||||
# Unmaintained ansi_term
|
||||
"RUSTSEC-2021-0139",
|
||||
|
||||
# Unmaintained encoding
|
||||
"RUSTSEC-2021-0153",
|
||||
]
|
||||
@@ -24,26 +20,19 @@ ignore = [
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base16ct", version = "0.1.1" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "convert_case", version = "0.4.0" },
|
||||
{ name = "curve25519-dalek", version = "3.2.0" },
|
||||
{ name = "darling_core", version = "<0.14" },
|
||||
{ name = "darling_macro", version = "<0.14" },
|
||||
{ name = "darling", version = "<0.14" },
|
||||
{ name = "der", version = "0.6.1" },
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "0.10.2" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "event-listener", version = "4.0.3" },
|
||||
{ name = "fastrand", version = "1.9.0" },
|
||||
{ name = "futures-lite", version = "1.13.0" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "idna", version = "0.4.0" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "h2", version = "0.3.26" },
|
||||
{ name = "http-body", version = "0.4.6" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "hyper", version = "0.14.28" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
@@ -51,29 +40,22 @@ skip = [
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "ring", version = "0.16.20" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "toml_edit", version = "0.21.1" },
|
||||
{ name = "untrusted", version = "0.7.1" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.52" },
|
||||
{ name = "windows-core", version = "<0.54.0" },
|
||||
{ name = "windows_i686_gnu", version = "<0.52" },
|
||||
{ name = "windows_i686_msvc", version = "<0.52" },
|
||||
{ name = "windows-sys", version = "<0.52" },
|
||||
{ name = "windows-sys", version = "<0.59" },
|
||||
{ name = "windows-targets", version = "<0.52" },
|
||||
{ name = "windows", version = "0.32.0" },
|
||||
{ name = "windows", version = "<0.54.0" },
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.52" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.52" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
{ name = "winreg", version = "0.50.0" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
35
flake.lock
generated
35
flake.lock
generated
@@ -48,11 +48,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713421495,
|
||||
"narHash": "sha256-5vVF9W1tJT+WdfpWAEG76KywktKDAW/71mVmNHEHjac=",
|
||||
"lastModified": 1729578683,
|
||||
"narHash": "sha256-h0Wmvrkadbyi3IJXFLPi+QyYjCAKDr2xQ6dLxlQ8cXY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "fd47b1f9404fae02a4f38bd9f4b12bad7833c96b",
|
||||
"rev": "d66cda53e8193a878742dcadb5bb75f4df7c3c0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -166,11 +166,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1713248628,
|
||||
"narHash": "sha256-NLznXB5AOnniUtZsyy/aPWOk8ussTuePp2acb9U+ISA=",
|
||||
"lastModified": 1729256560,
|
||||
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5672bc9dbf9d88246ddab5ac454e82318d094bb8",
|
||||
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -182,12 +182,11 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1713562564,
|
||||
"narHash": "sha256-NQpYhgoy0M89g9whRixSwsHb8RFIbwlxeYiVSDwSXJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "92d295f588631b0db2da509f381b4fb1e74173c5",
|
||||
"type": "github"
|
||||
"lastModified": 1711668574,
|
||||
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
|
||||
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
|
||||
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
@@ -196,11 +195,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1713537308,
|
||||
"narHash": "sha256-XtTSSIB2DA6tOv+l0FhvfDMiyCmhoRbNB+0SeInZkbk=",
|
||||
"lastModified": 1729413321,
|
||||
"narHash": "sha256-I4tuhRpZFa6Fu6dcH9Dlo5LlH17peT79vx1y1SpeKt0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5c24cf2f0a12ad855f444c30b2421d044120c66f",
|
||||
"rev": "1997e4aa514312c1af7e2bda7fad1644e778ff26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -223,11 +222,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1713373173,
|
||||
"narHash": "sha256-octd9BFY9G/Gbr4KfwK4itZp4Lx+qvJeRRcYnN+dEH8=",
|
||||
"lastModified": 1729533545,
|
||||
"narHash": "sha256-A/AuEWcGwwjpfBCZqWDNNg5GwYrJduzLvlMe+A7xG5U=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "46702ffc1a02a2ac153f1d1ce619ec917af8f3a6",
|
||||
"rev": "de2ff17bc513807412d7bbaba1d995a774938583",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
23
flake.nix
23
flake.nix
@@ -525,15 +525,26 @@
|
||||
};
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
devShells.default = let
|
||||
pkgs = import nixpkgs {
|
||||
system = system;
|
||||
overlays = [ fenix.overlays.default ];
|
||||
};
|
||||
in pkgs.mkShell {
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
clippy
|
||||
rustc
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"clippy"
|
||||
"rust-src"
|
||||
"rustc"
|
||||
"rustfmt"
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
2675
fuzz/Cargo.lock
generated
2675
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ module.exports = {
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
|
||||
DC_EVENT_CHANNEL_OVERFLOW: 2400,
|
||||
DC_EVENT_CHATLIST_CHANGED: 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
@@ -49,6 +50,7 @@ module.exports = {
|
||||
DC_EVENT_IMEX_PROGRESS: 2051,
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
|
||||
DC_EVENT_INCOMING_REACTION: 2002,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
@@ -66,6 +68,8 @@ module.exports = {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT: 2151,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
DC_GCL_ADD_SELF: 2,
|
||||
@@ -110,6 +114,7 @@ module.exports = {
|
||||
DC_MSG_IMAGE: 20,
|
||||
DC_MSG_STICKER: 23,
|
||||
DC_MSG_TEXT: 10,
|
||||
DC_MSG_VCARD: 90,
|
||||
DC_MSG_VIDEO: 50,
|
||||
DC_MSG_VIDEOCHAT_INVITATION: 70,
|
||||
DC_MSG_VOICE: 41,
|
||||
@@ -125,11 +130,13 @@ module.exports = {
|
||||
DC_QR_ASK_VERIFYCONTACT: 200,
|
||||
DC_QR_ASK_VERIFYGROUP: 202,
|
||||
DC_QR_BACKUP: 251,
|
||||
DC_QR_BACKUP2: 252,
|
||||
DC_QR_ERROR: 400,
|
||||
DC_QR_FPR_MISMATCH: 220,
|
||||
DC_QR_FPR_OK: 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR: 230,
|
||||
DC_QR_LOGIN: 520,
|
||||
DC_QR_PROXY: 271,
|
||||
DC_QR_REVIVE_VERIFYCONTACT: 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP: 512,
|
||||
DC_QR_TEXT: 330,
|
||||
@@ -173,6 +180,7 @@ module.exports = {
|
||||
DC_STR_CONFIGURATION_FAILED: 84,
|
||||
DC_STR_CONNECTED: 107,
|
||||
DC_STR_CONNTECTING: 108,
|
||||
DC_STR_CONTACT: 200,
|
||||
DC_STR_CONTACT_NOT_VERIFIED: 36,
|
||||
DC_STR_CONTACT_SETUP_CHANGED: 37,
|
||||
DC_STR_CONTACT_VERIFIED: 35,
|
||||
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
@@ -37,7 +38,10 @@ module.exports = {
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED'
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export enum C {
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
|
||||
DC_EVENT_CHANNEL_OVERFLOW = 2400,
|
||||
DC_EVENT_CHATLIST_CHANGED = 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
@@ -49,6 +50,7 @@ export enum C {
|
||||
DC_EVENT_IMEX_PROGRESS = 2051,
|
||||
DC_EVENT_INCOMING_MSG = 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
|
||||
DC_EVENT_INCOMING_REACTION = 2002,
|
||||
DC_EVENT_INFO = 100,
|
||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||
DC_EVENT_MSGS_CHANGED = 2000,
|
||||
@@ -66,6 +68,8 @@ export enum C {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT = 2151,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
@@ -110,6 +114,7 @@ export enum C {
|
||||
DC_MSG_IMAGE = 20,
|
||||
DC_MSG_STICKER = 23,
|
||||
DC_MSG_TEXT = 10,
|
||||
DC_MSG_VCARD = 90,
|
||||
DC_MSG_VIDEO = 50,
|
||||
DC_MSG_VIDEOCHAT_INVITATION = 70,
|
||||
DC_MSG_VOICE = 41,
|
||||
@@ -125,11 +130,13 @@ export enum C {
|
||||
DC_QR_ASK_VERIFYCONTACT = 200,
|
||||
DC_QR_ASK_VERIFYGROUP = 202,
|
||||
DC_QR_BACKUP = 251,
|
||||
DC_QR_BACKUP2 = 252,
|
||||
DC_QR_ERROR = 400,
|
||||
DC_QR_FPR_MISMATCH = 220,
|
||||
DC_QR_FPR_OK = 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230,
|
||||
DC_QR_LOGIN = 520,
|
||||
DC_QR_PROXY = 271,
|
||||
DC_QR_REVIVE_VERIFYCONTACT = 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP = 512,
|
||||
DC_QR_TEXT = 330,
|
||||
@@ -173,6 +180,7 @@ export enum C {
|
||||
DC_STR_CONFIGURATION_FAILED = 84,
|
||||
DC_STR_CONNECTED = 107,
|
||||
DC_STR_CONNTECTING = 108,
|
||||
DC_STR_CONTACT = 200,
|
||||
DC_STR_CONTACT_NOT_VERIFIED = 36,
|
||||
DC_STR_CONTACT_SETUP_CHANGED = 37,
|
||||
DC_STR_CONTACT_VERIFIED = 35,
|
||||
@@ -315,6 +323,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2002: 'DC_EVENT_INCOMING_REACTION',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
@@ -336,7 +345,10 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2151: 'DC_EVENT_WEBXDC_REALTIME_ADVERTISEMENT',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
|
||||
}
|
||||
|
||||
@@ -475,47 +475,6 @@ export class Context extends EventEmitter {
|
||||
return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
|
||||
}
|
||||
|
||||
getNextMediaMessage(
|
||||
messageId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
) {
|
||||
debug(
|
||||
`getNextMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
|
||||
)
|
||||
return this._getNextMedia(messageId, 1, msgType1, msgType2, msgType3)
|
||||
}
|
||||
|
||||
getPreviousMediaMessage(
|
||||
messageId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
) {
|
||||
debug(
|
||||
`getPreviousMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
|
||||
)
|
||||
return this._getNextMedia(messageId, -1, msgType1, msgType2, msgType3)
|
||||
}
|
||||
|
||||
_getNextMedia(
|
||||
messageId: number,
|
||||
dir: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
): number {
|
||||
return binding.dcn_get_next_media(
|
||||
this.dcn_context,
|
||||
Number(messageId),
|
||||
dir,
|
||||
msgType1 || 0,
|
||||
msgType2 || 0,
|
||||
msgType3 || 0
|
||||
)
|
||||
}
|
||||
|
||||
getSecurejoinQrCode(chatId: number): string {
|
||||
debug(`getSecurejoinQrCode ${chatId}`)
|
||||
return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))
|
||||
|
||||
@@ -1053,27 +1053,6 @@ NAPI_METHOD(dcn_get_msg_html) {
|
||||
NAPI_RETURN_AND_UNREF_STRING(msg_html);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_get_next_media) {
|
||||
NAPI_ARGV(6);
|
||||
NAPI_DCN_CONTEXT();
|
||||
NAPI_ARGV_UINT32(msg_id, 1);
|
||||
NAPI_ARGV_INT32(dir, 2);
|
||||
NAPI_ARGV_INT32(msg_type1, 3);
|
||||
NAPI_ARGV_INT32(msg_type2, 4);
|
||||
NAPI_ARGV_INT32(msg_type3, 5);
|
||||
|
||||
//TRACE("calling..");
|
||||
uint32_t next_id = dc_get_next_media(dcn_context->dc_context,
|
||||
msg_id,
|
||||
dir,
|
||||
msg_type1,
|
||||
msg_type2,
|
||||
msg_type3);
|
||||
//TRACE("result %d", next_id);
|
||||
|
||||
NAPI_RETURN_UINT32(next_id);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_set_chat_visibility) {
|
||||
NAPI_ARGV(3);
|
||||
NAPI_DCN_CONTEXT();
|
||||
@@ -3443,7 +3422,6 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_msg_cnt);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_msg_info);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_msg_html);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_next_media);
|
||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_visibility);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr);
|
||||
NAPI_EXPORT_FUNCTION(dcn_get_securejoin_qr_svg);
|
||||
|
||||
@@ -271,7 +271,7 @@ describe('Basic offline Tests', function () {
|
||||
'sync_msgs',
|
||||
'sentbox_watch',
|
||||
'show_emails',
|
||||
'socks5_enabled',
|
||||
'proxy_enabled',
|
||||
'sqlite_version',
|
||||
'uptime',
|
||||
'used_account_settings',
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"chai": "~4.3.10",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"mocha": "^8.2.1",
|
||||
"node-gyp": "^10.0.0",
|
||||
"node-gyp": "~10.1.0",
|
||||
"prebuildify": "^5.0.1",
|
||||
"prebuildify-ci": "^1.0.5",
|
||||
"prettier": "^3.0.3",
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.138.4"
|
||||
"version": "1.148.4"
|
||||
}
|
||||
|
||||
@@ -44,11 +44,6 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_configure_completed*
|
||||
""",
|
||||
)
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.138.4"
|
||||
version = "1.148.4"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
|
||||
@@ -194,15 +194,13 @@ class Account:
|
||||
assert res != ffi.NULL, f"config value not found for: {name!r}"
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
|
||||
def _preconfigure_keypair(self, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
"""
|
||||
res = lib.dc_preconfigure_keypair(
|
||||
self._dc_context,
|
||||
as_dc_charpointer(addr),
|
||||
ffi.NULL,
|
||||
as_dc_charpointer(secret),
|
||||
)
|
||||
if res == 0:
|
||||
|
||||
@@ -308,7 +308,7 @@ class Chat:
|
||||
msg = as_dc_charpointer(text)
|
||||
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
|
||||
if msg_id == 0:
|
||||
raise ValueError("message could not be send, does chat exist?")
|
||||
raise ValueError("The message could not be sent. Does the chat exist?")
|
||||
return Message.from_db(self.account, msg_id)
|
||||
|
||||
def send_file(self, path, mime_type="application/octet-stream"):
|
||||
|
||||
@@ -8,19 +8,19 @@ import io
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
from typing import List
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from deltachat import Account, const
|
||||
if TYPE_CHECKING:
|
||||
from deltachat import Account
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
@@ -28,7 +28,7 @@ ALL = "1:*"
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account: Account) -> None:
|
||||
def __init__(self, account: "Account") -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
@@ -36,27 +36,13 @@ class DirectImap:
|
||||
|
||||
def connect(self):
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
port = 993
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
host = user.rsplit("@")[-1]
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
if security == const.DC_SOCKET_PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.DC_SOCKET_STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
@@ -462,7 +462,7 @@ class ACFactory:
|
||||
def remove_preconfigured_keys(self) -> None:
|
||||
self._preconfigured_keys = []
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
def _preconfigure_key(self, account):
|
||||
# Only set a preconfigured key if we haven't used it yet for another account.
|
||||
try:
|
||||
keyname = self._preconfigured_keys.pop(0)
|
||||
@@ -471,9 +471,9 @@ class ACFactory:
|
||||
else:
|
||||
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
|
||||
if fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_sec)
|
||||
account._preconfigure_keypair(fname_sec)
|
||||
return True
|
||||
print(f"WARN: could not use preconfigured keys for {addr!r}")
|
||||
print("WARN: could not use preconfigured keys")
|
||||
|
||||
def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account:
|
||||
# do a pseudo-configured account
|
||||
@@ -492,7 +492,7 @@ class ACFactory:
|
||||
"configured": "1",
|
||||
},
|
||||
)
|
||||
self._preconfigure_key(ac, addr)
|
||||
self._preconfigure_key(ac)
|
||||
self._acsetup.init_logging(ac)
|
||||
return ac
|
||||
|
||||
@@ -525,9 +525,10 @@ class ACFactory:
|
||||
configdict.setdefault("mvbox_move", False)
|
||||
configdict.setdefault("sentbox_watch", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
configdict.setdefault("delete_server_after", 0)
|
||||
ac.update_config(configdict)
|
||||
self._acsetup._account2config[ac] = configdict
|
||||
self._preconfigure_key(ac, configdict["addr"])
|
||||
self._preconfigure_key(ac)
|
||||
return ac
|
||||
|
||||
def wait_configured(self, account) -> None:
|
||||
@@ -552,6 +553,15 @@ class ACFactory:
|
||||
|
||||
bot_cfg = self.get_next_liveconfig()
|
||||
bot_ac = self.prepare_account_from_liveconfig(bot_cfg)
|
||||
self._acsetup.start_configure(bot_ac)
|
||||
self.wait_configured(bot_ac)
|
||||
bot_ac.start_io()
|
||||
# Wait for DC_EVENT_IMAP_INBOX_IDLE so that all emails appeared in the bot's Inbox later are
|
||||
# considered new and not existing ones, and thus processed by the bot.
|
||||
print(bot_ac._logid, "waiting for inbox IDLE to become ready")
|
||||
bot_ac._evtracker.wait_idle_inbox_ready()
|
||||
bot_ac.stop_io()
|
||||
self._acsetup._account2state[bot_ac] = self._acsetup.IDLEREADY
|
||||
|
||||
# Forget ac as it will be opened by the bot subprocess
|
||||
# but keep something in the list to not confuse account generation
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import deltachat as dc
|
||||
@@ -675,3 +676,17 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
def test_deleted_msgs_dont_reappear(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
msg = chat.send_text("hello")
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
ac1.delete_messages([msg])
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELETED")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
time.sleep(5)
|
||||
assert len(chat.get_messages()) == 0
|
||||
|
||||
@@ -484,6 +484,24 @@ def test_move_works_on_self_sent(acfactory):
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
|
||||
|
||||
|
||||
def test_move_sync_msgs(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
# Sync messages may also be sent during the configuration.
|
||||
mvbox_msg_cnt = len(ac1.direct_imap.get_all_messages())
|
||||
|
||||
ac1.set_config("displayname", "Alice")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
ac1.set_config("displayname", "Bob")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||
ac1.direct_imap.select_folder("Inbox")
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 0
|
||||
ac1.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac1.direct_imap.get_all_messages()) == mvbox_msg_cnt + 2
|
||||
|
||||
|
||||
def test_forward_messages(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
@@ -610,7 +628,7 @@ def test_long_group_name(acfactory, lp):
|
||||
|
||||
|
||||
def test_send_self_message(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, bcc_self=True)
|
||||
acfactory.bring_accounts_online()
|
||||
lp.sec("ac1: create self chat")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
@@ -1562,8 +1580,6 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
|
||||
# check progress events for import
|
||||
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
|
||||
assert imex_tracker.wait_progress(500, progress_upper_limit=749)
|
||||
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
|
||||
assert imex_tracker.wait_progress(1000)
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
@@ -2068,12 +2084,11 @@ def test_send_receive_locations(acfactory, lp):
|
||||
def test_immediate_autodelete(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
# "1" means delete immediately, while "0" means do not delete
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
@@ -67,7 +67,7 @@ class TestOfflineAccountBasic:
|
||||
ac = acfactory.get_unconfigured_account()
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_secret)
|
||||
ac._preconfigure_keypair(alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-05-15
|
||||
2024-10-24
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.78.0
|
||||
RUST_VERSION=1.82.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -149,7 +149,7 @@ def process_data(data, file):
|
||||
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
|
||||
|
||||
provider = ""
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", ""))
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", "") or "")
|
||||
after_login_hint = cleanstr(data.get("after_login_hint", ""))
|
||||
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
|
||||
provider += (
|
||||
|
||||
@@ -3,4 +3,4 @@ set -euo pipefail
|
||||
|
||||
tox -c deltachat-rpc-client -e py --devenv venv
|
||||
venv/bin/pip install --upgrade pip
|
||||
cargo install --path deltachat-rpc-server/ --root "$PWD/venv" --debug
|
||||
cargo install --locked --path deltachat-rpc-server/ --root "$PWD/venv" --debug
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo install --path deltachat-rpc-server/ --root "$PWD/venv" --debug
|
||||
cargo install --locked --path deltachat-rpc-server/ --root "$PWD/venv" --debug
|
||||
PATH="$PWD/venv/bin:$PATH" tox -c deltachat-rpc-client
|
||||
|
||||
@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,py313,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=2f3db24107e4802c2df0aa0a40f0e144006c0a9b
|
||||
REV=77cbf92a8565fdf1bcaba10fa93c1455c750a1e9
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
77
spec.md
77
spec.md
@@ -1,6 +1,6 @@
|
||||
# chat-mail specification
|
||||
|
||||
Version: 0.34.0
|
||||
Version: 0.35.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -22,7 +22,13 @@ to implement typical messenger functions.
|
||||
- [Locations](#locations)
|
||||
- [User locations](#user-locations)
|
||||
- [Points of interest](#points-of-interest)
|
||||
- [Stickers](#stickers)
|
||||
- [Voice messages](#voice-messages)
|
||||
- [Reactions](#reactions)
|
||||
- [Attaching a contact to a message](#attaching-a-contact-to-a-message)
|
||||
- [Transitioning to a new e-mail address (AEAP)](#transitioning-to-a-new-e-mail-address-aeap)
|
||||
- [Miscellaneous](#miscellaneous)
|
||||
- [Sync messages](#sync-messages)
|
||||
|
||||
|
||||
# Encryption
|
||||
@@ -461,6 +467,58 @@ As an extension to RFC 9078, it is allowed to send empty reaction message,
|
||||
in which case all previously sent reactions are retracted.
|
||||
|
||||
|
||||
# Attaching a contact to a message
|
||||
|
||||
Messengers MAY allow the user to attach a contact to a message
|
||||
in order to share it with the chat partner.
|
||||
The contact MUST be sent as a [vCard](https://datatracker.ietf.org/doc/html/rfc6350).
|
||||
|
||||
The vCard MUST contain `EMAIL`,
|
||||
`FN` (display name),
|
||||
and `VERSION` (which version of the vCard standard you're using).
|
||||
If available, it SHOULD contain
|
||||
`REV` (current timestamp),
|
||||
`PHOTO` (avatar), and
|
||||
`KEY` (OpenPGP public key,
|
||||
in binary format,
|
||||
encoded with vanilla base64;
|
||||
note that this is different from the OpenPGP 'ASCII Armor' format).
|
||||
|
||||
Example vCard:
|
||||
|
||||
```
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
EMAIL:alice@example.org
|
||||
FN:Alice Wonderland
|
||||
KEY:data:application/pgp-keys;base64,[Base64-data]
|
||||
PHOTO:data:image/jpeg;base64,[image in Base64]
|
||||
REV:20240418T184242Z
|
||||
END:VCARD
|
||||
```
|
||||
|
||||
It is fine if messengers do include a full vCard parser
|
||||
and e.g. simply search for the line starting with `EMAIL`
|
||||
in order to get the email address.
|
||||
|
||||
|
||||
# Transitioning to a new e-mail address (AEAP)
|
||||
|
||||
When receiving a message:
|
||||
- If the key exists, but belongs to another address
|
||||
- AND there is a `Chat-Version` header
|
||||
- AND the message is signed correctly
|
||||
- AND the From address is (also) in the encrypted (and therefore signed) headers
|
||||
- AND the message timestamp is newer than the contact's `lastseen`
|
||||
(to prevent changing the address back when messages arrive out of order)
|
||||
(this condition is not that important
|
||||
since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups,
|
||||
possibly deduplicate the members list,
|
||||
and add a system message to all of these chats.
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
@@ -484,21 +542,4 @@ We define the effective date of a message
|
||||
as the sending time of the message as indicated by its Date header,
|
||||
or the time of first receipt if that date is in the future or unavailable.
|
||||
|
||||
|
||||
# Transitioning to a new e-mail address (AEAP)
|
||||
|
||||
When receiving a message:
|
||||
- If the key exists, but belongs to another address
|
||||
- AND there is a `Chat-Version` header
|
||||
- AND the message is signed correctly
|
||||
- AND the From address is (also) in the encrypted (and therefore signed) headers
|
||||
- AND the message timestamp is newer than the contact's `lastseen`
|
||||
(to prevent changing the address back when messages arrive out of order)
|
||||
(this condition is not that important
|
||||
since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups,
|
||||
possibly deduplicate the members list,
|
||||
and add a system message to all of these chats.
|
||||
|
||||
Copyright © 2017-2021 Delta Chat contributors.
|
||||
|
||||
@@ -166,6 +166,19 @@ impl Accounts {
|
||||
.remove(&id)
|
||||
.with_context(|| format!("no account with id {id}"))?;
|
||||
ctx.stop_io().await;
|
||||
|
||||
// Explicitly close the database
|
||||
// to make sure the database file is closed
|
||||
// and can be removed on Windows.
|
||||
// If some spawned task tries to use the database afterwards,
|
||||
// it will fail.
|
||||
//
|
||||
// Previously `stop_io()` aborted the tasks without awaiting them
|
||||
// and this resulted in keeping `Context` clones inside
|
||||
// `Future`s that were not dropped. This bug is fixed now,
|
||||
// but explicitly closing the database ensures that file is freed
|
||||
// even if not all `Context` references are dropped.
|
||||
ctx.sql.close().await;
|
||||
drop(ctx);
|
||||
|
||||
if let Some(cfg) = self.config.get_account(id) {
|
||||
@@ -485,10 +498,6 @@ impl Config {
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
|
||||
let dir = file
|
||||
.parent()
|
||||
.context("Cannot get config file directory")?
|
||||
.to_path_buf();
|
||||
let mut config = Self::new_nosync(file, writable).await?;
|
||||
let bytes = fs::read(&config.file)
|
||||
.await
|
||||
@@ -500,9 +509,13 @@ impl Config {
|
||||
// Convert them to relative paths.
|
||||
let mut modified = false;
|
||||
for account in &mut config.inner.accounts {
|
||||
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
|
||||
account.dir = new_dir.to_path_buf();
|
||||
modified = true;
|
||||
if account.dir.is_absolute() {
|
||||
if let Some(old_path_parent) = account.dir.parent() {
|
||||
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
|
||||
account.dir = new_path.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if modified && writable {
|
||||
|
||||
495
src/blob.rs
495
src/blob.rs
@@ -3,7 +3,7 @@
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -12,6 +12,7 @@ use anyhow::{format_err, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::ImageReader;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -252,16 +253,16 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// The extension part will always be lowercased.
|
||||
fn sanitise_name(name: &str) -> (String, String) {
|
||||
let mut name = name.to_string();
|
||||
let mut name = name;
|
||||
for part in name.rsplit('/') {
|
||||
if !part.is_empty() {
|
||||
name = part.to_string();
|
||||
name = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for part in name.rsplit('\\') {
|
||||
if !part.is_empty() {
|
||||
name = part.to_string();
|
||||
name = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -271,32 +272,39 @@ impl<'a> BlobObject<'a> {
|
||||
replacement: "",
|
||||
};
|
||||
|
||||
let clean = sanitize_filename::sanitize_with_options(name, opts);
|
||||
// Let's take the tricky filename
|
||||
let name = sanitize_filename::sanitize_with_options(name, opts);
|
||||
// Let's take a tricky filename,
|
||||
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
|
||||
// Split it into "file" and "with_lots_of_characters_behind_point_and_double_ending.tar.gz":
|
||||
let mut iter = clean.splitn(2, '.');
|
||||
|
||||
let stem: String = iter.next().unwrap_or_default().chars().take(64).collect();
|
||||
// stem == "file"
|
||||
|
||||
let ext_chars = iter.next().unwrap_or_default().chars();
|
||||
let ext: String = ext_chars
|
||||
// Assume that the extension is 32 chars maximum.
|
||||
let ext: String = name
|
||||
.chars()
|
||||
.rev()
|
||||
.take(32)
|
||||
.take_while(|c| !c.is_whitespace())
|
||||
.take(33)
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
.rev()
|
||||
.collect();
|
||||
// ext == "d_point_and_double_ending.tar.gz"
|
||||
// ext == "nd_point_and_double_ending.tar.gz"
|
||||
|
||||
if ext.is_empty() {
|
||||
(stem, "".to_string())
|
||||
// Split it into "nd_point_and_double_ending" and "tar.gz":
|
||||
let mut iter = ext.splitn(2, '.');
|
||||
iter.next();
|
||||
|
||||
let ext = iter.next().unwrap_or_default();
|
||||
let ext = if ext.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
(stem, format!(".{ext}").to_lowercase())
|
||||
// Return ("file", ".d_point_and_double_ending.tar.gz")
|
||||
// which is not perfect but acceptable.
|
||||
}
|
||||
format!(".{ext}")
|
||||
// ".tar.gz"
|
||||
};
|
||||
let stem = name
|
||||
.strip_suffix(&ext)
|
||||
.unwrap_or_default()
|
||||
.chars()
|
||||
.take(64)
|
||||
.collect();
|
||||
(stem, ext.to_lowercase())
|
||||
}
|
||||
|
||||
/// Checks whether a name is a valid blob name.
|
||||
@@ -426,9 +434,25 @@ impl<'a> BlobObject<'a> {
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let res = tokio::task::block_in_place(move || {
|
||||
let (nr_bytes, exif) = self.metadata()?;
|
||||
let mut file = std::fs::File::open(self.to_abs_path())?;
|
||||
let (nr_bytes, exif) = image_metadata(&file)?;
|
||||
*no_exif_ref = exif.is_none();
|
||||
let mut img = image::open(&blob_abs).context("image decode failure")?;
|
||||
// It's strange that BufReader modifies a file position while it takes a non-mut
|
||||
// reference. Ok, just rewind it.
|
||||
file.rewind()?;
|
||||
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
|
||||
let imgreader = match imgreader {
|
||||
Ok(ir) => ir,
|
||||
_ => {
|
||||
file.rewind()?;
|
||||
ImageReader::with_format(
|
||||
std::io::BufReader::new(&file),
|
||||
ImageFormat::from_path(&blob_abs)?,
|
||||
)
|
||||
}
|
||||
};
|
||||
let fmt = imgreader.format().context("No format??")?;
|
||||
let mut img = imgreader.decode().context("image decode failure")?;
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let mut encoded = Vec::new();
|
||||
let mut changed_name = None;
|
||||
@@ -457,10 +481,9 @@ impl<'a> BlobObject<'a> {
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
let fmt = ImageFormat::from_path(&blob_abs);
|
||||
let ofmt = match fmt {
|
||||
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
Ok(ImageFormat::Jpeg) => {
|
||||
ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
ImageFormat::Jpeg => {
|
||||
add_white_bg = false;
|
||||
ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
@@ -497,7 +520,7 @@ impl<'a> BlobObject<'a> {
|
||||
img_wh = max(img.width(), img.height());
|
||||
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
|
||||
// crate when recoding, so don't scale them down.
|
||||
if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
|
||||
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
|
||||
img_wh = img_wh * 2 / 3;
|
||||
}
|
||||
}
|
||||
@@ -538,7 +561,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
if do_scale || exif.is_some() {
|
||||
// The file format is JPEG/PNG now, we may have to change the file extension
|
||||
if !matches!(fmt, Ok(ImageFormat::Jpeg))
|
||||
if !matches!(fmt, ImageFormat::Jpeg)
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
|
||||
{
|
||||
blob_abs = blob_abs.with_extension("jpg");
|
||||
@@ -575,15 +598,14 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns image file size and Exif.
|
||||
pub fn metadata(&self) -> Result<(u64, Option<exif::Exif>)> {
|
||||
let file = std::fs::File::open(self.to_abs_path())?;
|
||||
let len = file.metadata()?.len();
|
||||
let mut bufreader = std::io::BufReader::new(&file);
|
||||
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
/// Returns image file size and Exif.
|
||||
pub fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
|
||||
let len = file.metadata()?.len();
|
||||
let mut bufreader = std::io::BufReader::new(file);
|
||||
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
|
||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
@@ -600,7 +622,7 @@ fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for BlobObject<'a> {
|
||||
impl fmt::Display for BlobObject<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "$BLOBDIR/{}", self.name)
|
||||
}
|
||||
@@ -651,10 +673,6 @@ impl<'a> BlobDirContents<'a> {
|
||||
pub(crate) fn iter(&self) -> BlobDirIter<'_> {
|
||||
BlobDirIter::new(self.context, self.inner.iter())
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// A iterator over all the [`BlobObject`]s in the blobdir.
|
||||
@@ -952,6 +970,19 @@ mod tests {
|
||||
assert!(!stem.contains(':'));
|
||||
assert!(!stem.contains('*'));
|
||||
assert!(!stem.contains('?'));
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name(
|
||||
"file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
|
||||
);
|
||||
assert_eq!(
|
||||
stem,
|
||||
"file.with_lots_of_characters_behind_point_and_double_ending"
|
||||
);
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
|
||||
let (stem, ext) = BlobObject::sanitise_name("a. tar.tar.gz");
|
||||
assert_eq!(stem, "a. tar");
|
||||
assert_eq!(ext, ".tar.gz");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1086,32 +1117,34 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_1() {
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1120,18 +1153,20 @@ mod tests {
|
||||
async fn test_recode_image_2() {
|
||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
2000,
|
||||
1800,
|
||||
270,
|
||||
1800,
|
||||
2000,
|
||||
)
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 2000,
|
||||
original_height: 1800,
|
||||
orientation: 270,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
@@ -1140,18 +1175,18 @@ mod tests {
|
||||
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
|
||||
let bytes = buf.into_inner();
|
||||
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
&bytes,
|
||||
"jpg",
|
||||
false, // no Exif
|
||||
1800,
|
||||
2000,
|
||||
0,
|
||||
1800,
|
||||
2000,
|
||||
)
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes: &bytes,
|
||||
extension: "jpg",
|
||||
original_width: 1800,
|
||||
original_height: 2000,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
@@ -1161,49 +1196,80 @@ mod tests {
|
||||
async fn test_recode_image_balanced_png() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
1920,
|
||||
1080,
|
||||
)
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::WORSE_IMAGE_SIZE,
|
||||
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
set_draft: true,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Sticker,
|
||||
Some("0"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
1920,
|
||||
1080,
|
||||
)
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1214,18 +1280,18 @@ mod tests {
|
||||
async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot-rgba.png");
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::WORSE_IMAGE_SIZE,
|
||||
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1233,18 +1299,19 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_huge_jpg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::BALANCED_IMAGE_SIZE,
|
||||
constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
||||
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1266,68 +1333,93 @@ mod tests {
|
||||
assert_eq!(luma, 0);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn send_image_check_mediaquality(
|
||||
viewtype: Viewtype,
|
||||
media_quality_config: Option<&str>,
|
||||
bytes: &[u8],
|
||||
extension: &str,
|
||||
has_exif: bool,
|
||||
original_width: u32,
|
||||
original_height: u32,
|
||||
orientation: i32,
|
||||
compressed_width: u32,
|
||||
compressed_height: u32,
|
||||
) -> anyhow::Result<DynamicImage> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, media_quality_config)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension(extension);
|
||||
#[derive(Default)]
|
||||
struct SendImageCheckMediaquality<'a> {
|
||||
pub(crate) viewtype: Viewtype,
|
||||
pub(crate) media_quality_config: &'a str,
|
||||
pub(crate) bytes: &'a [u8],
|
||||
pub(crate) extension: &'a str,
|
||||
pub(crate) has_exif: bool,
|
||||
pub(crate) original_width: u32,
|
||||
pub(crate) original_height: u32,
|
||||
pub(crate) orientation: i32,
|
||||
pub(crate) compressed_width: u32,
|
||||
pub(crate) compressed_height: u32,
|
||||
pub(crate) set_draft: bool,
|
||||
}
|
||||
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
check_image_size(&file, original_width, original_height);
|
||||
impl SendImageCheckMediaquality<'_> {
|
||||
pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
|
||||
let viewtype = self.viewtype;
|
||||
let media_quality_config = self.media_quality_config;
|
||||
let bytes = self.bytes;
|
||||
let extension = self.extension;
|
||||
let has_exif = self.has_exif;
|
||||
let original_width = self.original_width;
|
||||
let original_height = self.original_height;
|
||||
let orientation = self.orientation;
|
||||
let compressed_width = self.compressed_width;
|
||||
let compressed_height = self.compressed_height;
|
||||
let set_draft = self.set_draft;
|
||||
|
||||
let blob = BlobObject::new_from_path(&alice, &file).await?;
|
||||
let (_, exif) = blob.metadata()?;
|
||||
if has_exif {
|
||||
let exif = exif.unwrap();
|
||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||
} else {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, Some(media_quality_config))
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension(extension);
|
||||
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
check_image_size(&file, original_width, original_height);
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
||||
if has_exif {
|
||||
let exif = exif.unwrap();
|
||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||
} else {
|
||||
assert!(exif.is_none());
|
||||
}
|
||||
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
if set_draft {
|
||||
chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
||||
msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
}
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = alice
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||
alice_msg.save_file(&alice, &file_saved).await?;
|
||||
check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
if viewtype == Viewtype::File {
|
||||
assert_eq!(file_saved.extension().unwrap(), extension);
|
||||
let bytes1 = fs::read(&file_saved).await?;
|
||||
assert_eq!(&bytes1, bytes);
|
||||
}
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = alice
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||
alice_msg.save_file(&alice, &file_saved).await?;
|
||||
check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
|
||||
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
|
||||
let (_, exif) = blob.metadata()?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1359,8 +1451,7 @@ mod tests {
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
|
||||
let (file_size, _) = blob.metadata()?;
|
||||
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert_eq!(file_size, bytes.len() as u64);
|
||||
check_image_size(file_saved, width, height);
|
||||
Ok(())
|
||||
|
||||
651
src/chat.rs
651
src/chat.rs
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user