mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 15:42:10 +03:00
Compare commits
1129 Commits
v1.125.0
...
link2xt/py
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9dbf05d8d | ||
|
|
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 | ||
|
|
4a738ebd19 | ||
|
|
d02eccd303 | ||
|
|
f1fa053f9f | ||
|
|
38c1caf180 | ||
|
|
97d2812644 | ||
|
|
2ab713d968 | ||
|
|
b7a25d5092 | ||
|
|
8cd85fa7a4 | ||
|
|
7cfab9a931 | ||
|
|
30086038e6 | ||
|
|
eec1062619 | ||
|
|
07ceabdf85 | ||
|
|
c349bf8e0c | ||
|
|
21eb4f6648 | ||
|
|
10fed7d7de | ||
|
|
b08a283fe5 | ||
|
|
45a2805100 | ||
|
|
cc8157ecf1 | ||
|
|
0c98aca5f0 | ||
|
|
170e4b3530 | ||
|
|
5ed91e9f6e | ||
|
|
2779737c56 | ||
|
|
0d3c0a3d8f | ||
|
|
8e38e7220b | ||
|
|
acfde3cb7b | ||
|
|
b6a461e3b7 | ||
|
|
0541ecf22c | ||
|
|
77af0a2114 | ||
|
|
2f679bc21a | ||
|
|
518db9a20f | ||
|
|
edf8aafbdc | ||
|
|
ab1583eef9 | ||
|
|
e3cb9b894b | ||
|
|
c375c03d8e | ||
|
|
14aaab05b0 | ||
|
|
72c09feb64 | ||
|
|
8a4dff2212 | ||
|
|
022f836d35 | ||
|
|
636ab4a9e5 | ||
|
|
2bddefa1ab | ||
|
|
7d67100a3c | ||
|
|
1043916411 | ||
|
|
f4e58e90ae | ||
|
|
e4f10b32dd | ||
|
|
e9431888a6 | ||
|
|
1649073c0f | ||
|
|
b2cf18d8b3 | ||
|
|
2eceb4be29 | ||
|
|
ae7ff17ba2 | ||
|
|
026f678452 | ||
|
|
add8c0680f | ||
|
|
aee2b81c06 | ||
|
|
3624aad1b5 | ||
|
|
299d994d4b | ||
|
|
5e0f5ec390 | ||
|
|
c318ca5d1a | ||
|
|
38a2e07194 | ||
|
|
1ff6740938 | ||
|
|
402d5bed85 | ||
|
|
57bc046381 | ||
|
|
0617236eb0 | ||
|
|
8c5ffe0237 | ||
|
|
39f977c1e6 | ||
|
|
ec03614cae | ||
|
|
ea0b063c19 | ||
|
|
98d7a93909 | ||
|
|
49bf8414ed | ||
|
|
1e7dbea351 | ||
|
|
0412244646 | ||
|
|
bbd854d7bc | ||
|
|
ba2bb517f7 | ||
|
|
0ae831eca0 | ||
|
|
ab494ae786 | ||
|
|
8a58ae8a3a | ||
|
|
cf84255e99 | ||
|
|
462bd63065 | ||
|
|
6bfbf6547b | ||
|
|
13802bab42 | ||
|
|
adb2e4ea32 | ||
|
|
421a7b277d | ||
|
|
14d8139883 | ||
|
|
062905924c | ||
|
|
20d79970a2 | ||
|
|
f49588e64e | ||
|
|
496a8e3810 | ||
|
|
94dc65c1a2 | ||
|
|
4fe7fa3148 | ||
|
|
4cf923ccb9 | ||
|
|
56b86adf18 | ||
|
|
cfccee2ad4 | ||
|
|
37d92e3fa5 | ||
|
|
a1ee2b463f | ||
|
|
8df3b1bb1b | ||
|
|
22f240dd4d | ||
|
|
ae10ed5c40 | ||
|
|
aff6bf9402 | ||
|
|
43fc55e542 | ||
|
|
7ea05cb8a0 | ||
|
|
d036ad5853 | ||
|
|
e9280b8413 | ||
|
|
2108a8ba94 | ||
|
|
34f4ec02f6 | ||
|
|
72d5a387fb | ||
|
|
d17d89ea8f | ||
|
|
d2aa76c0ca | ||
|
|
406031773b | ||
|
|
242547f1e9 | ||
|
|
f43f5c6c0f | ||
|
|
910e4bfa37 | ||
|
|
ecf4e651ee | ||
|
|
7b724fa75a | ||
|
|
09776ae71c | ||
|
|
47bea5f8fb | ||
|
|
99cd6d10da | ||
|
|
fae4cb33bc | ||
|
|
7a3be74350 | ||
|
|
20a64ec357 | ||
|
|
92bf48684a | ||
|
|
17701b78d6 | ||
|
|
ff0d506c95 | ||
|
|
8ff3f08c2f | ||
|
|
7a32bcc1f4 | ||
|
|
65822e53e6 | ||
|
|
ac508a9e9c | ||
|
|
225112a8fe | ||
|
|
5d34b225b7 | ||
|
|
6ca6a439bd | ||
|
|
f9465f7512 | ||
|
|
489eae5d66 | ||
|
|
b6c6a63a39 | ||
|
|
c069190b68 | ||
|
|
94ac2b1097 | ||
|
|
6080a52024 | ||
|
|
0aea7d1e02 | ||
|
|
08cbc54c00 | ||
|
|
9731ec419e | ||
|
|
e9cfcd9d1b | ||
|
|
d39cbcdc8d | ||
|
|
fbbefe6b49 | ||
|
|
bab311730c | ||
|
|
b47cad7e68 | ||
|
|
a3b62b9743 | ||
|
|
9aa4c0e56b | ||
|
|
27d2b12e8d | ||
|
|
c1148e4117 | ||
|
|
295f7a291b | ||
|
|
2be28f1311 | ||
|
|
2e42243de8 | ||
|
|
00f2585d8c | ||
|
|
0b73f9cebd | ||
|
|
f5e8a04fd0 | ||
|
|
6721df7d57 | ||
|
|
18d98d643b | ||
|
|
62758658ed | ||
|
|
03bb751a9b | ||
|
|
3ebb1ea95f | ||
|
|
c1d251010f | ||
|
|
7e5959e495 | ||
|
|
823da56f2d | ||
|
|
5bcc44ca9b | ||
|
|
4304e3f0be | ||
|
|
e2e3abdf03 | ||
|
|
dcea188b62 | ||
|
|
5cf725a378 | ||
|
|
2bf0ea9d91 | ||
|
|
1df936aeac | ||
|
|
9ab2c6df16 | ||
|
|
cf11741a8c | ||
|
|
b6a12e3914 | ||
|
|
b753440a68 | ||
|
|
39abc8344c | ||
|
|
65c9e72bf4 | ||
|
|
ea4d954c77 | ||
|
|
43523a96a2 | ||
|
|
2e2fa9e74f | ||
|
|
e43ffb20a1 | ||
|
|
2f0f247e70 | ||
|
|
5bda4f0c26 | ||
|
|
d39c8a3a19 | ||
|
|
e465415039 | ||
|
|
5cef77b8e6 | ||
|
|
60e733c30c | ||
|
|
8b98816eb9 | ||
|
|
50165b3e35 | ||
|
|
0be8b5a5c4 | ||
|
|
451bb6e9db | ||
|
|
83196d4cb5 | ||
|
|
0003e55ad5 | ||
|
|
02014eda6c | ||
|
|
f1c6cd69e9 | ||
|
|
ace281ff6c | ||
|
|
c9edd525e0 | ||
|
|
3f35b442c3 | ||
|
|
87e9365016 | ||
|
|
9806509f4a | ||
|
|
91600a34b6 | ||
|
|
d16351d207 | ||
|
|
4caf638201 | ||
|
|
375fcbd63c | ||
|
|
6ff3a2cf7c | ||
|
|
a890fe3a9a | ||
|
|
2b8bf29fce | ||
|
|
26400a9e4e | ||
|
|
f8b9bb9083 | ||
|
|
42f9047a54 | ||
|
|
6433a3a5f3 | ||
|
|
4b6a03c904 | ||
|
|
ff3df01d98 | ||
|
|
cdc99854b2 | ||
|
|
e7072bcb75 | ||
|
|
7950bde3c6 | ||
|
|
a259669c98 | ||
|
|
603e6be9b4 | ||
|
|
a78c484467 | ||
|
|
e78f07b343 | ||
|
|
8abf10aacb | ||
|
|
2fef4acdd6 | ||
|
|
de27be3a36 | ||
|
|
c62e8539a1 | ||
|
|
22c0aef9c0 | ||
|
|
87805bc36d | ||
|
|
99c4d24eab | ||
|
|
7bf9c4a2d9 | ||
|
|
304e902fce | ||
|
|
0155d93622 | ||
|
|
ebd097bdbe | ||
|
|
a11d01f8a3 | ||
|
|
38491b694b | ||
|
|
e702c1a8ca | ||
|
|
1adea3c678 | ||
|
|
9af812a3e7 | ||
|
|
36bdf8a67e | ||
|
|
20b30fc70a | ||
|
|
e59ff6ca74 | ||
|
|
0e5db36205 | ||
|
|
7960944b14 | ||
|
|
71c2383cbe | ||
|
|
5f5b272726 | ||
|
|
b34fe8f118 | ||
|
|
810be4f6c7 | ||
|
|
1ebbe26ebb | ||
|
|
0f5d5dd2b2 | ||
|
|
473dbe01af | ||
|
|
069ed7afa6 | ||
|
|
9313ece3cd | ||
|
|
900168c68c | ||
|
|
0bd137b4e5 | ||
|
|
75da205ff6 | ||
|
|
67e5fbbfe3 | ||
|
|
570daf42ec | ||
|
|
fcbbb91cde | ||
|
|
c3a7fc4c8d | ||
|
|
4b4c57a480 | ||
|
|
b95d58208c | ||
|
|
c468eb088e | ||
|
|
de37135ed6 | ||
|
|
33777d8759 | ||
|
|
8cc348bfa4 | ||
|
|
76bbd5fd72 | ||
|
|
eaed2381e7 | ||
|
|
6198ed0ef5 | ||
|
|
9f4af679a3 | ||
|
|
e158b889c9 | ||
|
|
9f7defa8da | ||
|
|
e9d7fe0561 | ||
|
|
7d7289bd51 | ||
|
|
ebdc52247c | ||
|
|
36bb4a7a32 | ||
|
|
c0832af634 | ||
|
|
b6db0152b0 | ||
|
|
bc7fd4495b | ||
|
|
e67e86422f | ||
|
|
2030de11d9 | ||
|
|
2c5a0cac5f | ||
|
|
251917e602 | ||
|
|
273719ae7c | ||
|
|
e639b58c6f | ||
|
|
5addfa8d1d | ||
|
|
02d68332c7 | ||
|
|
97abb9a0a9 | ||
|
|
d0e0cfafef | ||
|
|
f630b5fb39 | ||
|
|
d9bab938d5 | ||
|
|
215ec14b20 | ||
|
|
ea728e9b62 | ||
|
|
2af9ff1d01 | ||
|
|
7502234686 | ||
|
|
863a386d0f | ||
|
|
e4b49dfdef | ||
|
|
612aa1431e | ||
|
|
781d3abdb9 | ||
|
|
78d01933ad | ||
|
|
1a1467f7cf | ||
|
|
8d09291d1e | ||
|
|
4ccd2b8d02 | ||
|
|
794596ec69 | ||
|
|
3a787519b3 | ||
|
|
c03e163ed2 | ||
|
|
6cee295a5d | ||
|
|
f0be7daae9 | ||
|
|
0b279ec84e | ||
|
|
e919de78a3 | ||
|
|
6ea675a12f | ||
|
|
b970ebe67a | ||
|
|
3c4c701f9b | ||
|
|
01ac9c8b90 | ||
|
|
f6de23738d | ||
|
|
ddc2704278 | ||
|
|
3d2b164c05 | ||
|
|
2094bc3135 | ||
|
|
acff8205e2 | ||
|
|
255400028a | ||
|
|
d7615b223f | ||
|
|
00fbf540c4 | ||
|
|
288eccf722 | ||
|
|
99ee769580 | ||
|
|
345759d653 | ||
|
|
db0143f01a | ||
|
|
4da0c19766 | ||
|
|
08247a5d37 | ||
|
|
ceadd8928e | ||
|
|
c3d96814ca | ||
|
|
c2953623b9 | ||
|
|
1907d1859e | ||
|
|
a1970e998f | ||
|
|
1e9baefca0 | ||
|
|
e16322d99d | ||
|
|
ecfe3898c6 | ||
|
|
5499ca52bf | ||
|
|
4e8979f7c8 | ||
|
|
417db31098 | ||
|
|
cd9f6c3d5b | ||
|
|
07870a6d69 | ||
|
|
b08a4d6fcf | ||
|
|
b3a82b416f | ||
|
|
4e5d7fb821 | ||
|
|
1d73f97ef3 | ||
|
|
f5601e7683 | ||
|
|
0000c09ad3 | ||
|
|
a83884d7e9 | ||
|
|
9e00e8627f | ||
|
|
85c9622675 | ||
|
|
30432d8fa5 | ||
|
|
8b9f19be70 | ||
|
|
39c317e211 | ||
|
|
36ab7bdf47 | ||
|
|
f8f0ca08da | ||
|
|
2a0a05d03c | ||
|
|
7bc2f0cb6b | ||
|
|
4355bd77a9 | ||
|
|
f0091696c2 | ||
|
|
d2e86c5852 | ||
|
|
d4a505b52e | ||
|
|
08a30031eb | ||
|
|
44686d6caa | ||
|
|
9862d40f89 | ||
|
|
256c8c13f1 | ||
|
|
0b3a56c3c4 | ||
|
|
89024bbf37 | ||
|
|
cf16671d8d | ||
|
|
671feb68a4 | ||
|
|
ccd5158109 | ||
|
|
0a18e32d62 | ||
|
|
e9fadc0785 | ||
|
|
cfa13f0669 | ||
|
|
89e43c6678 | ||
|
|
8a67797cb1 | ||
|
|
b29bc19ef4 | ||
|
|
e765066f05 | ||
|
|
67aa785a9e | ||
|
|
c88c26426d | ||
|
|
b4e9a9764f | ||
|
|
06e79e8926 | ||
|
|
9427f7b587 | ||
|
|
bce22edfe3 | ||
|
|
656d4ed506 | ||
|
|
5e3fcafb3a | ||
|
|
660cfd4f01 | ||
|
|
7a1270f861 | ||
|
|
b35b893351 | ||
|
|
f45f9263db | ||
|
|
8289dc92ed | ||
|
|
862107c708 | ||
|
|
778660a8c9 | ||
|
|
6e55f0c6e3 | ||
|
|
3b0e740c17 | ||
|
|
2dd87b6b5e | ||
|
|
cdcacf2f83 | ||
|
|
51aaaf2e8d | ||
|
|
e6438f9981 | ||
|
|
9135cffaa4 | ||
|
|
73492ca4bc | ||
|
|
fe3c1f69c3 | ||
|
|
31ee3feb57 | ||
|
|
f4ed63c54c | ||
|
|
8f88cdd826 | ||
|
|
9933a4268f | ||
|
|
8a54c228fd | ||
|
|
b5f2c747e0 | ||
|
|
ba35e83db2 | ||
|
|
61a2c551fc | ||
|
|
20c91ba2fa | ||
|
|
969f8b916b | ||
|
|
b7b7a7e95d | ||
|
|
455b108a6c | ||
|
|
645ca7741b | ||
|
|
36643c551d | ||
|
|
0fcdee8857 | ||
|
|
26ae686687 | ||
|
|
b94bd9a659 | ||
|
|
f15e7d43e3 | ||
|
|
05c256dd5b | ||
|
|
37295f6967 | ||
|
|
dfdbb91f0a | ||
|
|
72f93dca7a | ||
|
|
ec2cf31cfa | ||
|
|
ecd4d2afe0 | ||
|
|
ec9d104cf3 | ||
|
|
11214c7d1f | ||
|
|
fba27ff884 | ||
|
|
f8907e3c83 | ||
|
|
f1688d2b3f | ||
|
|
693045b542 | ||
|
|
14dfb9abec | ||
|
|
c8ed3ed73b | ||
|
|
bce5203eeb | ||
|
|
74c0c2cc38 | ||
|
|
4f25072352 | ||
|
|
91c3a39134 | ||
|
|
ae94b2a7b3 | ||
|
|
3b013a1017 | ||
|
|
80aab220b6 | ||
|
|
34c3e44b9d | ||
|
|
78d304443a | ||
|
|
d6c24eb9f6 | ||
|
|
f7fd1ef2bf | ||
|
|
af7bf5bd2b | ||
|
|
ea666f1098 | ||
|
|
5bb80f94c7 | ||
|
|
2f29c56a36 | ||
|
|
de86b8a96e | ||
|
|
060c9c8aa1 | ||
|
|
727428a965 | ||
|
|
df455bbcf5 | ||
|
|
946eea4c9e | ||
|
|
5cbc87369e | ||
|
|
5cdd5e0564 | ||
|
|
f493d6bb40 | ||
|
|
8e073b9c3e | ||
|
|
ea2a692d18 | ||
|
|
1b7c5be9c5 | ||
|
|
f7903df805 | ||
|
|
d2c61dc90e | ||
|
|
7b68098785 | ||
|
|
48f2ea717e | ||
|
|
cb3f03fd39 | ||
|
|
06f1fe18d6 | ||
|
|
1dbf924c6a | ||
|
|
3f6814f421 | ||
|
|
782828ac4f | ||
|
|
bd3759d55e | ||
|
|
672993e69e | ||
|
|
987bdaf237 | ||
|
|
7cf382a3b8 | ||
|
|
19dce9ddfa | ||
|
|
0afc0dd65a | ||
|
|
73d612a07d | ||
|
|
3b1529ef81 | ||
|
|
15187c0adb | ||
|
|
c5f31c3d03 | ||
|
|
2c17e78347 | ||
|
|
4ee646ce0b | ||
|
|
1f7b4a74fa | ||
|
|
4bc90701cc | ||
|
|
490deb9347 | ||
|
|
28d9484a13 | ||
|
|
e67e684ee0 | ||
|
|
6cfe3e6a97 | ||
|
|
99ac524905 | ||
|
|
2faf7fdb78 | ||
|
|
6a8ea8a083 | ||
|
|
e0e56cd831 | ||
|
|
bbc6febb72 | ||
|
|
7f7f42d721 | ||
|
|
589236c27b | ||
|
|
c16c5e0802 | ||
|
|
36cab40ac1 | ||
|
|
4186d78305 | ||
|
|
06cccb77f8 | ||
|
|
1895f4c556 | ||
|
|
849a873e61 | ||
|
|
b5c0372c99 | ||
|
|
1ba9b69849 | ||
|
|
6345a4f5b3 | ||
|
|
382fc75b1e | ||
|
|
92fc9ea971 | ||
|
|
de7ac2a240 | ||
|
|
7b0e5adaee | ||
|
|
406b59501b | ||
|
|
d5da2bed75 | ||
|
|
924d5b9377 | ||
|
|
bb47299ee4 | ||
|
|
20065d3daa | ||
|
|
ccb267beab | ||
|
|
32bcb59601 | ||
|
|
c708c44f0a | ||
|
|
9415a71f9d | ||
|
|
1fd42f2c53 | ||
|
|
1e52502ab3 | ||
|
|
a144d7e4f3 | ||
|
|
e855b79f9c | ||
|
|
2f8a8f9f50 | ||
|
|
b9a58bf625 | ||
|
|
c8075e53d2 | ||
|
|
ff54cf24a1 | ||
|
|
af0833e821 | ||
|
|
da11542322 | ||
|
|
3bcdd1770a | ||
|
|
4dc596e646 | ||
|
|
2e69210825 | ||
|
|
625887d249 | ||
|
|
b7c34b7794 | ||
|
|
941cf38a3e | ||
|
|
7f61896ec8 | ||
|
|
b14b49cbf0 | ||
|
|
6de3510a5d | ||
|
|
dea519095c | ||
|
|
3f8ca0cee9 | ||
|
|
1b998da57a | ||
|
|
772747d42d | ||
|
|
3998258afb | ||
|
|
4e86de98c4 | ||
|
|
2a497989e9 | ||
|
|
361b19e455 | ||
|
|
c036b26ae5 | ||
|
|
dcf6ffef12 | ||
|
|
865ede39fe | ||
|
|
a27e84ad89 | ||
|
|
b83bd26325 | ||
|
|
44227d7b86 | ||
|
|
6bcf022523 | ||
|
|
ccec26ffa7 | ||
|
|
83e159e42f | ||
|
|
cbabd4219e | ||
|
|
548afe3153 | ||
|
|
35c5f42b35 | ||
|
|
b9ff8b1d6c | ||
|
|
bb6a20dc11 | ||
|
|
e97955f5a0 | ||
|
|
35bd56ffea | ||
|
|
78affb766e | ||
|
|
9b1704e3b2 | ||
|
|
55cdbdc085 | ||
|
|
58620988d7 | ||
|
|
467f313091 | ||
|
|
091578573a | ||
|
|
62c1237024 | ||
|
|
8d41d02397 | ||
|
|
fce3f80654 | ||
|
|
2a0a51bea0 | ||
|
|
91d94d5920 | ||
|
|
c59f21230d | ||
|
|
828cc1fbd1 | ||
|
|
57f4958fc6 | ||
|
|
3aeb57b4df | ||
|
|
1b85614db9 | ||
|
|
57ecf49eb1 | ||
|
|
f279b0d1e5 | ||
|
|
32071297e6 | ||
|
|
1d98c38ff3 | ||
|
|
c09e0e2b65 | ||
|
|
0c8f967391 | ||
|
|
aca34379e0 | ||
|
|
1edd7045be | ||
|
|
c784c499c2 | ||
|
|
36c751bcc3 | ||
|
|
8a14a84bec | ||
|
|
b00703cec2 | ||
|
|
05e783564f | ||
|
|
330fb02486 | ||
|
|
1447ab8dac | ||
|
|
d574ee4edb | ||
|
|
814fe953a9 | ||
|
|
280f13b8cf | ||
|
|
a96b44a482 | ||
|
|
4286d248e9 | ||
|
|
116537019b | ||
|
|
8b37b8c1fd | ||
|
|
63b4339ca0 | ||
|
|
fdd239f61f | ||
|
|
5ca5d95c5e | ||
|
|
3fcad50924 | ||
|
|
8e40540d24 | ||
|
|
04d22bb84d | ||
|
|
5415f1bfa1 | ||
|
|
ff3bf4791a | ||
|
|
eebea216cb | ||
|
|
fbcd7f46b8 | ||
|
|
846278b18e | ||
|
|
2f2b1e18bf | ||
|
|
073c250fa4 | ||
|
|
1f336f89a6 | ||
|
|
a47fec7f6c | ||
|
|
084434d3b4 | ||
|
|
ebfbc11973 | ||
|
|
9cc9579b2d | ||
|
|
7beccd9dbc | ||
|
|
0e195bc7a2 | ||
|
|
f89efd5fce | ||
|
|
48d278fca9 | ||
|
|
c84effdaa1 | ||
|
|
e9601ef138 | ||
|
|
44c5cd5526 | ||
|
|
1c9662a8f2 | ||
|
|
5d08b2ce33 | ||
|
|
bb9d7d7ef3 | ||
|
|
766bb5c8aa | ||
|
|
84144659cf | ||
|
|
1394137436 | ||
|
|
998614b923 | ||
|
|
5b346397b8 | ||
|
|
1f99269002 | ||
|
|
160cbe8125 | ||
|
|
b9fa05c3bb | ||
|
|
4287a4d3ad | ||
|
|
37d2aafb26 | ||
|
|
4332170691 | ||
|
|
9a7c0f4737 | ||
|
|
9e7e172a7b | ||
|
|
71fbaf572a | ||
|
|
2ab29e5bfa | ||
|
|
85f8f910b9 | ||
|
|
b779d08d7f | ||
|
|
3b5634f14b | ||
|
|
f91ba357cf | ||
|
|
616faff96b | ||
|
|
5e6869403e | ||
|
|
7ff7d82959 | ||
|
|
9b751c1865 | ||
|
|
d1d31096e0 | ||
|
|
30f8522626 | ||
|
|
d3c221e061 | ||
|
|
8a421224f8 | ||
|
|
7dfce71ac9 | ||
|
|
35ba97f76a | ||
|
|
41921eaf3d | ||
|
|
03221ea86c | ||
|
|
b50761e4d1 | ||
|
|
40dea771cc | ||
|
|
e011f8f42f | ||
|
|
09d4b4354a | ||
|
|
ab151654fb | ||
|
|
ea9556b1b9 | ||
|
|
3dc6fd5c10 | ||
|
|
f39acbc037 | ||
|
|
005f7ff07e | ||
|
|
144ca7c171 | ||
|
|
7012b99d73 | ||
|
|
72bacd56f7 | ||
|
|
cc75038ccc | ||
|
|
f4810125e3 | ||
|
|
acf1faf151 | ||
|
|
255fbe94f7 | ||
|
|
b5d1eba28e | ||
|
|
1509978738 | ||
|
|
607b9e55a9 | ||
|
|
7c4c980409 | ||
|
|
b8ad3ec1b1 | ||
|
|
87dd33f66e | ||
|
|
7d8d13759a | ||
|
|
2b4f2a9171 | ||
|
|
8e869de350 | ||
|
|
b0ef082b2a | ||
|
|
bf8e74198d | ||
|
|
e77805471c | ||
|
|
45a8004b33 | ||
|
|
990f4dce9b | ||
|
|
0f36197c54 | ||
|
|
890a2bcc15 | ||
|
|
224355e83a | ||
|
|
c6ea4e389a | ||
|
|
678142b3fb | ||
|
|
ae6f83cd21 | ||
|
|
626b2be1fe | ||
|
|
ac5c789c75 | ||
|
|
ce2878f1e8 | ||
|
|
d4162899b4 | ||
|
|
e900d50e38 | ||
|
|
a438a4746a | ||
|
|
cfb819506f | ||
|
|
b86b915f40 | ||
|
|
ad5a5ad3db | ||
|
|
74081d8a36 | ||
|
|
34a434f07c | ||
|
|
dc944d8ca7 | ||
|
|
b06a7e7197 | ||
|
|
fa61d90115 | ||
|
|
7977c9ab44 | ||
|
|
6273a7d54e | ||
|
|
4d1a9c2aa1 | ||
|
|
b26ded423b | ||
|
|
e4b6eba5d7 | ||
|
|
bc225024a1 | ||
|
|
e616ecf160 | ||
|
|
f93562c6bf | ||
|
|
ac39c3699b | ||
|
|
091bc1ab13 | ||
|
|
fcbb66a788 | ||
|
|
ab2bc3bfb2 | ||
|
|
42dd6f9d08 | ||
|
|
465bcd46f8 | ||
|
|
cc88a6cb58 | ||
|
|
ba8f1bfcfd | ||
|
|
d4d6ced957 | ||
|
|
0b664e75cb | ||
|
|
1a4c2953f7 | ||
|
|
765c95de39 | ||
|
|
b2ea8f54df | ||
|
|
d7aecabcaa | ||
|
|
9ca049051c | ||
|
|
790509676f | ||
|
|
ce016eb567 | ||
|
|
57e34abe98 | ||
|
|
fd92b7c455 | ||
|
|
0ee68d1dfc | ||
|
|
1856c622a1 | ||
|
|
0a48a2effa | ||
|
|
543864f0f5 | ||
|
|
0fe94e47cc | ||
|
|
fc09210aea | ||
|
|
3e194969c0 | ||
|
|
391cffb454 | ||
|
|
47486f8bab | ||
|
|
620e363ce6 | ||
|
|
0c2276775d | ||
|
|
ad51a7cd85 | ||
|
|
28952789a4 | ||
|
|
14adcdb517 | ||
|
|
cc80590488 | ||
|
|
48416289ac | ||
|
|
003a27f625 | ||
|
|
bff4a2259f | ||
|
|
9adf856705 | ||
|
|
2215de5285 | ||
|
|
013467d6c6 | ||
|
|
1f52b8af2f | ||
|
|
ce32f76265 | ||
|
|
836f65376c | ||
|
|
99940dd28c | ||
|
|
ffeb801b58 | ||
|
|
339bbcf070 | ||
|
|
7b83bddc2d | ||
|
|
5549733a0b | ||
|
|
4e21917c0e | ||
|
|
939b4b2aab | ||
|
|
fd0770859d | ||
|
|
d5854fb3c9 | ||
|
|
3aa22a27cc | ||
|
|
10b1a2f5f5 | ||
|
|
a28a34773c | ||
|
|
7c744d14d7 | ||
|
|
6c68f2eb7e | ||
|
|
eb2d2b7313 | ||
|
|
2e50abedaa | ||
|
|
ee53136ed2 | ||
|
|
e923983dca | ||
|
|
f4753862f1 | ||
|
|
16b40f3a19 | ||
|
|
9cd3a7550b | ||
|
|
d840a7e6b9 | ||
|
|
d875691955 | ||
|
|
c600bfa8ca | ||
|
|
973ffa1a64 | ||
|
|
caffc3d93c | ||
|
|
a4dcf656f3 | ||
|
|
7bb5d48966 | ||
|
|
71b7b0b393 | ||
|
|
bd02eea66b | ||
|
|
cdcb10fb58 | ||
|
|
6cd7296001 | ||
|
|
168021523f | ||
|
|
03f2635296 | ||
|
|
e3b08fa92b | ||
|
|
79cebe66de | ||
|
|
0619e2a129 | ||
|
|
64a81e4f61 | ||
|
|
0431ae53ca | ||
|
|
89c873acd0 | ||
|
|
2e70cf9388 | ||
|
|
2efd0461d1 | ||
|
|
196a34684d | ||
|
|
402fd6850c | ||
|
|
72515f440d | ||
|
|
045d919cdc | ||
|
|
9b9108320e | ||
|
|
5efb100f12 | ||
|
|
b747dd6ae8 | ||
|
|
ed2bc9e44d | ||
|
|
9e1a2149fa | ||
|
|
22b6d8c17b | ||
|
|
3876846410 | ||
|
|
a93c79e001 | ||
|
|
6aae0276da | ||
|
|
1d80659bc3 | ||
|
|
94d5e86d4f | ||
|
|
aecf7729d8 | ||
|
|
f130d537b7 | ||
|
|
f30f862e7e | ||
|
|
81e1164358 | ||
|
|
542bd4cbb8 | ||
|
|
771b57778e | ||
|
|
9be56a5e56 | ||
|
|
da744958c2 | ||
|
|
f6bda1e480 | ||
|
|
d2e24534c7 | ||
|
|
df6f974eca | ||
|
|
2f5c6b5e16 | ||
|
|
97176b13f1 | ||
|
|
18bb7e58be | ||
|
|
c2bab44bdd | ||
|
|
53bb8a9831 | ||
|
|
1b66120e7d | ||
|
|
1478f321ae | ||
|
|
5fb92c78ad | ||
|
|
a0a792b821 | ||
|
|
3feb0e648d | ||
|
|
fa5358a5bf | ||
|
|
7399a398a7 | ||
|
|
25a78aceb9 | ||
|
|
66708454dd | ||
|
|
bb5e3d11d8 | ||
|
|
ff54db2e5f | ||
|
|
434d8fc35f | ||
|
|
12eb813bc3 | ||
|
|
88d5576150 | ||
|
|
af35e4adeb | ||
|
|
eaeacb8848 | ||
|
|
f00e68e142 | ||
|
|
113356a24e | ||
|
|
b89c134e7f | ||
|
|
3748794048 | ||
|
|
ccca12176e | ||
|
|
c89dd331f7 | ||
|
|
fa81ed5f39 | ||
|
|
6c34f6b8d9 | ||
|
|
4f21a5691d | ||
|
|
b2a839971b | ||
|
|
8ad99be322 | ||
|
|
4e771e8727 | ||
|
|
e725bdfb2b | ||
|
|
ac557f73b3 | ||
|
|
0ba3501a46 | ||
|
|
b1fe12881e | ||
|
|
c1eb33c0da | ||
|
|
03bb92c942 | ||
|
|
b0da5a54cc | ||
|
|
44e056e210 | ||
|
|
48a8680ba4 | ||
|
|
b73bcc2c22 | ||
|
|
cff42936aa | ||
|
|
d3b04004b4 | ||
|
|
5cd92f10ef | ||
|
|
22a3ab983b | ||
|
|
83d2e6b8b4 | ||
|
|
4e979c5880 | ||
|
|
71e1089139 | ||
|
|
349c154a99 | ||
|
|
54410dbe49 | ||
|
|
4e08bb7b05 | ||
|
|
934ca6a7d7 | ||
|
|
e878caebe3 | ||
|
|
088eda2983 | ||
|
|
418cd24979 | ||
|
|
b4fe9e3eec | ||
|
|
c13bbd05cd | ||
|
|
58330fe8b2 | ||
|
|
680d024b05 | ||
|
|
defcd5764b | ||
|
|
e87f785a0a | ||
|
|
0227bbc305 | ||
|
|
d05afec289 | ||
|
|
64035d3ecb | ||
|
|
21e0bb28ad | ||
|
|
c6358169ad | ||
|
|
955f4fbb19 | ||
|
|
df7c44ae42 | ||
|
|
8573649bf7 | ||
|
|
52c46c6dca | ||
|
|
54ea3ec5d6 | ||
|
|
1632035784 | ||
|
|
b239535964 | ||
|
|
0751cc50b9 | ||
|
|
da5d844ec4 | ||
|
|
2775fd1fcf | ||
|
|
37383c10ac | ||
|
|
a119b24eeb | ||
|
|
275791595c | ||
|
|
f3fb0dc5fe | ||
|
|
24ea90bd68 | ||
|
|
ef3e94c7e1 | ||
|
|
883832f78d | ||
|
|
1f03267273 | ||
|
|
8bc2ce1c30 | ||
|
|
2ae92c06e3 | ||
|
|
4a6a214f3c | ||
|
|
c2d7011aa7 | ||
|
|
c0195ab23f | ||
|
|
e4e50d0e81 | ||
|
|
573746ce54 | ||
|
|
6b2df13cdb | ||
|
|
3166b44580 | ||
|
|
e500485c21 | ||
|
|
59e5a63d5f | ||
|
|
5daa6274e8 | ||
|
|
d109a1b27f | ||
|
|
c41687586c | ||
|
|
59a3bc0ff4 | ||
|
|
a42a6ca18c | ||
|
|
061d091c97 | ||
|
|
e7617f0abd | ||
|
|
790e867af0 | ||
|
|
4a0585404a | ||
|
|
dcbf5996c2 | ||
|
|
f7a47e60cd | ||
|
|
2587ebbacd | ||
|
|
4815e9b990 | ||
|
|
f05b0ddf04 | ||
|
|
f94d34c94b | ||
|
|
e9811fb6da | ||
|
|
178fc1736d | ||
|
|
54d632adaf | ||
|
|
ae939e79da | ||
|
|
e12ef805a9 | ||
|
|
b36acb2dc0 | ||
|
|
1b883ae3fa | ||
|
|
72c94e1037 | ||
|
|
0a4c993bb8 | ||
|
|
ec56134583 | ||
|
|
d8bf1c1691 | ||
|
|
8ac1754e18 | ||
|
|
e1f1143919 | ||
|
|
d9e38289c4 | ||
|
|
486050d0b8 | ||
|
|
828c90ac3d | ||
|
|
ab09ecce7e | ||
|
|
aebad2eb10 | ||
|
|
2a39a85d9e | ||
|
|
cb0270baa7 | ||
|
|
99302c9598 | ||
|
|
88ae653760 | ||
|
|
6881f9d70f | ||
|
|
60ddbe5729 | ||
|
|
5e5e557c8b | ||
|
|
bc8023644c | ||
|
|
83ef25e7de | ||
|
|
9a7d1faf75 | ||
|
|
e59c4ee858 | ||
|
|
a520f0268f | ||
|
|
5e3b1fa540 | ||
|
|
95f29f7b63 | ||
|
|
20513475ef | ||
|
|
3b806320ec | ||
|
|
c857d6e1bd | ||
|
|
94cd9a713f | ||
|
|
7676473ebd | ||
|
|
8c778b3f5c | ||
|
|
a66f8bd9fc | ||
|
|
95b2a15930 | ||
|
|
0179ec2da9 | ||
|
|
8f2313bb2a | ||
|
|
488a3d1118 | ||
|
|
9094df7bc7 | ||
|
|
16aad3fa67 | ||
|
|
3b47c3f21d | ||
|
|
987ce58926 | ||
|
|
20c88743df | ||
|
|
03395b95cb | ||
|
|
53f04a134a | ||
|
|
885f26ea8c | ||
|
|
3ab181fdf8 | ||
|
|
e12044e6af | ||
|
|
954067eb6d | ||
|
|
aecbebd566 | ||
|
|
3111bcde5e | ||
|
|
e6cffd537e | ||
|
|
70000d9ebb | ||
|
|
d95843b0bf | ||
|
|
13e766bc37 | ||
|
|
c34edc582e | ||
|
|
8eee389c09 | ||
|
|
8ed6d4d709 | ||
|
|
60bacbec47 | ||
|
|
af013559de | ||
|
|
b784415c57 | ||
|
|
85739ba6ad | ||
|
|
a02a593f47 | ||
|
|
67f28f501a | ||
|
|
9b9703a48e | ||
|
|
c55a3d3873 | ||
|
|
f27d304f3b | ||
|
|
6d51d19f01 | ||
|
|
170968dfc2 | ||
|
|
f930576fd1 | ||
|
|
d797de7a8d | ||
|
|
acc7bb00c5 | ||
|
|
8fb8a877be | ||
|
|
b96028cd87 | ||
|
|
682e241edb | ||
|
|
3a63628f1f | ||
|
|
3705616cd9 | ||
|
|
b8fcb660ad | ||
|
|
5673294623 | ||
|
|
7062bb0502 | ||
|
|
659cffe0cc | ||
|
|
a1663a98e0 | ||
|
|
3de1dbc9e4 | ||
|
|
6d37e8601e | ||
|
|
d762753103 | ||
|
|
a020d5ccce | ||
|
|
1e28ea9bb0 | ||
|
|
17f2d33731 | ||
|
|
976797d4cf | ||
|
|
31e3169433 | ||
|
|
d2b15cb629 | ||
|
|
9cd000c4f2 | ||
|
|
243c035b03 |
138
.github/workflows/ci.yml
vendored
138
.github/workflows/ci.yml
vendored
@@ -14,8 +14,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
@@ -25,9 +24,11 @@ jobs:
|
|||||||
name: Lint Rust
|
name: Lint Rust
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
RUSTUP_TOOLCHAIN: 1.73.0
|
RUSTUP_TOOLCHAIN: 1.78.0
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Install rustfmt and clippy
|
- name: Install rustfmt and clippy
|
||||||
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
|
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
@@ -39,15 +40,25 @@ jobs:
|
|||||||
- name: Check
|
- name: Check
|
||||||
run: cargo check --workspace --all-targets --all-features
|
run: cargo check --workspace --all-targets --all-features
|
||||||
|
|
||||||
# Check with musl libc target which is used for `deltachat-rpc-server` releases.
|
npm_constants:
|
||||||
- name: Check musl
|
name: Check if node constants are up to date
|
||||||
run: scripts/zig-musl-check.sh
|
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:
|
cargo_deny:
|
||||||
name: cargo deny
|
name: cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||||
with:
|
with:
|
||||||
arguments: --all-features --workspace
|
arguments: --all-features --workspace
|
||||||
@@ -58,7 +69,9 @@ jobs:
|
|||||||
name: Check provider database
|
name: Check provider database
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Check provider database
|
- name: Check provider database
|
||||||
run: scripts/update-provider-database.sh
|
run: scripts/update-provider-database.sh
|
||||||
|
|
||||||
@@ -68,8 +81,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: -Dwarnings
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
- name: Rustdoc
|
- name: Rustdoc
|
||||||
@@ -81,22 +95,20 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: 1.73.0
|
rust: 1.78.0
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
rust: 1.73.0
|
rust: 1.78.0
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
rust: 1.73.0
|
rust: 1.78.0
|
||||||
|
|
||||||
# Minimum Supported Rust Version = 1.65.0
|
# Minimum Supported Rust Version = 1.77.0
|
||||||
#
|
|
||||||
# Minimum Supported Python Version = 3.7
|
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
|
||||||
# built.
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: 1.65.0
|
rust: 1.77.0
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
- name: Install Rust ${{ matrix.rust }}
|
- name: Install Rust ${{ matrix.rust }}
|
||||||
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
||||||
@@ -105,8 +117,20 @@ jobs:
|
|||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Install nextest
|
||||||
|
uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: nextest
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: cargo test --workspace
|
env:
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
run: cargo nextest run --workspace
|
||||||
|
|
||||||
|
- name: Doc-Tests
|
||||||
|
env:
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
run: cargo test --workspace --doc
|
||||||
|
|
||||||
- name: Test cargo vendor
|
- name: Test cargo vendor
|
||||||
run: cargo vendor
|
run: cargo vendor
|
||||||
@@ -118,7 +142,9 @@ jobs:
|
|||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
@@ -127,7 +153,7 @@ jobs:
|
|||||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||||
|
|
||||||
- name: Upload C library
|
- name: Upload C library
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug/libdeltachat.a
|
path: target/debug/libdeltachat.a
|
||||||
@@ -137,10 +163,12 @@ jobs:
|
|||||||
name: Build deltachat-rpc-server
|
name: Build deltachat-rpc-server
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
@@ -149,18 +177,19 @@ jobs:
|
|||||||
run: cargo build -p deltachat-rpc-server
|
run: cargo build -p deltachat-rpc-server
|
||||||
|
|
||||||
- name: Upload deltachat-rpc-server
|
- name: Upload deltachat-rpc-server
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: target/debug/deltachat-rpc-server
|
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
python_lint:
|
python_lint:
|
||||||
name: Python lint
|
name: Python lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: pip install tox
|
run: pip install tox
|
||||||
@@ -173,18 +202,18 @@ jobs:
|
|||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: tox -e lint
|
run: tox -e lint
|
||||||
|
|
||||||
python_tests:
|
cffi_python_tests:
|
||||||
name: Python tests
|
name: CFFI Python tests
|
||||||
needs: ["c_library", "python_lint"]
|
needs: ["c_library", "python_lint"]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Currently used Rust version.
|
# Currently used Python version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.12
|
python: 3.13
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.12
|
python: 3.13
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -200,41 +229,46 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
- name: Download libdeltachat.a
|
- name: Download libdeltachat.a
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug
|
path: target/debug
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
allow-prereleases: true
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: pip install tox
|
run: pip install tox
|
||||||
|
|
||||||
- name: Run python tests
|
- name: Run python tests
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
DCC_RS_TARGET: debug
|
DCC_RS_TARGET: debug
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
DCC_RS_DEV: ${{ github.workspace }}
|
||||||
working-directory: python
|
working-directory: python
|
||||||
run: tox -e mypy,doc,py
|
run: tox -e mypy,doc,py
|
||||||
|
|
||||||
aysnc_python_tests:
|
rpc_python_tests:
|
||||||
name: Async Python tests
|
name: JSON-RPC Python tests
|
||||||
needs: ["python_lint", "rpc_server"]
|
needs: ["python_lint", "rpc_server"]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.12
|
python: 3.13
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.12
|
python: 3.13
|
||||||
|
- os: windows-latest
|
||||||
|
python: 3.13
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -248,30 +282,40 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
allow-prereleases: true
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: pip install tox
|
run: pip install tox
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server
|
- name: Download deltachat-rpc-server
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: target/debug
|
path: target/debug
|
||||||
|
|
||||||
- name: Make deltachat-rpc-server executable
|
- name: Make deltachat-rpc-server executable
|
||||||
|
if: ${{ matrix.os != 'windows-latest' }}
|
||||||
run: chmod +x target/debug/deltachat-rpc-server
|
run: chmod +x target/debug/deltachat-rpc-server
|
||||||
|
|
||||||
- name: Add deltachat-rpc-server to path
|
- name: Add deltachat-rpc-server to path
|
||||||
|
if: ${{ matrix.os != 'windows-latest' }}
|
||||||
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
|
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Add deltachat-rpc-server to path
|
||||||
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
|
run: |
|
||||||
|
"${{ github.workspace }}/target/debug" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
- name: Run deltachat-rpc-client tests
|
- name: Run deltachat-rpc-client tests
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: tox -e py
|
run: tox -e py
|
||||||
|
|||||||
420
.github/workflows/deltachat-rpc-server.yml
vendored
420
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -21,118 +21,386 @@ jobs:
|
|||||||
# Build a version statically linked against musl libc
|
# Build a version statically linked against musl libc
|
||||||
# to avoid problems with glibc version incompatibility.
|
# to avoid problems with glibc version incompatibility.
|
||||||
build_linux:
|
build_linux:
|
||||||
name: Cross-compile deltachat-rpc-server for x86_64, i686, aarch64 and armv7 Linux
|
name: Linux
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: sh scripts/zig-rpc-server.sh
|
|
||||||
|
|
||||||
- name: Upload x86_64 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-x86_64
|
|
||||||
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload i686 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-i686
|
|
||||||
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload aarch64 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-aarch64
|
|
||||||
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload armv7 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv7
|
|
||||||
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build_windows:
|
|
||||||
name: Build deltachat-rpc-server for Windows
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||||
- os: windows-latest
|
runs-on: ubuntu-latest
|
||||||
artifact: win32.exe
|
|
||||||
path: deltachat-rpc-server.exe
|
|
||||||
target: i686-pc-windows-msvc
|
|
||||||
|
|
||||||
- os: windows-latest
|
|
||||||
artifact: win64.exe
|
|
||||||
path: deltachat-rpc-server.exe
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Setup rust target
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: rustup target add ${{ matrix.target }}
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.target }} --features vendored
|
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.artifact }}
|
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
path: target/${{ matrix.target}}/release/${{ matrix.path }}
|
path: result/bin/deltachat-rpc-server
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_windows:
|
||||||
|
name: Windows
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [win32, win64]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server binaries
|
||||||
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||||
|
path: result/bin/deltachat-rpc-server.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_macos:
|
build_macos:
|
||||||
name: Build deltachat-rpc-server for macOS
|
name: macOS
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [x86_64, aarch64]
|
||||||
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
- name: Setup rust target
|
- name: Setup rust target
|
||||||
run: rustup target add x86_64-apple-darwin
|
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release --package deltachat-rpc-server --target x86_64-apple-darwin --features vendored
|
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||||
path: target/x86_64-apple-darwin/release/deltachat-rpc-server
|
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_android:
|
||||||
|
name: Android
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server binaries
|
||||||
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
|
path: result/bin/deltachat-rpc-server
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Upload binaries to the release
|
name: Build wheels and upload binaries to the release
|
||||||
needs: ["build_linux", "build_windows", "build_macos"]
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
url: https://pypi.org/p/deltachat-rpc-server
|
||||||
permissions:
|
permissions:
|
||||||
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- name: Download built binaries
|
- uses: actions/checkout@v4
|
||||||
uses: "actions/download-artifact@v3"
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Compose dist/ directory
|
- name: Download Linux aarch64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux armv7l binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux armv6l binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux i686 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-i686-linux
|
||||||
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux x86_64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
|
- name: Download Win32 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win32
|
||||||
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
|
- name: Download Win64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win64
|
||||||
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for x86_64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for aarch64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
|
- name: Download Android binary for arm64-v8a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
|
- name: Download Android binary for armeabi-v7a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
|
|
||||||
|
- name: Create bin/ directory
|
||||||
run: |
|
run: |
|
||||||
mkdir dist
|
mkdir -p bin
|
||||||
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe x86_64-macos; do
|
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-linux
|
||||||
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
|
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv7l-linux
|
||||||
done
|
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv6l-linux
|
||||||
|
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-i686-linux
|
||||||
|
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-linux
|
||||||
|
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win32.exe
|
||||||
|
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
|
||||||
|
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
|
||||||
|
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
|
||||||
|
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
|
||||||
|
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
|
||||||
|
|
||||||
- name: List downloaded artifacts
|
- name: List binaries
|
||||||
|
run: ls -l bin/
|
||||||
|
|
||||||
|
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||||
|
- name: Install python 3.12
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
|
||||||
|
- name: Install wheel
|
||||||
|
run: pip install wheel
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server Python wheels and source package
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
nix build .#deltachat-rpc-server-x86_64-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-armv7l-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-armv6l-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-aarch64-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-i686-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-win64-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-win32-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-source
|
||||||
|
cp result/*.tar.gz dist/
|
||||||
|
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||||
|
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
||||||
|
mv *.whl dist/
|
||||||
|
|
||||||
|
- name: List artifacts
|
||||||
run: ls -l dist/
|
run: ls -l dist/
|
||||||
|
|
||||||
- name: Upload binaries to the GitHub release
|
- name: Upload binaries to the GitHub release
|
||||||
|
if: github.event_name == 'release'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
run: |
|
run: |
|
||||||
gh release upload ${{ github.ref_name }} \
|
gh release upload ${{ github.ref_name }} \
|
||||||
--repo ${{ github.repository }} \
|
--repo ${{ github.repository }} \
|
||||||
dist/*
|
bin/* dist/*
|
||||||
|
|
||||||
|
- name: Publish deltachat-rpc-client to PyPI
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
|
publish_npm_package:
|
||||||
|
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||||
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Needed to publish the binaries to the release.
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Download Linux aarch64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux armv7l binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux armv6l binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux i686 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-i686-linux
|
||||||
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux x86_64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
|
- name: Download Win32 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win32
|
||||||
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
|
- name: Download Win64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win64
|
||||||
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for x86_64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for aarch64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
|
- name: Download Android binary for arm64-v8a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
|
- name: Download Android binary for armeabi-v7a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
|
|
||||||
|
- name: make npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||||
|
run: |
|
||||||
|
cd deltachat-rpc-server/npm-package
|
||||||
|
|
||||||
|
python --version
|
||||||
|
|
||||||
|
python scripts/pack_binary_for_platform.py aarch64-unknown-linux-musl ../../deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py armv7-unknown-linux-musleabihf ../../deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py arm-unknown-linux-musleabihf ../../deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py i686-unknown-linux-musl ../../deltachat-rpc-server-i686-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py x86_64-unknown-linux-musl ../../deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py i686-pc-windows-gnu ../../deltachat-rpc-server-win32.d/deltachat-rpc-server.exe
|
||||||
|
python scripts/pack_binary_for_platform.py x86_64-pc-windows-gnu ../../deltachat-rpc-server-win64.d/deltachat-rpc-server.exe
|
||||||
|
python scripts/pack_binary_for_platform.py x86_64-apple-darwin ../../deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py aarch64-apple-darwin ../../deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py aarch64-linux-android ../../deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py armv7-linux-androideabi ../../deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server
|
||||||
|
|
||||||
|
ls -lah platform_package
|
||||||
|
|
||||||
|
for platform in ./platform_package/*; do npm pack "$platform"; done
|
||||||
|
npm pack
|
||||||
|
ls -lah
|
||||||
|
|
||||||
|
- name: Upload to artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-npm-package
|
||||||
|
path: deltachat-rpc-server/npm-package/*.tgz
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload npm packets to the GitHub release
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
run: |
|
||||||
|
gh release upload ${{ github.ref_name }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
deltachat-rpc-server/npm-package/*.tgz
|
||||||
|
|
||||||
|
# Configure Node.js for publishing.
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
working-directory: deltachat-rpc-server/npm-package
|
||||||
|
run: |
|
||||||
|
ls -lah platform_package
|
||||||
|
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
87
.github/workflows/jsonrpc-client-npm-package.yml
vendored
87
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -1,81 +1,38 @@
|
|||||||
name: "jsonrpc js client build"
|
name: "Publish @deltachat/jsonrpc-client"
|
||||||
on:
|
on:
|
||||||
pull_request:
|
workflow_dispatch:
|
||||||
push:
|
release:
|
||||||
tags:
|
types: [published]
|
||||||
- "*"
|
|
||||||
- "!py-*"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pack-module:
|
pack-module:
|
||||||
name: "Package @deltachat/jsonrpc-client and upload to download.delta.chat"
|
name: "Publish @deltachat/jsonrpc-client"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Install tree
|
- uses: actions/checkout@v4
|
||||||
run: sudo apt install tree
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
- name: Get tag
|
|
||||||
id: tag
|
- uses: actions/setup-node@v4
|
||||||
uses: dawidd6/action-get-tag@v1
|
with:
|
||||||
continue-on-error: true
|
node-version: 20
|
||||||
- name: Get Pull Request ID
|
registry-url: "https://registry.npmjs.org"
|
||||||
id: prepare
|
|
||||||
run: |
|
|
||||||
tag=${{ steps.tag.outputs.tag }}
|
|
||||||
if [ -z "$tag" ]; then
|
|
||||||
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
|
|
||||||
echo "No preview will be uploaded this time, but the $tag release"
|
|
||||||
fi
|
|
||||||
- name: System info
|
|
||||||
run: |
|
|
||||||
npm --version
|
|
||||||
node --version
|
|
||||||
echo $DELTACHAT_JSONRPC_TAR_GZ
|
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm install --ignore-scripts
|
run: npm install --ignore-scripts
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
shell: bash
|
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: |
|
run: |
|
||||||
npm run build
|
npm run build
|
||||||
npm pack .
|
npm pack .
|
||||||
ls -lah
|
|
||||||
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
- name: Publish
|
||||||
- name: Upload Prebuild
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
uses: actions/upload-artifact@v3
|
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||||
with:
|
|
||||||
name: deltachat-jsonrpc-client.tgz
|
|
||||||
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
|
||||||
# Upload to download.delta.chat/node/preview/
|
|
||||||
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
|
|
||||||
if: ${{ ! steps.tag.outputs.tag }}
|
|
||||||
id: upload-preview
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
|
||||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
|
||||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Post links to details
|
|
||||||
if: steps.upload-preview.outcome == 'success'
|
|
||||||
run: node ./node/scripts/postLinksToDetails.js
|
|
||||||
env:
|
env:
|
||||||
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
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/"
|
|
||||||
|
|||||||
16
.github/workflows/jsonrpc.yml
vendored
16
.github/workflows/jsonrpc.yml
vendored
@@ -2,9 +2,9 @@ name: JSON-RPC API Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [main]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -14,11 +14,13 @@ jobs:
|
|||||||
build_and_test:
|
build_and_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
show-progress: false
|
||||||
|
- name: Use Node.js 18.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
- name: Add Rust cache
|
- name: Add Rust cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: npm install
|
- name: npm install
|
||||||
@@ -31,7 +33,7 @@ jobs:
|
|||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm run test
|
run: npm run test
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
- name: make sure websocket server version still builds
|
- name: make sure websocket server version still builds
|
||||||
working-directory: deltachat-jsonrpc
|
working-directory: deltachat-jsonrpc
|
||||||
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||||
|
|||||||
14
.github/workflows/node-docs.yml
vendored
14
.github/workflows/node-docs.yml
vendored
@@ -8,18 +8,20 @@ name: Generate & upload node.js documentation
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate:
|
generate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Use Node.js 18.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
- name: npm install and generate documentation
|
- name: npm install and generate documentation
|
||||||
working-directory: node
|
working-directory: node
|
||||||
|
|||||||
61
.github/workflows/node-package.yml
vendored
61
.github/workflows/node-package.yml
vendored
@@ -14,11 +14,12 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, windows-latest]
|
os: [macos-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
- name: System info
|
- name: System info
|
||||||
run: |
|
run: |
|
||||||
rustc -vV
|
rustc -vV
|
||||||
@@ -28,7 +29,7 @@ jobs:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APPDATA }}/npm-cache
|
${{ env.APPDATA }}/npm-cache
|
||||||
@@ -36,7 +37,7 @@ jobs:
|
|||||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry/
|
~/.cargo/registry/
|
||||||
@@ -56,7 +57,7 @@ jobs:
|
|||||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||||
|
|
||||||
- name: Upload Prebuild
|
- name: Upload Prebuild
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}
|
name: ${{ matrix.os }}
|
||||||
path: node/${{ matrix.os }}.tar.gz
|
path: node/${{ matrix.os }}.tar.gz
|
||||||
@@ -66,21 +67,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
||||||
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
|
# Debian 10 contained glibc 2.28: https://packages.debian.org/buster/libc6
|
||||||
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
|
container: debian:10
|
||||||
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
|
|
||||||
container: ubuntu:18.04
|
|
||||||
steps:
|
steps:
|
||||||
# Working directory is owned by 1001:1001 by default.
|
# Working directory is owned by 1001:1001 by default.
|
||||||
# Change it to our user.
|
# Change it to our user.
|
||||||
- name: Change working directory owner
|
- name: Change working directory owner
|
||||||
run: chown root:root .
|
run: chown root:root .
|
||||||
|
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
- run: apt-get update
|
- run: apt-get update
|
||||||
|
|
||||||
# Python is needed for node-gyp
|
# Python is needed for node-gyp
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APPDATA }}/npm-cache
|
${{ env.APPDATA }}/npm-cache
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry/
|
~/.cargo/registry/
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
tar -zcvf "linux.tar.gz" -C prebuilds .
|
tar -zcvf "linux.tar.gz" -C prebuilds .
|
||||||
|
|
||||||
- name: Upload Prebuild
|
- name: Upload Prebuild
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux
|
name: linux
|
||||||
path: node/linux.tar.gz
|
path: node/linux.tar.gz
|
||||||
@@ -139,11 +139,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Install tree
|
- name: Install tree
|
||||||
run: sudo apt install tree
|
run: sudo apt install tree
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
- name: Get tag
|
- name: Get tag
|
||||||
id: tag
|
id: tag
|
||||||
uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
@@ -167,25 +168,25 @@ jobs:
|
|||||||
node --version
|
node --version
|
||||||
echo $DELTACHAT_NODE_TAR_GZ
|
echo $DELTACHAT_NODE_TAR_GZ
|
||||||
- name: Download Linux prebuild
|
- name: Download Linux prebuild
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux
|
name: linux
|
||||||
- name: Download macOS prebuild
|
- name: Download macOS prebuild
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-latest
|
name: macos-latest
|
||||||
- name: Download Windows prebuild
|
- name: Download Windows prebuild
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-latest
|
name: windows-latest
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir node/prebuilds
|
mkdir node/prebuilds
|
||||||
tar -xvzf linux/linux.tar.gz -C node/prebuilds
|
tar -xvzf linux.tar.gz -C node/prebuilds
|
||||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
tar -xvzf macos-latest.tar.gz -C node/prebuilds
|
||||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
tar -xvzf windows-latest.tar.gz -C node/prebuilds
|
||||||
tree node/prebuilds
|
tree node/prebuilds
|
||||||
rm -rf linux macos-latest windows-latest
|
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
run: |
|
run: |
|
||||||
npm install --ignore-scripts
|
npm install --ignore-scripts
|
||||||
@@ -203,7 +204,7 @@ jobs:
|
|||||||
ls -lah
|
ls -lah
|
||||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||||
- name: Upload prebuild
|
- name: Upload prebuild
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-node.tgz
|
name: deltachat-node.tgz
|
||||||
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||||
|
|||||||
17
.github/workflows/node-tests.yml
vendored
17
.github/workflows/node-tests.yml
vendored
@@ -13,7 +13,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
@@ -23,11 +23,12 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
- name: System info
|
- name: System info
|
||||||
run: |
|
run: |
|
||||||
rustc -vV
|
rustc -vV
|
||||||
@@ -37,7 +38,7 @@ jobs:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APPDATA }}/npm-cache
|
${{ env.APPDATA }}/npm-cache
|
||||||
@@ -45,7 +46,7 @@ jobs:
|
|||||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry/
|
~/.cargo/registry/
|
||||||
@@ -63,5 +64,5 @@ jobs:
|
|||||||
working-directory: node
|
working-directory: node
|
||||||
run: npm run test
|
run: npm run test
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||||
|
|||||||
47
.github/workflows/publish-deltachat-rpc-client-pypi.yml
vendored
Normal file
47
.github/workflows/publish-deltachat-rpc-client-pypi.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Publish deltachat-rpc-client to PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build distribution
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: python3 -m pip install build
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
working-directory: deltachat-rpc-client
|
||||||
|
run: python3 -m build
|
||||||
|
- name: Store the distribution packages
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: deltachat-rpc-client/dist/
|
||||||
|
|
||||||
|
publish-to-pypi:
|
||||||
|
name: Publish Python distribution to PyPI
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
url: https://pypi.org/p/deltachat-rpc-client
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all the dists
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
- name: Publish deltachat-rpc-client to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
16
.github/workflows/repl.yml
vendored
16
.github/workflows/repl.yml
vendored
@@ -10,15 +10,17 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build_repl:
|
build_repl:
|
||||||
name: Build REPL example
|
name: Build REPL example
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build -p deltachat-repl --features vendored
|
run: nix build .#deltachat-repl-win64
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: repl.exe
|
name: repl.exe
|
||||||
path: "target/debug/deltachat-repl.exe"
|
path: "result/bin/deltachat-repl.exe"
|
||||||
|
|||||||
90
.github/workflows/upload-docs.yml
vendored
90
.github/workflows/upload-docs.yml
vendored
@@ -1,25 +1,91 @@
|
|||||||
name: Build & Deploy Documentation on rs.delta.chat
|
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
- docs-gh-action
|
- build_jsonrpc_docs_ci
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-rs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Build the documentation with cargo
|
- name: Build the documentation with cargo
|
||||||
run: |
|
run: |
|
||||||
cargo doc --package deltachat --no-deps --document-private-items
|
cargo doc --package deltachat --no-deps --document-private-items
|
||||||
- name: Upload to rs.delta.chat
|
- name: Upload to rs.delta.chat
|
||||||
uses: up9cloud/action-rsync@v1.3
|
run: |
|
||||||
env:
|
mkdir -p "$HOME/.ssh"
|
||||||
USER: ${{ secrets.USERNAME }}
|
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||||
KEY: ${{ secrets.KEY }}
|
chmod 600 "$HOME/.ssh/key"
|
||||||
HOST: "delta.chat"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
|
||||||
SOURCE: "target/doc"
|
|
||||||
TARGET: "/var/www/html/rs/"
|
build-python:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- name: Build Python documentation
|
||||||
|
run: nix build .#python-docs
|
||||||
|
- name: Upload to py.delta.chat
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.ssh"
|
||||||
|
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||||
|
chmod 600 "$HOME/.ssh/key"
|
||||||
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
|
||||||
|
|
||||||
|
build-c:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- name: Build C documentation
|
||||||
|
run: nix build .#docs
|
||||||
|
- name: Upload to c.delta.chat
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.ssh"
|
||||||
|
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||||
|
chmod 600 "$HOME/.ssh/key"
|
||||||
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
||||||
|
|
||||||
|
build-ts:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./deltachat-jsonrpc/typescript
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
- name: npm install
|
||||||
|
run: npm install
|
||||||
|
- name: npm run build
|
||||||
|
run: npm run build
|
||||||
|
- name: Run docs script
|
||||||
|
run: npm run docs
|
||||||
|
- name: Upload to js.jsonrpc.delta.chat
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.ssh"
|
||||||
|
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||||
|
chmod 600 "$HOME/.ssh/key"
|
||||||
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"
|
||||||
|
|||||||
21
.github/workflows/upload-ffi-docs.yml
vendored
21
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
# GitHub Actions workflow
|
# GitHub Actions workflow
|
||||||
# to build `deltachat_fii` crate documentation
|
# to build `deltachat_ffi` crate documentation
|
||||||
# and upload it to <https://cffi.delta.chat/>
|
# and upload it to <https://cffi.delta.chat/>
|
||||||
|
|
||||||
name: Build & Deploy Documentation on cffi.delta.chat
|
name: Build & Deploy Documentation on cffi.delta.chat
|
||||||
@@ -7,23 +7,22 @@ name: Build & Deploy Documentation on cffi.delta.chat
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
- docs-gh-action
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Build the documentation with cargo
|
- name: Build the documentation with cargo
|
||||||
run: |
|
run: |
|
||||||
cargo doc --package deltachat_ffi --no-deps
|
cargo doc --package deltachat_ffi --no-deps
|
||||||
- name: Upload to cffi.delta.chat
|
- name: Upload to cffi.delta.chat
|
||||||
uses: up9cloud/action-rsync@v1.3
|
run: |
|
||||||
env:
|
mkdir -p "$HOME/.ssh"
|
||||||
USER: ${{ secrets.USERNAME }}
|
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||||
KEY: ${{ secrets.KEY }}
|
chmod 600 "$HOME/.ssh/key"
|
||||||
HOST: "delta.chat"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
|
||||||
SOURCE: "target/doc"
|
|
||||||
TARGET: "/var/www/html/cffi/"
|
|
||||||
|
|||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
# ignore vi temporaries
|
# ignore vi temporaries
|
||||||
*~
|
*~
|
||||||
@@ -32,7 +33,7 @@ deltachat-ffi/xml
|
|||||||
|
|
||||||
coverage/
|
coverage/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/launch.json
|
.vscode
|
||||||
python/accounts.txt
|
python/accounts.txt
|
||||||
python/all-testaccounts.txt
|
python/all-testaccounts.txt
|
||||||
tmp/
|
tmp/
|
||||||
@@ -43,3 +44,10 @@ node/build/
|
|||||||
node/dist/
|
node/dist/
|
||||||
node/prebuilds/
|
node/prebuilds/
|
||||||
node/.nyc_output/
|
node/.nyc_output/
|
||||||
|
|
||||||
|
# Nix symlink.
|
||||||
|
result
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.envrc
|
||||||
|
.direnv
|
||||||
|
|||||||
1460
CHANGELOG.md
1460
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -12,26 +12,22 @@ else()
|
|||||||
set(DYNAMIC_EXT "dll")
|
set(DYNAMIC_EXT "dll")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
||||||
|
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||||
|
else()
|
||||||
|
set(ARCH_DIR "./")
|
||||||
|
endif()
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT
|
OUTPUT
|
||||||
"target/release/libdeltachat.a"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||||
"target/release/pkgconfig/deltachat.pc"
|
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||||
COMMAND
|
COMMAND
|
||||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||||
${CARGO} build --release --no-default-features --features jsonrpc
|
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
|
||||||
|
|
||||||
# Build in `deltachat-ffi` directory instead of using
|
|
||||||
# `--package deltachat_ffi` to avoid feature resolver version
|
|
||||||
# "1" bug which makes `--no-default-features` affect only
|
|
||||||
# `deltachat`, but not `deltachat-ffi` package.
|
|
||||||
#
|
|
||||||
# We can't enable version "2" resolver [1] because it is not
|
|
||||||
# stable yet on rust 1.50.0.
|
|
||||||
#
|
|
||||||
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
|
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,12 +35,12 @@ add_custom_target(
|
|||||||
lib_deltachat
|
lib_deltachat
|
||||||
ALL
|
ALL
|
||||||
DEPENDS
|
DEPENDS
|
||||||
"target/release/libdeltachat.a"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||||
"target/release/pkgconfig/deltachat.pc"
|
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||||
|
|||||||
@@ -76,6 +76,40 @@ If you have multiple changes in one PR, create multiple conventional commits, an
|
|||||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||||
[git-cliff]: https://git-cliff.org/
|
[git-cliff]: https://git-cliff.org/
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||||
|
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
|
||||||
|
capitalize it but do not add a full stop as the contexts will be separated by `:`.
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
.with_context(|| format!("Unable to trash message {msg_id}"))
|
||||||
|
```
|
||||||
|
|
||||||
|
All errors should be handled in one of these ways:
|
||||||
|
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
|
||||||
|
- With `.log_err().ok()`.
|
||||||
|
- Bubbled up with `?`.
|
||||||
|
|
||||||
|
`backtrace` feature is enabled for `anyhow` crate
|
||||||
|
and `debug = 1` option is set in the test profile.
|
||||||
|
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||||
|
and get a backtrace with line numbers in resultified tests
|
||||||
|
which return `anyhow::Result`.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
For logging, use `info!`, `warn!` and `error!` macros.
|
||||||
|
Log messages should be capitalized and have a full stop in the end. For example:
|
||||||
|
```
|
||||||
|
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||||
|
```
|
||||||
|
|
||||||
|
Format anyhow errors with `{:#}` to print all the contexts like this:
|
||||||
|
```
|
||||||
|
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||||
|
```
|
||||||
|
|
||||||
### Reviewing
|
### Reviewing
|
||||||
|
|
||||||
Once a PR has an approval and passes CI, it can be merged.
|
Once a PR has an approval and passes CI, it can be merged.
|
||||||
|
|||||||
4011
Cargo.lock
generated
4011
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
103
Cargo.toml
103
Cargo.toml
@@ -1,9 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.125.0"
|
version = "1.140.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.65"
|
rust-version = "1.77"
|
||||||
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
@@ -11,6 +12,10 @@ panic = 'abort'
|
|||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
||||||
[profile.test]
|
[profile.test]
|
||||||
|
# Make anyhow `backtrace` feature useful.
|
||||||
|
# With `debug = 0` there are no line numbers in the backtrace
|
||||||
|
# produced with RUST_BACKTRACE=1.
|
||||||
|
debug = 1
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
|
|
||||||
# Always optimize dependencies.
|
# Always optimize dependencies.
|
||||||
@@ -24,87 +29,91 @@ lto = true
|
|||||||
panic = 'abort'
|
panic = 'abort'
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
[patch.crates-io]
|
|
||||||
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
|
||||||
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat_derive = { path = "./deltachat_derive" }
|
deltachat_derive = { path = "./deltachat_derive" }
|
||||||
|
deltachat-time = { path = "./deltachat-time" }
|
||||||
|
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
|
||||||
format-flowed = { path = "./format-flowed" }
|
format-flowed = { path = "./format-flowed" }
|
||||||
ratelimit = { path = "./deltachat-ratelimit" }
|
ratelimit = { path = "./deltachat-ratelimit" }
|
||||||
|
|
||||||
anyhow = "1"
|
anyhow = { workspace = true }
|
||||||
async-channel = "1.8.0"
|
async-broadcast = "0.7.0"
|
||||||
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
|
async-channel = "2.2.1"
|
||||||
|
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-smtp = { version = "0.9", 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"] }
|
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
base64 = "0.21"
|
base64 = "0.22"
|
||||||
brotli = { version = "3.3", default-features=false, features = ["std"] }
|
brotli = { version = "6", default-features=false, features = ["std"] }
|
||||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
chrono = { workspace = true }
|
||||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||||
escaper = "0.1"
|
escaper = "0.1"
|
||||||
fast-socks5 = "0.8"
|
fast-socks5 = "0.9"
|
||||||
|
fd-lock = "4"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-lite = "1.13.0"
|
futures-lite = "2.3.0"
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
|
hickory-resolver = "0.24"
|
||||||
humansize = "2"
|
humansize = "2"
|
||||||
image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||||
iroh = { version = "0.4.1", default-features = false }
|
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
|
||||||
kamadak-exif = "0.5"
|
iroh-net = "0.17.0"
|
||||||
|
iroh-gossip = { version = "0.17.0", features = ["net"] }
|
||||||
|
quinn = "0.10.0"
|
||||||
|
kamadak-exif = "0.5.3"
|
||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
mailparse = "0.14"
|
mailparse = "0.15"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
once_cell = "1.18.0"
|
once_cell = { workspace = true }
|
||||||
percent-encoding = "2.3"
|
percent-encoding = "2.3"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
pgp = { version = "0.10", default-features = false }
|
pgp = { version = "0.11", default-features = false }
|
||||||
pretty_env_logger = { version = "0.5", optional = true }
|
|
||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
quick-xml = "0.29"
|
quick-xml = "0.31"
|
||||||
|
quoted_printable = "0.5"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1.8"
|
regex = { workspace = true }
|
||||||
reqwest = { version = "0.11.18", features = ["json"] }
|
reqwest = { version = "0.11.27", features = ["json"] }
|
||||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||||
rust-hsluv = "0.1"
|
rust-hsluv = "0.1"
|
||||||
sanitize-filename = "0.4"
|
sanitize-filename = "0.5"
|
||||||
serde_json = "1.0"
|
serde_json = "1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha-1 = "0.10"
|
sha-1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
smallvec = "1"
|
smallvec = "1.13.2"
|
||||||
strum = "0.25"
|
strum = "0.26"
|
||||||
strum_macros = "0.25"
|
strum_macros = "0.26"
|
||||||
tagger = "4.3.4"
|
tagger = "4.3.4"
|
||||||
textwrap = "0.16.0"
|
textwrap = "0.16.1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tokio-io-timeout = "1.2.0"
|
tokio-io-timeout = "1.2.0"
|
||||||
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||||
tokio-util = "0.7.8"
|
tokio-util = "0.7.9"
|
||||||
toml = "0.7"
|
toml = "0.8"
|
||||||
trust-dns-resolver = "0.22"
|
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ansi_term = "0.12.0"
|
ansi_term = "0.12.0"
|
||||||
|
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||||
futures-lite = "1.13"
|
futures-lite = "2.3.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
pretty_env_logger = "0.5"
|
|
||||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
testdir = "0.8.0"
|
testdir = "0.9.0"
|
||||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
@@ -115,14 +124,11 @@ members = [
|
|||||||
"deltachat-rpc-server",
|
"deltachat-rpc-server",
|
||||||
"deltachat-ratelimit",
|
"deltachat-ratelimit",
|
||||||
"deltachat-repl",
|
"deltachat-repl",
|
||||||
|
"deltachat-time",
|
||||||
"format-flowed",
|
"format-flowed",
|
||||||
|
"deltachat-contact-tools",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "simple"
|
|
||||||
path = "examples/simple.rs"
|
|
||||||
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "create_account"
|
name = "create_account"
|
||||||
harness = false
|
harness = false
|
||||||
@@ -151,6 +157,13 @@ harness = false
|
|||||||
name = "send_events"
|
name = "send_events"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
regex = "1.10"
|
||||||
|
rusqlite = "0.31"
|
||||||
|
chrono = { version = "0.4.38", default-features=false, features = ["alloc", "clock", "std"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
internals = []
|
internals = []
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -1,8 +1,19 @@
|
|||||||
# Delta Chat Rust
|
<p align="center">
|
||||||
|
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
> Deltachat-core written in Rust
|
<p align="center">
|
||||||
|
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||||
|
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
|
||||||
|
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
<p align="center">
|
||||||
|
The core library for Delta Chat, written in Rust
|
||||||
|
</p>
|
||||||
|
|
||||||
## Installing Rust and Cargo
|
## Installing Rust and Cargo
|
||||||
|
|
||||||
@@ -19,7 +30,7 @@ $ curl https://sh.rustup.rs -sSf | sh
|
|||||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ RUST_LOG=deltachat_repl=info cargo run -p deltachat-repl -- ~/deltachat-db
|
$ cargo run -p deltachat-repl -- ~/deltachat-db
|
||||||
```
|
```
|
||||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||||
|
|
||||||
@@ -113,7 +124,7 @@ $ cargo build -p deltachat_ffi --release
|
|||||||
|
|
||||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
||||||
|
|
||||||
- `RUST_LOG=deltachat_repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
- `RUST_LOG=async_imap=trace,async_smtp=trace`: enable IMAP and
|
||||||
SMTP tracing in addition to info messages.
|
SMTP tracing in addition to info messages.
|
||||||
|
|
||||||
### Expensive tests
|
### Expensive tests
|
||||||
@@ -184,6 +195,7 @@ or its language bindings:
|
|||||||
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||||
|
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
||||||
- several **Bots**
|
- several **Bots**
|
||||||
|
|
||||||
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ 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).
|
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
|
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`.
|
||||||
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
|
|
||||||
|
|
||||||
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`.
|
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`
|
||||||
|
|
||||||
4. 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`.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 121 KiB |
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::Contact;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
@@ -8,7 +9,8 @@ async fn create_accounts(n: u32) {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let p: PathBuf = dir.path().join("accounts");
|
let p: PathBuf = dir.path().join("accounts");
|
||||||
|
|
||||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
let writable = true;
|
||||||
|
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||||
|
|
||||||
for expected_id in 2..n {
|
for expected_id in 2..n {
|
||||||
let id = accounts.add_account().await.unwrap();
|
let id = accounts.add_account().await.unwrap();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
use criterion::{criterion_group, criterion_main, Criterion};
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
|
|||||||
16
cliff.toml
16
cliff.toml
@@ -54,7 +54,7 @@ header = """
|
|||||||
# Changelog\n
|
# Changelog\n
|
||||||
"""
|
"""
|
||||||
# template for the changelog body
|
# template for the changelog body
|
||||||
# https://tera.netlify.app/docs/#introduction
|
# https://keats.github.io/tera/docs/#templates
|
||||||
body = """
|
body = """
|
||||||
{% if version %}\
|
{% if version %}\
|
||||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
@@ -77,3 +77,17 @@ body = """
|
|||||||
"""
|
"""
|
||||||
# remove the leading and trailing whitespace from the template
|
# remove the leading and trailing whitespace from the template
|
||||||
trim = true
|
trim = true
|
||||||
|
footer = """
|
||||||
|
{% for release in releases -%}
|
||||||
|
{% if release.version -%}
|
||||||
|
{% if release.previous.version -%}
|
||||||
|
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||||
|
https://github.com/deltachat/deltachat-core-rust\
|
||||||
|
/compare/{{ release.previous.version }}..{{ release.version }}
|
||||||
|
{% endif -%}
|
||||||
|
{% else -%}
|
||||||
|
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
|
||||||
|
/compare/{{ release.previous.version }}..HEAD
|
||||||
|
{% endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|||||||
18
deltachat-contact-tools/Cargo.toml
Normal file
18
deltachat-contact-tools/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "deltachat-contact-tools"
|
||||||
|
version = "0.0.0" # No semver-stable versioning
|
||||||
|
edition = "2021"
|
||||||
|
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
||||||
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
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 }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
671
deltachat-contact-tools/src/lib.rs
Normal file
671
deltachat-contact-tools/src/lib.rs
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
//! Contact-related tools, like parsing vcards and sanitizing name and address
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(
|
||||||
|
unused,
|
||||||
|
clippy::correctness,
|
||||||
|
missing_debug_implementations,
|
||||||
|
missing_docs,
|
||||||
|
clippy::all,
|
||||||
|
clippy::wildcard_imports,
|
||||||
|
clippy::needless_borrow,
|
||||||
|
clippy::cast_lossless,
|
||||||
|
clippy::unused_async,
|
||||||
|
clippy::explicit_iter_loop,
|
||||||
|
clippy::explicit_into_iter_loop,
|
||||||
|
clippy::cloned_instead_of_copied
|
||||||
|
)]
|
||||||
|
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
|
||||||
|
#![allow(
|
||||||
|
clippy::match_bool,
|
||||||
|
clippy::mixed_read_write_in_expression,
|
||||||
|
clippy::bool_assert_comparison,
|
||||||
|
clippy::manual_split_once,
|
||||||
|
clippy::format_push_string,
|
||||||
|
clippy::bool_to_int_with_if
|
||||||
|
)]
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use anyhow::Context as _;
|
||||||
|
use anyhow::Result;
|
||||||
|
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,
|
||||||
|
/// 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<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.
|
||||||
|
///
|
||||||
|
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||||
|
pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||||
|
fn format_timestamp(c: &VcardContact) -> Option<String> {
|
||||||
|
let timestamp = *c.timestamp.as_ref().ok()?;
|
||||||
|
let datetime = DateTime::from_timestamp(timestamp, 0)?;
|
||||||
|
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res = "".to_string();
|
||||||
|
for c in contacts {
|
||||||
|
let addr = &c.addr;
|
||||||
|
let display_name = c.display_name();
|
||||||
|
res += &format!(
|
||||||
|
"BEGIN:VCARD\n\
|
||||||
|
VERSION:4.0\n\
|
||||||
|
EMAIL:{addr}\n\
|
||||||
|
FN:{display_name}\n"
|
||||||
|
);
|
||||||
|
if let Some(key) = &c.key {
|
||||||
|
res += &format!("KEY:data:application/pgp-keys;base64,{key}\n");
|
||||||
|
}
|
||||||
|
if let Some(profile_image) = &c.profile_image {
|
||||||
|
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n");
|
||||||
|
}
|
||||||
|
if let Some(timestamp) = format_timestamp(c) {
|
||||||
|
res += &format!("REV:{timestamp}\n");
|
||||||
|
}
|
||||||
|
res += "END:VCARD\n";
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses `VcardContact`s from a given `&str`.
|
||||||
|
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())?;
|
||||||
|
|
||||||
|
if start_of_s.eq_ignore_ascii_case(prefix) {
|
||||||
|
s.get(prefix.len()..)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
|
||||||
|
let remainder = remove_prefix(s, property)?;
|
||||||
|
// 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
|
||||||
|
let (params, value) = remainder.split_once(':')?;
|
||||||
|
// In the example from above, `params` is now `;TYPE=work`
|
||||||
|
// and `value` is now `alice@example.com`
|
||||||
|
|
||||||
|
if params
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
// `s` started with `property`, but the next character after it was not punctuation,
|
||||||
|
// so this line's property is actually something else
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
// So, instead just parse using a format string.
|
||||||
|
|
||||||
|
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
|
||||||
|
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
|
||||||
|
Ok(datetime) => datetime.timestamp(),
|
||||||
|
// Parses 19961022T140000.
|
||||||
|
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
|
||||||
|
Ok(datetime) => datetime
|
||||||
|
.and_local_timezone(chrono::offset::Local)
|
||||||
|
.single()
|
||||||
|
.context("Could not apply local timezone to parsed date and time")?
|
||||||
|
.timestamp(),
|
||||||
|
Err(_) => return Err(e.into()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
// Skip to the start of the vcard:
|
||||||
|
for line in lines.by_ref() {
|
||||||
|
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut display_name = None;
|
||||||
|
let mut addr = None;
|
||||||
|
let mut key = None;
|
||||||
|
let mut photo = None;
|
||||||
|
let mut datetime = None;
|
||||||
|
|
||||||
|
for line in lines.by_ref() {
|
||||||
|
if let Some(email) = vcard_property(line, "email") {
|
||||||
|
addr.get_or_insert(email);
|
||||||
|
} else if let Some(name) = vcard_property(line, "fn") {
|
||||||
|
display_name.get_or_insert(name);
|
||||||
|
} 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,"))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
} else if let Some(rev) = vcard_property(line, "rev") {
|
||||||
|
datetime.get_or_insert(rev);
|
||||||
|
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (authname, addr) =
|
||||||
|
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||||
|
|
||||||
|
contacts.push(VcardContact {
|
||||||
|
authname,
|
||||||
|
addr,
|
||||||
|
key: key.map(|s| s.to_string()),
|
||||||
|
profile_image: photo.map(|s| s.to_string()),
|
||||||
|
timestamp: datetime
|
||||||
|
.context("No timestamp in vcard")
|
||||||
|
.and_then(parse_datetime),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valid contact address.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ContactAddress(String);
|
||||||
|
|
||||||
|
impl Deref for ContactAddress {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for ContactAddress {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ContactAddress {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContactAddress {
|
||||||
|
/// Constructs a new contact address from string,
|
||||||
|
/// normalizing and validating it.
|
||||||
|
pub fn new(s: &str) -> Result<Self> {
|
||||||
|
let addr = addr_normalize(s);
|
||||||
|
if !may_be_valid_addr(&addr) {
|
||||||
|
bail!("invalid address {:?}", s);
|
||||||
|
}
|
||||||
|
Ok(Self(addr.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||||
|
impl rusqlite::types::ToSql for ContactAddress {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
|
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||||
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make the name and address
|
||||||
|
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()))
|
||||||
|
} else {
|
||||||
|
strip_rtlo_characters(name)
|
||||||
|
},
|
||||||
|
captures
|
||||||
|
.get(2)
|
||||||
|
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
strip_rtlo_characters(&normalize_name(name)),
|
||||||
|
addr.to_string(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut name = normalize_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()`).
|
||||||
|
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||||
|
if name == addr {
|
||||||
|
name = "".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
(name, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalize 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
match full_name.as_bytes() {
|
||||||
|
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||||
|
.get(1..full_name.len() - 1)
|
||||||
|
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||||
|
_ => full_name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns false if addr is an invalid address, otherwise true.
|
||||||
|
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||||
|
let res = EmailAddress::new(addr);
|
||||||
|
res.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns address lowercased,
|
||||||
|
/// with whitespace trimmed and `mailto:` prefix removed.
|
||||||
|
pub fn addr_normalize(addr: &str) -> String {
|
||||||
|
let norm = addr.trim().to_lowercase();
|
||||||
|
|
||||||
|
if norm.starts_with("mailto:") {
|
||||||
|
norm.get(7..).unwrap_or(&norm).to_string()
|
||||||
|
} else {
|
||||||
|
norm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares two email addresses, normalizing them beforehand.
|
||||||
|
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||||
|
let norm1 = addr_normalize(addr1);
|
||||||
|
let norm2 = addr_normalize(addr2);
|
||||||
|
|
||||||
|
norm1 == norm2
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Represents an email address, right now just the `name@domain` portion.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use deltachat_contact_tools::EmailAddress;
|
||||||
|
/// let email = match EmailAddress::new("someone@example.com") {
|
||||||
|
/// Ok(addr) => addr,
|
||||||
|
/// Err(e) => panic!("Error parsing address, error was {}", e),
|
||||||
|
/// };
|
||||||
|
/// assert_eq!(&email.local, "someone");
|
||||||
|
/// assert_eq!(&email.domain, "example.com");
|
||||||
|
/// assert_eq!(email.to_string(), "someone@example.com");
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct EmailAddress {
|
||||||
|
/// Local part of the email address.
|
||||||
|
pub local: String,
|
||||||
|
|
||||||
|
/// Email address domain.
|
||||||
|
pub domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for EmailAddress {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}@{}", self.local, self.domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailAddress {
|
||||||
|
/// Performs a dead-simple parse of an email address.
|
||||||
|
pub fn new(input: &str) -> Result<EmailAddress> {
|
||||||
|
if input.is_empty() {
|
||||||
|
bail!("empty string is not valid");
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||||
|
|
||||||
|
if input
|
||||||
|
.chars()
|
||||||
|
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||||
|
{
|
||||||
|
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
match &parts[..] {
|
||||||
|
[domain, local] => {
|
||||||
|
if local.is_empty() {
|
||||||
|
bail!("empty string is not valid for local part in {:?}", input);
|
||||||
|
}
|
||||||
|
if domain.is_empty() {
|
||||||
|
bail!("missing domain after '@' in {:?}", input);
|
||||||
|
}
|
||||||
|
if domain.ends_with('.') {
|
||||||
|
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||||
|
}
|
||||||
|
Ok(EmailAddress {
|
||||||
|
local: (*local).to_string(),
|
||||||
|
domain: (*domain).to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => bail!("Email {:?} must contain '@' character", input),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rusqlite::types::ToSql for EmailAddress {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
|
let val = rusqlite::types::Value::Text(self.to_string());
|
||||||
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::TimeZone;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcard_thunderbird() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
"BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN:'Alice Mueller'
|
||||||
|
EMAIL;PREF=1:alice.mueller@posteo.de
|
||||||
|
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
|
||||||
|
END:VCARD
|
||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN:'bobzzz@freenet.de'
|
||||||
|
EMAIL;PREF=1:bobzzz@freenet.de
|
||||||
|
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||||
|
END:VCARD
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".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].authname, "".to_string());
|
||||||
|
assert_eq!(contacts[1].key, None);
|
||||||
|
assert_eq!(contacts[1].profile_image, None);
|
||||||
|
assert!(contacts[1].timestamp.is_err());
|
||||||
|
|
||||||
|
assert_eq!(contacts.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcard_simple_example() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
"BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN:Alice Wonderland
|
||||||
|
N:Wonderland;Alice;;;Ms.
|
||||||
|
GENDER:W
|
||||||
|
EMAIL;TYPE=work:alice@example.com
|
||||||
|
KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
||||||
|
REV:20240418T184242Z
|
||||||
|
|
||||||
|
END:VCARD",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(contacts[0].addr, "alice@example.com".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);
|
||||||
|
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_make_and_parse_vcard() {
|
||||||
|
let contacts = [
|
||||||
|
VcardContact {
|
||||||
|
addr: "alice@example.org".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(),
|
||||||
|
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);
|
||||||
|
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].authname, contacts[i].authname);
|
||||||
|
assert_eq!(parsed[i].key, contacts[i].key);
|
||||||
|
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
|
||||||
|
assert_eq!(
|
||||||
|
parsed[i].timestamp.as_ref().unwrap(),
|
||||||
|
contacts[i].timestamp.as_ref().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_contact_address() -> Result<()> {
|
||||||
|
let alice_addr = "alice@example.org";
|
||||||
|
let contact_address = ContactAddress::new(alice_addr)?;
|
||||||
|
assert_eq!(contact_address.as_ref(), alice_addr);
|
||||||
|
|
||||||
|
let invalid_addr = "<> foobar";
|
||||||
|
assert!(ContactAddress::new(invalid_addr).is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_emailaddress_parse() {
|
||||||
|
assert_eq!(EmailAddress::new("").is_ok(), false);
|
||||||
|
assert_eq!(
|
||||||
|
EmailAddress::new("user@domain.tld").unwrap(),
|
||||||
|
EmailAddress {
|
||||||
|
local: "user".into(),
|
||||||
|
domain: "domain.tld".into(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
EmailAddress::new("user@localhost").unwrap(),
|
||||||
|
EmailAddress {
|
||||||
|
local: "user".into(),
|
||||||
|
domain: "localhost".into()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
|
||||||
|
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
||||||
|
assert!(EmailAddress::new("tt.dd@uu").is_ok());
|
||||||
|
assert!(EmailAddress::new("u@d").is_ok());
|
||||||
|
assert!(EmailAddress::new("u@d.").is_err());
|
||||||
|
assert!(EmailAddress::new("u@d.t").is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
EmailAddress::new("u@d.tt").unwrap(),
|
||||||
|
EmailAddress {
|
||||||
|
local: "u".into(),
|
||||||
|
domain: "d.tt".into(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(EmailAddress::new("u@tt").is_ok());
|
||||||
|
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcard_android() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
"BEGIN:VCARD
|
||||||
|
VERSION:2.1
|
||||||
|
N:;Bob;;;
|
||||||
|
FN:Bob
|
||||||
|
TEL;CELL:+1-234-567-890
|
||||||
|
EMAIL;HOME:bob@example.org
|
||||||
|
END:VCARD
|
||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:2.1
|
||||||
|
N:;Alice;;;
|
||||||
|
FN:Alice
|
||||||
|
EMAIL;HOME:alice@example.org
|
||||||
|
END:VCARD
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
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, None);
|
||||||
|
|
||||||
|
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
||||||
|
assert_eq!(contacts[1].authname, "Alice".to_string());
|
||||||
|
assert_eq!(contacts[1].key, None);
|
||||||
|
assert_eq!(contacts[1].profile_image, None);
|
||||||
|
|
||||||
|
assert_eq!(contacts.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcard_local_datetime() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
"BEGIN:VCARD\n\
|
||||||
|
VERSION:4.0\n\
|
||||||
|
FN:Alice Wonderland\n\
|
||||||
|
EMAIL;TYPE=work:alice@example.org\n\
|
||||||
|
REV:20240418T184242\n\
|
||||||
|
END:VCARD",
|
||||||
|
);
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
assert_eq!(contacts[0].addr, "alice@example.org".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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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==");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.125.0"
|
version = "1.140.0"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -17,10 +17,10 @@ crate-type = ["cdylib", "staticlib"]
|
|||||||
deltachat = { path = "../", default-features = false }
|
deltachat = { path = "../", default-features = false }
|
||||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
human-panic = { version = "1", default-features = false }
|
human-panic = { version = "2", default-features = false }
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|||||||
@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
|
|||||||
# exclude all test directories use the pattern */test/*
|
# exclude all test directories use the pattern */test/*
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
|
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t
|
||||||
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
|
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
|
||||||
EXCLUDE_SYMBOLS += _dc_* jsmn*
|
EXCLUDE_SYMBOLS += _dc_* jsmn*
|
||||||
######################################################
|
######################################################
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<tab type="hierarchy" visible="no" title="" intro=""/>
|
<tab type="hierarchy" visible="no" title="" intro=""/>
|
||||||
<tab type="classmembers" visible="no" title="" intro=""/>
|
<tab type="classmembers" visible="no" title="" intro=""/>
|
||||||
</tab>
|
</tab>
|
||||||
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||||
<tab type="pages" visible="yes" title="" intro=""/>
|
<tab type="pages" visible="yes" title="" intro=""/>
|
||||||
<tab type="namespaces" visible="yes" title="">
|
<tab type="namespaces" visible="yes" title="">
|
||||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ typedef struct _dc_array dc_array_t;
|
|||||||
typedef struct _dc_chatlist dc_chatlist_t;
|
typedef struct _dc_chatlist dc_chatlist_t;
|
||||||
typedef struct _dc_chat dc_chat_t;
|
typedef struct _dc_chat dc_chat_t;
|
||||||
typedef struct _dc_msg dc_msg_t;
|
typedef struct _dc_msg dc_msg_t;
|
||||||
typedef struct _dc_reactions dc_reactions_t;
|
|
||||||
typedef struct _dc_contact dc_contact_t;
|
typedef struct _dc_contact dc_contact_t;
|
||||||
typedef struct _dc_lot dc_lot_t;
|
typedef struct _dc_lot dc_lot_t;
|
||||||
typedef struct _dc_provider dc_provider_t;
|
typedef struct _dc_provider dc_provider_t;
|
||||||
@@ -25,7 +24,6 @@ typedef struct _dc_event dc_event_t;
|
|||||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||||
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
||||||
typedef struct _dc_backup_provider dc_backup_provider_t;
|
typedef struct _dc_backup_provider dc_backup_provider_t;
|
||||||
typedef struct _dc_http_response dc_http_response_t;
|
|
||||||
|
|
||||||
// Alias for backwards compatibility, use dc_event_emitter_t instead.
|
// Alias for backwards compatibility, use dc_event_emitter_t instead.
|
||||||
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||||
@@ -364,8 +362,12 @@ uint32_t dc_get_id (dc_context_t* context);
|
|||||||
* Must be freed using dc_event_emitter_unref() after usage.
|
* Must be freed using dc_event_emitter_unref() after usage.
|
||||||
*
|
*
|
||||||
* Note: Use only one event emitter per context.
|
* Note: Use only one event emitter per context.
|
||||||
* Having more than one event emitter running at the same time on the same context
|
* The result of having multiple event emitters is unspecified.
|
||||||
* will result in events being randomly delivered to one of the emitters.
|
* Currently events are broadcasted to all existing event emitters,
|
||||||
|
* but previous versions delivered events to only one event emitter
|
||||||
|
* and this behavior may change again in the future.
|
||||||
|
* Events emitted before creation of event emitter
|
||||||
|
* may or may not be available to event emitter.
|
||||||
*/
|
*/
|
||||||
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
|
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
|
||||||
|
|
||||||
@@ -384,7 +386,12 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
/**
|
/**
|
||||||
* Configure the context. The configuration is handled by key=value pairs as:
|
* Configure the context. The configuration is handled by key=value pairs as:
|
||||||
*
|
*
|
||||||
* - `addr` = address to display (always needed)
|
* - `addr` = Email address to use for configuration.
|
||||||
|
* If dc_configure() fails this is not the email address actually in use.
|
||||||
|
* Use `configured_addr` to find out the email address actually in use.
|
||||||
|
* - `configured_addr` = Email address actually in use.
|
||||||
|
* Unless for testing, do not set this value using dc_set_config().
|
||||||
|
* Instead, set `addr` and call dc_configure().
|
||||||
* - `mail_server` = IMAP-server, guessed if left out
|
* - `mail_server` = IMAP-server, guessed if left out
|
||||||
* - `mail_user` = IMAP-username, guessed if left out
|
* - `mail_user` = IMAP-username, guessed if left out
|
||||||
* - `mail_pw` = IMAP-password (always needed)
|
* - `mail_pw` = IMAP-password (always needed)
|
||||||
@@ -419,19 +426,16 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* Sending messages to self is needed for a proper multi-account setup,
|
* 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.
|
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
|
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
|
||||||
* 0=do not watch the `Sent`-folder (default),
|
* 0=do not watch the `Sent`-folder (default).
|
||||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
|
||||||
* - `mvbox_move` = 1=detect chat messages,
|
* - `mvbox_move` = 1=detect chat messages,
|
||||||
* move them to the `DeltaChat` folder,
|
* move them to the `DeltaChat` folder,
|
||||||
* and watch the `DeltaChat` folder for updates (default),
|
* and watch the `DeltaChat` folder for updates (default),
|
||||||
* 0=do not move chat-messages
|
* 0=do not move chat-messages
|
||||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
|
||||||
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
|
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
|
||||||
* `DeltaChat` folder. Messages will still be fetched from the
|
* `DeltaChat` folder. Messages will still be fetched from the
|
||||||
* spam folder and `sendbox_watch` will also still be respected
|
* spam folder and `sendbox_watch` will also still be respected
|
||||||
* if enabled.
|
* if enabled.
|
||||||
* 0=watch all folders normally (default)
|
* 0=watch all folders normally (default)
|
||||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
|
||||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||||
* show direct replies to chats only,
|
* show direct replies to chats only,
|
||||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||||
@@ -506,11 +510,22 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||||
* seconds. 2 days by default.
|
* seconds. 2 days by default.
|
||||||
* This is not supposed to be changed by UIs and only used for testing.
|
* This is not supposed to be changed by UIs and only used for testing.
|
||||||
|
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
|
||||||
|
* to 1 if it supports verified 1:1 chats.
|
||||||
|
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
|
||||||
|
* and when the key changes, an info message is posted into the chat.
|
||||||
|
* 0=Nothing else happens when the key changes.
|
||||||
|
* 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.
|
||||||
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
* - `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,
|
* 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`.
|
* 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,
|
* 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.
|
* 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().
|
* If you want to retrieve a value, use dc_get_config().
|
||||||
*
|
*
|
||||||
@@ -678,8 +693,25 @@ int dc_get_connectivity (dc_context_t* context);
|
|||||||
char* dc_get_connectivity_html (dc_context_t* context);
|
char* dc_get_connectivity_html (dc_context_t* context);
|
||||||
|
|
||||||
|
|
||||||
|
#define DC_PUSH_NOT_CONNECTED 0
|
||||||
|
#define DC_PUSH_HEARTBEAT 1
|
||||||
|
#define DC_PUSH_CONNECTED 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current push notification state.
|
||||||
|
* One of:
|
||||||
|
* - DC_PUSH_NOT_CONNECTED
|
||||||
|
* - DC_PUSH_HEARTBEAT
|
||||||
|
* - DC_PUSH_CONNECTED
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @return Push notification state.
|
||||||
|
*/
|
||||||
|
int dc_get_push_state (dc_context_t* context);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standalone version of dc_accounts_all_work_done().
|
|
||||||
* Only used by the python tests.
|
* Only used by the python tests.
|
||||||
*/
|
*/
|
||||||
int dc_all_work_done (dc_context_t* context);
|
int dc_all_work_done (dc_context_t* context);
|
||||||
@@ -884,7 +916,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
|||||||
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
||||||
* is added as needed.
|
* is added as needed.
|
||||||
* @param query_str An optional query for filtering the list. Only chats matching this query
|
* @param query_str An optional query for filtering the list. Only chats matching this query
|
||||||
* are returned. Give NULL for no filtering.
|
* are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
|
||||||
|
* the chatlist is filtered such that only chats with unread messages show up.
|
||||||
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
|
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
|
||||||
* are returned. Give 0 for no filtering.
|
* are returned. Give 0 for no filtering.
|
||||||
* @return A chatlist as an dc_chatlist_t object.
|
* @return A chatlist as an dc_chatlist_t object.
|
||||||
@@ -1091,40 +1124,12 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
|||||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a reaction to message.
|
|
||||||
*
|
|
||||||
* Reaction is a string of emojis separated by spaces. Reaction to a
|
|
||||||
* single message can be sent multiple times. The last reaction
|
|
||||||
* received overrides all previously received reactions. It is
|
|
||||||
* possible to remove all reactions by sending an empty string.
|
|
||||||
*
|
|
||||||
* @memberof dc_context_t
|
|
||||||
* @param context The context object.
|
|
||||||
* @param msg_id ID of the message you react to.
|
|
||||||
* @param reaction A string consisting of emojis separated by spaces.
|
|
||||||
* @return The ID of the message sent out or 0 for errors.
|
|
||||||
*/
|
|
||||||
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a structure with reactions to the message.
|
|
||||||
*
|
|
||||||
* @memberof dc_context_t
|
|
||||||
* @param context The context object.
|
|
||||||
* @param msg_id The message ID to get reactions for.
|
|
||||||
* @return A structure with all reactions to the message.
|
|
||||||
*/
|
|
||||||
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A webxdc instance sends a status update to its other members.
|
* A webxdc instance sends a status update to its other members.
|
||||||
*
|
*
|
||||||
* In JS land, that would be mapped to something as:
|
* In JS land, that would be mapped to something as:
|
||||||
* ```
|
* ```
|
||||||
* success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
|
* success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4');
|
||||||
* ```
|
* ```
|
||||||
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
|
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
|
||||||
* See dc_get_webxdc_status_updates() for the receiving counterpart.
|
* See dc_get_webxdc_status_updates() for the receiving counterpart.
|
||||||
@@ -1181,6 +1186,65 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
|
|||||||
*/
|
*/
|
||||||
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
|
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Webxdc file as integration.
|
||||||
|
* see dc_init_webxdc_integration() for more details about Webxdc integrations.
|
||||||
|
*
|
||||||
|
* @warning This is an experimental API which may change in the future
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @param file The .xdc file to use as Webxdc integration.
|
||||||
|
*/
|
||||||
|
void dc_set_webxdc_integration (dc_context_t* context, const char* file);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init a Webxdc integration.
|
||||||
|
*
|
||||||
|
* A Webxdc integration is
|
||||||
|
* a Webxdc showing a map, getting locations via setUpdateListener(), setting POIs via sendUpdate();
|
||||||
|
* core takes eg. care of feeding locations to the Webxdc or sending the data out.
|
||||||
|
*
|
||||||
|
* @warning This is an experimental API, esp. support of integration types (eg. image editor, tools) is left out for simplicity
|
||||||
|
*
|
||||||
|
* Currently, Webxdc integrations are .xdc files shipped together with the main app.
|
||||||
|
* Before dc_init_webxdc_integration() can be called,
|
||||||
|
* UI has to call dc_set_webxdc_integration() to define a .xdc file to be used as integration.
|
||||||
|
*
|
||||||
|
* dc_init_webxdc_integration() returns a Webxdc message ID that
|
||||||
|
* UI can open and use mostly as usual.
|
||||||
|
*
|
||||||
|
* Concrete behaviour and status updates depend on the integration, driven by UI needs.
|
||||||
|
*
|
||||||
|
* There is no need to de-initialize the integration,
|
||||||
|
* however, unless documented otherwise,
|
||||||
|
* the integration is valid only as long as not re-initialized
|
||||||
|
* In other words, UI must not have a Webxdc with the same integration open twice.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ~~~
|
||||||
|
* // Define a .xdc file to be used as maps integration
|
||||||
|
* dc_set_webxdc_integration(context, path_to_maps_xdc);
|
||||||
|
*
|
||||||
|
* // Integrate the map to a chat, the map will show locations for this chat then:
|
||||||
|
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, any_chat_id);
|
||||||
|
*
|
||||||
|
* // Or use the Webxdc as a global map, showing locations of all chats:
|
||||||
|
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, 0);
|
||||||
|
* ~~~
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @param chat_id The chat to get the integration for.
|
||||||
|
* @return ID of the message that refers to the Webxdc instance.
|
||||||
|
* UI can open a Webxdc as usual with this instance.
|
||||||
|
*/
|
||||||
|
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a draft for a chat in the database.
|
* Save a draft for a chat in the database.
|
||||||
*
|
*
|
||||||
@@ -1506,24 +1570,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
|||||||
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);
|
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);
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or disable protection against active attacks.
|
|
||||||
* To enable protection, it is needed that all members are verified;
|
|
||||||
* if this condition is met, end-to-end-encryption is always enabled
|
|
||||||
* and only the verified keys are used.
|
|
||||||
*
|
|
||||||
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
|
|
||||||
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
|
||||||
*
|
|
||||||
* @memberof dc_context_t
|
|
||||||
* @param context The context object as returned from dc_context_new().
|
|
||||||
* @param chat_id The ID of the chat to change the protection for.
|
|
||||||
* @param protect 1=protect chat, 0=unprotect chat
|
|
||||||
* @return 1=success, 0=error, e.g. some members may be unverified
|
|
||||||
*/
|
|
||||||
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set chat visibility to pinned, archived or normal.
|
* Set chat visibility to pinned, archived or normal.
|
||||||
*
|
*
|
||||||
@@ -1717,24 +1763,12 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
|
|||||||
* Create a new broadcast list.
|
* Create a new broadcast list.
|
||||||
*
|
*
|
||||||
* Broadcast lists are similar to groups on the sending device,
|
* Broadcast lists are similar to groups on the sending device,
|
||||||
* however, recipients get the messages in normal one-to-one chats
|
* however, recipients get the messages in a read-only chat
|
||||||
* and will not be aware of other members.
|
* and will see who the other members are.
|
||||||
*
|
*
|
||||||
* Replies to broadcasts go only to the sender
|
* For historical reasons, this function does not take a name directly,
|
||||||
* and not to all broadcast recipients.
|
* instead you have to set the name using dc_set_chat_name()
|
||||||
* Moreover, replies will not appear in the broadcast list
|
* after creating the broadcast list.
|
||||||
* but in the one-to-one chat with the person answering.
|
|
||||||
*
|
|
||||||
* The name and the image of the broadcast list is set automatically
|
|
||||||
* and is visible to the sender only.
|
|
||||||
* Not asking for these data allows more focused creation
|
|
||||||
* and we bypass the question who will get which data.
|
|
||||||
* Also, many users will have at most one broadcast list
|
|
||||||
* so, a generic name and image is sufficient at the first place.
|
|
||||||
*
|
|
||||||
* Later on, however, the name can be changed using dc_set_chat_name().
|
|
||||||
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
|
||||||
* All in all, this is also what other messengers are doing here.
|
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object.
|
* @param context The context object.
|
||||||
@@ -2277,8 +2311,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
|||||||
* the backup is not encrypted.
|
* the backup is not encrypted.
|
||||||
* The backup contains all contacts, chats, images and other data and device independent settings.
|
* The backup contains all contacts, chats, images and other data and device independent settings.
|
||||||
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
||||||
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
|
* The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
|
||||||
* the format is `delta-chat-<day>-<number>.tar`
|
|
||||||
*
|
*
|
||||||
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
||||||
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
||||||
@@ -2579,7 +2612,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
|||||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||||
* works for protected groups as well as for normal groups.
|
* works for protected groups as well as for normal groups.
|
||||||
* If set to 0, the Setup-Contact protocol is offered in the QR code.
|
* If set to 0, the Setup-Contact protocol is offered in the QR code.
|
||||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
* See https://securejoin.delta.chat/
|
||||||
* for details about both protocols.
|
* for details about both protocols.
|
||||||
* @return The text that should go to the QR code,
|
* @return The text that should go to the QR code,
|
||||||
* On errors, an empty QR code is returned, NULL is never returned.
|
* On errors, an empty QR code is returned, NULL is never returned.
|
||||||
@@ -2615,8 +2648,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
|
|||||||
*
|
*
|
||||||
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
|
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
|
||||||
*
|
*
|
||||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
* See https://securejoin.delta.chat/ for details about both protocols.
|
||||||
* for details about both protocols.
|
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object.
|
* @param context The context object.
|
||||||
@@ -2959,16 +2991,18 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
|
|||||||
* use dc_accounts_remove_account().
|
* use dc_accounts_remove_account().
|
||||||
*
|
*
|
||||||
* @memberof dc_accounts_t
|
* @memberof dc_accounts_t
|
||||||
* @param os_name
|
|
||||||
* @param dir The directory to create the context-databases in.
|
* @param dir The directory to create the context-databases in.
|
||||||
* If the directory does not exist,
|
* If the directory does not exist,
|
||||||
* dc_accounts_new() will try to create it.
|
* dc_accounts_new() will try to create it.
|
||||||
|
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
|
||||||
|
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
|
||||||
|
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
|
||||||
* @return An account manager object.
|
* @return An account manager object.
|
||||||
* The object must be passed to the other account manager functions
|
* The object must be passed to the other account manager functions
|
||||||
* and must be freed using dc_accounts_unref() after usage.
|
* and must be freed using dc_accounts_unref() after usage.
|
||||||
* On errors, NULL is returned.
|
* On errors, NULL is returned.
|
||||||
*/
|
*/
|
||||||
dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir);
|
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3099,23 +3133,6 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
|
|||||||
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
|
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
|
|
||||||
*
|
|
||||||
* iOS can:
|
|
||||||
* - call dc_start_io() (in case IO was not running)
|
|
||||||
* - call dc_maybe_network()
|
|
||||||
* - while dc_accounts_all_work_done() returns false:
|
|
||||||
* - Wait for #DC_EVENT_CONNECTIVITY_CHANGED
|
|
||||||
*
|
|
||||||
* @memberof dc_accounts_t
|
|
||||||
* @param accounts The account manager as created by dc_accounts_new().
|
|
||||||
* @return Whether all accounts finished their background work.
|
|
||||||
* #DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
|
|
||||||
*/
|
|
||||||
int dc_accounts_all_work_done (dc_accounts_t* accounts);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
|
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
|
||||||
* If IO is already running, nothing happens.
|
* If IO is already running, nothing happens.
|
||||||
@@ -3166,6 +3183,33 @@ void dc_accounts_maybe_network (dc_accounts_t* accounts);
|
|||||||
void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a background fetch for all accounts in parallel with a timeout.
|
||||||
|
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
|
||||||
|
*
|
||||||
|
* dc_accounts_background_fetch() was created for the iOS Background fetch.
|
||||||
|
*
|
||||||
|
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end
|
||||||
|
* even in case of timeout, unless the function fails and returns 0.
|
||||||
|
* Process all events until you get this one and you can safely return to the background
|
||||||
|
* without forgetting to create notifications caused by timing race conditions.
|
||||||
|
*
|
||||||
|
* @memberof dc_accounts_t
|
||||||
|
* @param timeout The timeout in seconds
|
||||||
|
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
|
||||||
|
*/
|
||||||
|
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets device token for Apple Push Notification service.
|
||||||
|
* Returns immediately.
|
||||||
|
*
|
||||||
|
* @memberof dc_accounts_t
|
||||||
|
* @param token Hexadecimal device token
|
||||||
|
*/
|
||||||
|
void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const char *token);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the event emitter that is used to receive events.
|
* Create the event emitter that is used to receive events.
|
||||||
*
|
*
|
||||||
@@ -3747,9 +3791,22 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a chat is protected.
|
* Check if a chat is protected.
|
||||||
* Protected chats contain only verified members and encryption is always enabled.
|
*
|
||||||
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
|
* End-to-end encryption is guaranteed in protected chats
|
||||||
* The status can be changed using dc_set_chat_protection().
|
* and only verified contacts
|
||||||
|
* as determined by dc_contact_is_verified()
|
||||||
|
* can be added to protected chats.
|
||||||
|
*
|
||||||
|
* Protected chats are created using dc_create_group_chat()
|
||||||
|
* by setting the 'protect' parameter to 1.
|
||||||
|
* 1:1 chats become protected or unprotected automatically
|
||||||
|
* if `verified_one_on_one_chats` setting is enabled.
|
||||||
|
*
|
||||||
|
* UI should display a green checkmark
|
||||||
|
* in the chat title,
|
||||||
|
* in the chatlist item
|
||||||
|
* and in the chat profile
|
||||||
|
* if chat protection is enabled.
|
||||||
*
|
*
|
||||||
* @memberof dc_chat_t
|
* @memberof dc_chat_t
|
||||||
* @param chat The chat object.
|
* @param chat The chat object.
|
||||||
@@ -3758,6 +3815,26 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
|||||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the chat was protected, and then an incoming message broke this protection.
|
||||||
|
*
|
||||||
|
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||||
|
* otherwise it will return false for all chats.
|
||||||
|
*
|
||||||
|
* 1:1 chats are automatically set as protected when a contact is verified.
|
||||||
|
* When a message comes in that is not encrypted / signed correctly,
|
||||||
|
* the chat is automatically set as unprotected again.
|
||||||
|
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
|
||||||
|
*
|
||||||
|
* The UI should let the user confirm that this is OK with a message like
|
||||||
|
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
|
||||||
|
* @memberof dc_chat_t
|
||||||
|
* @param chat The chat object.
|
||||||
|
* @return 1=chat protection broken, 0=otherwise.
|
||||||
|
*/
|
||||||
|
int dc_chat_is_protection_broken (const dc_chat_t* chat);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if locations are sent to the chat
|
* Check if locations are sent to the chat
|
||||||
* at the time the object was created using dc_get_chat().
|
* at the time the object was created using dc_get_chat().
|
||||||
@@ -3962,7 +4039,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
|
|||||||
* Get the message time used for sorting.
|
* Get the message time used for sorting.
|
||||||
* This function returns the timestamp that is used for sorting the message
|
* This function returns the timestamp that is used for sorting the message
|
||||||
* into lists as returned e.g. by dc_get_chat_msgs().
|
* into lists as returned e.g. by dc_get_chat_msgs().
|
||||||
* This may be the reveived time, the sending time or another time.
|
* This may be the received time, the sending time or another time.
|
||||||
*
|
*
|
||||||
* To get the receiving time, use dc_msg_get_received_timestamp().
|
* To get the receiving time, use dc_msg_get_received_timestamp().
|
||||||
* To get the sending time, use dc_msg_get_timestamp().
|
* To get the sending time, use dc_msg_get_timestamp().
|
||||||
@@ -4032,6 +4109,19 @@ char* dc_msg_get_subject (const dc_msg_t* msg);
|
|||||||
char* dc_msg_get_file (const dc_msg_t* msg);
|
char* dc_msg_get_file (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save file copy at the user-provided path.
|
||||||
|
*
|
||||||
|
* Fails if file already exists at the provided path.
|
||||||
|
*
|
||||||
|
* @memberof dc_msg_t
|
||||||
|
* @param msg The message object.
|
||||||
|
* @param path Destination file path with filename and extension.
|
||||||
|
* @return 0 on failure, 1 on success.
|
||||||
|
*/
|
||||||
|
int dc_msg_save_file (const dc_msg_t* msg, const char* path);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an original attachment filename, with extension but without the path. To get the full path,
|
* Get an original attachment filename, with extension but without the path. To get the full path,
|
||||||
* use dc_msg_get_file().
|
* use dc_msg_get_file().
|
||||||
@@ -4096,7 +4186,6 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
|||||||
* true if the Webxdc should get full internet access, including Webrtc.
|
* true if the Webxdc should get full internet access, including Webrtc.
|
||||||
* currently, this is only true for encrypted Webxdc's in the self chat
|
* currently, this is only true for encrypted Webxdc's in the self chat
|
||||||
* that have requested internet access in the manifest.
|
* that have requested internet access in the manifest.
|
||||||
* this is useful for development and maybe for internal integrations at some point.
|
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
* @param msg The webxdc instance.
|
* @param msg The webxdc instance.
|
||||||
@@ -4305,9 +4394,9 @@ int dc_msg_has_deviating_timestamp(const dc_msg_t* msg);
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a message has a location bound to it.
|
* Check if a message has a POI location bound to it.
|
||||||
* These messages are also returned by dc_get_locations()
|
* These locations are also returned by dc_get_locations()
|
||||||
* and the UI may decide to display a special icon beside such messages,
|
* The UI may decide to display a special icon beside such messages.
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
* @param msg The message object.
|
* @param msg The message object.
|
||||||
@@ -4352,7 +4441,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
|
|||||||
* Check if the message is an informational message, created by the
|
* Check if the message is an informational message, created by the
|
||||||
* device or by another users. Such messages are not "typed" by the user but
|
* device or by another users. Such messages are not "typed" by the user but
|
||||||
* created due to other actions,
|
* created due to other actions,
|
||||||
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
|
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||||
* or dc_add_contact_to_chat().
|
* or dc_add_contact_to_chat().
|
||||||
*
|
*
|
||||||
* These messages are typically shown in the center of the chat view,
|
* These messages are typically shown in the center of the chat view,
|
||||||
@@ -4379,6 +4468,9 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
|||||||
* Currently, the following types are defined:
|
* Currently, the following types are defined:
|
||||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||||
|
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||||
|
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
|
||||||
|
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||||
*
|
*
|
||||||
* Even when you display an icon,
|
* Even when you display an icon,
|
||||||
* you should still display the text of the informational message using dc_msg_get_text()
|
* you should still display the text of the informational message using dc_msg_get_text()
|
||||||
@@ -4405,6 +4497,7 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
|
|||||||
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
||||||
#define DC_INFO_PROTECTION_ENABLED 11
|
#define DC_INFO_PROTECTION_ENABLED 11
|
||||||
#define DC_INFO_PROTECTION_DISABLED 12
|
#define DC_INFO_PROTECTION_DISABLED 12
|
||||||
|
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4563,15 +4656,18 @@ int dc_msg_has_html (dc_msg_t* msg);
|
|||||||
* if they are larger than the limit set by the dc_set_config()-option `download_limit`.
|
* if they are larger than the limit set by the dc_set_config()-option `download_limit`.
|
||||||
*
|
*
|
||||||
* The function returns one of:
|
* The function returns one of:
|
||||||
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
|
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
|
||||||
* and should be rendered as usual.
|
* and should be rendered as usual.
|
||||||
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
|
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
|
||||||
* In addition to the usual message rendering,
|
* In addition to the usual message rendering,
|
||||||
* the UI shall show a download button that calls dc_download_full_msg()
|
* the UI shall show a download button that calls dc_download_full_msg()
|
||||||
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
|
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
|
||||||
* If the download fails or succeeds,
|
* If the download fails or succeeds,
|
||||||
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
|
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
|
||||||
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
|
*
|
||||||
|
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any futher download action.
|
||||||
|
* It was fully downloaded, but we failed to decrypt it.
|
||||||
|
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
* @param msg The message object.
|
* @param msg The message object.
|
||||||
@@ -5029,10 +5125,16 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a contact was verified. E.g. by a secure-join QR code scan
|
* Check if the contact
|
||||||
* and if the key has not changed since this verification.
|
* can be added to verified chats,
|
||||||
|
* i.e. has a verified key
|
||||||
|
* and Autocrypt key matches the verified key.
|
||||||
*
|
*
|
||||||
* The UI may draw a checkbox or something like that beside verified contacts.
|
* If contact is verified
|
||||||
|
* UI should display green checkmark after the contact name
|
||||||
|
* in contact list items,
|
||||||
|
* in chat member list items
|
||||||
|
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
|
||||||
*
|
*
|
||||||
* @memberof dc_contact_t
|
* @memberof dc_contact_t
|
||||||
* @param contact The contact object.
|
* @param contact The contact object.
|
||||||
@@ -5041,28 +5143,29 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
|||||||
*/
|
*/
|
||||||
int dc_contact_is_verified (dc_contact_t* contact);
|
int dc_contact_is_verified (dc_contact_t* contact);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the address that verified a contact
|
* Returns whether contact is a bot.
|
||||||
*
|
|
||||||
* The UI may use this in addition to a checkmark showing the verification status
|
|
||||||
*
|
*
|
||||||
* @memberof dc_contact_t
|
* @memberof dc_contact_t
|
||||||
* @param contact The contact object.
|
* @param contact The contact object.
|
||||||
* @return
|
* @return 0 if the contact is not a bot, 1 otherwise.
|
||||||
* A string containing the verifiers address. If it is the same address as the contact itself,
|
|
||||||
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
|
||||||
* information or the contact is not verified.
|
|
||||||
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
|
|
||||||
*/
|
*/
|
||||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
int dc_contact_is_bot (dc_contact_t* contact);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the `ContactId` that verified a contact
|
* Return the contact ID that verified a contact.
|
||||||
*
|
*
|
||||||
* The UI may use this in addition to a checkmark showing the verification status
|
* If the function returns non-zero result,
|
||||||
|
* display green checkmark in the profile and "Introduced by ..." line
|
||||||
|
* with the name and address of the contact
|
||||||
|
* formatted by dc_contact_get_name_n_addr.
|
||||||
|
*
|
||||||
|
* If this function returns a verifier,
|
||||||
|
* this does not necessarily mean
|
||||||
|
* you can add the contact to verified chats.
|
||||||
|
* Use dc_contact_is_verified() to check
|
||||||
|
* if a contact can be added to a verified chat instead.
|
||||||
*
|
*
|
||||||
* @memberof dc_contact_t
|
* @memberof dc_contact_t
|
||||||
* @param contact The contact object.
|
* @param contact The contact object.
|
||||||
@@ -5167,72 +5270,6 @@ int dc_provider_get_status (const dc_provider_t* prov
|
|||||||
void dc_provider_unref (dc_provider_t* provider);
|
void dc_provider_unref (dc_provider_t* provider);
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an HTTP(S) GET response.
|
|
||||||
* This function can be used to download remote content for HTML emails.
|
|
||||||
*
|
|
||||||
* @memberof dc_context_t
|
|
||||||
* @param context The context object to take proxy settings from.
|
|
||||||
* @param url HTTP or HTTPS URL.
|
|
||||||
* @return The response must be released using dc_http_response_unref() after usage.
|
|
||||||
* NULL is returned on errors.
|
|
||||||
*/
|
|
||||||
dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class dc_http_response_t
|
|
||||||
*
|
|
||||||
* An object containing an HTTP(S) GET response.
|
|
||||||
* Created by dc_get_http_response().
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html".
|
|
||||||
*
|
|
||||||
* @memberof dc_http_response_t
|
|
||||||
* @param response HTTP response as returned by dc_get_http_response().
|
|
||||||
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
|
|
||||||
*/
|
|
||||||
char* dc_http_response_get_mimetype (const dc_http_response_t* response);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns HTTP response encoding, e.g. "utf-8".
|
|
||||||
*
|
|
||||||
* @memberof dc_http_response_t
|
|
||||||
* @param response HTTP response as returned by dc_get_http_response().
|
|
||||||
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
|
|
||||||
*/
|
|
||||||
char* dc_http_response_get_encoding (const dc_http_response_t* response);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns HTTP response contents.
|
|
||||||
*
|
|
||||||
* @memberof dc_http_response_t
|
|
||||||
* @param response HTTP response as returned by dc_get_http_response().
|
|
||||||
* @return The blob which must be released using dc_str_unref() after usage. NULL is never returned.
|
|
||||||
*/
|
|
||||||
uint8_t* dc_http_response_get_blob (const dc_http_response_t* response);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns HTTP response content size.
|
|
||||||
*
|
|
||||||
* @memberof dc_http_response_t
|
|
||||||
* @param response HTTP response as returned by dc_get_http_response().
|
|
||||||
* @return The blob size.
|
|
||||||
*/
|
|
||||||
size_t dc_http_response_get_size (const dc_http_response_t* response);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Free an HTTP response object.
|
|
||||||
*
|
|
||||||
* @memberof dc_http_response_t
|
|
||||||
* @param response HTTP response as returned by dc_get_http_response().
|
|
||||||
*/
|
|
||||||
void dc_http_response_unref (const dc_http_response_t* response);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class dc_lot_t
|
* @class dc_lot_t
|
||||||
*
|
*
|
||||||
@@ -5330,48 +5367,6 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
|
|||||||
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class dc_reactions_t
|
|
||||||
*
|
|
||||||
* An object representing all reactions for a single message.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns array of contacts which reacted to the given message.
|
|
||||||
*
|
|
||||||
* @memberof dc_reactions_t
|
|
||||||
* @param reactions The object containing message reactions.
|
|
||||||
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
|
|
||||||
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
|
|
||||||
*/
|
|
||||||
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string containing space-separated reactions of a single contact.
|
|
||||||
*
|
|
||||||
* @memberof dc_reactions_t
|
|
||||||
* @param reactions The object containing message reactions.
|
|
||||||
* @param contact_id ID of the contact.
|
|
||||||
* @return Space-separated list of emoji sequences, which could be empty.
|
|
||||||
* Returned string should not be modified and should be freed
|
|
||||||
* with dc_str_unref() after usage.
|
|
||||||
*/
|
|
||||||
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Frees an object containing message reactions.
|
|
||||||
*
|
|
||||||
* Reactions objects are created by dc_get_msg_reactions().
|
|
||||||
*
|
|
||||||
* @memberof dc_reactions_t
|
|
||||||
* @param reactions The object to free.
|
|
||||||
* If NULL is given, nothing is done.
|
|
||||||
*/
|
|
||||||
void dc_reactions_unref (dc_reactions_t* reactions);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @defgroup DC_MSG DC_MSG
|
* @defgroup DC_MSG DC_MSG
|
||||||
*
|
*
|
||||||
@@ -5488,6 +5483,11 @@ void dc_reactions_unref (dc_reactions_t* reactions);
|
|||||||
*/
|
*/
|
||||||
#define DC_MSG_WEBXDC 80
|
#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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
@@ -6062,10 +6062,12 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
* Downloading a bunch of messages just finished. This is an
|
* Downloading a bunch of messages just finished. This is an
|
||||||
* event to allow the UI to only show one notification per message bunch,
|
* event to allow the UI to only show one notification per message bunch,
|
||||||
* instead of cluttering the user with many notifications.
|
* instead of cluttering the user with many notifications.
|
||||||
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
|
* UI may store #DC_EVENT_INCOMING_MSG events
|
||||||
|
* and display notifications for all messages at once
|
||||||
|
* when this event arrives.
|
||||||
*
|
*
|
||||||
* @param data1 0
|
* @param data1 0
|
||||||
* @param data2 (char*) msg_ids, a json object with the message ids.
|
* @param data2 0
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
|
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
|
||||||
|
|
||||||
@@ -6225,6 +6227,7 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
* @param data2 (int) The progress as:
|
* @param data2 (int) The progress as:
|
||||||
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
* (Bob has verified alice and waits until Alice does the same for him)
|
* (Bob has verified alice and waits until Alice does the same for him)
|
||||||
|
* 1000=vg-member-added/vc-contact-confirm received
|
||||||
*/
|
*/
|
||||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||||
|
|
||||||
@@ -6248,6 +6251,18 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
#define DC_EVENT_SELFAVATAR_CHANGED 2110
|
#define DC_EVENT_SELFAVATAR_CHANGED 2110
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A multi-device synced config value changed. Maybe the app needs to refresh smth. For uniformity
|
||||||
|
* this is emitted on the source device too. The value isn't reported, otherwise it would be logged
|
||||||
|
* which might not be good for privacy. You can get the new value with
|
||||||
|
* `dc_get_config(context, data2)`.
|
||||||
|
*
|
||||||
|
* @param data1 0
|
||||||
|
* @param data2 (char*) Configuration key.
|
||||||
|
*/
|
||||||
|
#define DC_EVENT_CONFIG_SYNCED 2111
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* webxdc status update received.
|
* webxdc status update received.
|
||||||
* To get the received status update, use dc_get_webxdc_status_updates() with
|
* To get the received status update, use dc_get_webxdc_status_updates() with
|
||||||
@@ -6272,6 +6287,53 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
|
|
||||||
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
|
#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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells that the Background fetch was completed (or timed out).
|
||||||
|
*
|
||||||
|
* This event acts as a marker, when you reach this event you can be sure
|
||||||
|
* that all events emitted during the background fetch were processed.
|
||||||
|
*
|
||||||
|
* This event is only emitted by the account manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||||
|
*
|
||||||
|
* Sometimes this is emitted together with `DC_EVENT_CHATLIST_ITEM_CHANGED`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define DC_EVENT_CHATLIST_CHANGED 2300
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inform that all or a single chat list item changed and needs to be rerendered
|
||||||
|
* If `chat_id` is set to 0, 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.
|
||||||
|
*
|
||||||
|
* @param data1 (int) chat_id chat id of chatlist item to be rerendered, if chat_id = 0 all (cached & visible) items need to be rerendered
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
@@ -6417,22 +6479,27 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/**
|
/**
|
||||||
* Download not needed, see dc_msg_get_download_state() for details.
|
* Download not needed, see dc_msg_get_download_state() for details.
|
||||||
*/
|
*/
|
||||||
#define DC_DOWNLOAD_DONE 0
|
#define DC_DOWNLOAD_DONE 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download available, see dc_msg_get_download_state() for details.
|
* Download available, see dc_msg_get_download_state() for details.
|
||||||
*/
|
*/
|
||||||
#define DC_DOWNLOAD_AVAILABLE 10
|
#define DC_DOWNLOAD_AVAILABLE 10
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download failed, see dc_msg_get_download_state() for details.
|
* Download failed, see dc_msg_get_download_state() for details.
|
||||||
*/
|
*/
|
||||||
#define DC_DOWNLOAD_FAILURE 20
|
#define DC_DOWNLOAD_FAILURE 20
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download not needed, see dc_msg_get_download_state() for details.
|
||||||
|
*/
|
||||||
|
#define DC_DOWNLOAD_UNDECIPHERABLE 30
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download in progress, see dc_msg_get_download_state() for details.
|
* Download in progress, see dc_msg_get_download_state() for details.
|
||||||
*/
|
*/
|
||||||
#define DC_DOWNLOAD_IN_PROGRESS 1000
|
#define DC_DOWNLOAD_IN_PROGRESS 1000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6597,7 +6664,7 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// - %1$s will be replaced by the name of the verified contact
|
/// - %1$s will be replaced by the name of the verified contact
|
||||||
#define DC_STR_CONTACT_VERIFIED 35
|
#define DC_STR_CONTACT_VERIFIED 35
|
||||||
|
|
||||||
/// "Cannot verify %1$s."
|
/// "Cannot establish guaranteed end-to-end encryption with %1$s."
|
||||||
///
|
///
|
||||||
/// Used in status messages.
|
/// Used in status messages.
|
||||||
/// - %1$s will be replaced by the name of the contact that cannot be verified
|
/// - %1$s will be replaced by the name of the contact that cannot be verified
|
||||||
@@ -6787,15 +6854,6 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// Used in error strings.
|
/// Used in error strings.
|
||||||
#define DC_STR_ERROR_NO_NETWORK 87
|
#define DC_STR_ERROR_NO_NETWORK 87
|
||||||
|
|
||||||
/// "Chat protection enabled."
|
|
||||||
///
|
|
||||||
|
|
||||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
|
|
||||||
#define DC_STR_PROTECTION_ENABLED 88
|
|
||||||
|
|
||||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
|
|
||||||
#define DC_STR_PROTECTION_DISABLED 89
|
|
||||||
|
|
||||||
/// "Reply"
|
/// "Reply"
|
||||||
///
|
///
|
||||||
/// Used in summaries.
|
/// Used in summaries.
|
||||||
@@ -7044,6 +7102,8 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// "You added member %1$s."
|
/// "You added member %1$s."
|
||||||
///
|
///
|
||||||
/// Used in status messages.
|
/// Used in status messages.
|
||||||
|
///
|
||||||
|
/// `%1$s` will be replaced by the added member's name.
|
||||||
#define DC_STR_ADD_MEMBER_BY_YOU 128
|
#define DC_STR_ADD_MEMBER_BY_YOU 128
|
||||||
|
|
||||||
/// "Member %1$s added by %2$s."
|
/// "Member %1$s added by %2$s."
|
||||||
@@ -7240,26 +7300,6 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// `%2$s` will be replaced by name and address of the contact.
|
/// `%2$s` will be replaced by name and address of the contact.
|
||||||
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
||||||
|
|
||||||
/// "You enabled chat protection."
|
|
||||||
///
|
|
||||||
/// Used in status messages.
|
|
||||||
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
|
|
||||||
|
|
||||||
/// "Chat protection enabled by %1$s."
|
|
||||||
///
|
|
||||||
/// `%1$s` will be replaced by name and address of the contact.
|
|
||||||
///
|
|
||||||
/// Used in status messages.
|
|
||||||
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
|
|
||||||
|
|
||||||
/// "You disabled chat protection."
|
|
||||||
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
|
|
||||||
|
|
||||||
/// "Chat protection disabled by %1$s."
|
|
||||||
///
|
|
||||||
/// `%1$s` will be replaced by name and address of the contact.
|
|
||||||
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
|
|
||||||
|
|
||||||
/// "Scan to set up second device for %1$s"
|
/// "Scan to set up second device for %1$s"
|
||||||
///
|
///
|
||||||
/// `%1$s` will be replaced by name and address of the account.
|
/// `%1$s` will be replaced by name and address of the account.
|
||||||
@@ -7270,6 +7310,65 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// Used as a device message after a successful backup transfer.
|
/// Used as a device message after a successful backup transfer.
|
||||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||||
|
|
||||||
|
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||||
|
///
|
||||||
|
/// Used in info messages.
|
||||||
|
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||||
|
|
||||||
|
/// "%1$s sent a message from another device."
|
||||||
|
///
|
||||||
|
/// Used in info messages.
|
||||||
|
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||||
|
|
||||||
|
/// "Others will only see this group after you sent a first message."
|
||||||
|
///
|
||||||
|
/// Used as the first info messages in newly created groups.
|
||||||
|
#define DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE 172
|
||||||
|
|
||||||
|
/// "Member %1$s added."
|
||||||
|
///
|
||||||
|
/// Used as info messages.
|
||||||
|
///
|
||||||
|
/// `%1$s` will be replaced by the added member's name.
|
||||||
|
#define DC_STR_MESSAGE_ADD_MEMBER 173
|
||||||
|
|
||||||
|
/// "Your email provider %1$s requires end-to-end encryption which is not setup yet."
|
||||||
|
///
|
||||||
|
/// Used as info messages when a message cannot be sent because it cannot be encrypted.
|
||||||
|
///
|
||||||
|
/// `%1$s` will be replaced by the provider's domain.
|
||||||
|
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
|
||||||
|
|
||||||
|
/// "You reacted %1$s to '%2$s'"
|
||||||
|
///
|
||||||
|
/// `%1$s` will be replaced by the reaction, usually an emoji
|
||||||
|
/// `%2$s` will be replaced by the summary of the message the reaction refers to
|
||||||
|
///
|
||||||
|
/// Used in summaries.
|
||||||
|
#define DC_STR_YOU_REACTED 176
|
||||||
|
|
||||||
|
/// "%1$s reacted %2$s to '%3$s'"
|
||||||
|
///
|
||||||
|
/// `%1$s` will be replaced by the name the contact who reacted
|
||||||
|
/// `%2$s` will be replaced by the reaction, usually an emoji
|
||||||
|
/// `%3$s` will be replaced by the summary of the message the reaction refers to
|
||||||
|
///
|
||||||
|
/// Used in summaries.
|
||||||
|
#define DC_STR_REACTED_BY 177
|
||||||
|
|
||||||
|
/// "Establishing guaranteed end-to-end encryption, please wait…"
|
||||||
|
///
|
||||||
|
/// Used as info message.
|
||||||
|
#define DC_STR_SECUREJOIN_WAIT 190
|
||||||
|
|
||||||
|
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
|
||||||
|
///
|
||||||
|
/// Used as info message.
|
||||||
|
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||||
|
|
||||||
|
/// "Contact"
|
||||||
|
#define DC_STR_CONTACT 200
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
#![warn(unused, clippy::all)]
|
#![warn(unused, clippy::all)]
|
||||||
#![allow(
|
#![allow(
|
||||||
non_camel_case_types,
|
non_camel_case_types,
|
||||||
non_snake_case,
|
non_snake_case,
|
||||||
non_upper_case_globals,
|
non_upper_case_globals,
|
||||||
non_upper_case_globals,
|
|
||||||
non_camel_case_types,
|
|
||||||
clippy::missing_safety_doc,
|
clippy::missing_safety_doc,
|
||||||
clippy::expect_fun_call
|
clippy::expect_fun_call
|
||||||
)]
|
)]
|
||||||
@@ -26,16 +25,13 @@ use anyhow::Context as _;
|
|||||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
|
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
|
||||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||||
use deltachat::contact::{Contact, ContactId, Origin};
|
use deltachat::contact::{Contact, ContactId, Origin};
|
||||||
use deltachat::context::Context;
|
use deltachat::context::{Context, ContextBuilder};
|
||||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||||
use deltachat::imex::BackupProvider;
|
use deltachat::imex::BackupProvider;
|
||||||
use deltachat::key::{DcKey, DcSecretKey};
|
use deltachat::key::preconfigure_keypair;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
use deltachat::net::read_url_blob;
|
|
||||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
|
||||||
use deltachat::stock_str::StockMessage;
|
use deltachat::stock_str::StockMessage;
|
||||||
use deltachat::stock_str::StockStrings;
|
|
||||||
use deltachat::webxdc::StatusUpdateSerial;
|
use deltachat::webxdc::StatusUpdateSerial;
|
||||||
use deltachat::*;
|
use deltachat::*;
|
||||||
use deltachat::{accounts::Accounts, log::LogExt};
|
use deltachat::{accounts::Accounts, log::LogExt};
|
||||||
@@ -70,8 +66,6 @@ const DC_GCM_INFO_ONLY: u32 = 0x02;
|
|||||||
/// Struct representing the deltachat context.
|
/// Struct representing the deltachat context.
|
||||||
pub type dc_context_t = Context;
|
pub type dc_context_t = Context;
|
||||||
|
|
||||||
pub type dc_reactions_t = Reactions;
|
|
||||||
|
|
||||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||||
|
|
||||||
fn block_on<T>(fut: T) -> T::Output
|
fn block_on<T>(fut: T) -> T::Output
|
||||||
@@ -105,12 +99,11 @@ pub unsafe extern "C" fn dc_context_new(
|
|||||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||||
// generate random ID as this functionality is not yet available on the C-api.
|
// generate random ID as this functionality is not yet available on the C-api.
|
||||||
let id = rand::thread_rng().gen();
|
let id = rand::thread_rng().gen();
|
||||||
block_on(Context::new(
|
block_on(
|
||||||
as_path(dbfile),
|
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||||
id,
|
.with_id(id)
|
||||||
Events::new(),
|
.open(),
|
||||||
StockStrings::new(),
|
)
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!("blobdir can not be defined explicitly anymore");
|
eprintln!("blobdir can not be defined explicitly anymore");
|
||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
@@ -134,12 +127,11 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
|
|||||||
}
|
}
|
||||||
|
|
||||||
let id = rand::thread_rng().gen();
|
let id = rand::thread_rng().gen();
|
||||||
match block_on(Context::new_closed(
|
match block_on(
|
||||||
as_path(dbfile),
|
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||||
id,
|
.with_id(id)
|
||||||
Events::new(),
|
.build(),
|
||||||
StockStrings::new(),
|
) {
|
||||||
)) {
|
|
||||||
Ok(context) => Box::into_raw(Box::new(context)),
|
Ok(context) => Box::into_raw(Box::new(context)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("failed to create context: {err:#}");
|
eprintln!("failed to create context: {err:#}");
|
||||||
@@ -388,7 +380,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
block_on(async move { ctx.get_connectivity().await as u32 as libc::c_int })
|
block_on(ctx.get_connectivity()) as u32 as libc::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -411,6 +403,16 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc::c_int {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_get_push_state()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
block_on(ctx.push_state()) as libc::c_int
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
|
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
|
||||||
if context.is_null() {
|
if context.is_null() {
|
||||||
@@ -489,7 +491,7 @@ pub unsafe extern "C" fn dc_start_io(context: *mut dc_context_t) {
|
|||||||
if context.is_null() {
|
if context.is_null() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &mut *context;
|
||||||
|
|
||||||
block_on(ctx.start_io())
|
block_on(ctx.start_io())
|
||||||
}
|
}
|
||||||
@@ -557,8 +559,14 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
|||||||
EventType::SecurejoinJoinerProgress { .. } => 2061,
|
EventType::SecurejoinJoinerProgress { .. } => 2061,
|
||||||
EventType::ConnectivityChanged => 2100,
|
EventType::ConnectivityChanged => 2100,
|
||||||
EventType::SelfavatarChanged => 2110,
|
EventType::SelfavatarChanged => 2110,
|
||||||
|
EventType::ConfigSynced { .. } => 2111,
|
||||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||||
|
EventType::WebxdcRealtimeData { .. } => 2150,
|
||||||
|
EventType::AccountsBackgroundFetchDone => 2200,
|
||||||
|
EventType::ChatlistChanged => 2300,
|
||||||
|
EventType::ChatlistItemChanged { .. } => 2301,
|
||||||
|
EventType::EventChannelOverflow { .. } => 2400,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,8 +592,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::Error(_)
|
| EventType::Error(_)
|
||||||
| EventType::ConnectivityChanged
|
| EventType::ConnectivityChanged
|
||||||
| EventType::SelfavatarChanged
|
| EventType::SelfavatarChanged
|
||||||
|
| EventType::ConfigSynced { .. }
|
||||||
| EventType::IncomingMsgBunch { .. }
|
| EventType::IncomingMsgBunch { .. }
|
||||||
| EventType::ErrorSelfNotInGroup(_) => 0,
|
| EventType::ErrorSelfNotInGroup(_)
|
||||||
|
| EventType::AccountsBackgroundFetchDone => 0,
|
||||||
|
EventType::ChatlistChanged => 0,
|
||||||
EventType::MsgsChanged { chat_id, .. }
|
EventType::MsgsChanged { chat_id, .. }
|
||||||
| EventType::ReactionsChanged { chat_id, .. }
|
| EventType::ReactionsChanged { chat_id, .. }
|
||||||
| EventType::IncomingMsg { chat_id, .. }
|
| EventType::IncomingMsg { chat_id, .. }
|
||||||
@@ -608,8 +619,13 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
|
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
|
||||||
contact_id.to_u32() as libc::c_int
|
contact_id.to_u32() as libc::c_int
|
||||||
}
|
}
|
||||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
EventType::WebxdcRealtimeData { msg_id, .. }
|
||||||
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
| EventType::WebxdcStatusUpdate { 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,8 +660,13 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::ConnectivityChanged
|
| EventType::ConnectivityChanged
|
||||||
| EventType::WebxdcInstanceDeleted { .. }
|
| EventType::WebxdcInstanceDeleted { .. }
|
||||||
| EventType::IncomingMsgBunch { .. }
|
| EventType::IncomingMsgBunch { .. }
|
||||||
| EventType::SelfavatarChanged => 0,
|
| EventType::SelfavatarChanged
|
||||||
EventType::ChatModified(_) => 0,
|
| EventType::AccountsBackgroundFetchDone
|
||||||
|
| EventType::ChatlistChanged
|
||||||
|
| EventType::ChatlistItemChanged { .. }
|
||||||
|
| EventType::ConfigSynced { .. }
|
||||||
|
| EventType::ChatModified(_)
|
||||||
|
| EventType::EventChannelOverflow { .. } => 0,
|
||||||
EventType::MsgsChanged { msg_id, .. }
|
EventType::MsgsChanged { msg_id, .. }
|
||||||
| EventType::ReactionsChanged { msg_id, .. }
|
| EventType::ReactionsChanged { msg_id, .. }
|
||||||
| EventType::IncomingMsg { msg_id, .. }
|
| EventType::IncomingMsg { msg_id, .. }
|
||||||
@@ -660,6 +681,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
|||||||
status_update_serial,
|
status_update_serial,
|
||||||
..
|
..
|
||||||
} => status_update_serial.to_u32() as libc::c_int,
|
} => status_update_serial.to_u32() as libc::c_int,
|
||||||
|
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,7 +728,12 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
|||||||
| EventType::SelfavatarChanged
|
| EventType::SelfavatarChanged
|
||||||
| EventType::WebxdcStatusUpdate { .. }
|
| EventType::WebxdcStatusUpdate { .. }
|
||||||
| EventType::WebxdcInstanceDeleted { .. }
|
| EventType::WebxdcInstanceDeleted { .. }
|
||||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
| EventType::AccountsBackgroundFetchDone
|
||||||
|
| EventType::ChatEphemeralTimerModified { .. }
|
||||||
|
| EventType::IncomingMsgBunch { .. }
|
||||||
|
| EventType::ChatlistItemChanged { .. }
|
||||||
|
| EventType::ChatlistChanged
|
||||||
|
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||||
EventType::ConfigureProgress { comment, .. } => {
|
EventType::ConfigureProgress { comment, .. } => {
|
||||||
if let Some(comment) = comment {
|
if let Some(comment) = comment {
|
||||||
comment.to_c_string().unwrap_or_default().into_raw()
|
comment.to_c_string().unwrap_or_default().into_raw()
|
||||||
@@ -718,11 +745,15 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
|||||||
let data2 = file.to_c_string().unwrap_or_default();
|
let data2 = file.to_c_string().unwrap_or_default();
|
||||||
data2.into_raw()
|
data2.into_raw()
|
||||||
}
|
}
|
||||||
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
|
EventType::ConfigSynced { key } => {
|
||||||
.unwrap_or_default()
|
let data2 = key.to_string().to_c_string().unwrap_or_default();
|
||||||
.to_c_string()
|
data2.into_raw()
|
||||||
.unwrap_or_default()
|
}
|
||||||
.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,21 +844,12 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
block_on(async move {
|
let addr = to_string_lossy(addr);
|
||||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
let secret_data = to_string_lossy(secret_data);
|
||||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
block_on(preconfigure_keypair(ctx, &addr, &secret_data))
|
||||||
let public = secret.split_public_key()?;
|
.context("Failed to save keypair")
|
||||||
let keypair = key::KeyPair {
|
.log_err(ctx)
|
||||||
addr,
|
.is_ok() as libc::c_int
|
||||||
public,
|
|
||||||
secret,
|
|
||||||
};
|
|
||||||
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
|
|
||||||
Ok::<_, anyhow::Error>(1)
|
|
||||||
})
|
|
||||||
.context("Failed to save keypair")
|
|
||||||
.log_err(ctx)
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -1011,49 +1033,6 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_send_reaction(
|
|
||||||
context: *mut dc_context_t,
|
|
||||||
msg_id: u32,
|
|
||||||
reaction: *const libc::c_char,
|
|
||||||
) -> u32 {
|
|
||||||
if context.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_send_reaction()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
|
|
||||||
block_on(async move {
|
|
||||||
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
|
|
||||||
.await
|
|
||||||
.map(|msg_id| msg_id.to_u32())
|
|
||||||
.unwrap_or_log_default(ctx, "Failed to send reaction")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_get_msg_reactions(
|
|
||||||
context: *mut dc_context_t,
|
|
||||||
msg_id: u32,
|
|
||||||
) -> *mut dc_reactions_t {
|
|
||||||
if context.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_get_msg_reactions()");
|
|
||||||
return ptr::null_mut();
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
|
|
||||||
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
|
|
||||||
.context("failed dc_get_msg_reactions() call")
|
|
||||||
.log_err(ctx)
|
|
||||||
{
|
|
||||||
reactions
|
|
||||||
} else {
|
|
||||||
return ptr::null_mut();
|
|
||||||
};
|
|
||||||
|
|
||||||
Box::into_raw(Box::new(reactions))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -1097,6 +1076,43 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
|
|||||||
.strdup()
|
.strdup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_set_webxdc_integration(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
file: *const libc::c_char,
|
||||||
|
) {
|
||||||
|
if context.is_null() || file.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_set_webxdc_integration()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
block_on(ctx.set_webxdc_integration(&to_string_lossy(file)))
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_init_webxdc_integration(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
chat_id: u32,
|
||||||
|
) -> u32 {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_init_webxdc_integration()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
let chat_id = if chat_id == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ChatId::new(chat_id))
|
||||||
|
};
|
||||||
|
|
||||||
|
block_on(ctx.init_webxdc_integration(chat_id))
|
||||||
|
.log_err(ctx)
|
||||||
|
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_set_draft(
|
pub unsafe extern "C" fn dc_set_draft(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -1472,32 +1488,6 @@ pub unsafe extern "C" fn dc_get_next_media(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_set_chat_protection(
|
|
||||||
context: *mut dc_context_t,
|
|
||||||
chat_id: u32,
|
|
||||||
protect: libc::c_int,
|
|
||||||
) -> libc::c_int {
|
|
||||||
if context.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_set_chat_protection()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
|
|
||||||
s
|
|
||||||
} else {
|
|
||||||
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
block_on(async move {
|
|
||||||
match ChatId::new(chat_id).set_protection(ctx, protect).await {
|
|
||||||
Ok(()) => 1,
|
|
||||||
Err(_) => 0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_set_chat_visibility(
|
pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -2068,7 +2058,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
|
|||||||
);
|
);
|
||||||
message::Message::default()
|
message::Message::default()
|
||||||
} else {
|
} else {
|
||||||
error!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
|
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
|
||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3132,6 +3122,16 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
|
|||||||
ffi_chat.chat.is_protected() as libc::c_int
|
ffi_chat.chat.is_protected() as libc::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
|
||||||
|
if chat.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ffi_chat = &*chat;
|
||||||
|
ffi_chat.chat.is_protection_broken() as libc::c_int
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
||||||
if chat.is_null() {
|
if chat.is_null() {
|
||||||
@@ -3381,6 +3381,34 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
|||||||
.unwrap_or_else(|| "".strdup())
|
.unwrap_or_else(|| "".strdup())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_msg_save_file(
|
||||||
|
msg: *mut dc_msg_t,
|
||||||
|
path: *const libc::c_char,
|
||||||
|
) -> libc::c_int {
|
||||||
|
if msg.is_null() || path.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_msg_save_file()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ffi_msg = &*msg;
|
||||||
|
let ctx = &*ffi_msg.context;
|
||||||
|
let path = to_string_lossy(path);
|
||||||
|
let r = block_on(
|
||||||
|
ffi_msg
|
||||||
|
.message
|
||||||
|
.save_file(ctx, &std::path::PathBuf::from(path)),
|
||||||
|
);
|
||||||
|
match r {
|
||||||
|
Ok(()) => 1,
|
||||||
|
Err(_) => {
|
||||||
|
r.context("Failed to save file from message")
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or_default();
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
@@ -4138,27 +4166,26 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
|
|||||||
let ffi_contact = &*contact;
|
let ffi_contact = &*contact;
|
||||||
let ctx = &*ffi_contact.context;
|
let ctx = &*ffi_contact.context;
|
||||||
|
|
||||||
block_on(ffi_contact.contact.is_verified(ctx))
|
if block_on(ffi_contact.contact.is_verified(ctx))
|
||||||
.context("is_verified failed")
|
.context("is_verified failed")
|
||||||
.log_err(ctx)
|
.log_err(ctx)
|
||||||
.unwrap_or_default() as libc::c_int
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
// Return value is essentially a boolean,
|
||||||
|
// but we return 2 for true for backwards compatibility.
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_contact_get_verifier_addr(
|
pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::c_int {
|
||||||
contact: *mut dc_contact_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
if contact.is_null() {
|
if contact.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_contact_get_verifier_addr()");
|
eprintln!("ignoring careless call to dc_contact_is_bot()");
|
||||||
return "".strdup();
|
return 0;
|
||||||
}
|
}
|
||||||
let ffi_contact = &*contact;
|
(*contact).contact.is_bot() as libc::c_int
|
||||||
let ctx = &*ffi_contact.context;
|
|
||||||
block_on(ffi_contact.contact.get_verifier_addr(ctx))
|
|
||||||
.context("failed to get verifier for contact")
|
|
||||||
.log_err(ctx)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.strdup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -4257,45 +4284,6 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
|
|||||||
lot.get_timestamp()
|
lot.get_timestamp()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_reactions_get_contacts(
|
|
||||||
reactions: *mut dc_reactions_t,
|
|
||||||
) -> *mut dc_array::dc_array_t {
|
|
||||||
if reactions.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
|
|
||||||
return ptr::null_mut();
|
|
||||||
}
|
|
||||||
|
|
||||||
let reactions = &*reactions;
|
|
||||||
let array: dc_array_t = reactions.contacts().into();
|
|
||||||
|
|
||||||
Box::into_raw(Box::new(array))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
|
|
||||||
reactions: *mut dc_reactions_t,
|
|
||||||
contact_id: u32,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
if reactions.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
|
|
||||||
return ptr::null_mut();
|
|
||||||
}
|
|
||||||
|
|
||||||
let reactions = &*reactions;
|
|
||||||
reactions.get(ContactId::new(contact_id)).as_str().strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
|
|
||||||
if reactions.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_reactions_unref()");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(Box::from_raw(reactions));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||||
libc::free(s as *mut _)
|
libc::free(s as *mut _)
|
||||||
@@ -4496,19 +4484,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ResultNullableExt<T> {
|
|
||||||
fn into_raw(self) -> *mut T;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E> ResultNullableExt<T> for Result<T, E> {
|
|
||||||
fn into_raw(self) -> *mut T {
|
|
||||||
match self {
|
|
||||||
Ok(t) => Box::into_raw(Box::new(t)),
|
|
||||||
Err(_) => ptr::null_mut(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> Vec<MsgId> {
|
fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> Vec<MsgId> {
|
||||||
let ids = unsafe { std::slice::from_raw_parts(msg_ids, msg_cnt as usize) };
|
let ids = unsafe { std::slice::from_raw_parts(msg_ids, msg_cnt as usize) };
|
||||||
let msg_ids: Vec<MsgId> = ids
|
let msg_ids: Vec<MsgId> = ids
|
||||||
@@ -4632,96 +4607,6 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
|||||||
// this may change once we start localizing string.
|
// this may change once we start localizing string.
|
||||||
}
|
}
|
||||||
|
|
||||||
// dc_http_response_t
|
|
||||||
|
|
||||||
pub type dc_http_response_t = net::HttpResponse;
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_get_http_response(
|
|
||||||
context: *const dc_context_t,
|
|
||||||
url: *const libc::c_char,
|
|
||||||
) -> *mut dc_http_response_t {
|
|
||||||
if context.is_null() || url.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_get_http_response()");
|
|
||||||
return ptr::null_mut();
|
|
||||||
}
|
|
||||||
|
|
||||||
let context = &*context;
|
|
||||||
let url = to_string_lossy(url);
|
|
||||||
if let Ok(response) = block_on(read_url_blob(context, &url))
|
|
||||||
.context("read_url_blob")
|
|
||||||
.log_err(context)
|
|
||||||
{
|
|
||||||
Box::into_raw(Box::new(response))
|
|
||||||
} else {
|
|
||||||
ptr::null_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_http_response_get_mimetype(
|
|
||||||
response: *const dc_http_response_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
if response.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_http_response_get_mimetype()");
|
|
||||||
return ptr::null_mut();
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = &*response;
|
|
||||||
response.mimetype.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_http_response_get_encoding(
|
|
||||||
response: *const dc_http_response_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
if response.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_http_response_get_encoding()");
|
|
||||||
return ptr::null_mut();
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = &*response;
|
|
||||||
response.encoding.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_http_response_get_blob(
|
|
||||||
response: *const dc_http_response_t,
|
|
||||||
) -> *mut libc::c_char {
|
|
||||||
if response.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_http_response_get_blob()");
|
|
||||||
return ptr::null_mut();
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = &*response;
|
|
||||||
let blob_len = response.blob.len();
|
|
||||||
let ptr = libc::malloc(blob_len);
|
|
||||||
libc::memcpy(ptr, response.blob.as_ptr() as *mut libc::c_void, blob_len);
|
|
||||||
ptr as *mut libc::c_char
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_http_response_get_size(
|
|
||||||
response: *const dc_http_response_t,
|
|
||||||
) -> libc::size_t {
|
|
||||||
if response.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_http_response_get_size()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = &*response;
|
|
||||||
response.blob.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_http_response_unref(response: *mut dc_http_response_t) {
|
|
||||||
if response.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_http_response_unref()");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
drop(Box::from_raw(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Accounts
|
// -- Accounts
|
||||||
|
|
||||||
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
||||||
@@ -4750,17 +4635,17 @@ pub type dc_accounts_t = AccountsWrapper;
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_new(
|
pub unsafe extern "C" fn dc_accounts_new(
|
||||||
_os_name: *const libc::c_char,
|
dir: *const libc::c_char,
|
||||||
dbfile: *const libc::c_char,
|
writable: libc::c_int,
|
||||||
) -> *mut dc_accounts_t {
|
) -> *mut dc_accounts_t {
|
||||||
setup_panic!();
|
setup_panic!();
|
||||||
|
|
||||||
if dbfile.is_null() {
|
if dir.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_new()");
|
eprintln!("ignoring careless call to dc_accounts_new()");
|
||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
}
|
}
|
||||||
|
|
||||||
let accs = block_on(Accounts::new(as_path(dbfile).into()));
|
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
|
||||||
|
|
||||||
match accs {
|
match accs {
|
||||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||||
@@ -4954,16 +4839,6 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
|||||||
Box::into_raw(Box::new(array))
|
Box::into_raw(Box::new(array))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
|
|
||||||
if accounts.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_accounts_all_work_done()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let accounts = &*accounts;
|
|
||||||
block_on(async move { accounts.read().await.all_work_done().await as libc::c_int })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
@@ -4971,8 +4846,8 @@ pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts = &*accounts;
|
let accounts = &mut *accounts;
|
||||||
block_on(async move { accounts.read().await.start_io().await });
|
block_on(async move { accounts.write().await.start_io().await });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -5008,6 +4883,49 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
|||||||
block_on(async move { accounts.write().await.maybe_network_lost().await });
|
block_on(async move { accounts.write().await.maybe_network_lost().await });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||||
|
accounts: *mut dc_accounts_t,
|
||||||
|
timeout_in_seconds: u64,
|
||||||
|
) -> libc::c_int {
|
||||||
|
if accounts.is_null() || timeout_in_seconds <= 2 {
|
||||||
|
eprintln!("ignoring careless call to dc_accounts_background_fetch()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accounts = &*accounts;
|
||||||
|
block_on(async move {
|
||||||
|
let accounts = accounts.read().await;
|
||||||
|
accounts
|
||||||
|
.background_fetch(Duration::from_secs(timeout_in_seconds))
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||||
|
accounts: *mut dc_accounts_t,
|
||||||
|
token: *const libc::c_char,
|
||||||
|
) {
|
||||||
|
if accounts.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_accounts_set_push_device_token()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accounts = &*accounts;
|
||||||
|
let token = to_string_lossy(token);
|
||||||
|
|
||||||
|
block_on(async move {
|
||||||
|
let mut accounts = accounts.write().await;
|
||||||
|
if let Err(err) = accounts.set_push_device_token(&token).await {
|
||||||
|
accounts.emit_event(EventType::Error(format!(
|
||||||
|
"Failed to set notify token: {err:#}."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *mut dc_accounts_t,
|
||||||
@@ -5045,7 +4963,9 @@ mod jsonrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let account_manager = &*account_manager;
|
let account_manager = &*account_manager;
|
||||||
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
|
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
||||||
|
account_manager.inner.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
let (request_handle, receiver) = RpcClient::new();
|
let (request_handle, receiver) = RpcClient::new();
|
||||||
let handle = RpcSession::new(request_handle, cmd_api);
|
let handle = RpcSession::new(request_handle, cmd_api);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "1.125.0"
|
version = "1.140.0"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "deltachat-jsonrpc-server"
|
default-run = "deltachat-jsonrpc-server"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "deltachat-jsonrpc-server"
|
name = "deltachat-jsonrpc-server"
|
||||||
@@ -14,27 +15,28 @@ required-features = ["webserver"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
deltachat = { path = ".." }
|
deltachat = { path = ".." }
|
||||||
|
deltachat-contact-tools = { path = "../deltachat-contact-tools" }
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
schemars = "0.8.11"
|
schemars = "0.8.19"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tempfile = "3.6.0"
|
tempfile = "3.10.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
async-channel = { version = "1.8.0" }
|
async-channel = { version = "2.2.1" }
|
||||||
futures = { version = "0.3.28" }
|
futures = { version = "0.3.30" }
|
||||||
serde_json = "1.0.99"
|
serde_json = "1"
|
||||||
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
|
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||||
tokio = { version = "1.29.1" }
|
tokio = { version = "1.37.0" }
|
||||||
sanitize-filename = "0.4"
|
sanitize-filename = "0.5"
|
||||||
walkdir = "2.3.3"
|
walkdir = "2.5.0"
|
||||||
base64 = "0.21"
|
base64 = "0.22"
|
||||||
|
|
||||||
# optional dependencies
|
# optional dependencies
|
||||||
axum = { version = "0.6.18", optional = true, features = ["ws"] }
|
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||||
env_logger = { version = "0.10.0", optional = true }
|
env_logger = { version = "0.11.3", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
|
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ This will build the `deltachat-jsonrpc-server` binary and then run a test suite
|
|||||||
|
|
||||||
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
|
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
|
||||||
|
|
||||||
Then, set the `DCC_NEW_TMP_EMAIL` environment variable to your mailadm token before running the tests.
|
Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
|
||||||
|
|
||||||
```
|
```
|
||||||
DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=yourtoken npm run test
|
CHATMAIL_DOMAIN=chat.example.org npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Test Coverage
|
#### Test Coverage
|
||||||
|
|||||||
@@ -1,33 +1,40 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||||
pub use deltachat::accounts::Accounts;
|
pub use deltachat::accounts::Accounts;
|
||||||
use deltachat::message::get_msg_read_receipts;
|
use deltachat::chat::{
|
||||||
use deltachat::qr::Qr;
|
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||||
use deltachat::{
|
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||||
chat::{
|
ProtectionStatus,
|
||||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
|
||||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
|
||||||
ProtectionStatus,
|
|
||||||
},
|
|
||||||
chatlist::Chatlist,
|
|
||||||
config::Config,
|
|
||||||
constants::DC_MSG_ID_DAYMARKER,
|
|
||||||
contact::{may_be_valid_addr, Contact, ContactId, Origin},
|
|
||||||
context::get_info,
|
|
||||||
ephemeral::Timer,
|
|
||||||
imex, location,
|
|
||||||
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
|
|
||||||
provider::get_provider_info,
|
|
||||||
qr,
|
|
||||||
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
|
|
||||||
reaction::{get_msg_reactions, send_reaction},
|
|
||||||
securejoin,
|
|
||||||
stock_str::StockMessage,
|
|
||||||
webxdc::StatusUpdateSerial,
|
|
||||||
};
|
};
|
||||||
|
use deltachat::chatlist::Chatlist;
|
||||||
|
use deltachat::config::Config;
|
||||||
|
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::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};
|
||||||
|
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||||
|
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 sanitize_filename::is_sanitized;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::sync::{watch, Mutex, RwLock};
|
use tokio::sync::{watch, Mutex, RwLock};
|
||||||
@@ -39,7 +46,7 @@ pub mod types;
|
|||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use types::account::Account;
|
use types::account::Account;
|
||||||
use types::chat::FullChat;
|
use types::chat::FullChat;
|
||||||
use types::contact::ContactObject;
|
use types::contact::{ContactObject, VcardContact};
|
||||||
use types::events::Event;
|
use types::events::Event;
|
||||||
use types::http::HttpResponse;
|
use types::http::HttpResponse;
|
||||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||||
@@ -47,7 +54,7 @@ use types::provider_info::ProviderInfo;
|
|||||||
use types::reactions::JSONRPCReactions;
|
use types::reactions::JSONRPCReactions;
|
||||||
use types::webxdc::WebxdcMessageInfo;
|
use types::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
use self::types::message::MessageLoadResult;
|
use self::types::message::{MessageInfo, MessageLoadResult};
|
||||||
use self::types::{
|
use self::types::{
|
||||||
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
||||||
location::JsonrpcLocation,
|
location::JsonrpcLocation,
|
||||||
@@ -62,14 +69,14 @@ use crate::api::types::qr::QrObject;
|
|||||||
struct AccountState {
|
struct AccountState {
|
||||||
/// The Qr code for current [`CommandApi::provide_backup`] call.
|
/// The Qr code for current [`CommandApi::provide_backup`] call.
|
||||||
///
|
///
|
||||||
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
|
/// If there is currently is a call to [`CommandApi::provide_backup`] this will be
|
||||||
/// `Pending` or `Ready`, otherwise `NoProvider`.
|
/// `Some`, otherwise `None`.
|
||||||
backup_provider_qr: watch::Sender<ProviderQr>,
|
backup_provider_qr: watch::Sender<Option<Qr>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AccountState {
|
impl Default for AccountState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
|
let tx = watch::Sender::new(None);
|
||||||
Self {
|
Self {
|
||||||
backup_provider_qr: tx,
|
backup_provider_qr: tx,
|
||||||
}
|
}
|
||||||
@@ -80,21 +87,30 @@ impl Default for AccountState {
|
|||||||
pub struct CommandApi {
|
pub struct CommandApi {
|
||||||
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
||||||
|
|
||||||
|
/// Receiver side of the event channel.
|
||||||
|
///
|
||||||
|
/// Events from it can be received by calling `get_next_event` method.
|
||||||
|
event_emitter: Arc<EventEmitter>,
|
||||||
|
|
||||||
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandApi {
|
impl CommandApi {
|
||||||
pub fn new(accounts: Accounts) -> Self {
|
pub fn new(accounts: Accounts) -> Self {
|
||||||
|
let event_emitter = Arc::new(accounts.get_event_emitter());
|
||||||
CommandApi {
|
CommandApi {
|
||||||
accounts: Arc::new(RwLock::new(accounts)),
|
accounts: Arc::new(RwLock::new(accounts)),
|
||||||
|
event_emitter,
|
||||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
pub async fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||||
|
let event_emitter = Arc::new(accounts.read().await.get_event_emitter());
|
||||||
CommandApi {
|
CommandApi {
|
||||||
accounts,
|
accounts,
|
||||||
|
event_emitter,
|
||||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,30 +139,18 @@ impl CommandApi {
|
|||||||
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
|
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let val: ProviderQr = receiver.borrow_and_update().clone();
|
loop {
|
||||||
match val {
|
if let Some(qr) = receiver.borrow_and_update().clone() {
|
||||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
return Ok(qr);
|
||||||
ProviderQr::Pending => loop {
|
}
|
||||||
if receiver.changed().await.is_err() {
|
if receiver.changed().await.is_err() {
|
||||||
bail!("No backup being provided (account state dropped)");
|
bail!("No backup being provided (account state dropped)");
|
||||||
}
|
}
|
||||||
let val: ProviderQr = receiver.borrow().clone();
|
|
||||||
match val {
|
|
||||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
|
||||||
ProviderQr::Pending => continue,
|
|
||||||
ProviderQr::Ready(qr) => break Ok(qr),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
ProviderQr::Ready(qr) => Ok(qr),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rpc(
|
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||||
all_positional,
|
|
||||||
ts_outdir = "typescript/generated",
|
|
||||||
openrpc_outdir = "openrpc"
|
|
||||||
)]
|
|
||||||
impl CommandApi {
|
impl CommandApi {
|
||||||
/// Test function.
|
/// Test function.
|
||||||
async fn sleep(&self, delay: f64) {
|
async fn sleep(&self, delay: f64) {
|
||||||
@@ -157,20 +161,19 @@ impl CommandApi {
|
|||||||
// Misc top level functions
|
// Misc top level functions
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
/// Check if an email address is valid.
|
/// Checks if an email address is valid.
|
||||||
async fn check_email_validity(&self, email: String) -> bool {
|
async fn check_email_validity(&self, email: String) -> bool {
|
||||||
may_be_valid_addr(&email)
|
may_be_valid_addr(&email)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get general system info.
|
/// Returns general system info.
|
||||||
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
|
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
|
||||||
get_info()
|
get_info()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the next event.
|
/// Get the next event.
|
||||||
async fn get_next_event(&self) -> Result<Event> {
|
async fn get_next_event(&self) -> Result<Event> {
|
||||||
let event_emitter = self.accounts.read().await.get_event_emitter();
|
self.event_emitter
|
||||||
event_emitter
|
|
||||||
.recv()
|
.recv()
|
||||||
.await
|
.await
|
||||||
.map(|event| event.into())
|
.map(|event| event.into())
|
||||||
@@ -185,6 +188,16 @@ impl CommandApi {
|
|||||||
self.accounts.write().await.add_account().await
|
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<()> {
|
async fn remove_account(&self, account_id: u32) -> Result<()> {
|
||||||
self.accounts
|
self.accounts
|
||||||
.write()
|
.write()
|
||||||
@@ -223,13 +236,29 @@ impl CommandApi {
|
|||||||
Ok(accounts)
|
Ok(accounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts background tasks for all accounts.
|
||||||
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
||||||
self.accounts.read().await.start_io().await;
|
self.accounts.write().await.start_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stops background tasks for all accounts.
|
||||||
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
||||||
self.accounts.read().await.stop_io().await;
|
self.accounts.write().await.stop_io().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||||
|
///
|
||||||
|
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
|
||||||
|
/// Process all events until you get this one and you can safely return to the background
|
||||||
|
/// without forgetting to create notifications caused by timing race conditions.
|
||||||
|
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||||
|
self.accounts
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||||
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,14 +266,16 @@ impl CommandApi {
|
|||||||
// Methods that work on individual accounts
|
// Methods that work on individual accounts
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
async fn start_io(&self, id: u32) -> Result<()> {
|
/// Starts background tasks for a single account.
|
||||||
let ctx = self.get_context(id).await?;
|
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.start_io().await;
|
ctx.start_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_io(&self, id: u32) -> Result<()> {
|
/// Stops background tasks for a single account.
|
||||||
let ctx = self.get_context(id).await?;
|
async fn stop_io(&self, account_id: u32) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.stop_io().await;
|
ctx.stop_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -311,11 +342,23 @@ impl CommandApi {
|
|||||||
ctx.get_info().await
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the given configuration key.
|
||||||
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
set_config(&ctx, &key, value.as_deref()).await
|
set_config(&ctx, &key, value.as_deref()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates a batch of configuration values.
|
||||||
async fn batch_set_config(
|
async fn batch_set_config(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -347,6 +390,7 @@ impl CommandApi {
|
|||||||
Ok(qr_object)
|
Ok(qr_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns configuration value for the given key.
|
||||||
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
get_config(&ctx, &key).await
|
get_config(&ctx, &key).await
|
||||||
@@ -675,8 +719,7 @@ impl CommandApi {
|
|||||||
/// the Verified-Group-Invite protocol is offered in the QR code;
|
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||||
/// works for protected groups as well as for normal groups.
|
/// works for protected groups as well as for normal groups.
|
||||||
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
||||||
/// See https://countermitm.readthedocs.io/en/latest/new.html
|
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||||
/// for details about both protocols.
|
|
||||||
///
|
///
|
||||||
/// return format: `[code, svg]`
|
/// return format: `[code, svg]`
|
||||||
async fn get_chat_securejoin_qr_code_svg(
|
async fn get_chat_securejoin_qr_code_svg(
|
||||||
@@ -704,8 +747,7 @@ impl CommandApi {
|
|||||||
///
|
///
|
||||||
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||||
///
|
///
|
||||||
/// See https://countermitm.readthedocs.io/en/latest/new.html
|
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||||
/// for details about both protocols.
|
|
||||||
///
|
///
|
||||||
/// **qr**: The text of the scanned QR code. Typically, the same string as given
|
/// **qr**: The text of the scanned QR code. Typically, the same string as given
|
||||||
/// to `check_qr()`.
|
/// to `check_qr()`.
|
||||||
@@ -812,24 +854,12 @@ impl CommandApi {
|
|||||||
/// Create a new broadcast list.
|
/// Create a new broadcast list.
|
||||||
///
|
///
|
||||||
/// Broadcast lists are similar to groups on the sending device,
|
/// Broadcast lists are similar to groups on the sending device,
|
||||||
/// however, recipients get the messages in normal one-to-one chats
|
/// however, recipients get the messages in a read-only chat
|
||||||
/// and will not be aware of other members.
|
/// and will see who the other members are.
|
||||||
///
|
///
|
||||||
/// Replies to broadcasts go only to the sender
|
/// For historical reasons, this function does not take a name directly,
|
||||||
/// and not to all broadcast recipients.
|
/// instead you have to set the name using dc_set_chat_name()
|
||||||
/// Moreover, replies will not appear in the broadcast list
|
/// after creating the broadcast list.
|
||||||
/// but in the one-to-one chat with the person answering.
|
|
||||||
///
|
|
||||||
/// The name and the image of the broadcast list is set automatically
|
|
||||||
/// and is visible to the sender only.
|
|
||||||
/// Not asking for these data allows more focused creation
|
|
||||||
/// and we bypass the question who will get which data.
|
|
||||||
/// Also, many users will have at most one broadcast list
|
|
||||||
/// so, a generic name and image is sufficient at the first place.
|
|
||||||
///
|
|
||||||
/// Later on, however, the name can be changed using dc_set_chat_name().
|
|
||||||
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
|
||||||
/// All in all, this is also what other messengers are doing here.
|
|
||||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
chat::create_broadcast_list(&ctx)
|
chat::create_broadcast_list(&ctx)
|
||||||
@@ -905,19 +935,35 @@ impl CommandApi {
|
|||||||
.to_u32())
|
.to_u32())
|
||||||
}
|
}
|
||||||
|
|
||||||
// for now only text messages, because we only used text messages in desktop thusfar
|
/// Add a message to the device-chat.
|
||||||
|
/// Device-messages usually contain update information
|
||||||
|
/// and some hints that are added during the program runs, multi-device etc.
|
||||||
|
/// The device-message may be defined by a label;
|
||||||
|
/// if a message with the same label was added or skipped before,
|
||||||
|
/// the message is not added again, even if the message was deleted in between.
|
||||||
|
/// If needed, the device-chat is created before.
|
||||||
|
///
|
||||||
|
/// Sends the `MsgsChanged` event on success.
|
||||||
|
///
|
||||||
|
/// Setting msg to None will prevent the device message with this label from being added in the future.
|
||||||
async fn add_device_message(
|
async fn add_device_message(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
label: String,
|
label: String,
|
||||||
text: String,
|
msg: Option<MessageData>,
|
||||||
) -> Result<u32> {
|
) -> Result<Option<u32>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
if let Some(msg) = msg {
|
||||||
msg.set_text(text);
|
let mut message = msg.create_message(&ctx).await?;
|
||||||
let message_id =
|
let message_id =
|
||||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut message)).await?;
|
||||||
Ok(message_id.to_u32())
|
if !message_id.is_unset() {
|
||||||
|
return Ok(Some(message_id.to_u32()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deltachat::chat::add_device_msg(&ctx, Some(&label), None).await?;
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark all messages in a chat as _noticed_.
|
/// Mark all messages in a chat as _noticed_.
|
||||||
@@ -1071,9 +1117,12 @@ impl CommandApi {
|
|||||||
.collect::<Vec<JSONRPCMessageListItem>>())
|
.collect::<Vec<JSONRPCMessageListItem>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
|
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
MessageObject::from_message_id(&ctx, message_id).await
|
let msg_id = MsgId::new(msg_id);
|
||||||
|
MessageObject::from_msg_id(&ctx, msg_id)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
||||||
@@ -1093,7 +1142,7 @@ impl CommandApi {
|
|||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
|
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
|
||||||
for message_id in message_ids {
|
for message_id in message_ids {
|
||||||
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
|
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
|
||||||
messages.insert(
|
messages.insert(
|
||||||
message_id,
|
message_id,
|
||||||
match message_result {
|
match message_result {
|
||||||
@@ -1135,6 +1184,16 @@ impl CommandApi {
|
|||||||
MsgId::new(message_id).get_info(&ctx).await
|
MsgId::new(message_id).get_info(&ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns additional information for single message.
|
||||||
|
async fn get_message_info_object(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
message_id: u32,
|
||||||
|
) -> Result<MessageInfo> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
MessageInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns contacts that sent read receipts and the time of reading.
|
/// Returns contacts that sent read receipts and the time of reading.
|
||||||
async fn get_message_read_receipts(
|
async fn get_message_read_receipts(
|
||||||
&self,
|
&self,
|
||||||
@@ -1386,10 +1445,54 @@ impl CommandApi {
|
|||||||
Ok(contact_id.map(|id| id.to_u32()))
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// chat
|
// chat
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
|
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||||
|
///
|
||||||
|
/// If it does not exist, `None` is returned.
|
||||||
|
async fn get_chat_id_by_contact_id(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
contact_id: u32,
|
||||||
|
) -> Result<Option<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let chat_id = ChatId::lookup_by_contact(&ctx, ContactId::new(contact_id)).await?;
|
||||||
|
Ok(chat_id.map(|id| id.to_u32()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns all message IDs of the given types in a chat.
|
/// Returns all message IDs of the given types in a chat.
|
||||||
/// Typically used to show a gallery.
|
/// Typically used to show a gallery.
|
||||||
///
|
///
|
||||||
@@ -1517,20 +1620,21 @@ impl CommandApi {
|
|||||||
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
||||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
self.with_state(account_id, |state| {
|
|
||||||
state.backup_provider_qr.send_replace(ProviderQr::Pending);
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let provider = imex::BackupProvider::prepare(&ctx).await?;
|
let provider = imex::BackupProvider::prepare(&ctx).await?;
|
||||||
self.with_state(account_id, |state| {
|
self.with_state(account_id, |state| {
|
||||||
state
|
state.backup_provider_qr.send_replace(Some(provider.qr()));
|
||||||
.backup_provider_qr
|
|
||||||
.send_replace(ProviderQr::Ready(provider.qr()));
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
provider.await
|
let res = provider.await;
|
||||||
|
|
||||||
|
self.with_state(account_id, |state| {
|
||||||
|
state.backup_provider_qr.send_replace(None);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
|
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
|
||||||
@@ -1538,11 +1642,17 @@ impl CommandApi {
|
|||||||
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
|
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
|
||||||
/// retrieve the backup and setup this second device.
|
/// retrieve the backup and setup this second device.
|
||||||
///
|
///
|
||||||
/// This call will fail if there is currently no concurrent call to
|
/// This call will block until the QR code is ready,
|
||||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||||
/// ready.
|
/// but will fail after 10 seconds to avoid deadlocks.
|
||||||
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
||||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
let qr = tokio::time::timeout(
|
||||||
|
Duration::from_secs(10),
|
||||||
|
self.inner_get_backup_qr(account_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Backup provider did not start in time")?
|
||||||
|
.context("Failed to get backup QR code")?;
|
||||||
qr::format_backup(&qr)
|
qr::format_backup(&qr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1551,14 +1661,20 @@ impl CommandApi {
|
|||||||
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
|
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
|
||||||
/// retrieve the backup and setup this second device.
|
/// retrieve the backup and setup this second device.
|
||||||
///
|
///
|
||||||
/// This call will fail if there is currently no concurrent call to
|
/// This call will block until the QR code is ready,
|
||||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||||
/// ready.
|
/// but will fail after 10 seconds to avoid deadlocks.
|
||||||
///
|
///
|
||||||
/// Returns the QR code rendered as an SVG image.
|
/// Returns the QR code rendered as an SVG image.
|
||||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
let qr = tokio::time::timeout(
|
||||||
|
Duration::from_secs(10),
|
||||||
|
self.inner_get_backup_qr(account_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Backup provider did not start in time")?
|
||||||
|
.context("Failed to get backup QR code")?;
|
||||||
generate_backup_qr(&ctx, &qr).await
|
generate_backup_qr(&ctx, &qr).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1568,6 +1684,9 @@ impl CommandApi {
|
|||||||
/// the current device.
|
/// the current device.
|
||||||
///
|
///
|
||||||
/// Can be cancelled by stopping the ongoing process.
|
/// Can be cancelled by stopping the ongoing process.
|
||||||
|
///
|
||||||
|
/// Do not forget to call start_io on the account after a successful import,
|
||||||
|
/// otherwise it will not connect to the email server.
|
||||||
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
|
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let qr = qr::check_qr(&ctx, &qr_text).await?;
|
let qr = qr::check_qr(&ctx, &qr_text).await?;
|
||||||
@@ -1661,6 +1780,37 @@ impl CommandApi {
|
|||||||
.await
|
.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(
|
async fn get_webxdc_status_updates(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1702,6 +1852,29 @@ impl CommandApi {
|
|||||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets Webxdc file as integration.
|
||||||
|
/// `file` is the .xdc to use as Webxdc integration.
|
||||||
|
async fn set_webxdc_integration(&self, account_id: u32, file_path: String) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
ctx.set_webxdc_integration(&file_path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns Webxdc instance used for optional integrations.
|
||||||
|
/// UI can open the Webxdc as usual.
|
||||||
|
/// Returns `None` if there is no integration; the caller can add one using `set_webxdc_integration` then.
|
||||||
|
/// `integrate_for` is the chat to get the integration for.
|
||||||
|
async fn init_webxdc_integration(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
chat_id: Option<u32>,
|
||||||
|
) -> Result<Option<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
Ok(ctx
|
||||||
|
.init_webxdc_integration(chat_id.map(ChatId::new))
|
||||||
|
.await?
|
||||||
|
.map(|msg_id| msg_id.to_u32()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Makes an HTTP GET request and returns a response.
|
/// Makes an HTTP GET request and returns a response.
|
||||||
///
|
///
|
||||||
/// `url` is the HTTP or HTTPS URL.
|
/// `url` is the HTTP or HTTPS URL.
|
||||||
@@ -1753,6 +1926,9 @@ impl CommandApi {
|
|||||||
let mut msg = Message::new(Viewtype::Sticker);
|
let mut msg = Message::new(Viewtype::Sticker);
|
||||||
msg.set_file(&sticker_path, None);
|
msg.set_file(&sticker_path, None);
|
||||||
|
|
||||||
|
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
||||||
|
msg.force_sticker();
|
||||||
|
|
||||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
}
|
}
|
||||||
@@ -1791,38 +1967,7 @@ impl CommandApi {
|
|||||||
|
|
||||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
|
let mut message = data.create_message(&ctx).await?;
|
||||||
viewtype.into()
|
|
||||||
} else if data.file.is_some() {
|
|
||||||
Viewtype::File
|
|
||||||
} else {
|
|
||||||
Viewtype::Text
|
|
||||||
});
|
|
||||||
message.set_text(data.text.unwrap_or_default());
|
|
||||||
if data.html.is_some() {
|
|
||||||
message.set_html(data.html);
|
|
||||||
}
|
|
||||||
if data.override_sender_name.is_some() {
|
|
||||||
message.set_override_sender_name(data.override_sender_name);
|
|
||||||
}
|
|
||||||
if let Some(file) = data.file {
|
|
||||||
message.set_file(file, None);
|
|
||||||
}
|
|
||||||
if let Some((latitude, longitude)) = data.location {
|
|
||||||
message.set_location(latitude, longitude);
|
|
||||||
}
|
|
||||||
if let Some(id) = data.quoted_message_id {
|
|
||||||
message
|
|
||||||
.set_quote(
|
|
||||||
&ctx,
|
|
||||||
Some(
|
|
||||||
&Message::load_from_db(&ctx, MsgId::new(id))
|
|
||||||
.await
|
|
||||||
.context("message to quote could not be loaded")?,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||||
.await?
|
.await?
|
||||||
.to_u32();
|
.to_u32();
|
||||||
@@ -1838,6 +1983,15 @@ impl CommandApi {
|
|||||||
Ok(can_send)
|
Ok(can_send)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saves a file copy at the user-provided path.
|
||||||
|
///
|
||||||
|
/// Fails if file already exists at the provided path.
|
||||||
|
async fn save_msg_file(&self, account_id: u32, msg_id: u32, path: String) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||||
|
message.save_file(&ctx, Path::new(&path)).await
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// functions for the composer
|
// functions for the composer
|
||||||
// the composer is the message input field
|
// the composer is the message input field
|
||||||
@@ -1886,7 +2040,7 @@ impl CommandApi {
|
|||||||
.context("path conversion to string failed")
|
.context("path conversion to string failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// save a sticker to a collection/folder in the account's sticker folder
|
/// Saves a sticker to a collection/folder in the account's sticker folder.
|
||||||
async fn misc_save_sticker(
|
async fn misc_save_sticker(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1910,19 +2064,21 @@ impl CommandApi {
|
|||||||
);
|
);
|
||||||
let destination_path = account_folder.join("stickers").join(collection);
|
let destination_path = account_folder.join("stickers").join(collection);
|
||||||
fs::create_dir_all(&destination_path).await?;
|
fs::create_dir_all(&destination_path).await?;
|
||||||
let file = message.get_file(&ctx).context("no file")?;
|
let file = message.get_filename().context("no file?")?;
|
||||||
fs::copy(
|
message
|
||||||
&file,
|
.save_file(
|
||||||
destination_path.join(format!(
|
&ctx,
|
||||||
"{}.{}",
|
&destination_path.join(format!(
|
||||||
msg_id,
|
"{}.{}",
|
||||||
file.extension()
|
msg_id,
|
||||||
.unwrap_or_default()
|
Path::new(&file)
|
||||||
.to_str()
|
.extension()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
)),
|
.to_str()
|
||||||
)
|
.unwrap_or_default()
|
||||||
.await?;
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2021,11 +2177,9 @@ impl CommandApi {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
|
||||||
.await?
|
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
|
||||||
.to_u32();
|
Ok((msg_id.to_u32(), message))
|
||||||
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
|
|
||||||
Ok((msg_id, message))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
|
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
|
||||||
@@ -2039,13 +2193,19 @@ impl CommandApi {
|
|||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
quoted_message_id: Option<u32>,
|
quoted_message_id: Option<u32>,
|
||||||
|
view_type: Option<MessageViewtype>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut draft = Message::new(if file.is_some() {
|
let mut draft = Message::new(view_type.map_or_else(
|
||||||
Viewtype::File
|
|| {
|
||||||
} else {
|
if file.is_some() {
|
||||||
Viewtype::Text
|
Viewtype::File
|
||||||
});
|
} else {
|
||||||
|
Viewtype::Text
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|v| v.into(),
|
||||||
|
));
|
||||||
draft.set_text(text.unwrap_or_default());
|
draft.set_text(text.unwrap_or_default());
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
draft.set_file(file, None);
|
draft.set_file(file, None);
|
||||||
@@ -2065,6 +2225,23 @@ impl CommandApi {
|
|||||||
|
|
||||||
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send the chat's current set draft
|
||||||
|
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
|
||||||
|
let mut draft = draft;
|
||||||
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
|
||||||
|
.await?
|
||||||
|
.to_u32();
|
||||||
|
Ok(msg_id)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"chat with id {} doesn't have draft message",
|
||||||
|
chat_id
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions (to prevent code duplication)
|
// Helper functions (to prevent code duplication)
|
||||||
@@ -2081,13 +2258,6 @@ async fn set_config(
|
|||||||
value,
|
value,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match key {
|
|
||||||
"sentbox_watch" | "mvbox_move" | "only_fetch_mvbox" => {
|
|
||||||
ctx.restart_io_if_running().await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2103,15 +2273,3 @@ async fn get_config(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a QR code for a BackupProvider is currently available.
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
enum ProviderQr {
|
|
||||||
/// There is no provider, asking for a QR is an error.
|
|
||||||
NoProvider,
|
|
||||||
/// There is a provider, the QR code is pending.
|
|
||||||
Pending,
|
|
||||||
/// There is a provider and QR code.
|
|
||||||
Ready(Qr),
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
|
|||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "kind")]
|
||||||
pub enum Account {
|
pub enum Account {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Configured {
|
Configured {
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ use super::contact::ContactObject;
|
|||||||
pub struct FullChat {
|
pub struct FullChat {
|
||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
|
/// True if the chat is protected.
|
||||||
|
///
|
||||||
|
/// UI should display a green checkmark
|
||||||
|
/// in the chat title,
|
||||||
|
/// in the chat profile title and
|
||||||
|
/// in the chatlist item
|
||||||
|
/// if chat protection is enabled.
|
||||||
|
/// UI should also display a green checkmark
|
||||||
|
/// in the contact profile
|
||||||
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
@@ -31,6 +42,7 @@ pub struct FullChat {
|
|||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
// is_group - please check over chat.type in frontend instead
|
// is_group - please check over chat.type in frontend instead
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
is_protection_broken: bool,
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
self_in_group: bool,
|
self_in_group: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
@@ -73,7 +85,7 @@ impl FullChat {
|
|||||||
let can_send = chat.can_send(context).await?;
|
let can_send = chat.can_send(context).await?;
|
||||||
|
|
||||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||||
match contact_ids.get(0) {
|
match contact_ids.first() {
|
||||||
Some(contact) => Contact::get_by_id(context, *contact)
|
Some(contact) => Contact::get_by_id(context, *contact)
|
||||||
.await
|
.await
|
||||||
.context("failed to load contact for was_seen_recently")?
|
.context("failed to load contact for was_seen_recently")?
|
||||||
@@ -100,6 +112,7 @@ impl FullChat {
|
|||||||
color,
|
color,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_protection_broken: chat.is_protection_broken(),
|
||||||
is_device_chat: chat.is_device_talk(),
|
is_device_chat: chat.is_device_talk(),
|
||||||
self_in_group: contact_ids.contains(&ContactId::SELF),
|
self_in_group: contact_ids.contains(&ContactId::SELF),
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
@@ -126,6 +139,17 @@ impl FullChat {
|
|||||||
pub struct BasicChat {
|
pub struct BasicChat {
|
||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
|
/// True if the chat is protected.
|
||||||
|
///
|
||||||
|
/// UI should display a green checkmark
|
||||||
|
/// in the chat title,
|
||||||
|
/// in the chat profile title and
|
||||||
|
/// in the chatlist item
|
||||||
|
/// if chat protection is enabled.
|
||||||
|
/// UI should also display a green checkmark
|
||||||
|
/// in the contact profile
|
||||||
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
@@ -134,6 +158,7 @@ pub struct BasicChat {
|
|||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
color: String,
|
color: String,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
is_protection_broken: bool,
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
}
|
}
|
||||||
@@ -160,6 +185,7 @@ impl BasicChat {
|
|||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
color,
|
color,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_protection_broken: chat.is_protection_broken(),
|
||||||
is_device_chat: chat.is_device_talk(),
|
is_device_chat: chat.is_device_talk(),
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
})
|
})
|
||||||
@@ -167,10 +193,11 @@ impl BasicChat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
pub enum MuteDuration {
|
pub enum MuteDuration {
|
||||||
NotMuted,
|
NotMuted,
|
||||||
Forever,
|
Forever,
|
||||||
Until(i64),
|
Until { duration: i64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MuteDuration {
|
impl MuteDuration {
|
||||||
@@ -178,13 +205,13 @@ impl MuteDuration {
|
|||||||
match self {
|
match self {
|
||||||
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
||||||
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
||||||
MuteDuration::Until(n) => {
|
MuteDuration::Until { duration } => {
|
||||||
if n <= 0 {
|
if duration <= 0 {
|
||||||
bail!("failed to read mute duration")
|
bail!("failed to read mute duration")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SystemTime::now()
|
Ok(SystemTime::now()
|
||||||
.checked_add(Duration::from_secs(n as u64))
|
.checked_add(Duration::from_secs(duration as u64))
|
||||||
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use super::color_int_to_hex_string;
|
|||||||
use super::message::MessageViewtype;
|
use super::message::MessageViewtype;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "kind")]
|
||||||
pub enum ChatListItemFetchResult {
|
pub enum ChatListItemFetchResult {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatListItem {
|
ChatListItem {
|
||||||
@@ -102,7 +102,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
||||||
|
|
||||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||||
let contact = chat_contacts.get(0);
|
let contact = chat_contacts.first();
|
||||||
let was_seen_recently = match contact {
|
let was_seen_recently = match contact {
|
||||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use deltachat::contact::VerifiedStatus;
|
use deltachat::color;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
@@ -19,14 +19,36 @@ pub struct ContactObject {
|
|||||||
profile_image: Option<String>, // BLOBS
|
profile_image: Option<String>, // BLOBS
|
||||||
name_and_addr: String,
|
name_and_addr: String,
|
||||||
is_blocked: bool,
|
is_blocked: bool,
|
||||||
|
|
||||||
|
/// True if the contact can be added to verified groups.
|
||||||
|
///
|
||||||
|
/// If this is true
|
||||||
|
/// UI should display green checkmark after the contact name
|
||||||
|
/// in contact list items,
|
||||||
|
/// in chat member list items
|
||||||
|
/// and in profiles if no chat with the contact exist.
|
||||||
is_verified: bool,
|
is_verified: bool,
|
||||||
/// the address that verified this contact
|
|
||||||
verifier_addr: Option<String>,
|
/// True if the contact profile title should have a green checkmark.
|
||||||
/// the id of the contact that verified this contact
|
///
|
||||||
|
/// This indicates whether 1:1 chat has a green checkmark
|
||||||
|
/// or will have a green checkmark if created.
|
||||||
|
is_profile_verified: bool,
|
||||||
|
|
||||||
|
/// The ID of the contact that verified this contact.
|
||||||
|
///
|
||||||
|
/// If this is present,
|
||||||
|
/// display a green checkmark and "Introduced by ..."
|
||||||
|
/// string followed by the verifier contact name and address
|
||||||
|
/// in the contact profile.
|
||||||
verifier_id: Option<u32>,
|
verifier_id: Option<u32>,
|
||||||
|
|
||||||
/// the contact's last seen timestamp
|
/// the contact's last seen timestamp
|
||||||
last_seen: i64,
|
last_seen: i64,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
|
|
||||||
|
/// If the contact is a bot.
|
||||||
|
is_bot: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContactObject {
|
impl ContactObject {
|
||||||
@@ -38,19 +60,13 @@ impl ContactObject {
|
|||||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
|
let is_verified = contact.is_verified(context).await?;
|
||||||
|
let is_profile_verified = contact.is_profile_verified(context).await?;
|
||||||
|
|
||||||
let (verifier_addr, verifier_id) = if is_verified {
|
let verifier_id = contact
|
||||||
(
|
.get_verifier_id(context)
|
||||||
contact.get_verifier_addr(context).await?,
|
.await?
|
||||||
contact
|
.map(|contact_id| contact_id.to_u32());
|
||||||
.get_verifier_id(context)
|
|
||||||
.await?
|
|
||||||
.map(|contact_id| contact_id.to_u32()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ContactObject {
|
Ok(ContactObject {
|
||||||
address: contact.get_addr().to_owned(),
|
address: contact.get_addr().to_owned(),
|
||||||
@@ -64,10 +80,43 @@ impl ContactObject {
|
|||||||
name_and_addr: contact.get_name_n_addr(),
|
name_and_addr: contact.get_name_n_addr(),
|
||||||
is_blocked: contact.is_blocked(),
|
is_blocked: contact.is_blocked(),
|
||||||
is_verified,
|
is_verified,
|
||||||
verifier_addr,
|
is_profile_verified,
|
||||||
verifier_id,
|
verifier_id,
|
||||||
last_seen: contact.last_seen(),
|
last_seen: contact.last_seen(),
|
||||||
was_seen_recently: contact.was_seen_recently(),
|
was_seen_recently: contact.was_seen_recently(),
|
||||||
|
is_bot: contact.is_bot(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,61 +22,43 @@ impl From<CoreEvent> for Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "kind")]
|
||||||
pub enum EventType {
|
pub enum EventType {
|
||||||
/// The library-user may write an informational string to the log.
|
/// The library-user may write an informational string to the log.
|
||||||
///
|
///
|
||||||
/// This event should *not* be reported to the end-user using a popup or something like
|
/// This event should *not* be reported to the end-user using a popup or something like
|
||||||
/// that.
|
/// that.
|
||||||
Info {
|
Info { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when SMTP connection is established and login was successful.
|
/// Emitted when SMTP connection is established and login was successful.
|
||||||
SmtpConnected {
|
SmtpConnected { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when IMAP connection is established and login was successful.
|
/// Emitted when IMAP connection is established and login was successful.
|
||||||
ImapConnected {
|
ImapConnected { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when a message was successfully sent to the SMTP server.
|
/// Emitted when a message was successfully sent to the SMTP server.
|
||||||
SmtpMessageSent {
|
SmtpMessageSent { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when an IMAP message has been marked as deleted
|
/// Emitted when an IMAP message has been marked as deleted
|
||||||
ImapMessageDeleted {
|
ImapMessageDeleted { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when an IMAP message has been moved
|
/// Emitted when an IMAP message has been moved
|
||||||
ImapMessageMoved {
|
ImapMessageMoved { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted before going into IDLE on the Inbox folder.
|
/// Emitted before going into IDLE on the Inbox folder.
|
||||||
ImapInboxIdle,
|
ImapInboxIdle,
|
||||||
|
|
||||||
/// Emitted when an new file in the $BLOBDIR was created
|
/// Emitted when an new file in the $BLOBDIR was created
|
||||||
NewBlobFile {
|
NewBlobFile { file: String },
|
||||||
file: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when an file in the $BLOBDIR was deleted
|
/// Emitted when an file in the $BLOBDIR was deleted
|
||||||
DeletedBlobFile {
|
DeletedBlobFile { file: String },
|
||||||
file: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The library-user should write a warning string to the log.
|
/// The library-user should write a warning string to the log.
|
||||||
///
|
///
|
||||||
/// This event should *not* be reported to the end-user using a popup or something like
|
/// This event should *not* be reported to the end-user using a popup or something like
|
||||||
/// that.
|
/// that.
|
||||||
Warning {
|
Warning { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The library-user should report an error to the end-user.
|
/// The library-user should report an error to the end-user.
|
||||||
///
|
///
|
||||||
@@ -88,18 +70,14 @@ pub enum EventType {
|
|||||||
/// it might be better to delay showing these events until the function has really
|
/// it might be better to delay showing these events until the function has really
|
||||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||||
/// in a messasge box then.
|
/// in a messasge box then.
|
||||||
Error {
|
Error { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// An action cannot be performed because the user is not in the group.
|
/// An action cannot be performed because the user is not in the group.
|
||||||
/// Reported eg. after a call to
|
/// Reported eg. after a call to
|
||||||
/// setChatName(), setChatProfileImage(),
|
/// setChatName(), setChatProfileImage(),
|
||||||
/// addContactToChat(), removeContactFromChat(),
|
/// addContactToChat(), removeContactFromChat(),
|
||||||
/// and messages sending functions.
|
/// and messages sending functions.
|
||||||
ErrorSelfNotInGroup {
|
ErrorSelfNotInGroup { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Messages or chats changed. One or more messages or chats changed for various
|
/// Messages or chats changed. One or more messages or chats changed for various
|
||||||
/// reasons in the database:
|
/// reasons in the database:
|
||||||
@@ -110,10 +88,7 @@ pub enum EventType {
|
|||||||
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||||
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgsChanged {
|
MsgsChanged { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Reactions for the message changed.
|
/// Reactions for the message changed.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -126,60 +101,39 @@ pub enum EventType {
|
|||||||
/// There is a fresh message. Typically, the user will show an notification
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
IncomingMsg {
|
IncomingMsg { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Downloading a bunch of messages just finished. This is an experimental
|
/// Downloading a bunch of messages just finished. This is an
|
||||||
/// event to allow the UI to only show one notification per message bunch,
|
/// event to allow the UI to only show one notification per message bunch,
|
||||||
/// instead of cluttering the user with many notifications.
|
/// instead of cluttering the user with many notifications.
|
||||||
///
|
|
||||||
/// msg_ids contains the message ids.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
IncomingMsgBunch {
|
IncomingMsgBunch,
|
||||||
msg_ids: Vec<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Messages were seen or noticed.
|
/// Messages were seen or noticed.
|
||||||
/// chat id is always set.
|
/// chat id is always set.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgsNoticed {
|
MsgsNoticed { chat_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||||
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgDelivered {
|
MsgDelivered { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||||
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgFailed {
|
MsgFailed { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||||
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgRead {
|
MsgRead { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message is deleted.
|
/// A single message is deleted.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgDeleted {
|
MsgDeleted { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||||
/// Or the verify state of a chat has changed.
|
/// Or the verify state of a chat has changed.
|
||||||
@@ -189,24 +143,17 @@ pub enum EventType {
|
|||||||
/// This event does not include ephemeral timer modification, which
|
/// This event does not include ephemeral timer modification, which
|
||||||
/// is a separate event.
|
/// is a separate event.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatModified {
|
ChatModified { chat_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Chat ephemeral timer changed.
|
/// Chat ephemeral timer changed.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatEphemeralTimerModified {
|
ChatEphemeralTimerModified { chat_id: u32, timer: u32 },
|
||||||
chat_id: u32,
|
|
||||||
timer: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Contact(s) created, renamed, blocked or deleted.
|
/// Contact(s) created, renamed, blocked or deleted.
|
||||||
///
|
///
|
||||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ContactsChanged {
|
ContactsChanged { contact_id: Option<u32> },
|
||||||
contact_id: Option<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Location of one or more contact has changed.
|
/// Location of one or more contact has changed.
|
||||||
///
|
///
|
||||||
@@ -214,9 +161,7 @@ pub enum EventType {
|
|||||||
/// If the locations of several contacts have been changed,
|
/// If the locations of several contacts have been changed,
|
||||||
/// this parameter is set to `None`.
|
/// this parameter is set to `None`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
LocationChanged {
|
LocationChanged { contact_id: Option<u32> },
|
||||||
contact_id: Option<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Inform about the configuration progress started by configure().
|
/// Inform about the configuration progress started by configure().
|
||||||
ConfigureProgress {
|
ConfigureProgress {
|
||||||
@@ -234,9 +179,7 @@ pub enum EventType {
|
|||||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
/// @param data2 0
|
/// @param data2 0
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexProgress {
|
ImexProgress { progress: usize },
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A file has been exported. A file has been written by imex().
|
/// A file has been exported. A file has been written by imex().
|
||||||
/// This event may be sent multiple times by a single call to imex().
|
/// This event may be sent multiple times by a single call to imex().
|
||||||
@@ -246,9 +189,7 @@ pub enum EventType {
|
|||||||
///
|
///
|
||||||
/// @param data2 0
|
/// @param data2 0
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexFileWritten {
|
ImexFileWritten { path: String },
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Progress information of a secure-join handshake from the view of the inviter
|
/// Progress information of a secure-join handshake from the view of the inviter
|
||||||
/// (Alice, the person who shows the QR code).
|
/// (Alice, the person who shows the QR code).
|
||||||
@@ -263,10 +204,7 @@ pub enum EventType {
|
|||||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||||
/// 1000=Protocol finished for this contact.
|
/// 1000=Protocol finished for this contact.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
SecurejoinInviterProgress {
|
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
||||||
contact_id: u32,
|
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Progress information of a secure-join handshake from the view of the joiner
|
/// Progress information of a secure-join handshake from the view of the joiner
|
||||||
/// (Bob, the person who scans the QR code).
|
/// (Bob, the person who scans the QR code).
|
||||||
@@ -277,10 +215,7 @@ pub enum EventType {
|
|||||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
SecurejoinJoinerProgress {
|
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||||
contact_id: u32,
|
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The connectivity to the server changed.
|
/// The connectivity to the server changed.
|
||||||
/// This means that you should refresh the connectivity view
|
/// This means that you should refresh the connectivity view
|
||||||
@@ -288,19 +223,49 @@ pub enum EventType {
|
|||||||
/// getConnectivityHtml() for details.
|
/// getConnectivityHtml() for details.
|
||||||
ConnectivityChanged,
|
ConnectivityChanged,
|
||||||
|
|
||||||
|
/// Deprecated by `ConfigSynced`.
|
||||||
SelfavatarChanged,
|
SelfavatarChanged,
|
||||||
|
|
||||||
|
/// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
|
||||||
|
/// uniformity this is emitted on the source device too. The value isn't here, otherwise it
|
||||||
|
/// would be logged which might not be good for privacy.
|
||||||
|
ConfigSynced {
|
||||||
|
/// Configuration key.
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcStatusUpdate {
|
WebxdcStatusUpdate {
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
status_update_serial: u32,
|
status_update_serial: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Data received over an ephemeral peer channel.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
|
||||||
|
|
||||||
/// Inform that a message containing a webxdc instance has been deleted
|
/// Inform that a message containing a webxdc instance has been deleted
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcInstanceDeleted {
|
WebxdcInstanceDeleted { msg_id: u32 },
|
||||||
msg_id: u32,
|
|
||||||
},
|
/// Tells that the Background fetch was completed (or timed out).
|
||||||
|
/// This event acts as a marker, when you reach this event you can be sure
|
||||||
|
/// that all events emitted during the background fetch were processed.
|
||||||
|
///
|
||||||
|
/// This event is only emitted by the account manager
|
||||||
|
AccountsBackgroundFetchDone,
|
||||||
|
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||||
|
///
|
||||||
|
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
|
||||||
|
ChatlistChanged,
|
||||||
|
|
||||||
|
/// Inform that a single chat list item changed and needs to be rerendered.
|
||||||
|
/// 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 {
|
impl From<CoreEventType> for EventType {
|
||||||
@@ -336,9 +301,7 @@ impl From<CoreEventType> for EventType {
|
|||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
CoreEventType::IncomingMsgBunch => IncomingMsgBunch,
|
||||||
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
|
||||||
},
|
|
||||||
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
},
|
},
|
||||||
@@ -396,6 +359,9 @@ impl From<CoreEventType> for EventType {
|
|||||||
},
|
},
|
||||||
CoreEventType::ConnectivityChanged => ConnectivityChanged,
|
CoreEventType::ConnectivityChanged => ConnectivityChanged,
|
||||||
CoreEventType::SelfavatarChanged => SelfavatarChanged,
|
CoreEventType::SelfavatarChanged => SelfavatarChanged,
|
||||||
|
CoreEventType::ConfigSynced { key } => ConfigSynced {
|
||||||
|
key: key.to_string(),
|
||||||
|
},
|
||||||
CoreEventType::WebxdcStatusUpdate {
|
CoreEventType::WebxdcStatusUpdate {
|
||||||
msg_id,
|
msg_id,
|
||||||
status_update_serial,
|
status_update_serial,
|
||||||
@@ -403,9 +369,19 @@ impl From<CoreEventType> for EventType {
|
|||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
status_update_serial: status_update_serial.to_u32(),
|
status_update_serial: status_update_serial.to_u32(),
|
||||||
},
|
},
|
||||||
|
CoreEventType::WebxdcRealtimeData { msg_id, data } => WebxdcRealtimeData {
|
||||||
|
msg_id: msg_id.to_u32(),
|
||||||
|
data,
|
||||||
|
},
|
||||||
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
|
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
|
||||||
|
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
|
||||||
|
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 anyhow::{Context as _, Result};
|
||||||
use deltachat::chat::Chat;
|
use deltachat::chat::Chat;
|
||||||
use deltachat::chat::ChatItem;
|
use deltachat::chat::ChatItem;
|
||||||
@@ -19,7 +20,7 @@ use super::reactions::JSONRPCReactions;
|
|||||||
use super::webxdc::WebxdcMessageInfo;
|
use super::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", tag = "variant")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
pub enum MessageLoadResult {
|
pub enum MessageLoadResult {
|
||||||
Message(MessageObject),
|
Message(MessageObject),
|
||||||
LoadingError { error: String },
|
LoadingError { error: String },
|
||||||
@@ -35,6 +36,10 @@ pub struct MessageObject {
|
|||||||
parent_id: Option<u32>,
|
parent_id: Option<u32>,
|
||||||
|
|
||||||
text: String,
|
text: String,
|
||||||
|
|
||||||
|
/// Check if a message has a POI location bound to it.
|
||||||
|
/// These locations are also returned by `get_locations` method.
|
||||||
|
/// The UI may decide to display a special icon beside such messages.
|
||||||
has_location: bool,
|
has_location: bool,
|
||||||
has_html: bool,
|
has_html: bool,
|
||||||
view_type: MessageViewtype,
|
view_type: MessageViewtype,
|
||||||
@@ -83,6 +88,8 @@ pub struct MessageObject {
|
|||||||
download_state: DownloadState,
|
download_state: DownloadState,
|
||||||
|
|
||||||
reactions: Option<JSONRPCReactions>,
|
reactions: Option<JSONRPCReactions>,
|
||||||
|
|
||||||
|
vcard_contact: Option<VcardContact>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
@@ -105,11 +112,6 @@ enum MessageQuote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageObject {
|
impl MessageObject {
|
||||||
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
|
|
||||||
let msg_id = MsgId::new(message_id);
|
|
||||||
Self::from_msg_id(context, msg_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||||
let message = Message::load_from_db(context, msg_id).await?;
|
let message = Message::load_from_db(context, msg_id).await?;
|
||||||
|
|
||||||
@@ -174,6 +176,13 @@ impl MessageObject {
|
|||||||
Some(reactions.into())
|
Some(reactions.into())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let vcard_contacts: Vec<VcardContact> = message
|
||||||
|
.vcard_contacts(context)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(MessageObject {
|
Ok(MessageObject {
|
||||||
id: msg_id.to_u32(),
|
id: msg_id.to_u32(),
|
||||||
chat_id: message.get_chat_id().to_u32(),
|
chat_id: message.get_chat_id().to_u32(),
|
||||||
@@ -233,6 +242,8 @@ impl MessageObject {
|
|||||||
download_state,
|
download_state,
|
||||||
|
|
||||||
reactions,
|
reactions,
|
||||||
|
|
||||||
|
vcard_contact: vcard_contacts.first().cloned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,6 +286,11 @@ pub enum MessageViewtype {
|
|||||||
|
|
||||||
/// Message is an webxdc instance.
|
/// Message is an webxdc instance.
|
||||||
Webxdc,
|
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 {
|
impl From<Viewtype> for MessageViewtype {
|
||||||
@@ -291,6 +307,7 @@ impl From<Viewtype> for MessageViewtype {
|
|||||||
Viewtype::File => MessageViewtype::File,
|
Viewtype::File => MessageViewtype::File,
|
||||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||||
|
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,6 +326,7 @@ impl From<MessageViewtype> for Viewtype {
|
|||||||
MessageViewtype::File => Viewtype::File,
|
MessageViewtype::File => Viewtype::File,
|
||||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||||
|
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,6 +363,15 @@ pub enum SystemMessageType {
|
|||||||
SecurejoinMessage,
|
SecurejoinMessage,
|
||||||
LocationStreamingEnabled,
|
LocationStreamingEnabled,
|
||||||
LocationOnly,
|
LocationOnly,
|
||||||
|
InvalidUnencryptedMail,
|
||||||
|
|
||||||
|
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
|
||||||
|
/// to complete.
|
||||||
|
SecurejoinWait,
|
||||||
|
|
||||||
|
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
|
||||||
|
/// send messages.
|
||||||
|
SecurejoinWaitTimeout,
|
||||||
|
|
||||||
/// Chat ephemeral message timer is changed.
|
/// Chat ephemeral message timer is changed.
|
||||||
EphemeralTimerChanged,
|
EphemeralTimerChanged,
|
||||||
@@ -364,6 +391,9 @@ pub enum SystemMessageType {
|
|||||||
|
|
||||||
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
||||||
WebxdcInfoMessage,
|
WebxdcInfoMessage,
|
||||||
|
|
||||||
|
/// This message contains a users iroh node address.
|
||||||
|
IrohNodeAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||||
@@ -385,6 +415,10 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||||
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
||||||
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
||||||
|
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
|
||||||
|
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
||||||
|
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
||||||
|
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,9 +580,115 @@ pub struct MessageData {
|
|||||||
pub quoted_message_id: Option<u32>,
|
pub quoted_message_id: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MessageData {
|
||||||
|
pub(crate) async fn create_message(self, context: &Context) -> Result<Message> {
|
||||||
|
let mut message = Message::new(if let Some(viewtype) = self.viewtype {
|
||||||
|
viewtype.into()
|
||||||
|
} else if self.file.is_some() {
|
||||||
|
Viewtype::File
|
||||||
|
} else {
|
||||||
|
Viewtype::Text
|
||||||
|
});
|
||||||
|
message.set_text(self.text.unwrap_or_default());
|
||||||
|
if self.html.is_some() {
|
||||||
|
message.set_html(self.html);
|
||||||
|
}
|
||||||
|
if self.override_sender_name.is_some() {
|
||||||
|
message.set_override_sender_name(self.override_sender_name);
|
||||||
|
}
|
||||||
|
if let Some(file) = self.file {
|
||||||
|
message.set_file(file, None);
|
||||||
|
}
|
||||||
|
if let Some((latitude, longitude)) = self.location {
|
||||||
|
message.set_location(latitude, longitude);
|
||||||
|
}
|
||||||
|
if let Some(id) = self.quoted_message_id {
|
||||||
|
message
|
||||||
|
.set_quote(
|
||||||
|
context,
|
||||||
|
Some(
|
||||||
|
&Message::load_from_db(context, MsgId::new(id))
|
||||||
|
.await
|
||||||
|
.context("message to quote could not be loaded")?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageReadReceipt {
|
pub struct MessageReadReceipt {
|
||||||
pub contact_id: u32,
|
pub contact_id: u32,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MessageInfo {
|
||||||
|
rawtext: String,
|
||||||
|
ephemeral_timer: EphemeralTimer,
|
||||||
|
/// When message is ephemeral this contains the timestamp of the message expiry
|
||||||
|
ephemeral_timestamp: Option<i64>,
|
||||||
|
error: Option<String>,
|
||||||
|
rfc724_mid: String,
|
||||||
|
server_urls: Vec<String>,
|
||||||
|
hop_info: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageInfo {
|
||||||
|
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||||
|
let message = Message::load_from_db(context, msg_id).await?;
|
||||||
|
let rawtext = msg_id.rawtext(context).await?;
|
||||||
|
let ephemeral_timer = message.get_ephemeral_timer().into();
|
||||||
|
let ephemeral_timestamp = match message.get_ephemeral_timer() {
|
||||||
|
deltachat::ephemeral::Timer::Disabled => None,
|
||||||
|
deltachat::ephemeral::Timer::Enabled { .. } => Some(message.get_ephemeral_timestamp()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let server_urls =
|
||||||
|
MsgId::get_info_server_urls(context, message.rfc724_mid().to_owned()).await?;
|
||||||
|
|
||||||
|
let hop_info = msg_id.hop_info(context).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
rawtext,
|
||||||
|
ephemeral_timer,
|
||||||
|
ephemeral_timestamp,
|
||||||
|
error: message.error(),
|
||||||
|
rfc724_mid: message.rfc724_mid().to_owned(),
|
||||||
|
server_urls,
|
||||||
|
hop_info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
|
pub enum EphemeralTimer {
|
||||||
|
/// Timer is disabled.
|
||||||
|
Disabled,
|
||||||
|
|
||||||
|
/// Timer is enabled.
|
||||||
|
Enabled {
|
||||||
|
/// Timer duration in seconds.
|
||||||
|
///
|
||||||
|
/// The value cannot be 0.
|
||||||
|
duration: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
|
||||||
|
fn from(value: deltachat::ephemeral::Timer) -> Self {
|
||||||
|
match value {
|
||||||
|
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
|
||||||
|
deltachat::ephemeral::Timer::Enabled { duration } => {
|
||||||
|
EphemeralTimer::Enabled { duration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use typescript_type_def::TypeDef;
|
|||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ProviderInfo {
|
pub struct ProviderInfo {
|
||||||
|
/// Unique ID, corresponding to provider database filename.
|
||||||
|
pub id: String,
|
||||||
pub before_login_hint: String,
|
pub before_login_hint: String,
|
||||||
pub overview_page: String,
|
pub overview_page: String,
|
||||||
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
|
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
|
||||||
@@ -14,6 +16,7 @@ pub struct ProviderInfo {
|
|||||||
impl ProviderInfo {
|
impl ProviderInfo {
|
||||||
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
|
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
|
||||||
provider.map(|p| ProviderInfo {
|
provider.map(|p| ProviderInfo {
|
||||||
|
id: p.id.to_owned(),
|
||||||
before_login_hint: p.before_login_hint.to_owned(),
|
before_login_hint: p.before_login_hint.to_owned(),
|
||||||
overview_page: p.overview_page.to_owned(),
|
overview_page: p.overview_page.to_owned(),
|
||||||
status: p.status.to_u32().unwrap(),
|
status: p.status.to_u32().unwrap(),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "kind")]
|
||||||
pub enum QrObject {
|
pub enum QrObject {
|
||||||
AskVerifyContact {
|
AskVerifyContact {
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub use yerpc;
|
pub use yerpc;
|
||||||
|
|
||||||
@@ -13,10 +14,11 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||||
let accounts = Accounts::new(tmp_dir).await?;
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||||
let api = CommandApi::new(accounts);
|
let api = CommandApi::new(accounts);
|
||||||
|
|
||||||
let (sender, mut receiver) = unbounded::<String>();
|
let (sender, receiver) = unbounded::<String>();
|
||||||
|
|
||||||
let (client, mut rx) = RpcClient::new();
|
let (client, mut rx) = RpcClient::new();
|
||||||
let session = RpcSession::new(client, api);
|
let session = RpcSession::new(client, api);
|
||||||
@@ -35,17 +37,17 @@ mod tests {
|
|||||||
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.next().await;
|
let result = receiver.recv().await?;
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
assert_eq!(result, Some(response.to_owned()));
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
|
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.next().await;
|
let result = receiver.recv().await?;
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
assert_eq!(result, Some(response.to_owned()));
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -54,10 +56,11 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_batch_set_config() -> anyhow::Result<()> {
|
async fn test_batch_set_config() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||||
let accounts = Accounts::new(tmp_dir).await?;
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||||
let api = CommandApi::new(accounts);
|
let api = CommandApi::new(accounts);
|
||||||
|
|
||||||
let (sender, mut receiver) = unbounded::<String>();
|
let (sender, receiver) = unbounded::<String>();
|
||||||
|
|
||||||
let (client, mut rx) = RpcClient::new();
|
let (client, mut rx) = RpcClient::new();
|
||||||
let session = RpcSession::new(client, api);
|
let session = RpcSession::new(client, api);
|
||||||
@@ -76,15 +79,15 @@ mod tests {
|
|||||||
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.next().await;
|
let result = receiver.recv().await?;
|
||||||
assert_eq!(result, Some(response.to_owned()));
|
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":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.next().await;
|
let result = receiver.recv().await?;
|
||||||
assert_eq!(result, Some(response.to_owned()));
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -19,7 +20,8 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
||||||
.unwrap_or(DEFAULT_PORT);
|
.unwrap_or(DEFAULT_PORT);
|
||||||
log::info!("Starting with accounts directory `{path}`.");
|
log::info!("Starting with accounts directory `{path}`.");
|
||||||
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
|
||||||
let state = CommandApi::new(accounts);
|
let state = CommandApi::new(accounts);
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -27,15 +29,13 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
.layer(Extension(state.clone()));
|
.layer(Extension(state.clone()));
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
state.accounts.read().await.start_io().await;
|
state.accounts.write().await.start_io().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||||
log::info!("JSON-RPC WebSocket server listening on {}", addr);
|
log::info!("JSON-RPC WebSocket server listening on {}", addr);
|
||||||
axum::Server::bind(&addr)
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
.serve(app.into_make_service())
|
axum::serve(listener, app).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async function run() {
|
|||||||
const accounts = await client.rpc.getAllAccounts();
|
const accounts = await client.rpc.getAllAccounts();
|
||||||
console.log("accounts loaded", accounts);
|
console.log("accounts loaded", accounts);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
if (account.type === "Configured") {
|
if (account.kind === "Configured") {
|
||||||
write(
|
write(
|
||||||
$head,
|
$head,
|
||||||
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
||||||
@@ -57,7 +57,7 @@ async function run() {
|
|||||||
clear($main);
|
clear($main);
|
||||||
const selectedAccount = SELECTED_ACCOUNT;
|
const selectedAccount = SELECTED_ACCOUNT;
|
||||||
const info = await client.rpc.getAccountInfo(selectedAccount);
|
const info = await client.rpc.getAccountInfo(selectedAccount);
|
||||||
if (info.type !== "Configured") {
|
if (info.kind !== "Configured") {
|
||||||
return write($main, "Account is not configured");
|
return write($main, "Account is not configured");
|
||||||
}
|
}
|
||||||
write($main, `<h2>${info.addr!}</h2>`);
|
write($main, `<h2>${info.addr!}</h2>`);
|
||||||
@@ -81,8 +81,7 @@ async function run() {
|
|||||||
messageIds
|
messageIds
|
||||||
);
|
);
|
||||||
for (const [_messageId, message] of Object.entries(messages)) {
|
for (const [_messageId, message] of Object.entries(messages)) {
|
||||||
if (message.variant === "message")
|
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
|
||||||
write($main, `<p>${message.text}</p>`);
|
|
||||||
else write($main, `<p>loading error: ${message.error}</p>`);
|
else write($main, `<p>loading error: ${message.error}</p>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +92,9 @@ async function run() {
|
|||||||
$side,
|
$side,
|
||||||
`
|
`
|
||||||
<p class="message">
|
<p class="message">
|
||||||
[<strong>${event.type}</strong> on account ${accountId}]<br>
|
[<strong>${event.kind}</strong> on account ${accountId}]<br>
|
||||||
<em>f1:</em> ${JSON.stringify(
|
<em>f1:</em> ${JSON.stringify(
|
||||||
Object.assign({}, event, { type: undefined })
|
Object.assign({}, event, { kind: undefined })
|
||||||
)}
|
)}
|
||||||
</p>`
|
</p>`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"@types/chai": "^4.2.21",
|
"@types/chai": "^4.2.21",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/node-fetch": "^2.5.7",
|
|
||||||
"@types/ws": "^7.2.4",
|
"@types/ws": "^7.2.4",
|
||||||
"c8": "^7.10.0",
|
"c8": "^7.10.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
"esbuild": "^0.17.9",
|
"esbuild": "^0.17.9",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"mocha": "^9.1.1",
|
"mocha": "^9.1.1",
|
||||||
"node-fetch": "^2.6.1",
|
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"typedoc": "^0.23.2",
|
"typedoc": "^0.23.2",
|
||||||
@@ -27,12 +25,17 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/deltachat.js",
|
"import": "./dist/deltachat.js",
|
||||||
"require": "./dist/deltachat.cjs"
|
"require": "./dist/deltachat.cjs",
|
||||||
|
"types": "./dist/deltachat.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"main": "dist/deltachat.js",
|
"main": "dist/deltachat.js",
|
||||||
"name": "@deltachat/jsonrpc-client",
|
"name": "@deltachat/jsonrpc-client",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
|
"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",
|
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||||
@@ -55,5 +58,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "1.125.0"
|
"version": "1.140.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
|||||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||||
|
|
||||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||||
[Property in EventType["type"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
accountId: number,
|
accountId: number,
|
||||||
event: Extract<EventType, { type: Property }>
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||||
[Property in EventType["type"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
event: Extract<EventType, { type: Property }>
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DcEvent = EventType;
|
export type DcEvent = EventType;
|
||||||
export type DcEventType<T extends EventType["type"]> = Extract<
|
export type DcEventType<T extends EventType["kind"]> = Extract<
|
||||||
EventType,
|
EventType,
|
||||||
{ type: T }
|
{ kind: T }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export class BaseDeltaChat<
|
export class BaseDeltaChat<
|
||||||
@@ -46,12 +46,12 @@ export class BaseDeltaChat<
|
|||||||
while (true) {
|
while (true) {
|
||||||
const event = await this.rpc.getNextEvent();
|
const event = await this.rpc.getNextEvent();
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.emit(event.event.type, event.contextId, event.event);
|
this.emit(event.event.kind, event.contextId, event.event);
|
||||||
this.emit("ALL", event.contextId, event.event);
|
this.emit("ALL", event.contextId, event.event);
|
||||||
|
|
||||||
if (this.contextEmitters[event.contextId]) {
|
if (this.contextEmitters[event.contextId]) {
|
||||||
this.contextEmitters[event.contextId].emit(
|
this.contextEmitters[event.contextId].emit(
|
||||||
event.event.type,
|
event.event.kind,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
event.event as any
|
event.event as any
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ describe("basic tests", () => {
|
|||||||
accountId = await dc.rpc.addAccount();
|
accountId = await dc.rpc.addAccount();
|
||||||
});
|
});
|
||||||
it("should block and unblock contact", async function () {
|
it("should block and unblock contact", async function () {
|
||||||
|
// Cannot send sync messages to self as we do not have a self address.
|
||||||
|
await dc.rpc.setConfig(accountId, "sync_msgs", "0");
|
||||||
|
|
||||||
const contactId = await dc.rpc.createContact(
|
const contactId = await dc.rpc.createContact(
|
||||||
accountId,
|
accountId,
|
||||||
"example@delta.chat",
|
"example@delta.chat",
|
||||||
|
|||||||
@@ -13,27 +13,27 @@ describe("online tests", function () {
|
|||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
if (!process.env.DCC_NEW_TMP_EMAIL) {
|
if (!process.env.CHATMAIL_DOMAIN) {
|
||||||
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
||||||
console.error(
|
console.error(
|
||||||
"CAN NOT RUN COVERAGE correctly: Missing DCC_NEW_TMP_EMAIL environment variable!\n\n",
|
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
|
||||||
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
|
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
"Missing DCC_NEW_TMP_EMAIL environment variable!, skip integration tests"
|
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
serverHandle = await startServer();
|
serverHandle = await startServer();
|
||||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||||
|
|
||||||
dc.on("ALL", (contextId, { type }) => {
|
dc.on("ALL", (contextId, { kind }) => {
|
||||||
if (type !== "Info") console.log(contextId, type);
|
if (kind !== "Info") console.log(contextId, kind);
|
||||||
});
|
});
|
||||||
|
|
||||||
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||||
if (!account1 || !account1.email || !account1.password) {
|
if (!account1 || !account1.email || !account1.password) {
|
||||||
console.log(
|
console.log(
|
||||||
"We didn't got back an account from the api, skip integration tests"
|
"We didn't got back an account from the api, skip integration tests"
|
||||||
@@ -41,7 +41,7 @@ describe("online tests", function () {
|
|||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
|
|
||||||
account2 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||||
if (!account2 || !account2.email || !account2.password) {
|
if (!account2 || !account2.email || !account2.password) {
|
||||||
console.log(
|
console.log(
|
||||||
"We didn't got back an account2 from the api, skip integration tests"
|
"We didn't got back an account2 from the api, skip integration tests"
|
||||||
@@ -148,7 +148,7 @@ describe("online tests", function () {
|
|||||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||||
]);
|
]);
|
||||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||||
// Check if answer arives at A and if it is encrypted
|
// Check if answer arrives at A and if it is encrypted
|
||||||
await eventPromise2;
|
await eventPromise2;
|
||||||
|
|
||||||
const messageId = (
|
const messageId = (
|
||||||
@@ -177,12 +177,12 @@ describe("online tests", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function waitForEvent<T extends DcEvent["type"]>(
|
async function waitForEvent<T extends DcEvent["kind"]>(
|
||||||
dc: DeltaChat,
|
dc: DeltaChat,
|
||||||
eventType: T,
|
eventType: T,
|
||||||
accountId: number,
|
accountId: number,
|
||||||
timeout: number = EVENT_TIMEOUT
|
timeout: number = EVENT_TIMEOUT
|
||||||
): Promise<Extract<DcEvent, { type: T }>> {
|
): Promise<Extract<DcEvent, { kind: T }>> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const rejectTimeout = setTimeout(
|
const rejectTimeout = setTimeout(
|
||||||
() => reject(new Error("Timeout reached before event came in")),
|
() => reject(new Error("Timeout reached before event came in")),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { tmpdir } from "os";
|
|||||||
import { join, resolve } from "path";
|
import { join, resolve } from "path";
|
||||||
import { mkdtemp, rm } from "fs/promises";
|
import { mkdtemp, rm } from "fs/promises";
|
||||||
import { spawn, exec } from "child_process";
|
import { spawn, exec } from "child_process";
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { Readable, Writable } from "node:stream";
|
import { Readable, Writable } from "node:stream";
|
||||||
|
|
||||||
export type RpcServerHandle = {
|
export type RpcServerHandle = {
|
||||||
@@ -57,15 +56,14 @@ export async function startServer(): Promise<RpcServerHandle> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTempUser(url: string) {
|
export function createTempUser(chatmailDomain: String) {
|
||||||
const response = await fetch(url, {
|
const charset = "2345789acdefghjkmnpqrstuvwxyz";
|
||||||
method: "POST",
|
let user = "ci-";
|
||||||
headers: {
|
for (let i = 0; i < 6; i++) {
|
||||||
"cache-control": "no-cache",
|
user += charset[Math.floor(Math.random() * charset.length)];
|
||||||
},
|
}
|
||||||
});
|
const email = user + "@" + chatmailDomain;
|
||||||
if (!response.ok) throw new Error("Received invalid response");
|
return { email: email, password: user + "$" + user };
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTargetDir(): Promise<string> {
|
function getTargetDir(): Promise<string> {
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "1.125.0"
|
version = "1.140.0"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.12.1"
|
ansi_term = "0.12.1"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
deltachat = { path = "..", features = ["internals"]}
|
deltachat = { path = "..", features = ["internals"]}
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
log = "0.4.19"
|
log = "0.4.21"
|
||||||
pretty_env_logger = "0.5"
|
rusqlite = "0.31"
|
||||||
rusqlite = "0.29"
|
rustyline = "14"
|
||||||
rustyline = "12"
|
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ extern crate dirs;
|
|||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use deltachat::chat::{
|
use deltachat::chat::{
|
||||||
@@ -18,6 +18,8 @@ use deltachat::imex::*;
|
|||||||
use deltachat::location;
|
use deltachat::location;
|
||||||
use deltachat::log::LogExt;
|
use deltachat::log::LogExt;
|
||||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
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::peerstate::*;
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
use deltachat::reaction::send_reaction;
|
use deltachat::reaction::send_reaction;
|
||||||
@@ -32,14 +34,6 @@ use tokio::fs;
|
|||||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||||
async fn reset_tables(context: &Context, bits: i32) {
|
async fn reset_tables(context: &Context, bits: i32) {
|
||||||
println!("Resetting tables ({bits})...");
|
println!("Resetting tables ({bits})...");
|
||||||
if 0 != bits & 1 {
|
|
||||||
context
|
|
||||||
.sql()
|
|
||||||
.execute("DELETE FROM jobs;", ())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
println!("(1) Jobs reset.");
|
|
||||||
}
|
|
||||||
if 0 != bits & 2 {
|
if 0 != bits & 2 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
@@ -210,7 +204,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
} else {
|
} else {
|
||||||
"[FRESH]"
|
"[FRESH]"
|
||||||
},
|
},
|
||||||
if msg.is_info() { "[INFO]" } else { "" },
|
if msg.is_info() {
|
||||||
|
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
|
||||||
|
"[INFO 🛡️]"
|
||||||
|
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
|
||||||
|
"[INFO 🛡️❌]"
|
||||||
|
} else {
|
||||||
|
"[INFO]"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||||
format!(
|
format!(
|
||||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||||
@@ -273,13 +277,8 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
|||||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||||
let name = contact.get_display_name();
|
let name = contact.get_display_name();
|
||||||
let addr = contact.get_addr();
|
let addr = contact.get_addr();
|
||||||
let verified_state = contact.is_verified(context).await?;
|
let verified_str = if contact.is_verified(context).await? {
|
||||||
let verified_str = if VerifiedStatus::Unverified != verified_state {
|
" √"
|
||||||
if verified_state == VerifiedStatus::BidirectVerified {
|
|
||||||
" √√"
|
|
||||||
} else {
|
|
||||||
" √"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
@@ -395,8 +394,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
unpin <chat-id>\n\
|
unpin <chat-id>\n\
|
||||||
mute <chat-id> [<seconds>]\n\
|
mute <chat-id> [<seconds>]\n\
|
||||||
unmute <chat-id>\n\
|
unmute <chat-id>\n\
|
||||||
protect <chat-id>\n\
|
|
||||||
unprotect <chat-id>\n\
|
|
||||||
delchat <chat-id>\n\
|
delchat <chat-id>\n\
|
||||||
accept <chat-id>\n\
|
accept <chat-id>\n\
|
||||||
decline <chat-id>\n\
|
decline <chat-id>\n\
|
||||||
@@ -646,6 +643,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("{cnt} chats");
|
println!("{cnt} chats");
|
||||||
println!("{time_needed:?} to create this list");
|
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" => {
|
"chat" => {
|
||||||
if sel_chat.is_none() && arg1.is_empty() {
|
if sel_chat.is_none() && arg1.is_empty() {
|
||||||
bail!("Argument [chat-id] is missing.");
|
bail!("Argument [chat-id] is missing.");
|
||||||
@@ -1071,20 +1092,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
};
|
};
|
||||||
chat::set_muted(&context, chat_id, duration).await?;
|
chat::set_muted(&context, chat_id, duration).await?;
|
||||||
}
|
}
|
||||||
"protect" | "unprotect" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
|
||||||
let chat_id = ChatId::new(arg1.parse()?);
|
|
||||||
chat_id
|
|
||||||
.set_protection(
|
|
||||||
&context,
|
|
||||||
match arg0 {
|
|
||||||
"protect" => ProtectionStatus::Protected,
|
|
||||||
"unprotect" => ProtectionStatus::Unprotected,
|
|
||||||
_ => unreachable!("arg0={:?}", arg0),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
"delchat" => {
|
"delchat" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||||
let chat_id = ChatId::new(arg1.parse()?);
|
let chat_id = ChatId::new(arg1.parse()?);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
//! This is a CLI program and a little testing frame. This file must not be
|
//! This is a CLI program and a little testing frame. This file must not be
|
||||||
//! included when using Delta Chat Core as a library.
|
//! included when using Delta Chat Core as a library.
|
||||||
//!
|
//!
|
||||||
@@ -9,7 +10,6 @@ extern crate deltachat;
|
|||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use ansi_term::Color;
|
use ansi_term::Color;
|
||||||
@@ -20,8 +20,7 @@ use deltachat::context::*;
|
|||||||
use deltachat::oauth2::*;
|
use deltachat::oauth2::*;
|
||||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||||
use deltachat::securejoin::*;
|
use deltachat::securejoin::*;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::EventType;
|
||||||
use deltachat::{EventType, Events};
|
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
@@ -33,6 +32,7 @@ use rustyline::{
|
|||||||
};
|
};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
mod cmdline;
|
mod cmdline;
|
||||||
use self::cmdline::*;
|
use self::cmdline::*;
|
||||||
@@ -299,8 +299,8 @@ impl Highlighter for DcHelper {
|
|||||||
self.highlighter.highlight(line, pos)
|
self.highlighter.highlight(line, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_char(&self, line: &str, pos: usize) -> bool {
|
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
|
||||||
self.highlighter.highlight_char(line, pos)
|
self.highlighter.highlight_char(line, pos, forced)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +312,10 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
println!("Error: Bad arguments, expected [db-name].");
|
println!("Error: Bad arguments, expected [db-name].");
|
||||||
bail!("No db-name specified");
|
bail!("No db-name specified");
|
||||||
}
|
}
|
||||||
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
|
let context = ContextBuilder::new(args[1].clone().into())
|
||||||
|
.with_id(1)
|
||||||
|
.open()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let events = context.get_event_emitter();
|
let events = context.get_event_emitter();
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
@@ -481,7 +484,11 @@ async fn handle_cmd(
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Error> {
|
async fn main() -> Result<(), Error> {
|
||||||
let _ = pretty_env_logger::try_init();
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::from_default_env().add_directive("deltachat_repl=info".parse()?),
|
||||||
|
)
|
||||||
|
.init();
|
||||||
|
|
||||||
let args = std::env::args().collect();
|
let args = std::env::args().collect();
|
||||||
start(args).await?;
|
start(args).await?;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ $ pip install .
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||||
2. Run `PATH="../target/debug:$PATH" tox`.
|
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||||
|
|
||||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
||||||
Pass --help to the CLI to see available options.
|
Pass --help to the CLI to see available options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from deltachat_rpc_client import events, run_bot_cli
|
from deltachat_rpc_client import events, run_bot_cli
|
||||||
|
|
||||||
hooks = events.HookCollection()
|
hooks = events.HookCollection()
|
||||||
|
|||||||
5
deltachat-rpc-client/examples/echobot_advanced.py
Normal file → Executable file
5
deltachat-rpc-client/examples/echobot_advanced.py
Normal file → Executable file
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
it will echo back any message that has non-empty text and also supports the /help command.
|
it will echo back any message that has non-empty text and also supports the /help command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@@ -14,9 +15,9 @@ hooks = events.HookCollection()
|
|||||||
|
|
||||||
@hooks.on(events.RawEvent)
|
@hooks.on(events.RawEvent)
|
||||||
def log_event(event):
|
def log_event(event):
|
||||||
if event.type == EventType.INFO:
|
if event.kind == EventType.INFO:
|
||||||
logging.info(event.msg)
|
logging.info(event.msg)
|
||||||
elif event.type == EventType.WARNING:
|
elif event.kind == EventType.WARNING:
|
||||||
logging.warning(event.msg)
|
logging.warning(event.msg)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
deltachat-rpc-client/examples/echobot_no_hooks.py
Normal file → Executable file
9
deltachat-rpc-client/examples/echobot_no_hooks.py
Normal file → Executable file
@@ -2,6 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
Example echo bot without using hooks
|
Example echo bot without using hooks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -40,13 +41,13 @@ def main():
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
if event["type"] == EventType.INFO:
|
if event["kind"] == EventType.INFO:
|
||||||
logging.info("%s", event["msg"])
|
logging.info("%s", event["msg"])
|
||||||
elif event["type"] == EventType.WARNING:
|
elif event["kind"] == EventType.WARNING:
|
||||||
logging.warning("%s", event["msg"])
|
logging.warning("%s", event["msg"])
|
||||||
elif event["type"] == EventType.ERROR:
|
elif event["kind"] == EventType.ERROR:
|
||||||
logging.error("%s", event["msg"])
|
logging.error("%s", event["msg"])
|
||||||
elif event["type"] == EventType.INCOMING_MSG:
|
elif event["kind"] == EventType.INCOMING_MSG:
|
||||||
logging.info("Got an incoming message")
|
logging.info("Got an incoming message")
|
||||||
process_messages()
|
process_messages()
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
|
requires = ["setuptools>=45"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deltachat-rpc-client"
|
name = "deltachat-rpc-client"
|
||||||
|
version = "1.140.0"
|
||||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
@@ -16,11 +17,14 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
"Topic :: Communications :: Chat",
|
"Topic :: Communications :: Chat",
|
||||||
"Topic :: Communications :: Email"
|
"Topic :: Communications :: Email"
|
||||||
]
|
]
|
||||||
dynamic = [
|
readme = "README.md"
|
||||||
"version"
|
dependencies = [
|
||||||
|
"imap-tools",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
@@ -31,14 +35,11 @@ deltachat_rpc_client = [
|
|||||||
[project.entry-points.pytest11]
|
[project.entry-points.pytest11]
|
||||||
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
|
||||||
root = ".."
|
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = [
|
lint.select = [
|
||||||
"E", "W", # pycodestyle
|
"E", "W", # pycodestyle
|
||||||
"F", # Pyflakes
|
"F", # Pyflakes
|
||||||
"N", # pep8-naming
|
"N", # pep8-naming
|
||||||
@@ -71,6 +72,3 @@ line-length = 120
|
|||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
log_cli = true
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Delta Chat JSON-RPC high-level API"""
|
"""Delta Chat JSON-RPC high-level API"""
|
||||||
|
|
||||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||||
from .account import Account
|
from .account import Account
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
|
|||||||
@@ -104,7 +104,11 @@ def _run_cli(
|
|||||||
if not client.is_configured():
|
if not client.is_configured():
|
||||||
assert args.email, "Account is not configured and email must be provided"
|
assert args.email, "Account is not configured and email must be provided"
|
||||||
assert args.password, "Account is not configured and password must be provided"
|
assert args.password, "Account is not configured and password must be provided"
|
||||||
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
configure_thread = Thread(
|
||||||
|
target=client.configure,
|
||||||
|
daemon=True,
|
||||||
|
kwargs={"email": args.email, "password": args.password},
|
||||||
|
)
|
||||||
configure_thread.start()
|
configure_thread.start()
|
||||||
client.run_forever()
|
client.run_forever()
|
||||||
|
|
||||||
@@ -168,3 +172,33 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
|||||||
return "removed", addr, addr
|
return "removed", addr, addr
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class futuremethod: # noqa: N801
|
||||||
|
"""Decorator for async methods."""
|
||||||
|
|
||||||
|
def __init__(self, func):
|
||||||
|
self._func = func
|
||||||
|
|
||||||
|
def __get__(self, instance, owner=None):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def future(*args):
|
||||||
|
generator = self._func(instance, *args)
|
||||||
|
res = next(generator)
|
||||||
|
|
||||||
|
def f():
|
||||||
|
try:
|
||||||
|
generator.send(res())
|
||||||
|
except StopIteration as e:
|
||||||
|
return e.value
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
def wrapper(*args):
|
||||||
|
f = future(*args)
|
||||||
|
return f()
|
||||||
|
|
||||||
|
wrapper.future = future
|
||||||
|
return wrapper
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict, futuremethod
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
from .const import ChatlistFlag, ContactFlag, SpecialContactId
|
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
|
||||||
@@ -28,6 +30,10 @@ class Account:
|
|||||||
"""Wait until the next event and return it."""
|
"""Wait until the next event and return it."""
|
||||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||||
|
|
||||||
|
def clear_all_events(self):
|
||||||
|
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||||
|
self._rpc.clear_all_events(self.id)
|
||||||
|
|
||||||
def remove(self) -> None:
|
def remove(self) -> None:
|
||||||
"""Remove the account."""
|
"""Remove the account."""
|
||||||
self._rpc.remove_account(self.id)
|
self._rpc.remove_account(self.id)
|
||||||
@@ -76,9 +82,24 @@ class Account:
|
|||||||
"""Get self avatar."""
|
"""Get self avatar."""
|
||||||
return self.get_config("selfavatar")
|
return self.get_config("selfavatar")
|
||||||
|
|
||||||
def configure(self) -> None:
|
def check_qr(self, qr):
|
||||||
|
return self._rpc.check_qr(self.id, qr)
|
||||||
|
|
||||||
|
def set_config_from_qr(self, qr: str):
|
||||||
|
self._rpc.set_config_from_qr(self.id, qr)
|
||||||
|
|
||||||
|
@futuremethod
|
||||||
|
def configure(self):
|
||||||
"""Configure an account."""
|
"""Configure an account."""
|
||||||
self._rpc.configure(self.id)
|
yield self._rpc.configure.future(self.id)
|
||||||
|
|
||||||
|
def bring_online(self):
|
||||||
|
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||||
|
self.start_io()
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||||
|
break
|
||||||
|
|
||||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||||
"""Create a new Contact or return an existing one.
|
"""Create a new Contact or return an existing one.
|
||||||
@@ -97,6 +118,11 @@ class Account:
|
|||||||
obj = obj.get_snapshot().address
|
obj = obj.get_snapshot().address
|
||||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||||
|
|
||||||
|
def create_chat(self, account: "Account") -> Chat:
|
||||||
|
addr = account.get_config("addr")
|
||||||
|
contact = self.create_contact(addr)
|
||||||
|
return contact.create_chat()
|
||||||
|
|
||||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||||
"""Return Contact instance for the given contact ID."""
|
"""Return Contact instance for the given contact ID."""
|
||||||
return Contact(self, contact_id)
|
return Contact(self, contact_id)
|
||||||
@@ -106,18 +132,32 @@ class Account:
|
|||||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||||
return contact_id and Contact(self, contact_id)
|
return contact_id and Contact(self, contact_id)
|
||||||
|
|
||||||
def get_blocked_contacts(self) -> List[AttrDict]:
|
def get_blocked_contacts(self) -> list[AttrDict]:
|
||||||
"""Return a list with snapshots of all blocked contacts."""
|
"""Return a list with snapshots of all blocked contacts."""
|
||||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||||
|
|
||||||
|
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
||||||
|
"""Return 1:1 chat for a contact if it exists."""
|
||||||
|
if isinstance(contact, Contact):
|
||||||
|
assert contact.account == self
|
||||||
|
contact_id = contact.id
|
||||||
|
elif isinstance(contact, int):
|
||||||
|
contact_id = contact
|
||||||
|
else:
|
||||||
|
raise ValueError(f"{contact!r} is not a contact")
|
||||||
|
chat_id = self._rpc.get_chat_id_by_contact_id(self.id, contact_id)
|
||||||
|
if chat_id:
|
||||||
|
return Chat(self, chat_id)
|
||||||
|
return None
|
||||||
|
|
||||||
def get_contacts(
|
def get_contacts(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
with_self: bool = False,
|
with_self: bool = False,
|
||||||
verified_only: bool = False,
|
verified_only: bool = False,
|
||||||
snapshot: bool = False,
|
snapshot: bool = False,
|
||||||
) -> Union[List[Contact], List[AttrDict]]:
|
) -> Union[list[Contact], list[AttrDict]]:
|
||||||
"""Get a filtered list of contacts.
|
"""Get a filtered list of contacts.
|
||||||
|
|
||||||
:param query: if a string is specified, only return contacts
|
:param query: if a string is specified, only return contacts
|
||||||
@@ -152,7 +192,7 @@ class Account:
|
|||||||
no_specials: bool = False,
|
no_specials: bool = False,
|
||||||
alldone_hint: bool = False,
|
alldone_hint: bool = False,
|
||||||
snapshot: bool = False,
|
snapshot: bool = False,
|
||||||
) -> Union[List[Chat], List[AttrDict]]:
|
) -> Union[list[Chat], list[AttrDict]]:
|
||||||
"""Return list of chats.
|
"""Return list of chats.
|
||||||
|
|
||||||
:param query: if a string is specified only chats matching this query are returned.
|
:param query: if a string is specified only chats matching this query are returned.
|
||||||
@@ -204,13 +244,13 @@ class Account:
|
|||||||
The function returns immediately and the handshake runs in background, sending
|
The function returns immediately and the handshake runs in background, sending
|
||||||
and receiving several messages.
|
and receiving several messages.
|
||||||
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||||
See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
|
See https://securejoin.delta.chat/ for protocol details.
|
||||||
|
|
||||||
:param qrdata: The text of the scanned QR code.
|
:param qrdata: The text of the scanned QR code.
|
||||||
"""
|
"""
|
||||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||||
|
|
||||||
def get_qr_code(self) -> Tuple[str, str]:
|
def get_qr_code(self) -> tuple[str, str]:
|
||||||
"""Get Setup-Contact QR Code text and SVG data.
|
"""Get Setup-Contact QR Code text and SVG data.
|
||||||
|
|
||||||
this data needs to be transferred to another Delta Chat account
|
this data needs to be transferred to another Delta Chat account
|
||||||
@@ -222,15 +262,15 @@ class Account:
|
|||||||
"""Return the Message instance with the given ID."""
|
"""Return the Message instance with the given ID."""
|
||||||
return Message(self, msg_id)
|
return Message(self, msg_id)
|
||||||
|
|
||||||
def mark_seen_messages(self, messages: List[Message]) -> None:
|
def mark_seen_messages(self, messages: list[Message]) -> None:
|
||||||
"""Mark the given set of messages as seen."""
|
"""Mark the given set of messages as seen."""
|
||||||
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||||
|
|
||||||
def delete_messages(self, messages: List[Message]) -> None:
|
def delete_messages(self, messages: list[Message]) -> None:
|
||||||
"""Delete messages (local and remote)."""
|
"""Delete messages (local and remote)."""
|
||||||
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||||
|
|
||||||
def get_fresh_messages(self) -> List[Message]:
|
def get_fresh_messages(self) -> list[Message]:
|
||||||
"""Return the list of fresh messages, newest messages first.
|
"""Return the list of fresh messages, newest messages first.
|
||||||
|
|
||||||
This call is intended for displaying notifications.
|
This call is intended for displaying notifications.
|
||||||
@@ -240,17 +280,48 @@ class Account:
|
|||||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||||
|
|
||||||
def get_next_messages(self) -> List[Message]:
|
def get_next_messages(self) -> list[Message]:
|
||||||
"""Return a list of next messages."""
|
"""Return a list of next messages."""
|
||||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
def wait_next_messages(self) -> List[Message]:
|
def wait_next_messages(self) -> list[Message]:
|
||||||
"""Wait for new messages and return a list of them."""
|
"""Wait for new messages and return a list of them."""
|
||||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
def wait_for_incoming_msg_event(self):
|
||||||
|
"""Wait for incoming message event and return it."""
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
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()
|
||||||
|
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||||
|
break
|
||||||
|
|
||||||
|
def wait_for_securejoin_joiner_success(self):
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||||
|
break
|
||||||
|
|
||||||
|
def wait_for_reactions_changed(self):
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
if event.kind == EventType.REACTIONS_CHANGED:
|
||||||
|
return event
|
||||||
|
|
||||||
|
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||||
warn(
|
warn(
|
||||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||||
@@ -267,3 +338,13 @@ class Account:
|
|||||||
def import_backup(self, path, passphrase: str = "") -> None:
|
def import_backup(self, path, passphrase: str = "") -> None:
|
||||||
"""Import backup."""
|
"""Import backup."""
|
||||||
self._rpc.import_backup(self.id, str(path), passphrase)
|
self._rpc.import_backup(self.id, str(path), passphrase)
|
||||||
|
|
||||||
|
def export_self_keys(self, path) -> None:
|
||||||
|
"""Export keys."""
|
||||||
|
passphrase = "" # Setting passphrase is currently not supported.
|
||||||
|
self._rpc.export_self_keys(self.id, str(path), passphrase)
|
||||||
|
|
||||||
|
def import_self_keys(self, path) -> None:
|
||||||
|
"""Import keys."""
|
||||||
|
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||||
|
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict
|
||||||
from .const import ChatVisibility, ViewType
|
from .const import ChatVisibility, ViewType
|
||||||
@@ -54,14 +57,14 @@ class Chat:
|
|||||||
"""
|
"""
|
||||||
if duration is not None:
|
if duration is not None:
|
||||||
assert duration > 0, "Invalid duration"
|
assert duration > 0, "Invalid duration"
|
||||||
dur: Union[str, dict] = {"Until": duration}
|
dur: dict = {"kind": "Until", "duration": duration}
|
||||||
else:
|
else:
|
||||||
dur = "Forever"
|
dur = {"kind": "Forever"}
|
||||||
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||||
|
|
||||||
def unmute(self) -> None:
|
def unmute(self) -> None:
|
||||||
"""Unmute this chat."""
|
"""Unmute this chat."""
|
||||||
self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
|
self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
|
||||||
|
|
||||||
def pin(self) -> None:
|
def pin(self) -> None:
|
||||||
"""Pin this chat."""
|
"""Pin this chat."""
|
||||||
@@ -84,14 +87,16 @@ class Chat:
|
|||||||
self._rpc.set_chat_name(self.account.id, self.id, name)
|
self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||||
|
|
||||||
def set_ephemeral_timer(self, timer: int) -> None:
|
def set_ephemeral_timer(self, timer: int) -> None:
|
||||||
"""Set ephemeral timer of this chat."""
|
"""Set ephemeral timer of this chat in seconds.
|
||||||
|
|
||||||
|
0 means the timer is disabled, use 1 for immediate deletion."""
|
||||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||||
|
|
||||||
def get_encryption_info(self) -> str:
|
def get_encryption_info(self) -> str:
|
||||||
"""Return encryption info for this chat."""
|
"""Return encryption info for this chat."""
|
||||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
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) -> tuple[str, str]:
|
||||||
"""Get Join-Group QR code text and SVG data."""
|
"""Get Join-Group QR code text and SVG data."""
|
||||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||||
|
|
||||||
@@ -115,7 +120,7 @@ class Chat:
|
|||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
viewtype: Optional[ViewType] = None,
|
viewtype: Optional[ViewType] = None,
|
||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
location: Optional[Tuple[float, float]] = None,
|
location: Optional[tuple[float, float]] = None,
|
||||||
override_sender_name: Optional[str] = None,
|
override_sender_name: Optional[str] = None,
|
||||||
quoted_msg: Optional[Union[int, Message]] = None,
|
quoted_msg: Optional[Union[int, Message]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
@@ -140,6 +145,10 @@ class Chat:
|
|||||||
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
|
def send_file(self, path):
|
||||||
|
"""Send a file and return the resulting Message instance."""
|
||||||
|
return self.send_message(file=path)
|
||||||
|
|
||||||
def send_videochat_invitation(self) -> Message:
|
def send_videochat_invitation(self) -> Message:
|
||||||
"""Send a videochat invitation and return the resulting Message instance."""
|
"""Send a videochat invitation and return the resulting Message instance."""
|
||||||
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||||
@@ -150,7 +159,7 @@ class Chat:
|
|||||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
def forward_messages(self, messages: List[Message]) -> None:
|
def forward_messages(self, messages: list[Message]) -> None:
|
||||||
"""Forward a list of messages to this chat."""
|
"""Forward a list of messages to this chat."""
|
||||||
msg_ids = [msg.id for msg in messages]
|
msg_ids = [msg.id for msg in messages]
|
||||||
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||||
@@ -160,11 +169,12 @@ class Chat:
|
|||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
quoted_msg: Optional[int] = None,
|
quoted_msg: Optional[int] = None,
|
||||||
|
viewtype: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set draft message."""
|
"""Set draft message."""
|
||||||
if isinstance(quoted_msg, Message):
|
if isinstance(quoted_msg, Message):
|
||||||
quoted_msg = quoted_msg.id
|
quoted_msg = quoted_msg.id
|
||||||
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype)
|
||||||
|
|
||||||
def remove_draft(self) -> None:
|
def remove_draft(self) -> None:
|
||||||
"""Remove draft message."""
|
"""Remove draft message."""
|
||||||
@@ -181,7 +191,7 @@ class Chat:
|
|||||||
snapshot["message"] = Message(self.account, snapshot.id)
|
snapshot["message"] = Message(self.account, snapshot.id)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||||
"""get the list of messages in this chat."""
|
"""get the list of messages in this chat."""
|
||||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||||
@@ -216,7 +226,7 @@ class Chat:
|
|||||||
contact_id = cnt
|
contact_id = cnt
|
||||||
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||||
|
|
||||||
def get_contacts(self) -> List[Contact]:
|
def get_contacts(self) -> list[Contact]:
|
||||||
"""Get the contacts belonging to this chat.
|
"""Get the contacts belonging to this chat.
|
||||||
|
|
||||||
For single/direct chats self-address is not included.
|
For single/direct chats self-address is not included.
|
||||||
@@ -240,7 +250,7 @@ class Chat:
|
|||||||
contact: Optional[Contact] = None,
|
contact: Optional[Contact] = None,
|
||||||
timestamp_from: Optional["datetime"] = None,
|
timestamp_from: Optional["datetime"] = None,
|
||||||
timestamp_to: Optional["datetime"] = None,
|
timestamp_to: Optional["datetime"] = None,
|
||||||
) -> List[AttrDict]:
|
) -> list[AttrDict]:
|
||||||
"""Get list of location snapshots for the given contact in the given timespan."""
|
"""Get list of location snapshots for the given contact in the given timespan."""
|
||||||
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||||
@@ -248,7 +258,7 @@ class Chat:
|
|||||||
|
|
||||||
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||||
locations = []
|
locations = []
|
||||||
contacts: Dict[int, Contact] = {}
|
contacts: dict[int, Contact] = {}
|
||||||
for loc in result:
|
for loc in result:
|
||||||
location = AttrDict(loc)
|
location = AttrDict(loc)
|
||||||
location["chat"] = self
|
location["chat"] = self
|
||||||
@@ -256,3 +266,11 @@ class Chat:
|
|||||||
location["message"] = Message(self.account, location.msg_id)
|
location["message"] = Message(self.account, location.msg_id)
|
||||||
locations.append(location)
|
locations.append(location)
|
||||||
return locations
|
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})
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
"""Event loop implementations offering high level event handling/hooking."""
|
"""Event loop implementations offering high level event handling/hooking."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
Iterable,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
|
||||||
Tuple,
|
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
@@ -39,16 +38,16 @@ class Client:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
account: "Account",
|
account: "Account",
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
|
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||||
logger: Optional[logging.Logger] = None,
|
logger: Optional[logging.Logger] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.account = account
|
self.account = account
|
||||||
self.logger = logger or logging
|
self.logger = logger or logging
|
||||||
self._hooks: Dict[type, Set[tuple]] = {}
|
self._hooks: dict[type, set[tuple]] = {}
|
||||||
self._should_process_messages = 0
|
self._should_process_messages = 0
|
||||||
self.add_hooks(hooks or [])
|
self.add_hooks(hooks or [])
|
||||||
|
|
||||||
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
|
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||||
for hook, event in hooks:
|
for hook, event in hooks:
|
||||||
self.add_hook(hook, event)
|
self.add_hook(hook, event)
|
||||||
|
|
||||||
@@ -92,7 +91,7 @@ class Client:
|
|||||||
"""Process events forever."""
|
"""Process events forever."""
|
||||||
self.run_until(lambda _: False)
|
self.run_until(lambda _: False)
|
||||||
|
|
||||||
def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||||
"""Process events until the given callable evaluates to True.
|
"""Process events until the given callable evaluates to True.
|
||||||
|
|
||||||
The callable should accept an AttrDict object representing the
|
The callable should accept an AttrDict object representing the
|
||||||
@@ -105,10 +104,10 @@ class Client:
|
|||||||
self._process_messages() # Process old messages.
|
self._process_messages() # Process old messages.
|
||||||
while True:
|
while True:
|
||||||
event = self.account.wait_for_event()
|
event = self.account.wait_for_event()
|
||||||
event["type"] = EventType(event.type)
|
event["kind"] = EventType(event.kind)
|
||||||
event["account"] = self.account
|
event["account"] = self.account
|
||||||
self._on_event(event)
|
self._on_event(event)
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
self._process_messages()
|
self._process_messages()
|
||||||
|
|
||||||
stop = func(event)
|
stop = func(event)
|
||||||
@@ -196,7 +195,7 @@ class Client:
|
|||||||
|
|
||||||
|
|
||||||
class Bot(Client):
|
class Bot(Client):
|
||||||
"""Simple bot implementation that listent to events of a single account."""
|
"""Simple bot implementation that listens to events of a single account."""
|
||||||
|
|
||||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
kwargs.setdefault("bot", "1")
|
kwargs.setdefault("bot", "1")
|
||||||
|
|||||||
@@ -59,6 +59,19 @@ class EventType(str, Enum):
|
|||||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||||
|
CHATLIST_CHANGED = "ChatlistChanged"
|
||||||
|
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||||
|
CONFIG_SYNCED = "ConfigSynced"
|
||||||
|
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatId(IntEnum):
|
||||||
|
"""Special chat ids"""
|
||||||
|
|
||||||
|
TRASH = 3
|
||||||
|
ARCHIVED_LINK = 6
|
||||||
|
ALLDONE_HINT = 7
|
||||||
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
class ChatType(IntEnum):
|
class ChatType(IntEnum):
|
||||||
@@ -102,6 +115,7 @@ class ViewType(str, Enum):
|
|||||||
FILE = "File"
|
FILE = "File"
|
||||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||||
WEBXDC = "Webxdc"
|
WEBXDC = "Webxdc"
|
||||||
|
VCARD = "Vcard"
|
||||||
|
|
||||||
|
|
||||||
class SystemMessageType(str, Enum):
|
class SystemMessageType(str, Enum):
|
||||||
@@ -122,3 +136,107 @@ class SystemMessageType(str, Enum):
|
|||||||
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
|
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
|
||||||
MULTI_DEVICE_SYNC = "MultiDeviceSync"
|
MULTI_DEVICE_SYNC = "MultiDeviceSync"
|
||||||
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
|
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
|
||||||
|
|
||||||
|
|
||||||
|
class MessageState(IntEnum):
|
||||||
|
"""State of the message."""
|
||||||
|
|
||||||
|
UNDEFINED = 0
|
||||||
|
IN_FRESH = 10
|
||||||
|
IN_NOTICED = 13
|
||||||
|
IN_SEEN = 16
|
||||||
|
OUT_PREPARING = 18
|
||||||
|
OUT_DRAFT = 19
|
||||||
|
OUT_PENDING = 20
|
||||||
|
OUT_FAILED = 24
|
||||||
|
OUT_DELIVERED = 26
|
||||||
|
OUT_MDN_RCVD = 28
|
||||||
|
|
||||||
|
|
||||||
|
class MessageId(IntEnum):
|
||||||
|
"""Special message ids"""
|
||||||
|
|
||||||
|
DAYMARKER = 9
|
||||||
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
|
class CertificateChecks(IntEnum):
|
||||||
|
"""Certificate checks mode"""
|
||||||
|
|
||||||
|
AUTOMATIC = 0
|
||||||
|
STRICT = 1
|
||||||
|
ACCEPT_INVALID_CERTIFICATES = 3
|
||||||
|
|
||||||
|
|
||||||
|
class Connectivity(IntEnum):
|
||||||
|
"""Connectivity states"""
|
||||||
|
|
||||||
|
NOT_CONNECTED = 1000
|
||||||
|
CONNECTING = 2000
|
||||||
|
WORKING = 3000
|
||||||
|
CONNECTED = 4000
|
||||||
|
|
||||||
|
|
||||||
|
class KeyGenType(IntEnum):
|
||||||
|
"""Type of the key to generate"""
|
||||||
|
|
||||||
|
DEFAULT = 0
|
||||||
|
RSA2048 = 1
|
||||||
|
ED25519 = 2
|
||||||
|
RSA4096 = 3
|
||||||
|
|
||||||
|
|
||||||
|
# "Lp" means "login parameters"
|
||||||
|
class LpAuthFlag(IntEnum):
|
||||||
|
"""Authorization flags"""
|
||||||
|
|
||||||
|
OAUTH2 = 0x2
|
||||||
|
NORMAL = 0x4
|
||||||
|
|
||||||
|
|
||||||
|
class MediaQuality(IntEnum):
|
||||||
|
"""Media quality setting"""
|
||||||
|
|
||||||
|
BALANCED = 0
|
||||||
|
WORSE = 1
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderStatus(IntEnum):
|
||||||
|
"""Provider status according to manual testing"""
|
||||||
|
|
||||||
|
OK = 1
|
||||||
|
PREPARATION = 2
|
||||||
|
BROKEN = 3
|
||||||
|
|
||||||
|
|
||||||
|
class PushNotifyState(IntEnum):
|
||||||
|
"""Push notifications state"""
|
||||||
|
|
||||||
|
NOT_CONNECTED = 0
|
||||||
|
HEARTBEAT = 1
|
||||||
|
CONNECTED = 2
|
||||||
|
|
||||||
|
|
||||||
|
class ShowEmails(IntEnum):
|
||||||
|
"""Show emails mode"""
|
||||||
|
|
||||||
|
OFF = 0
|
||||||
|
ACCEPTED_CONTACTS = 1
|
||||||
|
ALL = 2
|
||||||
|
|
||||||
|
|
||||||
|
class SocketSecurity(IntEnum):
|
||||||
|
"""Socket security"""
|
||||||
|
|
||||||
|
AUTOMATIC = 0
|
||||||
|
SSL = 1
|
||||||
|
STARTTLS = 2
|
||||||
|
PLAIN = 3
|
||||||
|
|
||||||
|
|
||||||
|
class VideochatType(IntEnum):
|
||||||
|
"""Video chat URL type"""
|
||||||
|
|
||||||
|
UNKNOWN = 0
|
||||||
|
BASICWEBRTC = 1
|
||||||
|
JITSI = 2
|
||||||
|
|||||||
@@ -60,3 +60,6 @@ class Contact:
|
|||||||
self.account,
|
self.account,
|
||||||
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
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])
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from typing import TYPE_CHECKING, Dict, List
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict
|
||||||
from .account import Account
|
from .account import Account
|
||||||
@@ -21,7 +23,7 @@ class DeltaChat:
|
|||||||
account_id = self.rpc.add_account()
|
account_id = self.rpc.add_account()
|
||||||
return Account(self, account_id)
|
return Account(self, account_id)
|
||||||
|
|
||||||
def get_all_accounts(self) -> List[Account]:
|
def get_all_accounts(self) -> list[Account]:
|
||||||
"""Return a list of all available accounts."""
|
"""Return a list of all available accounts."""
|
||||||
account_ids = self.rpc.get_all_account_ids()
|
account_ids = self.rpc.get_all_account_ids()
|
||||||
return [Account(self, account_id) for account_id in account_ids]
|
return [Account(self, account_id) for account_id in account_ids]
|
||||||
@@ -44,6 +46,6 @@ class DeltaChat:
|
|||||||
"""Get information about the Delta Chat core in this system."""
|
"""Get information about the Delta Chat core in this system."""
|
||||||
return AttrDict(self.rpc.get_system_info())
|
return AttrDict(self.rpc.get_system_info())
|
||||||
|
|
||||||
def set_translations(self, translations: Dict[str, str]) -> None:
|
def set_translations(self, translations: dict[str, str]) -> None:
|
||||||
"""Set stock translation strings."""
|
"""Set stock translation strings."""
|
||||||
self.rpc.set_stock_strings(translations)
|
self.rpc.set_stock_strings(translations)
|
||||||
|
|||||||
226
deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py
Normal file
226
deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
Internal Python-level IMAP handling used by the tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import imaplib
|
||||||
|
import io
|
||||||
|
import pathlib
|
||||||
|
import ssl
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from imap_tools import (
|
||||||
|
AND,
|
||||||
|
Header,
|
||||||
|
MailBox,
|
||||||
|
MailBoxTls,
|
||||||
|
MailMessage,
|
||||||
|
MailMessageFlags,
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import Account, const
|
||||||
|
|
||||||
|
FLAGS = b"FLAGS"
|
||||||
|
FETCH = b"FETCH"
|
||||||
|
ALL = "1:*"
|
||||||
|
|
||||||
|
|
||||||
|
class DirectImap:
|
||||||
|
def __init__(self, account: Account) -> None:
|
||||||
|
self.account = account
|
||||||
|
self.logid = account.get_config("displayname") or id(account)
|
||||||
|
self._idling = False
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
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"))
|
||||||
|
|
||||||
|
user = self.account.get_config("addr")
|
||||||
|
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.login(user, pw)
|
||||||
|
|
||||||
|
self.select_folder("INBOX")
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
try:
|
||||||
|
self.conn.logout()
|
||||||
|
except (OSError, imaplib.IMAP4.abort):
|
||||||
|
print("Could not logout direct_imap conn")
|
||||||
|
|
||||||
|
def create_folder(self, foldername):
|
||||||
|
try:
|
||||||
|
self.conn.folder.create(foldername)
|
||||||
|
except errors.MailboxFolderCreateError as e:
|
||||||
|
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||||
|
|
||||||
|
def select_folder(self, foldername: str) -> tuple:
|
||||||
|
assert not self._idling
|
||||||
|
return self.conn.folder.set(foldername)
|
||||||
|
|
||||||
|
def select_config_folder(self, config_name: str):
|
||||||
|
"""Return info about selected folder if it is
|
||||||
|
configured, otherwise None.
|
||||||
|
"""
|
||||||
|
if "_" not in config_name:
|
||||||
|
config_name = f"configured_{config_name}_folder"
|
||||||
|
foldername = self.account.get_config(config_name)
|
||||||
|
if foldername:
|
||||||
|
return self.select_folder(foldername)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_folders(self) -> list[str]:
|
||||||
|
"""return list of all existing folder names."""
|
||||||
|
assert not self._idling
|
||||||
|
return [folder.name for folder in self.conn.folder.list()]
|
||||||
|
|
||||||
|
def delete(self, uid_list: str, expunge=True):
|
||||||
|
"""delete a range of messages (imap-syntax).
|
||||||
|
If expunge is true, perform the expunge-operation
|
||||||
|
to make sure the messages are really gone and not
|
||||||
|
just flagged as deleted.
|
||||||
|
"""
|
||||||
|
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
|
||||||
|
if expunge:
|
||||||
|
self.conn.expunge()
|
||||||
|
|
||||||
|
def get_all_messages(self) -> list[MailMessage]:
|
||||||
|
assert not self._idling
|
||||||
|
return list(self.conn.fetch())
|
||||||
|
|
||||||
|
def get_unread_messages(self) -> list[str]:
|
||||||
|
assert not self._idling
|
||||||
|
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||||
|
|
||||||
|
def mark_all_read(self):
|
||||||
|
messages = self.get_unread_messages()
|
||||||
|
if messages:
|
||||||
|
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||||
|
print("marked seen:", messages, res)
|
||||||
|
|
||||||
|
def get_unread_cnt(self) -> int:
|
||||||
|
return len(self.get_unread_messages())
|
||||||
|
|
||||||
|
def dump_imap_structures(self, dir, logfile):
|
||||||
|
assert not self._idling
|
||||||
|
stream = io.StringIO()
|
||||||
|
|
||||||
|
def log(*args, **kwargs):
|
||||||
|
kwargs["file"] = stream
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
empty_folders = []
|
||||||
|
for imapfolder in self.list_folders():
|
||||||
|
self.select_folder(imapfolder)
|
||||||
|
messages = list(self.get_all_messages())
|
||||||
|
if not messages:
|
||||||
|
empty_folders.append(imapfolder)
|
||||||
|
continue
|
||||||
|
|
||||||
|
log("---------", imapfolder, len(messages), "messages ---------")
|
||||||
|
# get message content without auto-marking it as seen
|
||||||
|
# fetching 'RFC822' would mark it as seen.
|
||||||
|
for msg in self.conn.fetch(mark_seen=False):
|
||||||
|
body = getattr(msg.obj, "text", None)
|
||||||
|
if not body:
|
||||||
|
body = getattr(msg.obj, "html", None)
|
||||||
|
if not body:
|
||||||
|
log("Message", msg.uid, "has empty body")
|
||||||
|
continue
|
||||||
|
|
||||||
|
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
fn = path.joinpath(str(msg.uid))
|
||||||
|
fn.write_bytes(body)
|
||||||
|
log("Message", msg.uid, fn)
|
||||||
|
log(
|
||||||
|
"Message",
|
||||||
|
msg.uid,
|
||||||
|
msg.flags,
|
||||||
|
"Message-Id:",
|
||||||
|
msg.obj.get("Message-Id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if empty_folders:
|
||||||
|
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||||
|
|
||||||
|
print(stream.getvalue(), file=logfile)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def idle(self):
|
||||||
|
"""return Idle ContextManager."""
|
||||||
|
idle_manager = IdleManager(self)
|
||||||
|
try:
|
||||||
|
yield idle_manager
|
||||||
|
finally:
|
||||||
|
idle_manager.done()
|
||||||
|
|
||||||
|
def append(self, folder: str, msg: str):
|
||||||
|
"""Upload a message to *folder*.
|
||||||
|
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||||
|
"""
|
||||||
|
if msg.startswith("\n"):
|
||||||
|
msg = msg[1:]
|
||||||
|
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
|
||||||
|
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||||
|
|
||||||
|
def get_uid_by_message_id(self, message_id) -> str:
|
||||||
|
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||||
|
if len(msgs) == 0:
|
||||||
|
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||||
|
return msgs[0]
|
||||||
|
|
||||||
|
|
||||||
|
class IdleManager:
|
||||||
|
def __init__(self, direct_imap) -> None:
|
||||||
|
self.direct_imap = direct_imap
|
||||||
|
self.log = direct_imap.account.log
|
||||||
|
# fetch latest messages before starting idle so that it only
|
||||||
|
# returns messages that arrive anew
|
||||||
|
self.direct_imap.conn.fetch("1:*")
|
||||||
|
self.direct_imap.conn.idle.start()
|
||||||
|
|
||||||
|
def check(self, timeout=None) -> list[bytes]:
|
||||||
|
"""(blocking) wait for next idle message from server."""
|
||||||
|
self.log("imap-direct: calling idle_check")
|
||||||
|
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||||
|
self.log(f"imap-direct: idle_check returned {res!r}")
|
||||||
|
return res
|
||||||
|
|
||||||
|
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||||
|
while True:
|
||||||
|
for item in self.check(timeout=timeout):
|
||||||
|
if b"EXISTS" in item or b"RECENT" in item:
|
||||||
|
return item
|
||||||
|
|
||||||
|
def wait_for_seen(self, timeout=None) -> int:
|
||||||
|
"""Return first message with SEEN flag from a running idle-stream."""
|
||||||
|
while True:
|
||||||
|
for item in self.check(timeout=timeout):
|
||||||
|
if FETCH in item:
|
||||||
|
self.log(str(item))
|
||||||
|
if FLAGS in item and rb"\Seen" in item:
|
||||||
|
return int(item.split(b" ")[1])
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
"""send idle-done to server if we are currently in idle mode."""
|
||||||
|
return self.direct_imap.conn.idle.stop()
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
"""High-level classes for event processing and filtering."""
|
"""High-level classes for event processing and filtering."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union
|
||||||
|
|
||||||
from .const import EventType
|
from .const import EventType
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@ class RawEvent(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
if self.types and event.type not in self.types:
|
if self.types and event.kind not in self.types:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
@@ -262,9 +265,9 @@ class HookCollection:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
|
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
|
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]:
|
||||||
return iter(self._hooks)
|
return iter(self._hooks)
|
||||||
|
|
||||||
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import json
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict, futuremethod
|
||||||
|
from .const import EventType
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -21,9 +22,10 @@ class Message:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
def send_reaction(self, *reaction: str):
|
def send_reaction(self, *reaction: str) -> "Message":
|
||||||
"""Send a reaction to this message."""
|
"""Send a reaction to this message."""
|
||||||
self._rpc.send_reaction(self.account.id, self.id, reaction)
|
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||||
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
def get_snapshot(self) -> AttrDict:
|
def get_snapshot(self) -> AttrDict:
|
||||||
"""Get a snapshot with the properties of this message."""
|
"""Get a snapshot with the properties of this message."""
|
||||||
@@ -42,6 +44,10 @@ class Message:
|
|||||||
return AttrDict(reactions)
|
return AttrDict(reactions)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_sender_contact(self) -> Contact:
|
||||||
|
from_id = self.get_snapshot().from_id
|
||||||
|
return self.account.get_contact_by_id(from_id)
|
||||||
|
|
||||||
def mark_seen(self) -> None:
|
def mark_seen(self) -> None:
|
||||||
"""Mark the message as seen."""
|
"""Mark the message as seen."""
|
||||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||||
@@ -57,3 +63,18 @@ class Message:
|
|||||||
|
|
||||||
def get_webxdc_info(self) -> dict:
|
def get_webxdc_info(self) -> dict:
|
||||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||||
|
|
||||||
|
def wait_until_delivered(self) -> None:
|
||||||
|
"""Consume events until the message is delivered."""
|
||||||
|
while True:
|
||||||
|
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))
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import json
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import urllib.request
|
import random
|
||||||
from typing import AsyncGenerator, List, Optional
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||||
|
from ._utils import futuremethod
|
||||||
from .rpc import Rpc
|
from .rpc import Rpc
|
||||||
|
|
||||||
|
|
||||||
def get_temp_credentials() -> dict:
|
def get_temp_credentials() -> dict:
|
||||||
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||||
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||||
|
password = f"{username}${username}"
|
||||||
request = urllib.request.Request(url, method="POST")
|
addr = f"{username}@{domain}"
|
||||||
with urllib.request.urlopen(request, timeout=60) as f:
|
return {"email": addr, "password": password}
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
class ACFactory:
|
class ACFactory:
|
||||||
@@ -23,7 +24,9 @@ class ACFactory:
|
|||||||
self.deltachat = deltachat
|
self.deltachat = deltachat
|
||||||
|
|
||||||
def get_unconfigured_account(self) -> Account:
|
def get_unconfigured_account(self) -> Account:
|
||||||
return self.deltachat.add_account()
|
account = self.deltachat.add_account()
|
||||||
|
account.set_config("verified_one_on_one_chats", "1")
|
||||||
|
return account
|
||||||
|
|
||||||
def get_unconfigured_bot(self) -> Bot:
|
def get_unconfigured_bot(self) -> Bot:
|
||||||
return Bot(self.get_unconfigured_account())
|
return Bot(self.get_unconfigured_account())
|
||||||
@@ -37,9 +40,10 @@ class ACFactory:
|
|||||||
assert not account.is_configured()
|
assert not account.is_configured()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
def new_configured_account(self) -> Account:
|
@futuremethod
|
||||||
|
def new_configured_account(self):
|
||||||
account = self.new_preconfigured_account()
|
account = self.new_preconfigured_account()
|
||||||
account.configure()
|
yield account.configure.future()
|
||||||
assert account.is_configured()
|
assert account.is_configured()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
@@ -49,17 +53,29 @@ class ACFactory:
|
|||||||
bot.configure(credentials["email"], credentials["password"])
|
bot.configure(credentials["email"], credentials["password"])
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
def get_online_account(self) -> Account:
|
@futuremethod
|
||||||
account = self.new_configured_account()
|
def get_online_account(self):
|
||||||
account.start_io()
|
account = yield self.new_configured_account.future()
|
||||||
while True:
|
account.bring_online()
|
||||||
event = account.wait_for_event()
|
|
||||||
if event.type == EventType.IMAP_INBOX_IDLE:
|
|
||||||
break
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
def get_online_accounts(self, num: int) -> List[Account]:
|
def get_online_accounts(self, num: int) -> list[Account]:
|
||||||
return [self.get_online_account() for _ in range(num)]
|
futures = [self.get_online_account.future() for _ in range(num)]
|
||||||
|
return [f() for f in futures]
|
||||||
|
|
||||||
|
def resetup_account(self, ac: Account) -> Account:
|
||||||
|
"""Resetup account from scratch, losing the encryption key."""
|
||||||
|
ac.stop_io()
|
||||||
|
ac_clone = self.get_unconfigured_account()
|
||||||
|
for i in ["addr", "mail_pw"]:
|
||||||
|
ac_clone.set_config(i, ac.get_config(i))
|
||||||
|
ac.remove()
|
||||||
|
ac_clone.configure()
|
||||||
|
return ac_clone
|
||||||
|
|
||||||
|
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||||
|
ac2.create_chat(ac1)
|
||||||
|
return ac1.create_chat(ac2)
|
||||||
|
|
||||||
def send_message(
|
def send_message(
|
||||||
self,
|
self,
|
||||||
@@ -95,7 +111,7 @@ class ACFactory:
|
|||||||
group=group,
|
group=group,
|
||||||
)
|
)
|
||||||
|
|
||||||
return to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
|||||||
@@ -1,17 +1,62 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from queue import Queue
|
from queue import Empty, Queue
|
||||||
from threading import Event, Thread
|
from threading import Event, Thread
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Iterator, Optional
|
||||||
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RpcFuture:
|
||||||
|
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||||
|
self.rpc = rpc
|
||||||
|
self.request_id = request_id
|
||||||
|
self.event = event
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self.event.wait()
|
||||||
|
response = self.rpc.request_results.pop(self.request_id)
|
||||||
|
if "error" in response:
|
||||||
|
raise JsonRpcError(response["error"])
|
||||||
|
if "result" in response:
|
||||||
|
return response["result"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class RpcMethod:
|
||||||
|
def __init__(self, rpc: "Rpc", name: str):
|
||||||
|
self.rpc = rpc
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __call__(self, *args) -> Any:
|
||||||
|
"""Synchronously calls JSON-RPC method."""
|
||||||
|
future = self.future(*args)
|
||||||
|
return future()
|
||||||
|
|
||||||
|
def future(self, *args) -> Any:
|
||||||
|
"""Asynchronously calls JSON-RPC method."""
|
||||||
|
request_id = next(self.rpc.id_iterator)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": self.name,
|
||||||
|
"params": args,
|
||||||
|
"id": request_id,
|
||||||
|
}
|
||||||
|
event = Event()
|
||||||
|
self.rpc.request_events[request_id] = event
|
||||||
|
self.rpc.request_queue.put(request)
|
||||||
|
|
||||||
|
return RpcFuture(self.rpc, request_id, event)
|
||||||
|
|
||||||
|
|
||||||
class Rpc:
|
class Rpc:
|
||||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||||
"""The given arguments will be passed to subprocess.Popen()"""
|
"""The given arguments will be passed to subprocess.Popen()"""
|
||||||
@@ -23,12 +68,12 @@ class Rpc:
|
|||||||
|
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
self.process: subprocess.Popen
|
self.process: subprocess.Popen
|
||||||
self.id: int
|
self.id_iterator: Iterator[int]
|
||||||
self.event_queues: Dict[int, Queue]
|
self.event_queues: dict[int, Queue]
|
||||||
# Map from request ID to `threading.Event`.
|
# Map from request ID to `threading.Event`.
|
||||||
self.request_events: Dict[int, Event]
|
self.request_events: dict[int, Event]
|
||||||
# Map from request ID to the result.
|
# Map from request ID to the result.
|
||||||
self.request_results: Dict[int, Any]
|
self.request_results: dict[int, Any]
|
||||||
self.request_queue: Queue[Any]
|
self.request_queue: Queue[Any]
|
||||||
self.closing: bool
|
self.closing: bool
|
||||||
self.reader_thread: Thread
|
self.reader_thread: Thread
|
||||||
@@ -54,7 +99,7 @@ class Rpc:
|
|||||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||||
**self._kwargs,
|
**self._kwargs,
|
||||||
)
|
)
|
||||||
self.id = 0
|
self.id_iterator = itertools.count(start=1)
|
||||||
self.event_queues = {}
|
self.event_queues = {}
|
||||||
self.request_events = {}
|
self.request_events = {}
|
||||||
self.request_results = {}
|
self.request_results = {}
|
||||||
@@ -131,7 +176,9 @@ class Rpc:
|
|||||||
event = self.get_next_event()
|
event = self.get_next_event()
|
||||||
account_id = event["contextId"]
|
account_id = event["contextId"]
|
||||||
queue = self.get_queue(account_id)
|
queue = self.get_queue(account_id)
|
||||||
queue.put(event["event"])
|
event = event["event"]
|
||||||
|
logging.debug("account_id=%d got an event %s", account_id, event)
|
||||||
|
queue.put(event)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Log an exception if the event loop dies.
|
# Log an exception if the event loop dies.
|
||||||
logging.exception("Exception in the event loop")
|
logging.exception("Exception in the event loop")
|
||||||
@@ -141,27 +188,14 @@ class Rpc:
|
|||||||
queue = self.get_queue(account_id)
|
queue = self.get_queue(account_id)
|
||||||
return queue.get()
|
return queue.get()
|
||||||
|
|
||||||
|
def clear_all_events(self, account_id: int):
|
||||||
|
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||||
|
queue = self.get_queue(account_id)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
queue.get_nowait()
|
||||||
|
except Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
def __getattr__(self, attr: str):
|
def __getattr__(self, attr: str):
|
||||||
def method(*args) -> Any:
|
return RpcMethod(self, attr)
|
||||||
self.id += 1
|
|
||||||
request_id = self.id
|
|
||||||
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": attr,
|
|
||||||
"params": args,
|
|
||||||
"id": self.id,
|
|
||||||
}
|
|
||||||
event = Event()
|
|
||||||
self.request_events[request_id] = event
|
|
||||||
self.request_queue.put(request)
|
|
||||||
event.wait()
|
|
||||||
|
|
||||||
response = self.request_results.pop(request_id)
|
|
||||||
if "error" in response:
|
|
||||||
raise JsonRpcError(response["error"])
|
|
||||||
if "result" in response:
|
|
||||||
return response["result"]
|
|
||||||
return None
|
|
||||||
|
|
||||||
return method
|
|
||||||
|
|||||||
217
deltachat-rpc-client/tests/test_chatlist_events.py
Normal file
217
deltachat-rpc-client/tests/test_chatlist_events.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from deltachat_rpc_client import Account, EventType, const
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_chatlist_and_specific_item(account, chat_id):
|
||||||
|
first_event = ""
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.CHATLIST_CHANGED:
|
||||||
|
first_event = "change"
|
||||||
|
break
|
||||||
|
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||||
|
first_event = "item_change"
|
||||||
|
break
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.CHATLIST_CHANGED and first_event == "item_change":
|
||||||
|
break
|
||||||
|
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id and first_event == "change":
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_chatlist_specific_item(account, chat_id):
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_chatlist(account):
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.CHATLIST_CHANGED:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def test_delivery_status(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test change status on chatlistitem when status changes (delivered, read)
|
||||||
|
"""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
bob.stop_io()
|
||||||
|
alice.stop_io()
|
||||||
|
alice_chat_bob.send_text("hi")
|
||||||
|
wait_for_chatlist_and_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
alice.start_io()
|
||||||
|
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||||
|
|
||||||
|
bob.clear_all_events()
|
||||||
|
bob.start_io()
|
||||||
|
|
||||||
|
event = bob.wait_for_incoming_msg_event()
|
||||||
|
msg = bob.get_message_by_id(event.msg_id)
|
||||||
|
msg.get_snapshot().chat.accept()
|
||||||
|
msg.mark_seen()
|
||||||
|
|
||||||
|
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||||
|
assert chat_item["summaryStatus"] == const.MessageState.OUT_DELIVERED
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event = alice.wait_for_event()
|
||||||
|
if event.kind == EventType.MSG_READ:
|
||||||
|
break
|
||||||
|
|
||||||
|
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||||
|
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||||
|
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
|
||||||
|
|
||||||
|
|
||||||
|
def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test change status on chatlistitem when status changes failed
|
||||||
|
"""
|
||||||
|
(alice,) = acfactory.get_online_accounts(1)
|
||||||
|
|
||||||
|
invalid_contact = alice.create_contact("example@example.com", "invalid address")
|
||||||
|
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
|
||||||
|
failing_message = invalid_chat.send_text("test")
|
||||||
|
|
||||||
|
wait_for_chatlist_and_specific_item(alice, invalid_chat.id)
|
||||||
|
|
||||||
|
assert failing_message.get_snapshot().state == const.MessageState.OUT_PENDING
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event = alice.wait_for_event()
|
||||||
|
if event.kind == EventType.MSG_FAILED:
|
||||||
|
break
|
||||||
|
|
||||||
|
wait_for_chatlist_specific_item(alice, invalid_chat.id)
|
||||||
|
|
||||||
|
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test if download on demand emits chatlist update events.
|
||||||
|
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
|
||||||
|
"""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("hi")
|
||||||
|
|
||||||
|
alice.set_config("download_limit", "1")
|
||||||
|
|
||||||
|
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(
|
||||||
|
"Hello World, this message is bigger than 5 bytes",
|
||||||
|
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
message = alice.wait_for_incoming_msg()
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.download_state == const.DownloadState.AVAILABLE
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("hi")
|
||||||
|
|
||||||
|
bob.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
|
alice_second_device: Account = acfactory.get_unconfigured_account()
|
||||||
|
|
||||||
|
alice._rpc.provide_backup.future(alice.id)
|
||||||
|
backup_code = alice._rpc.get_backup_qr(alice.id)
|
||||||
|
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
|
||||||
|
alice_second_device.start_io()
|
||||||
|
alice.clear_all_events()
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
bob.clear_all_events()
|
||||||
|
return [alice, alice_second_device, bob, alice_chat_bob]
|
||||||
|
|
||||||
|
|
||||||
|
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test that chatlist changed events are emitted for the second device
|
||||||
|
when the message is marked as read on the first device
|
||||||
|
"""
|
||||||
|
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||||
|
|
||||||
|
alice_chat_bob.send_text("hello")
|
||||||
|
|
||||||
|
msg = bob.wait_for_incoming_msg()
|
||||||
|
bob_chat_id = msg.get_snapshot().chat_id
|
||||||
|
msg.get_snapshot().chat.accept()
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
bob.get_chat_by_id(bob_chat_id).send_text("hello")
|
||||||
|
|
||||||
|
# make sure alice_second_device already received the message
|
||||||
|
alice_second_device.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
|
msg = alice.wait_for_incoming_msg()
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
msg.mark_seen()
|
||||||
|
|
||||||
|
wait_for_chatlist_specific_item(bob, bob_chat_id)
|
||||||
|
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test multidevice sync: syncing chat visibility and muting across multiple devices
|
||||||
|
"""
|
||||||
|
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||||
|
|
||||||
|
alice_chat_bob.archive()
|
||||||
|
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().archived
|
||||||
|
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
alice_chat_bob.pin()
|
||||||
|
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||||
|
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
alice_chat_bob.mute()
|
||||||
|
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().is_muted
|
||||||
186
deltachat-rpc-client/tests/test_iroh_webxdc.py
Normal file
186
deltachat-rpc-client/tests/test_iroh_webxdc.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/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 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 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")
|
||||||
|
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
|
||||||
|
|
||||||
|
log("ac2: waiting for realtime data")
|
||||||
|
while 1:
|
||||||
|
event = ac2.wait_for_event()
|
||||||
|
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||||
|
assert event.data == list(b"foo")
|
||||||
|
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
|
||||||
618
deltachat-rpc-client/tests/test_securejoin.py
Normal file
618
deltachat-rpc-client/tests/test_securejoin.py
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
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()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
|
||||||
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# Test that Alice verified Bob's profile.
|
||||||
|
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||||
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# Test that Bob verified Alice's profile.
|
||||||
|
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||||
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
# Test that if Bob changes the key, backwards verification is lost.
|
||||||
|
logging.info("Bob 2 is created")
|
||||||
|
bob2 = acfactory.new_configured_account()
|
||||||
|
bob2.export_self_keys(tmp_path)
|
||||||
|
|
||||||
|
logging.info("Bob imports a key")
|
||||||
|
bob.import_self_keys(tmp_path / "private-key-default.asc")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
logging.info("Alice creates a verified group")
|
||||||
|
alice_chat = alice.create_group("Verified 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()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
|
||||||
|
# Check that at least some of the handshake messages are deleted.
|
||||||
|
for ac in [alice, bob]:
|
||||||
|
while True:
|
||||||
|
event = ac.wait_for_event()
|
||||||
|
if event["kind"] == "ImapMessageDeleted":
|
||||||
|
break
|
||||||
|
|
||||||
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# Test that Alice verified Bob's profile.
|
||||||
|
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||||
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
||||||
|
|
||||||
|
# Test that Bob verified Alice's profile.
|
||||||
|
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||||
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello!"
|
||||||
|
bob_chat_alice = snapshot.chat
|
||||||
|
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
|
alice_chat = alice.create_group("Verified group", protect=True)
|
||||||
|
logging.info("Bob joins verified group")
|
||||||
|
qr_code, _svg = alice_chat.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
while True:
|
||||||
|
event = bob.wait_for_event()
|
||||||
|
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Chat stays being a contact request.
|
||||||
|
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
charlie.secure_join(qr_code)
|
||||||
|
|
||||||
|
for joiner in [bob, charlie]:
|
||||||
|
joiner.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
logging.info("Alice creates a verified group")
|
||||||
|
group = alice.create_group("Group", protect=True)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
charlie_addr = charlie.get_config("addr")
|
||||||
|
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
|
||||||
|
|
||||||
|
group.add_contact(alice_contact_bob)
|
||||||
|
group.add_contact(alice_contact_charlie)
|
||||||
|
|
||||||
|
# Promote a group.
|
||||||
|
group.send_message(text="Hello")
|
||||||
|
|
||||||
|
logging.info("Bob and Charlie receive a group")
|
||||||
|
|
||||||
|
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
|
||||||
|
bob_message = bob.get_message_by_id(bob_msg_id)
|
||||||
|
bob_snapshot = bob_message.get_snapshot()
|
||||||
|
assert bob_snapshot.text == "Hello"
|
||||||
|
|
||||||
|
# Charlie receives the same "Hello" message as Bob.
|
||||||
|
charlie.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
|
logging.info("Bob sends a message to the group")
|
||||||
|
|
||||||
|
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
|
||||||
|
|
||||||
|
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
|
||||||
|
charlie_message = charlie.get_message_by_id(charlie_msg_id)
|
||||||
|
charlie_snapshot = charlie_message.get_snapshot()
|
||||||
|
assert charlie_snapshot.text == "Hi from Bob!"
|
||||||
|
|
||||||
|
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
|
||||||
|
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||||
|
|
||||||
|
logging.info("Charlie reads Bob's message")
|
||||||
|
charlie_message.mark_seen()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event = bob.wait_for_event()
|
||||||
|
if event["kind"] == "MsgRead" and event["msg_id"] == bob_out_message.id:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Receiving a read receipt from Charlie
|
||||||
|
# should not unblock hidden chat with Charlie for Bob.
|
||||||
|
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
alice = acfactory.resetup_account(alice)
|
||||||
|
|
||||||
|
qr_code, _svg = alice.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
|
||||||
|
def test_verified_group_recovery(acfactory) -> None:
|
||||||
|
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||||
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
logging.info("ac1 creates verified group")
|
||||||
|
chat = ac1.create_group("Verified group", protect=True)
|
||||||
|
assert chat.get_basic_snapshot().is_protected
|
||||||
|
|
||||||
|
logging.info("ac2 joins verified group")
|
||||||
|
qr_code, _svg = chat.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 has ac2 directly verified.
|
||||||
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||||
|
|
||||||
|
logging.info("ac3 joins verified group")
|
||||||
|
ac3_chat = ac3.secure_join(qr_code)
|
||||||
|
ac3.wait_for_securejoin_joiner_success()
|
||||||
|
ac3.wait_for_incoming_msg_event() # Member added
|
||||||
|
|
||||||
|
logging.info("ac2 logs in on a new device")
|
||||||
|
ac2 = acfactory.resetup_account(ac2)
|
||||||
|
|
||||||
|
logging.info("ac2 reverifies with ac3")
|
||||||
|
qr_code, _svg = ac3.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
logging.info("ac3 sends a message to the group")
|
||||||
|
assert len(ac3_chat.get_contacts()) == 3
|
||||||
|
ac3_chat.send_text("Hi!")
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
|
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
|
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
|
||||||
|
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
|
||||||
|
assert ac1_contact.get_snapshot().is_verified
|
||||||
|
|
||||||
|
# ac2 can write messages to the group.
|
||||||
|
snapshot.chat.send_text("Works again!")
|
||||||
|
|
||||||
|
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
ac1_chat_messages = snapshot.chat.get_messages()
|
||||||
|
ac2_addr = ac2.get_config("addr")
|
||||||
|
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
|
||||||
|
|
||||||
|
# ac2 is now verified by ac3 for ac1
|
||||||
|
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||||
|
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
|
||||||
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
logging.info("ac1 creates verified group")
|
||||||
|
chat = ac1.create_group("Verified group", protect=True)
|
||||||
|
assert chat.get_basic_snapshot().is_protected
|
||||||
|
|
||||||
|
logging.info("ac2 joins verified group")
|
||||||
|
qr_code, _svg = chat.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 has ac2 directly verified.
|
||||||
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||||
|
|
||||||
|
logging.info("ac3 joins verified group")
|
||||||
|
ac3_chat = ac3.secure_join(qr_code)
|
||||||
|
ac3.wait_for_securejoin_joiner_success()
|
||||||
|
ac3.wait_for_incoming_msg_event() # Member added
|
||||||
|
|
||||||
|
logging.info("ac2 logs in on a new device")
|
||||||
|
ac2 = acfactory.resetup_account(ac2)
|
||||||
|
|
||||||
|
logging.info("ac2 reverifies with ac3")
|
||||||
|
qr_code, _svg = ac3.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
logging.info("ac3 sends a message to the group")
|
||||||
|
assert len(ac3_chat.get_contacts()) == 3
|
||||||
|
ac3_chat.send_text("Hi!")
|
||||||
|
|
||||||
|
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
logging.info("Received message %s", snapshot.text)
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
|
ac1.wait_for_incoming_msg_event() # Hi!
|
||||||
|
|
||||||
|
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)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert "removed" in snapshot.text
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert "removed" in snapshot.text
|
||||||
|
|
||||||
|
event = ac2.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
chat_id = event.chat_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
logging.info("ac2 got event message: %s", snapshot.text)
|
||||||
|
assert "added" in snapshot.text
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert "added" in snapshot.text
|
||||||
|
|
||||||
|
chat = Chat(ac2, chat_id)
|
||||||
|
chat.send_text("Works again!")
|
||||||
|
|
||||||
|
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac3.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
|
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||||
|
assert ac1_contact_ac2_snapshot.is_verified
|
||||||
|
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
|
||||||
|
|
||||||
|
# ac2 is now verified by ac3 for ac1
|
||||||
|
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||||
|
"""Regression test for
|
||||||
|
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
|
||||||
|
"""
|
||||||
|
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||||
|
|
||||||
|
logging.info("ac3: verify with ac2")
|
||||||
|
qr_code, _svg = ac2.get_qr_code()
|
||||||
|
ac3.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# in order for ac2 to have pending bobstate with a verified group
|
||||||
|
# we first create a fully joined verified group, and then start
|
||||||
|
# joining a second time but interrupt it, to create pending bob state
|
||||||
|
|
||||||
|
logging.info("ac1: create verified group that ac2 fully joins")
|
||||||
|
ch1 = ac1.create_group("Group", protect=True)
|
||||||
|
qr_code, _svg = ch1.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||||
|
ch1.send_text("ac1 says hello")
|
||||||
|
while 1:
|
||||||
|
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
if snapshot.text == "ac1 says hello":
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||||
|
break
|
||||||
|
|
||||||
|
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||||
|
qr_code, _svg = ch1.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac1.remove()
|
||||||
|
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||||
|
|
||||||
|
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||||
|
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
|
||||||
|
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
|
||||||
|
|
||||||
|
logging.info("ac3: create a verified group VG with ac2")
|
||||||
|
vg = ac3.create_group("ac3-created", protect=True)
|
||||||
|
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
|
||||||
|
|
||||||
|
# ensure ac2 receives message in VG
|
||||||
|
vg.send_text("hello")
|
||||||
|
while 1:
|
||||||
|
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
if msg.text == "hello":
|
||||||
|
assert msg.chat.get_basic_snapshot().is_protected
|
||||||
|
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()
|
||||||
|
ac4.secure_join(qr_code)
|
||||||
|
ac3.wait_for_securejoin_inviter_success()
|
||||||
|
while 1:
|
||||||
|
ev = ac2.wait_for_event()
|
||||||
|
if "added by unrelated SecureJoin" in str(ev):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def test_qr_new_group_unblocked(acfactory):
|
||||||
|
"""Regression test for a bug introduced in core v1.113.0.
|
||||||
|
ac2 scans a verified group QR code created by ac1.
|
||||||
|
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||||
|
but ac1 contact is not blocked on ac2.
|
||||||
|
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||||
|
ac2 should receive a message and create a contact request for the group.
|
||||||
|
Due to a bug previously ac2 created a blocked group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
|
||||||
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
ac1_new_chat = ac1.create_group("Another group")
|
||||||
|
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
|
||||||
|
# Receive "Member added" message.
|
||||||
|
ac2.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
|
ac1_new_chat.send_text("Hello!")
|
||||||
|
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert ac2_msg.text == "Hello!"
|
||||||
|
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
logging.info("ac2: start QR-code based join-group protocol")
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
logging.info("sending first message")
|
||||||
|
msg_out = chat.send_text("old address").get_snapshot()
|
||||||
|
|
||||||
|
logging.info("receiving first message")
|
||||||
|
ac2.wait_for_incoming_msg_event() # member added message
|
||||||
|
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert msg_in_1.text == msg_out.text
|
||||||
|
|
||||||
|
logging.info("changing email account")
|
||||||
|
ac1.set_config("addr", ac1new.get_config("addr"))
|
||||||
|
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
||||||
|
ac1.stop_io()
|
||||||
|
ac1.configure()
|
||||||
|
ac1.start_io()
|
||||||
|
|
||||||
|
logging.info("sending second message")
|
||||||
|
msg_out = chat.send_text("changed address").get_snapshot()
|
||||||
|
|
||||||
|
logging.info("receiving second message")
|
||||||
|
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||||
|
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||||
|
assert msg_in_2_snapshot.text == msg_out.text
|
||||||
|
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
||||||
|
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
|
||||||
|
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
|
||||||
|
assert ac1new.get_config("addr") in [
|
||||||
|
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_gossip_verification(acfactory) -> None:
|
||||||
|
alice, bob, carol = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
# Bob verifies Alice.
|
||||||
|
qr_code, _svg = 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()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
|
||||||
|
bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
|
||||||
|
carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
|
||||||
|
|
||||||
|
logging.info("Bob creates an Autocrypt group")
|
||||||
|
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||||
|
assert not bob_group_chat.get_basic_snapshot().is_protected
|
||||||
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
|
bob_group_chat.send_message(text="Hello Autocrypt group")
|
||||||
|
|
||||||
|
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Autocrypt group"
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
# Autocrypt group does not propagate verification.
|
||||||
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
|
assert not carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
logging.info("Bob creates a Securejoin group")
|
||||||
|
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
|
||||||
|
assert bob_group_chat.get_basic_snapshot().is_protected
|
||||||
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
|
bob_group_chat.send_message(text="Hello Securejoin group")
|
||||||
|
|
||||||
|
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Securejoin group"
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
# Securejoin propagates verification.
|
||||||
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
|
assert carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||||
|
"""
|
||||||
|
Regression test for a bug that prevented joining verified group with a QR code
|
||||||
|
if the group is already created and contains
|
||||||
|
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
|
||||||
|
"""
|
||||||
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
# ac3 creates protected group with ac1.
|
||||||
|
ac3_chat = ac3.create_group("Verified group", protect=True)
|
||||||
|
|
||||||
|
# ac1 joins ac3 group.
|
||||||
|
ac3_qr_code, _svg = 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()
|
||||||
|
|
||||||
|
# ac2 verifies ac1
|
||||||
|
qr_code, _svg = ac1.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 is verified for ac2.
|
||||||
|
ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "")
|
||||||
|
assert ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
|
# 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!")
|
||||||
|
|
||||||
|
# ac2 receives a message.
|
||||||
|
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "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 goes offline.
|
||||||
|
ac1.remove()
|
||||||
|
|
||||||
|
# Scanning a QR code results in creating an unprotected group with an inviter.
|
||||||
|
# In this case inviter is ac1 which has an inconsistent key state.
|
||||||
|
# Normally inviter becomes verified as a result of Securejoin protocol
|
||||||
|
# and then the group chat becomes verified when "Member added" is received,
|
||||||
|
# but in this case ac1 is offline and this Securejoin process will never finish.
|
||||||
|
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
|
||||||
|
ac2.secure_join(ac1_qr_code)
|
||||||
|
|
||||||
|
logging.info("ac2 scans ac3 QR code")
|
||||||
|
ac2.secure_join(ac3_qr_code)
|
||||||
|
|
||||||
|
logging.info("ac2 waits for joiner success")
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# Wait for member added.
|
||||||
|
logging.info("ac2 waits for member added message")
|
||||||
|
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.is_info
|
||||||
|
ac2_chat = snapshot.chat
|
||||||
|
assert ac2_chat.get_basic_snapshot().is_protected
|
||||||
|
assert len(ac2_chat.get_contacts()) == 3
|
||||||
|
|
||||||
|
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||||
|
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_withdraw_securejoin_qr(acfactory):
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
logging.info("Alice creates a verified group")
|
||||||
|
alice_chat = alice.create_group("Verified group", protect=True)
|
||||||
|
assert alice_chat.get_basic_snapshot().is_protected
|
||||||
|
logging.info("Bob joins verified group")
|
||||||
|
|
||||||
|
qr_code, _svg = alice_chat.get_qr_code()
|
||||||
|
bob_chat = bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||||
|
bob_chat.leave()
|
||||||
|
|
||||||
|
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||||
|
|
||||||
|
logging.info("Alice withdraws QR code.")
|
||||||
|
qr = alice.check_qr(qr_code)
|
||||||
|
assert qr["kind"] == "withdrawVerifyGroup"
|
||||||
|
alice.set_config_from_qr(qr_code)
|
||||||
|
|
||||||
|
logging.info("Bob scans withdrawn QR code.")
|
||||||
|
bob_chat = bob.secure_join(qr_code)
|
||||||
|
|
||||||
|
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:
|
||||||
|
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,8 +1,15 @@
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from deltachat_rpc_client import EventType, events
|
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
|
||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +49,7 @@ def test_acfactory(acfactory) -> None:
|
|||||||
account = acfactory.new_configured_account()
|
account = acfactory.new_configured_account()
|
||||||
while True:
|
while True:
|
||||||
event = account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
if event.type == EventType.CONFIGURE_PROGRESS:
|
if event.kind == EventType.CONFIGURE_PROGRESS:
|
||||||
assert event.progress != 0 # Progress 0 indicates error.
|
assert event.progress != 0 # Progress 0 indicates error.
|
||||||
if event.progress == 1000: # Success
|
if event.progress == 1000: # Success
|
||||||
break
|
break
|
||||||
@@ -71,7 +78,7 @@ def test_account(acfactory) -> None:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
chat_id = event.chat_id
|
chat_id = event.chat_id
|
||||||
msg_id = event.msg_id
|
msg_id = event.msg_id
|
||||||
break
|
break
|
||||||
@@ -138,12 +145,9 @@ def test_chat(acfactory) -> None:
|
|||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
event = bob.wait_for_incoming_msg_event()
|
||||||
event = bob.wait_for_event()
|
chat_id = event.chat_id
|
||||||
if event.type == EventType.INCOMING_MSG:
|
msg_id = event.msg_id
|
||||||
chat_id = event.chat_id
|
|
||||||
msg_id = event.msg_id
|
|
||||||
break
|
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == chat_id
|
assert snapshot.chat_id == chat_id
|
||||||
@@ -222,12 +226,9 @@ def test_message(acfactory) -> None:
|
|||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
event = bob.wait_for_incoming_msg_event()
|
||||||
event = bob.wait_for_event()
|
chat_id = event.chat_id
|
||||||
if event.type == EventType.INCOMING_MSG:
|
msg_id = event.msg_id
|
||||||
chat_id = event.chat_id
|
|
||||||
msg_id = event.msg_id
|
|
||||||
break
|
|
||||||
|
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
@@ -263,7 +264,7 @@ def test_is_bot(acfactory) -> None:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
msg_id = event.msg_id
|
msg_id = event.msg_id
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
@@ -329,7 +330,7 @@ def test_wait_next_messages(acfactory) -> None:
|
|||||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||||
|
|
||||||
bot_addr = bot.get_config("addr")
|
bot_addr = bot.get_config("addr")
|
||||||
alice_contact_bot = alice.create_contact(bot_addr, "Bob")
|
alice_contact_bot = alice.create_contact(bot_addr, "Bot")
|
||||||
alice_chat_bot = alice_contact_bot.create_chat()
|
alice_chat_bot = alice_contact_bot.create_chat()
|
||||||
alice_chat_bot.send_text("Hello!")
|
alice_chat_bot.send_text("Hello!")
|
||||||
|
|
||||||
@@ -339,10 +340,276 @@ def test_wait_next_messages(acfactory) -> None:
|
|||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
def test_import_export(acfactory, tmp_path) -> None:
|
def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||||
alice = acfactory.new_configured_account()
|
alice = acfactory.new_configured_account()
|
||||||
alice.export_backup(tmp_path)
|
alice.export_backup(tmp_path)
|
||||||
|
|
||||||
files = list(tmp_path.glob("*.tar"))
|
files = list(tmp_path.glob("*.tar"))
|
||||||
alice2 = acfactory.get_unconfigured_account()
|
alice2 = acfactory.get_unconfigured_account()
|
||||||
alice2.import_backup(files[0])
|
alice2.import_backup(files[0])
|
||||||
|
|
||||||
|
assert alice2.manager.get_system_info()
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
|
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Bob!"
|
||||||
|
|
||||||
|
# Alice resetups account, but keeps the key.
|
||||||
|
alice_keys_path = tmp_path / "alice_keys"
|
||||||
|
alice_keys_path.mkdir()
|
||||||
|
alice.export_self_keys(alice_keys_path)
|
||||||
|
alice = acfactory.resetup_account(alice)
|
||||||
|
alice.import_self_keys(alice_keys_path)
|
||||||
|
|
||||||
|
snapshot.chat.accept()
|
||||||
|
snapshot.chat.send_text("Hello Alice!")
|
||||||
|
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Alice!"
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrpc_command_line() -> None:
|
||||||
|
"""Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
|
||||||
|
out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
|
||||||
|
openrpc = json.loads(out)
|
||||||
|
assert "openrpc" in openrpc
|
||||||
|
assert "methods" in openrpc
|
||||||
|
|
||||||
|
|
||||||
|
def test_provider_info(rpc) -> None:
|
||||||
|
account_id = rpc.add_account()
|
||||||
|
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "example.org")
|
||||||
|
assert provider_info["id"] == "example.com"
|
||||||
|
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
|
||||||
|
assert provider_info is None
|
||||||
|
|
||||||
|
# Test MX record resolution.
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||||
|
assert provider_info["id"] == "gmail"
|
||||||
|
|
||||||
|
# Disable MX record resolution.
|
||||||
|
rpc.set_config(account_id, "socks5_enabled", "1")
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||||
|
assert provider_info is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
|
||||||
|
# Bob creates chat manually so chat with Alice is accepted.
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
|
# Alice sends a message to Bob.
|
||||||
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
|
event = bob.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
message = bob.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
|
||||||
|
# Bob sends a message to Alice.
|
||||||
|
bob_chat_alice = snapshot.chat
|
||||||
|
bob_chat_alice.accept()
|
||||||
|
bob_chat_alice.send_text("Hello Alice!")
|
||||||
|
event = alice.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
message = alice.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
# Alice reads Bob's message.
|
||||||
|
message.mark_seen()
|
||||||
|
while True:
|
||||||
|
event = bob.wait_for_event()
|
||||||
|
if event.kind == EventType.MSG_READ:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Bob sends a message to Alice, it should also be encrypted.
|
||||||
|
bob_chat_alice.send_text("Hi Alice!")
|
||||||
|
event = alice.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
message = alice.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
|
||||||
|
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
|
||||||
|
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||||
|
messages are received out of order".
|
||||||
|
|
||||||
|
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||||
|
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||||
|
|
||||||
|
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||||
|
with online test as follows:
|
||||||
|
- Bob enables download limit and goes offline.
|
||||||
|
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||||
|
- Bob goes online
|
||||||
|
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||||
|
message, then processes a partially downloaded message.
|
||||||
|
- As a result, Bob does not see a reaction
|
||||||
|
"""
|
||||||
|
download_limit = 300000
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1_addr = ac1.get_config("addr")
|
||||||
|
chat = ac1.create_chat(ac2)
|
||||||
|
ac2.set_config("download_limit", str(download_limit))
|
||||||
|
ac2.stop_io()
|
||||||
|
|
||||||
|
logging.info("sending small+large messages from ac1 to ac2")
|
||||||
|
msgs = []
|
||||||
|
msgs.append(chat.send_text("hi"))
|
||||||
|
path = tmp_path / "large"
|
||||||
|
path.write_bytes(os.urandom(download_limit + 1))
|
||||||
|
msgs.append(chat.send_file(str(path)))
|
||||||
|
for m in msgs:
|
||||||
|
m.wait_until_delivered()
|
||||||
|
|
||||||
|
logging.info("sending a reaction to the large message from ac1 to ac2")
|
||||||
|
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||||
|
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||||
|
# have a later INTERNALDATE.
|
||||||
|
time.sleep(1.1)
|
||||||
|
react_str = "\N{THUMBS UP SIGN}"
|
||||||
|
msgs.append(msgs[-1].send_reaction(react_str))
|
||||||
|
msgs[-1].wait_until_delivered()
|
||||||
|
|
||||||
|
ac2.start_io()
|
||||||
|
|
||||||
|
logging.info("wait for ac2 to receive a reaction")
|
||||||
|
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||||
|
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
|
||||||
|
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
|
||||||
|
reactions = msg2.get_reactions()
|
||||||
|
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||||
|
assert len(contacts) == 1
|
||||||
|
assert contacts[0].get_snapshot().address == ac1_addr
|
||||||
|
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reactions_for_a_reordering_move(acfactory):
|
||||||
|
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||||
|
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||||
|
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||||
|
messages they refer to and thus dropped.
|
||||||
|
"""
|
||||||
|
(ac1,) = acfactory.get_online_accounts(1)
|
||||||
|
ac2 = acfactory.new_preconfigured_account()
|
||||||
|
ac2.configure()
|
||||||
|
ac2.set_config("mvbox_move", "1")
|
||||||
|
ac2.bring_online()
|
||||||
|
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
|
ac2.stop_io()
|
||||||
|
|
||||||
|
logging.info("sending message + reaction from ac1 to ac2")
|
||||||
|
msg1 = chat1.send_text("hi")
|
||||||
|
msg1.wait_until_delivered()
|
||||||
|
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||||
|
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||||
|
time.sleep(1.1)
|
||||||
|
react_str = "\N{THUMBS UP SIGN}"
|
||||||
|
msg1.send_reaction(react_str).wait_until_delivered()
|
||||||
|
|
||||||
|
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||||
|
ac2_direct_imap = DirectImap(ac2)
|
||||||
|
ac2_direct_imap.connect()
|
||||||
|
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||||
|
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||||
|
|
||||||
|
logging.info("receiving messages by ac2")
|
||||||
|
ac2.start_io()
|
||||||
|
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||||
|
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||||
|
reactions = msg2.get_reactions()
|
||||||
|
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||||
|
assert len(contacts) == 1
|
||||||
|
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
|
||||||
|
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("n_accounts", [3, 2])
|
||||||
|
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||||
|
download_limit = 300000
|
||||||
|
|
||||||
|
alice, *others = acfactory.get_online_accounts(n_accounts)
|
||||||
|
bob = others[0]
|
||||||
|
|
||||||
|
alice_group = alice.create_group("test group")
|
||||||
|
for account in others:
|
||||||
|
chat = account.create_chat(alice)
|
||||||
|
chat.send_text("Hello Alice!")
|
||||||
|
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
|
||||||
|
|
||||||
|
contact_addr = account.get_config("addr")
|
||||||
|
contact = alice.create_contact(contact_addr, "")
|
||||||
|
|
||||||
|
alice_group.add_contact(contact)
|
||||||
|
|
||||||
|
if n_accounts == 2:
|
||||||
|
bob_chat_alice = bob.create_chat(alice)
|
||||||
|
bob.set_config("download_limit", str(download_limit))
|
||||||
|
|
||||||
|
alice_group.send_text("hi")
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "hi"
|
||||||
|
bob_group = snapshot.chat
|
||||||
|
|
||||||
|
path = tmp_path / "large"
|
||||||
|
path.write_bytes(os.urandom(download_limit + 1))
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
logging.info("Sending message %s", i)
|
||||||
|
alice_group.send_file(str(path))
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||||
|
if n_accounts > 2:
|
||||||
|
assert snapshot.chat == bob_group
|
||||||
|
else:
|
||||||
|
# Group contains only Alice and Bob,
|
||||||
|
# so partially downloaded messages are
|
||||||
|
# hard to distinguish from private replies to group messages.
|
||||||
|
#
|
||||||
|
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
|
||||||
|
assert snapshot.chat == bob_chat_alice
|
||||||
|
|
||||||
|
|
||||||
|
def test_markseen_contact_request(acfactory, tmp_path):
|
||||||
|
"""
|
||||||
|
Test that seen status is synchronized for contact request messages
|
||||||
|
even though read receipt is not sent.
|
||||||
|
"""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
# Bob sets up a second device.
|
||||||
|
bob.export_backup(tmp_path)
|
||||||
|
files = list(tmp_path.glob("*.tar"))
|
||||||
|
bob2 = acfactory.get_unconfigured_account()
|
||||||
|
bob2.import_backup(files[0])
|
||||||
|
bob2.start_io()
|
||||||
|
|
||||||
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
|
|
||||||
|
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
|
||||||
|
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
|
||||||
|
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||||
|
|
||||||
|
message.mark_seen()
|
||||||
|
while True:
|
||||||
|
event = bob2.wait_for_event()
|
||||||
|
if event.kind == EventType.MSGS_NOTICED:
|
||||||
|
break
|
||||||
|
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||||
|
|||||||
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"
|
||||||
@@ -11,7 +11,7 @@ def test_webxdc(acfactory) -> None:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||||
message = bob.get_message_by_id(event.msg_id)
|
message = bob.get_message_by_id(event.msg_id)
|
||||||
break
|
break
|
||||||
@@ -43,3 +43,15 @@ def test_webxdc(acfactory) -> None:
|
|||||||
assert status_updates == [
|
assert status_updates == [
|
||||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webxdc_insert_lots_of_updates(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_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||||
|
|
||||||
|
for i in range(2000):
|
||||||
|
message.send_webxdc_status_update({"payload": str(i)}, "description")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ setenv =
|
|||||||
# Avoid stack overflow when Rust core is built without optimizations.
|
# Avoid stack overflow when Rust core is built without optimizations.
|
||||||
RUST_MIN_STACK=8388608
|
RUST_MIN_STACK=8388608
|
||||||
passenv =
|
passenv =
|
||||||
DCC_NEW_TMP_EMAIL
|
CHATMAIL_DOMAIN
|
||||||
deps =
|
deps =
|
||||||
pytest
|
pytest
|
||||||
pytest-timeout
|
pytest-timeout
|
||||||
@@ -22,10 +22,11 @@ skipsdist = True
|
|||||||
skip_install = True
|
skip_install = True
|
||||||
deps =
|
deps =
|
||||||
ruff
|
ruff
|
||||||
black
|
|
||||||
commands =
|
commands =
|
||||||
black --quiet --check --diff src/ examples/ tests/
|
ruff format --quiet --diff src/ examples/ tests/
|
||||||
ruff src/ examples/ tests/
|
ruff check src/ examples/ tests/
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
timeout = 60
|
timeout = 300
|
||||||
|
#log_cli = true
|
||||||
|
log_level = debug
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "1.125.0"
|
version = "1.140.0"
|
||||||
description = "DeltaChat JSON-RPC server"
|
description = "DeltaChat JSON-RPC server"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -14,14 +14,14 @@ deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
|||||||
deltachat = { path = "..", default-features = false }
|
deltachat = { path = "..", default-features = false }
|
||||||
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
env_logger = { version = "0.10.0" }
|
futures-lite = "2.3.0"
|
||||||
futures-lite = "1.13.0"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde_json = "1.0.99"
|
serde_json = "1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tokio = { version = "1.29.1", features = ["io-std"] }
|
tokio = { version = "1.37.0", features = ["io-std"] }
|
||||||
tokio-util = "0.7.8"
|
tokio-util = "0.7.9"
|
||||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -30,5 +30,8 @@ deltachat-rpc-server
|
|||||||
The common use case for this program is to create bindings to use Delta Chat core from programming
|
The common use case for this program is to create bindings to use Delta Chat core from programming
|
||||||
languages other than Rust, for example:
|
languages other than Rust, for example:
|
||||||
|
|
||||||
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
|
1. Python: https://pypi.org/project/deltachat-rpc-client/
|
||||||
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
||||||
|
|
||||||
|
Run `deltachat-rpc-server --version` to check the version of the server.
|
||||||
|
Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.
|
||||||
|
|||||||
3
deltachat-rpc-server/npm-package/.gitignore
vendored
Normal file
3
deltachat-rpc-server/npm-package/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
platform_package
|
||||||
|
*.tgz
|
||||||
|
package-lock.json
|
||||||
3
deltachat-rpc-server/npm-package/.npmignore
Normal file
3
deltachat-rpc-server/npm-package/.npmignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
platform_package/*
|
||||||
|
scripts/
|
||||||
|
*.tgz
|
||||||
78
deltachat-rpc-server/npm-package/README.md
Normal file
78
deltachat-rpc-server/npm-package/README.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
## npm package for deltachat-rpc-server
|
||||||
|
|
||||||
|
This is the successor of `deltachat-node`,
|
||||||
|
it does not use NAPI bindings but instead uses stdio executables
|
||||||
|
to let you talk to core over jsonrpc over stdio.
|
||||||
|
This simplifies cross-compilation and even reduces binary size (no CFFI layer and no NAPI layer).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
> The **minimum** nodejs version for this package is `20.11`
|
||||||
|
|
||||||
|
```
|
||||||
|
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||||
|
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).
|
||||||
|
|
||||||
|
## How to use on an unsupported platform
|
||||||
|
|
||||||
|
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
|
||||||
|
|
||||||
|
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
|
||||||
|
|
||||||
|
## How does it work when you install it
|
||||||
|
|
||||||
|
NPM automatically installs platform dependent optional dependencies when `os` and `cpu` fields are set correctly.
|
||||||
|
|
||||||
|
references:
|
||||||
|
|
||||||
|
- https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages, [webarchive version](https://web.archive.org/web/20240309234250/https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages)
|
||||||
|
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#cpu
|
||||||
|
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#os
|
||||||
|
|
||||||
|
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. 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:
|
||||||
|
```
|
||||||
|
python3 build_platform_package.py <cargo-target>
|
||||||
|
# example
|
||||||
|
python3 build_platform_package.py x86_64-apple-darwin
|
||||||
|
```
|
||||||
|
- Then pass it as an artifact to the last CI action that publishes the main package.
|
||||||
|
- upload all packages from `deltachat-rpc-server/npm-package/platform_package`.
|
||||||
|
- then publish `deltachat-rpc-server/npm-package`,
|
||||||
|
- this will run `update_optional_dependencies_and_version.js` (in the `prepack` script),
|
||||||
|
which puts all platform packages into `optionalDependencies` and updates the `version` in `package.json`
|
||||||
|
|
||||||
|
## How to build a version you can use localy on your host machine for development
|
||||||
|
|
||||||
|
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have seperate scripts for making it work for local installation.
|
||||||
|
|
||||||
|
- If you just need your host platform run `python scripts/make_local_dev_version.py`
|
||||||
|
- note: this clears the `platform_package` folder
|
||||||
|
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple plaftorms with `build_platform_package.py`
|
||||||
|
|
||||||
|
## Thanks to nlnet
|
||||||
|
|
||||||
|
The initial work on this package was funded by nlnet as part of the [Delta Tauri](https://nlnet.nl/project/DeltaTauri/) Project.
|
||||||
42
deltachat-rpc-server/npm-package/index.d.ts
vendored
Normal file
42
deltachat-rpc-server/npm-package/index.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||||
|
|
||||||
|
export interface SearchOptions {
|
||||||
|
/** whether take deltachat-rpc-server inside of $PATH*/
|
||||||
|
takeVersionFromPATH: boolean;
|
||||||
|
|
||||||
|
/** whether to disable the DELTA_CHAT_RPC_SERVER environment variable */
|
||||||
|
disableEnvPath: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns absolute path to deltachat-rpc-server binary
|
||||||
|
* @throws when it is not found
|
||||||
|
*/
|
||||||
|
export function getRPCServerPath(
|
||||||
|
options?: Partial<SearchOptions>
|
||||||
|
): Promise<string>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type DeltaChatOverJsonRpcServer = StdioDeltaChat & {
|
||||||
|
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 & StartOptions> ): Promise<DeltaChatOverJsonRpcServer>
|
||||||
|
|
||||||
|
|
||||||
|
export namespace FnTypes {
|
||||||
|
export type getRPCServerPath = typeof getRPCServerPath
|
||||||
|
export type startDeltaChat = typeof startDeltaChat
|
||||||
|
}
|
||||||
106
deltachat-rpc-server/npm-package/index.js
Normal file
106
deltachat-rpc-server/npm-package/index.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
//@ts-check
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { stat } from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import process from "node:process";
|
||||||
|
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
|
||||||
|
import {
|
||||||
|
ENV_VAR_LOCATION_NOT_FOUND,
|
||||||
|
FAILED_TO_START_SERVER_EXECUTABLE,
|
||||||
|
NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR,
|
||||||
|
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";
|
||||||
|
|
||||||
|
function findRPCServerInNodeModules() {
|
||||||
|
const arch = os.arch();
|
||||||
|
const operating_system = process.platform;
|
||||||
|
const package_name = `@deltachat/stdio-rpc-server-${operating_system}-${arch}`;
|
||||||
|
try {
|
||||||
|
const { resolve } = createRequire(import.meta.url);
|
||||||
|
return resolve(package_name);
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("findRpcServerInNodeModules", error);
|
||||||
|
if (Object.keys(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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("./index").FnTypes.getRPCServerPath} */
|
||||||
|
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 {
|
||||||
|
if (!(await stat(process.env[ENV_VAR_NAME])).isFile()) {
|
||||||
|
throw new Error(
|
||||||
|
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(ENV_VAR_LOCATION_NOT_FOUND());
|
||||||
|
}
|
||||||
|
return process.env[ENV_VAR_NAME];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. check if PATH should be used
|
||||||
|
if (takeVersionFromPATH) {
|
||||||
|
return PATH_EXECUTABLE_NAME;
|
||||||
|
}
|
||||||
|
// 3. check for prebuilds
|
||||||
|
|
||||||
|
return findRPCServerInNodeModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||||
|
|
||||||
|
/** @type {import("./index").FnTypes.startDeltaChat} */
|
||||||
|
export async function startDeltaChat(directory, options = {}) {
|
||||||
|
const pathToServerBinary = await getRPCServerPath(options);
|
||||||
|
const server = spawn(pathToServerBinary, {
|
||||||
|
env: {
|
||||||
|
RUST_LOG: process.env.RUST_LOG,
|
||||||
|
DC_ACCOUNTS_PATH: directory,
|
||||||
|
},
|
||||||
|
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("error", (err) => {
|
||||||
|
throw new Error(FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err));
|
||||||
|
});
|
||||||
|
let shouldClose = false;
|
||||||
|
|
||||||
|
server.on("exit", () => {
|
||||||
|
if (shouldClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error("Server quit");
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
|
||||||
|
//@ts-expect-error
|
||||||
|
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
|
||||||
|
|
||||||
|
dc.close = () => {
|
||||||
|
shouldClose = true;
|
||||||
|
if (!server.kill()) {
|
||||||
|
console.log("server termination failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
dc.pathToServerBinary = pathToServerBinary;
|
||||||
|
|
||||||
|
return dc;
|
||||||
|
}
|
||||||
19
deltachat-rpc-server/npm-package/package.json
Normal file
19
deltachat-rpc-server/npm-package/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"name": "@deltachat/stdio-rpc-server",
|
||||||
|
"optionalDependencies": {},
|
||||||
|
"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.140.0"
|
||||||
|
}
|
||||||
53
deltachat-rpc-server/npm-package/scripts/build_platform_package.py
Executable file
53
deltachat-rpc-server/npm-package/scripts/build_platform_package.py
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import subprocess
|
||||||
|
from sys import argv
|
||||||
|
from os import path, makedirs, chdir
|
||||||
|
from shutil import copy
|
||||||
|
from src.make_package import write_package_json
|
||||||
|
|
||||||
|
# ensure correct working directory
|
||||||
|
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
|
||||||
|
|
||||||
|
if len(argv) < 2:
|
||||||
|
print("First argument should be target architecture as required by cargo")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
target = argv[1].strip()
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
["cargo", "build", "--release", "-p", "deltachat-rpc-server", "--target", target],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
newpath = "platform_package"
|
||||||
|
if not path.exists(newpath):
|
||||||
|
makedirs(newpath)
|
||||||
|
|
||||||
|
# make new folder
|
||||||
|
|
||||||
|
platform_path = "platform_package/" + target
|
||||||
|
if not path.exists(platform_path):
|
||||||
|
makedirs(platform_path)
|
||||||
|
|
||||||
|
# copy binary it over
|
||||||
|
|
||||||
|
|
||||||
|
def binary_path(binary_name):
|
||||||
|
return "../../target/" + target + "/release/" + binary_name
|
||||||
|
|
||||||
|
|
||||||
|
my_binary_name = "deltachat-rpc-server"
|
||||||
|
|
||||||
|
if not path.isfile(binary_path("deltachat-rpc-server")):
|
||||||
|
my_binary_name = "deltachat-rpc-server.exe"
|
||||||
|
if not path.isfile(binary_path("deltachat-rpc-server.exe")):
|
||||||
|
print("Did not find the build")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
my_binary_path = binary_path(my_binary_name)
|
||||||
|
|
||||||
|
copy(my_binary_path, platform_path + "/" + my_binary_name)
|
||||||
|
|
||||||
|
# make a package.json for it
|
||||||
|
|
||||||
|
write_package_json(platform_path, target, my_binary_name)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# This script is for making a version of the npm packet that you can install locally
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from sys import argv
|
||||||
|
from os import path, makedirs, chdir
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import tomllib
|
||||||
|
from shutil import copy, rmtree
|
||||||
|
|
||||||
|
# ensure correct working directory
|
||||||
|
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
|
||||||
|
|
||||||
|
# get host target with "rustc -vV"
|
||||||
|
output = subprocess.run(["rustc", "-vV"], capture_output=True)
|
||||||
|
host_target = re.search('host: ([-\\w]*)', output.stdout.decode("utf-8")).group(1)
|
||||||
|
print("host target to build for is:", host_target)
|
||||||
|
|
||||||
|
# clean platform_package folder
|
||||||
|
newpath = r'platform_package'
|
||||||
|
if not path.exists(newpath):
|
||||||
|
makedirs(newpath)
|
||||||
|
else:
|
||||||
|
rmtree(path.join(path.dirname(path.abspath(__file__)), "../platform_package/"))
|
||||||
|
makedirs(newpath)
|
||||||
|
|
||||||
|
# run build_platform_package.py with the host's target to build it
|
||||||
|
subprocess.run(["python", "scripts/build_platform_package.py", host_target], capture_output=False, check=True)
|
||||||
|
|
||||||
|
# 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)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import subprocess
|
||||||
|
from sys import argv
|
||||||
|
from os import path, makedirs, chdir, chmod, stat
|
||||||
|
import json
|
||||||
|
from shutil import copy
|
||||||
|
from src.make_package import write_package_json
|
||||||
|
|
||||||
|
# ensure correct working directory
|
||||||
|
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
|
||||||
|
|
||||||
|
if len(argv) < 3:
|
||||||
|
print("First argument should be target architecture as required by cargo")
|
||||||
|
print("Second argument should be the location of th built binary (binary_path)")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
target = argv[1].strip()
|
||||||
|
binary_path = argv[2].strip()
|
||||||
|
|
||||||
|
output = subprocess.run(["rustc","--print","target-list"], capture_output=True, check=True)
|
||||||
|
available_targets = output.stdout.decode("utf-8")
|
||||||
|
|
||||||
|
if available_targets.find(target) == -1:
|
||||||
|
print("target", target, "is not known / not valid")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
newpath = r'platform_package'
|
||||||
|
if not path.exists(newpath):
|
||||||
|
makedirs(newpath)
|
||||||
|
|
||||||
|
# make new folder
|
||||||
|
|
||||||
|
platform_path = 'platform_package/' + target
|
||||||
|
if not path.exists(platform_path):
|
||||||
|
makedirs(platform_path)
|
||||||
|
|
||||||
|
# copy binary it over
|
||||||
|
|
||||||
|
my_binary_name = path.basename(binary_path)
|
||||||
|
new_binary_path = platform_path + "/" + my_binary_name
|
||||||
|
copy(binary_path, new_binary_path)
|
||||||
|
chmod(new_binary_path, 0o555) # everyone can read & execute, nobody can write
|
||||||
|
|
||||||
|
# make a package.json for it
|
||||||
|
|
||||||
|
write_package_json(platform_path, target, my_binary_name)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
def convert_cpu_arch_to_npm_cpu_arch(arch):
|
||||||
|
if arch == "x86_64":
|
||||||
|
return "x64"
|
||||||
|
if arch == "i686":
|
||||||
|
return "ia32"
|
||||||
|
if arch == "aarch64":
|
||||||
|
return "arm64"
|
||||||
|
if arch == "armv7" or arch == "arm":
|
||||||
|
return "arm"
|
||||||
|
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.arch':", arch)
|
||||||
|
return arch
|
||||||
|
|
||||||
|
def convert_os_to_npm_os(os):
|
||||||
|
if os == "windows":
|
||||||
|
return "win32"
|
||||||
|
if os == "darwin" or os == "linux":
|
||||||
|
return os
|
||||||
|
if os.startswith("android"):
|
||||||
|
return "android"
|
||||||
|
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.platform':", os)
|
||||||
|
return os
|
||||||
34
deltachat-rpc-server/npm-package/scripts/src/make_package.py
Normal file
34
deltachat-rpc-server/npm-package/scripts/src/make_package.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import tomllib
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .convert_platform import convert_cpu_arch_to_npm_cpu_arch, convert_os_to_npm_os
|
||||||
|
|
||||||
|
def write_package_json(platform_path, rust_target, my_binary_name):
|
||||||
|
if len(rust_target.split("-")) == 3:
|
||||||
|
[cpu_arch, vendor, os] = rust_target.split("-")
|
||||||
|
else:
|
||||||
|
[cpu_arch, vendor, os, _env] = rust_target.split("-")
|
||||||
|
|
||||||
|
# read version
|
||||||
|
tomlfile = open("../../Cargo.toml", 'rb')
|
||||||
|
version = tomllib.load(tomlfile)['package']['version']
|
||||||
|
|
||||||
|
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",
|
||||||
|
"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))
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const expected_cwd = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
|
if (process.cwd() !== expected_cwd) {
|
||||||
|
console.error(
|
||||||
|
"CWD missmatch: this script needs to be run from " + expected_cwd,
|
||||||
|
{ actual: process.cwd(), expected: expected_cwd }
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// whether to use local paths instead of npm registry version number for the prebuilds in optionalDependencies
|
||||||
|
// useful for local development
|
||||||
|
const is_local = process.argv.includes("--local");
|
||||||
|
|
||||||
|
const package_json = JSON.parse(await fs.readFile("./package.json", "utf8"));
|
||||||
|
|
||||||
|
const cargo_toml = await fs.readFile("../Cargo.toml", "utf8");
|
||||||
|
const version = cargo_toml
|
||||||
|
.split("\n")
|
||||||
|
.find((line) => line.includes("version"))
|
||||||
|
.split('"')[1];
|
||||||
|
|
||||||
|
const platform_packages_dir = "./platform_package";
|
||||||
|
|
||||||
|
const platform_package_names = await Promise.all(
|
||||||
|
(await fs.readdir(platform_packages_dir)).map(async (name) => {
|
||||||
|
const p = JSON.parse(
|
||||||
|
await fs.readFile(
|
||||||
|
join(platform_packages_dir, name, "package.json"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (p.version !== version) {
|
||||||
|
console.error(
|
||||||
|
name,
|
||||||
|
"has a different version than the version of the rpc server.",
|
||||||
|
{ rpc_server: version, platform_package: p.version }
|
||||||
|
);
|
||||||
|
throw new Error("version missmatch");
|
||||||
|
}
|
||||||
|
return { folder_name: name, package_name: p.name };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
package_json.version = version;
|
||||||
|
package_json.optionalDependencies = {};
|
||||||
|
for (const { folder_name, package_name } of platform_package_names) {
|
||||||
|
package_json.optionalDependencies[package_name] = is_local
|
||||||
|
? `file:${expected_cwd}/platform_package/${folder_name}` // npm seems to work better with an absolute path here
|
||||||
|
: version;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_local) {
|
||||||
|
package_json.peerDependencies["@deltachat/jsonrpc-client"] = 'file:../../deltachat-jsonrpc/typescript'
|
||||||
|
} else {
|
||||||
|
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile("./package.json", JSON.stringify(package_json, null, 4));
|
||||||
5
deltachat-rpc-server/npm-package/src/const.js
Normal file
5
deltachat-rpc-server/npm-package/src/const.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//@ts-check
|
||||||
|
|
||||||
|
export const PATH_EXECUTABLE_NAME = 'deltachat-rpc-server'
|
||||||
|
|
||||||
|
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"
|
||||||
41
deltachat-rpc-server/npm-package/src/errors.js
Normal file
41
deltachat-rpc-server/npm-package/src/errors.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//@ts-check
|
||||||
|
import { ENV_VAR_NAME } from "./const.js";
|
||||||
|
|
||||||
|
const cargoInstallCommand =
|
||||||
|
"cargo install --git https://github.com/deltachat/deltachat-core-rust deltachat-rpc-server";
|
||||||
|
|
||||||
|
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
|
||||||
|
return `deltachat-rpc-server not found:
|
||||||
|
|
||||||
|
- Install it with "npm i ${package_name}"
|
||||||
|
- or download/compile deltachat-rpc-server for your platform and
|
||||||
|
- either put it into your PATH (for example with "${cargoInstallCommand}")
|
||||||
|
- or set the "${ENV_VAR_NAME}" env var to the path to deltachat-rpc-server"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR() {
|
||||||
|
return `deltachat-rpc-server not found:
|
||||||
|
|
||||||
|
Unfortunately no prebuild is available for your system, so you need to provide deltachat-rpc-server yourself.
|
||||||
|
|
||||||
|
- Download or Compile deltachat-rpc-server for your platform and
|
||||||
|
- either put it into your PATH (for example with "${cargoInstallCommand}")
|
||||||
|
- or set the "${ENV_VAR_NAME}" env var to the path to deltachat-rpc-server"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ENV_VAR_LOCATION_NOT_FOUND(error) {
|
||||||
|
return `deltachat-rpc-server not found in ${ENV_VAR_NAME}:
|
||||||
|
|
||||||
|
Error: ${error}
|
||||||
|
|
||||||
|
Content of ${ENV_VAR_NAME}: "${process.env[ENV_VAR_NAME]}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, error) {
|
||||||
|
return `Failed to start server executable at '${pathToServerBinary}',
|
||||||
|
|
||||||
|
Error: ${error}
|
||||||
|
|
||||||
|
Make sure the deltachat-rpc-server binary exists at this location
|
||||||
|
and you can start it with \`${pathToServerBinary} --version\``;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
//! Delta Chat core RPC server.
|
//! Delta Chat core RPC server.
|
||||||
//!
|
//!
|
||||||
//! It speaks JSON Lines over stdio.
|
//! It speaks JSON Lines over stdio.
|
||||||
@@ -10,6 +11,8 @@ use deltachat::constants::DC_VERSION_STR;
|
|||||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||||
use futures_lite::stream::StreamExt;
|
use futures_lite::stream::StreamExt;
|
||||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use yerpc::RpcServer as _;
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
use tokio::signal::unix as signal_unix;
|
use tokio::signal::unix as signal_unix;
|
||||||
@@ -26,6 +29,9 @@ async fn main() {
|
|||||||
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
// "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
|
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
|
||||||
// until the user presses enter."
|
// 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 });
|
std::process::exit(if r.is_ok() { 0 } else { 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +45,12 @@ async fn main_impl() -> Result<()> {
|
|||||||
}
|
}
|
||||||
eprintln!("{}", &*DC_VERSION_STR);
|
eprintln!("{}", &*DC_VERSION_STR);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
} else if first_arg.to_str() == Some("--openrpc") {
|
||||||
|
if let Some(arg) = args.next() {
|
||||||
|
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||||
|
}
|
||||||
|
println!("{}", CommandApi::openrpc_specification()?);
|
||||||
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
||||||
}
|
}
|
||||||
@@ -52,15 +64,22 @@ async fn main_impl() -> Result<()> {
|
|||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
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());
|
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||||
log::info!("Starting with accounts directory `{}`.", path);
|
log::info!("Starting with accounts directory `{}`.", path);
|
||||||
let accounts = Accounts::new(PathBuf::from(&path)).await?;
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
|
||||||
|
|
||||||
log::info!("Creating JSON-RPC API.");
|
log::info!("Creating JSON-RPC API.");
|
||||||
let accounts = Arc::new(RwLock::new(accounts));
|
let accounts = Arc::new(RwLock::new(accounts));
|
||||||
let state = CommandApi::from_arc(accounts.clone());
|
let state = CommandApi::from_arc(accounts.clone()).await;
|
||||||
|
|
||||||
let (client, mut out_receiver) = RpcClient::new();
|
let (client, mut out_receiver) = RpcClient::new();
|
||||||
let session = RpcSession::new(client.clone(), state.clone());
|
let session = RpcSession::new(client.clone(), state.clone());
|
||||||
|
|||||||
8
deltachat-time/Cargo.toml
Normal file
8
deltachat-time/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "deltachat-time"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Time-related tools"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user