mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:12:49 +03:00
Compare commits
640 Commits
link2xt/in
...
v2.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2cfc4cb3 | ||
|
|
4157d1986f | ||
|
|
d13eb2f580 | ||
|
|
5476f69179 | ||
|
|
dcdf30da35 | ||
|
|
55746c8c19 | ||
|
|
dbdf5f2746 | ||
|
|
b4e28deed3 | ||
|
|
f4a604dcfb | ||
|
|
b3c5787ec8 | ||
|
|
471d0469dd | ||
|
|
113eda575f | ||
|
|
45f1da82fe | ||
|
|
5f45ff77e4 | ||
|
|
1c0201ee3d | ||
|
|
c7340e04ec | ||
|
|
0a32476dc5 | ||
|
|
e02bc6ffb5 | ||
|
|
f41a3970b2 | ||
|
|
6c536f3a9b | ||
|
|
4b24b6a848 | ||
|
|
5f254a929f | ||
|
|
8df1a01ace | ||
|
|
27b5ffb34f | ||
|
|
80af012962 | ||
|
|
615c80bef4 | ||
|
|
f5f4026dbb | ||
|
|
b431206ede | ||
|
|
c4878e9b49 | ||
|
|
aa452971a6 | ||
|
|
2d798f7cfe | ||
|
|
08bb0484eb | ||
|
|
b0b7337f5a | ||
|
|
93241a4beb | ||
|
|
4f1bf1f13c | ||
|
|
2d0b7b5bd8 | ||
|
|
8fe3ce5cab | ||
|
|
59a0f1d94f | ||
|
|
5175dc3450 | ||
|
|
9a22ccd058 | ||
|
|
c06ed49a2a | ||
|
|
2e51a5a454 | ||
|
|
75cc353528 | ||
|
|
3977580426 | ||
|
|
3a1370e174 | ||
|
|
c218c05b96 | ||
|
|
db247d9f9a | ||
|
|
78b7715ea6 | ||
|
|
ba76944d75 | ||
|
|
4a1a2122f0 | ||
|
|
d80b749dec | ||
|
|
039a8b7c36 | ||
|
|
779f58ab16 | ||
|
|
b9183fe5eb | ||
|
|
9d342671d5 | ||
|
|
4e47ebd5fc | ||
|
|
d5c418e909 | ||
|
|
85414558c5 | ||
|
|
d6af8d2526 | ||
|
|
1209e95e34 | ||
|
|
51f9279e67 | ||
|
|
f27d54f7fa | ||
|
|
7f3648f8ae | ||
|
|
49fc258578 | ||
|
|
0c51b4fe41 | ||
|
|
dbad714539 | ||
|
|
edd8008650 | ||
|
|
615a1b3f4e | ||
|
|
fe6044e1aa | ||
|
|
46b275bfab | ||
|
|
25f44c517a | ||
|
|
cac04f8ee4 | ||
|
|
45d8566ec0 | ||
|
|
29a98ba13b | ||
|
|
e3973f6448 | ||
|
|
7b41425fe4 | ||
|
|
2c7d51f98f | ||
|
|
a2df29515a | ||
|
|
6df1d165dd | ||
|
|
e03e2d9a68 | ||
|
|
8fc6ea19b4 | ||
|
|
c5c947e175 | ||
|
|
6d8dff54a7 | ||
|
|
a0f6bdffeb | ||
|
|
e6fd52afff | ||
|
|
0142515887 | ||
|
|
d45ec7f34d | ||
|
|
752f45f0f0 | ||
|
|
0299543a86 | ||
|
|
d3908d6b36 | ||
|
|
2cf979de53 | ||
|
|
f5e8c8083d | ||
|
|
58b99f59f7 | ||
|
|
402e42f858 | ||
|
|
fbae0739a6 | ||
|
|
0359481ba4 | ||
|
|
6406f305b8 | ||
|
|
e5e0f0cdd7 | ||
|
|
0bac4acdd8 | ||
|
|
ce5697c5f7 | ||
|
|
22258f7269 | ||
|
|
5ab107866a | ||
|
|
374a5ef687 | ||
|
|
1a2e355bb8 | ||
|
|
192a6a2b9d | ||
|
|
4ca0ce2fb2 | ||
|
|
ab4cb01065 | ||
|
|
661a8864b9 | ||
|
|
67f00fbb84 | ||
|
|
389649ea8a | ||
|
|
a87ee030fc | ||
|
|
3f66ae91cd | ||
|
|
75b7bea78f | ||
|
|
acba27a328 | ||
|
|
cba9eb98d6 | ||
|
|
da9b24d191 | ||
|
|
c9c5d94666 | ||
|
|
aad8f698dd | ||
|
|
35e107e87d | ||
|
|
d9b361f066 | ||
|
|
94e75cb3b8 | ||
|
|
c7fb64e2f3 | ||
|
|
ebddabe958 | ||
|
|
b81f7cfcab | ||
|
|
9197ef04f7 | ||
|
|
7e4d4cf680 | ||
|
|
0a73c2b7ab | ||
|
|
2ee3f58b69 | ||
|
|
f60af72a5e | ||
|
|
95125d30ef | ||
|
|
48a9fafe6c | ||
|
|
c4cc2fe731 | ||
|
|
3df0bd8890 | ||
|
|
2a5a0717aa | ||
|
|
ee8364913b | ||
|
|
3267126a33 | ||
|
|
2ee3675ba2 | ||
|
|
faf4fd1ca6 | ||
|
|
53ebf2ca27 | ||
|
|
f3eea9937c | ||
|
|
5c3de759d3 | ||
|
|
0ffd4d9f87 | ||
|
|
416131b4a2 | ||
|
|
7ac04d0204 | ||
|
|
a40337f4e0 | ||
|
|
b45d9aa464 | ||
|
|
48b2e2bc1f | ||
|
|
545007aca5 | ||
|
|
07ce319839 | ||
|
|
0d36c85568 | ||
|
|
139fbfae85 | ||
|
|
0568393157 | ||
|
|
7ec732977a | ||
|
|
a8a7cec376 | ||
|
|
7f6beeeecb | ||
|
|
15092407ea | ||
|
|
bd70d48cdf | ||
|
|
ce04e904e2 | ||
|
|
026ddbf9f1 | ||
|
|
628b178076 | ||
|
|
823a16e8e9 | ||
|
|
407ec1311e | ||
|
|
b9667aae6b | ||
|
|
806b437209 | ||
|
|
1a5232f863 | ||
|
|
7ad119f126 | ||
|
|
1682f4b252 | ||
|
|
6a320d545b | ||
|
|
e7aebd6fbc | ||
|
|
8189abd660 | ||
|
|
5ded153ae4 | ||
|
|
2fd5507c00 | ||
|
|
becb83faf1 | ||
|
|
32263b4574 | ||
|
|
fd3e48dcb2 | ||
|
|
69573cd735 | ||
|
|
5c2af42cdd | ||
|
|
42975b2ff3 | ||
|
|
c7063c00f7 | ||
|
|
89df9536e9 | ||
|
|
0e45c2246f | ||
|
|
81a6afde15 | ||
|
|
adcc8a919c | ||
|
|
a24e6d4278 | ||
|
|
776b2247dd | ||
|
|
37dc1f5ca0 | ||
|
|
a68ddab703 | ||
|
|
877f873910 | ||
|
|
53fa0147ae | ||
|
|
7655c5b150 | ||
|
|
235b625f71 | ||
|
|
014b0024a0 | ||
|
|
b0508e661a | ||
|
|
ab3cd6a8f7 | ||
|
|
85461204c5 | ||
|
|
3abf2b5227 | ||
|
|
0d5d7032fe | ||
|
|
c48b04ab99 | ||
|
|
eaa30dbe21 | ||
|
|
bb0f812f71 | ||
|
|
4c287075da | ||
|
|
09d18f9097 | ||
|
|
47b9bfc8bf | ||
|
|
21d13e8a9c | ||
|
|
079260a7cf | ||
|
|
fdec78c092 | ||
|
|
259ffef0bb | ||
|
|
6661a0803e | ||
|
|
c1471bdbd9 | ||
|
|
a981573e48 | ||
|
|
8fb3a7514e | ||
|
|
846c8e7f1b | ||
|
|
98a1b9e373 | ||
|
|
ba55dd339e | ||
|
|
5a2ce60392 | ||
|
|
7ebcee14e7 | ||
|
|
ccf829fe8c | ||
|
|
a274f5fb86 | ||
|
|
5421a555f4 | ||
|
|
b1233b2b07 | ||
|
|
e55ac59846 | ||
|
|
cd6cd6ba47 | ||
|
|
026b06003b | ||
|
|
02141b86c2 | ||
|
|
9bb2600d73 | ||
|
|
33ea13daf4 | ||
|
|
10b6019e7e | ||
|
|
727f0ab6ce | ||
|
|
31752e9674 | ||
|
|
841d4e6e1e | ||
|
|
7dc890119d | ||
|
|
293a683484 | ||
|
|
737bc15382 | ||
|
|
1a72711999 | ||
|
|
3fea829340 | ||
|
|
6dba14158a | ||
|
|
83bc497f0d | ||
|
|
990a13fd96 | ||
|
|
29b84424f4 | ||
|
|
ef798cd86d | ||
|
|
9d3450f50c | ||
|
|
1db9b77711 | ||
|
|
a6713630b9 | ||
|
|
4168985869 | ||
|
|
1ea8647018 | ||
|
|
f311cae5ad | ||
|
|
7e8e4d2f39 | ||
|
|
1379821b03 | ||
|
|
1722cb8851 | ||
|
|
49c300d2ac | ||
|
|
0e3277bc5a | ||
|
|
9f5e608c61 | ||
|
|
0b82b42128 | ||
|
|
b4828c251f | ||
|
|
7a4f0eed23 | ||
|
|
54a6b0efcb | ||
|
|
9229eae4e0 | ||
|
|
3e8987b460 | ||
|
|
634cbd14f0 | ||
|
|
31cf663f8b | ||
|
|
175145969c | ||
|
|
8db1a01d9a | ||
|
|
b3c5f64315 | ||
|
|
35e717dd49 | ||
|
|
203e668928 | ||
|
|
de38b413f1 | ||
|
|
21010f4de6 | ||
|
|
b03edabb11 | ||
|
|
4001d79e4b | ||
|
|
3513a97a3d | ||
|
|
072855daef | ||
|
|
dc87ba87c9 | ||
|
|
2b3f030d6a | ||
|
|
a3bbdf0bec | ||
|
|
7a7f95f5ef | ||
|
|
746b071be0 | ||
|
|
d307e75b2f | ||
|
|
de5cbd3de3 | ||
|
|
5210b37601 | ||
|
|
4ad9fa144d | ||
|
|
a2d5a10f84 | ||
|
|
b056314fd0 | ||
|
|
3b35d5e0ea | ||
|
|
5e95a70eca | ||
|
|
8d1e43b9d3 | ||
|
|
ed2cf0a9d1 | ||
|
|
ab0b4cad52 | ||
|
|
11469ace78 | ||
|
|
0ab54da2eb | ||
|
|
953eb90e87 | ||
|
|
d2803c4305 | ||
|
|
ab47d6f611 | ||
|
|
07946a18c3 | ||
|
|
0e874735ac | ||
|
|
b56cf72c87 | ||
|
|
9c5cf84c9f | ||
|
|
c19197a960 | ||
|
|
cecd3a2956 | ||
|
|
c8c6beb1b6 | ||
|
|
03635c8d7f | ||
|
|
f942a63c5d | ||
|
|
211badee41 | ||
|
|
c239da542c | ||
|
|
9ea3f23fef | ||
|
|
4681b33eac | ||
|
|
28a6ff3270 | ||
|
|
71bd3aefd6 | ||
|
|
ba15591c22 | ||
|
|
e5b79bf405 | ||
|
|
cfaa8ceba2 | ||
|
|
89a73d775e | ||
|
|
66e3dc7226 | ||
|
|
fce14ebc99 | ||
|
|
5806cadad6 | ||
|
|
68ce6491a5 | ||
|
|
01638ce99e | ||
|
|
c26e43630c | ||
|
|
011779dcf7 | ||
|
|
18e5d5b67a | ||
|
|
0c1afa527b | ||
|
|
159068c772 | ||
|
|
f8841a85d7 | ||
|
|
92620d9c82 | ||
|
|
1cc03ca264 | ||
|
|
5cf8864066 | ||
|
|
c16c6f3ad6 | ||
|
|
b0fa413aa9 | ||
|
|
9c974b40ac | ||
|
|
5b47c4947f | ||
|
|
c62bab3fe5 | ||
|
|
7776060d68 | ||
|
|
3aea6884ac | ||
|
|
1ba0dd503c | ||
|
|
a1837aeb8c | ||
|
|
ee079ce021 | ||
|
|
70563867a6 | ||
|
|
f72d27f7de | ||
|
|
ddc2f55a6f | ||
|
|
8f3fc10625 | ||
|
|
97b0d09ed2 | ||
|
|
e2f9c80cd5 | ||
|
|
394cba3c78 | ||
|
|
f472c05120 | ||
|
|
3efd94914c | ||
|
|
99a6756d28 | ||
|
|
3310315865 | ||
|
|
a7729e3548 | ||
|
|
dc2e4df286 | ||
|
|
386b91a9a7 | ||
|
|
d4847206cf | ||
|
|
7624a50cb1 | ||
|
|
568c044a90 | ||
|
|
a8f8d34c25 | ||
|
|
a308766e47 | ||
|
|
0df86b6308 | ||
|
|
e951a697ec | ||
|
|
1ebaa2a718 | ||
|
|
6cb6daaab2 | ||
|
|
d25fb4770c | ||
|
|
e4e738ec5f | ||
|
|
8a5a67d6f2 | ||
|
|
ee68b9c7ba | ||
|
|
a51b2fa751 | ||
|
|
4c4646e72c | ||
|
|
2ca866b644 | ||
|
|
ed7dfd6b65 | ||
|
|
de79cd1583 | ||
|
|
0e84cfd8ad | ||
|
|
8a9e60afc3 | ||
|
|
b5fa6553af | ||
|
|
5280448cd3 | ||
|
|
891e166996 | ||
|
|
df24532503 | ||
|
|
b82fa19c6f | ||
|
|
8cb136ab9d | ||
|
|
73095bcaff | ||
|
|
ea5f778cc0 | ||
|
|
14a7e39625 | ||
|
|
4a2bfe03da | ||
|
|
8fd972a2f9 | ||
|
|
5d334ee6ee | ||
|
|
dc17f2692c | ||
|
|
94187f7ee1 | ||
|
|
fa7bf179fb | ||
|
|
9bca0b3b90 | ||
|
|
4c93feeddb | ||
|
|
3d061d1dbd | ||
|
|
156f9642fe | ||
|
|
ef008d4ca0 | ||
|
|
0931d9326e | ||
|
|
65ea456bd8 | ||
|
|
7f55613607 | ||
|
|
03b0185b8e | ||
|
|
1fa9707317 | ||
|
|
e10f95b3ea | ||
|
|
82f61035d4 | ||
|
|
4ec20ab9dc | ||
|
|
296d2aa7f4 | ||
|
|
10e711621c | ||
|
|
1e3c894827 | ||
|
|
da4f1b2a98 | ||
|
|
51bbdadfad | ||
|
|
339f695bd6 | ||
|
|
f8c4662c9a | ||
|
|
c825b2584b | ||
|
|
c12c4f64c4 | ||
|
|
b5acbaa31c | ||
|
|
b5de5d0dc0 | ||
|
|
fa4de8f72e | ||
|
|
3b3d5767b0 | ||
|
|
e5a3eae531 | ||
|
|
10633531e5 | ||
|
|
d69db8f336 | ||
|
|
491d6abe49 | ||
|
|
8e9c79061f | ||
|
|
94f57e786d | ||
|
|
db1a7f6084 | ||
|
|
25df14707e | ||
|
|
26672900d5 | ||
|
|
82573dc78c | ||
|
|
35d4eb5168 | ||
|
|
b6d4d10025 | ||
|
|
53fa9ebf11 | ||
|
|
287829d385 | ||
|
|
58b7efe006 | ||
|
|
d2e1e57890 | ||
|
|
6a29cca349 | ||
|
|
c51f7a4249 | ||
|
|
71dfcaa81c | ||
|
|
8e25639126 | ||
|
|
c4e6823396 | ||
|
|
8e5f4a2d53 | ||
|
|
dd6e3973d2 | ||
|
|
33b9a582f3 | ||
|
|
0913b6707b | ||
|
|
476224b980 | ||
|
|
b699ac1aca | ||
|
|
97d8bd89bf | ||
|
|
9a915b2a95 | ||
|
|
d6209e08e6 | ||
|
|
b9acd603a5 | ||
|
|
df61905455 | ||
|
|
71582304f3 | ||
|
|
c6b6967fec | ||
|
|
9b4e49e979 | ||
|
|
e3dac9abbb | ||
|
|
1bc97385b9 | ||
|
|
ffb903092a | ||
|
|
490171650a | ||
|
|
586aae690c | ||
|
|
ba0a7f1f0b | ||
|
|
a50b43598f | ||
|
|
02a18420e5 | ||
|
|
ef6c5870bb | ||
|
|
efbc4780f2 | ||
|
|
5c49706dfd | ||
|
|
36e6b2306b | ||
|
|
4942303c19 | ||
|
|
2168e39156 | ||
|
|
2ee83bf786 | ||
|
|
43a40b9349 | ||
|
|
43a3e40bc7 | ||
|
|
33f96d4010 | ||
|
|
b5e9a5ebb6 | ||
|
|
44f72e7f85 | ||
|
|
08be98f693 | ||
|
|
483f4eaa17 | ||
|
|
8c2207d15e | ||
|
|
3b51e22b2e | ||
|
|
ae1bc54b69 | ||
|
|
a9fbdafda5 | ||
|
|
c58f6107ba | ||
|
|
a4e478a071 | ||
|
|
8ffdd55f79 | ||
|
|
9f67d0f905 | ||
|
|
c5cf16f32a | ||
|
|
3df693a1bb | ||
|
|
1cabca34db | ||
|
|
7b3a1b88e6 | ||
|
|
fbf3ff0112 | ||
|
|
b916937a7a | ||
|
|
3d7ac9d2a1 | ||
|
|
f94b21d4aa | ||
|
|
985ef22d75 | ||
|
|
38b08fab2f | ||
|
|
a49dfeca6e | ||
|
|
253331b7fd | ||
|
|
85cbfde6e4 | ||
|
|
85cd3836e0 | ||
|
|
bbb267331c | ||
|
|
449ba4e192 | ||
|
|
1ad72fd826 | ||
|
|
4a25860e22 | ||
|
|
67f768fec0 | ||
|
|
82ea4e2ae2 | ||
|
|
e0dfba87b6 | ||
|
|
0f449cc7eb | ||
|
|
48fcf66002 | ||
|
|
8eff4f40ff | ||
|
|
20d6f0f2ca | ||
|
|
546d13ef72 | ||
|
|
130bef8c4a | ||
|
|
41c2a80bd7 | ||
|
|
4f71c77ae4 | ||
|
|
96704eb73d | ||
|
|
5c3d1e7dae | ||
|
|
4fb24d05dc | ||
|
|
9b6ef5e54f | ||
|
|
81e9628ab7 | ||
|
|
aaa02968d3 | ||
|
|
302aa5a5f7 | ||
|
|
8bddd455a7 | ||
|
|
a0ff0d71bc | ||
|
|
068726453e | ||
|
|
0973a46245 | ||
|
|
e22d980845 | ||
|
|
0c0afead2c | ||
|
|
3eae9cb30c | ||
|
|
4ef6788ffd | ||
|
|
4198ed1efb | ||
|
|
6f5620dad5 | ||
|
|
1d55458781 | ||
|
|
6297bb967a | ||
|
|
0040c17892 | ||
|
|
258b5cde70 | ||
|
|
a58103ae4a | ||
|
|
bf36a479db | ||
|
|
9a2924ed88 | ||
|
|
4be4a3c72f | ||
|
|
7b6ba0e011 | ||
|
|
4e601c31b4 | ||
|
|
fa0382da2d | ||
|
|
64bd05aa44 | ||
|
|
e651001a57 | ||
|
|
8c251afeb1 | ||
|
|
8e7f1d83ec | ||
|
|
15fc12e525 | ||
|
|
81930c1731 | ||
|
|
ee39615dbd | ||
|
|
058ac3006c | ||
|
|
f0c4414d34 | ||
|
|
4e5125b98d | ||
|
|
8cb1ba5000 | ||
|
|
feac84c5fc | ||
|
|
d762972c95 | ||
|
|
ae893d57a9 | ||
|
|
602d379aef | ||
|
|
18c02f5bf9 | ||
|
|
23033fb0a0 | ||
|
|
5e65c19f00 | ||
|
|
c23809ccd5 | ||
|
|
54d3a2ad47 | ||
|
|
1f7e57181e | ||
|
|
7e886cbf2b | ||
|
|
ebeb742ba6 | ||
|
|
ecbec41b97 | ||
|
|
c760e173fa | ||
|
|
0df042af49 | ||
|
|
fcdbe3ff4a | ||
|
|
963576752b | ||
|
|
5bde9b66d1 | ||
|
|
14d048bea8 | ||
|
|
1cfa07726d | ||
|
|
3b6369a8c8 | ||
|
|
a563c4851c | ||
|
|
28e3fbfebb | ||
|
|
60f8b68690 | ||
|
|
e6ea09641a | ||
|
|
1fd6d80e6d | ||
|
|
104cc3accf | ||
|
|
fc06351fa3 | ||
|
|
787f54feda | ||
|
|
b0c8d46762 | ||
|
|
6430977670 | ||
|
|
8435f40dae | ||
|
|
49a0b2d948 | ||
|
|
7bc9dd6c98 | ||
|
|
1a3a09dfc3 | ||
|
|
32459b3fdc | ||
|
|
52e9daaa1f | ||
|
|
a3734a5f87 | ||
|
|
30e1df0754 | ||
|
|
3959305b4a | ||
|
|
744cab1553 | ||
|
|
8f58c4777e | ||
|
|
8dcd8aa69d | ||
|
|
65a9c4b79b | ||
|
|
22a7cfe9c3 | ||
|
|
1ebf2c1985 | ||
|
|
723ff25067 | ||
|
|
2b5ce35c2d | ||
|
|
39bf3bee59 | ||
|
|
e3b9c9b209 | ||
|
|
74930e995d | ||
|
|
8af6cdf49c | ||
|
|
19a841657c | ||
|
|
d4b1f8694f | ||
|
|
0d8c2ee9ff | ||
|
|
3cbfb47b6e | ||
|
|
0b9746b57e | ||
|
|
fa016b36fb | ||
|
|
69e01b5197 | ||
|
|
ffd2ec9424 | ||
|
|
498979c608 | ||
|
|
3e7b662796 | ||
|
|
6057b40910 | ||
|
|
53572fce5c | ||
|
|
53dca8ce1a | ||
|
|
29d7e0131e | ||
|
|
4ec50d1990 | ||
|
|
187274d7b7 | ||
|
|
5dc8788eab | ||
|
|
de63527d94 | ||
|
|
cb43382896 | ||
|
|
a9e177f1e7 | ||
|
|
6e8668e348 | ||
|
|
7f7c76f706 | ||
|
|
3fe9a7b17f | ||
|
|
fff4020013 | ||
|
|
4ffc0ca047 | ||
|
|
3d19996f34 | ||
|
|
7e5cec66ba | ||
|
|
a7eab13ad6 | ||
|
|
d26a27484b | ||
|
|
ed2a3a76b4 | ||
|
|
49f5523b67 | ||
|
|
548fadc84a | ||
|
|
2bce4466d7 | ||
|
|
f31e86d203 | ||
|
|
8ec098210e | ||
|
|
62e22286bb | ||
|
|
c596bfc44e | ||
|
|
379b31835b | ||
|
|
5a69d9c355 | ||
|
|
e689db4376 | ||
|
|
2d173512af | ||
|
|
adddc8e4ad | ||
|
|
8a27c3edf0 | ||
|
|
7164786165 | ||
|
|
0cfd84d803 |
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Report something that isn't working.
|
||||||
|
title: ''
|
||||||
|
assignees: ''
|
||||||
|
labels: bug
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This is the chatmail core's bug report tracker.
|
||||||
|
For Delta Chat feature requests and support, please go to the forum: https://support.delta.chat
|
||||||
|
Please fill out as much of this form as you can (leaving out stuff that is not applicable is ok).
|
||||||
|
-->
|
||||||
|
|
||||||
|
- Operating System (Linux/Mac/Windows/iOS/Android):
|
||||||
|
- Core Version:
|
||||||
|
- Client Version:
|
||||||
|
|
||||||
|
## Expected behavior
|
||||||
|
|
||||||
|
*What did you try to achieve?*
|
||||||
|
|
||||||
|
## Actual behavior
|
||||||
|
|
||||||
|
*What happened instead?*
|
||||||
|
|
||||||
|
### Steps to reproduce the problem
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
93
.github/workflows/ci.yml
vendored
93
.github/workflows/ci.yml
vendored
@@ -16,21 +16,28 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
|
RUST_VERSION: 1.89.0
|
||||||
|
|
||||||
|
# Minimum Supported Rust Version
|
||||||
|
MSRV: 1.85.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint_rust:
|
lint_rust:
|
||||||
name: Lint Rust
|
name: Lint Rust
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
RUSTUP_TOOLCHAIN: 1.83.0
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: 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 $RUST_VERSION --profile minimal --component rustfmt --component clippy
|
||||||
|
- run: rustup override set $RUST_VERSION
|
||||||
|
shell: bash
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
- name: Run rustfmt
|
- name: Run rustfmt
|
||||||
@@ -42,25 +49,14 @@ jobs:
|
|||||||
- name: Check with only default features
|
- name: Check with only default features
|
||||||
run: cargo check --all-targets
|
run: cargo check --all-targets
|
||||||
|
|
||||||
npm_constants:
|
|
||||||
name: Check if node constants are up to date
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
- name: Rebuild constants
|
|
||||||
run: npm run build:core:constants
|
|
||||||
- name: Check that constants are not changed
|
|
||||||
run: git diff --exit-code
|
|
||||||
|
|
||||||
cargo_deny:
|
cargo_deny:
|
||||||
name: cargo deny
|
name: cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: --all-features --workspace
|
arguments: --all-features --workspace
|
||||||
@@ -71,9 +67,10 @@ jobs:
|
|||||||
name: Check provider database
|
name: Check provider database
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- name: Check provider database
|
- name: Check provider database
|
||||||
run: scripts/update-provider-database.sh
|
run: scripts/update-provider-database.sh
|
||||||
|
|
||||||
@@ -83,9 +80,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: -Dwarnings
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: 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
|
||||||
@@ -97,24 +95,36 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: 1.83.0
|
rust: latest
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
rust: 1.83.0
|
rust: latest
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
rust: 1.83.0
|
rust: latest
|
||||||
|
|
||||||
# Minimum Supported Rust Version = 1.77.0
|
# Minimum Supported Rust Version
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: 1.77.0
|
rust: minimum
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- run:
|
||||||
|
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
if: matrix.rust == 'minimum'
|
||||||
|
- run:
|
||||||
|
echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
if: matrix.rust == 'latest'
|
||||||
|
|
||||||
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: 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 $RUSTUP_TOOLCHAIN
|
||||||
- run: rustup override set ${{ matrix.rust }}
|
shell: bash
|
||||||
|
- run: rustup override set $RUSTUP_TOOLCHAIN
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
@@ -144,15 +154,16 @@ jobs:
|
|||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Build C library
|
- name: Build C library
|
||||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
run: cargo build -p deltachat_ffi
|
||||||
|
|
||||||
- name: Upload C library
|
- name: Upload C library
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -168,9 +179,10 @@ jobs:
|
|||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
@@ -189,9 +201,10 @@ jobs:
|
|||||||
name: Python lint
|
name: Python lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: pip install tox
|
run: pip install tox
|
||||||
@@ -223,20 +236,21 @@ jobs:
|
|||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.10
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.7
|
# Minimum Supported Python Version = 3.8
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
# This is the minimum version for which manylinux Python wheels are
|
||||||
# built. Test it with minimum supported Rust version.
|
# built. Test it with minimum supported Rust version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.7
|
python: 3.8
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download libdeltachat.a
|
- name: Download libdeltachat.a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug
|
path: target/debug
|
||||||
@@ -277,15 +291,16 @@ jobs:
|
|||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.10
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.7
|
# Minimum Supported Python Version = 3.8
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.7
|
python: 3.8
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
@@ -296,7 +311,7 @@ jobs:
|
|||||||
run: pip install tox
|
run: pip install tox
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server
|
- name: Download deltachat-rpc-server
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: target/debug
|
path: target/debug
|
||||||
|
|||||||
78
.github/workflows/deltachat-rpc-server.yml
vendored
78
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -17,6 +17,8 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
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.
|
||||||
@@ -28,11 +30,11 @@ jobs:
|
|||||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
@@ -52,11 +54,11 @@ jobs:
|
|||||||
arch: [win32, win64]
|
arch: [win32, win64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||||
@@ -77,9 +79,10 @@ jobs:
|
|||||||
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup rust target
|
- name: Setup rust target
|
||||||
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||||
@@ -102,11 +105,11 @@ jobs:
|
|||||||
arch: [arm64-v8a, armeabi-v7a]
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
@@ -129,74 +132,74 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
- name: Download Linux aarch64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-linux
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
path: deltachat-rpc-server-aarch64-linux.d
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv7l binary
|
- name: Download Linux armv7l binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv7l-linux
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
path: deltachat-rpc-server-armv7l-linux.d
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv6l binary
|
- name: Download Linux armv6l binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv6l-linux
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
path: deltachat-rpc-server-armv6l-linux.d
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
- name: Download Linux i686 binary
|
- name: Download Linux i686 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-i686-linux
|
name: deltachat-rpc-server-i686-linux
|
||||||
path: deltachat-rpc-server-i686-linux.d
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
- name: Download Linux x86_64 binary
|
- name: Download Linux x86_64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-linux
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
path: deltachat-rpc-server-x86_64-linux.d
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
- name: Download Win32 binary
|
- name: Download Win32 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win32
|
name: deltachat-rpc-server-win32
|
||||||
path: deltachat-rpc-server-win32.d
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
- name: Download Win64 binary
|
- name: Download Win64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win64
|
name: deltachat-rpc-server-win64
|
||||||
path: deltachat-rpc-server-win64.d
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
- name: Download macOS binary for x86_64
|
- name: Download macOS binary for x86_64
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
path: deltachat-rpc-server-x86_64-macos.d
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
- name: Download macOS binary for aarch64
|
- name: Download macOS binary for aarch64
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-macos
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
path: deltachat-rpc-server-aarch64-macos.d
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
- name: Download Android binary for arm64-v8a
|
- name: Download Android binary for arm64-v8a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-arm64-v8a-android
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
- name: Download Android binary for armeabi-v7a
|
- name: Download Android binary for armeabi-v7a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
@@ -245,6 +248,10 @@ jobs:
|
|||||||
cp result/*.whl dist/
|
cp result/*.whl dist/
|
||||||
nix build .#deltachat-rpc-server-win32-wheel
|
nix build .#deltachat-rpc-server-win32-wheel
|
||||||
cp result/*.whl dist/
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-arm64-v8a-android-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
nix build .#deltachat-rpc-server-source
|
nix build .#deltachat-rpc-server-source
|
||||||
cp result/*.tar.gz dist/
|
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 x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||||
@@ -258,8 +265,9 @@ jobs:
|
|||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
gh release upload ${{ github.ref_name }} \
|
gh release upload "$REF_NAME" \
|
||||||
--repo ${{ github.repository }} \
|
--repo ${{ github.repository }} \
|
||||||
bin/* dist/*
|
bin/* dist/*
|
||||||
|
|
||||||
@@ -277,75 +285,76 @@ jobs:
|
|||||||
# Needed to publish the binaries to the release.
|
# Needed to publish the binaries to the release.
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
- name: Download Linux aarch64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-linux
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
path: deltachat-rpc-server-aarch64-linux.d
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv7l binary
|
- name: Download Linux armv7l binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv7l-linux
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
path: deltachat-rpc-server-armv7l-linux.d
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv6l binary
|
- name: Download Linux armv6l binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv6l-linux
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
path: deltachat-rpc-server-armv6l-linux.d
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
- name: Download Linux i686 binary
|
- name: Download Linux i686 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-i686-linux
|
name: deltachat-rpc-server-i686-linux
|
||||||
path: deltachat-rpc-server-i686-linux.d
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
- name: Download Linux x86_64 binary
|
- name: Download Linux x86_64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-linux
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
path: deltachat-rpc-server-x86_64-linux.d
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
- name: Download Win32 binary
|
- name: Download Win32 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win32
|
name: deltachat-rpc-server-win32
|
||||||
path: deltachat-rpc-server-win32.d
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
- name: Download Win64 binary
|
- name: Download Win64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win64
|
name: deltachat-rpc-server-win64
|
||||||
path: deltachat-rpc-server-win64.d
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
- name: Download macOS binary for x86_64
|
- name: Download macOS binary for x86_64
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
path: deltachat-rpc-server-x86_64-macos.d
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
- name: Download macOS binary for aarch64
|
- name: Download macOS binary for aarch64
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-macos
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
path: deltachat-rpc-server-aarch64-macos.d
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
- name: Download Android binary for arm64-v8a
|
- name: Download Android binary for arm64-v8a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-arm64-v8a-android
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
- name: Download Android binary for armeabi-v7a
|
- name: Download Android binary for armeabi-v7a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
@@ -385,8 +394,9 @@ jobs:
|
|||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
gh release upload ${{ github.ref_name }} \
|
gh release upload "$REF_NAME" \
|
||||||
--repo ${{ github.repository }} \
|
--repo ${{ github.repository }} \
|
||||||
deltachat-rpc-server/npm-package/*.tgz
|
deltachat-rpc-server/npm-package/*.tgz
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Dependabot metadata
|
- name: Dependabot metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@v2.2.0
|
uses: dependabot/fetch-metadata@v2.4.0
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
- name: Approve a PR
|
- name: Approve a PR
|
||||||
|
|||||||
@@ -4,17 +4,20 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pack-module:
|
pack-module:
|
||||||
name: "Publish @deltachat/jsonrpc-client"
|
name: "Publish @deltachat/jsonrpc-client"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
8
.github/workflows/jsonrpc.yml
vendored
8
.github/workflows/jsonrpc.yml
vendored
@@ -6,6 +6,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_MIN_STACK: "8388608"
|
RUST_MIN_STACK: "8388608"
|
||||||
@@ -14,9 +16,10 @@ jobs:
|
|||||||
build_and_test:
|
build_and_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -34,9 +37,6 @@ jobs:
|
|||||||
run: npm run test
|
run: npm run test
|
||||||
env:
|
env:
|
||||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
||||||
- name: make sure websocket server version still builds
|
|
||||||
working-directory: deltachat-jsonrpc
|
|
||||||
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm run prettier:check
|
run: npm run prettier:check
|
||||||
|
|||||||
19
.github/workflows/nix.yml
vendored
19
.github/workflows/nix.yml
vendored
@@ -12,16 +12,18 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
format:
|
format:
|
||||||
name: check flake formatting
|
name: check flake formatting
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
- run: nix fmt
|
- run: nix fmt
|
||||||
|
|
||||||
# Check that formatting does not change anything.
|
# Check that formatting does not change anything.
|
||||||
@@ -53,7 +55,9 @@ jobs:
|
|||||||
- deltachat-rpc-server-aarch64-linux
|
- deltachat-rpc-server-aarch64-linux
|
||||||
- deltachat-rpc-server-aarch64-linux-wheel
|
- deltachat-rpc-server-aarch64-linux-wheel
|
||||||
- deltachat-rpc-server-arm64-v8a-android
|
- deltachat-rpc-server-arm64-v8a-android
|
||||||
|
- deltachat-rpc-server-arm64-v8a-android-wheel
|
||||||
- deltachat-rpc-server-armeabi-v7a-android
|
- deltachat-rpc-server-armeabi-v7a-android
|
||||||
|
- deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||||
- deltachat-rpc-server-armv6l-linux
|
- deltachat-rpc-server-armv6l-linux
|
||||||
- deltachat-rpc-server-armv6l-linux-wheel
|
- deltachat-rpc-server-armv6l-linux-wheel
|
||||||
- deltachat-rpc-server-armv7l-linux
|
- deltachat-rpc-server-armv7l-linux
|
||||||
@@ -77,11 +81,11 @@ jobs:
|
|||||||
#- deltachat-rpc-server-x86_64-android
|
#- deltachat-rpc-server-x86_64-android
|
||||||
#- deltachat-rpc-server-x86-android
|
#- deltachat-rpc-server-x86-android
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
- run: nix build .#${{ matrix.installable }}
|
- run: nix build .#${{ matrix.installable }}
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
@@ -91,14 +95,15 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
installable:
|
installable:
|
||||||
- deltachat-rpc-server-aarch64-darwin
|
- deltachat-rpc-server
|
||||||
|
|
||||||
# Fails to bulid
|
# Fails to bulid
|
||||||
|
# - deltachat-rpc-server-aarch64-darwin
|
||||||
# - deltachat-rpc-server-x86_64-darwin
|
# - deltachat-rpc-server-x86_64-darwin
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
- run: nix build .#${{ matrix.installable }}
|
- run: nix build .#${{ matrix.installable }}
|
||||||
|
|||||||
41
.github/workflows/node-docs.yml
vendored
41
.github/workflows/node-docs.yml
vendored
@@ -1,41 +0,0 @@
|
|||||||
# GitHub Actions workflow to build
|
|
||||||
# Node.js bindings documentation
|
|
||||||
# and upload it to the web server.
|
|
||||||
# Built documentation is available at <https://js.delta.chat/>
|
|
||||||
|
|
||||||
name: Generate & upload node.js documentation
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
generate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
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
|
|
||||||
working-directory: node
|
|
||||||
run: |
|
|
||||||
npm i --ignore-scripts
|
|
||||||
npx typedoc
|
|
||||||
mv docs js
|
|
||||||
|
|
||||||
- name: Upload
|
|
||||||
uses: horochx/deploy-via-scp@1.1.0
|
|
||||||
with:
|
|
||||||
user: ${{ secrets.USERNAME }}
|
|
||||||
key: ${{ secrets.KEY }}
|
|
||||||
host: "delta.chat"
|
|
||||||
port: 22
|
|
||||||
local: "node/js"
|
|
||||||
remote: "/var/www/html/"
|
|
||||||
235
.github/workflows/node-package.yml
vendored
235
.github/workflows/node-package.yml
vendored
@@ -1,235 +0,0 @@
|
|||||||
name: "node.js build"
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
- "!py-*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
prebuild:
|
|
||||||
name: Prebuild
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [macos-latest, windows-latest]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "18"
|
|
||||||
- name: System info
|
|
||||||
run: |
|
|
||||||
rustc -vV
|
|
||||||
rustup -vV
|
|
||||||
cargo -vV
|
|
||||||
npm --version
|
|
||||||
node --version
|
|
||||||
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ env.APPDATA }}/npm-cache
|
|
||||||
~/.npm
|
|
||||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
|
||||||
|
|
||||||
- name: Cache cargo index
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry/
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
|
|
||||||
|
|
||||||
- name: Install dependencies & build
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
working-directory: node
|
|
||||||
run: npm install --verbose
|
|
||||||
|
|
||||||
- name: Build Prebuild
|
|
||||||
working-directory: node
|
|
||||||
run: |
|
|
||||||
npm run prebuildify
|
|
||||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
|
||||||
|
|
||||||
- name: Upload Prebuild
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.os }}
|
|
||||||
path: node/${{ matrix.os }}.tar.gz
|
|
||||||
|
|
||||||
prebuild-linux:
|
|
||||||
name: Prebuild Linux
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
|
||||||
# Debian 10 contained glibc 2.28: https://packages.debian.org/buster/libc6
|
|
||||||
container: debian:10
|
|
||||||
steps:
|
|
||||||
# Working directory is owned by 1001:1001 by default.
|
|
||||||
# Change it to our user.
|
|
||||||
- name: Change working directory owner
|
|
||||||
run: chown root:root .
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "18"
|
|
||||||
- run: apt-get update
|
|
||||||
|
|
||||||
# Python is needed for node-gyp
|
|
||||||
- name: Install curl, python and compilers
|
|
||||||
run: apt-get install -y curl build-essential python3
|
|
||||||
- name: Install Rust
|
|
||||||
run: |
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
||||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
||||||
- name: System info
|
|
||||||
run: |
|
|
||||||
rustc -vV
|
|
||||||
rustup -vV
|
|
||||||
cargo -vV
|
|
||||||
npm --version
|
|
||||||
node --version
|
|
||||||
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ env.APPDATA }}/npm-cache
|
|
||||||
~/.npm
|
|
||||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
|
||||||
|
|
||||||
- name: Cache cargo index
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry/
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
|
|
||||||
|
|
||||||
- name: Install dependencies & build
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
working-directory: node
|
|
||||||
run: npm install --verbose
|
|
||||||
|
|
||||||
- name: Build Prebuild
|
|
||||||
working-directory: node
|
|
||||||
run: |
|
|
||||||
npm run prebuildify
|
|
||||||
tar -zcvf "linux.tar.gz" -C prebuilds .
|
|
||||||
|
|
||||||
- name: Upload Prebuild
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: linux
|
|
||||||
path: node/linux.tar.gz
|
|
||||||
|
|
||||||
pack-module:
|
|
||||||
needs: [prebuild, prebuild-linux]
|
|
||||||
name: Package deltachat-node and upload to download.delta.chat
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Install tree
|
|
||||||
run: sudo apt install tree
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "18"
|
|
||||||
- name: Get tag
|
|
||||||
id: tag
|
|
||||||
uses: dawidd6/action-get-tag@v1
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Get Pull Request ID
|
|
||||||
id: prepare
|
|
||||||
run: |
|
|
||||||
tag=${{ steps.tag.outputs.tag }}
|
|
||||||
if [ -z "$tag" ]; then
|
|
||||||
node -e "console.log('DELTACHAT_NODE_TAR_GZ=deltachat-node-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "DELTACHAT_NODE_TAR_GZ=deltachat-node-${{ 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: |
|
|
||||||
rustc -vV
|
|
||||||
rustup -vV
|
|
||||||
cargo -vV
|
|
||||||
npm --version
|
|
||||||
node --version
|
|
||||||
echo $DELTACHAT_NODE_TAR_GZ
|
|
||||||
- name: Download Linux prebuild
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: linux
|
|
||||||
- name: Download macOS prebuild
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: macos-latest
|
|
||||||
- name: Download Windows prebuild
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows-latest
|
|
||||||
- shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir node/prebuilds
|
|
||||||
tar -xvzf linux.tar.gz -C node/prebuilds
|
|
||||||
tar -xvzf macos-latest.tar.gz -C node/prebuilds
|
|
||||||
tar -xvzf windows-latest.tar.gz -C node/prebuilds
|
|
||||||
tree node/prebuilds
|
|
||||||
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
|
|
||||||
- name: Install dependencies without running scripts
|
|
||||||
run: |
|
|
||||||
npm install --ignore-scripts
|
|
||||||
- name: Build constants
|
|
||||||
run: |
|
|
||||||
npm run build:core:constants
|
|
||||||
- name: Build TypeScript part
|
|
||||||
run: |
|
|
||||||
npm run build:bindings:ts
|
|
||||||
- name: Package
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mv node/README.md README.md
|
|
||||||
npm pack .
|
|
||||||
ls -lah
|
|
||||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
|
||||||
- name: Upload prebuild
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: deltachat-node.tgz
|
|
||||||
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
|
||||||
# Upload to download.delta.chat/node/preview/
|
|
||||||
- name: Upload deltachat-node 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_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Post links to details
|
|
||||||
if: steps.upload-preview.outcome == 'success'
|
|
||||||
run: node ./node/scripts/postLinksToDetails.js
|
|
||||||
env:
|
|
||||||
URL: preview/${{ env.DELTACHAT_NODE_TAR_GZ }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# Upload to download.delta.chat/node/
|
|
||||||
- name: Upload deltachat-node 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_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
|
||||||
68
.github/workflows/node-tests.yml
vendored
68
.github/workflows/node-tests.yml
vendored
@@ -1,68 +0,0 @@
|
|||||||
# GitHub Actions workflow
|
|
||||||
# to test Node.js bindings.
|
|
||||||
|
|
||||||
name: "node.js tests"
|
|
||||||
|
|
||||||
# Cancel previously started workflow runs
|
|
||||||
# when the branch is updated.
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tests:
|
|
||||||
name: Tests
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "18"
|
|
||||||
- name: System info
|
|
||||||
run: |
|
|
||||||
rustc -vV
|
|
||||||
rustup -vV
|
|
||||||
cargo -vV
|
|
||||||
npm --version
|
|
||||||
node --version
|
|
||||||
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ env.APPDATA }}/npm-cache
|
|
||||||
~/.npm
|
|
||||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
|
||||||
|
|
||||||
- name: Cache cargo index
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry/
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
|
|
||||||
|
|
||||||
- name: Install dependencies & build
|
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
|
||||||
working-directory: node
|
|
||||||
run: npm install --verbose
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
timeout-minutes: 10
|
|
||||||
working-directory: node
|
|
||||||
run: npm run test
|
|
||||||
env:
|
|
||||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
|
||||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
|
||||||
@@ -5,15 +5,18 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build distribution
|
name: Build distribution
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- name: Install pypa/build
|
- name: Install pypa/build
|
||||||
run: python3 -m pip install build
|
run: python3 -m pip install build
|
||||||
- name: Build a binary wheel and a source tarball
|
- name: Build a binary wheel and a source tarball
|
||||||
@@ -39,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download all the dists
|
- name: Download all the dists
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|||||||
6
.github/workflows/repl.yml
vendored
6
.github/workflows/repl.yml
vendored
@@ -7,16 +7,18 @@ name: Build Windows REPL .exe
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_repl:
|
build_repl:
|
||||||
name: Build REPL example
|
name: Build REPL example
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: nix build .#deltachat-repl-win64
|
run: nix build .#deltachat-repl-win64
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
|
|||||||
16
.github/workflows/upload-docs.yml
vendored
16
.github/workflows/upload-docs.yml
vendored
@@ -6,14 +6,17 @@ on:
|
|||||||
- main
|
- main
|
||||||
- build_jsonrpc_docs_ci
|
- build_jsonrpc_docs_ci
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-rs:
|
build-rs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: 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
|
||||||
@@ -28,12 +31,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
- name: Build Python documentation
|
- name: Build Python documentation
|
||||||
run: nix build .#python-docs
|
run: nix build .#python-docs
|
||||||
- name: Upload to py.delta.chat
|
- name: Upload to py.delta.chat
|
||||||
@@ -47,12 +50,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: DeterminateSystems/magic-nix-cache-action@main
|
|
||||||
- name: Build C documentation
|
- name: Build C documentation
|
||||||
run: nix build .#docs
|
run: nix build .#docs
|
||||||
- name: Upload to c.delta.chat
|
- name: Upload to c.delta.chat
|
||||||
@@ -69,9 +72,10 @@ jobs:
|
|||||||
working-directory: ./deltachat-jsonrpc/typescript
|
working-directory: ./deltachat-jsonrpc/typescript
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
5
.github/workflows/upload-ffi-docs.yml
vendored
5
.github/workflows/upload-ffi-docs.yml
vendored
@@ -9,14 +9,17 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
persist-credentials: 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
|
||||||
|
|||||||
31
.github/workflows/zizmor-scan.yml
vendored
Normal file
31
.github/workflows/zizmor-scan.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: GitHub Actions Security Analysis with zizmor
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
zizmor:
|
||||||
|
name: zizmor latest via PyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install the latest version of uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
|
||||||
|
- name: Run zizmor
|
||||||
|
run: uvx zizmor --format sarif . > results.sarif
|
||||||
|
|
||||||
|
- name: Upload SARIF file
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
|
category: zizmor
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,7 +1,9 @@
|
|||||||
/target
|
target/
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
/build
|
/build
|
||||||
/dist
|
/dist
|
||||||
|
/fuzz/fuzz_targets/corpus/
|
||||||
|
/fuzz/fuzz_targets/crashes/
|
||||||
|
|
||||||
# ignore vi temporaries
|
# ignore vi temporaries
|
||||||
*~
|
*~
|
||||||
@@ -51,3 +53,4 @@ result
|
|||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv
|
.direnv
|
||||||
|
.aider*
|
||||||
|
|||||||
1959
CHANGELOG.md
1959
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ add_custom_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 --target-dir=${CMAKE_BINARY_DIR}/target --release --features jsonrpc
|
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Bug reports
|
## Bug reports
|
||||||
|
|
||||||
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
|
If you found a bug, [report it on GitHub](https://github.com/chatmail/core/issues).
|
||||||
If the bug you found is specific to
|
If the bug you found is specific to
|
||||||
[Android](https://github.com/deltachat/deltachat-android/issues),
|
[Android](https://github.com/deltachat/deltachat-android/issues),
|
||||||
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
||||||
@@ -67,7 +67,7 @@ If you want to contribute a code, follow this guide.
|
|||||||
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||||
```
|
```
|
||||||
|
|
||||||
4. [**Open a Pull Request**](https://github.com/deltachat/deltachat-core-rust/pulls).
|
4. [**Open a Pull Request**](https://github.com/chatmail/core/pulls).
|
||||||
|
|
||||||
Refer to the corresponding issue.
|
Refer to the corresponding issue.
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ For other ways to contribute, refer to the [website](https://delta.chat/en/contr
|
|||||||
|
|
||||||
You can find the list of good first issues
|
You can find the list of good first issues
|
||||||
and a link to this guide
|
and a link to this guide
|
||||||
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
on the contributing page: <https://github.com/chatmail/core/contribute>
|
||||||
|
|
||||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||||
[git-cliff]: https://git-cliff.org/
|
[git-cliff]: https://git-cliff.org/
|
||||||
|
|||||||
3859
Cargo.lock
generated
3859
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
97
Cargo.toml
97
Cargo.toml
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.152.2"
|
version = "2.11.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.77"
|
rust-version = "1.85"
|
||||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
repository = "https://github.com/chatmail/core"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
@@ -18,6 +18,9 @@ opt-level = 1
|
|||||||
debug = 1
|
debug = 1
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
|
|
||||||
|
[profile.fuzz]
|
||||||
|
inherits = "test"
|
||||||
|
|
||||||
# Always optimize dependencies.
|
# Always optimize dependencies.
|
||||||
# This does not apply to crates in the workspace.
|
# This does not apply to crates in the workspace.
|
||||||
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
|
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
|
||||||
@@ -41,42 +44,40 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-broadcast = "0.7.2"
|
async-broadcast = "0.7.2"
|
||||||
async-channel = { workspace = true }
|
async-channel = { workspace = true }
|
||||||
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
|
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||||
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.10", default-features = false, features = ["runtime-tokio"] }
|
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
brotli = { version = "7", default-features=false, features = ["std"] }
|
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
data-encoding = "2.9.0"
|
||||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
|
||||||
escaper = "0.1"
|
escaper = "0.1"
|
||||||
fast-socks5 = "0.10"
|
fast-socks5 = "0.10"
|
||||||
fd-lock = "4"
|
fd-lock = "4"
|
||||||
futures-lite = { workspace = true }
|
futures-lite = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
hickory-resolver = "=0.25.0-alpha.2"
|
hickory-resolver = "0.25.2"
|
||||||
http-body-util = "0.1.2"
|
http-body-util = "0.1.3"
|
||||||
humansize = "2"
|
humansize = "2"
|
||||||
hyper = "1"
|
hyper = "1"
|
||||||
hyper-util = "0.1.10"
|
hyper-util = "0.1.16"
|
||||||
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||||
iroh-gossip = { version = "0.28.1", default-features = false, features = ["net"] }
|
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||||
iroh-net = { version = "0.28.1", default-features = false }
|
iroh = { version = "0.35", default-features = false }
|
||||||
kamadak-exif = "0.6.1"
|
kamadak-exif = "0.6.1"
|
||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
mailparse = "0.15"
|
mail-builder = { version = "0.4.4", default-features = false }
|
||||||
|
mailparse = { workspace = true }
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
num_cpus = "1.16"
|
num_cpus = "1.17"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
parking_lot = "0.12.4"
|
||||||
parking_lot = "0.12"
|
|
||||||
percent-encoding = "2.3"
|
percent-encoding = "2.3"
|
||||||
pgp = { version = "0.14.2", default-features = false }
|
pgp = { version = "0.16.0", default-features = false }
|
||||||
pin-project = "1"
|
pin-project = "1"
|
||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
quick-xml = "0.37"
|
quick-xml = "0.37"
|
||||||
@@ -85,42 +86,44 @@ rand = { workspace = true }
|
|||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||||
rust-hsluv = "0.1"
|
rust-hsluv = "0.1"
|
||||||
rustls-pki-types = "1.10.1"
|
rustls-pki-types = "1.12.0"
|
||||||
rustls = { version = "0.23.19", default-features = false }
|
rustls = { version = "0.23.22", default-features = false }
|
||||||
sanitize-filename = { workspace = true }
|
sanitize-filename = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
sha-1 = "0.10"
|
sha-1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
shadowsocks = { version = "1.21.0", default-features = false, features = ["aead-cipher-2022"] }
|
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||||
smallvec = "1.13.2"
|
smallvec = "1.15.1"
|
||||||
strum = "0.26"
|
strum = "0.27"
|
||||||
strum_macros = "0.26"
|
strum_macros = "0.27"
|
||||||
tagger = "4.3.4"
|
tagger = "4.3.4"
|
||||||
textwrap = "0.16.1"
|
textwrap = "0.16.2"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio-io-timeout = "1.2.0"
|
tokio-io-timeout = "1.2.1"
|
||||||
tokio-rustls = { version = "0.26.1", default-features = false }
|
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
tokio-stream = { version = "0.1.17", 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 = { workspace = true }
|
tokio-util = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
toml = "0.8"
|
toml = "0.9"
|
||||||
|
tracing = "0.1.41"
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
webpki-roots = "0.26.7"
|
webpki-roots = "0.26.8"
|
||||||
|
blake3 = "1.8.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||||
futures-lite = { workspace = true }
|
futures-lite = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
testdir = "0.9.0"
|
testdir = "0.9.3"
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
@@ -134,6 +137,7 @@ members = [
|
|||||||
"deltachat-time",
|
"deltachat-time",
|
||||||
"format-flowed",
|
"format-flowed",
|
||||||
"deltachat-contact-tools",
|
"deltachat-contact-tools",
|
||||||
|
"fuzz",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
@@ -157,6 +161,10 @@ harness = false
|
|||||||
name = "get_chat_msgs"
|
name = "get_chat_msgs"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "marknoticed_chat"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "get_chatlist"
|
name = "get_chatlist"
|
||||||
harness = false
|
harness = false
|
||||||
@@ -167,37 +175,38 @@ harness = false
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-channel = "2.3.1"
|
async-channel = "2.5.0"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
chrono = { version = "0.4.39", default-features = false }
|
chrono = { version = "0.4.41", default-features = false }
|
||||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||||
deltachat = { path = ".", default-features = false }
|
deltachat = { path = ".", default-features = false }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
futures-lite = "2.5.0"
|
futures-lite = "2.6.0"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
mailparse = "0.16.1"
|
||||||
nu-ansi-term = "0.46"
|
nu-ansi-term = "0.46"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
once_cell = "1.20.2"
|
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
rusqlite = "0.32"
|
rusqlite = "0.36"
|
||||||
sanitize-filename = "0.5"
|
sanitize-filename = "0.5"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tempfile = "3.14.0"
|
tempfile = "3.20.0"
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
tokio = "1"
|
tokio = "1"
|
||||||
tokio-util = "0.7.13"
|
tokio-util = "0.7.14"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
yerpc = "0.6.2"
|
yerpc = "0.6.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
internals = []
|
internals = []
|
||||||
vendored = [
|
vendored = [
|
||||||
"rusqlite/bundled-sqlcipher-vendored-openssl"
|
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||||
|
"async-native-tls/vendored"
|
||||||
]
|
]
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
|
|||||||
101
README.md
101
README.md
@@ -1,19 +1,41 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
|
<img alt="Chatmail logo" src="https://github.com/user-attachments/assets/25742da7-a837-48cd-a503-b303af55f10d" width="300" style="float:middle;" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
<a href="https://github.com/chatmail/core/actions/workflows/ci.yml">
|
||||||
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
<img alt="Rust CI" src="https://github.com/chatmail/core/actions/workflows/ci.yml/badge.svg">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
|
<a href="https://deps.rs/repo/github/chatmail/core">
|
||||||
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
|
<img alt="dependency status" src="https://deps.rs/repo/github/chatmail/core/status.svg">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
The chatmail core library implements low-level network and encryption protocols,
|
||||||
The core library for Delta Chat, written in Rust
|
integrated by many chat bots and higher level applications,
|
||||||
</p>
|
allowing to securely participate in the globally scaled e-mail server network.
|
||||||
|
We provide reproducibly-built `deltachat-rpc-server` static binaries
|
||||||
|
that offer a stdio-based high-level JSON-RPC API for instant messaging purposes.
|
||||||
|
|
||||||
|
The following protocols are handled without requiring API users to know much about them:
|
||||||
|
|
||||||
|
- secure TLS setup with DNS caching and shadowsocks/proxy support
|
||||||
|
|
||||||
|
- robust [SMTP](https://github.com/chatmail/async-imap)
|
||||||
|
and [IMAP](https://github.com/chatmail/async-smtp) handling
|
||||||
|
|
||||||
|
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
|
||||||
|
and [MIME building](https://github.com/stalwartlabs/mail-builder).
|
||||||
|
|
||||||
|
- security-audited end-to-end encryption with [rPGP](https://github.com/rpgp/rpgp)
|
||||||
|
and [Autocrypt and SecureJoin protocols](https://securejoin.rtfd.io)
|
||||||
|
|
||||||
|
- ephemeral [Peer-to-Peer networking using Iroh](https://iroh.computer) for multi-device setup and
|
||||||
|
[webxdc realtime data](https://delta.chat/en/2024-11-20-webxdc-realtime).
|
||||||
|
|
||||||
|
- a simulation- and real-world tested [P2P group membership
|
||||||
|
protocol without requiring server state](https://github.com/chatmail/models/tree/main/group-membership).
|
||||||
|
|
||||||
|
|
||||||
## Installing Rust and Cargo
|
## Installing Rust and Cargo
|
||||||
|
|
||||||
@@ -27,12 +49,12 @@ $ curl https://sh.rustup.rs -sSf | sh
|
|||||||
|
|
||||||
## Using the CLI client
|
## Using the CLI client
|
||||||
|
|
||||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
Compile and run the command line utility, using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cargo run --locked -p deltachat-repl -- ~/deltachat-db
|
$ cargo run --locked -p deltachat-repl -- ~/profile-db
|
||||||
```
|
```
|
||||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
where ~/profile-db is the database file. The utility will create it if it does not exist.
|
||||||
|
|
||||||
Optionally, install `deltachat-repl` binary with
|
Optionally, install `deltachat-repl` binary with
|
||||||
```
|
```
|
||||||
@@ -40,13 +62,13 @@ $ cargo install --locked --path deltachat-repl/
|
|||||||
```
|
```
|
||||||
and run as
|
and run as
|
||||||
```
|
```
|
||||||
$ deltachat-repl ~/deltachat-db
|
$ deltachat-repl ~/profile-db
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure your account (if not already configured):
|
Configure your account (if not already configured):
|
||||||
|
|
||||||
```
|
```
|
||||||
Delta Chat Core is awaiting your commands.
|
Chatmail is awaiting your commands.
|
||||||
> set addr your@email.org
|
> set addr your@email.org
|
||||||
> set mail_pw yourpassword
|
> set mail_pw yourpassword
|
||||||
> configure
|
> configure
|
||||||
@@ -58,37 +80,43 @@ Connect to your mail server (if already configured):
|
|||||||
> connect
|
> connect
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a contact:
|
Export your public key to a vCard file:
|
||||||
|
|
||||||
|
```
|
||||||
|
> make-vcard my.vcard 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Create contacts by address or vCard file:
|
||||||
|
|
||||||
```
|
```
|
||||||
> addcontact yourfriends@email.org
|
> addcontact yourfriends@email.org
|
||||||
Command executed successfully.
|
> import-vcard key-contact.vcard
|
||||||
```
|
```
|
||||||
|
|
||||||
List contacts:
|
List contacts:
|
||||||
|
|
||||||
```
|
```
|
||||||
> listcontacts
|
> listcontacts
|
||||||
Contact#10: <name unset> <yourfriends@email.org>
|
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
|
||||||
Contact#1: Me √√ <your@email.org>
|
Contact#Contact#Self: Me √ <your@email.org>
|
||||||
|
2 key contacts.
|
||||||
|
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
||||||
|
1 address contacts.
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a chat with your friend and send a message:
|
Create a chat with your friend and send a message:
|
||||||
|
|
||||||
```
|
```
|
||||||
> createchat 10
|
> createchat 10
|
||||||
Single#10 created successfully.
|
Single#Chat#12 created successfully.
|
||||||
> chat 10
|
> chat 12
|
||||||
Single#10: yourfriends@email.org [yourfriends@email.org]
|
Selecting chat Chat#12
|
||||||
|
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
|
||||||
|
0 messages.
|
||||||
|
81.252µs to create this list, 123.625µs to mark all messages as noticed.
|
||||||
> send hi
|
> send hi
|
||||||
Message sent.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If `yourfriend@email.org` uses DeltaChat, but does not receive message just
|
|
||||||
sent, it is advisable to check `Spam` folder. It is known that at least
|
|
||||||
`gmx.com` treat such test messages as spam, unless told otherwise with web
|
|
||||||
interface.
|
|
||||||
|
|
||||||
List messages when inside a chat:
|
List messages when inside a chat:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -104,7 +132,7 @@ For more commands type:
|
|||||||
## Installing libdeltachat system wide
|
## Installing libdeltachat system wide
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/deltachat/deltachat-core-rust.git
|
$ git clone https://github.com/chatmail/core.git
|
||||||
$ cd deltachat-core-rust
|
$ cd deltachat-core-rust
|
||||||
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
||||||
$ cmake --build build
|
$ cmake --build build
|
||||||
@@ -145,7 +173,7 @@ $ cargo install cargo-bolero
|
|||||||
Run fuzzing tests with
|
Run fuzzing tests with
|
||||||
```sh
|
```sh
|
||||||
$ cd fuzz
|
$ cd fuzz
|
||||||
$ cargo bolero test fuzz_mailparse --release=false -s NONE
|
$ cargo bolero test fuzz_mailparse -s NONE
|
||||||
```
|
```
|
||||||
|
|
||||||
Corpus is created at `fuzz/fuzz_targets/corpus`,
|
Corpus is created at `fuzz/fuzz_targets/corpus`,
|
||||||
@@ -153,11 +181,6 @@ you can add initial inputs there.
|
|||||||
For `fuzz_mailparse` target corpus can be populated with
|
For `fuzz_mailparse` target corpus can be populated with
|
||||||
`../test-data/message/*.eml`.
|
`../test-data/message/*.eml`.
|
||||||
|
|
||||||
To run with AFL instead of libFuzzer:
|
|
||||||
```sh
|
|
||||||
$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
|
|
||||||
```
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||||
@@ -165,20 +188,16 @@ $ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
|
|||||||
## Update Provider Data
|
## Update Provider Data
|
||||||
|
|
||||||
To add the updates from the
|
To add the updates from the
|
||||||
[provider-db](https://github.com/deltachat/provider-db) to the core, run:
|
[provider-db](https://github.com/chatmail/provider-db) to the core,
|
||||||
|
check line `REV=` inside `./scripts/update-provider-database.sh`
|
||||||
```
|
and then run the script.
|
||||||
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Language bindings and frontend projects
|
## Language bindings and frontend projects
|
||||||
|
|
||||||
Language bindings are available for:
|
Language bindings are available for:
|
||||||
|
|
||||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||||
- **Node.js**
|
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
||||||
- over JSON-RPC: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
|
||||||
- over CFFI[^1]: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat/)\]
|
|
||||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||||
- **Go**
|
- **Go**
|
||||||
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
For example, to release version 1.116.0 of the core, do the following steps.
|
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/chatmail/core/labels/blocker).
|
||||||
|
|
||||||
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`.
|
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||||
|
|
||||||
3. add a link to compare previous with current version to the end of CHANGELOG.md:
|
3. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||||
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
|
`[1.116.0]: https://github.com/chatmail/core/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`.
|
||||||
|
|
||||||
|
|||||||
42
STYLE.md
42
STYLE.md
@@ -78,12 +78,40 @@ All errors should be handled in one of these ways:
|
|||||||
- With `.log_err().ok()`.
|
- With `.log_err().ok()`.
|
||||||
- Bubbled up with `?`.
|
- Bubbled up with `?`.
|
||||||
|
|
||||||
|
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
|
||||||
|
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
|
||||||
|
```
|
||||||
|
while let Some(event) = stream.try_next().await? {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
instead of
|
||||||
|
```
|
||||||
|
while let Some(event_res) = stream.next().await {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
as it allows bubbling up the error early with `?`
|
||||||
|
with no way to accidentally skip error processing
|
||||||
|
with early `continue` or `break`.
|
||||||
|
Some streams reading from a connection
|
||||||
|
return infinite number of `Some(Err(_))`
|
||||||
|
items when connection breaks and not processing
|
||||||
|
errors may result in infinite loop.
|
||||||
|
|
||||||
`backtrace` feature is enabled for `anyhow` crate
|
`backtrace` feature is enabled for `anyhow` crate
|
||||||
and `debug = 1` option is set in the test profile.
|
and `debug = 1` option is set in the test profile.
|
||||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||||
and get a backtrace with line numbers in resultified tests
|
and get a backtrace with line numbers in resultified tests
|
||||||
which return `anyhow::Result`.
|
which return `anyhow::Result`.
|
||||||
|
|
||||||
|
`unwrap` and `expect` are not used in the library
|
||||||
|
because panics are difficult to debug on user devices.
|
||||||
|
However, in the tests `.expect` may be used.
|
||||||
|
Follow
|
||||||
|
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
|
||||||
|
for `.expect` message style.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
For logging, use `info!`, `warn!` and `error!` macros.
|
For logging, use `info!`, `warn!` and `error!` macros.
|
||||||
@@ -96,3 +124,17 @@ Format anyhow errors with `{:#}` to print all the contexts like this:
|
|||||||
```
|
```
|
||||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Documentation comments
|
||||||
|
|
||||||
|
All public modules, methods and fields should be documented.
|
||||||
|
This is checked by [`missing_docs`](https://doc.rust-lang.org/rustdoc/lints.html#missing_docs) lint.
|
||||||
|
|
||||||
|
Private items do not have to be documented,
|
||||||
|
but CI uses `cargo doc --document-private-items`
|
||||||
|
to build the documentation,
|
||||||
|
so it is preferred that new items
|
||||||
|
are documented.
|
||||||
|
|
||||||
|
Follow Rust guidelines for the documentation comments:
|
||||||
|
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
||||||
|
|||||||
BIN
assets/icon-unencrypted.png
Normal file
BIN
assets/icon-unencrypted.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
47
assets/icon-unencrypted.svg
Normal file
47
assets/icon-unencrypted.svg
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
height="480"
|
||||||
|
viewBox="0 -960 9600 9600"
|
||||||
|
width="480"
|
||||||
|
fill="#ffffff"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="icon-email.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="0.99091847"
|
||||||
|
inkscape:cx="263.392"
|
||||||
|
inkscape:cy="177.613"
|
||||||
|
inkscape:window-width="1884"
|
||||||
|
inkscape:window-height="1052"
|
||||||
|
inkscape:window-x="36"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<rect
|
||||||
|
style="fill:#8c8c8c;fill-opacity:1;stroke:none;stroke-width:680.523;stroke-dasharray:none;paint-order:markers fill stroke"
|
||||||
|
id="rect1"
|
||||||
|
width="9951.9541"
|
||||||
|
height="9767.4756"
|
||||||
|
x="-71.697792"
|
||||||
|
y="-1012.83"
|
||||||
|
ry="0.43547946" />
|
||||||
|
<path
|
||||||
|
d="m 2948.0033,5553.6941 q -130.7292,0 -228.7761,-96.3953 -98.0468,-96.3953 -98.0468,-224.9223 V 2447.6234 q 0,-128.527 98.0468,-224.9223 98.0469,-96.3953 228.7761,-96.3953 h 3703.9934 q 130.7292,0 228.776,96.3953 98.0469,96.3953 98.0469,224.9223 v 2784.7531 q 0,128.527 -98.0469,224.9223 -98.0468,96.3953 -228.776,96.3953 z M 4800,3936.3952 2948.0033,2742.1646 V 5232.3765 H 6651.9967 V 2742.1646 Z m 0,-321.3176 1830.2085,-1167.4541 h -3654.97 z m -1851.9967,-872.913 v -294.5412 2784.7531 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:5.40098" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
7
assets/self-reporting-bot.vcf
Normal file
7
assets/self-reporting-bot.vcf
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
EMAIL:self_reporting@testrun.org
|
||||||
|
FN:Statistics bot
|
||||||
|
KEY:data:application/pgp-keys;base64,xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCMPNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUICQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+NqI4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARlt8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGBYIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ4=
|
||||||
|
REV:20250412T195751Z
|
||||||
|
END:VCARD
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use std::hint::black_box;
|
||||||
|
|
||||||
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use deltachat::Events;
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::Contact;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::Events;
|
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
async fn address_book_benchmark(n: u32, read_count: u32) {
|
async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
use std::hint::black_box;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
use deltachat::accounts::Accounts;
|
use deltachat::accounts::Accounts;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
use std::hint::black_box;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use deltachat::Events;
|
||||||
use deltachat::chat::{self, ChatId};
|
use deltachat::chat::{self, ChatId};
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::Events;
|
|
||||||
|
|
||||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||||
let id = 100;
|
let id = 100;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
use std::hint::black_box;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use deltachat::Events;
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::Events;
|
|
||||||
|
|
||||||
async fn get_chat_list_benchmark(context: &Context) {
|
async fn get_chat_list_benchmark(context: &Context) {
|
||||||
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||||
|
|||||||
95
benches/marknoticed_chat.rs
Normal file
95
benches/marknoticed_chat.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
|
use std::hint::black_box;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
||||||
|
use deltachat::Events;
|
||||||
|
use deltachat::chat::{self, ChatId};
|
||||||
|
use deltachat::chatlist::Chatlist;
|
||||||
|
use deltachat::context::Context;
|
||||||
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use futures_lite::future::block_on;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
async fn marknoticed_chat_benchmark(context: &Context, chats: &[ChatId]) {
|
||||||
|
for c in chats.iter().take(20) {
|
||||||
|
chat::marknoticed_chat(context, *c).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
||||||
|
// messages, such as your primary account.
|
||||||
|
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|
||||||
|
let chats: Vec<_> = rt.block_on(async {
|
||||||
|
let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
|
||||||
|
let len = chatlist.len();
|
||||||
|
(1..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
// This mainly tests the performance of marknoticed_chat()
|
||||||
|
// when nothing has to be done
|
||||||
|
c.bench_function(
|
||||||
|
"chat::marknoticed_chat (mark 20 chats as noticed repeatedly)",
|
||||||
|
|b| {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let dir = dir.path();
|
||||||
|
let new_db = dir.join("dc.db");
|
||||||
|
std::fs::copy(&path, &new_db).unwrap();
|
||||||
|
|
||||||
|
let context = block_on(async {
|
||||||
|
Context::new(Path::new(&new_db), 100, Events::new(), StockStrings::new())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
b.to_async(&rt)
|
||||||
|
.iter(|| marknoticed_chat_benchmark(&context, black_box(&chats)))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the first 20 chats contain fresh messages or reactions,
|
||||||
|
// this tests the performance of marking them as noticed.
|
||||||
|
c.bench_function(
|
||||||
|
"chat::marknoticed_chat (mark 20 chats as noticed, resetting after every iteration)",
|
||||||
|
|b| {
|
||||||
|
b.to_async(&rt).iter_batched(
|
||||||
|
|| {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let new_db = dir.path().join("dc.db");
|
||||||
|
std::fs::copy(&path, &new_db).unwrap();
|
||||||
|
|
||||||
|
let context = block_on(async {
|
||||||
|
Context::new(
|
||||||
|
Path::new(&new_db),
|
||||||
|
100,
|
||||||
|
Events::new(),
|
||||||
|
StockStrings::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
(dir, context)
|
||||||
|
},
|
||||||
|
|(_dir, context)| {
|
||||||
|
let chats = &chats;
|
||||||
|
async move {
|
||||||
|
marknoticed_chat_benchmark(black_box(&context), black_box(chats)).await
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BatchSize::PerIteration,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, criterion_benchmark);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
use std::hint::black_box;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
|
Events,
|
||||||
config::Config,
|
config::Config,
|
||||||
context::Context,
|
context::Context,
|
||||||
imex::{imex, ImexMode},
|
imex::{ImexMode, imex},
|
||||||
receive_imf::receive_imf,
|
receive_imf::receive_imf,
|
||||||
stock_str::StockStrings,
|
stock_str::StockStrings,
|
||||||
Events,
|
|
||||||
};
|
};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
use std::hint::black_box;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use deltachat::Events;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::Events;
|
|
||||||
|
|
||||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||||
let id = 100;
|
let id = 100;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
use criterion::{criterion_group, criterion_main, Criterion};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::{info, Event, EventType, Events};
|
use deltachat::{Event, EventType, Events};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
async fn send_events_benchmark(context: &Context) {
|
async fn send_events_benchmark(context: &Context) {
|
||||||
let emitter = context.get_event_emitter();
|
let emitter = context.get_event_emitter();
|
||||||
for _i in 0..1_000_000 {
|
for _i in 0..1_000_000 {
|
||||||
info!(context, "interesting event...");
|
context.emit_event(EventType::Info("interesting event...".to_string()));
|
||||||
}
|
}
|
||||||
info!(context, "DONE");
|
context.emit_event(EventType::Info("DONE".to_string()));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match emitter.recv().await.unwrap() {
|
match emitter.recv().await.unwrap() {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ filter_unconventional = false
|
|||||||
split_commits = false
|
split_commits = false
|
||||||
# regex for preprocessing the commit messages
|
# regex for preprocessing the commit messages
|
||||||
commit_preprocessors = [
|
commit_preprocessors = [
|
||||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers
|
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/chatmail/core/pull/${2}))"}, # replace pull request / issue numbers
|
||||||
]
|
]
|
||||||
# regex for parsing and grouping commits
|
# regex for parsing and grouping commits
|
||||||
commit_parsers = [
|
commit_parsers = [
|
||||||
@@ -82,11 +82,11 @@ footer = """
|
|||||||
{% if release.version -%}
|
{% if release.version -%}
|
||||||
{% if release.previous.version -%}
|
{% if release.previous.version -%}
|
||||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||||
https://github.com/deltachat/deltachat-core-rust\
|
https://github.com/chatmail/core\
|
||||||
/compare/{{ release.previous.version }}..{{ release.version }}
|
/compare/{{ release.previous.version }}..{{ release.version }}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% else -%}
|
{% else -%}
|
||||||
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
|
[unreleased]: https://github.com/chatmail/core\
|
||||||
/compare/{{ release.previous.version }}..HEAD
|
/compare/{{ release.previous.version }}..HEAD
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ license = "MPL-2.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
|
||||||
regex = { 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.
|
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||||
|
|||||||
@@ -29,203 +29,14 @@
|
|||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use anyhow::Context as _;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::{DateTime, NaiveDateTime};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
#[derive(Debug)]
|
mod vcard;
|
||||||
/// A Contact, as represented in a VCard.
|
pub use vcard::{make_vcard, parse_vcard, VcardContact};
|
||||||
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`
|
|
||||||
|
|
||||||
// Note: This doesn't handle the case where there are quotes around a colon,
|
|
||||||
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
|
|
||||||
// This could be improved in the future, but for now, the parsing is good enough.
|
|
||||||
let (params, value) = remainder.split_once(':')?;
|
|
||||||
// In the example from above, `params` is now `;TYPE=work`
|
|
||||||
// and `value` is now `alice@example.com`
|
|
||||||
|
|
||||||
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 mut line in lines.by_ref() {
|
|
||||||
if let Some(remainder) = remove_prefix(line, "item1.") {
|
|
||||||
// Remove the group name, if the group is called "item1".
|
|
||||||
// If necessary, we can improve this to also remove groups that are called something different that "item1".
|
|
||||||
//
|
|
||||||
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
|
|
||||||
line = remainder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(email) = vcard_property(line, "email") {
|
|
||||||
addr.get_or_insert(email);
|
|
||||||
} else if let Some(name) = vcard_property(line, "fn") {
|
|
||||||
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,"))
|
|
||||||
.or_else(|| remove_prefix(line, "KEY;PREF=1:data:application/pgp-keys;base64,"))
|
|
||||||
{
|
|
||||||
key.get_or_insert(k);
|
|
||||||
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
|
|
||||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
|
|
||||||
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
|
|
||||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
|
|
||||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
|
|
||||||
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
|
|
||||||
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
|
|
||||||
{
|
|
||||||
photo.get_or_insert(p);
|
|
||||||
} 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.
|
/// Valid contact address.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -265,7 +76,7 @@ impl ContactAddress {
|
|||||||
|
|
||||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||||
impl rusqlite::types::ToSql for ContactAddress {
|
impl rusqlite::types::ToSql for ContactAddress {
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
Ok(out)
|
Ok(out)
|
||||||
@@ -277,7 +88,8 @@ impl rusqlite::types::ToSql for ContactAddress {
|
|||||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
/// - Removes special characters from the name, see [`sanitize_name()`]
|
||||||
/// - Removes the name if it is equal to the address by setting it to ""
|
/// - Removes the name if it is equal to the address by setting it to ""
|
||||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||||
(
|
(
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
@@ -470,7 +282,7 @@ impl EmailAddress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl rusqlite::types::ToSql for EmailAddress {
|
impl rusqlite::types::ToSql for EmailAddress {
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||||
let val = rusqlite::types::Value::Text(self.to_string());
|
let val = rusqlite::types::Value::Text(self.to_string());
|
||||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
Ok(out)
|
Ok(out)
|
||||||
@@ -479,124 +291,8 @@ impl rusqlite::types::ToSql for EmailAddress {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::TimeZone;
|
|
||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_contact_address() -> Result<()> {
|
fn test_contact_address() -> Result<()> {
|
||||||
let alice_addr = "alice@example.org";
|
let alice_addr = "alice@example.org";
|
||||||
@@ -643,112 +339,6 @@ END:VCARD",
|
|||||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
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==");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_protonmail_vcard() {
|
|
||||||
let contacts = parse_vcard(
|
|
||||||
"BEGIN:VCARD
|
|
||||||
VERSION:4.0
|
|
||||||
FN;PREF=1:Alice Wonderland
|
|
||||||
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
|
|
||||||
ITEM1.EMAIL;PREF=1:alice@example.org
|
|
||||||
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
|
|
||||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
|
||||||
ITEM1.X-PM-ENCRYPT:true
|
|
||||||
ITEM1.X-PM-SIGN:true
|
|
||||||
END:VCARD",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(contacts.len(), 1);
|
|
||||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
|
||||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
|
||||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
||||||
assert!(contacts[0].timestamp.is_err());
|
|
||||||
assert_eq!(contacts[0].profile_image, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sanitize_name() {
|
fn test_sanitize_name() {
|
||||||
assert_eq!(&sanitize_name(" hello world "), "hello world");
|
assert_eq!(&sanitize_name(" hello world "), "hello world");
|
||||||
|
|||||||
247
deltachat-contact-tools/src/vcard.rs
Normal file
247
deltachat-contact-tools/src/vcard.rs
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::sanitize_name_and_addr;
|
||||||
|
|
||||||
|
#[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 biography, stored in the vcard property `note`
|
||||||
|
pub biography: 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape(s: &str) -> String {
|
||||||
|
s.replace(',', "\\,")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res = "".to_string();
|
||||||
|
for c in contacts {
|
||||||
|
// Mustn't contain ',', but it's easier to escape than to error out.
|
||||||
|
let addr = escape(&c.addr);
|
||||||
|
let display_name = escape(c.display_name());
|
||||||
|
res += &format!(
|
||||||
|
"BEGIN:VCARD\r\n\
|
||||||
|
VERSION:4.0\r\n\
|
||||||
|
EMAIL:{addr}\r\n\
|
||||||
|
FN:{display_name}\r\n"
|
||||||
|
);
|
||||||
|
if let Some(key) = &c.key {
|
||||||
|
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
|
||||||
|
}
|
||||||
|
if let Some(profile_image) = &c.profile_image {
|
||||||
|
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
|
||||||
|
}
|
||||||
|
if let Some(biography) = &c.biography {
|
||||||
|
res += &format!("NOTE:{}\r\n", escape(biography));
|
||||||
|
}
|
||||||
|
if let Some(timestamp) = format_timestamp(c) {
|
||||||
|
res += &format!("REV:{timestamp}\r\n");
|
||||||
|
}
|
||||||
|
res += "END:VCARD\r\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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Returns (parameters, raw value) tuple.
|
||||||
|
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||||
|
let remainder = remove_prefix(line, property)?;
|
||||||
|
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||||
|
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||||
|
|
||||||
|
// Note: This doesn't handle the case where there are quotes around a colon,
|
||||||
|
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
|
||||||
|
// This could be improved in the future, but for now, the parsing is good enough.
|
||||||
|
let (mut 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;
|
||||||
|
}
|
||||||
|
if let Some(p) = remove_prefix(params, ";") {
|
||||||
|
params = p;
|
||||||
|
}
|
||||||
|
if let Some(p) = remove_prefix(params, "PREF=1") {
|
||||||
|
params = p;
|
||||||
|
}
|
||||||
|
Some((params, value))
|
||||||
|
}
|
||||||
|
/// Returns (parameters, unescaped value) tuple.
|
||||||
|
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
||||||
|
let (params, value) = vcard_property_raw(line, property)?;
|
||||||
|
// Some fields can't contain commas, but unescape them everywhere for safety.
|
||||||
|
Some((params, value.replace("\\,", ",")))
|
||||||
|
}
|
||||||
|
fn base64_key(line: &str) -> Option<&str> {
|
||||||
|
let (params, value) = vcard_property_raw(line, "key")?;
|
||||||
|
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|
||||||
|
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
|
||||||
|
{
|
||||||
|
return Some(value);
|
||||||
|
}
|
||||||
|
remove_prefix(value, "data:application/pgp-keys;base64\\,")
|
||||||
|
// Old Delta Chat format.
|
||||||
|
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
|
||||||
|
}
|
||||||
|
fn base64_photo(line: &str) -> Option<&str> {
|
||||||
|
let (params, value) = vcard_property_raw(line, "photo")?;
|
||||||
|
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|
||||||
|
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|
||||||
|
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|
||||||
|
|| params.eq_ignore_ascii_case("ENCODING=b;TYPE=JPEG")
|
||||||
|
|| params.eq_ignore_ascii_case("ENCODING=BASE64;TYPE=JPEG")
|
||||||
|
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=BASE64")
|
||||||
|
{
|
||||||
|
return Some(value);
|
||||||
|
}
|
||||||
|
remove_prefix(value, "data:image/jpeg;base64\\,")
|
||||||
|
// Old Delta Chat format.
|
||||||
|
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
|
||||||
|
}
|
||||||
|
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: LazyLock<Regex> =
|
||||||
|
LazyLock::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 biography = None;
|
||||||
|
let mut datetime = None;
|
||||||
|
|
||||||
|
for mut line in lines.by_ref() {
|
||||||
|
if let Some(remainder) = remove_prefix(line, "item1.") {
|
||||||
|
// Remove the group name, if the group is called "item1".
|
||||||
|
// If necessary, we can improve this to also remove groups that are called something different that "item1".
|
||||||
|
//
|
||||||
|
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
|
||||||
|
line = remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((_params, email)) = vcard_property(line, "email") {
|
||||||
|
addr.get_or_insert(email);
|
||||||
|
} else if let Some((_params, name)) = vcard_property(line, "fn") {
|
||||||
|
display_name.get_or_insert(name);
|
||||||
|
} else if let Some(k) = base64_key(line) {
|
||||||
|
key.get_or_insert(k);
|
||||||
|
} else if let Some(p) = base64_photo(line) {
|
||||||
|
photo.get_or_insert(p);
|
||||||
|
} else if let Some((_params, bio)) = vcard_property(line, "note") {
|
||||||
|
biography.get_or_insert(bio);
|
||||||
|
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
|
||||||
|
datetime.get_or_insert(rev);
|
||||||
|
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||||
|
let (authname, addr) = sanitize_name_and_addr(
|
||||||
|
&display_name.unwrap_or_default(),
|
||||||
|
&addr.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
contacts.push(VcardContact {
|
||||||
|
authname,
|
||||||
|
addr,
|
||||||
|
key: key.map(|s| s.to_string()),
|
||||||
|
profile_image: photo.map(|s| s.to_string()),
|
||||||
|
biography,
|
||||||
|
timestamp: datetime
|
||||||
|
.as_deref()
|
||||||
|
.context("No timestamp in vcard")
|
||||||
|
.and_then(parse_datetime),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contacts
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod vcard_tests;
|
||||||
278
deltachat-contact-tools/src/vcard/vcard_tests.rs
Normal file
278
deltachat-contact-tools/src/vcard/vcard_tests.rs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
use chrono::TimeZone as _;
|
||||||
|
|
||||||
|
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_vcard_with_trailing_newline() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
"BEGIN:VCARD\r
|
||||||
|
VERSION:4.0\r
|
||||||
|
FN:Alice Wonderland\r
|
||||||
|
N:Wonderland;Alice;;;Ms.\r
|
||||||
|
GENDER:W\r
|
||||||
|
EMAIL;TYPE=work:alice@example.com\r
|
||||||
|
KEY;TYPE=PGP;ENCODING=b:[base64-data]\r
|
||||||
|
REV:20240418T184242Z\r
|
||||||
|
END:VCARD\r
|
||||||
|
\r",
|
||||||
|
);
|
||||||
|
|
||||||
|
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()),
|
||||||
|
biography: Some("Hi, I'm Alice".to_string()),
|
||||||
|
timestamp: Ok(1713465762),
|
||||||
|
},
|
||||||
|
VcardContact {
|
||||||
|
addr: "bob@example.com".to_string(),
|
||||||
|
authname: "".to_string(),
|
||||||
|
key: None,
|
||||||
|
profile_image: None,
|
||||||
|
biography: None,
|
||||||
|
timestamp: Ok(0),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let items = [
|
||||||
|
"BEGIN:VCARD\r\n\
|
||||||
|
VERSION:4.0\r\n\
|
||||||
|
EMAIL:alice@example.org\r\n\
|
||||||
|
FN:Alice Wonderland\r\n\
|
||||||
|
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
||||||
|
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
||||||
|
NOTE:Hi\\, I'm Alice\r\n\
|
||||||
|
REV:20240418T184242Z\r\n\
|
||||||
|
END:VCARD\r\n",
|
||||||
|
"BEGIN:VCARD\r\n\
|
||||||
|
VERSION:4.0\r\n\
|
||||||
|
EMAIL:bob@example.com\r\n\
|
||||||
|
FN:bob@example.com\r\n\
|
||||||
|
REV:19700101T000000Z\r\n\
|
||||||
|
END:VCARD\r\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_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==");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_protonmail_vcard() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
"BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN;PREF=1:Alice Wonderland
|
||||||
|
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
|
||||||
|
ITEM1.EMAIL;PREF=1:alice@example.org
|
||||||
|
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
|
||||||
|
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||||
|
ITEM1.X-PM-ENCRYPT:true
|
||||||
|
ITEM1.X-PM-SIGN:true
|
||||||
|
END:VCARD",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||||
|
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||||
|
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||||
|
assert!(contacts[0].timestamp.is_err());
|
||||||
|
assert_eq!(contacts[0].profile_image, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proton at some point slightly changed the format of their vcards.
|
||||||
|
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
|
||||||
|
#[test]
|
||||||
|
fn test_protonmail_vcard2() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
r"BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN;PREF=1:Alice
|
||||||
|
PHOTO;PREF=1:data:image/jpeg;base64,/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z
|
||||||
|
REV:Invalid Date
|
||||||
|
ITEM1.EMAIL;PREF=1:alice@example.org
|
||||||
|
KEY;PREF=1:data:application/pgp-keys;base64,xsaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==
|
||||||
|
UID:proton-web-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
|
||||||
|
END:VCARD",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||||
|
assert_eq!(&contacts[0].authname, "Alice");
|
||||||
|
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
|
||||||
|
assert!(contacts[0].timestamp.is_err());
|
||||||
|
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.152.2"
|
version = "2.11.0"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -15,7 +15,7 @@ crate-type = ["cdylib", "staticlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat = { workspace = true, default-features = false }
|
deltachat = { workspace = true, default-features = false }
|
||||||
deltachat-jsonrpc = { workspace = true, optional = true }
|
deltachat-jsonrpc = { workspace = true }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
human-panic = { version = "2", default-features = false }
|
human-panic = { version = "2", default-features = false }
|
||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
@@ -24,11 +24,9 @@ tokio = { workspace = true, features = ["rt-multi-thread"] }
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
|
||||||
yerpc = { workspace = true, features = ["anyhow_expose"] }
|
yerpc = { workspace = true, features = ["anyhow_expose"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
|
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
|
||||||
jsonrpc = ["dep:deltachat-jsonrpc"]
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
<doxygenlayout version="1.0">
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Generated by doxygen 1.8.20 -->
|
<doxygenlayout version="2.0">
|
||||||
|
<!-- Generated by doxygen 1.13.2 -->
|
||||||
<!-- Navigation index tabs for HTML output -->
|
<!-- Navigation index tabs for HTML output -->
|
||||||
<navindex>
|
<navindex>
|
||||||
<tab type="mainpage" visible="yes" title=""/>
|
<tab type="mainpage" visible="yes" title=""/>
|
||||||
@@ -11,10 +12,16 @@
|
|||||||
</tab>
|
</tab>
|
||||||
<tab type="topics" 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="modules" visible="yes" title="" intro="">
|
||||||
|
<tab type="modulelist" visible="yes" title="" intro=""/>
|
||||||
|
<tab type="modulemembers" visible="yes" title="" intro=""/>
|
||||||
|
</tab>
|
||||||
<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=""/>
|
||||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
||||||
</tab>
|
</tab>
|
||||||
|
<tab type="concepts" visible="yes" title="">
|
||||||
|
</tab>
|
||||||
<tab type="interfaces" visible="yes" title="">
|
<tab type="interfaces" visible="yes" title="">
|
||||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
||||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||||
@@ -35,4 +42,228 @@
|
|||||||
</tab>
|
</tab>
|
||||||
<tab type="examples" visible="yes" title="" intro=""/>
|
<tab type="examples" visible="yes" title="" intro=""/>
|
||||||
</navindex>
|
</navindex>
|
||||||
|
|
||||||
|
<!-- Layout definition for a class page -->
|
||||||
|
<class>
|
||||||
|
<briefdescription visible="yes"/>
|
||||||
|
<includes visible="$SHOW_HEADERFILE"/>
|
||||||
|
<inheritancegraph visible="yes"/>
|
||||||
|
<collaborationgraph visible="yes"/>
|
||||||
|
<memberdecl>
|
||||||
|
<nestedclasses visible="yes" title=""/>
|
||||||
|
<publictypes visible="yes" title=""/>
|
||||||
|
<services visible="yes" title=""/>
|
||||||
|
<interfaces visible="yes" title=""/>
|
||||||
|
<publicslots visible="yes" title=""/>
|
||||||
|
<signals visible="yes" title=""/>
|
||||||
|
<publicmethods visible="yes" title=""/>
|
||||||
|
<publicstaticmethods visible="yes" title=""/>
|
||||||
|
<publicattributes visible="yes" title=""/>
|
||||||
|
<publicstaticattributes visible="yes" title=""/>
|
||||||
|
<protectedtypes visible="yes" title=""/>
|
||||||
|
<protectedslots visible="yes" title=""/>
|
||||||
|
<protectedmethods visible="yes" title=""/>
|
||||||
|
<protectedstaticmethods visible="yes" title=""/>
|
||||||
|
<protectedattributes visible="yes" title=""/>
|
||||||
|
<protectedstaticattributes visible="yes" title=""/>
|
||||||
|
<packagetypes visible="yes" title=""/>
|
||||||
|
<packagemethods visible="yes" title=""/>
|
||||||
|
<packagestaticmethods visible="yes" title=""/>
|
||||||
|
<packageattributes visible="yes" title=""/>
|
||||||
|
<packagestaticattributes visible="yes" title=""/>
|
||||||
|
<properties visible="yes" title=""/>
|
||||||
|
<events visible="yes" title=""/>
|
||||||
|
<privatetypes visible="yes" title=""/>
|
||||||
|
<privateslots visible="yes" title=""/>
|
||||||
|
<privatemethods visible="yes" title=""/>
|
||||||
|
<privatestaticmethods visible="yes" title=""/>
|
||||||
|
<privateattributes visible="yes" title=""/>
|
||||||
|
<privatestaticattributes visible="yes" title=""/>
|
||||||
|
<friends visible="yes" title=""/>
|
||||||
|
<related visible="yes" title="" subtitle=""/>
|
||||||
|
<membergroups visible="yes"/>
|
||||||
|
</memberdecl>
|
||||||
|
<detaileddescription visible="yes" title=""/>
|
||||||
|
<memberdef>
|
||||||
|
<inlineclasses visible="yes" title=""/>
|
||||||
|
<typedefs visible="yes" title=""/>
|
||||||
|
<enums visible="yes" title=""/>
|
||||||
|
<services visible="yes" title=""/>
|
||||||
|
<interfaces visible="yes" title=""/>
|
||||||
|
<constructors visible="yes" title=""/>
|
||||||
|
<functions visible="yes" title=""/>
|
||||||
|
<related visible="yes" title=""/>
|
||||||
|
<variables visible="yes" title=""/>
|
||||||
|
<properties visible="yes" title=""/>
|
||||||
|
<events visible="yes" title=""/>
|
||||||
|
</memberdef>
|
||||||
|
<allmemberslink visible="yes"/>
|
||||||
|
<usedfiles visible="$SHOW_USED_FILES"/>
|
||||||
|
<authorsection visible="yes"/>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
<!-- Layout definition for a namespace page -->
|
||||||
|
<namespace>
|
||||||
|
<briefdescription visible="yes"/>
|
||||||
|
<memberdecl>
|
||||||
|
<nestednamespaces visible="yes" title=""/>
|
||||||
|
<constantgroups visible="yes" title=""/>
|
||||||
|
<interfaces visible="yes" title=""/>
|
||||||
|
<classes visible="yes" title=""/>
|
||||||
|
<concepts visible="yes" title=""/>
|
||||||
|
<structs visible="yes" title=""/>
|
||||||
|
<exceptions visible="yes" title=""/>
|
||||||
|
<typedefs visible="yes" title=""/>
|
||||||
|
<sequences visible="yes" title=""/>
|
||||||
|
<dictionaries visible="yes" title=""/>
|
||||||
|
<enums visible="yes" title=""/>
|
||||||
|
<functions visible="yes" title=""/>
|
||||||
|
<variables visible="yes" title=""/>
|
||||||
|
<properties visible="yes" title=""/>
|
||||||
|
<membergroups visible="yes" visible="yes"/>
|
||||||
|
</memberdecl>
|
||||||
|
<detaileddescription visible="yes" title=""/>
|
||||||
|
<memberdef>
|
||||||
|
<inlineclasses visible="yes" title=""/>
|
||||||
|
<typedefs visible="yes" title=""/>
|
||||||
|
<sequences visible="yes" title=""/>
|
||||||
|
<dictionaries visible="yes" title=""/>
|
||||||
|
<enums visible="yes" title=""/>
|
||||||
|
<functions visible="yes" title=""/>
|
||||||
|
<variables visible="yes" title=""/>
|
||||||
|
<properties visible="yes" title=""/>
|
||||||
|
</memberdef>
|
||||||
|
<authorsection visible="yes"/>
|
||||||
|
</namespace>
|
||||||
|
|
||||||
|
<!-- Layout definition for a concept page -->
|
||||||
|
<concept>
|
||||||
|
<briefdescription visible="yes"/>
|
||||||
|
<includes visible="$SHOW_HEADERFILE"/>
|
||||||
|
<definition visible="yes" title=""/>
|
||||||
|
<detaileddescription visible="yes" title=""/>
|
||||||
|
<authorsection visible="yes"/>
|
||||||
|
</concept>
|
||||||
|
|
||||||
|
<!-- Layout definition for a file page -->
|
||||||
|
<file>
|
||||||
|
<briefdescription visible="yes"/>
|
||||||
|
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||||
|
<includegraph visible="yes"/>
|
||||||
|
<includedbygraph visible="yes"/>
|
||||||
|
<sourcelink visible="yes"/>
|
||||||
|
<memberdecl>
|
||||||
|
<interfaces visible="yes" title=""/>
|
||||||
|
<classes visible="yes" title=""/>
|
||||||
|
<structs visible="yes" title=""/>
|
||||||
|
<exceptions visible="yes" title=""/>
|
||||||
|
<namespaces visible="yes" title=""/>
|
||||||
|
<concepts visible="yes" title=""/>
|
||||||
|
<constantgroups visible="yes" title=""/>
|
||||||
|
<defines visible="yes" title=""/>
|
||||||
|
<typedefs visible="yes" title=""/>
|
||||||
|
<sequences visible="yes" title=""/>
|
||||||
|
<dictionaries visible="yes" title=""/>
|
||||||
|
<enums visible="yes" title=""/>
|
||||||
|
<functions visible="yes" title=""/>
|
||||||
|
<variables visible="yes" title=""/>
|
||||||
|
<properties visible="yes" title=""/>
|
||||||
|
<membergroups visible="yes" visible="yes"/>
|
||||||
|
</memberdecl>
|
||||||
|
<detaileddescription visible="yes" title=""/>
|
||||||
|
<memberdef>
|
||||||
|
<inlineclasses visible="yes" title=""/>
|
||||||
|
<defines visible="yes" title=""/>
|
||||||
|
<typedefs visible="yes" title=""/>
|
||||||
|
<sequences visible="yes" title=""/>
|
||||||
|
<dictionaries visible="yes" title=""/>
|
||||||
|
<enums visible="yes" title=""/>
|
||||||
|
<functions visible="yes" title=""/>
|
||||||
|
<variables visible="yes" title=""/>
|
||||||
|
<properties visible="yes" title=""/>
|
||||||
|
</memberdef>
|
||||||
|
<authorsection/>
|
||||||
|
</file>
|
||||||
|
|
||||||
|
<!-- Layout definition for a group page -->
|
||||||
|
<group>
|
||||||
|
<briefdescription visible="yes"/>
|
||||||
|
<groupgraph visible="yes"/>
|
||||||
|
<memberdecl>
|
||||||
|
<nestedgroups visible="yes" title=""/>
|
||||||
|
<modules visible="yes" title=""/>
|
||||||
|
<dirs visible="yes" title=""/>
|
||||||
|
<files visible="yes" title=""/>
|
||||||
|
<namespaces visible="yes" title=""/>
|
||||||
|
<concepts visible="yes" title=""/>
|
||||||
|
<classes visible="yes" title=""/>
|
||||||
|
<defines visible="yes" title=""/>
|
||||||
|
<typedefs visible="yes" title=""/>
|
||||||
|
<sequences visible="yes" title=""/>
|
||||||
|
<dictionaries visible="yes" title=""/>
|
||||||
|
<enums visible="yes" title=""/>
|
||||||
|
<enumvalues visible="yes" title=""/>
|
||||||
|
<functions visible="yes" title=""/>
|
||||||
|
<variables visible="yes" title=""/>
|
||||||
|
<signals visible="yes" title=""/>
|
||||||
|
<publicslots visible="yes" title=""/>
|
||||||
|
<protectedslots visible="yes" title=""/>
|
||||||
|
<privateslots visible="yes" title=""/>
|
||||||
|
<events visible="yes" title=""/>
|
||||||
|
<properties visible="yes" title=""/>
|
||||||
|
<friends visible="yes" title=""/>
|
||||||
|
<membergroups visible="yes"/>
|
||||||
|
</memberdecl>
|
||||||
|
<detaileddescription visible="yes" title=""/>
|
||||||
|
<memberdef>
|
||||||
|
<pagedocs/>
|
||||||
|
<inlineclasses visible="yes" title=""/>
|
||||||
|
<defines visible="yes" title=""/>
|
||||||
|
<typedefs visible="yes" title=""/>
|
||||||
|
<sequences visible="yes" title=""/>
|
||||||
|
<dictionaries visible="yes" title=""/>
|
||||||
|
<enums visible="yes" title=""/>
|
||||||
|
<enumvalues visible="yes" title=""/>
|
||||||
|
<functions visible="yes" title=""/>
|
||||||
|
<variables visible="yes" title=""/>
|
||||||
|
<signals visible="yes" title=""/>
|
||||||
|
<publicslots visible="yes" title=""/>
|
||||||
|
<protectedslots visible="yes" title=""/>
|
||||||
|
<privateslots visible="yes" title=""/>
|
||||||
|
<events visible="yes" title=""/>
|
||||||
|
<properties visible="yes" title=""/>
|
||||||
|
<friends visible="yes" title=""/>
|
||||||
|
</memberdef>
|
||||||
|
<authorsection visible="yes"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- Layout definition for a C++20 module page -->
|
||||||
|
<module>
|
||||||
|
<briefdescription visible="yes"/>
|
||||||
|
<exportedmodules visible="yes"/>
|
||||||
|
<memberdecl>
|
||||||
|
<concepts visible="yes" title=""/>
|
||||||
|
<classes visible="yes" title=""/>
|
||||||
|
<enums visible="yes" title=""/>
|
||||||
|
<typedefs visible="yes" title=""/>
|
||||||
|
<functions visible="yes" title=""/>
|
||||||
|
<variables visible="yes" title=""/>
|
||||||
|
<membergroups visible="yes" title=""/>
|
||||||
|
</memberdecl>
|
||||||
|
<detaileddescription visible="yes" title=""/>
|
||||||
|
<memberdecl>
|
||||||
|
<files visible="yes"/>
|
||||||
|
</memberdecl>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- Layout definition for a directory page -->
|
||||||
|
<directory>
|
||||||
|
<briefdescription visible="yes"/>
|
||||||
|
<directorygraph visible="yes"/>
|
||||||
|
<memberdecl>
|
||||||
|
<dirs visible="yes"/>
|
||||||
|
<files visible="yes"/>
|
||||||
|
</memberdecl>
|
||||||
|
<detaileddescription visible="yes" title=""/>
|
||||||
|
</directory>
|
||||||
</doxygenlayout>
|
</doxygenlayout>
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
|||||||
* - Strings in function arguments or return values are usually UTF-8 encoded.
|
* - Strings in function arguments or return values are usually UTF-8 encoded.
|
||||||
*
|
*
|
||||||
* - The issue-tracker for the core library is here:
|
* - The issue-tracker for the core library is here:
|
||||||
* <https://github.com/deltachat/deltachat-core-rust/issues>
|
* <https://github.com/chatmail/core/issues>
|
||||||
*
|
*
|
||||||
* If you need further assistance,
|
* If you need further assistance,
|
||||||
* please do not hesitate to contact us
|
* please do not hesitate to contact us
|
||||||
@@ -440,17 +440,6 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* also show all mails of confirmed contacts,
|
* also show all mails of confirmed contacts,
|
||||||
* DC_SHOW_EMAILS_ALL (2)=
|
* DC_SHOW_EMAILS_ALL (2)=
|
||||||
* also show mails of unconfirmed contacts (default).
|
* also show mails of unconfirmed contacts (default).
|
||||||
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
|
|
||||||
* generate recommended key type (default),
|
|
||||||
* DC_KEY_GEN_RSA2048 (1)=
|
|
||||||
* generate RSA 2048 keypair
|
|
||||||
* DC_KEY_GEN_ED25519 (2)=
|
|
||||||
* generate Curve25519 keypair
|
|
||||||
* DC_KEY_GEN_RSA4096 (3)=
|
|
||||||
* generate RSA 4096 keypair
|
|
||||||
* - `save_mime_headers` = 1=save mime headers
|
|
||||||
* and make dc_get_mime_headers() work for subsequent calls,
|
|
||||||
* 0=do not save mime headers (default)
|
|
||||||
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
||||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||||
@@ -514,13 +503,6 @@ 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.
|
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||||
* - `is_muted` = Whether a context is muted by the user.
|
* - `is_muted` = Whether a context is muted by the user.
|
||||||
* Muted contexts should not sound, vibrate or show notifications.
|
* Muted contexts should not sound, vibrate or show notifications.
|
||||||
@@ -975,7 +957,7 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co
|
|||||||
* ~~~
|
* ~~~
|
||||||
* dc_msg_t* msg = dc_msg_new(context, DC_MSG_IMAGE);
|
* dc_msg_t* msg = dc_msg_new(context, DC_MSG_IMAGE);
|
||||||
*
|
*
|
||||||
* dc_msg_set_file(msg, "/file/to/send.jpg", NULL);
|
* dc_msg_set_file_and_deduplicate(msg, "/file/to/send.jpg", NULL, NULL);
|
||||||
* dc_send_msg(context, chat_id, msg);
|
* dc_send_msg(context, chat_id, msg);
|
||||||
*
|
*
|
||||||
* dc_msg_unref(msg);
|
* dc_msg_unref(msg);
|
||||||
@@ -1039,6 +1021,38 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
|
|||||||
uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send);
|
uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send chat members a request to edit the given message's text.
|
||||||
|
*
|
||||||
|
* Only outgoing messages sent by self can be edited.
|
||||||
|
* Edited messages should be flagged as such in the UI, see dc_msg_is_edited().
|
||||||
|
* UI is informed about changes using the event #DC_EVENT_MSGS_CHANGED.
|
||||||
|
* If the text is not changed, no event and no edit request message are sent.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object as returned from dc_context_new().
|
||||||
|
* @param msg_id The message ID of the message to edit.
|
||||||
|
* @param new_text The new text.
|
||||||
|
* This must not be NULL nor empty.
|
||||||
|
*/
|
||||||
|
void dc_send_edit_request (dc_context_t* context, uint32_t msg_id, const char* new_text);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send chat members a request to delete the given messages.
|
||||||
|
*
|
||||||
|
* Only outgoing messages can be deleted this way
|
||||||
|
* and all messages must be in the same chat.
|
||||||
|
* No tombstone or sth. like that is left.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object as returned from dc_context_new().
|
||||||
|
* @param msg_ids An array of uint32_t containing all message IDs to delete.
|
||||||
|
* @param msg_cnt The number of messages IDs in the msg_ids array.
|
||||||
|
*/
|
||||||
|
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send invitation to a videochat.
|
* Send invitation to a videochat.
|
||||||
*
|
*
|
||||||
@@ -1318,12 +1332,14 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
|||||||
* Optionally, some special markers added to the ID array may help to
|
* Optionally, some special markers added to the ID array may help to
|
||||||
* implement virtual lists.
|
* implement virtual lists.
|
||||||
*
|
*
|
||||||
|
* To get the concrete time of the message, use dc_array_get_timestamp().
|
||||||
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object as returned from dc_context_new().
|
* @param context The context object as returned from dc_context_new().
|
||||||
* @param chat_id The chat ID of which the messages IDs should be queried.
|
* @param chat_id The chat ID of which the messages IDs should be queried.
|
||||||
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
|
||||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
|
||||||
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
|
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
|
||||||
* @param marker1before Deprecated, set this to 0.
|
* @param marker1before Deprecated, set this to 0.
|
||||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||||
@@ -1929,24 +1945,7 @@ void dc_download_full_msg (dc_context_t* context, int msg_id);
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the raw mime-headers of the given message.
|
* Delete messages. The messages are deleted on all devices and
|
||||||
* Raw headers are saved for incoming messages
|
|
||||||
* only if `dc_set_config(context, "save_mime_headers", "1")`
|
|
||||||
* was called before.
|
|
||||||
*
|
|
||||||
* @memberof dc_context_t
|
|
||||||
* @param context The context object.
|
|
||||||
* @param msg_id The message ID, must be the ID of an incoming message.
|
|
||||||
* @return Raw headers as a multi-line string, must be released using dc_str_unref() after usage.
|
|
||||||
* Returns NULL if there are no headers saved for the given message,
|
|
||||||
* e.g. because of save_mime_headers is not set
|
|
||||||
* or the message is not incoming.
|
|
||||||
*/
|
|
||||||
char* dc_get_mime_headers (dc_context_t* context, uint32_t msg_id);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete messages. The messages are deleted on the current device and
|
|
||||||
* on the IMAP server.
|
* on the IMAP server.
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
@@ -1974,6 +1973,36 @@ void dc_delete_msgs (dc_context_t* context, const uint3
|
|||||||
void dc_forward_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id);
|
void dc_forward_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a copy of messages in "Saved Messages".
|
||||||
|
*
|
||||||
|
* In contrast to forwarding messages,
|
||||||
|
* information as author, date and origin are preserved.
|
||||||
|
* The action completes locally, so "Saved Messages" do not show sending errors in case one is offline.
|
||||||
|
* Still, a sync message is emitted, so that other devices will save the same message,
|
||||||
|
* as long as not deleted before.
|
||||||
|
*
|
||||||
|
* To check if a message was saved, use dc_msg_get_saved_msg_id(),
|
||||||
|
* UI may show an indicator and offer an "Unsave" instead of a "Save" button then.
|
||||||
|
*
|
||||||
|
* The other way round, from inside the "Saved Messages" chat,
|
||||||
|
* UI may show the indicator and "Unsave" button checking dc_msg_get_original_msg_id()
|
||||||
|
* and offer a button to go the original message.
|
||||||
|
*
|
||||||
|
* "Unsave" is done by deleting the saved message.
|
||||||
|
* Webxdc updates are not copied on purpose.
|
||||||
|
*
|
||||||
|
* For performance reasons, esp. when saving lots of messages,
|
||||||
|
* UI should call this function from a background thread.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @param msg_ids An array of uint32_t containing all message IDs that should be saved.
|
||||||
|
* @param msg_cnt The number of messages IDs in the msg_ids array.
|
||||||
|
*/
|
||||||
|
void dc_save_msgs (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resend messages and make information available for newly added chat members.
|
* Resend messages and make information available for newly added chat members.
|
||||||
* Resending sends out the original message, however, recipients and webxdc-status may differ.
|
* Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||||
@@ -2060,9 +2089,19 @@ int dc_may_be_valid_addr (const char* addr);
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an e-mail address belongs to a known and unblocked contact.
|
* Looks up a known and unblocked contact with a given e-mail address.
|
||||||
* To get a list of all known and unblocked contacts, use dc_get_contacts().
|
* To get a list of all known and unblocked contacts, use dc_get_contacts().
|
||||||
*
|
*
|
||||||
|
* **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||||
|
* (e.g. an address-contact and a key-contact),
|
||||||
|
* this looks up the most recently seen contact,
|
||||||
|
* i.e. which contact is returned depends on which contact last sent a message.
|
||||||
|
* If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||||
|
* But **DO NOT** internally represent contacts by their email address
|
||||||
|
* and do not use this function to look them up;
|
||||||
|
* otherwise this function will sometimes look up the wrong contact.
|
||||||
|
* Instead, you should internally represent contacts by their ids.
|
||||||
|
*
|
||||||
* To validate an e-mail address independently of the contact database
|
* To validate an e-mail address independently of the contact database
|
||||||
* use dc_may_be_valid_addr().
|
* use dc_may_be_valid_addr().
|
||||||
*
|
*
|
||||||
@@ -2084,6 +2123,13 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
|
|||||||
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
|
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
|
||||||
* a bunch of addresses.
|
* a bunch of addresses.
|
||||||
*
|
*
|
||||||
|
* This will always create or look up an address-contact,
|
||||||
|
* i.e. a contact identified by an email address,
|
||||||
|
* with all messages sent to and from this contact being unencrypted.
|
||||||
|
* If the user just clicked on an email address,
|
||||||
|
* you should first check `lookup_contact_id_by_addr`,
|
||||||
|
* and only if there is no contact yet, call this function here.
|
||||||
|
*
|
||||||
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
|
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
@@ -2098,8 +2144,12 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
|
|||||||
uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr);
|
uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr);
|
||||||
|
|
||||||
|
|
||||||
#define DC_GCL_VERIFIED_ONLY 0x01
|
|
||||||
|
// Deprecated 2025-05-20, setting this flag is a no-op.
|
||||||
|
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
|
||||||
|
|
||||||
#define DC_GCL_ADD_SELF 0x02
|
#define DC_GCL_ADD_SELF 0x02
|
||||||
|
#define DC_GCL_ADDRESS 0x04
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2128,17 +2178,40 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
|
|||||||
int dc_add_address_book (dc_context_t* context, const char* addr_book);
|
int dc_add_address_book (dc_context_t* context, const char* addr_book);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a vCard.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @param contact_id The ID of the contact to make the vCard of.
|
||||||
|
* @return vCard, must be released using dc_str_unref() after usage.
|
||||||
|
*/
|
||||||
|
char* dc_make_vcard (dc_context_t* context, uint32_t contact_id);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a vCard.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @param vcard vCard contents.
|
||||||
|
* @return Returns the IDs of the contacts in the order they appear in the vCard.
|
||||||
|
* Must be dc_array_unref()'d after usage.
|
||||||
|
*/
|
||||||
|
dc_array_t* dc_import_vcard (dc_context_t* context, const char* vcard);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns known and unblocked contacts.
|
* Returns known and unblocked contacts.
|
||||||
*
|
*
|
||||||
* To get information about a single contact, see dc_get_contact().
|
* To get information about a single contact, see dc_get_contact().
|
||||||
|
* By default, key-contacts are listed.
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object.
|
* @param context The context object.
|
||||||
* @param flags A combination of flags:
|
* @param flags A combination of flags:
|
||||||
* - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
|
* - DC_GCL_ADD_SELF: SELF is added to the list unless filtered by other parameters
|
||||||
* - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
|
* - DC_GCL_ADDRESS: List address-contacts instead of key-contacts.
|
||||||
* if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
|
|
||||||
* @param query A string to filter the list. Typically used to implement an
|
* @param query A string to filter the list. Typically used to implement an
|
||||||
* incremental search. NULL for no filtering.
|
* incremental search. NULL for no filtering.
|
||||||
* @return An array containing all contact IDs. Must be dc_array_unref()'d
|
* @return An array containing all contact IDs. Must be dc_array_unref()'d
|
||||||
@@ -2429,8 +2502,9 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
|||||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||||
#define DC_QR_BACKUP 251
|
#define DC_QR_BACKUP 251 // deprecated
|
||||||
#define DC_QR_BACKUP2 252
|
#define DC_QR_BACKUP2 252
|
||||||
|
#define DC_QR_BACKUP_TOO_NEW 255
|
||||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||||
#define DC_QR_ADDR 320 // id=contact
|
#define DC_QR_ADDR 320 // id=contact
|
||||||
@@ -2477,11 +2551,14 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
|||||||
* ask the user if they want to create an account on the given domain,
|
* ask the user if they want to create an account on the given domain,
|
||||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||||
*
|
*
|
||||||
* - DC_QR_BACKUP:
|
|
||||||
* - DC_QR_BACKUP2:
|
* - DC_QR_BACKUP2:
|
||||||
* ask the user if they want to set up a new device.
|
* ask the user if they want to set up a new device.
|
||||||
* If so, pass the qr-code to dc_receive_backup().
|
* If so, pass the qr-code to dc_receive_backup().
|
||||||
*
|
*
|
||||||
|
* - DC_QR_BACKUP_TOO_NEW:
|
||||||
|
* show a hint to the user that this backup comes from a newer Delta Chat version
|
||||||
|
* and this device needs an update
|
||||||
|
*
|
||||||
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
||||||
* ask the user if they want to use the given service for video chats;
|
* ask the user if they want to use the given service for video chats;
|
||||||
* if so, call dc_set_config_from_qr().
|
* if so, call dc_set_config_from_qr().
|
||||||
@@ -3753,21 +3830,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
|||||||
/**
|
/**
|
||||||
* Check if a chat is protected.
|
* Check if a chat is protected.
|
||||||
*
|
*
|
||||||
* End-to-end encryption is guaranteed in protected chats
|
* Only verified contacts
|
||||||
* and only verified contacts
|
|
||||||
* as determined by dc_contact_is_verified()
|
* as determined by dc_contact_is_verified()
|
||||||
* can be added to protected chats.
|
* can be added to protected chats.
|
||||||
*
|
*
|
||||||
* Protected chats are created using dc_create_group_chat()
|
* Protected chats are created using dc_create_group_chat()
|
||||||
* by setting the 'protect' parameter to 1.
|
* 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.
|
||||||
@@ -3776,6 +3844,21 @@ 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);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the chat is encrypted.
|
||||||
|
*
|
||||||
|
* 1:1 chats with key-contacts and group chats with key-contacts
|
||||||
|
* are encrypted.
|
||||||
|
* 1:1 chats with emails contacts and ad-hoc groups
|
||||||
|
* created for email threads are not encrypted.
|
||||||
|
*
|
||||||
|
* @memberof dc_chat_t
|
||||||
|
* @param chat The chat object.
|
||||||
|
* @return 1=chat is encrypted, 0=chat is not encrypted.
|
||||||
|
*/
|
||||||
|
int dc_chat_is_encrypted (const dc_chat_t *chat);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the chat was protected, and then an incoming message broke this protection.
|
* Checks if the chat was protected, and then an incoming message broke this protection.
|
||||||
*
|
*
|
||||||
@@ -3789,6 +3872,8 @@ int dc_chat_is_protected (const dc_chat_t* chat);
|
|||||||
*
|
*
|
||||||
* The UI should let the user confirm that this is OK with a message like
|
* 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().
|
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
|
||||||
|
*
|
||||||
|
* @deprecated 2025-07 chats protection cannot break any longer
|
||||||
* @memberof dc_chat_t
|
* @memberof dc_chat_t
|
||||||
* @param chat The chat object.
|
* @param chat The chat object.
|
||||||
* @return 1=chat protection broken, 0=otherwise.
|
* @return 1=chat protection broken, 0=otherwise.
|
||||||
@@ -4223,11 +4308,16 @@ int dc_msg_get_duration (const dc_msg_t* msg);
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a padlock should be shown beside the message.
|
* Check if message was correctly encrypted and signed.
|
||||||
|
*
|
||||||
|
* Historically, UIs showed a small padlock on the message then.
|
||||||
|
* Today, the UIs should instead
|
||||||
|
* show a small email-icon on the message if the message is not encrypted or signed,
|
||||||
|
* and nothing otherwise.
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
* @param msg The message object.
|
* @param msg The message object.
|
||||||
* @return 1=padlock should be shown beside message, 0=do not show a padlock beside the message.
|
* @return 1=message correctly encrypted and signed, no need to show anything; 0=show email-icon beside the message.
|
||||||
*/
|
*/
|
||||||
int dc_msg_get_showpadlock (const dc_msg_t* msg);
|
int dc_msg_get_showpadlock (const dc_msg_t* msg);
|
||||||
|
|
||||||
@@ -4402,6 +4492,20 @@ int dc_msg_is_sent (const dc_msg_t* msg);
|
|||||||
int dc_msg_is_forwarded (const dc_msg_t* msg);
|
int dc_msg_is_forwarded (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the message was edited.
|
||||||
|
*
|
||||||
|
* Edited messages should be marked by the UI as such,
|
||||||
|
* e.g. by the text "Edited" beside the time.
|
||||||
|
* To edit messages, use dc_send_edit_request().
|
||||||
|
*
|
||||||
|
* @memberof dc_msg_t
|
||||||
|
* @param msg The message object.
|
||||||
|
* @return 1=message is edited, 0=message not edited.
|
||||||
|
*/
|
||||||
|
int dc_msg_is_edited (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
|
||||||
@@ -4431,12 +4535,21 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
|||||||
* UIs can display e.g. an icon based upon the type.
|
* UIs can display e.g. an icon based upon the type.
|
||||||
*
|
*
|
||||||
* 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_GROUP_NAME_CHANGED (2) - "Group name changd from OLD to BY by CONTACT"
|
||||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
* - DC_INFO_GROUP_IMAGE_CHANGED (3) - "Group image changd by CONTACT"
|
||||||
|
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
|
||||||
|
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
|
||||||
|
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
|
||||||
|
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected"
|
||||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
* - 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
|
* 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
|
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||||
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
||||||
|
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
|
||||||
|
*
|
||||||
|
* For the messages that refer to a CONTACT,
|
||||||
|
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||||
|
* The UI should open the contact's profile when tapping the info message.
|
||||||
*
|
*
|
||||||
* 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()
|
||||||
@@ -4450,6 +4563,29 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
|||||||
int dc_msg_get_info_type (const dc_msg_t* msg);
|
int dc_msg_get_info_type (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the contact ID of the profile to open when tapping the info message.
|
||||||
|
*
|
||||||
|
* - For DC_INFO_MEMBER_ADDED_TO_GROUP and DC_INFO_MEMBER_REMOVED_FROM_GROUP,
|
||||||
|
* this is the contact being added/removed.
|
||||||
|
* The contact that did the adding/removal is usually only a tap away
|
||||||
|
* (as introducer and/or atop of the memberlist),
|
||||||
|
* and usually more known anyways.
|
||||||
|
* - For DC_INFO_GROUP_NAME_CHANGED, DC_INFO_GROUP_IMAGE_CHANGED and DC_INFO_EPHEMERAL_TIMER_CHANGED
|
||||||
|
* this is the contact who did the change.
|
||||||
|
*
|
||||||
|
* No need to check additionally for dc_msg_get_info_type(),
|
||||||
|
* unless you e.g. want to show the info message in another style.
|
||||||
|
*
|
||||||
|
* @memberof dc_msg_t
|
||||||
|
* @param msg The message object.
|
||||||
|
* @return If the info message refers to a contact,
|
||||||
|
* this contact ID or DC_CONTACT_ID_SELF is returned.
|
||||||
|
* Otherwise 0.
|
||||||
|
*/
|
||||||
|
uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
// DC_INFO* uses the same values as SystemMessage in rust-land
|
// DC_INFO* uses the same values as SystemMessage in rust-land
|
||||||
#define DC_INFO_UNKNOWN 0
|
#define DC_INFO_UNKNOWN 0
|
||||||
#define DC_INFO_GROUP_NAME_CHANGED 2
|
#define DC_INFO_GROUP_NAME_CHANGED 2
|
||||||
@@ -4462,9 +4598,10 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
|
|||||||
#define DC_INFO_LOCATION_ONLY 9
|
#define DC_INFO_LOCATION_ONLY 9
|
||||||
#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 // deprecated 2025-07
|
||||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||||
|
#define DC_INFO_CHAT_E2EE 50
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4712,23 +4849,33 @@ void dc_msg_set_override_sender_name(dc_msg_t* msg, const char* name)
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the file associated with a message object.
|
* Sets the file associated with a message.
|
||||||
* This does not alter any information in the database
|
*
|
||||||
* nor copy or move the file or checks if the file exist.
|
* If `name` is non-null, it is used as the file name
|
||||||
* All this can be done with dc_send_msg() later.
|
* and the actual current name of the file is ignored.
|
||||||
|
*
|
||||||
|
* If the source file is already in the blobdir, it will be renamed,
|
||||||
|
* otherwise it will be copied to the blobdir first.
|
||||||
|
*
|
||||||
|
* In order to deduplicate files that contain the same data,
|
||||||
|
* the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
|
||||||
|
*
|
||||||
|
* NOTE:
|
||||||
|
* - This function will rename the file. To get the new file path, call `get_file()`.
|
||||||
|
* - The file must not be modified after this function was called.
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
* @param msg The message object.
|
* @param msg The message object. Must not be NULL.
|
||||||
* @param file If the message object is used in dc_send_msg() later,
|
* @param file The path of the file to attach. Must not be NULL.
|
||||||
* this must be the full path of the image file to send.
|
* @param name The original filename of the attachment. If NULL, the current name of `file` will be used instead.
|
||||||
* @param filemime The MIME type of the file. NULL if you don't know or don't care.
|
* @param filemime The MIME type of the file. NULL if you don't know or don't care.
|
||||||
*/
|
*/
|
||||||
void dc_msg_set_file (dc_msg_t* msg, const char* file, const char* filemime);
|
void dc_msg_set_file_and_deduplicate(dc_msg_t* msg, const char* file, const char* name, const char* filemime);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the dimensions associated with message object.
|
* Set the dimensions associated with message object.
|
||||||
* Typically this is the width and the height of an image or video associated using dc_msg_set_file().
|
* Typically this is the width and the height of an image or video associated using dc_msg_set_file_and_deduplicate().
|
||||||
* This does not alter any information in the database; this may be done by dc_send_msg() later.
|
* This does not alter any information in the database; this may be done by dc_send_msg() later.
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
@@ -4741,7 +4888,7 @@ void dc_msg_set_dimension (dc_msg_t* msg, int width, int hei
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the duration associated with message object.
|
* Set the duration associated with message object.
|
||||||
* Typically this is the duration of an audio or video associated using dc_msg_set_file().
|
* Typically this is the duration of an audio or video associated using dc_msg_set_file_and_deduplicate().
|
||||||
* This does not alter any information in the database; this may be done by dc_send_msg() later.
|
* This does not alter any information in the database; this may be done by dc_send_msg() later.
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
@@ -4868,6 +5015,35 @@ dc_msg_t* dc_msg_get_quoted_msg (const dc_msg_t* msg);
|
|||||||
dc_msg_t* dc_msg_get_parent (const dc_msg_t* msg);
|
dc_msg_t* dc_msg_get_parent (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original message ID for a saved message from the "Saved Messages" chat.
|
||||||
|
*
|
||||||
|
* Can be used by UI to show a button to go the original message
|
||||||
|
* and an option to "Unsave" the message.
|
||||||
|
*
|
||||||
|
* @memberof dc_msg_t
|
||||||
|
* @param msg The message object. Usually, this refers to a a message inside "Saved Messages".
|
||||||
|
* @return The message ID of the original message.
|
||||||
|
* 0 if the given message object is not a "Saved Message"
|
||||||
|
* or if the original message does no longer exist.
|
||||||
|
*/
|
||||||
|
uint32_t dc_msg_get_original_msg_id (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a message was saved and return its ID inside "Saved Messages".
|
||||||
|
*
|
||||||
|
* Deleting the returned message will un-save the message.
|
||||||
|
* The state "is saved" can be used to show some icon to indicate that a message was saved.
|
||||||
|
*
|
||||||
|
* @memberof dc_msg_t
|
||||||
|
* @param msg The message object. Usually, this refers to a a message outside "Saved Messages".
|
||||||
|
* @return The message ID inside "Saved Messages", if any.
|
||||||
|
* 0 if the given message object is not saved.
|
||||||
|
*/
|
||||||
|
uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force the message to be sent in plain text.
|
* Force the message to be sent in plain text.
|
||||||
*
|
*
|
||||||
@@ -5096,20 +5272,14 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the contact
|
* Check if the contact
|
||||||
* can be added to verified chats,
|
* can be added to protected chats.
|
||||||
* i.e. has a verified key
|
|
||||||
* and Autocrypt key matches the verified key.
|
|
||||||
*
|
*
|
||||||
* If contact is verified
|
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||||
* 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.
|
||||||
* @return 0: contact is not verified.
|
* @return 0: contact is not verified.
|
||||||
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
|
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||||
*/
|
*/
|
||||||
int dc_contact_is_verified (dc_contact_t* contact);
|
int dc_contact_is_verified (dc_contact_t* contact);
|
||||||
|
|
||||||
@@ -5123,19 +5293,39 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
|||||||
int dc_contact_is_bot (dc_contact_t* contact);
|
int dc_contact_is_bot (dc_contact_t* contact);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether contact is a key-contact,
|
||||||
|
* i.e. it is identified by the public key
|
||||||
|
* rather than the email address.
|
||||||
|
*
|
||||||
|
* If so, all messages to and from this contact are encrypted.
|
||||||
|
*
|
||||||
|
* @memberof dc_contact_t
|
||||||
|
* @param contact The contact object.
|
||||||
|
* @return 1 if the contact is a key-contact, 0 if it is an address-contact.
|
||||||
|
*/
|
||||||
|
int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the contact ID that verified a contact.
|
* Return the contact ID that verified a contact.
|
||||||
*
|
*
|
||||||
* If the function returns non-zero result,
|
* As verifier may be unknown,
|
||||||
* display green checkmark in the profile and "Introduced by ..." line
|
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||||
* with the name and address of the contact
|
|
||||||
* formatted by dc_contact_get_name_n_addr.
|
|
||||||
*
|
*
|
||||||
* If this function returns a verifier,
|
* UI should display the information in the contact's profile as follows:
|
||||||
* this does not necessarily mean
|
*
|
||||||
* you can add the contact to verified chats.
|
* - If dc_contact_get_verifier_id() != 0,
|
||||||
* Use dc_contact_is_verified() to check
|
* display text "Introduced by ..."
|
||||||
* if a contact can be added to a verified chat instead.
|
* with the name and address of the contact
|
||||||
|
* formatted by dc_contact_get_name_n_addr().
|
||||||
|
* Prefix the text by a green checkmark.
|
||||||
|
*
|
||||||
|
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
|
||||||
|
* display "Introduced" prefixed by a green checkmark.
|
||||||
|
*
|
||||||
|
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
|
||||||
|
* display nothing
|
||||||
*
|
*
|
||||||
* @memberof dc_contact_t
|
* @memberof dc_contact_t
|
||||||
* @param contact The contact object.
|
* @param contact The contact object.
|
||||||
@@ -5351,7 +5541,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
* If you want to define the type of a dc_msg_t object for sending,
|
* If you want to define the type of a dc_msg_t object for sending,
|
||||||
* use dc_msg_new().
|
* use dc_msg_new().
|
||||||
* Depending on the type, you will set more properties using e.g.
|
* Depending on the type, you will set more properties using e.g.
|
||||||
* dc_msg_set_text() or dc_msg_set_file().
|
* dc_msg_set_text() or dc_msg_set_file_and_deduplicate().
|
||||||
* To finally send the message, use dc_send_msg().
|
* To finally send the message, use dc_send_msg().
|
||||||
*
|
*
|
||||||
* To get the types of dc_msg_t objects received, use dc_msg_get_viewtype().
|
* To get the types of dc_msg_t objects received, use dc_msg_get_viewtype().
|
||||||
@@ -5372,7 +5562,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
/**
|
/**
|
||||||
* Image message.
|
* Image message.
|
||||||
* If the image is an animated GIF, the type #DC_MSG_GIF should be used.
|
* If the image is an animated GIF, the type #DC_MSG_GIF should be used.
|
||||||
* File, width, and height are set via dc_msg_set_file(), dc_msg_set_dimension()
|
* File, width, and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension()
|
||||||
* and retrieved via dc_msg_get_file(), dc_msg_get_width(), and dc_msg_get_height().
|
* and retrieved via dc_msg_get_file(), dc_msg_get_width(), and dc_msg_get_height().
|
||||||
*
|
*
|
||||||
* Before sending, the image is recoded to an reasonable size,
|
* Before sending, the image is recoded to an reasonable size,
|
||||||
@@ -5385,7 +5575,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Animated GIF message.
|
* Animated GIF message.
|
||||||
* File, width, and height are set via dc_msg_set_file(), dc_msg_set_dimension()
|
* File, width, and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension()
|
||||||
* and retrieved via dc_msg_get_file(), dc_msg_get_width(), and dc_msg_get_height().
|
* and retrieved via dc_msg_get_file(), dc_msg_get_width(), and dc_msg_get_height().
|
||||||
*/
|
*/
|
||||||
#define DC_MSG_GIF 21
|
#define DC_MSG_GIF 21
|
||||||
@@ -5403,7 +5593,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Message containing an audio file.
|
* Message containing an audio file.
|
||||||
* File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
|
* File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration()
|
||||||
* and retrieved via dc_msg_get_file(), and dc_msg_get_duration().
|
* and retrieved via dc_msg_get_file(), and dc_msg_get_duration().
|
||||||
*/
|
*/
|
||||||
#define DC_MSG_AUDIO 40
|
#define DC_MSG_AUDIO 40
|
||||||
@@ -5412,7 +5602,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
/**
|
/**
|
||||||
* A voice message that was directly recorded by the user.
|
* A voice message that was directly recorded by the user.
|
||||||
* For all other audio messages, the type #DC_MSG_AUDIO should be used.
|
* For all other audio messages, the type #DC_MSG_AUDIO should be used.
|
||||||
* File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
|
* File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration()
|
||||||
* and retrieved via dc_msg_get_file(), and dc_msg_get_duration().
|
* and retrieved via dc_msg_get_file(), and dc_msg_get_duration().
|
||||||
*/
|
*/
|
||||||
#define DC_MSG_VOICE 41
|
#define DC_MSG_VOICE 41
|
||||||
@@ -5421,7 +5611,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
/**
|
/**
|
||||||
* Video messages.
|
* Video messages.
|
||||||
* File, width, height, and duration
|
* File, width, height, and duration
|
||||||
* are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
|
* are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension(), dc_msg_set_duration()
|
||||||
* and retrieved via
|
* and retrieved via
|
||||||
* dc_msg_get_file(), dc_msg_get_width(),
|
* dc_msg_get_file(), dc_msg_get_width(),
|
||||||
* dc_msg_get_height(), and dc_msg_get_duration().
|
* dc_msg_get_height(), and dc_msg_get_duration().
|
||||||
@@ -5431,7 +5621,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Message containing any file, e.g. a PDF.
|
* Message containing any file, e.g. a PDF.
|
||||||
* The file is set via dc_msg_set_file()
|
* The file is set via dc_msg_set_file_and_deduplicate()
|
||||||
* and retrieved via dc_msg_get_file().
|
* and retrieved via dc_msg_get_file().
|
||||||
*/
|
*/
|
||||||
#define DC_MSG_FILE 60
|
#define DC_MSG_FILE 60
|
||||||
@@ -5568,9 +5758,33 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
|||||||
#define DC_CHAT_TYPE_MAILINGLIST 140
|
#define DC_CHAT_TYPE_MAILINGLIST 140
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A broadcast list. See dc_chat_get_type() for details.
|
* Outgoing broadcast channel, called "Channel" in the UI.
|
||||||
|
*
|
||||||
|
* The user can send into this chat,
|
||||||
|
* and all recipients will receive messages
|
||||||
|
* in a `DC_CHAT_TYPE_IN_BROADCAST`.
|
||||||
|
*
|
||||||
|
* Called `broadcast` here rather than `channel`,
|
||||||
|
* because the word "channel" already appears a lot in the code,
|
||||||
|
* which would make it hard to grep for it.
|
||||||
*/
|
*/
|
||||||
#define DC_CHAT_TYPE_BROADCAST 160
|
#define DC_CHAT_TYPE_OUT_BROADCAST 160
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incoming broadcast channel, called "Channel" in the UI.
|
||||||
|
*
|
||||||
|
* This chat is read-only,
|
||||||
|
* and we do not know who the other recipients are.
|
||||||
|
*
|
||||||
|
* This is similar to `DC_CHAT_TYPE_MAILINGLIST`,
|
||||||
|
* with the main difference being that
|
||||||
|
* broadcasts are encrypted.
|
||||||
|
*
|
||||||
|
* Called `broadcast` here rather than `channel`,
|
||||||
|
* because the word "channel" already appears a lot in the code,
|
||||||
|
* which would make it hard to grep for it.
|
||||||
|
*/
|
||||||
|
#define DC_CHAT_TYPE_IN_BROADCAST 165
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
@@ -6177,7 +6391,6 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
|
||||||
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||||
* and dc_remove_contact_from_chat().
|
* and dc_remove_contact_from_chat().
|
||||||
*
|
*
|
||||||
@@ -6195,6 +6408,18 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
|
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat was deleted.
|
||||||
|
* This event is emitted in response to dc_delete_chat()
|
||||||
|
* called on this or another device.
|
||||||
|
* The event is a good place to remove notifications or homescreen shortcuts.
|
||||||
|
*
|
||||||
|
* @param data1 (int) chat_id
|
||||||
|
* @param data2 (int) 0
|
||||||
|
*/
|
||||||
|
#define DC_EVENT_CHAT_DELETED 2023
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||||
*
|
*
|
||||||
@@ -6435,15 +6660,6 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
#define DC_MEDIA_QUALITY_WORSE 1
|
#define DC_MEDIA_QUALITY_WORSE 1
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Values for dc_get|set_config("key_gen_type")
|
|
||||||
*/
|
|
||||||
#define DC_KEY_GEN_DEFAULT 0
|
|
||||||
#define DC_KEY_GEN_RSA2048 1
|
|
||||||
#define DC_KEY_GEN_ED25519 2
|
|
||||||
#define DC_KEY_GEN_RSA4096 3
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
|
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
|
||||||
*
|
*
|
||||||
@@ -6687,9 +6903,7 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// Used in summaries.
|
/// Used in summaries.
|
||||||
#define DC_STR_GIF 23
|
#define DC_STR_GIF 23
|
||||||
|
|
||||||
/// "Encrypted message"
|
/// @deprecated 2025-07, this string is no longer needed.
|
||||||
///
|
|
||||||
/// Used in subjects of outgoing messages.
|
|
||||||
#define DC_STR_ENCRYPTEDMSG 24
|
#define DC_STR_ENCRYPTEDMSG 24
|
||||||
|
|
||||||
/// "End-to-end encryption available."
|
/// "End-to-end encryption available."
|
||||||
@@ -6736,6 +6950,7 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// "End-to-end encryption preferred."
|
/// "End-to-end encryption preferred."
|
||||||
///
|
///
|
||||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||||
|
/// @deprecated 2025-06-05
|
||||||
#define DC_STR_E2E_PREFERRED 34
|
#define DC_STR_E2E_PREFERRED 34
|
||||||
|
|
||||||
/// "%1$s verified"
|
/// "%1$s verified"
|
||||||
@@ -6748,12 +6963,14 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
///
|
///
|
||||||
/// 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
|
||||||
|
/// @deprecated 2025-06-05
|
||||||
#define DC_STR_CONTACT_NOT_VERIFIED 36
|
#define DC_STR_CONTACT_NOT_VERIFIED 36
|
||||||
|
|
||||||
/// "Changed setup for %1$s."
|
/// "Changed setup for %1$s."
|
||||||
///
|
///
|
||||||
/// Used in status messages.
|
/// Used in status messages.
|
||||||
/// - %1$s will be replaced by the name of the contact with the changed setup
|
/// - %1$s will be replaced by the name of the contact with the changed setup
|
||||||
|
/// @deprecated 2025-06-05
|
||||||
#define DC_STR_CONTACT_SETUP_CHANGED 37
|
#define DC_STR_CONTACT_SETUP_CHANGED 37
|
||||||
|
|
||||||
/// "Archived chats"
|
/// "Archived chats"
|
||||||
@@ -6763,12 +6980,12 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
|
|
||||||
/// "Autocrypt Setup Message"
|
/// "Autocrypt Setup Message"
|
||||||
///
|
///
|
||||||
/// Used in subjects of outgoing Autocrypt Setup Messages.
|
/// @deprecated 2025-04
|
||||||
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
|
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
|
||||||
|
|
||||||
/// "This is the Autocrypt Setup Message, open it in a compatible client to use your setup"
|
/// "This is the Autocrypt Setup Message, open it in a compatible client to use your setup"
|
||||||
///
|
///
|
||||||
/// Used as message text of outgoing Autocrypt Setup Messages.
|
/// @deprecated 2025-04
|
||||||
#define DC_STR_AC_SETUP_MSG_BODY 43
|
#define DC_STR_AC_SETUP_MSG_BODY 43
|
||||||
|
|
||||||
/// "Cannot login as %1$s."
|
/// "Cannot login as %1$s."
|
||||||
@@ -7143,6 +7360,7 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// "%1$s changed their address from %2$s to %3$s"
|
/// "%1$s changed their address from %2$s to %3$s"
|
||||||
///
|
///
|
||||||
/// Used as an info message to chats with contacts that changed their address.
|
/// Used as an info message to chats with contacts that changed their address.
|
||||||
|
/// @deprecated 2025-06-05
|
||||||
#define DC_STR_AEAP_ADDR_CHANGED 122
|
#define DC_STR_AEAP_ADDR_CHANGED 122
|
||||||
|
|
||||||
/// "You changed your email address from %1$s to %2$s.
|
/// "You changed your email address from %1$s to %2$s.
|
||||||
@@ -7209,7 +7427,7 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// Used in status messages.
|
/// Used in status messages.
|
||||||
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
|
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
|
||||||
|
|
||||||
/// "You left the group."
|
/// "You left."
|
||||||
///
|
///
|
||||||
/// Used in status messages.
|
/// Used in status messages.
|
||||||
#define DC_STR_GROUP_LEFT_BY_YOU 132
|
#define DC_STR_GROUP_LEFT_BY_YOU 132
|
||||||
@@ -7380,6 +7598,18 @@ 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 set message deletion timer to 1 year."
|
||||||
|
///
|
||||||
|
/// Used in status messages.
|
||||||
|
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_YOU 158
|
||||||
|
|
||||||
|
/// "Message deletion timer is set to 1 year by %1$s."
|
||||||
|
///
|
||||||
|
/// `%1$s` will be replaced by name and address of the contact.
|
||||||
|
///
|
||||||
|
/// Used in status messages.
|
||||||
|
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_OTHER 159
|
||||||
|
|
||||||
/// "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.
|
||||||
@@ -7390,7 +7620,7 @@ 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."
|
/// "Messages are end-to-end encrypted."
|
||||||
///
|
///
|
||||||
/// Used in info messages.
|
/// Used in info messages.
|
||||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||||
@@ -7398,6 +7628,7 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// "%1$s sent a message from another device."
|
/// "%1$s sent a message from another device."
|
||||||
///
|
///
|
||||||
/// Used in info messages.
|
/// Used in info messages.
|
||||||
|
/// @deprecated 2025-07
|
||||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||||
|
|
||||||
/// "Others will only see this group after you sent a first message."
|
/// "Others will only see this group after you sent a first message."
|
||||||
@@ -7443,9 +7674,18 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
|
|
||||||
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
|
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
|
||||||
///
|
///
|
||||||
/// Used as info message.
|
/// @deprecated 2025-03
|
||||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||||
|
|
||||||
|
/// "The contact must be online to proceed. This process will continue automatically in background."
|
||||||
|
///
|
||||||
|
/// Used as info message.
|
||||||
|
/// @deprecated 2025-06-05
|
||||||
|
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
|
||||||
|
|
||||||
|
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
|
||||||
|
#define DC_STR_DONATION_REQUEST 193
|
||||||
|
|
||||||
/// "Contact". Deprecated, currently unused.
|
/// "Contact". Deprecated, currently unused.
|
||||||
#define DC_STR_CONTACT 200
|
#define DC_STR_CONTACT 200
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ pub enum Meaning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Lot {
|
impl Lot {
|
||||||
pub fn get_text1(&self) -> Option<Cow<str>> {
|
pub fn get_text1(&self) -> Option<Cow<'_, str>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Summary(summary) => match &summary.prefix {
|
Self::Summary(summary) => match &summary.prefix {
|
||||||
None => None,
|
None => None,
|
||||||
@@ -50,6 +50,7 @@ impl Lot {
|
|||||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||||
Qr::Backup2 { .. } => None,
|
Qr::Backup2 { .. } => None,
|
||||||
|
Qr::BackupTooNew { .. } => None,
|
||||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||||
@@ -65,7 +66,7 @@ impl Lot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
pub fn get_text2(&self) -> Option<Cow<'_, str>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||||
Self::Qr(_) => None,
|
Self::Qr(_) => None,
|
||||||
@@ -103,6 +104,7 @@ impl Lot {
|
|||||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||||
Qr::Account { .. } => LotState::QrAccount,
|
Qr::Account { .. } => LotState::QrAccount,
|
||||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||||
|
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||||
Qr::Proxy { .. } => LotState::QrProxy,
|
Qr::Proxy { .. } => LotState::QrProxy,
|
||||||
Qr::Addr { .. } => LotState::QrAddr,
|
Qr::Addr { .. } => LotState::QrAddr,
|
||||||
@@ -129,6 +131,7 @@ impl Lot {
|
|||||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||||
Qr::Account { .. } => Default::default(),
|
Qr::Account { .. } => Default::default(),
|
||||||
Qr::Backup2 { .. } => Default::default(),
|
Qr::Backup2 { .. } => Default::default(),
|
||||||
|
Qr::BackupTooNew { .. } => Default::default(),
|
||||||
Qr::WebrtcInstance { .. } => Default::default(),
|
Qr::WebrtcInstance { .. } => Default::default(),
|
||||||
Qr::Proxy { .. } => Default::default(),
|
Qr::Proxy { .. } => Default::default(),
|
||||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||||
@@ -178,10 +181,10 @@ pub enum LotState {
|
|||||||
/// text1=domain
|
/// text1=domain
|
||||||
QrAccount = 250,
|
QrAccount = 250,
|
||||||
|
|
||||||
QrBackup = 251,
|
|
||||||
|
|
||||||
QrBackup2 = 252,
|
QrBackup2 = 252,
|
||||||
|
|
||||||
|
QrBackupTooNew = 255,
|
||||||
|
|
||||||
/// text1=domain, text2=instance pattern
|
/// text1=domain, text2=instance pattern
|
||||||
QrWebrtcInstance = 260,
|
QrWebrtcInstance = 260,
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "1.152.2"
|
version = "2.11.0"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "deltachat-jsonrpc-server"
|
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
repository = "https://github.com/chatmail/core"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "deltachat-jsonrpc-server"
|
|
||||||
path = "src/webserver.rs"
|
|
||||||
required-features = ["webserver"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
deltachat = { workspace = true }
|
deltachat = { workspace = true }
|
||||||
deltachat-contact-tools = { workspace = true }
|
deltachat-contact-tools = { workspace = true }
|
||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
schemars = "0.8.21"
|
schemars = "0.8.22"
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
tempfile = { workspace = true }
|
|
||||||
log = { workspace = true }
|
|
||||||
async-channel = { workspace = true }
|
async-channel = { workspace = true }
|
||||||
futures = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||||
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
||||||
@@ -31,15 +22,12 @@ sanitize-filename = { workspace = true }
|
|||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
|
|
||||||
# optional dependencies
|
|
||||||
axum = { version = "0.7", optional = true, features = ["ws"] }
|
|
||||||
env_logger = { version = "0.11.6", optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
webserver = ["dep:env_logger", "dep:axum", "tokio/full", "yerpc/support-axum"]
|
|
||||||
vendored = ["deltachat/vendored"]
|
vendored = ["deltachat/vendored"]
|
||||||
|
|||||||
@@ -4,46 +4,16 @@ This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) inte
|
|||||||
|
|
||||||
The JSON-RPC API is exposed in two fashions:
|
The JSON-RPC API is exposed in two fashions:
|
||||||
|
|
||||||
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
|
* A executable `deltachat-rpc-server` that exposes the JSON-RPC API through stdio.
|
||||||
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). It exposes the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
||||||
|
|
||||||
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
|
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
#### Running the WebSocket server
|
|
||||||
|
|
||||||
From within this folder, you can start the WebSocket server with the following command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --features webserver
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to use the server in a production setup, first build it in release mode:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo build --features webserver --release
|
|
||||||
```
|
|
||||||
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
|
|
||||||
|
|
||||||
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
|
|
||||||
|
|
||||||
The server can be configured with environment variables:
|
|
||||||
|
|
||||||
|variable|default|description|
|
|
||||||
|-|-|-|
|
|
||||||
|`DC_PORT`|`20808`|port to listen on|
|
|
||||||
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
|
|
||||||
|
|
||||||
If you are targeting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cross build --features=webserver --target armv7-linux-androideabi --release
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using the TypeScript/JavaScript client
|
#### Using the TypeScript/JavaScript client
|
||||||
|
|
||||||
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
|
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/chatmail/yerpc)). Find the source in the [`typescript`](typescript) folder.
|
||||||
|
|
||||||
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
|
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
|
||||||
```sh
|
```sh
|
||||||
@@ -52,15 +22,7 @@ npm install
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
|
The JavaScript client is [published on NPM](https://www.npmjs.com/package/@deltachat/jsonrpc-client).
|
||||||
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { DeltaChat } from './deltachat.bundle.js'
|
|
||||||
const dc = new DeltaChat('ws://localhost:20808/ws')
|
|
||||||
const accounts = await dc.rpc.getAllAccounts()
|
|
||||||
console.log('accounts', accounts)
|
|
||||||
```
|
|
||||||
|
|
||||||
A script is included to build autogenerated documentation, which includes all RPC methods:
|
A script is included to build autogenerated documentation, which includes all RPC methods:
|
||||||
```sh
|
```sh
|
||||||
@@ -73,18 +35,6 @@ Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
|
|||||||
|
|
||||||
#### Running the example app
|
#### Running the example app
|
||||||
|
|
||||||
We include a small demo web application that talks to the WebSocket server. It can be used for testing. Feel invited to expand this.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd typescript
|
|
||||||
npm run build
|
|
||||||
npm run example:build
|
|
||||||
npm run example:start
|
|
||||||
```
|
|
||||||
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
|
|
||||||
|
|
||||||
Run `npm run example:dev` to live-rebuild the example app when files changes.
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
|
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
|
||||||
@@ -104,14 +54,12 @@ cd typescript
|
|||||||
npm run test
|
npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
|
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, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
|
||||||
|
|
||||||
Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
CHATMAIL_DOMAIN=chat.example.org npm run test
|
CHATMAIL_DOMAIN=ci-chatmail.testrun.org npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Test Coverage
|
#### Test Coverage
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
- [ ] different test type to simulate two devices: to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer
|
|
||||||
|
|
||||||
## MVP - Websocket server&client
|
|
||||||
|
|
||||||
For kaiOS and other experiments, like a deltachat "web" over network from an android phone.
|
|
||||||
|
|
||||||
- [ ] coverage for a majority of the API
|
|
||||||
- [ ] Blobs served
|
|
||||||
- [ ] Blob upload (for attachments, setting profile-picture, importing backup and so on)
|
|
||||||
- [ ] other way blobs can be addressed when using websocket vs. jsonrpc over dc-node
|
|
||||||
- [ ] Web push API? At least some kind of notification hook closure this lib can accept.
|
|
||||||
|
|
||||||
### Other Ideas for the Websocket server
|
|
||||||
|
|
||||||
- [ ] make sure there can only be one connection at a time to the ws
|
|
||||||
- why? , it could give problems if its commanded from multiple connections
|
|
||||||
- [ ] encrypted connection?
|
|
||||||
- [ ] authenticated connection?
|
|
||||||
- [ ] Look into unit-testing for the proc macros?
|
|
||||||
- [ ] proc macro taking over doc comments to generated typescript file
|
|
||||||
|
|
||||||
## Desktop Apis
|
|
||||||
|
|
||||||
Incomplete todo for desktop api porting, just some remainders for points that might need more work:
|
|
||||||
|
|
||||||
- [ ] manual start/stop io functions in the api for context and accounts, so "not syncing all accounts" can still be done in desktop -> webserver should then not do start io on all accounts by default
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -7,6 +7,7 @@ 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::blob::BlobObject;
|
||||||
use deltachat::chat::{
|
use deltachat::chat::{
|
||||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
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,
|
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||||
@@ -18,10 +19,11 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
|||||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||||
use deltachat::context::get_info;
|
use deltachat::context::get_info;
|
||||||
use deltachat::ephemeral::Timer;
|
use deltachat::ephemeral::Timer;
|
||||||
|
use deltachat::imex;
|
||||||
use deltachat::location;
|
use deltachat::location;
|
||||||
use deltachat::message::get_msg_read_receipts;
|
use deltachat::message::get_msg_read_receipts;
|
||||||
use deltachat::message::{
|
use deltachat::message::{
|
||||||
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||||
};
|
};
|
||||||
use deltachat::peer_channels::{
|
use deltachat::peer_channels::{
|
||||||
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
|
||||||
@@ -34,10 +36,10 @@ use deltachat::securejoin;
|
|||||||
use deltachat::stock_str::StockMessage;
|
use deltachat::stock_str::StockMessage;
|
||||||
use deltachat::webxdc::StatusUpdateSerial;
|
use deltachat::webxdc::StatusUpdateSerial;
|
||||||
use deltachat::EventEmitter;
|
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};
|
||||||
|
use types::login_param::EnteredLoginParam;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use yerpc::rpc;
|
use yerpc::rpc;
|
||||||
|
|
||||||
@@ -212,23 +214,30 @@ impl CommandApi {
|
|||||||
self.accounts.read().await.get_all()
|
self.accounts.read().await.get_all()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select account id for internally selected state.
|
/// Select account in account manager, this saves the last used account to accounts.toml
|
||||||
/// TODO: Likely this is deprecated as all methods take an account id now.
|
|
||||||
async fn select_account(&self, id: u32) -> Result<()> {
|
async fn select_account(&self, id: u32) -> Result<()> {
|
||||||
self.accounts.write().await.select_account(id).await
|
self.accounts.write().await.select_account(id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the selected account id of the internal state..
|
/// Get the selected account from the account manager (on startup it is read from accounts.toml)
|
||||||
/// TODO: Likely this is deprecated as all methods take an account id now.
|
|
||||||
async fn get_selected_account_id(&self) -> Option<u32> {
|
async fn get_selected_account_id(&self) -> Option<u32> {
|
||||||
self.accounts.read().await.get_selected_account_id()
|
self.accounts.read().await.get_selected_account_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the order of accounts.
|
||||||
|
/// The provided list should contain all account IDs in the desired order.
|
||||||
|
/// If an account ID is missing from the list, it will be appended at the end.
|
||||||
|
/// If the list contains non-existent account IDs, they will be ignored.
|
||||||
|
async fn set_accounts_order(&self, order: Vec<u32>) -> Result<()> {
|
||||||
|
self.accounts.write().await.set_accounts_order(order).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a list of all configured accounts.
|
/// Get a list of all configured accounts.
|
||||||
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
|
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
|
||||||
let mut accounts = Vec::new();
|
let mut accounts = Vec::new();
|
||||||
for id in self.accounts.read().await.get_all() {
|
let accounts_lock = self.accounts.read().await;
|
||||||
let context_option = self.accounts.read().await.get_account(id);
|
for id in accounts_lock.get_all() {
|
||||||
|
let context_option = accounts_lock.get_account(id);
|
||||||
if let Some(ctx) = context_option {
|
if let Some(ctx) = context_option {
|
||||||
accounts.push(Account::from_context(&ctx, id).await?)
|
accounts.push(Account::from_context(&ctx, id).await?)
|
||||||
}
|
}
|
||||||
@@ -326,8 +335,12 @@ impl CommandApi {
|
|||||||
.get_config_bool(deltachat::config::Config::ProxyEnabled)
|
.get_config_bool(deltachat::config::Config::ProxyEnabled)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let provider_info =
|
let provider_info = get_provider_info(
|
||||||
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await;
|
&ctx,
|
||||||
|
email.split('@').next_back().unwrap_or(""),
|
||||||
|
proxy_enabled,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
Ok(ProviderInfo::from_dc_type(provider_info))
|
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,11 +356,33 @@ impl CommandApi {
|
|||||||
ctx.get_info().await
|
ctx.get_info().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the blob dir.
|
||||||
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
|
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
|
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If there was an error while the account was opened
|
||||||
|
/// and migrated to the current version,
|
||||||
|
/// then this function returns it.
|
||||||
|
///
|
||||||
|
/// This function is useful because the key-contacts migration could fail due to bugs
|
||||||
|
/// and then the account will not work properly.
|
||||||
|
///
|
||||||
|
/// After opening an account, the UI should call this function
|
||||||
|
/// and show the error string if one is returned.
|
||||||
|
async fn get_migration_error(&self, account_id: u32) -> Result<Option<String>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
Ok(ctx.get_migration_error())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy file to blob dir.
|
||||||
|
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let file = Path::new(&path);
|
||||||
|
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
|
||||||
|
}
|
||||||
|
|
||||||
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
|
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
Ok(ctx.draft_self_report().await?.to_u32())
|
Ok(ctx.draft_self_report().await?.to_u32())
|
||||||
@@ -424,6 +459,9 @@ impl CommandApi {
|
|||||||
|
|
||||||
/// Configures this account with the currently set parameters.
|
/// Configures this account with the currently set parameters.
|
||||||
/// Setup the credential config before calling this.
|
/// Setup the credential config before calling this.
|
||||||
|
///
|
||||||
|
/// Deprecated as of 2025-02; use `add_transport_from_qr()`
|
||||||
|
/// or `add_or_update_transport()` instead.
|
||||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.stop_io().await;
|
ctx.stop_io().await;
|
||||||
@@ -438,6 +476,78 @@ impl CommandApi {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configures a new email account using the provided parameters
|
||||||
|
/// and adds it as a transport.
|
||||||
|
///
|
||||||
|
/// If the email address is the same as an existing transport,
|
||||||
|
/// then this existing account will be reconfigured instead of a new one being added.
|
||||||
|
///
|
||||||
|
/// This function stops and starts IO as needed.
|
||||||
|
///
|
||||||
|
/// Usually it will be enough to only set `addr` and `password`,
|
||||||
|
/// and all the other settings will be autoconfigured.
|
||||||
|
///
|
||||||
|
/// During configuration, ConfigureProgress events are emitted;
|
||||||
|
/// they indicate a successful configuration as well as errors
|
||||||
|
/// and may be used to create a progress bar.
|
||||||
|
/// This function will return after configuration is finished.
|
||||||
|
///
|
||||||
|
/// If configuration is successful,
|
||||||
|
/// the working server parameters will be saved
|
||||||
|
/// and used for connecting to the server.
|
||||||
|
/// The parameters entered by the user will be saved separately
|
||||||
|
/// so that they can be prefilled when the user opens the server-configuration screen again.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// - [Self::is_configured()] to check whether there is
|
||||||
|
/// at least one working transport.
|
||||||
|
/// - [Self::add_transport_from_qr()] to add a transport
|
||||||
|
/// from a server encoded in a QR code.
|
||||||
|
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||||
|
/// - [Self::delete_transport()] to remove a transport.
|
||||||
|
async fn add_or_update_transport(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
param: EnteredLoginParam,
|
||||||
|
) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
ctx.add_or_update_transport(&mut param.try_into()?).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deprecated 2025-04. Alias for [Self::add_or_update_transport()].
|
||||||
|
async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
|
||||||
|
self.add_or_update_transport(account_id, param).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new email account as a transport
|
||||||
|
/// using the server encoded in the QR code.
|
||||||
|
/// See [Self::add_or_update_transport].
|
||||||
|
async fn add_transport_from_qr(&self, account_id: u32, qr: String) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
ctx.add_transport_from_qr(&qr).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||||
|
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||||
|
/// and [Self::delete_transport()] to delete a transport.
|
||||||
|
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let res = ctx
|
||||||
|
.list_transports()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.into())
|
||||||
|
.collect();
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the transport with the specified email address
|
||||||
|
/// (i.e. [EnteredLoginParam::addr]).
|
||||||
|
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
ctx.delete_transport(&addr).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Signal an ongoing process to stop.
|
/// Signal an ongoing process to stop.
|
||||||
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
@@ -824,7 +934,7 @@ impl CommandApi {
|
|||||||
/// explicitly as it may happen that oneself gets removed from a still existing
|
/// explicitly as it may happen that oneself gets removed from a still existing
|
||||||
/// group
|
/// group
|
||||||
///
|
///
|
||||||
/// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
/// - for broadcast channels, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
||||||
///
|
///
|
||||||
/// - for mailing lists, the behavior is not documented currently, we will decide on that later.
|
/// - for mailing lists, the behavior is not documented currently, we will decide on that later.
|
||||||
/// for now, the UI should not show the list for mailing lists.
|
/// for now, the UI should not show the list for mailing lists.
|
||||||
@@ -836,7 +946,14 @@ impl CommandApi {
|
|||||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new group chat.
|
/// Returns contact IDs of the past chat members.
|
||||||
|
async fn get_past_chat_contacts(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let contacts = chat::get_past_chat_contacts(&ctx, ChatId::new(chat_id)).await?;
|
||||||
|
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new encrypted group chat (with key-contacts).
|
||||||
///
|
///
|
||||||
/// After creation,
|
/// After creation,
|
||||||
/// the group has one member with the ID DC_CONTACT_ID_SELF
|
/// the group has one member with the ID DC_CONTACT_ID_SELF
|
||||||
@@ -854,30 +971,52 @@ impl CommandApi {
|
|||||||
///
|
///
|
||||||
/// @param protect If set to 1 the function creates group with protection initially enabled.
|
/// @param protect If set to 1 the function creates group with protection initially enabled.
|
||||||
/// Only verified members are allowed in these groups
|
/// Only verified members are allowed in these groups
|
||||||
/// and end-to-end-encryption is always enabled.
|
|
||||||
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
|
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let protect = match protect {
|
let protect = match protect {
|
||||||
true => ProtectionStatus::Protected,
|
true => ProtectionStatus::Protected,
|
||||||
false => ProtectionStatus::Unprotected,
|
false => ProtectionStatus::Unprotected,
|
||||||
};
|
};
|
||||||
chat::create_group_chat(&ctx, protect, &name)
|
chat::create_group_ex(&ctx, Some(protect), &name)
|
||||||
.await
|
.await
|
||||||
.map(|id| id.to_u32())
|
.map(|id| id.to_u32())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new broadcast list.
|
/// Create a new unencrypted group chat.
|
||||||
///
|
///
|
||||||
/// Broadcast lists are similar to groups on the sending device,
|
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
|
||||||
/// however, recipients get the messages in a read-only chat
|
/// address-contacts.
|
||||||
/// and will see who the other members are.
|
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
|
||||||
///
|
|
||||||
/// For historical reasons, this function does not take a name directly,
|
|
||||||
/// instead you have to set the name using dc_set_chat_name()
|
|
||||||
/// after creating the broadcast list.
|
|
||||||
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_group_ex(&ctx, None, &name)
|
||||||
|
.await
|
||||||
|
.map(|id| id.to_u32())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deprecated 2025-07 in favor of create_broadcast().
|
||||||
|
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||||
|
self.create_broadcast(account_id, "Channel".to_string())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new **broadcast channel**
|
||||||
|
/// (called "Channel" in the UI).
|
||||||
|
///
|
||||||
|
/// Broadcast channels are similar to groups on the sending device,
|
||||||
|
/// however, recipients get the messages in a read-only chat
|
||||||
|
/// and will not see who the other members are.
|
||||||
|
///
|
||||||
|
/// Called `broadcast` here rather than `channel`,
|
||||||
|
/// because the word "channel" already appears a lot in the code,
|
||||||
|
/// which would make it hard to grep for it.
|
||||||
|
///
|
||||||
|
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||||
|
/// see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
|
||||||
|
///
|
||||||
|
/// Returns the created chat's id.
|
||||||
|
async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result<u32> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
chat::create_broadcast(&ctx, chat_name)
|
||||||
.await
|
.await
|
||||||
.map(|id| id.to_u32())
|
.map(|id| id.to_u32())
|
||||||
}
|
}
|
||||||
@@ -993,6 +1132,12 @@ impl CommandApi {
|
|||||||
marknoticed_chat(&ctx, ChatId::new(chat_id)).await
|
marknoticed_chat(&ctx, ChatId::new(chat_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the message that is immediately followed by the last seen
|
||||||
|
/// message.
|
||||||
|
/// From the point of view of the user this is effectively
|
||||||
|
/// "first unread", but in reality in the database a seen message
|
||||||
|
/// _can_ be followed by a fresh (unseen) message
|
||||||
|
/// if that message has not been individually marked as seen.
|
||||||
async fn get_first_unread_message_of_chat(
|
async fn get_first_unread_message_of_chat(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1081,6 +1226,11 @@ impl CommandApi {
|
|||||||
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
|
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns all messages of a particular chat.
|
||||||
|
///
|
||||||
|
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
|
||||||
|
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
|
||||||
|
/// corresponding (following) day in the local timezone.
|
||||||
async fn get_message_ids(
|
async fn get_message_ids(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1191,7 +1341,15 @@ impl CommandApi {
|
|||||||
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||||
delete_msgs(&ctx, &msgs).await
|
delete_msgs_ex(&ctx, &msgs, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete messages. The messages are deleted on the current device,
|
||||||
|
/// on the IMAP server and also for all chat members
|
||||||
|
async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||||
|
delete_msgs_ex(&ctx, &msgs, true).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an informational text for a single message. The text is multiline and may
|
/// Get an informational text for a single message. The text is multiline and may
|
||||||
@@ -1291,6 +1449,12 @@ impl CommandApi {
|
|||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn save_msgs(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||||
|
chat::save_msgs(&ctx, &message_ids).await
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// contact
|
// contact
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
@@ -1309,7 +1473,14 @@ impl CommandApi {
|
|||||||
|
|
||||||
/// Add a single contact as a result of an explicit user action.
|
/// Add a single contact as a result of an explicit user action.
|
||||||
///
|
///
|
||||||
/// Returns contact id of the created or existing contact
|
/// This will always create or look up an address-contact,
|
||||||
|
/// i.e. a contact identified by an email address,
|
||||||
|
/// with all messages sent to and from this contact being unencrypted.
|
||||||
|
/// If the user just clicked on an email address,
|
||||||
|
/// you should first check [`Self::lookup_contact_id_by_addr`]/`lookupContactIdByAddr.`,
|
||||||
|
/// and only if there is no contact yet, call this function here.
|
||||||
|
///
|
||||||
|
/// Returns contact id of the created or existing contact.
|
||||||
async fn create_contact(
|
async fn create_contact(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1361,6 +1532,14 @@ impl CommandApi {
|
|||||||
Ok(contacts)
|
Ok(contacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns ids of known and unblocked contacts.
|
||||||
|
///
|
||||||
|
/// By default, key-contacts are listed.
|
||||||
|
///
|
||||||
|
/// * `list_flags` - A combination of flags:
|
||||||
|
/// - `DC_GCL_ADD_SELF` - Add SELF unless filtered by other parameters.
|
||||||
|
/// - `DC_GCL_ADDRESS` - List address-contacts instead of key-contacts.
|
||||||
|
/// * `query` - A string to filter the list.
|
||||||
async fn get_contact_ids(
|
async fn get_contact_ids(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1372,8 +1551,10 @@ impl CommandApi {
|
|||||||
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
|
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a list of contacts.
|
/// Returns known and unblocked contacts.
|
||||||
/// (formerly called getContacts2 in desktop)
|
///
|
||||||
|
/// Formerly called `getContacts2` in Desktop.
|
||||||
|
/// See [`Self::get_contact_ids`] for parameters and more info.
|
||||||
async fn get_contacts(
|
async fn get_contacts(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1424,15 +1605,7 @@ impl CommandApi {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets contact encryption.
|
/// Sets display name for existing contact.
|
||||||
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
|
|
||||||
let ctx = self.get_context(account_id).await?;
|
|
||||||
let contact_id = ContactId::new(contact_id);
|
|
||||||
|
|
||||||
contact_id.reset_encryption(&ctx).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn change_contact_name(
|
async fn change_contact_name(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1441,9 +1614,7 @@ impl CommandApi {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let contact_id = ContactId::new(contact_id);
|
let contact_id = ContactId::new(contact_id);
|
||||||
let contact = Contact::get_by_id(&ctx, contact_id).await?;
|
contact_id.set_name(&ctx, &name).await?;
|
||||||
let addr = contact.get_addr();
|
|
||||||
Contact::create(&ctx, &name, addr).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1459,9 +1630,19 @@ impl CommandApi {
|
|||||||
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
|
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if an e-mail address belongs to a known and unblocked contact.
|
/// Looks up a known and unblocked contact with a given e-mail address.
|
||||||
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||||
///
|
///
|
||||||
|
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||||
|
/// (e.g. an address-contact and a key-contact),
|
||||||
|
/// this looks up the most recently seen contact,
|
||||||
|
/// i.e. which contact is returned depends on which contact last sent a message.
|
||||||
|
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||||
|
/// But **DO NOT** internally represent contacts by their email address
|
||||||
|
/// and do not use this function to look them up;
|
||||||
|
/// otherwise this function will sometimes look up the wrong contact.
|
||||||
|
/// Instead, you should internally represent contacts by their ids.
|
||||||
|
///
|
||||||
/// To validate an e-mail address independently of the contact database
|
/// To validate an e-mail address independently of the contact database
|
||||||
/// use check_email_validity().
|
/// use check_email_validity().
|
||||||
async fn lookup_contact_id_by_addr(
|
async fn lookup_contact_id_by_addr(
|
||||||
@@ -1498,6 +1679,18 @@ impl CommandApi {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Imports contacts from a vCard.
|
||||||
|
///
|
||||||
|
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
|
||||||
|
async fn import_vcard_contents(&self, account_id: u32, vcard: String) -> Result<Vec<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
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.
|
/// Returns a vCard containing contacts with the given ids.
|
||||||
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
|
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
@@ -1790,12 +1983,10 @@ impl CommandApi {
|
|||||||
instance_msg_id: u32,
|
instance_msg_id: u32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
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) =
|
||||||
if let Some(fut) = fut {
|
send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?
|
||||||
tokio::spawn(async move {
|
{
|
||||||
fut.await.ok();
|
tokio::spawn(fut);
|
||||||
info!(ctx, "send_webxdc_realtime_advertisement done")
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1831,13 +2022,9 @@ impl CommandApi {
|
|||||||
|
|
||||||
/// Get href from a WebxdcInfoMessage which might include a hash holding
|
/// Get href from a WebxdcInfoMessage which might include a hash holding
|
||||||
/// information about a specific position or state in a webxdc app (optional)
|
/// information about a specific position or state in a webxdc app (optional)
|
||||||
async fn get_webxdc_href(
|
async fn get_webxdc_href(&self, account_id: u32, info_msg_id: u32) -> Result<Option<String>> {
|
||||||
&self,
|
|
||||||
account_id: u32,
|
|
||||||
instance_msg_id: u32,
|
|
||||||
) -> Result<Option<String>> {
|
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
let message = Message::load_from_db(&ctx, MsgId::new(info_msg_id)).await?;
|
||||||
Ok(message.get_webxdc_href())
|
Ok(message.get_webxdc_href())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1930,7 +2117,7 @@ impl CommandApi {
|
|||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
|
||||||
let mut msg = Message::new(Viewtype::Sticker);
|
let mut msg = Message::new(Viewtype::Sticker);
|
||||||
msg.set_file(&sticker_path, None);
|
msg.set_file_and_deduplicate(&ctx, Path::new(&sticker_path), None, None)?;
|
||||||
|
|
||||||
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
||||||
msg.force_sticker();
|
msg.force_sticker();
|
||||||
@@ -1984,6 +2171,16 @@ impl CommandApi {
|
|||||||
Ok(msg_id)
|
Ok(msg_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_edit_request(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
msg_id: u32,
|
||||||
|
new_text: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
chat::send_edit_request(&ctx, MsgId::new(msg_id), new_text).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks if messages can be sent to a given chat.
|
/// Checks if messages can be sent to a given chat.
|
||||||
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
@@ -2150,12 +2347,45 @@ impl CommandApi {
|
|||||||
|
|
||||||
// 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,
|
||||||
// the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map
|
// the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map
|
||||||
|
/// Send a message to a chat.
|
||||||
|
///
|
||||||
|
/// This function returns after the message has been placed in the sending queue.
|
||||||
|
/// This does not imply that the message was really sent out yet.
|
||||||
|
/// However, from your view, you're done with the message.
|
||||||
|
/// Sooner or later it will find its way.
|
||||||
|
///
|
||||||
|
/// **Attaching files:**
|
||||||
|
///
|
||||||
|
/// Pass the file path in the `file` parameter.
|
||||||
|
/// If `file` is not in the blob directory yet,
|
||||||
|
/// it will be copied into the blob directory.
|
||||||
|
/// If you want, you can delete the file immediately after this function returns.
|
||||||
|
///
|
||||||
|
/// You can also write the attachment directly into the blob directory
|
||||||
|
/// and then pass the path as the `file` parameter;
|
||||||
|
/// this will prevent an unnecessary copying of the file.
|
||||||
|
///
|
||||||
|
/// In `filename`, you can pass the original name of the file,
|
||||||
|
/// which will then be shown in the UI.
|
||||||
|
/// in this case the current name of `file` on the filesystem will be ignored.
|
||||||
|
///
|
||||||
|
/// In order to deduplicate files that contain the same data,
|
||||||
|
/// the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
|
||||||
|
///
|
||||||
|
/// NOTE:
|
||||||
|
/// - This function will rename the file. To get the new file path, call `get_file()`.
|
||||||
|
/// - The file must not be modified after this function was called.
|
||||||
|
/// - Images etc. will NOT be recoded.
|
||||||
|
/// In order to recode images,
|
||||||
|
/// use `misc_set_draft` and pass `Image` as the viewtype.
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
async fn misc_send_msg(
|
async fn misc_send_msg(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
chat_id: u32,
|
chat_id: u32,
|
||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
|
filename: Option<String>,
|
||||||
location: Option<(f64, f64)>,
|
location: Option<(f64, f64)>,
|
||||||
quoted_message_id: Option<u32>,
|
quoted_message_id: Option<u32>,
|
||||||
) -> Result<(u32, MessageObject)> {
|
) -> Result<(u32, MessageObject)> {
|
||||||
@@ -2167,7 +2397,7 @@ impl CommandApi {
|
|||||||
});
|
});
|
||||||
message.set_text(text.unwrap_or_default());
|
message.set_text(text.unwrap_or_default());
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
message.set_file(file, None);
|
message.set_file_and_deduplicate(&ctx, Path::new(&file), filename.as_deref(), None)?;
|
||||||
}
|
}
|
||||||
if let Some((latitude, longitude)) = location {
|
if let Some((latitude, longitude)) = location {
|
||||||
message.set_location(latitude, longitude);
|
message.set_location(latitude, longitude);
|
||||||
@@ -2195,12 +2425,14 @@ impl CommandApi {
|
|||||||
// the better version should support:
|
// the better version should support:
|
||||||
// - changing viewtype to enable/disable compression
|
// - changing viewtype to enable/disable compression
|
||||||
// - keeping same message id as long as attachment does not change for webxdc messages
|
// - keeping same message id as long as attachment does not change for webxdc messages
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
async fn misc_set_draft(
|
async fn misc_set_draft(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
chat_id: u32,
|
chat_id: u32,
|
||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
|
filename: Option<String>,
|
||||||
quoted_message_id: Option<u32>,
|
quoted_message_id: Option<u32>,
|
||||||
view_type: Option<MessageViewtype>,
|
view_type: Option<MessageViewtype>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -2217,7 +2449,7 @@ impl CommandApi {
|
|||||||
));
|
));
|
||||||
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_and_deduplicate(&ctx, Path::new(&file), filename.as_deref(), None)?;
|
||||||
}
|
}
|
||||||
if let Some(id) = quoted_message_id {
|
if let Some(id) = quoted_message_id {
|
||||||
draft
|
draft
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::{bail, Context as _, Result};
|
use anyhow::{bail, Context as _, Result};
|
||||||
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
|
||||||
use deltachat::chat::{Chat, ChatId};
|
use deltachat::chat::{Chat, ChatId};
|
||||||
use deltachat::constants::Chattype;
|
use deltachat::constants::Chattype;
|
||||||
use deltachat::contact::{Contact, ContactId};
|
use deltachat::contact::{Contact, ContactId};
|
||||||
@@ -21,15 +21,39 @@ pub struct FullChat {
|
|||||||
|
|
||||||
/// True if the chat is protected.
|
/// True if the chat is protected.
|
||||||
///
|
///
|
||||||
/// UI should display a green checkmark
|
/// Only verified contacts
|
||||||
/// in the chat title,
|
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
|
||||||
/// in the chat profile title and
|
/// can be added to protected chats.
|
||||||
/// in the chatlist item
|
///
|
||||||
/// if chat protection is enabled.
|
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
|
||||||
/// UI should also display a green checkmark
|
/// by setting the 'protect' parameter to true.
|
||||||
/// in the contact profile
|
///
|
||||||
/// if 1:1 chat with this contact exists and is protected.
|
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
|
|
||||||
|
/// True if the chat is encrypted.
|
||||||
|
/// This means that all messages in the chat are encrypted,
|
||||||
|
/// and all contacts in the chat are "key-contacts",
|
||||||
|
/// i.e. identified by the PGP key fingerprint.
|
||||||
|
///
|
||||||
|
/// False if the chat is unencrypted.
|
||||||
|
/// This means that all messages in the chat are unencrypted,
|
||||||
|
/// and all contacts in the chat are "address-contacts",
|
||||||
|
/// i.e. identified by the email address.
|
||||||
|
/// The UI should mark this chat e.g. with a mail-letter icon.
|
||||||
|
///
|
||||||
|
/// Unencrypted groups are called "ad-hoc groups"
|
||||||
|
/// and the user can't add/remove members,
|
||||||
|
/// create a QR invite code,
|
||||||
|
/// or set an avatar.
|
||||||
|
/// These options should therefore be disabled in the UI.
|
||||||
|
///
|
||||||
|
/// Note that it can happen that an encrypted chat
|
||||||
|
/// contains unencrypted messages that were received in core <= v1.159.*
|
||||||
|
/// and vice versa.
|
||||||
|
///
|
||||||
|
/// See also `is_key_contact` on `Contact`.
|
||||||
|
is_encrypted: bool,
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
pinned: bool,
|
pinned: bool,
|
||||||
@@ -39,11 +63,17 @@ pub struct FullChat {
|
|||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
contacts: Vec<ContactObject>,
|
contacts: Vec<ContactObject>,
|
||||||
contact_ids: Vec<u32>,
|
contact_ids: Vec<u32>,
|
||||||
|
|
||||||
|
/// Contact IDs of the past chat members.
|
||||||
|
past_contact_ids: Vec<u32>,
|
||||||
|
|
||||||
color: String,
|
color: String,
|
||||||
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,
|
||||||
|
/// Deprecated 2025-07. Chats protection cannot break any longer.
|
||||||
is_protection_broken: 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,
|
||||||
@@ -59,6 +89,7 @@ impl FullChat {
|
|||||||
let chat = Chat::load_from_db(context, rust_chat_id).await?;
|
let chat = Chat::load_from_db(context, rust_chat_id).await?;
|
||||||
|
|
||||||
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
||||||
|
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
||||||
|
|
||||||
let mut contacts = Vec::with_capacity(contact_ids.len());
|
let mut contacts = Vec::with_capacity(contact_ids.len());
|
||||||
|
|
||||||
@@ -103,6 +134,7 @@ impl FullChat {
|
|||||||
id: chat_id,
|
id: chat_id,
|
||||||
name: chat.name.clone(),
|
name: chat.name.clone(),
|
||||||
is_protected: chat.is_protected(),
|
is_protected: chat.is_protected(),
|
||||||
|
is_encrypted: chat.is_encrypted(context).await?,
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||||
@@ -111,6 +143,7 @@ impl FullChat {
|
|||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
contacts,
|
contacts,
|
||||||
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||||
|
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||||
color,
|
color,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
@@ -153,6 +186,30 @@ pub struct BasicChat {
|
|||||||
/// in the contact profile
|
/// in the contact profile
|
||||||
/// if 1:1 chat with this contact exists and is protected.
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
|
|
||||||
|
/// True if the chat is encrypted.
|
||||||
|
/// This means that all messages in the chat are encrypted,
|
||||||
|
/// and all contacts in the chat are "key-contacts",
|
||||||
|
/// i.e. identified by the PGP key fingerprint.
|
||||||
|
///
|
||||||
|
/// False if the chat is unencrypted.
|
||||||
|
/// This means that all messages in the chat are unencrypted,
|
||||||
|
/// and all contacts in the chat are "address-contacts",
|
||||||
|
/// i.e. identified by the email address.
|
||||||
|
/// The UI should mark this chat e.g. with a mail-letter icon.
|
||||||
|
///
|
||||||
|
/// Unencrypted groups are called "ad-hoc groups"
|
||||||
|
/// and the user can't add/remove members,
|
||||||
|
/// create a QR invite code,
|
||||||
|
/// or set an avatar.
|
||||||
|
/// These options should therefore be disabled in the UI.
|
||||||
|
///
|
||||||
|
/// Note that it can happen that an encrypted chat
|
||||||
|
/// contains unencrypted messages that were received in core <= v1.159.*
|
||||||
|
/// and vice versa.
|
||||||
|
///
|
||||||
|
/// See also `is_key_contact` on `Contact`.
|
||||||
|
is_encrypted: bool,
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
pinned: bool,
|
pinned: bool,
|
||||||
@@ -161,7 +218,9 @@ pub struct BasicChat {
|
|||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
color: String,
|
color: String,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
/// Deprecated 2025-07. Chats protection cannot break any longer.
|
||||||
is_protection_broken: bool,
|
is_protection_broken: bool,
|
||||||
|
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
}
|
}
|
||||||
@@ -181,6 +240,7 @@ impl BasicChat {
|
|||||||
id: chat_id,
|
id: chat_id,
|
||||||
name: chat.name.clone(),
|
name: chat.name.clone(),
|
||||||
is_protected: chat.is_protected(),
|
is_protected: chat.is_protected(),
|
||||||
|
is_encrypted: chat.is_encrypted(context).await?,
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub enum ChatListItemFetchResult {
|
|||||||
name: String,
|
name: String,
|
||||||
avatar_path: Option<String>,
|
avatar_path: Option<String>,
|
||||||
color: String,
|
color: String,
|
||||||
|
chat_type: u32,
|
||||||
last_updated: Option<i64>,
|
last_updated: Option<i64>,
|
||||||
summary_text1: String,
|
summary_text1: String,
|
||||||
summary_text2: String,
|
summary_text2: String,
|
||||||
@@ -30,6 +31,31 @@ pub enum ChatListItemFetchResult {
|
|||||||
/// showing preview if last chat message is image
|
/// showing preview if last chat message is image
|
||||||
summary_preview_image: Option<String>,
|
summary_preview_image: Option<String>,
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
|
|
||||||
|
/// True if the chat is encrypted.
|
||||||
|
/// This means that all messages in the chat are encrypted,
|
||||||
|
/// and all contacts in the chat are "key-contacts",
|
||||||
|
/// i.e. identified by the PGP key fingerprint.
|
||||||
|
///
|
||||||
|
/// False if the chat is unencrypted.
|
||||||
|
/// This means that all messages in the chat are unencrypted,
|
||||||
|
/// and all contacts in the chat are "address-contacts",
|
||||||
|
/// i.e. identified by the email address.
|
||||||
|
/// The UI should mark this chat e.g. with a mail-letter icon.
|
||||||
|
///
|
||||||
|
/// Unencrypted groups are called "ad-hoc groups"
|
||||||
|
/// and the user can't add/remove members,
|
||||||
|
/// create a QR invite code,
|
||||||
|
/// or set an avatar.
|
||||||
|
/// These options should therefore be disabled in the UI.
|
||||||
|
///
|
||||||
|
/// Note that it can happen that an encrypted chat
|
||||||
|
/// contains unencrypted messages that were received in core <= v1.159.*
|
||||||
|
/// and vice versa.
|
||||||
|
///
|
||||||
|
/// See also `is_key_contact` on `Contact`.
|
||||||
|
is_encrypted: bool,
|
||||||
|
/// deprecated 2025-07, use chat_type instead
|
||||||
is_group: bool,
|
is_group: bool,
|
||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
@@ -40,8 +66,6 @@ pub enum ChatListItemFetchResult {
|
|||||||
is_pinned: bool,
|
is_pinned: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
/// true when chat is a broadcastlist
|
|
||||||
is_broadcast: bool,
|
|
||||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||||
dm_chat_contact: Option<u32>,
|
dm_chat_contact: Option<u32>,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
@@ -131,12 +155,14 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
name: chat.get_name().to_owned(),
|
name: chat.get_name().to_owned(),
|
||||||
avatar_path,
|
avatar_path,
|
||||||
color,
|
color,
|
||||||
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
last_updated,
|
last_updated,
|
||||||
summary_text1,
|
summary_text1,
|
||||||
summary_text2,
|
summary_text2,
|
||||||
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
||||||
summary_preview_image,
|
summary_preview_image,
|
||||||
is_protected: chat.is_protected(),
|
is_protected: chat.is_protected(),
|
||||||
|
is_encrypted: chat.is_encrypted(ctx).await?,
|
||||||
is_group: chat.get_type() == Chattype::Group,
|
is_group: chat.get_type() == Chattype::Group,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
@@ -147,7 +173,6 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
is_pinned: visibility == ChatVisibility::Pinned,
|
is_pinned: visibility == ChatVisibility::Pinned,
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
is_broadcast: chat.get_type() == Chattype::Broadcast,
|
|
||||||
dm_chat_contact,
|
dm_chat_contact,
|
||||||
was_seen_recently,
|
was_seen_recently,
|
||||||
last_message_type: message_type,
|
last_message_type: message_type,
|
||||||
|
|||||||
@@ -19,15 +19,23 @@ 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,
|
||||||
|
|
||||||
|
/// Is the contact a key contact.
|
||||||
|
is_key_contact: bool,
|
||||||
|
|
||||||
|
/// Is encryption available for this contact.
|
||||||
|
///
|
||||||
|
/// This can only be true for key-contacts.
|
||||||
|
/// However, it is possible to have a key-contact
|
||||||
|
/// for which encryption is not available because we don't have a key yet,
|
||||||
|
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||||
e2ee_avail: bool,
|
e2ee_avail: bool,
|
||||||
|
|
||||||
/// True if the contact can be added to verified groups.
|
/// True if the contact
|
||||||
|
/// can be added to protected chats
|
||||||
|
/// because SELF and contact have verified their fingerprints in both directions.
|
||||||
///
|
///
|
||||||
/// If this is true
|
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||||
/// 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,
|
||||||
|
|
||||||
/// True if the contact profile title should have a green checkmark.
|
/// True if the contact profile title should have a green checkmark.
|
||||||
@@ -36,12 +44,29 @@ pub struct ContactObject {
|
|||||||
/// or will have a green checkmark if created.
|
/// or will have a green checkmark if created.
|
||||||
is_profile_verified: bool,
|
is_profile_verified: bool,
|
||||||
|
|
||||||
/// The ID of the contact that verified this contact.
|
/// The contact ID that verified a contact.
|
||||||
///
|
///
|
||||||
/// If this is present,
|
/// As verifier may be unknown,
|
||||||
/// display a green checkmark and "Introduced by ..."
|
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||||
/// string followed by the verifier contact name and address
|
///
|
||||||
/// in the contact profile.
|
/// UI should display the information in the contact's profile as follows:
|
||||||
|
///
|
||||||
|
/// - If `verifierId` != 0,
|
||||||
|
/// display text "Introduced by ..."
|
||||||
|
/// with the name and address of the contact
|
||||||
|
/// formatted by `name_and_addr`/`nameAndAddr`.
|
||||||
|
/// Prefix the text by a green checkmark.
|
||||||
|
///
|
||||||
|
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||||
|
/// display "Introduced" prefixed by a green checkmark.
|
||||||
|
///
|
||||||
|
/// - if `verifierId` == 0 and `isVerified` == 0,
|
||||||
|
/// display nothing
|
||||||
|
///
|
||||||
|
/// This contains the contact ID of the verifier.
|
||||||
|
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
||||||
|
/// If it is None/Null, we don't have verifier information or
|
||||||
|
/// the contact is not verified.
|
||||||
verifier_id: Option<u32>,
|
verifier_id: Option<u32>,
|
||||||
|
|
||||||
/// the contact's last seen timestamp
|
/// the contact's last seen timestamp
|
||||||
@@ -67,6 +92,7 @@ impl ContactObject {
|
|||||||
let verifier_id = contact
|
let verifier_id = contact
|
||||||
.get_verifier_id(context)
|
.get_verifier_id(context)
|
||||||
.await?
|
.await?
|
||||||
|
.flatten()
|
||||||
.map(|contact_id| contact_id.to_u32());
|
.map(|contact_id| contact_id.to_u32());
|
||||||
|
|
||||||
Ok(ContactObject {
|
Ok(ContactObject {
|
||||||
@@ -80,6 +106,7 @@ impl ContactObject {
|
|||||||
profile_image, //BLOBS
|
profile_image, //BLOBS
|
||||||
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_key_contact: contact.is_key_contact(),
|
||||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||||
is_verified,
|
is_verified,
|
||||||
is_profile_verified,
|
is_profile_verified,
|
||||||
|
|||||||
@@ -84,44 +84,78 @@ pub enum EventType {
|
|||||||
/// - Messages sent, received or removed
|
/// - Messages sent, received or removed
|
||||||
/// - Chats created, deleted or archived
|
/// - Chats created, deleted or archived
|
||||||
/// - A draft has been set
|
/// - A draft has been set
|
||||||
///
|
|
||||||
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
|
||||||
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgsChanged { chat_id: u32, msg_id: u32 },
|
MsgsChanged {
|
||||||
|
/// Set if only a single chat is affected by the changes, otherwise 0.
|
||||||
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// Set if only a single message is affected by the changes, otherwise 0.
|
||||||
|
msg_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
/// Reactions for the message changed.
|
/// Reactions for the message changed.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ReactionsChanged {
|
ReactionsChanged {
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
chat_id: u32,
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// ID of the message for which reactions were changed.
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
|
|
||||||
|
/// ID of the contact whose reaction set is changed.
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Incoming reaction, should be notified.
|
/// A reaction to one's own sent message received.
|
||||||
|
/// Typically, the UI will show a notification for that.
|
||||||
|
///
|
||||||
|
/// In addition to this event, ReactionsChanged is emitted.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
IncomingReaction {
|
IncomingReaction {
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// ID of the contact whose reaction set is changed.
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
|
|
||||||
|
/// ID of the message for which reactions were changed.
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
|
|
||||||
|
/// The reaction.
|
||||||
reaction: String,
|
reaction: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Incoming webxdc info or summary update, should be notified.
|
/// Incoming webxdc info or summary update, should be notified.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
IncomingWebxdcNotify {
|
IncomingWebxdcNotify {
|
||||||
|
/// ID of the chat.
|
||||||
chat_id: u32,
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// ID of the contact sending.
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
|
|
||||||
|
/// ID of the added info message or webxdc instance in case of summary change.
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
|
|
||||||
|
/// Text to notify.
|
||||||
text: String,
|
text: String,
|
||||||
|
|
||||||
|
/// Link assigned to this notification, if any.
|
||||||
href: Option<String>,
|
href: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// There is a fresh message. Typically, the user will show an notification
|
/// There is a fresh message. Typically, the user will show a notification
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent 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 { chat_id: u32, msg_id: u32 },
|
IncomingMsg {
|
||||||
|
/// ID of the chat where the message is assigned.
|
||||||
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// ID of the message.
|
||||||
|
msg_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
/// 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,
|
||||||
@@ -137,24 +171,59 @@ pub enum EventType {
|
|||||||
/// 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 { chat_id: u32, msg_id: u32 },
|
MsgDelivered {
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// ID of the message that was successfully sent.
|
||||||
|
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 { chat_id: u32, msg_id: u32 },
|
MsgFailed {
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// ID of the message that could not be sent.
|
||||||
|
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 { chat_id: u32, msg_id: u32 },
|
MsgRead {
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
|
chat_id: u32,
|
||||||
|
|
||||||
/// A single message is deleted.
|
/// ID of the message that was read.
|
||||||
|
msg_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A single message was deleted.
|
||||||
|
///
|
||||||
|
/// This event means that the message will no longer appear in the messagelist.
|
||||||
|
/// UI should remove the message from the messagelist
|
||||||
|
/// in response to this event if the message is currently displayed.
|
||||||
|
///
|
||||||
|
/// The message may have been explicitly deleted by the user or expired.
|
||||||
|
/// Internally the message may have been removed from the database,
|
||||||
|
/// moved to the trash chat or hidden.
|
||||||
|
///
|
||||||
|
/// This event does not indicate the message
|
||||||
|
/// deletion from the server.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgDeleted { chat_id: u32, msg_id: u32 },
|
MsgDeleted {
|
||||||
|
/// ID of the chat where the message was prior to deletion.
|
||||||
|
/// Never 0.
|
||||||
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// ID of the deleted message. Never 0.
|
||||||
|
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.
|
|
||||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||||
/// and removeContactFromChat().
|
/// and removeContactFromChat().
|
||||||
///
|
///
|
||||||
@@ -165,21 +234,35 @@ pub enum EventType {
|
|||||||
|
|
||||||
/// Chat ephemeral timer changed.
|
/// Chat ephemeral timer changed.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatEphemeralTimerModified { chat_id: u32, timer: u32 },
|
ChatEphemeralTimerModified {
|
||||||
|
/// Chat ID.
|
||||||
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// New ephemeral timer value.
|
||||||
|
timer: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Chat deleted.
|
||||||
|
ChatDeleted {
|
||||||
|
/// Chat ID.
|
||||||
|
chat_id: 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.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ContactsChanged { contact_id: Option<u32> },
|
ContactsChanged {
|
||||||
|
/// If set, this is the contact_id of an added contact that should be selected.
|
||||||
|
contact_id: Option<u32>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Location of one or more contact has changed.
|
/// Location of one or more contact has changed.
|
||||||
///
|
|
||||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
|
||||||
/// If the locations of several contacts have been changed,
|
|
||||||
/// this parameter is set to `None`.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
LocationChanged { contact_id: Option<u32> },
|
LocationChanged {
|
||||||
|
/// contact_id of the contact for which the location has changed.
|
||||||
|
/// If the locations of several contacts have been changed,
|
||||||
|
/// this parameter is set to `None`.
|
||||||
|
contact_id: Option<u32>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Inform about the configuration progress started by configure().
|
/// Inform about the configuration progress started by configure().
|
||||||
ConfigureProgress {
|
ConfigureProgress {
|
||||||
@@ -194,10 +277,11 @@ pub enum EventType {
|
|||||||
|
|
||||||
/// Inform about the import/export progress started by imex().
|
/// Inform about the import/export progress started by imex().
|
||||||
///
|
///
|
||||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
|
||||||
/// @param data2 0
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexProgress { progress: usize },
|
ImexProgress {
|
||||||
|
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
|
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().
|
||||||
@@ -214,26 +298,34 @@ pub enum EventType {
|
|||||||
///
|
///
|
||||||
/// These events are typically sent after a joiner has scanned the QR code
|
/// These events are typically sent after a joiner has scanned the QR code
|
||||||
/// generated by getChatSecurejoinQrCodeSvg().
|
/// generated by getChatSecurejoinQrCodeSvg().
|
||||||
///
|
|
||||||
/// @param data1 (int) ID of the contact that wants to join.
|
|
||||||
/// @param data2 (int) Progress as:
|
|
||||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
|
||||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
|
||||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
|
||||||
/// 1000=Protocol finished for this contact.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
SecurejoinInviterProgress {
|
||||||
|
/// ID of the contact that wants to join.
|
||||||
|
contact_id: u32,
|
||||||
|
|
||||||
|
/// Progress as:
|
||||||
|
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||||
|
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||||
|
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
||||||
|
/// 1000=Protocol finished for this contact.
|
||||||
|
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).
|
||||||
/// The events are typically sent while secureJoin(), which
|
/// The events are typically sent while secureJoin(), which
|
||||||
/// may take some time, is executed.
|
/// may take some time, is executed.
|
||||||
/// @param data1 (int) ID of the inviting contact.
|
|
||||||
/// @param data2 (int) Progress as:
|
|
||||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
|
||||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
SecurejoinJoinerProgress {
|
||||||
|
/// ID of the inviting contact.
|
||||||
|
contact_id: u32,
|
||||||
|
|
||||||
|
/// Progress as:
|
||||||
|
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
|
/// 1000=vg-member-added/vc-contact-confirm received
|
||||||
|
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
|
||||||
@@ -254,22 +346,37 @@ pub enum EventType {
|
|||||||
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcStatusUpdate {
|
WebxdcStatusUpdate {
|
||||||
|
/// Message ID.
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
|
|
||||||
|
/// Status update ID.
|
||||||
status_update_serial: u32,
|
status_update_serial: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Data received over an ephemeral peer channel.
|
/// Data received over an ephemeral peer channel.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
|
WebxdcRealtimeData {
|
||||||
|
/// Message ID.
|
||||||
|
msg_id: u32,
|
||||||
|
|
||||||
|
/// Realtime data.
|
||||||
|
data: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Advertisement received over an ephemeral peer channel.
|
/// Advertisement received over an ephemeral peer channel.
|
||||||
/// This can be used by bots to initiate peer-to-peer communication from their side.
|
/// This can be used by bots to initiate peer-to-peer communication from their side.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcRealtimeAdvertisementReceived { msg_id: u32 },
|
WebxdcRealtimeAdvertisementReceived {
|
||||||
|
/// Message ID of the webxdc instance.
|
||||||
|
msg_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
/// 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 { msg_id: u32 },
|
WebxdcInstanceDeleted {
|
||||||
|
/// ID of the deleted message.
|
||||||
|
msg_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
/// Tells that the Background fetch was completed (or timed out).
|
/// 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
|
/// This event acts as a marker, when you reach this event you can be sure
|
||||||
@@ -285,7 +392,10 @@ pub enum EventType {
|
|||||||
/// Inform that a single chat list item changed and needs to be rerendered.
|
/// 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.
|
/// 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")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatlistItemChanged { chat_id: Option<u32> },
|
ChatlistItemChanged {
|
||||||
|
/// ID of the changed chat
|
||||||
|
chat_id: Option<u32>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
|
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
|
||||||
///
|
///
|
||||||
@@ -302,7 +412,10 @@ pub enum EventType {
|
|||||||
AccountsItemChanged,
|
AccountsItemChanged,
|
||||||
|
|
||||||
/// Inform than some events have been skipped due to event channel overflow.
|
/// Inform than some events have been skipped due to event channel overflow.
|
||||||
EventChannelOverflow { n: u64 },
|
EventChannelOverflow {
|
||||||
|
/// Number of events skipped.
|
||||||
|
n: u64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CoreEventType> for EventType {
|
impl From<CoreEventType> for EventType {
|
||||||
@@ -335,10 +448,12 @@ impl From<CoreEventType> for EventType {
|
|||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
},
|
},
|
||||||
CoreEventType::IncomingReaction {
|
CoreEventType::IncomingReaction {
|
||||||
|
chat_id,
|
||||||
contact_id,
|
contact_id,
|
||||||
msg_id,
|
msg_id,
|
||||||
reaction,
|
reaction,
|
||||||
} => IncomingReaction {
|
} => IncomingReaction {
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
reaction: reaction.as_str().to_string(),
|
reaction: reaction.as_str().to_string(),
|
||||||
@@ -389,6 +504,9 @@ impl From<CoreEventType> for EventType {
|
|||||||
timer: timer.to_u32(),
|
timer: timer.to_u32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CoreEventType::ChatDeleted { chat_id } => ChatDeleted {
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
|
},
|
||||||
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
||||||
contact_id: contact.map(|c| c.to_u32()),
|
contact_id: contact.map(|c| c.to_u32()),
|
||||||
},
|
},
|
||||||
|
|||||||
203
deltachat-jsonrpc/src/api/types/login_param.rs
Normal file
203
deltachat-jsonrpc/src/api/types/login_param.rs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use deltachat::login_param as dc;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use yerpc::TypeDef;
|
||||||
|
|
||||||
|
/// Login parameters entered by the user.
|
||||||
|
///
|
||||||
|
/// Usually it will be enough to only set `addr` and `password`,
|
||||||
|
/// and all the other settings will be autoconfigured.
|
||||||
|
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EnteredLoginParam {
|
||||||
|
/// Email address.
|
||||||
|
pub addr: String,
|
||||||
|
|
||||||
|
/// Password.
|
||||||
|
pub password: String,
|
||||||
|
|
||||||
|
/// Imap server hostname or IP address.
|
||||||
|
pub imap_server: Option<String>,
|
||||||
|
|
||||||
|
/// Imap server port.
|
||||||
|
pub imap_port: Option<u16>,
|
||||||
|
|
||||||
|
/// Imap socket security.
|
||||||
|
pub imap_security: Option<Socket>,
|
||||||
|
|
||||||
|
/// Imap username.
|
||||||
|
pub imap_user: Option<String>,
|
||||||
|
|
||||||
|
/// SMTP server hostname or IP address.
|
||||||
|
pub smtp_server: Option<String>,
|
||||||
|
|
||||||
|
/// SMTP server port.
|
||||||
|
pub smtp_port: Option<u16>,
|
||||||
|
|
||||||
|
/// SMTP socket security.
|
||||||
|
pub smtp_security: Option<Socket>,
|
||||||
|
|
||||||
|
/// SMTP username.
|
||||||
|
pub smtp_user: Option<String>,
|
||||||
|
|
||||||
|
/// SMTP Password.
|
||||||
|
///
|
||||||
|
/// Only needs to be specified if different than IMAP password.
|
||||||
|
pub smtp_password: Option<String>,
|
||||||
|
|
||||||
|
/// TLS options: whether to allow invalid certificates and/or
|
||||||
|
/// invalid hostnames.
|
||||||
|
/// Default: Automatic
|
||||||
|
pub certificate_checks: Option<EnteredCertificateChecks>,
|
||||||
|
|
||||||
|
/// If true, login via OAUTH2 (not recommended anymore).
|
||||||
|
/// Default: false
|
||||||
|
pub oauth2: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||||
|
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||||
|
let imap_security: Socket = param.imap.security.into();
|
||||||
|
let smtp_security: Socket = param.smtp.security.into();
|
||||||
|
let certificate_checks: EnteredCertificateChecks = param.certificate_checks.into();
|
||||||
|
Self {
|
||||||
|
addr: param.addr,
|
||||||
|
password: param.imap.password,
|
||||||
|
imap_server: param.imap.server.into_option(),
|
||||||
|
imap_port: param.imap.port.into_option(),
|
||||||
|
imap_security: imap_security.into_option(),
|
||||||
|
imap_user: param.imap.user.into_option(),
|
||||||
|
smtp_server: param.smtp.server.into_option(),
|
||||||
|
smtp_port: param.smtp.port.into_option(),
|
||||||
|
smtp_security: smtp_security.into_option(),
|
||||||
|
smtp_user: param.smtp.user.into_option(),
|
||||||
|
smtp_password: param.smtp.password.into_option(),
|
||||||
|
certificate_checks: certificate_checks.into_option(),
|
||||||
|
oauth2: param.oauth2.into_option(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
addr: param.addr,
|
||||||
|
imap: dc::EnteredServerLoginParam {
|
||||||
|
server: param.imap_server.unwrap_or_default(),
|
||||||
|
port: param.imap_port.unwrap_or_default(),
|
||||||
|
security: param.imap_security.unwrap_or_default().into(),
|
||||||
|
user: param.imap_user.unwrap_or_default(),
|
||||||
|
password: param.password,
|
||||||
|
},
|
||||||
|
smtp: dc::EnteredServerLoginParam {
|
||||||
|
server: param.smtp_server.unwrap_or_default(),
|
||||||
|
port: param.smtp_port.unwrap_or_default(),
|
||||||
|
security: param.smtp_security.unwrap_or_default().into(),
|
||||||
|
user: param.smtp_user.unwrap_or_default(),
|
||||||
|
password: param.smtp_password.unwrap_or_default(),
|
||||||
|
},
|
||||||
|
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
||||||
|
oauth2: param.oauth2.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum Socket {
|
||||||
|
/// Unspecified socket security, select automatically.
|
||||||
|
#[default]
|
||||||
|
Automatic,
|
||||||
|
|
||||||
|
/// TLS connection.
|
||||||
|
Ssl,
|
||||||
|
|
||||||
|
/// STARTTLS connection.
|
||||||
|
Starttls,
|
||||||
|
|
||||||
|
/// No TLS, plaintext connection.
|
||||||
|
Plain,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<dc::Socket> for Socket {
|
||||||
|
fn from(value: dc::Socket) -> Self {
|
||||||
|
match value {
|
||||||
|
dc::Socket::Automatic => Self::Automatic,
|
||||||
|
dc::Socket::Ssl => Self::Ssl,
|
||||||
|
dc::Socket::Starttls => Self::Starttls,
|
||||||
|
dc::Socket::Plain => Self::Plain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Socket> for dc::Socket {
|
||||||
|
fn from(value: Socket) -> Self {
|
||||||
|
match value {
|
||||||
|
Socket::Automatic => Self::Automatic,
|
||||||
|
Socket::Ssl => Self::Ssl,
|
||||||
|
Socket::Starttls => Self::Starttls,
|
||||||
|
Socket::Plain => Self::Plain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum EnteredCertificateChecks {
|
||||||
|
/// `Automatic` means that provider database setting should be taken.
|
||||||
|
/// If there is no provider database setting for certificate checks,
|
||||||
|
/// check certificates strictly.
|
||||||
|
#[default]
|
||||||
|
Automatic,
|
||||||
|
|
||||||
|
/// Ensure that TLS certificate is valid for the server hostname.
|
||||||
|
Strict,
|
||||||
|
|
||||||
|
/// Accept certificates that are expired, self-signed
|
||||||
|
/// or otherwise not valid for the server hostname.
|
||||||
|
AcceptInvalidCertificates,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<dc::EnteredCertificateChecks> for EnteredCertificateChecks {
|
||||||
|
fn from(value: dc::EnteredCertificateChecks) -> Self {
|
||||||
|
match value {
|
||||||
|
dc::EnteredCertificateChecks::Automatic => Self::Automatic,
|
||||||
|
dc::EnteredCertificateChecks::Strict => Self::Strict,
|
||||||
|
dc::EnteredCertificateChecks::AcceptInvalidCertificates => {
|
||||||
|
Self::AcceptInvalidCertificates
|
||||||
|
}
|
||||||
|
dc::EnteredCertificateChecks::AcceptInvalidCertificates2 => {
|
||||||
|
Self::AcceptInvalidCertificates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EnteredCertificateChecks> for dc::EnteredCertificateChecks {
|
||||||
|
fn from(value: EnteredCertificateChecks) -> Self {
|
||||||
|
match value {
|
||||||
|
EnteredCertificateChecks::Automatic => Self::Automatic,
|
||||||
|
EnteredCertificateChecks::Strict => Self::Strict,
|
||||||
|
EnteredCertificateChecks::AcceptInvalidCertificates => Self::AcceptInvalidCertificates,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait IntoOption<T> {
|
||||||
|
fn into_option(self) -> Option<T>;
|
||||||
|
}
|
||||||
|
impl<T> IntoOption<T> for T
|
||||||
|
where
|
||||||
|
T: Default + std::cmp::PartialEq,
|
||||||
|
{
|
||||||
|
fn into_option(self) -> Option<T> {
|
||||||
|
if self == T::default() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::api::VcardContact;
|
use crate::api::VcardContact;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use deltachat::chat::Chat;
|
use deltachat::chat::Chat;
|
||||||
@@ -17,10 +19,10 @@ use typescript_type_def::TypeDef;
|
|||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::contact::ContactObject;
|
use super::contact::ContactObject;
|
||||||
use super::reactions::JSONRPCReactions;
|
use super::reactions::JSONRPCReactions;
|
||||||
use super::webxdc::WebxdcMessageInfo;
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
|
#[expect(clippy::large_enum_variant)]
|
||||||
pub enum MessageLoadResult {
|
pub enum MessageLoadResult {
|
||||||
Message(MessageObject),
|
Message(MessageObject),
|
||||||
LoadingError { error: String },
|
LoadingError { error: String },
|
||||||
@@ -37,6 +39,8 @@ pub struct MessageObject {
|
|||||||
|
|
||||||
text: String,
|
text: String,
|
||||||
|
|
||||||
|
is_edited: bool,
|
||||||
|
|
||||||
/// Check if a message has a POI location bound to it.
|
/// Check if a message has a POI location bound to it.
|
||||||
/// These locations are also returned by `get_locations` method.
|
/// These locations are also returned by `get_locations` method.
|
||||||
/// The UI may decide to display a special icon beside such messages.
|
/// The UI may decide to display a special icon beside such messages.
|
||||||
@@ -55,6 +59,13 @@ pub struct MessageObject {
|
|||||||
|
|
||||||
// summary - use/create another function if you need it
|
// summary - use/create another function if you need it
|
||||||
subject: String,
|
subject: String,
|
||||||
|
|
||||||
|
/// True if the message was correctly encrypted&signed, false otherwise.
|
||||||
|
/// Historically, UIs showed a small padlock on the message then.
|
||||||
|
///
|
||||||
|
/// Today, the UIs should instead show a small email-icon on the message
|
||||||
|
/// if `show_padlock` is `false`,
|
||||||
|
/// and nothing if it is `true`.
|
||||||
show_padlock: bool,
|
show_padlock: bool,
|
||||||
is_setupmessage: bool,
|
is_setupmessage: bool,
|
||||||
is_info: bool,
|
is_info: bool,
|
||||||
@@ -66,6 +77,9 @@ pub struct MessageObject {
|
|||||||
/// when is_info is true this describes what type of system message it is
|
/// when is_info is true this describes what type of system message it is
|
||||||
system_message_type: SystemMessageType,
|
system_message_type: SystemMessageType,
|
||||||
|
|
||||||
|
/// if is_info is set, this refers to the contact profile that should be opened when the info message is tapped.
|
||||||
|
info_contact_id: Option<u32>,
|
||||||
|
|
||||||
duration: i32,
|
duration: i32,
|
||||||
dimensions_height: i32,
|
dimensions_height: i32,
|
||||||
dimensions_width: i32,
|
dimensions_width: i32,
|
||||||
@@ -83,12 +97,14 @@ pub struct MessageObject {
|
|||||||
file_bytes: u64,
|
file_bytes: u64,
|
||||||
file_name: Option<String>,
|
file_name: Option<String>,
|
||||||
|
|
||||||
webxdc_info: Option<WebxdcMessageInfo>,
|
|
||||||
|
|
||||||
webxdc_href: Option<String>,
|
webxdc_href: Option<String>,
|
||||||
|
|
||||||
download_state: DownloadState,
|
download_state: DownloadState,
|
||||||
|
|
||||||
|
original_msg_id: Option<u32>,
|
||||||
|
|
||||||
|
saved_message_id: Option<u32>,
|
||||||
|
|
||||||
reactions: Option<JSONRPCReactions>,
|
reactions: Option<JSONRPCReactions>,
|
||||||
|
|
||||||
vcard_contact: Option<VcardContact>,
|
vcard_contact: Option<VcardContact>,
|
||||||
@@ -104,6 +120,9 @@ enum MessageQuote {
|
|||||||
WithMessage {
|
WithMessage {
|
||||||
text: String,
|
text: String,
|
||||||
message_id: u32,
|
message_id: u32,
|
||||||
|
/// The quoted message does not always belong
|
||||||
|
/// to the same chat, e.g. when "Reply Privately" is used.
|
||||||
|
chat_id: u32,
|
||||||
author_display_name: String,
|
author_display_name: String,
|
||||||
author_display_color: String,
|
author_display_color: String,
|
||||||
override_sender_name: Option<String>,
|
override_sender_name: Option<String>,
|
||||||
@@ -128,12 +147,6 @@ impl MessageObject {
|
|||||||
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
||||||
let override_sender_name = message.get_override_sender_name();
|
let override_sender_name = message.get_override_sender_name();
|
||||||
|
|
||||||
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
|
|
||||||
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
||||||
|
|
||||||
let download_state = message.download_state().into();
|
let download_state = message.download_state().into();
|
||||||
@@ -147,6 +160,7 @@ impl MessageObject {
|
|||||||
Some(MessageQuote::WithMessage {
|
Some(MessageQuote::WithMessage {
|
||||||
text: quoted_text,
|
text: quoted_text,
|
||||||
message_id: quote.get_id().to_u32(),
|
message_id: quote.get_id().to_u32(),
|
||||||
|
chat_id: quote.get_chat_id().to_u32(),
|
||||||
author_display_name: quote_author.get_display_name().to_owned(),
|
author_display_name: quote_author.get_display_name().to_owned(),
|
||||||
author_display_color: color_int_to_hex_string(quote_author.get_color()),
|
author_display_color: color_int_to_hex_string(quote_author.get_color()),
|
||||||
override_sender_name: quote.get_override_sender_name(),
|
override_sender_name: quote.get_override_sender_name(),
|
||||||
@@ -194,6 +208,7 @@ impl MessageObject {
|
|||||||
quote,
|
quote,
|
||||||
parent_id,
|
parent_id,
|
||||||
text: message.get_text(),
|
text: message.get_text(),
|
||||||
|
is_edited: message.is_edited(),
|
||||||
has_location: message.has_location(),
|
has_location: message.has_location(),
|
||||||
has_html: message.has_html(),
|
has_html: message.has_html(),
|
||||||
view_type: message.get_viewtype().into(),
|
view_type: message.get_viewtype().into(),
|
||||||
@@ -215,6 +230,10 @@ impl MessageObject {
|
|||||||
is_forwarded: message.is_forwarded(),
|
is_forwarded: message.is_forwarded(),
|
||||||
is_bot: message.is_bot(),
|
is_bot: message.is_bot(),
|
||||||
system_message_type: message.get_info_type().into(),
|
system_message_type: message.get_info_type().into(),
|
||||||
|
info_contact_id: message
|
||||||
|
.get_info_contact_id(context)
|
||||||
|
.await?
|
||||||
|
.map(|id| id.to_u32()),
|
||||||
|
|
||||||
duration: message.get_duration(),
|
duration: message.get_duration(),
|
||||||
dimensions_height: message.get_height(),
|
dimensions_height: message.get_height(),
|
||||||
@@ -241,7 +260,6 @@ impl MessageObject {
|
|||||||
file_mime: message.get_filemime(),
|
file_mime: message.get_filemime(),
|
||||||
file_bytes,
|
file_bytes,
|
||||||
file_name: message.get_filename(),
|
file_name: message.get_filename(),
|
||||||
webxdc_info,
|
|
||||||
|
|
||||||
// On a WebxdcInfoMessage this might include a hash holding
|
// On a WebxdcInfoMessage this might include a hash holding
|
||||||
// information about a specific position or state in a webxdc app
|
// information about a specific position or state in a webxdc app
|
||||||
@@ -249,6 +267,16 @@ impl MessageObject {
|
|||||||
|
|
||||||
download_state,
|
download_state,
|
||||||
|
|
||||||
|
original_msg_id: message
|
||||||
|
.get_original_msg_id(context)
|
||||||
|
.await?
|
||||||
|
.map(|id| id.to_u32()),
|
||||||
|
|
||||||
|
saved_message_id: message
|
||||||
|
.get_saved_msg_id(context)
|
||||||
|
.await?
|
||||||
|
.map(|id| id.to_u32()),
|
||||||
|
|
||||||
reactions,
|
reactions,
|
||||||
|
|
||||||
vcard_contact: vcard_contacts.first().cloned(),
|
vcard_contact: vcard_contacts.first().cloned(),
|
||||||
@@ -388,6 +416,9 @@ pub enum SystemMessageType {
|
|||||||
/// Chat ephemeral message timer is changed.
|
/// Chat ephemeral message timer is changed.
|
||||||
EphemeralTimerChanged,
|
EphemeralTimerChanged,
|
||||||
|
|
||||||
|
// Chat is e2ee
|
||||||
|
ChatE2ee,
|
||||||
|
|
||||||
// Chat protection state changed
|
// Chat protection state changed
|
||||||
ChatProtectionEnabled,
|
ChatProtectionEnabled,
|
||||||
ChatProtectionDisabled,
|
ChatProtectionDisabled,
|
||||||
@@ -422,6 +453,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||||
|
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
|
||||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||||
@@ -589,6 +621,7 @@ pub struct MessageData {
|
|||||||
pub html: Option<String>,
|
pub html: Option<String>,
|
||||||
pub viewtype: Option<MessageViewtype>,
|
pub viewtype: Option<MessageViewtype>,
|
||||||
pub file: Option<String>,
|
pub file: Option<String>,
|
||||||
|
pub filename: Option<String>,
|
||||||
pub location: Option<(f64, f64)>,
|
pub location: Option<(f64, f64)>,
|
||||||
pub override_sender_name: Option<String>,
|
pub override_sender_name: Option<String>,
|
||||||
/// Quoted message id. Takes preference over `quoted_text` (see below).
|
/// Quoted message id. Takes preference over `quoted_text` (see below).
|
||||||
@@ -613,7 +646,12 @@ impl MessageData {
|
|||||||
message.set_override_sender_name(self.override_sender_name);
|
message.set_override_sender_name(self.override_sender_name);
|
||||||
}
|
}
|
||||||
if let Some(file) = self.file {
|
if let Some(file) = self.file {
|
||||||
message.set_file(file, None);
|
message.set_file_and_deduplicate(
|
||||||
|
context,
|
||||||
|
Path::new(&file),
|
||||||
|
self.filename.as_deref(),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
if let Some((latitude, longitude)) = self.location {
|
if let Some((latitude, longitude)) = self.location {
|
||||||
message.set_location(latitude, longitude);
|
message.set_location(latitude, longitude);
|
||||||
@@ -644,7 +682,6 @@ pub struct MessageReadReceipt {
|
|||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageInfo {
|
pub struct MessageInfo {
|
||||||
rawtext: String,
|
|
||||||
ephemeral_timer: EphemeralTimer,
|
ephemeral_timer: EphemeralTimer,
|
||||||
/// When message is ephemeral this contains the timestamp of the message expiry
|
/// When message is ephemeral this contains the timestamp of the message expiry
|
||||||
ephemeral_timestamp: Option<i64>,
|
ephemeral_timestamp: Option<i64>,
|
||||||
@@ -657,7 +694,6 @@ pub struct MessageInfo {
|
|||||||
impl MessageInfo {
|
impl MessageInfo {
|
||||||
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?;
|
||||||
let rawtext = msg_id.rawtext(context).await?;
|
|
||||||
let ephemeral_timer = message.get_ephemeral_timer().into();
|
let ephemeral_timer = message.get_ephemeral_timer().into();
|
||||||
let ephemeral_timestamp = match message.get_ephemeral_timer() {
|
let ephemeral_timestamp = match message.get_ephemeral_timer() {
|
||||||
deltachat::ephemeral::Timer::Disabled => None,
|
deltachat::ephemeral::Timer::Disabled => None,
|
||||||
@@ -670,7 +706,6 @@ impl MessageInfo {
|
|||||||
let hop_info = msg_id.hop_info(context).await?;
|
let hop_info = msg_id.hop_info(context).await?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
rawtext,
|
|
||||||
ephemeral_timer,
|
ephemeral_timer,
|
||||||
ephemeral_timestamp,
|
ephemeral_timestamp,
|
||||||
error: message.error(),
|
error: message.error(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod contact;
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod location;
|
pub mod location;
|
||||||
|
pub mod login_param;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod provider_info;
|
pub mod provider_info;
|
||||||
pub mod qr;
|
pub mod qr;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ pub enum QrObject {
|
|||||||
/// Iroh node address.
|
/// Iroh node address.
|
||||||
node_addr: String,
|
node_addr: String,
|
||||||
},
|
},
|
||||||
|
BackupTooNew {},
|
||||||
/// Ask the user if they want to use the given service for video chats.
|
/// Ask the user if they want to use the given service for video chats.
|
||||||
WebrtcInstance {
|
WebrtcInstance {
|
||||||
domain: String,
|
domain: String,
|
||||||
@@ -100,11 +101,15 @@ pub enum QrObject {
|
|||||||
/// URL scanned.
|
/// URL scanned.
|
||||||
///
|
///
|
||||||
/// Ask the user if they want to open a browser or copy the URL to clipboard.
|
/// Ask the user if they want to open a browser or copy the URL to clipboard.
|
||||||
Url { url: String },
|
Url {
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
/// Text scanned.
|
/// Text scanned.
|
||||||
///
|
///
|
||||||
/// Ask the user if they want to copy the text to clipboard.
|
/// Ask the user if they want to copy the text to clipboard.
|
||||||
Text { text: String },
|
Text {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
/// Ask the user if they want to withdraw their own QR code.
|
/// Ask the user if they want to withdraw their own QR code.
|
||||||
WithdrawVerifyContact {
|
WithdrawVerifyContact {
|
||||||
/// Contact ID.
|
/// Contact ID.
|
||||||
@@ -160,7 +165,9 @@ pub enum QrObject {
|
|||||||
/// `dclogin:` scheme parameters.
|
/// `dclogin:` scheme parameters.
|
||||||
///
|
///
|
||||||
/// Ask the user if they want to login with the email address.
|
/// Ask the user if they want to login with the email address.
|
||||||
Login { address: String },
|
Login {
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Qr> for QrObject {
|
impl From<Qr> for QrObject {
|
||||||
@@ -217,6 +224,7 @@ impl From<Qr> for QrObject {
|
|||||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
||||||
auth_token,
|
auth_token,
|
||||||
},
|
},
|
||||||
|
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
||||||
Qr::WebrtcInstance {
|
Qr::WebrtcInstance {
|
||||||
domain,
|
domain,
|
||||||
instance_pattern,
|
instance_pattern,
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
|
|
||||||
use yerpc::axum::handle_ws_rpc;
|
|
||||||
use yerpc::{RpcClient, RpcSession};
|
|
||||||
|
|
||||||
mod api;
|
|
||||||
use api::{Accounts, CommandApi};
|
|
||||||
|
|
||||||
const DEFAULT_PORT: u16 = 20808;
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
|
||||||
async fn main() -> Result<(), std::io::Error> {
|
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
|
||||||
|
|
||||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "./accounts".to_string());
|
|
||||||
let port = std::env::var("DC_PORT")
|
|
||||||
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
|
||||||
.unwrap_or(DEFAULT_PORT);
|
|
||||||
log::info!("Starting with accounts directory `{path}`.");
|
|
||||||
let writable = true;
|
|
||||||
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
|
|
||||||
let state = CommandApi::new(accounts);
|
|
||||||
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/ws", get(handler))
|
|
||||||
.layer(Extension(state.clone()));
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
state.accounts.write().await.start_io().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
|
||||||
log::info!("JSON-RPC WebSocket server listening on {}", addr);
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
|
||||||
let (client, out_receiver) = RpcClient::new();
|
|
||||||
let session = RpcSession::new(client.clone(), api.clone());
|
|
||||||
handle_ws_rpc(ws, out_receiver, session).await
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>DeltaChat JSON-RPC example</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: monospace;
|
|
||||||
background: black;
|
|
||||||
color: grey;
|
|
||||||
}
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 3fr 1fr;
|
|
||||||
grid-template-areas: "a a" "b c";
|
|
||||||
}
|
|
||||||
.message {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
#header {
|
|
||||||
grid-area: a;
|
|
||||||
color: white;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
#header a {
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
#main {
|
|
||||||
grid-area: b;
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
#main h2,
|
|
||||||
#main h3 {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
#side {
|
|
||||||
grid-area: c;
|
|
||||||
color: #777;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script type="module" src="dist/example.bundle.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>DeltaChat JSON-RPC example</h1>
|
|
||||||
<div class="grid">
|
|
||||||
<div id="header"></div>
|
|
||||||
<div id="main"></div>
|
|
||||||
<div id="side"><h2>log</h2></div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Tip: open the dev console and use the client with
|
|
||||||
<code>window.client</code>
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { DcEvent, DeltaChat } from "../deltachat.js";
|
|
||||||
|
|
||||||
var SELECTED_ACCOUNT = 0;
|
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", (_event) => {
|
|
||||||
(window as any).selectDeltaAccount = (id: string) => {
|
|
||||||
SELECTED_ACCOUNT = Number(id);
|
|
||||||
window.dispatchEvent(new Event("account-changed"));
|
|
||||||
};
|
|
||||||
console.log("launch run script...");
|
|
||||||
run().catch((err) => console.error("run failed", err));
|
|
||||||
});
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const $main = document.getElementById("main")!;
|
|
||||||
const $side = document.getElementById("side")!;
|
|
||||||
const $head = document.getElementById("header")!;
|
|
||||||
|
|
||||||
const client = new DeltaChat("ws://localhost:20808/ws");
|
|
||||||
|
|
||||||
(window as any).client = client.rpc;
|
|
||||||
|
|
||||||
client.on("ALL", (accountId, event) => {
|
|
||||||
onIncomingEvent(accountId, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("account-changed", async (_event: Event) => {
|
|
||||||
listChatsForSelectedAccount();
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
|
|
||||||
|
|
||||||
async function loadAccountsInHeader() {
|
|
||||||
console.log("load accounts");
|
|
||||||
const accounts = await client.rpc.getAllAccounts();
|
|
||||||
console.log("accounts loaded", accounts);
|
|
||||||
for (const account of accounts) {
|
|
||||||
if (account.kind === "Configured") {
|
|
||||||
write(
|
|
||||||
$head,
|
|
||||||
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
|
||||||
${account.id}: ${account.addr!}
|
|
||||||
</a> `
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
write(
|
|
||||||
$head,
|
|
||||||
`<a href="#">
|
|
||||||
${account.id}: (unconfigured)
|
|
||||||
</a> `
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listChatsForSelectedAccount() {
|
|
||||||
clear($main);
|
|
||||||
const selectedAccount = SELECTED_ACCOUNT;
|
|
||||||
const info = await client.rpc.getAccountInfo(selectedAccount);
|
|
||||||
if (info.kind !== "Configured") {
|
|
||||||
return write($main, "Account is not configured");
|
|
||||||
}
|
|
||||||
write($main, `<h2>${info.addr!}</h2>`);
|
|
||||||
const chats = await client.rpc.getChatlistEntries(
|
|
||||||
selectedAccount,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
for (const chatId of chats) {
|
|
||||||
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
|
|
||||||
write($main, `<h3>${chat.name}</h3>`);
|
|
||||||
const messageIds = await client.rpc.getMessageIds(
|
|
||||||
selectedAccount,
|
|
||||||
chatId,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
const messages = await client.rpc.getMessages(
|
|
||||||
selectedAccount,
|
|
||||||
messageIds
|
|
||||||
);
|
|
||||||
for (const [_messageId, message] of Object.entries(messages)) {
|
|
||||||
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
|
|
||||||
else write($main, `<p>loading error: ${message.error}</p>`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onIncomingEvent(accountId: number, event: DcEvent) {
|
|
||||||
write(
|
|
||||||
$side,
|
|
||||||
`
|
|
||||||
<p class="message">
|
|
||||||
[<strong>${event.kind}</strong> on account ${accountId}]<br>
|
|
||||||
<em>f1:</em> ${JSON.stringify(
|
|
||||||
Object.assign({}, event, { kind: undefined })
|
|
||||||
)}
|
|
||||||
</p>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function write(el: HTMLElement, html: string) {
|
|
||||||
el.innerHTML += html;
|
|
||||||
}
|
|
||||||
function clear(el: HTMLElement) {
|
|
||||||
el.innerHTML = "";
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { DeltaChat } from "../dist/deltachat.js";
|
|
||||||
|
|
||||||
run().catch(console.error);
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const delta = new DeltaChat("ws://localhost:20808/ws");
|
|
||||||
delta.on("event", (event) => {
|
|
||||||
console.log("event", event.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = process.argv[2];
|
|
||||||
const password = process.argv[3];
|
|
||||||
if (!email || !password)
|
|
||||||
throw new Error(
|
|
||||||
"USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>"
|
|
||||||
);
|
|
||||||
console.log(`creating account for ${email}`);
|
|
||||||
const id = await delta.rpc.addAccount();
|
|
||||||
console.log(`created account id ${id}`);
|
|
||||||
await delta.rpc.setConfig(id, "addr", email);
|
|
||||||
await delta.rpc.setConfig(id, "mail_pw", password);
|
|
||||||
console.log("configuration updated");
|
|
||||||
await delta.rpc.configure(id);
|
|
||||||
console.log("account configured!");
|
|
||||||
|
|
||||||
const accounts = await delta.rpc.getAllAccounts();
|
|
||||||
console.log("accounts", accounts);
|
|
||||||
console.log("waiting for events...");
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { DeltaChat } from "../dist/deltachat.js";
|
|
||||||
|
|
||||||
run().catch(console.error);
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const delta = new DeltaChat();
|
|
||||||
delta.on("event", (event) => {
|
|
||||||
console.log("event", event.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
const accounts = await delta.rpc.getAllAccounts();
|
|
||||||
console.log("accounts", accounts);
|
|
||||||
console.log("waiting for events...");
|
|
||||||
}
|
|
||||||
@@ -2,24 +2,24 @@
|
|||||||
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@deltachat/tiny-emitter": "3.0.0",
|
"@deltachat/tiny-emitter": "3.0.0",
|
||||||
"isomorphic-ws": "^4.0.1",
|
"isomorphic-ws": "^5.0.0",
|
||||||
"yerpc": "^0.6.2"
|
"yerpc": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.21",
|
"@types/chai": "^4.3.10",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.8",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^10.0.4",
|
||||||
"@types/ws": "^7.2.4",
|
"@types/ws": "^8.5.9",
|
||||||
"c8": "^7.10.0",
|
"c8": "^8.0.1",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"esbuild": "^0.17.9",
|
"esbuild": "^0.25.5",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"mocha": "^9.1.1",
|
"mocha": "^10.2.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^3.5.3",
|
||||||
"typedoc": "^0.23.2",
|
"typedoc": "^0.28.5",
|
||||||
"typescript": "^4.5.5",
|
"typescript": "^5.8.3",
|
||||||
"ws": "^8.5.0"
|
"ws": "^8.5.0"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"name": "@deltachat/jsonrpc-client",
|
"name": "@deltachat/jsonrpc-client",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
"url": "https://github.com/chatmail/core.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",
|
||||||
@@ -42,10 +42,6 @@
|
|||||||
"build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs",
|
"build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs",
|
||||||
"build:tsc": "tsc",
|
"build:tsc": "tsc",
|
||||||
"docs": "typedoc --out docs deltachat.ts",
|
"docs": "typedoc --out docs deltachat.ts",
|
||||||
"example": "run-s build example:build example:start",
|
|
||||||
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
|
|
||||||
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
|
||||||
"example:start": "http-server .",
|
|
||||||
"extract-constants": "node ./scripts/generate-constants.js",
|
"extract-constants": "node ./scripts/generate-constants.js",
|
||||||
"generate-bindings": "cargo test",
|
"generate-bindings": "cargo test",
|
||||||
"prettier:check": "prettier --check .",
|
"prettier:check": "prettier --check .",
|
||||||
@@ -58,5 +54,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "1.152.2"
|
"version": "2.11.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,24 @@ const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
|
|||||||
const jsonCoverage =
|
const jsonCoverage =
|
||||||
json[Object.keys(json).find((k) => k.includes(generatedFile))];
|
json[Object.keys(json).find((k) => k.includes(generatedFile))];
|
||||||
const fnMap = Object.keys(jsonCoverage.fnMap).map(
|
const fnMap = Object.keys(jsonCoverage.fnMap).map(
|
||||||
(key) => jsonCoverage.fnMap[key]
|
(key) => jsonCoverage.fnMap[key],
|
||||||
);
|
);
|
||||||
const htmlCoverage = readFileSync(
|
const htmlCoverage = readFileSync(
|
||||||
"./coverage/" + generatedFile + ".html",
|
"./coverage/" + generatedFile + ".html",
|
||||||
"utf8"
|
"utf8",
|
||||||
);
|
);
|
||||||
const uncoveredLines = htmlCoverage
|
const uncoveredLines = htmlCoverage
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes(`"function not covered"`));
|
.filter((line) => line.includes(`"function not covered"`));
|
||||||
const uncoveredFunctions = uncoveredLines.map(
|
const uncoveredFunctions = uncoveredLines.map(
|
||||||
(line) => />([\w_]+)\(/.exec(line)[1]
|
(line) => />([\w_]+)\(/.exec(line)[1],
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
"\nUncovered api functions:\n" +
|
"\nUncovered api functions:\n" +
|
||||||
uncoveredFunctions
|
uncoveredFunctions
|
||||||
.map((uF) => fnMap.find(({ name }) => name === uF))
|
.map((uF) => fnMap.find(({ name }) => name === uF))
|
||||||
.map(
|
.map(
|
||||||
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
|
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`,
|
||||||
)
|
)
|
||||||
.join("\n")
|
.join("\n"),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ while (null != (match = regex.exec(header_data))) {
|
|||||||
|
|
||||||
const constants = data
|
const constants = data
|
||||||
.filter(
|
.filter(
|
||||||
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
({ key }) => key.toUpperCase()[0] === key[0], // check if define name is uppercase
|
||||||
)
|
)
|
||||||
.sort((lhs, rhs) => {
|
.sort((lhs, rhs) => {
|
||||||
if (lhs.key < rhs.key) return -1;
|
if (lhs.key < rhs.key) return -1;
|
||||||
@@ -50,5 +50,5 @@ const constants = data
|
|||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
resolve(__dirname, "../generated/constants.ts"),
|
resolve(__dirname, "../generated/constants.ts"),
|
||||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
|
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import * as T from "../generated/types.js";
|
|||||||
import { EventType } from "../generated/types.js";
|
import { EventType } from "../generated/types.js";
|
||||||
import * as RPC from "../generated/jsonrpc.js";
|
import * as RPC from "../generated/jsonrpc.js";
|
||||||
import { RawClient } from "../generated/client.js";
|
import { RawClient } from "../generated/client.js";
|
||||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
import { 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["kind"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
accountId: number,
|
accountId: number,
|
||||||
event: Extract<EventType, { kind: Property }>
|
event: Extract<EventType, { kind: Property }>,
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||||
[Property in EventType["kind"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
event: Extract<EventType, { kind: Property }>
|
event: Extract<EventType, { kind: Property }>,
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export type DcEventType<T extends EventType["kind"]> = Extract<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export class BaseDeltaChat<
|
export class BaseDeltaChat<
|
||||||
Transport extends BaseTransport<any>
|
Transport extends BaseTransport<any>,
|
||||||
> extends TinyEmitter<Events> {
|
> extends TinyEmitter<Events> {
|
||||||
rpc: RawClient;
|
rpc: RawClient;
|
||||||
account?: T.Account;
|
account?: T.Account;
|
||||||
@@ -34,7 +34,10 @@ export class BaseDeltaChat<
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
private eventTask: Promise<void>;
|
private eventTask: Promise<void>;
|
||||||
|
|
||||||
constructor(public transport: Transport, startEventLoop: boolean) {
|
constructor(
|
||||||
|
public transport: Transport,
|
||||||
|
startEventLoop: boolean,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.rpc = new RawClient(this.transport);
|
this.rpc = new RawClient(this.transport);
|
||||||
if (startEventLoop) {
|
if (startEventLoop) {
|
||||||
@@ -53,7 +56,7 @@ export class BaseDeltaChat<
|
|||||||
this.contextEmitters[event.contextId].emit(
|
this.contextEmitters[event.contextId].emit(
|
||||||
event.event.kind,
|
event.event.kind,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
event.event as any
|
event.event as any,
|
||||||
);
|
);
|
||||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||||
}
|
}
|
||||||
@@ -74,34 +77,6 @@ export class BaseDeltaChat<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Opts = {
|
|
||||||
url: string;
|
|
||||||
startEventLoop: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_OPTS: Opts = {
|
|
||||||
url: "ws://localhost:20808/ws",
|
|
||||||
startEventLoop: true,
|
|
||||||
};
|
|
||||||
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
|
||||||
opts: Opts;
|
|
||||||
close() {
|
|
||||||
this.transport.close();
|
|
||||||
}
|
|
||||||
constructor(opts?: Opts | string) {
|
|
||||||
if (typeof opts === "string") {
|
|
||||||
opts = { ...DEFAULT_OPTS, url: opts };
|
|
||||||
} else if (opts) {
|
|
||||||
opts = { ...DEFAULT_OPTS, ...opts };
|
|
||||||
} else {
|
|
||||||
opts = { ...DEFAULT_OPTS };
|
|
||||||
}
|
|
||||||
const transport = new WebsocketTransport(opts.url);
|
|
||||||
super(transport, opts.startEventLoop);
|
|
||||||
this.opts = opts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||||
close() {}
|
close() {}
|
||||||
constructor(input: any, output: any, startEventLoop: boolean) {
|
constructor(input: any, output: any, startEventLoop: boolean) {
|
||||||
@@ -111,7 +86,10 @@ export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class StdioTransport extends BaseTransport {
|
export class StdioTransport extends BaseTransport {
|
||||||
constructor(public input: any, public output: any) {
|
constructor(
|
||||||
|
public input: any,
|
||||||
|
public output: any,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
var buffer = "";
|
var buffer = "";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { strictEqual } from "assert";
|
|
||||||
import chai, { assert, expect } from "chai";
|
import chai, { assert, expect } from "chai";
|
||||||
import chaiAsPromised from "chai-as-promised";
|
import chaiAsPromised from "chai-as-promised";
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
@@ -32,14 +31,14 @@ describe("basic tests", () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
validAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
||||||
)
|
),
|
||||||
).to.not.contain(false);
|
).to.not.contain(false);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
||||||
)
|
),
|
||||||
).to.not.contain(true);
|
).to.not.contain(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ describe("basic tests", () => {
|
|||||||
const contactId = await dc.rpc.createContact(
|
const contactId = await dc.rpc.createContact(
|
||||||
accountId,
|
accountId,
|
||||||
"example@delta.chat",
|
"example@delta.chat",
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
||||||
.false;
|
.false;
|
||||||
@@ -127,7 +126,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config)
|
Object.keys(config),
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
@@ -139,7 +138,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config)
|
Object.keys(config),
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
@@ -153,7 +152,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config)
|
Object.keys(config),
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { assert, expect } from "chai";
|
import { assert, expect } from "chai";
|
||||||
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
|
import { StdioDeltaChat as DeltaChat, DcEvent, C } from "../deltachat.js";
|
||||||
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
|
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
|
||||||
|
|
||||||
const EVENT_TIMEOUT = 20000;
|
const EVENT_TIMEOUT = 20000;
|
||||||
@@ -17,12 +17,12 @@ describe("online tests", function () {
|
|||||||
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 CHATMAIL_DOMAIN 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 CHATMAIL_DOMAIN environment variable!, skip integration tests"
|
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests",
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ describe("online tests", function () {
|
|||||||
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
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",
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ describe("online tests", function () {
|
|||||||
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
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",
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -80,11 +80,8 @@ describe("online tests", function () {
|
|||||||
}
|
}
|
||||||
this.timeout(15000);
|
this.timeout(15000);
|
||||||
|
|
||||||
const contactId = await dc.rpc.createContact(
|
const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
|
||||||
accountId1,
|
const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
|
||||||
account2.email,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
||||||
|
|
||||||
@@ -95,26 +92,26 @@ describe("online tests", function () {
|
|||||||
accountId2,
|
accountId2,
|
||||||
chatIdOnAccountB,
|
chatIdOnAccountB,
|
||||||
false,
|
false,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(messageList).have.length(1);
|
// There are 2 messages in the chat:
|
||||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
|
||||||
|
expect(messageList).have.length(2);
|
||||||
|
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
|
||||||
expect(message.text).equal("Hello");
|
expect(message.text).equal("Hello");
|
||||||
|
expect(message.showPadlock).equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("send and receive text message roundtrip, encrypted on answer onwards", async function () {
|
it("send and receive text message roundtrip", async function () {
|
||||||
if (!accountsConfigured) {
|
if (!accountsConfigured) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
|
||||||
// send message from A to B
|
// send message from A to B
|
||||||
const contactId = await dc.rpc.createContact(
|
const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
|
||||||
accountId1,
|
const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
|
||||||
account2.email,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
||||||
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
|
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
|
||||||
@@ -129,11 +126,11 @@ describe("online tests", function () {
|
|||||||
accountId2,
|
accountId2,
|
||||||
chatIdOnAccountB,
|
chatIdOnAccountB,
|
||||||
false,
|
false,
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
const message = await dc.rpc.getMessage(
|
const message = await dc.rpc.getMessage(
|
||||||
accountId2,
|
accountId2,
|
||||||
messageList.reverse()[0]
|
messageList.reverse()[0],
|
||||||
);
|
);
|
||||||
expect(message.text).equal("Hello2");
|
expect(message.text).equal("Hello2");
|
||||||
// Send message back from B to A
|
// Send message back from B to A
|
||||||
@@ -155,7 +152,7 @@ describe("online tests", function () {
|
|||||||
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||||
expect(info).to.be.not.null;
|
expect(info).to.be.not.null;
|
||||||
expect(info?.overviewPage).to.equal(
|
expect(info?.overviewPage).to.equal(
|
||||||
"https://providers.delta.chat/example-com"
|
"https://providers.delta.chat/example-com",
|
||||||
);
|
);
|
||||||
expect(info?.status).to.equal(3);
|
expect(info?.status).to.equal(3);
|
||||||
});
|
});
|
||||||
@@ -172,12 +169,12 @@ 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, { kind: 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")),
|
||||||
timeout
|
timeout,
|
||||||
);
|
);
|
||||||
const callback = (contextId: number, event: DcEvent) => {
|
const callback = (contextId: number, event: DcEvent) => {
|
||||||
if (contextId == accountId) {
|
if (contextId == accountId) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
|||||||
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
||||||
|
|
||||||
const pathToServerBinary = resolve(
|
const pathToServerBinary = resolve(
|
||||||
join(await getTargetDir(), "debug/deltachat-rpc-server")
|
join(await getTargetDir(), "debug/deltachat-rpc-server"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const server = spawn(pathToServerBinary, {
|
const server = spawn(pathToServerBinary, {
|
||||||
@@ -29,7 +29,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to start server executable " +
|
"Failed to start server executable " +
|
||||||
pathToServerBinary +
|
pathToServerBinary +
|
||||||
", make sure you built it first."
|
", make sure you built it first.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
let shouldClose = false;
|
let shouldClose = false;
|
||||||
@@ -83,7 +83,7 @@ function getTargetDir(): Promise<string> {
|
|||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,6 @@
|
|||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"isolatedModules": true
|
"isolatedModules": true
|
||||||
},
|
},
|
||||||
"include": ["*.ts", "example/*.ts", "test/*.ts"],
|
"include": ["*.ts", "test/*.ts"],
|
||||||
"compileOnSave": false
|
"compileOnSave": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "1.152.2"
|
version = "2.11.0"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
repository = "https://github.com/chatmail/core"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
deltachat = { workspace = true, features = ["internals"]}
|
deltachat = { workspace = true, features = ["internals"]}
|
||||||
dirs = "5"
|
dirs = "6"
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
qr2term = "0.3.3"
|
qr2term = "0.3.3"
|
||||||
rusqlite = { workspace = true }
|
rusqlite = { workspace = true }
|
||||||
rustyline = "14"
|
rustyline = "16"
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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::mimeparser::SystemMessage;
|
||||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
||||||
use deltachat::peerstate::*;
|
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
use deltachat::qr_code_generator::create_qr_svg;
|
use deltachat::qr_code_generator::create_qr_svg;
|
||||||
use deltachat::reaction::send_reaction;
|
use deltachat::reaction::send_reaction;
|
||||||
@@ -35,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 & 2 {
|
|
||||||
context
|
|
||||||
.sql()
|
|
||||||
.execute("DELETE FROM acpeerstates;", ())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
println!("(2) Peerstates reset.");
|
|
||||||
}
|
|
||||||
if 0 != bits & 4 {
|
if 0 != bits & 4 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
@@ -92,11 +83,11 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
context.emit_msgs_changed_without_ids();
|
context.emit_msgs_changed_without_ids();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||||
let data = read_file(context, filename).await?;
|
let data = read_file(context, filename).await?;
|
||||||
|
|
||||||
if let Err(err) = receive_imf(context, &data, false).await {
|
if let Err(err) = receive_imf(context, &data, false).await {
|
||||||
println!("receive_imf errored: {err:?}");
|
eprintln!("receive_imf errored: {err:?}");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -120,13 +111,13 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
} else {
|
} else {
|
||||||
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
||||||
if rs.is_none() {
|
if rs.is_none() {
|
||||||
error!(context, "Import: No file or folder given.");
|
eprintln!("Import: No file or folder given.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
real_spec = rs.unwrap();
|
real_spec = rs.unwrap();
|
||||||
}
|
}
|
||||||
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
|
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
|
||||||
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
|
if suffix == "eml" && poke_eml_file(context, Path::new(&real_spec)).await.is_ok() {
|
||||||
read_cnt += 1
|
read_cnt += 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -140,13 +131,16 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
if name.ends_with(".eml") {
|
if name.ends_with(".eml") {
|
||||||
let path_plus_name = format!("{}/{}", &real_spec, name);
|
let path_plus_name = format!("{}/{}", &real_spec, name);
|
||||||
println!("Import: {path_plus_name}");
|
println!("Import: {path_plus_name}");
|
||||||
if poke_eml_file(context, path_plus_name).await.is_ok() {
|
if poke_eml_file(context, Path::new(&path_plus_name))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
read_cnt += 1
|
read_cnt += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +268,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
|||||||
|
|
||||||
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
||||||
for contact_id in contacts {
|
for contact_id in contacts {
|
||||||
let mut line2 = "".to_string();
|
let line2 = "".to_string();
|
||||||
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();
|
||||||
@@ -293,15 +287,6 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
|||||||
verified_str,
|
verified_str,
|
||||||
if !addr.is_empty() { addr } else { "addr unset" }
|
if !addr.is_empty() { addr } else { "addr unset" }
|
||||||
);
|
);
|
||||||
let peerstate = Peerstate::from_addr(context, addr)
|
|
||||||
.await
|
|
||||||
.expect("peerstate error");
|
|
||||||
if peerstate.is_some() && *contact_id != ContactId::SELF {
|
|
||||||
line2 = format!(
|
|
||||||
", prefer-encrypt={}",
|
|
||||||
peerstate.as_ref().unwrap().prefer_encrypt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||||
}
|
}
|
||||||
@@ -339,7 +324,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
send-backup\n\
|
send-backup\n\
|
||||||
receive-backup <qr>\n\
|
receive-backup <qr>\n\
|
||||||
export-keys\n\
|
export-keys\n\
|
||||||
import-keys\n\
|
import-keys <key-file>\n\
|
||||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||||
reset <flags>\n\
|
reset <flags>\n\
|
||||||
stop\n\
|
stop\n\
|
||||||
@@ -348,8 +333,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
_ => println!(
|
_ => println!(
|
||||||
"==========================Database commands==\n\
|
"==========================Database commands==\n\
|
||||||
info\n\
|
info\n\
|
||||||
open <file to open or create>\n\
|
|
||||||
close\n\
|
|
||||||
set <configuration-key> [<value>]\n\
|
set <configuration-key> [<value>]\n\
|
||||||
get <configuration-key>\n\
|
get <configuration-key>\n\
|
||||||
oauth2\n\
|
oauth2\n\
|
||||||
@@ -364,21 +347,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
==============================Chat commands==\n\
|
==============================Chat commands==\n\
|
||||||
listchats [<query>]\n\
|
listchats [<query>]\n\
|
||||||
listarchived\n\
|
listarchived\n\
|
||||||
|
start-realtime <msg-id>\n\
|
||||||
|
send-realtime <msg-id> <data>\n\
|
||||||
chat [<chat-id>|0]\n\
|
chat [<chat-id>|0]\n\
|
||||||
createchat <contact-id>\n\
|
createchat <contact-id>\n\
|
||||||
creategroup <name>\n\
|
creategroup <name>\n\
|
||||||
createbroadcast\n\
|
createbroadcast <name>\n\
|
||||||
createprotected <name>\n\
|
createprotected <name>\n\
|
||||||
addmember <contact-id>\n\
|
addmember <contact-id>\n\
|
||||||
removemember <contact-id>\n\
|
removemember <contact-id>\n\
|
||||||
groupname <name>\n\
|
groupname <name>\n\
|
||||||
groupimage [<file>]\n\
|
groupimage <image>\n\
|
||||||
chatinfo\n\
|
chatinfo\n\
|
||||||
sendlocations <seconds>\n\
|
sendlocations <seconds>\n\
|
||||||
setlocation <lat> <lng>\n\
|
setlocation <lat> <lng>\n\
|
||||||
dellocations\n\
|
dellocations\n\
|
||||||
getlocations [<contact-id>]\n\
|
getlocations [<contact-id>]\n\
|
||||||
send <text>\n\
|
send <text>\n\
|
||||||
|
sendempty\n\
|
||||||
sendimage <file> [<text>]\n\
|
sendimage <file> [<text>]\n\
|
||||||
sendsticker <file> [<text>]\n\
|
sendsticker <file> [<text>]\n\
|
||||||
sendfile <file> [<text>]\n\
|
sendfile <file> [<text>]\n\
|
||||||
@@ -397,7 +383,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
unmute <chat-id>\n\
|
unmute <chat-id>\n\
|
||||||
delchat <chat-id>\n\
|
delchat <chat-id>\n\
|
||||||
accept <chat-id>\n\
|
accept <chat-id>\n\
|
||||||
decline <chat-id>\n\
|
blockchat <chat-id>\n\
|
||||||
===========================Message commands==\n\
|
===========================Message commands==\n\
|
||||||
listmsgs <query>\n\
|
listmsgs <query>\n\
|
||||||
msginfo <msg-id>\n\
|
msginfo <msg-id>\n\
|
||||||
@@ -411,14 +397,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
react <msg-id> [<reaction>]\n\
|
react <msg-id> [<reaction>]\n\
|
||||||
===========================Contact commands==\n\
|
===========================Contact commands==\n\
|
||||||
listcontacts [<query>]\n\
|
listcontacts [<query>]\n\
|
||||||
listverified [<query>]\n\
|
|
||||||
addcontact [<name>] <addr>\n\
|
addcontact [<name>] <addr>\n\
|
||||||
contactinfo <contact-id>\n\
|
contactinfo <contact-id>\n\
|
||||||
delcontact <contact-id>\n\
|
delcontact <contact-id>\n\
|
||||||
cleanupcontacts\n\
|
|
||||||
block <contact-id>\n\
|
block <contact-id>\n\
|
||||||
unblock <contact-id>\n\
|
unblock <contact-id>\n\
|
||||||
listblocked\n\
|
listblocked\n\
|
||||||
|
import-vcard <file>\n\
|
||||||
|
make-vcard <file> <contact-id> [contact-id ...]\n\
|
||||||
======================================Misc.==\n\
|
======================================Misc.==\n\
|
||||||
getqr [<chat-id>]\n\
|
getqr [<chat-id>]\n\
|
||||||
getqrsvg [<chat-id>]\n\
|
getqrsvg [<chat-id>]\n\
|
||||||
@@ -490,7 +476,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"send-backup" => {
|
"send-backup" => {
|
||||||
let provider = BackupProvider::prepare(&context).await?;
|
let provider = BackupProvider::prepare(&context).await?;
|
||||||
let qr = format_backup(&provider.qr())?;
|
let qr = format_backup(&provider.qr())?;
|
||||||
println!("QR code: {}", qr);
|
println!("QR code: {qr}");
|
||||||
qr2term::print_qr(qr.as_str())?;
|
qr2term::print_qr(qr.as_str())?;
|
||||||
provider.await?;
|
provider.await?;
|
||||||
}
|
}
|
||||||
@@ -505,13 +491,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Exported to {}.", dir.to_string_lossy());
|
println!("Exported to {}.", dir.to_string_lossy());
|
||||||
}
|
}
|
||||||
"import-keys" => {
|
"import-keys" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <key-file> missing.");
|
||||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
||||||
}
|
}
|
||||||
"poke" => {
|
"poke" => {
|
||||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||||
}
|
}
|
||||||
"reset" => {
|
"reset" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
|
ensure!(
|
||||||
|
!arg1.is_empty(),
|
||||||
|
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
||||||
|
);
|
||||||
let bits: i32 = arg1.parse()?;
|
let bits: i32 = arg1.parse()?;
|
||||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||||
reset_tables(&context, bits).await;
|
reset_tables(&context, bits).await;
|
||||||
@@ -633,7 +623,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Location streaming enabled.");
|
println!("Location streaming enabled.");
|
||||||
}
|
}
|
||||||
println!("{cnt} chats");
|
println!("{cnt} chats");
|
||||||
println!("{time_needed:?} to create this list");
|
eprintln!("{time_needed:?} to create this list");
|
||||||
}
|
}
|
||||||
"start-realtime" => {
|
"start-realtime" => {
|
||||||
if arg1.is_empty() {
|
if arg1.is_empty() {
|
||||||
@@ -743,7 +733,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||||
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
||||||
|
|
||||||
println!(
|
eprintln!(
|
||||||
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -762,7 +752,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Group#{chat_id} created successfully.");
|
println!("Group#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
"createbroadcast" => {
|
"createbroadcast" => {
|
||||||
let chat_id = chat::create_broadcast_list(&context).await?;
|
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||||
|
let chat_id = chat::create_broadcast(&context, arg1.to_string()).await?;
|
||||||
|
|
||||||
println!("Broadcast#{chat_id} created successfully.");
|
println!("Broadcast#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
@@ -939,7 +930,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
} else {
|
} else {
|
||||||
Viewtype::File
|
Viewtype::File
|
||||||
});
|
});
|
||||||
msg.set_file(arg1, None);
|
msg.set_file_and_deduplicate(&context, Path::new(arg1), None, None)?;
|
||||||
msg.set_text(arg2.to_string());
|
msg.set_text(arg2.to_string());
|
||||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||||
}
|
}
|
||||||
@@ -996,7 +987,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
},
|
},
|
||||||
query,
|
query,
|
||||||
);
|
);
|
||||||
println!("{time_needed:?} to create this list");
|
eprintln!("{time_needed:?} to create this list");
|
||||||
}
|
}
|
||||||
"draft" => {
|
"draft" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
@@ -1159,19 +1150,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let reaction = arg2;
|
let reaction = arg2;
|
||||||
send_reaction(&context, msg_id, reaction).await?;
|
send_reaction(&context, msg_id, reaction).await?;
|
||||||
}
|
}
|
||||||
"listcontacts" | "contacts" | "listverified" => {
|
"listcontacts" | "contacts" => {
|
||||||
let contacts = Contact::get_all(
|
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
||||||
&context,
|
|
||||||
if arg0 == "listverified" {
|
|
||||||
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
|
|
||||||
} else {
|
|
||||||
DC_GCL_ADD_SELF
|
|
||||||
},
|
|
||||||
Some(arg1),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
println!("{} contacts.", contacts.len());
|
println!("{} key contacts.", contacts.len());
|
||||||
|
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
|
||||||
|
log_contactlist(&context, &addrcontacts).await?;
|
||||||
|
println!("{} address contacts.", addrcontacts.len());
|
||||||
}
|
}
|
||||||
"addcontact" => {
|
"addcontact" => {
|
||||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||||
@@ -1235,6 +1220,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
println!("{} blocked contacts.", contacts.len());
|
println!("{} blocked contacts.", contacts.len());
|
||||||
}
|
}
|
||||||
|
"import-vcard" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||||
|
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
|
||||||
|
let contacts = import_vcard(&context, &vcard_content).await?;
|
||||||
|
println!("vCard contacts imported:");
|
||||||
|
log_contactlist(&context, &contacts).await?;
|
||||||
|
}
|
||||||
|
"make-vcard" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||||
|
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
|
||||||
|
let mut contact_ids = vec![];
|
||||||
|
for x in arg2.split_whitespace() {
|
||||||
|
contact_ids.push(ContactId::new(x.parse()?))
|
||||||
|
}
|
||||||
|
let vcard_content = make_vcard(&context, &contact_ids).await?;
|
||||||
|
fs::write(&arg1.to_string(), vcard_content).await?;
|
||||||
|
println!("vCard written to: {arg1}");
|
||||||
|
}
|
||||||
"checkqr" => {
|
"checkqr" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||||
let qr = check_qr(&context, arg1).await?;
|
let qr = check_qr(&context, arg1).await?;
|
||||||
@@ -1244,7 +1247,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||||
match set_config_from_qr(&context, arg1).await {
|
match set_config_from_qr(&context, arg1).await {
|
||||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||||
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"createqrsvg" => {
|
"createqrsvg" => {
|
||||||
@@ -1278,7 +1281,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"fileinfo" => {
|
"fileinfo" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||||
|
|
||||||
if let Ok(buf) = read_file(&context, &arg1).await {
|
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
|
||||||
let (width, height) = get_filemeta(&buf)?;
|
let (width, height) = get_filemeta(&buf)?;
|
||||||
println!("width={width}, height={height}");
|
println!("width={width}, height={height}");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
//! Usage: cargo run --example repl --release -- <databasefile>
|
//! Usage: cargo run --example repl --release -- <databasefile>
|
||||||
//! All further options can be set using the set-command (type ? for help).
|
//! All further options can be set using the set-command (type ? for help).
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate deltachat;
|
extern crate deltachat;
|
||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
@@ -22,7 +21,7 @@ use log::{error, info, warn};
|
|||||||
use nu_ansi_term::Color;
|
use nu_ansi_term::Color;
|
||||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
use rustyline::highlight::{CmdKind as HighlightCmdKind, Highlighter, MatchingBracketHighlighter};
|
||||||
use rustyline::hint::{Hinter, HistoryHinter};
|
use rustyline::hint::{Hinter, HistoryHinter};
|
||||||
use rustyline::validate::Validator;
|
use rustyline::validate::Validator;
|
||||||
use rustyline::{
|
use rustyline::{
|
||||||
@@ -41,25 +40,25 @@ fn receive_event(event: EventType) {
|
|||||||
match event {
|
match event {
|
||||||
EventType::Info(msg) => {
|
EventType::Info(msg) => {
|
||||||
/* do not show the event as this would fill the screen */
|
/* do not show the event as this would fill the screen */
|
||||||
info!("{}", msg);
|
info!("{msg}");
|
||||||
}
|
}
|
||||||
EventType::SmtpConnected(msg) => {
|
EventType::SmtpConnected(msg) => {
|
||||||
info!("[SMTP_CONNECTED] {}", msg);
|
info!("[SMTP_CONNECTED] {msg}");
|
||||||
}
|
}
|
||||||
EventType::ImapConnected(msg) => {
|
EventType::ImapConnected(msg) => {
|
||||||
info!("[IMAP_CONNECTED] {}", msg);
|
info!("[IMAP_CONNECTED] {msg}");
|
||||||
}
|
}
|
||||||
EventType::SmtpMessageSent(msg) => {
|
EventType::SmtpMessageSent(msg) => {
|
||||||
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
info!("[SMTP_MESSAGE_SENT] {msg}");
|
||||||
}
|
}
|
||||||
EventType::Warning(msg) => {
|
EventType::Warning(msg) => {
|
||||||
warn!("{}", msg);
|
warn!("{msg}");
|
||||||
}
|
}
|
||||||
EventType::Error(msg) => {
|
EventType::Error(msg) => {
|
||||||
error!("{}", msg);
|
error!("{msg}");
|
||||||
}
|
}
|
||||||
EventType::ErrorSelfNotInGroup(msg) => {
|
EventType::ErrorSelfNotInGroup(msg) => {
|
||||||
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
error!("[SELF_NOT_IN_GROUP] {msg}");
|
||||||
}
|
}
|
||||||
EventType::MsgsChanged { chat_id, msg_id } => {
|
EventType::MsgsChanged { chat_id, msg_id } => {
|
||||||
info!(
|
info!(
|
||||||
@@ -124,7 +123,7 @@ fn receive_event(event: EventType) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
info!("Received {:?}", event);
|
info!("Received {event:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,9 +179,11 @@ const DB_COMMANDS: [&str; 11] = [
|
|||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 36] = [
|
const CHAT_COMMANDS: [&str; 39] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
|
"start-realtime",
|
||||||
|
"send-realtime",
|
||||||
"chat",
|
"chat",
|
||||||
"createchat",
|
"createchat",
|
||||||
"creategroup",
|
"creategroup",
|
||||||
@@ -198,13 +199,16 @@ const CHAT_COMMANDS: [&str; 36] = [
|
|||||||
"dellocations",
|
"dellocations",
|
||||||
"getlocations",
|
"getlocations",
|
||||||
"send",
|
"send",
|
||||||
|
"sendempty",
|
||||||
"sendimage",
|
"sendimage",
|
||||||
|
"sendsticker",
|
||||||
"sendfile",
|
"sendfile",
|
||||||
"sendhtml",
|
"sendhtml",
|
||||||
"sendsyncmsg",
|
"sendsyncmsg",
|
||||||
"sendupdate",
|
"sendupdate",
|
||||||
"videochat",
|
"videochat",
|
||||||
"draft",
|
"draft",
|
||||||
|
"devicemsg",
|
||||||
"listmedia",
|
"listmedia",
|
||||||
"archive",
|
"archive",
|
||||||
"unarchive",
|
"unarchive",
|
||||||
@@ -212,47 +216,48 @@ const CHAT_COMMANDS: [&str; 36] = [
|
|||||||
"unpin",
|
"unpin",
|
||||||
"mute",
|
"mute",
|
||||||
"unmute",
|
"unmute",
|
||||||
"protect",
|
|
||||||
"unprotect",
|
|
||||||
"delchat",
|
"delchat",
|
||||||
"accept",
|
"accept",
|
||||||
"blockchat",
|
"blockchat",
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 9] = [
|
const MESSAGE_COMMANDS: [&str; 10] = [
|
||||||
"listmsgs",
|
"listmsgs",
|
||||||
"msginfo",
|
"msginfo",
|
||||||
|
"download",
|
||||||
|
"html",
|
||||||
"listfresh",
|
"listfresh",
|
||||||
"forward",
|
"forward",
|
||||||
"resend",
|
"resend",
|
||||||
"markseen",
|
"markseen",
|
||||||
"delmsg",
|
"delmsg",
|
||||||
"download",
|
|
||||||
"react",
|
"react",
|
||||||
];
|
];
|
||||||
const CONTACT_COMMANDS: [&str; 9] = [
|
const CONTACT_COMMANDS: [&str; 9] = [
|
||||||
"listcontacts",
|
"listcontacts",
|
||||||
"listverified",
|
|
||||||
"addcontact",
|
"addcontact",
|
||||||
"contactinfo",
|
"contactinfo",
|
||||||
"delcontact",
|
"delcontact",
|
||||||
"cleanupcontacts",
|
|
||||||
"block",
|
"block",
|
||||||
"unblock",
|
"unblock",
|
||||||
"listblocked",
|
"listblocked",
|
||||||
|
"import-vcard",
|
||||||
|
"make-vcard",
|
||||||
];
|
];
|
||||||
const MISC_COMMANDS: [&str; 12] = [
|
const MISC_COMMANDS: [&str; 14] = [
|
||||||
"getqr",
|
"getqr",
|
||||||
"getqrsvg",
|
"getqrsvg",
|
||||||
"getbadqr",
|
"getbadqr",
|
||||||
"checkqr",
|
"checkqr",
|
||||||
"joinqr",
|
"joinqr",
|
||||||
|
"setqr",
|
||||||
"createqrsvg",
|
"createqrsvg",
|
||||||
|
"providerinfo",
|
||||||
"fileinfo",
|
"fileinfo",
|
||||||
|
"estimatedeletion",
|
||||||
"clear",
|
"clear",
|
||||||
"exit",
|
"exit",
|
||||||
"quit",
|
"quit",
|
||||||
"help",
|
"help",
|
||||||
"estimatedeletion",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Hinter for DcHelper {
|
impl Hinter for DcHelper {
|
||||||
@@ -298,8 +303,8 @@ impl Highlighter for DcHelper {
|
|||||||
self.highlighter.highlight(line, pos)
|
self.highlighter.highlight(line, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
|
fn highlight_char(&self, line: &str, pos: usize, kind: HighlightCmdKind) -> bool {
|
||||||
self.highlighter.highlight_char(line, pos, forced)
|
self.highlighter.highlight_char(line, pos, kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +313,7 @@ impl Validator for DcHelper {}
|
|||||||
|
|
||||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
println!("Error: Bad arguments, expected [db-name].");
|
eprintln!("Error: Bad arguments, expected [db-name].");
|
||||||
bail!("No db-name specified");
|
bail!("No db-name specified");
|
||||||
}
|
}
|
||||||
let context = ContextBuilder::new(args[1].clone().into())
|
let context = ContextBuilder::new(args[1].clone().into())
|
||||||
@@ -323,7 +328,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("Delta Chat Core is awaiting your commands.");
|
println!("Chatmail is awaiting your commands.");
|
||||||
|
|
||||||
let config = Config::builder()
|
let config = Config::builder()
|
||||||
.history_ignore_space(true)
|
.history_ignore_space(true)
|
||||||
@@ -363,7 +368,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error: {err:#}");
|
eprintln!("Error: {err:#}");
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,7 +383,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error: {err:#}");
|
eprintln!("Error: {err:#}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deltachat-rpc-client"
|
name = "deltachat-rpc-client"
|
||||||
version = "1.152.2"
|
version = "2.11.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",
|
||||||
@@ -13,7 +13,6 @@ classifiers = [
|
|||||||
"Operating System :: POSIX :: Linux",
|
"Operating System :: POSIX :: Linux",
|
||||||
"Operating System :: MacOS :: MacOS X",
|
"Operating System :: MacOS :: MacOS X",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.7",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
@@ -24,6 +23,7 @@ classifiers = [
|
|||||||
"Topic :: Communications :: Email"
|
"Topic :: Communications :: Email"
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
deltachat_rpc_client = [
|
deltachat_rpc_client = [
|
||||||
@@ -66,7 +66,18 @@ lint.select = [
|
|||||||
|
|
||||||
"RUF006" # asyncio-dangling-task
|
"RUF006" # asyncio-dangling-task
|
||||||
]
|
]
|
||||||
|
lint.ignore = [
|
||||||
|
"PLC0415" # `import` should be at the top-level of a file
|
||||||
|
]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"imap-tools",
|
||||||
|
"pytest",
|
||||||
|
"pytest-timeout",
|
||||||
|
"pytest-xdist",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@@ -89,8 +90,8 @@ def _run_cli(
|
|||||||
help="accounts folder (default: current working directory)",
|
help="accounts folder (default: current working directory)",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
)
|
)
|
||||||
parser.add_argument("--email", action="store", help="email address")
|
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
|
||||||
parser.add_argument("--password", action="store", help="password")
|
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
|
||||||
args = parser.parse_args(argv[1:])
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||||
@@ -114,7 +115,7 @@ def _run_cli(
|
|||||||
|
|
||||||
|
|
||||||
def extract_addr(text: str) -> str:
|
def extract_addr(text: str) -> str:
|
||||||
"""extract email address from the given text."""
|
"""Extract email address from the given text."""
|
||||||
match = re.match(r".*\((.+@.+)\)", text)
|
match = re.match(r".*\((.+@.+)\)", text)
|
||||||
if match:
|
if match:
|
||||||
text = match.group(1)
|
text = match.group(1)
|
||||||
@@ -123,7 +124,7 @@ def extract_addr(text: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
||||||
"""return image changed/deleted info from parsing the given system message text."""
|
"""Return image changed/deleted info from parsing the given system message text."""
|
||||||
text = text.lower()
|
text = text.lower()
|
||||||
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
||||||
if match:
|
if match:
|
||||||
@@ -142,7 +143,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
|
|||||||
|
|
||||||
|
|
||||||
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||||
"""return add/remove info from parsing the given system message text.
|
"""Return add/remove info from parsing the given system message text.
|
||||||
|
|
||||||
returns a (action, affected, actor) tuple.
|
returns a (action, affected, actor) tuple.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""Account module."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -26,18 +28,36 @@ class Account:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.manager.rpc
|
return self.manager.rpc
|
||||||
|
|
||||||
def wait_for_event(self) -> AttrDict:
|
def wait_for_event(self, event_type=None) -> AttrDict:
|
||||||
"""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))
|
while True:
|
||||||
|
next_event = AttrDict(self._rpc.wait_for_event(self.id))
|
||||||
|
if event_type is None or next_event.kind == event_type:
|
||||||
|
return next_event
|
||||||
|
|
||||||
def clear_all_events(self):
|
def clear_all_events(self):
|
||||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
"""Remove all queued-up events for a given account.
|
||||||
|
|
||||||
|
Useful for tests.
|
||||||
|
"""
|
||||||
self._rpc.clear_all_events(self.id)
|
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)
|
||||||
|
|
||||||
|
def clone(self) -> "Account":
|
||||||
|
"""Clone given account.
|
||||||
|
|
||||||
|
This uses backup-transfer via iroh, i.e. the 'Add second device' feature.
|
||||||
|
"""
|
||||||
|
future = self._rpc.provide_backup.future(self.id)
|
||||||
|
qr = self._rpc.get_backup_qr(self.id)
|
||||||
|
new_account = self.manager.add_account()
|
||||||
|
new_account._rpc.get_backup(new_account.id, qr)
|
||||||
|
future()
|
||||||
|
return new_account
|
||||||
|
|
||||||
def start_io(self) -> None:
|
def start_io(self) -> None:
|
||||||
"""Start the account I/O."""
|
"""Start the account I/O."""
|
||||||
self._rpc.start_io(self.id)
|
self._rpc.start_io(self.id)
|
||||||
@@ -67,7 +87,7 @@ class Account:
|
|||||||
return self._rpc.get_config(self.id, key)
|
return self._rpc.get_config(self.id, key)
|
||||||
|
|
||||||
def update_config(self, **kwargs) -> None:
|
def update_config(self, **kwargs) -> None:
|
||||||
"""update config values."""
|
"""Update config values."""
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self.set_config(key, value)
|
self.set_config(key, value)
|
||||||
|
|
||||||
@@ -83,9 +103,15 @@ class Account:
|
|||||||
return self.get_config("selfavatar")
|
return self.get_config("selfavatar")
|
||||||
|
|
||||||
def check_qr(self, qr):
|
def check_qr(self, qr):
|
||||||
|
"""Parse QR code contents.
|
||||||
|
|
||||||
|
This function takes the raw text scanned
|
||||||
|
and checks what can be done with it.
|
||||||
|
"""
|
||||||
return self._rpc.check_qr(self.id, qr)
|
return self._rpc.check_qr(self.id, qr)
|
||||||
|
|
||||||
def set_config_from_qr(self, qr: str):
|
def set_config_from_qr(self, qr: str):
|
||||||
|
"""Set configuration values from a QR code."""
|
||||||
self._rpc.set_config_from_qr(self.id, qr)
|
self._rpc.set_config_from_qr(self.id, qr)
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
@@ -93,15 +119,23 @@ class Account:
|
|||||||
"""Configure an account."""
|
"""Configure an account."""
|
||||||
yield self._rpc.configure.future(self.id)
|
yield self._rpc.configure.future(self.id)
|
||||||
|
|
||||||
|
@futuremethod
|
||||||
|
def add_or_update_transport(self, params):
|
||||||
|
"""Add a new transport."""
|
||||||
|
yield self._rpc.add_or_update_transport.future(self.id, params)
|
||||||
|
|
||||||
|
@futuremethod
|
||||||
|
def list_transports(self):
|
||||||
|
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||||
|
transports = yield self._rpc.list_transports.future(self.id)
|
||||||
|
return transports
|
||||||
|
|
||||||
def bring_online(self):
|
def bring_online(self):
|
||||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||||
self.start_io()
|
self.start_io()
|
||||||
while True:
|
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||||
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, "Account"], name: Optional[str] = None) -> Contact:
|
||||||
"""Create a new Contact or return an existing one.
|
"""Create a new Contact or return an existing one.
|
||||||
|
|
||||||
Calling this method will always result in the same
|
Calling this method will always result in the same
|
||||||
@@ -109,26 +143,63 @@ class Account:
|
|||||||
with that e-mail address, it is unblocked and its display
|
with that e-mail address, it is unblocked and its display
|
||||||
name is updated if specified.
|
name is updated if specified.
|
||||||
|
|
||||||
:param obj: email-address or contact id.
|
:param obj: email-address, contact id or account.
|
||||||
:param name: (optional) display name for this contact.
|
:param name: (optional) display name for this contact.
|
||||||
"""
|
"""
|
||||||
|
if isinstance(obj, Account):
|
||||||
|
vcard = obj.self_contact.make_vcard()
|
||||||
|
[contact] = self.import_vcard(vcard)
|
||||||
|
if name:
|
||||||
|
contact.set_name(name)
|
||||||
|
return contact
|
||||||
if isinstance(obj, int):
|
if isinstance(obj, int):
|
||||||
obj = Contact(self, obj)
|
obj = Contact(self, obj)
|
||||||
if isinstance(obj, Contact):
|
if isinstance(obj, Contact):
|
||||||
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 make_vcard(self, contacts: list[Contact]) -> str:
|
||||||
|
"""Create vCard with the given contacts."""
|
||||||
|
assert all(contact.account == self for contact in contacts)
|
||||||
|
contact_ids = [contact.id for contact in contacts]
|
||||||
|
return self._rpc.make_vcard(self.id, contact_ids)
|
||||||
|
|
||||||
|
def import_vcard(self, vcard: str) -> list[Contact]:
|
||||||
|
"""Import vCard.
|
||||||
|
|
||||||
|
Return created or modified contacts in the order they appear in vCard.
|
||||||
|
"""
|
||||||
|
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
|
||||||
|
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||||
|
|
||||||
def create_chat(self, account: "Account") -> Chat:
|
def create_chat(self, account: "Account") -> Chat:
|
||||||
addr = account.get_config("addr")
|
"""Create a 1:1 chat with another account."""
|
||||||
contact = self.create_contact(addr)
|
return self.create_contact(account).create_chat()
|
||||||
return contact.create_chat()
|
|
||||||
|
def get_device_chat(self) -> Chat:
|
||||||
|
"""Return device chat."""
|
||||||
|
return self.device_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)
|
||||||
|
|
||||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||||
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
"""Looks up a known and unblocked contact with a given e-mail address.
|
||||||
|
To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||||
|
|
||||||
|
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||||
|
(e.g. an address-contact and a key-contact),
|
||||||
|
this looks up the most recently seen contact,
|
||||||
|
i.e. which contact is returned depends on which contact last sent a message.
|
||||||
|
If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||||
|
But **DO NOT** internally represent contacts by their email address
|
||||||
|
and do not use this function to look them up;
|
||||||
|
otherwise this function will sometimes look up the wrong contact.
|
||||||
|
Instead, you should internally represent contacts by their ids.
|
||||||
|
|
||||||
|
To validate an e-mail address independently of the contact database
|
||||||
|
use check_email_validity()."""
|
||||||
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)
|
||||||
|
|
||||||
@@ -154,8 +225,8 @@ class Account:
|
|||||||
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,
|
|
||||||
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.
|
||||||
@@ -163,12 +234,9 @@ class Account:
|
|||||||
:param query: if a string is specified, only return contacts
|
:param query: if a string is specified, only return contacts
|
||||||
whose name or e-mail matches query.
|
whose name or e-mail matches query.
|
||||||
:param with_self: if True the self-contact is also included if it matches the query.
|
:param with_self: if True the self-contact is also included if it matches the query.
|
||||||
:param only_verified: if True only return verified contacts.
|
|
||||||
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
|
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
|
||||||
"""
|
"""
|
||||||
flags = 0
|
flags = 0
|
||||||
if verified_only:
|
|
||||||
flags |= ContactFlag.VERIFIED_ONLY
|
|
||||||
if with_self:
|
if with_self:
|
||||||
flags |= ContactFlag.ADD_SELF
|
flags |= ContactFlag.ADD_SELF
|
||||||
|
|
||||||
@@ -180,9 +248,14 @@ class Account:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def self_contact(self) -> Contact:
|
def self_contact(self) -> Contact:
|
||||||
"""This account's identity as a Contact."""
|
"""Account's identity as a Contact."""
|
||||||
return Contact(self, SpecialContactId.SELF)
|
return Contact(self, SpecialContactId.SELF)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_contact(self) -> Chat:
|
||||||
|
"""Account's device contact."""
|
||||||
|
return Contact(self, SpecialContactId.DEVICE)
|
||||||
|
|
||||||
def get_chatlist(
|
def get_chatlist(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
@@ -229,17 +302,52 @@ class Account:
|
|||||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||||
"""Create a new group chat.
|
"""Create a new group chat.
|
||||||
|
|
||||||
After creation, the group has only self-contact as member and is in unpromoted state.
|
After creation,
|
||||||
|
the group has only self-contact as member one member (see `SpecialContactId.SELF`)
|
||||||
|
and is in _unpromoted_ state.
|
||||||
|
This means, you can add or remove members, change the name,
|
||||||
|
the group image and so on without messages being sent to all group members.
|
||||||
|
|
||||||
|
This changes as soon as the first message is sent to the group members
|
||||||
|
and the group becomes _promoted_.
|
||||||
|
After that, all changes are synced with all group members
|
||||||
|
by sending status message.
|
||||||
|
|
||||||
|
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
|
||||||
|
(see `get_full_snapshot()` / `get_basic_snapshot()`).
|
||||||
|
This may be useful if you want to show some help for just created groups.
|
||||||
|
|
||||||
|
:param protect: If set to 1 the function creates group with protection initially enabled.
|
||||||
|
Only verified members are allowed in these groups
|
||||||
|
and end-to-end-encryption is always enabled.
|
||||||
"""
|
"""
|
||||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||||
|
|
||||||
|
def create_broadcast(self, name: str) -> Chat:
|
||||||
|
"""Create a new **broadcast channel**
|
||||||
|
(called "Channel" in the UI).
|
||||||
|
|
||||||
|
Broadcast channels are similar to groups on the sending device,
|
||||||
|
however, recipients get the messages in a read-only chat
|
||||||
|
and will not see who the other members are.
|
||||||
|
|
||||||
|
Called `broadcast` here rather than `channel`,
|
||||||
|
because the word "channel" already appears a lot in the code,
|
||||||
|
which would make it hard to grep for it.
|
||||||
|
|
||||||
|
After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||||
|
see `create_group()` for more information on the unpromoted state.
|
||||||
|
|
||||||
|
Returns the created chat.
|
||||||
|
"""
|
||||||
|
return Chat(self, self._rpc.create_broadcast(self.id, name))
|
||||||
|
|
||||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||||
"""Return the Chat instance with the given ID."""
|
"""Return the Chat instance with the given ID."""
|
||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def secure_join(self, qrdata: str) -> Chat:
|
def secure_join(self, qrdata: str) -> Chat:
|
||||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
|
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
|
||||||
another device.
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -296,34 +404,40 @@ class Account:
|
|||||||
|
|
||||||
def wait_for_incoming_msg_event(self):
|
def wait_for_incoming_msg_event(self):
|
||||||
"""Wait for incoming message event and return it."""
|
"""Wait for incoming message event and return it."""
|
||||||
while True:
|
return self.wait_for_event(EventType.INCOMING_MSG)
|
||||||
event = self.wait_for_event()
|
|
||||||
if event.kind == EventType.INCOMING_MSG:
|
def wait_for_msgs_changed_event(self):
|
||||||
return event
|
"""Wait for messages changed event and return it."""
|
||||||
|
return self.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
|
|
||||||
|
def wait_for_msgs_noticed_event(self):
|
||||||
|
"""Wait for messages noticed event and return it."""
|
||||||
|
return self.wait_for_event(EventType.MSGS_NOTICED)
|
||||||
|
|
||||||
def wait_for_incoming_msg(self):
|
def wait_for_incoming_msg(self):
|
||||||
"""Wait for incoming message and return it.
|
"""Wait for incoming message and return it.
|
||||||
|
|
||||||
Consumes all events before the next incoming message event."""
|
Consumes all events before the next incoming message event.
|
||||||
|
"""
|
||||||
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
||||||
|
|
||||||
def wait_for_securejoin_inviter_success(self):
|
def wait_for_securejoin_inviter_success(self):
|
||||||
|
"""Wait until SecureJoin process finishes successfully on the inviter side."""
|
||||||
while True:
|
while True:
|
||||||
event = self.wait_for_event()
|
event = self.wait_for_event()
|
||||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||||
break
|
break
|
||||||
|
|
||||||
def wait_for_securejoin_joiner_success(self):
|
def wait_for_securejoin_joiner_success(self):
|
||||||
|
"""Wait until SecureJoin process finishes successfully on the joiner side."""
|
||||||
while True:
|
while True:
|
||||||
event = self.wait_for_event()
|
event = self.wait_for_event()
|
||||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||||
break
|
break
|
||||||
|
|
||||||
def wait_for_reactions_changed(self):
|
def wait_for_reactions_changed(self):
|
||||||
while True:
|
"""Wait for reaction change event."""
|
||||||
event = self.wait_for_event()
|
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||||
if event.kind == EventType.REACTIONS_CHANGED:
|
|
||||||
return event
|
|
||||||
|
|
||||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
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."""
|
||||||
@@ -352,3 +466,7 @@ class Account:
|
|||||||
"""Import keys."""
|
"""Import keys."""
|
||||||
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||||
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||||
|
|
||||||
|
def initiate_autocrypt_key_transfer(self) -> None:
|
||||||
|
"""Send Autocrypt Setup Message."""
|
||||||
|
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""Chat module."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
@@ -89,7 +91,8 @@ class Chat:
|
|||||||
def set_ephemeral_timer(self, timer: int) -> None:
|
def set_ephemeral_timer(self, timer: int) -> None:
|
||||||
"""Set ephemeral timer of this chat in seconds.
|
"""Set ephemeral timer of this chat in seconds.
|
||||||
|
|
||||||
0 means the timer is disabled, use 1 for immediate deletion."""
|
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:
|
||||||
@@ -124,6 +127,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,
|
||||||
|
filename: 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,
|
||||||
@@ -137,6 +141,7 @@ class Chat:
|
|||||||
"html": html,
|
"html": html,
|
||||||
"viewtype": viewtype,
|
"viewtype": viewtype,
|
||||||
"file": file,
|
"file": file,
|
||||||
|
"filename": filename,
|
||||||
"location": location,
|
"location": location,
|
||||||
"overrideSenderName": override_sender_name,
|
"overrideSenderName": override_sender_name,
|
||||||
"quotedMessageId": quoted_msg,
|
"quotedMessageId": quoted_msg,
|
||||||
@@ -172,13 +177,14 @@ class Chat:
|
|||||||
self,
|
self,
|
||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
|
filename: Optional[str] = None,
|
||||||
quoted_msg: Optional[int] = None,
|
quoted_msg: Optional[int] = None,
|
||||||
viewtype: Optional[str] = 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, viewtype)
|
self._rpc.misc_set_draft(self.account.id, self.id, text, file, filename, quoted_msg, viewtype)
|
||||||
|
|
||||||
def remove_draft(self) -> None:
|
def remove_draft(self) -> None:
|
||||||
"""Remove draft message."""
|
"""Remove draft message."""
|
||||||
@@ -196,12 +202,12 @@ class Chat:
|
|||||||
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]
|
||||||
|
|
||||||
def get_fresh_message_count(self) -> int:
|
def get_fresh_message_count(self) -> int:
|
||||||
"""Get number of fresh messages in this chat"""
|
"""Get number of fresh messages in this chat."""
|
||||||
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||||
|
|
||||||
def mark_noticed(self) -> None:
|
def mark_noticed(self) -> None:
|
||||||
@@ -238,6 +244,11 @@ class Chat:
|
|||||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||||
|
|
||||||
|
def get_past_contacts(self) -> list[Contact]:
|
||||||
|
"""Get past contacts for this chat."""
|
||||||
|
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
||||||
|
return [Contact(self.account, contact_id) for contact_id in past_contacts]
|
||||||
|
|
||||||
def set_image(self, path: str) -> None:
|
def set_image(self, path: str) -> None:
|
||||||
"""Set profile image of this chat.
|
"""Set profile image of this chat.
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class Client:
|
|||||||
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:
|
||||||
|
"""Register multiple hooks."""
|
||||||
for hook, event in hooks:
|
for hook, event in hooks:
|
||||||
self.add_hook(hook, event)
|
self.add_hook(hook, event)
|
||||||
|
|
||||||
@@ -77,9 +78,11 @@ class Client:
|
|||||||
self._hooks.get(type(event), set()).remove((hook, event))
|
self._hooks.get(type(event), set()).remove((hook, event))
|
||||||
|
|
||||||
def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
|
"""Return True if the client is configured."""
|
||||||
return self.account.is_configured()
|
return self.account.is_configured()
|
||||||
|
|
||||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
|
"""Configure the client."""
|
||||||
self.account.set_config("addr", email)
|
self.account.set_config("addr", email)
|
||||||
self.account.set_config("mail_pw", password)
|
self.account.set_config("mail_pw", password)
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
@@ -198,5 +201,6 @@ class Bot(Client):
|
|||||||
"""Simple bot implementation that listens 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:
|
||||||
|
"""Configure the bot."""
|
||||||
kwargs.setdefault("bot", "1")
|
kwargs.setdefault("bot", "1")
|
||||||
super().configure(email, password, **kwargs)
|
super().configure(email, password, **kwargs)
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
"""Constants module."""
|
||||||
|
|
||||||
from enum import Enum, IntEnum
|
from enum import Enum, IntEnum
|
||||||
|
|
||||||
COMMAND_PREFIX = "/"
|
COMMAND_PREFIX = "/"
|
||||||
|
|
||||||
|
|
||||||
class ContactFlag(IntEnum):
|
class ContactFlag(IntEnum):
|
||||||
VERIFIED_ONLY = 0x01
|
"""Bit flags for get_contacts() method."""
|
||||||
|
|
||||||
ADD_SELF = 0x02
|
ADD_SELF = 0x02
|
||||||
|
ADDRESS = 0x04
|
||||||
|
|
||||||
|
|
||||||
class ChatlistFlag(IntEnum):
|
class ChatlistFlag(IntEnum):
|
||||||
|
"""Bit flags for get_chatlist() method."""
|
||||||
|
|
||||||
ARCHIVED_ONLY = 0x01
|
ARCHIVED_ONLY = 0x01
|
||||||
NO_SPECIALS = 0x02
|
NO_SPECIALS = 0x02
|
||||||
ADD_ALLDONE_HINT = 0x04
|
ADD_ALLDONE_HINT = 0x04
|
||||||
@@ -16,6 +22,8 @@ class ChatlistFlag(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class SpecialContactId(IntEnum):
|
class SpecialContactId(IntEnum):
|
||||||
|
"""Special contact IDs."""
|
||||||
|
|
||||||
SELF = 1
|
SELF = 1
|
||||||
INFO = 2 # centered messages as "member added", used in all chats
|
INFO = 2 # centered messages as "member added", used in all chats
|
||||||
DEVICE = 5 # messages "update info" in the device-chat
|
DEVICE = 5 # messages "update info" in the device-chat
|
||||||
@@ -23,7 +31,7 @@ class SpecialContactId(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class EventType(str, Enum):
|
class EventType(str, Enum):
|
||||||
"""Core event types"""
|
"""Core event types."""
|
||||||
|
|
||||||
INFO = "Info"
|
INFO = "Info"
|
||||||
SMTP_CONNECTED = "SmtpConnected"
|
SMTP_CONNECTED = "SmtpConnected"
|
||||||
@@ -48,6 +56,7 @@ class EventType(str, Enum):
|
|||||||
MSG_READ = "MsgRead"
|
MSG_READ = "MsgRead"
|
||||||
MSG_DELETED = "MsgDeleted"
|
MSG_DELETED = "MsgDeleted"
|
||||||
CHAT_MODIFIED = "ChatModified"
|
CHAT_MODIFIED = "ChatModified"
|
||||||
|
CHAT_DELETED = "ChatDeleted"
|
||||||
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
||||||
CONTACTS_CHANGED = "ContactsChanged"
|
CONTACTS_CHANGED = "ContactsChanged"
|
||||||
LOCATION_CHANGED = "LocationChanged"
|
LOCATION_CHANGED = "LocationChanged"
|
||||||
@@ -70,7 +79,7 @@ class EventType(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class ChatId(IntEnum):
|
class ChatId(IntEnum):
|
||||||
"""Special chat ids"""
|
"""Special chat IDs."""
|
||||||
|
|
||||||
TRASH = 3
|
TRASH = 3
|
||||||
ARCHIVED_LINK = 6
|
ARCHIVED_LINK = 6
|
||||||
@@ -79,17 +88,47 @@ class ChatId(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class ChatType(IntEnum):
|
class ChatType(IntEnum):
|
||||||
"""Chat types"""
|
"""Chat type."""
|
||||||
|
|
||||||
UNDEFINED = 0
|
UNDEFINED = 0
|
||||||
|
|
||||||
SINGLE = 100
|
SINGLE = 100
|
||||||
|
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||||
|
|
||||||
GROUP = 120
|
GROUP = 120
|
||||||
|
|
||||||
MAILINGLIST = 140
|
MAILINGLIST = 140
|
||||||
BROADCAST = 160
|
|
||||||
|
OUT_BROADCAST = 160
|
||||||
|
"""Outgoing broadcast channel, called "Channel" in the UI.
|
||||||
|
|
||||||
|
The user can send into this channel,
|
||||||
|
and all recipients will receive messages
|
||||||
|
in an `IN_BROADCAST`.
|
||||||
|
|
||||||
|
Called `broadcast` here rather than `channel`,
|
||||||
|
because the word "channel" already appears a lot in the code,
|
||||||
|
which would make it hard to grep for it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
IN_BROADCAST = 165
|
||||||
|
"""Incoming broadcast channel, called "Channel" in the UI.
|
||||||
|
|
||||||
|
This channel is read-only,
|
||||||
|
and we do not know who the other recipients are.
|
||||||
|
|
||||||
|
This is similar to a `MAILINGLIST`,
|
||||||
|
with the main difference being that
|
||||||
|
`IN_BROADCAST`s are encrypted.
|
||||||
|
|
||||||
|
Called `broadcast` here rather than `channel`,
|
||||||
|
because the word "channel" already appears a lot in the code,
|
||||||
|
which would make it hard to grep for it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ChatVisibility(str, Enum):
|
class ChatVisibility(str, Enum):
|
||||||
"""Chat visibility types"""
|
"""Chat visibility types."""
|
||||||
|
|
||||||
NORMAL = "Normal"
|
NORMAL = "Normal"
|
||||||
ARCHIVED = "Archived"
|
ARCHIVED = "Archived"
|
||||||
@@ -97,7 +136,7 @@ class ChatVisibility(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class DownloadState(str, Enum):
|
class DownloadState(str, Enum):
|
||||||
"""Message download state"""
|
"""Message download state."""
|
||||||
|
|
||||||
DONE = "Done"
|
DONE = "Done"
|
||||||
AVAILABLE = "Available"
|
AVAILABLE = "Available"
|
||||||
@@ -158,14 +197,14 @@ class MessageState(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class MessageId(IntEnum):
|
class MessageId(IntEnum):
|
||||||
"""Special message ids"""
|
"""Special message IDs."""
|
||||||
|
|
||||||
DAYMARKER = 9
|
DAYMARKER = 9
|
||||||
LAST_SPECIAL = 9
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
class CertificateChecks(IntEnum):
|
class CertificateChecks(IntEnum):
|
||||||
"""Certificate checks mode"""
|
"""Certificate checks mode."""
|
||||||
|
|
||||||
AUTOMATIC = 0
|
AUTOMATIC = 0
|
||||||
STRICT = 1
|
STRICT = 1
|
||||||
@@ -173,7 +212,7 @@ class CertificateChecks(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class Connectivity(IntEnum):
|
class Connectivity(IntEnum):
|
||||||
"""Connectivity states"""
|
"""Connectivity states."""
|
||||||
|
|
||||||
NOT_CONNECTED = 1000
|
NOT_CONNECTED = 1000
|
||||||
CONNECTING = 2000
|
CONNECTING = 2000
|
||||||
@@ -182,7 +221,7 @@ class Connectivity(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class KeyGenType(IntEnum):
|
class KeyGenType(IntEnum):
|
||||||
"""Type of the key to generate"""
|
"""Type of the key to generate."""
|
||||||
|
|
||||||
DEFAULT = 0
|
DEFAULT = 0
|
||||||
RSA2048 = 1
|
RSA2048 = 1
|
||||||
@@ -192,21 +231,21 @@ class KeyGenType(IntEnum):
|
|||||||
|
|
||||||
# "Lp" means "login parameters"
|
# "Lp" means "login parameters"
|
||||||
class LpAuthFlag(IntEnum):
|
class LpAuthFlag(IntEnum):
|
||||||
"""Authorization flags"""
|
"""Authorization flags."""
|
||||||
|
|
||||||
OAUTH2 = 0x2
|
OAUTH2 = 0x2
|
||||||
NORMAL = 0x4
|
NORMAL = 0x4
|
||||||
|
|
||||||
|
|
||||||
class MediaQuality(IntEnum):
|
class MediaQuality(IntEnum):
|
||||||
"""Media quality setting"""
|
"""Media quality setting."""
|
||||||
|
|
||||||
BALANCED = 0
|
BALANCED = 0
|
||||||
WORSE = 1
|
WORSE = 1
|
||||||
|
|
||||||
|
|
||||||
class ProviderStatus(IntEnum):
|
class ProviderStatus(IntEnum):
|
||||||
"""Provider status according to manual testing"""
|
"""Provider status according to manual testing."""
|
||||||
|
|
||||||
OK = 1
|
OK = 1
|
||||||
PREPARATION = 2
|
PREPARATION = 2
|
||||||
@@ -214,7 +253,7 @@ class ProviderStatus(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class PushNotifyState(IntEnum):
|
class PushNotifyState(IntEnum):
|
||||||
"""Push notifications state"""
|
"""Push notifications state."""
|
||||||
|
|
||||||
NOT_CONNECTED = 0
|
NOT_CONNECTED = 0
|
||||||
HEARTBEAT = 1
|
HEARTBEAT = 1
|
||||||
@@ -222,7 +261,7 @@ class PushNotifyState(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class ShowEmails(IntEnum):
|
class ShowEmails(IntEnum):
|
||||||
"""Show emails mode"""
|
"""Show emails mode."""
|
||||||
|
|
||||||
OFF = 0
|
OFF = 0
|
||||||
ACCEPTED_CONTACTS = 1
|
ACCEPTED_CONTACTS = 1
|
||||||
@@ -230,7 +269,7 @@ class ShowEmails(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class SocketSecurity(IntEnum):
|
class SocketSecurity(IntEnum):
|
||||||
"""Socket security"""
|
"""Socket security."""
|
||||||
|
|
||||||
AUTOMATIC = 0
|
AUTOMATIC = 0
|
||||||
SSL = 1
|
SSL = 1
|
||||||
@@ -239,7 +278,7 @@ class SocketSecurity(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class VideochatType(IntEnum):
|
class VideochatType(IntEnum):
|
||||||
"""Video chat URL type"""
|
"""Video chat URL type."""
|
||||||
|
|
||||||
UNKNOWN = 0
|
UNKNOWN = 0
|
||||||
BASICWEBRTC = 1
|
BASICWEBRTC = 1
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""Contact module."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -11,8 +13,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Contact:
|
class Contact:
|
||||||
"""
|
"""Contact API.
|
||||||
Contact API.
|
|
||||||
|
|
||||||
Essentially a wrapper for RPC, account ID and a contact ID.
|
Essentially a wrapper for RPC, account ID and a contact ID.
|
||||||
"""
|
"""
|
||||||
@@ -36,17 +37,14 @@ class Contact:
|
|||||||
"""Delete contact."""
|
"""Delete contact."""
|
||||||
self._rpc.delete_contact(self.account.id, self.id)
|
self._rpc.delete_contact(self.account.id, self.id)
|
||||||
|
|
||||||
def reset_encryption(self) -> None:
|
|
||||||
"""Reset contact encryption."""
|
|
||||||
self._rpc.reset_contact_encryption(self.account.id, self.id)
|
|
||||||
|
|
||||||
def set_name(self, name: str) -> None:
|
def set_name(self, name: str) -> None:
|
||||||
"""Change the name of this contact."""
|
"""Change the name of this contact."""
|
||||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||||
|
|
||||||
def get_encryption_info(self) -> str:
|
def get_encryption_info(self) -> str:
|
||||||
"""Get a multi-line encryption info, containing your fingerprint and
|
"""Get a multi-line encryption info.
|
||||||
the fingerprint of the contact.
|
|
||||||
|
Encryption info contains your fingerprint and the fingerprint of the contact.
|
||||||
"""
|
"""
|
||||||
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||||
|
|
||||||
@@ -66,4 +64,5 @@ class Contact:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def make_vcard(self) -> str:
|
def make_vcard(self) -> str:
|
||||||
return self._rpc.make_vcard(self.account.id, [self.id])
|
"""Make a vCard for the contact."""
|
||||||
|
return self.account.make_vcard([self])
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""Account manager module."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -10,12 +12,13 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class DeltaChat:
|
class DeltaChat:
|
||||||
"""
|
"""Delta Chat accounts manager.
|
||||||
Delta Chat accounts manager.
|
|
||||||
This is the root of the object oriented API.
|
This is the root of the object oriented API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rpc: "Rpc") -> None:
|
def __init__(self, rpc: "Rpc") -> None:
|
||||||
|
"""Initialize account manager."""
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
|
|
||||||
def add_account(self) -> Account:
|
def add_account(self) -> Account:
|
||||||
@@ -37,9 +40,7 @@ class DeltaChat:
|
|||||||
self.rpc.stop_io_for_all_accounts()
|
self.rpc.stop_io_for_all_accounts()
|
||||||
|
|
||||||
def maybe_network(self) -> None:
|
def maybe_network(self) -> None:
|
||||||
"""Indicate that the network likely has come back or just that the network
|
"""Indicate that the network conditions might have changed."""
|
||||||
conditions might have changed.
|
|
||||||
"""
|
|
||||||
self.rpc.maybe_network()
|
self.rpc.maybe_network()
|
||||||
|
|
||||||
def get_system_info(self) -> AttrDict:
|
def get_system_info(self) -> AttrDict:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class EventFilter(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
"""Object's unique hash"""
|
"""Object's unique hash."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other) -> bool:
|
||||||
@@ -52,9 +52,7 @@ class EventFilter(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def filter(self, event):
|
def filter(self, event):
|
||||||
"""Return True-like value if the event passed the filter and should be
|
"""Return True-like value if the event passed the filter."""
|
||||||
used, or False-like value otherwise.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class RawEvent(EventFilter):
|
class RawEvent(EventFilter):
|
||||||
@@ -82,31 +80,17 @@ class RawEvent(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
|
"""Filter an event.
|
||||||
|
|
||||||
|
Return true if the event should be processed.
|
||||||
|
"""
|
||||||
if self.types and event.kind 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)
|
||||||
|
|
||||||
|
|
||||||
class NewMessage(EventFilter):
|
class NewMessage(EventFilter):
|
||||||
"""Matches whenever a new message arrives.
|
"""Matches whenever a new message arrives."""
|
||||||
|
|
||||||
Warning: registering a handler for this event will cause the messages
|
|
||||||
to be marked as read. Its usage is mainly intended for bots.
|
|
||||||
|
|
||||||
:param pattern: if set, this Pattern will be used to filter the message by its text
|
|
||||||
content.
|
|
||||||
:param command: If set, only match messages with the given command (ex. /help).
|
|
||||||
Setting this property implies `is_info==False`.
|
|
||||||
:param is_bot: If set to True only match messages sent by bots, if set to None
|
|
||||||
match messages from bots and users. If omitted or set to False
|
|
||||||
only messages from users will be matched.
|
|
||||||
:param is_info: If set to True only match info/system messages, if set to False
|
|
||||||
only match messages that are not info/system messages. If omitted
|
|
||||||
info/system messages as well as normal messages will be matched.
|
|
||||||
:param func: A Callable function that should accept the event as input
|
|
||||||
parameter, and return a bool value indicating whether the event
|
|
||||||
should be dispatched or not.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -121,6 +105,25 @@ class NewMessage(EventFilter):
|
|||||||
is_info: Optional[bool] = None,
|
is_info: Optional[bool] = None,
|
||||||
func: Optional[Callable[["AttrDict"], bool]] = None,
|
func: Optional[Callable[["AttrDict"], bool]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Initialize a new message filter.
|
||||||
|
|
||||||
|
Warning: registering a handler for this event will cause the messages
|
||||||
|
to be marked as read. Its usage is mainly intended for bots.
|
||||||
|
|
||||||
|
:param pattern: if set, this Pattern will be used to filter the message by its text
|
||||||
|
content.
|
||||||
|
:param command: If set, only match messages with the given command (ex. /help).
|
||||||
|
Setting this property implies `is_info==False`.
|
||||||
|
:param is_bot: If set to True only match messages sent by bots, if set to None
|
||||||
|
match messages from bots and users. If omitted or set to False
|
||||||
|
only messages from users will be matched.
|
||||||
|
:param is_info: If set to True only match info/system messages, if set to False
|
||||||
|
only match messages that are not info/system messages. If omitted
|
||||||
|
info/system messages as well as normal messages will be matched.
|
||||||
|
:param func: A Callable function that should accept the event as input
|
||||||
|
parameter, and return a bool value indicating whether the event
|
||||||
|
should be dispatched or not.
|
||||||
|
"""
|
||||||
super().__init__(func=func)
|
super().__init__(func=func)
|
||||||
self.is_bot = is_bot
|
self.is_bot = is_bot
|
||||||
self.is_info = is_info
|
self.is_info = is_info
|
||||||
@@ -159,6 +162,7 @@ class NewMessage(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
|
"""Return true if if the event is a new message event."""
|
||||||
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
||||||
return False
|
return False
|
||||||
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
||||||
@@ -199,6 +203,7 @@ class MemberListChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
|
"""Return true if if the event is a member addition event."""
|
||||||
if self.added is not None and self.added != event.member_added:
|
if self.added is not None and self.added != event.member_added:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
@@ -231,6 +236,7 @@ class GroupImageChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
|
"""Return True if event is matched."""
|
||||||
if self.deleted is not None and self.deleted != event.image_deleted:
|
if self.deleted is not None and self.deleted != event.image_deleted:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
@@ -256,13 +262,12 @@ class GroupNameChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
|
"""Return True if event is matched."""
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class HookCollection:
|
class HookCollection:
|
||||||
"""
|
"""Helper class to collect event hooks that can later be added to a Delta Chat client."""
|
||||||
Helper class to collect event hooks that can later be added to a Delta Chat client.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
"""Message module."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
|
|
||||||
from ._utils import AttrDict, futuremethod
|
from ._utils import AttrDict, futuremethod
|
||||||
from .const import EventType
|
from .const import EventType
|
||||||
@@ -37,6 +39,11 @@ class Message:
|
|||||||
snapshot["message"] = self
|
snapshot["message"] = self
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
|
def get_read_receipts(self) -> List[AttrDict]:
|
||||||
|
"""Get message read receipts."""
|
||||||
|
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
|
||||||
|
return [AttrDict(read_receipt) for read_receipt in read_receipts]
|
||||||
|
|
||||||
def get_reactions(self) -> Optional[AttrDict]:
|
def get_reactions(self) -> Optional[AttrDict]:
|
||||||
"""Get message reactions."""
|
"""Get message reactions."""
|
||||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||||
@@ -45,6 +52,7 @@ class Message:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_sender_contact(self) -> Contact:
|
def get_sender_contact(self) -> Contact:
|
||||||
|
"""Return sender contact."""
|
||||||
from_id = self.get_snapshot().from_id
|
from_id = self.get_snapshot().from_id
|
||||||
return self.account.get_contact_by_id(from_id)
|
return self.account.get_contact_by_id(from_id)
|
||||||
|
|
||||||
@@ -52,6 +60,14 @@ class Message:
|
|||||||
"""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])
|
||||||
|
|
||||||
|
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||||
|
"""Continue the Autocrypt Setup Message key transfer.
|
||||||
|
|
||||||
|
This function can be called on received Autocrypt Setup Message
|
||||||
|
to import the key encrypted with the provided setup code.
|
||||||
|
"""
|
||||||
|
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
|
||||||
|
|
||||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||||
"""Send a webxdc status update. This message must be a webxdc."""
|
"""Send a webxdc status update. This message must be a webxdc."""
|
||||||
if not isinstance(update, str):
|
if not isinstance(update, str):
|
||||||
@@ -59,9 +75,15 @@ class Message:
|
|||||||
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||||
|
|
||||||
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||||
|
"""Return a list of Webxdc status updates for Webxdc instance message."""
|
||||||
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||||
|
|
||||||
|
def get_info(self) -> str:
|
||||||
|
"""Return message info."""
|
||||||
|
return self._rpc.get_message_info(self.account.id, self.id)
|
||||||
|
|
||||||
def get_webxdc_info(self) -> dict:
|
def get_webxdc_info(self) -> dict:
|
||||||
|
"""Get info from a Webxdc message in JSON format."""
|
||||||
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:
|
def wait_until_delivered(self) -> None:
|
||||||
@@ -73,8 +95,10 @@ class Message:
|
|||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def send_webxdc_realtime_advertisement(self):
|
def send_webxdc_realtime_advertisement(self):
|
||||||
|
"""Send an advertisement to join the realtime channel."""
|
||||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def send_webxdc_realtime_data(self, data) -> None:
|
def send_webxdc_realtime_data(self, data) -> None:
|
||||||
|
"""Send data to the realtime channel."""
|
||||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||||
|
|||||||
@@ -1,65 +1,74 @@
|
|||||||
|
"""Pytest plugin module."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
from typing import AsyncGenerator, Optional
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||||
from ._utils import futuremethod
|
from ._utils import futuremethod
|
||||||
from .rpc import Rpc
|
from .rpc import Rpc
|
||||||
|
|
||||||
|
E2EE_INFO_MSGS = 1
|
||||||
def get_temp_credentials() -> dict:
|
"""
|
||||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
The number of info messages added to new e2ee chats.
|
||||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
Currently this is "End-to-end encryption available".
|
||||||
password = f"{username}${username}"
|
"""
|
||||||
addr = f"{username}@{domain}"
|
|
||||||
return {"email": addr, "password": password}
|
|
||||||
|
|
||||||
|
|
||||||
class ACFactory:
|
class ACFactory:
|
||||||
|
"""Test account factory."""
|
||||||
|
|
||||||
def __init__(self, deltachat: DeltaChat) -> None:
|
def __init__(self, deltachat: DeltaChat) -> None:
|
||||||
self.deltachat = deltachat
|
self.deltachat = deltachat
|
||||||
|
|
||||||
def get_unconfigured_account(self) -> Account:
|
def get_unconfigured_account(self) -> Account:
|
||||||
|
"""Create a new unconfigured account."""
|
||||||
account = self.deltachat.add_account()
|
account = self.deltachat.add_account()
|
||||||
account.set_config("verified_one_on_one_chats", "1")
|
account.set_config("verified_one_on_one_chats", "1")
|
||||||
return account
|
return account
|
||||||
|
|
||||||
def get_unconfigured_bot(self) -> Bot:
|
def get_unconfigured_bot(self) -> Bot:
|
||||||
|
"""Create a new unconfigured bot."""
|
||||||
return Bot(self.get_unconfigured_account())
|
return Bot(self.get_unconfigured_account())
|
||||||
|
|
||||||
def new_preconfigured_account(self) -> Account:
|
def get_credentials(self) -> (str, str):
|
||||||
"""Make a new account with configuration options set, but configuration not started."""
|
"""Generate new credentials for chatmail account."""
|
||||||
credentials = get_temp_credentials()
|
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||||
account = self.get_unconfigured_account()
|
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||||
account.set_config("addr", credentials["email"])
|
return f"{username}@{domain}", f"{username}${username}"
|
||||||
account.set_config("mail_pw", credentials["password"])
|
|
||||||
assert not account.is_configured()
|
|
||||||
return account
|
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def new_configured_account(self):
|
def new_configured_account(self):
|
||||||
account = self.new_preconfigured_account()
|
"""Create a new configured account."""
|
||||||
yield account.configure.future()
|
addr, password = self.get_credentials()
|
||||||
|
account = self.get_unconfigured_account()
|
||||||
|
params = {"addr": addr, "password": password}
|
||||||
|
yield account.add_or_update_transport.future(params)
|
||||||
|
|
||||||
assert account.is_configured()
|
assert account.is_configured()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
def new_configured_bot(self) -> Bot:
|
def new_configured_bot(self) -> Bot:
|
||||||
credentials = get_temp_credentials()
|
"""Create a new configured bot."""
|
||||||
|
addr, password = self.get_credentials()
|
||||||
bot = self.get_unconfigured_bot()
|
bot = self.get_unconfigured_bot()
|
||||||
bot.configure(credentials["email"], credentials["password"])
|
bot.configure(addr, password)
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def get_online_account(self):
|
def get_online_account(self):
|
||||||
|
"""Create a new account and start I/O."""
|
||||||
account = yield self.new_configured_account.future()
|
account = yield self.new_configured_account.future()
|
||||||
account.bring_online()
|
account.bring_online()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
def get_online_accounts(self, num: int) -> list[Account]:
|
def get_online_accounts(self, num: int) -> list[Account]:
|
||||||
|
"""Create multiple online accounts."""
|
||||||
futures = [self.get_online_account.future() for _ in range(num)]
|
futures = [self.get_online_account.future() for _ in range(num)]
|
||||||
return [f() for f in futures]
|
return [f() for f in futures]
|
||||||
|
|
||||||
@@ -74,6 +83,10 @@ class ACFactory:
|
|||||||
return ac_clone
|
return ac_clone
|
||||||
|
|
||||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||||
|
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
|
||||||
|
|
||||||
|
Returned chat is a chat with ac2 from ac1 point of view.
|
||||||
|
"""
|
||||||
ac2.create_chat(ac1)
|
ac2.create_chat(ac1)
|
||||||
return ac1.create_chat(ac2)
|
return ac1.create_chat(ac2)
|
||||||
|
|
||||||
@@ -85,9 +98,10 @@ class ACFactory:
|
|||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
group: Optional[str] = None,
|
group: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
|
"""Send a message."""
|
||||||
if not from_account:
|
if not from_account:
|
||||||
from_account = (self.get_online_accounts(1))[0]
|
from_account = (self.get_online_accounts(1))[0]
|
||||||
to_contact = from_account.create_contact(to_account.get_config("addr"))
|
to_contact = from_account.create_contact(to_account)
|
||||||
if group:
|
if group:
|
||||||
to_chat = from_account.create_group(group)
|
to_chat = from_account.create_group(group)
|
||||||
to_chat.add_contact(to_contact)
|
to_chat.add_contact(to_contact)
|
||||||
@@ -103,6 +117,7 @@ class ACFactory:
|
|||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
group: Optional[str] = None,
|
group: Optional[str] = None,
|
||||||
) -> AttrDict:
|
) -> AttrDict:
|
||||||
|
"""Send a message and wait until recipient processes it."""
|
||||||
self.send_message(
|
self.send_message(
|
||||||
to_account=to_client.account,
|
to_account=to_client.account,
|
||||||
from_account=from_account,
|
from_account=from_account,
|
||||||
@@ -116,6 +131,7 @@ class ACFactory:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def rpc(tmp_path) -> AsyncGenerator:
|
def rpc(tmp_path) -> AsyncGenerator:
|
||||||
|
"""RPC client fixture."""
|
||||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||||
with rpc_server:
|
with rpc_server:
|
||||||
yield rpc_server
|
yield rpc_server
|
||||||
@@ -123,4 +139,52 @@ def rpc(tmp_path) -> AsyncGenerator:
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def acfactory(rpc) -> AsyncGenerator:
|
def acfactory(rpc) -> AsyncGenerator:
|
||||||
|
"""Return account factory fixture."""
|
||||||
return ACFactory(DeltaChat(rpc))
|
return ACFactory(DeltaChat(rpc))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def data():
|
||||||
|
"""Test data."""
|
||||||
|
|
||||||
|
class Data:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
for path in reversed(py.path.local(__file__).parts()):
|
||||||
|
datadir = path.join("test-data")
|
||||||
|
if datadir.isdir():
|
||||||
|
self.path = datadir
|
||||||
|
return
|
||||||
|
raise Exception("Data path cannot be found")
|
||||||
|
|
||||||
|
def get_path(self, bn):
|
||||||
|
"""Return path of file or None if it doesn't exist."""
|
||||||
|
fn = os.path.join(self.path, *bn.split("/"))
|
||||||
|
assert os.path.exists(fn)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
def read_path(self, bn, mode="r"):
|
||||||
|
fn = self.get_path(bn)
|
||||||
|
if fn is not None:
|
||||||
|
with open(fn, mode) as f:
|
||||||
|
return f.read()
|
||||||
|
return None
|
||||||
|
|
||||||
|
return Data()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def log():
|
||||||
|
"""Log printer fixture."""
|
||||||
|
|
||||||
|
class Printer:
|
||||||
|
def section(self, msg: str) -> None:
|
||||||
|
print()
|
||||||
|
print("=" * 10, msg, "=" * 10)
|
||||||
|
|
||||||
|
def step(self, msg: str) -> None:
|
||||||
|
print("-" * 5, "step " + msg, "-" * 5)
|
||||||
|
|
||||||
|
def indent(self, msg: str) -> None:
|
||||||
|
print(" " + msg)
|
||||||
|
|
||||||
|
return Printer()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""JSON-RPC client module."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
@@ -12,16 +14,19 @@ from typing import Any, Iterator, Optional
|
|||||||
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
pass
|
"""JSON-RPC error."""
|
||||||
|
|
||||||
|
|
||||||
class RpcFuture:
|
class RpcFuture:
|
||||||
|
"""RPC future waiting for RPC call result."""
|
||||||
|
|
||||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
self.request_id = request_id
|
self.request_id = request_id
|
||||||
self.event = event
|
self.event = event
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
|
"""Wait for the future to return the result."""
|
||||||
self.event.wait()
|
self.event.wait()
|
||||||
response = self.rpc.request_results.pop(self.request_id)
|
response = self.rpc.request_results.pop(self.request_id)
|
||||||
if "error" in response:
|
if "error" in response:
|
||||||
@@ -32,17 +37,19 @@ class RpcFuture:
|
|||||||
|
|
||||||
|
|
||||||
class RpcMethod:
|
class RpcMethod:
|
||||||
|
"""RPC method."""
|
||||||
|
|
||||||
def __init__(self, rpc: "Rpc", name: str):
|
def __init__(self, rpc: "Rpc", name: str):
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def __call__(self, *args) -> Any:
|
def __call__(self, *args) -> Any:
|
||||||
"""Synchronously calls JSON-RPC method."""
|
"""Call JSON-RPC method synchronously."""
|
||||||
future = self.future(*args)
|
future = self.future(*args)
|
||||||
return future()
|
return future()
|
||||||
|
|
||||||
def future(self, *args) -> Any:
|
def future(self, *args) -> Any:
|
||||||
"""Asynchronously calls JSON-RPC method."""
|
"""Call JSON-RPC method asynchronously."""
|
||||||
request_id = next(self.rpc.id_iterator)
|
request_id = next(self.rpc.id_iterator)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -58,8 +65,13 @@ class RpcMethod:
|
|||||||
|
|
||||||
|
|
||||||
class Rpc:
|
class Rpc:
|
||||||
|
"""RPC client."""
|
||||||
|
|
||||||
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()"""
|
"""Initialize RPC client.
|
||||||
|
|
||||||
|
The given arguments will be passed to subprocess.Popen().
|
||||||
|
"""
|
||||||
if accounts_dir:
|
if accounts_dir:
|
||||||
kwargs["env"] = {
|
kwargs["env"] = {
|
||||||
**kwargs.get("env", os.environ),
|
**kwargs.get("env", os.environ),
|
||||||
@@ -81,6 +93,7 @@ class Rpc:
|
|||||||
self.events_thread: Thread
|
self.events_thread: Thread
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
|
"""Start RPC server subprocess."""
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
self.process = subprocess.Popen(
|
self.process = subprocess.Popen(
|
||||||
"deltachat-rpc-server",
|
"deltachat-rpc-server",
|
||||||
@@ -130,11 +143,9 @@ class Rpc:
|
|||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def reader_loop(self) -> None:
|
def reader_loop(self) -> None:
|
||||||
|
"""Process JSON-RPC responses from the RPC server process output."""
|
||||||
try:
|
try:
|
||||||
while True:
|
while line := self.process.stdout.readline():
|
||||||
line = self.process.stdout.readline()
|
|
||||||
if not line: # EOF
|
|
||||||
break
|
|
||||||
response = json.loads(line)
|
response = json.loads(line)
|
||||||
if "id" in response:
|
if "id" in response:
|
||||||
response_id = response["id"]
|
response_id = response["id"]
|
||||||
@@ -150,10 +161,7 @@ class Rpc:
|
|||||||
def writer_loop(self) -> None:
|
def writer_loop(self) -> None:
|
||||||
"""Writer loop ensuring only a single thread writes requests."""
|
"""Writer loop ensuring only a single thread writes requests."""
|
||||||
try:
|
try:
|
||||||
while True:
|
while request := self.request_queue.get():
|
||||||
request = self.request_queue.get()
|
|
||||||
if not request:
|
|
||||||
break
|
|
||||||
data = (json.dumps(request) + "\n").encode()
|
data = (json.dumps(request) + "\n").encode()
|
||||||
self.process.stdin.write(data)
|
self.process.stdin.write(data)
|
||||||
self.process.stdin.flush()
|
self.process.stdin.flush()
|
||||||
@@ -163,12 +171,13 @@ class Rpc:
|
|||||||
logging.exception("Exception in the writer loop")
|
logging.exception("Exception in the writer loop")
|
||||||
|
|
||||||
def get_queue(self, account_id: int) -> Queue:
|
def get_queue(self, account_id: int) -> Queue:
|
||||||
|
"""Get event queue corresponding to the given account ID."""
|
||||||
if account_id not in self.event_queues:
|
if account_id not in self.event_queues:
|
||||||
self.event_queues[account_id] = Queue()
|
self.event_queues[account_id] = Queue()
|
||||||
return self.event_queues[account_id]
|
return self.event_queues[account_id]
|
||||||
|
|
||||||
def events_loop(self) -> None:
|
def events_loop(self) -> None:
|
||||||
"""Requests new events and distributes them between queues."""
|
"""Request new events and distributes them between queues."""
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
if self.closing:
|
if self.closing:
|
||||||
@@ -184,12 +193,12 @@ class Rpc:
|
|||||||
logging.exception("Exception in the event loop")
|
logging.exception("Exception in the event loop")
|
||||||
|
|
||||||
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||||
"""Waits for the next event from the given account and returns it."""
|
"""Wait for the next event from the given account and returns it."""
|
||||||
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):
|
def clear_all_events(self, account_id: int):
|
||||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
"""Remove all queued-up events for a given account. Useful for tests."""
|
||||||
queue = self.get_queue(account_id)
|
queue = self.get_queue(account_id)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ def test_event_on_configuration(acfactory: ACFactory) -> None:
|
|||||||
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
account = acfactory.new_preconfigured_account()
|
addr, password = acfactory.get_credentials()
|
||||||
|
account = acfactory.get_unconfigured_account()
|
||||||
account.clear_all_events()
|
account.clear_all_events()
|
||||||
assert not account.is_configured()
|
assert not account.is_configured()
|
||||||
future = account.configure.future()
|
future = account.add_or_update_transport.future({"addr": addr, "password": password})
|
||||||
while True:
|
while True:
|
||||||
event = account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
|
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ def test_delivery_status(acfactory: ACFactory) -> None:
|
|||||||
"""
|
"""
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
alice.clear_all_events()
|
alice.clear_all_events()
|
||||||
@@ -119,8 +118,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
|||||||
"""
|
"""
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_text("hi")
|
alice_chat_bob.send_text("hi")
|
||||||
|
|
||||||
@@ -150,18 +148,13 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
|||||||
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_text("hi")
|
alice_chat_bob.send_text("hi")
|
||||||
|
|
||||||
bob.wait_for_incoming_msg_event()
|
bob.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
alice_second_device: Account = acfactory.get_unconfigured_account()
|
alice_second_device = alice.clone()
|
||||||
|
|
||||||
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_second_device.start_io()
|
||||||
alice.clear_all_events()
|
alice.clear_all_events()
|
||||||
alice_second_device.clear_all_events()
|
alice_second_device.clear_all_events()
|
||||||
|
|||||||
@@ -175,17 +175,11 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
|||||||
|
|
||||||
threading.Thread(target=thread_run, daemon=True).start()
|
threading.Thread(target=thread_run, daemon=True).start()
|
||||||
|
|
||||||
while 1:
|
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||||
event = ac2.wait_for_event()
|
n = int(bytes(event.data).decode())
|
||||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
|
||||||
n = int(bytes(event.data).decode())
|
|
||||||
break
|
|
||||||
|
|
||||||
while 1:
|
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||||
event = ac2.wait_for_event()
|
assert int(bytes(event.data).decode()) > n
|
||||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
|
||||||
assert int(bytes(event.data).decode()) > n
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_reordering(acfactory, path_to_webxdc):
|
def test_no_reordering(acfactory, path_to_webxdc):
|
||||||
@@ -229,8 +223,5 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
|||||||
ac2_hello_msg_snapshot.chat.accept()
|
ac2_hello_msg_snapshot.chat.accept()
|
||||||
|
|
||||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||||
while 1:
|
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||||
event = ac1.wait_for_event()
|
assert event.msg_id == ac1_webxdc_msg.id
|
||||||
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
|
|
||||||
assert event.msg_id == ac1_webxdc_msg.id
|
|
||||||
break
|
|
||||||
|
|||||||
53
deltachat-rpc-client/tests/test_key_transfer.py
Normal file
53
deltachat-rpc-client/tests/test_key_transfer.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from deltachat_rpc_client import EventType
|
||||||
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_autocrypt_setup_message(account):
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0:
|
||||||
|
msg_id = event.msg_id
|
||||||
|
msg = account.get_message_by_id(msg_id)
|
||||||
|
if msg.get_snapshot().is_setupmessage:
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||||
|
alice1 = acfactory.get_online_account()
|
||||||
|
|
||||||
|
alice2 = acfactory.get_unconfigured_account()
|
||||||
|
alice2.set_config("addr", alice1.get_config("addr"))
|
||||||
|
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||||
|
alice2.configure()
|
||||||
|
alice2.bring_online()
|
||||||
|
|
||||||
|
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||||
|
msg = wait_for_autocrypt_setup_message(alice2)
|
||||||
|
|
||||||
|
# Test that entering wrong code returns an error.
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756")
|
||||||
|
|
||||||
|
msg.continue_autocrypt_key_transfer(setup_code)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ac_setup_message_twice(acfactory):
|
||||||
|
alice1 = acfactory.get_online_account()
|
||||||
|
|
||||||
|
alice2 = acfactory.get_unconfigured_account()
|
||||||
|
alice2.set_config("addr", alice1.get_config("addr"))
|
||||||
|
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||||
|
alice2.configure()
|
||||||
|
alice2.bring_online()
|
||||||
|
|
||||||
|
# Send the first Autocrypt Setup Message and ignore it.
|
||||||
|
_setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||||
|
wait_for_autocrypt_setup_message(alice2)
|
||||||
|
|
||||||
|
# Send the second Autocrypt Setup Message and import it.
|
||||||
|
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||||
|
msg = wait_for_autocrypt_setup_message(alice2)
|
||||||
|
|
||||||
|
msg.continue_autocrypt_key_transfer(setup_code)
|
||||||
113
deltachat-rpc-client/tests/test_multidevice.py
Normal file
113
deltachat-rpc-client/tests/test_multidevice.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
from imap_tools import AND
|
||||||
|
|
||||||
|
from deltachat_rpc_client import EventType
|
||||||
|
from deltachat_rpc_client.const import MessageState
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1_clone = ac1.clone()
|
||||||
|
ac1_clone.bring_online()
|
||||||
|
|
||||||
|
log.section("send out message without bcc to ourselves")
|
||||||
|
ac1.set_config("bcc_self", "0")
|
||||||
|
chat = ac1.create_chat(ac2)
|
||||||
|
self_addr = ac1.get_config("addr")
|
||||||
|
other_addr = ac2.get_config("addr")
|
||||||
|
|
||||||
|
msg_out = chat.send_text("message1")
|
||||||
|
assert not msg_out.get_snapshot().is_forwarded
|
||||||
|
|
||||||
|
# wait for send out (no BCC)
|
||||||
|
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||||
|
assert ac1.get_config("bcc_self") == "0"
|
||||||
|
|
||||||
|
assert self_addr not in ev.msg
|
||||||
|
assert other_addr in ev.msg
|
||||||
|
|
||||||
|
log.section("ac1: setting bcc_self=1")
|
||||||
|
ac1.set_config("bcc_self", "1")
|
||||||
|
|
||||||
|
log.section("send out message with bcc to ourselves")
|
||||||
|
msg_out = chat.send_text("message2")
|
||||||
|
|
||||||
|
# wait for send out (BCC)
|
||||||
|
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||||
|
assert ac1.get_config("bcc_self") == "1"
|
||||||
|
|
||||||
|
# Second client receives only second message, but not the first.
|
||||||
|
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
|
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||||
|
|
||||||
|
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
|
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||||
|
|
||||||
|
# now make sure we are sending message to ourselves too
|
||||||
|
assert self_addr in ev.msg
|
||||||
|
assert self_addr in ev.msg
|
||||||
|
|
||||||
|
# BCC-self messages are marked as seen by the sender device.
|
||||||
|
while True:
|
||||||
|
event = ac1.wait_for_event()
|
||||||
|
if event.kind == EventType.INFO and event.msg.endswith("Marked messages 1 in folder INBOX as seen."):
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check that the message is marked as seen on IMAP.
|
||||||
|
ac1_direct_imap = direct_imap(ac1)
|
||||||
|
ac1_direct_imap.connect()
|
||||||
|
ac1_direct_imap.select_folder("Inbox")
|
||||||
|
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_multidevice_sync_seen(acfactory, log):
|
||||||
|
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1_clone = ac1.clone()
|
||||||
|
ac1_clone.bring_online()
|
||||||
|
|
||||||
|
ac1.set_config("bcc_self", "1")
|
||||||
|
ac1_clone.set_config("bcc_self", "1")
|
||||||
|
|
||||||
|
ac1_chat = ac1.create_chat(ac2)
|
||||||
|
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||||
|
ac2_chat = ac2.create_chat(ac1)
|
||||||
|
|
||||||
|
log.section("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
||||||
|
ac2_chat.send_text("Hi")
|
||||||
|
ac1_message = ac1.wait_for_incoming_msg()
|
||||||
|
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
|
||||||
|
assert ac1_chat.get_fresh_message_count() == 1
|
||||||
|
assert ac1_clone_chat.get_fresh_message_count() == 1
|
||||||
|
assert ac1_message.get_snapshot().state == MessageState.IN_FRESH
|
||||||
|
assert ac1_clone_message.get_snapshot().state == MessageState.IN_FRESH
|
||||||
|
|
||||||
|
log.section("ac1 marks message as seen on the first device")
|
||||||
|
ac1.mark_seen_messages([ac1_message])
|
||||||
|
assert ac1_message.get_snapshot().state == MessageState.IN_SEEN
|
||||||
|
|
||||||
|
log.section("ac1 clone detects that message is marked as seen")
|
||||||
|
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
|
||||||
|
assert ev.chat_id == ac1_clone_chat.id
|
||||||
|
|
||||||
|
log.section("Send an ephemeral message from ac2 to ac1")
|
||||||
|
ac2_chat.set_ephemeral_timer(60)
|
||||||
|
ac1.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED)
|
||||||
|
ac1.wait_for_incoming_msg()
|
||||||
|
ac1_clone.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED)
|
||||||
|
ac1_clone.wait_for_incoming_msg()
|
||||||
|
|
||||||
|
ac2_chat.send_text("Foobar")
|
||||||
|
ac1_message = ac1.wait_for_incoming_msg()
|
||||||
|
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
|
||||||
|
assert "Ephemeral timer: 60\n" in ac1_message.get_info()
|
||||||
|
assert "Expires: " not in ac1_clone_message.get_info()
|
||||||
|
assert "Ephemeral timer: 60\n" in ac1_message.get_info()
|
||||||
|
assert "Expires: " not in ac1_clone_message.get_info()
|
||||||
|
|
||||||
|
ac1_message.mark_seen()
|
||||||
|
assert "Expires: " in ac1_message.get_info()
|
||||||
|
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
|
||||||
|
assert ev.chat_id == ac1_clone_chat.id
|
||||||
|
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
|
||||||
|
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||||
|
assert "Expires: " in ac1_clone_message.get_info()
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||||
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||||
@@ -15,28 +15,32 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
|||||||
alice.wait_for_securejoin_inviter_success()
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
# Test that Alice verified Bob's profile.
|
# Test that Alice verified Bob's profile.
|
||||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
alice_contact_bob = alice.create_contact(bob)
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert alice_contact_bob_snapshot.is_verified
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
# Test that Bob verified Alice's profile.
|
# Test that Bob verified Alice's profile.
|
||||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
bob_contact_alice = bob.create_contact(alice)
|
||||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
assert bob_contact_alice_snapshot.is_verified
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
# Test that if Bob changes the key, backwards verification is lost.
|
# Test that if Bob imports a key,
|
||||||
|
# backwards verification is not lost
|
||||||
|
# because default key is not changed.
|
||||||
logging.info("Bob 2 is created")
|
logging.info("Bob 2 is created")
|
||||||
bob2 = acfactory.new_configured_account()
|
bob2 = acfactory.new_configured_account()
|
||||||
bob2.export_self_keys(tmp_path)
|
bob2.export_self_keys(tmp_path)
|
||||||
|
|
||||||
logging.info("Bob imports a key")
|
logging.info("Bob tries to import a key")
|
||||||
bob.import_self_keys(tmp_path)
|
# Importing a second key is not allowed.
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
bob.import_self_keys(tmp_path)
|
||||||
|
|
||||||
assert bob.get_config("key_id") == "2"
|
assert bob.get_config("key_id") == "1"
|
||||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
assert not bob_contact_alice_snapshot.is_verified
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
def test_qr_setup_contact_svg(acfactory) -> None:
|
def test_qr_setup_contact_svg(acfactory) -> None:
|
||||||
@@ -55,15 +59,12 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("protect", [True, False])
|
@pytest.mark.parametrize("protect", [True, False])
|
||||||
def test_qr_securejoin(acfactory, protect, tmp_path):
|
def test_qr_securejoin(acfactory, protect):
|
||||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
# Setup second device for Alice
|
# Setup second device for Alice
|
||||||
# to test observing securejoin protocol.
|
# to test observing securejoin protocol.
|
||||||
alice.export_backup(tmp_path)
|
alice2 = alice.clone()
|
||||||
files = list(tmp_path.glob("*.tar"))
|
|
||||||
alice2 = acfactory.get_unconfigured_account()
|
|
||||||
alice2.import_backup(files[0])
|
|
||||||
|
|
||||||
logging.info("Alice creates a group")
|
logging.info("Alice creates a group")
|
||||||
alice_chat = alice.create_group("Group", protect=protect)
|
alice_chat = alice.create_group("Group", protect=protect)
|
||||||
@@ -74,30 +75,24 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
|
|||||||
bob.secure_join(qr_code)
|
bob.secure_join(qr_code)
|
||||||
|
|
||||||
# Alice deletes "vg-request".
|
# Alice deletes "vg-request".
|
||||||
while True:
|
alice.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||||
event = alice.wait_for_event()
|
|
||||||
if event["kind"] == "ImapMessageDeleted":
|
|
||||||
break
|
|
||||||
alice.wait_for_securejoin_inviter_success()
|
alice.wait_for_securejoin_inviter_success()
|
||||||
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
|
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
|
||||||
for ac in [alice, bob]:
|
for ac in [alice, bob]:
|
||||||
while True:
|
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||||
event = ac.wait_for_event()
|
|
||||||
if event["kind"] == "ImapMessageDeleted":
|
|
||||||
break
|
|
||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
# Test that Alice verified Bob's profile.
|
# Test that Alice verified Bob's profile.
|
||||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
alice_contact_bob = alice.create_contact(bob)
|
||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert alice_contact_bob_snapshot.is_verified
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
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.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||||
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
||||||
|
|
||||||
# Test that Bob verified Alice's profile.
|
# Test that Bob verified Alice's profile.
|
||||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
bob_contact_alice = bob.create_contact(alice)
|
||||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
assert bob_contact_alice_snapshot.is_verified
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
@@ -105,7 +100,7 @@ def test_qr_securejoin(acfactory, protect, tmp_path):
|
|||||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||||
alice2.start_io()
|
alice2.start_io()
|
||||||
alice2.wait_for_securejoin_inviter_success()
|
alice2.wait_for_securejoin_inviter_success()
|
||||||
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
|
alice2_contact_bob = alice2.create_contact(bob)
|
||||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||||
assert alice2_contact_bob_snapshot.is_verified
|
assert alice2_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
@@ -121,8 +116,7 @@ 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 invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
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!")
|
||||||
|
|
||||||
@@ -159,11 +153,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
|||||||
logging.info("Alice creates a verified group")
|
logging.info("Alice creates a verified group")
|
||||||
group = alice.create_group("Group", protect=True)
|
group = alice.create_group("Group", protect=True)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
charlie_addr = charlie.get_config("addr")
|
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
||||||
|
|
||||||
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_bob)
|
||||||
group.add_contact(alice_contact_charlie)
|
group.add_contact(alice_contact_charlie)
|
||||||
@@ -190,7 +181,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
|||||||
charlie_snapshot = charlie_message.get_snapshot()
|
charlie_snapshot = charlie_message.get_snapshot()
|
||||||
assert charlie_snapshot.text == "Hi from Bob!"
|
assert charlie_snapshot.text == "Hi from Bob!"
|
||||||
|
|
||||||
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
|
bob_contact_charlie = bob.create_contact(charlie, "Charlie")
|
||||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||||
|
|
||||||
logging.info("Charlie reads Bob's message")
|
logging.info("Charlie reads Bob's message")
|
||||||
@@ -221,72 +212,8 @@ def test_setup_contact_resetup(acfactory) -> None:
|
|||||||
bob.wait_for_securejoin_joiner_success()
|
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 = 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 = 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:
|
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||||
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
|
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
logging.info("ac1 creates verified group")
|
logging.info("ac1 creates verified group")
|
||||||
@@ -299,7 +226,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
ac2.wait_for_securejoin_joiner_success()
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
# ac1 has ac2 directly verified.
|
# ac1 has ac2 directly verified.
|
||||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||||
|
|
||||||
logging.info("ac3 joins verified group")
|
logging.info("ac3 joins verified group")
|
||||||
@@ -307,6 +234,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
ac3.wait_for_securejoin_joiner_success()
|
ac3.wait_for_securejoin_joiner_success()
|
||||||
ac3.wait_for_incoming_msg_event() # Member added
|
ac3.wait_for_incoming_msg_event() # Member added
|
||||||
|
|
||||||
|
ac3_contact_ac2_old = ac3.create_contact(ac2)
|
||||||
|
|
||||||
logging.info("ac2 logs in on a new device")
|
logging.info("ac2 logs in on a new device")
|
||||||
ac2 = acfactory.resetup_account(ac2)
|
ac2 = acfactory.resetup_account(ac2)
|
||||||
|
|
||||||
@@ -319,21 +248,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
assert len(ac3_chat.get_contacts()) == 3
|
assert len(ac3_chat.get_contacts()) == 3
|
||||||
ac3_chat.send_text("Hi!")
|
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!
|
ac1.wait_for_incoming_msg_event() # Hi!
|
||||||
|
|
||||||
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
ac3_contact_ac2 = ac3.create_contact(ac2)
|
||||||
ac3_chat.remove_contact(ac3_contact_ac2)
|
ac3_chat.remove_contact(ac3_contact_ac2_old)
|
||||||
|
|
||||||
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()
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert "removed" in snapshot.text
|
assert "removed" in snapshot.text
|
||||||
@@ -362,19 +280,16 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Works again!"
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||||
|
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||||
assert ac1_contact_ac2_snapshot.is_verified
|
assert ac1_contact_ac2_snapshot.is_verified
|
||||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
|
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.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):
|
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||||
"""Regression test for
|
"""Regression test for
|
||||||
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
|
issue <https://github.com/chatmail/core/issues/4894>.
|
||||||
"""
|
"""
|
||||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||||
|
|
||||||
@@ -408,12 +323,12 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
|||||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||||
|
|
||||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
# 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 ac3.create_contact(ac2).get_snapshot().is_verified
|
||||||
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
|
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||||
|
|
||||||
logging.info("ac3: create a verified group VG with ac2")
|
logging.info("ac3: create a verified group VG with ac2")
|
||||||
vg = ac3.create_group("ac3-created", protect=True)
|
vg = ac3.create_group("ac3-created", protect=True)
|
||||||
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
|
vg.add_contact(ac3.create_contact(ac2))
|
||||||
|
|
||||||
# ensure ac2 receives message in VG
|
# ensure ac2 receives message in VG
|
||||||
vg.send_text("hello")
|
vg.send_text("hello")
|
||||||
@@ -451,7 +366,7 @@ def test_qr_new_group_unblocked(acfactory):
|
|||||||
ac1.wait_for_securejoin_inviter_success()
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
ac1_new_chat = ac1.create_group("Another group")
|
ac1_new_chat = ac1.create_group("Another group")
|
||||||
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
|
ac1_new_chat.add_contact(ac1.create_contact(ac2))
|
||||||
# Receive "Member added" message.
|
# Receive "Member added" message.
|
||||||
ac2.wait_for_incoming_msg_event()
|
ac2.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
@@ -461,12 +376,12 @@ def test_qr_new_group_unblocked(acfactory):
|
|||||||
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="AEAP is disabled for now")
|
||||||
def test_aeap_flow_verified(acfactory):
|
def test_aeap_flow_verified(acfactory):
|
||||||
"""Test that a new address is added to a contact when it changes its address."""
|
"""Test that a new address is added to a contact when it changes its address."""
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
# ac1new is only used to get a new address.
|
addr, password = acfactory.get_credentials()
|
||||||
ac1new = acfactory.new_preconfigured_account()
|
|
||||||
|
|
||||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||||
chat = ac1.create_group("hello", protect=True)
|
chat = ac1.create_group("hello", protect=True)
|
||||||
@@ -486,8 +401,8 @@ def test_aeap_flow_verified(acfactory):
|
|||||||
assert msg_in_1.text == msg_out.text
|
assert msg_in_1.text == msg_out.text
|
||||||
|
|
||||||
logging.info("changing email account")
|
logging.info("changing email account")
|
||||||
ac1.set_config("addr", ac1new.get_config("addr"))
|
ac1.set_config("addr", addr)
|
||||||
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
ac1.set_config("mail_pw", password)
|
||||||
ac1.stop_io()
|
ac1.stop_io()
|
||||||
ac1.configure()
|
ac1.configure()
|
||||||
ac1.start_io()
|
ac1.start_io()
|
||||||
@@ -500,11 +415,9 @@ def test_aeap_flow_verified(acfactory):
|
|||||||
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||||
assert msg_in_2_snapshot.text == msg_out.text
|
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_snapshot.chat.id == msg_in_1.chat.id
|
||||||
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
|
assert msg_in_2.get_sender_contact().get_snapshot().address == addr
|
||||||
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
|
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
|
||||||
assert ac1new.get_config("addr") in [
|
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
|
||||||
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_gossip_verification(acfactory) -> None:
|
def test_gossip_verification(acfactory) -> None:
|
||||||
@@ -520,9 +433,9 @@ def test_gossip_verification(acfactory) -> None:
|
|||||||
bob.secure_join(qr_code)
|
bob.secure_join(qr_code)
|
||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
|
bob_contact_alice = bob.create_contact(alice, "Alice")
|
||||||
bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
|
bob_contact_carol = bob.create_contact(carol, "Carol")
|
||||||
carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
|
carol_contact_alice = carol.create_contact(alice, "Alice")
|
||||||
|
|
||||||
logging.info("Bob creates an Autocrypt group")
|
logging.info("Bob creates an Autocrypt group")
|
||||||
bob_group_chat = bob.create_group("Autocrypt Group")
|
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||||
@@ -573,7 +486,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
|
|
||||||
# ac1 waits for member added message and creates a QR code.
|
# 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()
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
|
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||||
|
|
||||||
# ac2 verifies ac1
|
# ac2 verifies ac1
|
||||||
@@ -582,35 +495,13 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
ac2.wait_for_securejoin_joiner_success()
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
# ac1 is verified for ac2.
|
# ac1 is verified for ac2.
|
||||||
ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "")
|
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||||
assert ac2_contact_ac1.get_snapshot().is_verified
|
assert ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
# ac1 resetups the account.
|
# ac1 resetups the account.
|
||||||
ac1 = acfactory.resetup_account(ac1)
|
ac1 = acfactory.resetup_account(ac1)
|
||||||
|
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||||
# Loop sending message from ac1 to ac2
|
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||||
# until ac2 accepts new ac1 key.
|
|
||||||
#
|
|
||||||
# This may not happen immediately because resetup of ac1
|
|
||||||
# rewinds "smeared timestamp" so Date: header for messages
|
|
||||||
# sent by new ac1 are in the past compared to the last Date:
|
|
||||||
# header sent by old ac1.
|
|
||||||
while True:
|
|
||||||
# ac1 sends a message to ac2.
|
|
||||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
|
||||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
|
||||||
ac1_chat_ac2.send_text("Hello!")
|
|
||||||
|
|
||||||
# ac2 receives a message.
|
|
||||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
|
||||||
assert snapshot.text == "Hello!"
|
|
||||||
logging.info("ac2 received Hello!")
|
|
||||||
|
|
||||||
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
|
||||||
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
|
|
||||||
if not ac2_contact_ac1.get_snapshot().is_verified:
|
|
||||||
break
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# ac1 goes offline.
|
# ac1 goes offline.
|
||||||
ac1.remove()
|
ac1.remove()
|
||||||
@@ -653,12 +544,14 @@ def test_withdraw_securejoin_qr(acfactory):
|
|||||||
bob_chat = bob.secure_join(qr_code)
|
bob_chat = bob.secure_join(qr_code)
|
||||||
bob.wait_for_securejoin_joiner_success()
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
|
||||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
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.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||||
bob_chat.leave()
|
bob_chat.leave()
|
||||||
|
|
||||||
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||||
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||||
|
|
||||||
logging.info("Alice withdraws QR code.")
|
logging.info("Alice withdraws QR code.")
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ from unittest.mock import MagicMock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
|
||||||
|
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@@ -61,61 +62,102 @@ def test_acfactory(acfactory) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_configure_starttls(acfactory) -> None:
|
def test_configure_starttls(acfactory) -> None:
|
||||||
account = acfactory.new_preconfigured_account()
|
addr, password = acfactory.get_credentials()
|
||||||
|
account = acfactory.get_unconfigured_account()
|
||||||
# Use STARTTLS
|
account.add_or_update_transport(
|
||||||
account.set_config("mail_security", "2")
|
{
|
||||||
account.set_config("send_security", "2")
|
"addr": addr,
|
||||||
account.configure()
|
"password": password,
|
||||||
|
"imapSecurity": "starttls",
|
||||||
|
"smtpSecurity": "starttls",
|
||||||
|
},
|
||||||
|
)
|
||||||
assert account.is_configured()
|
assert account.is_configured()
|
||||||
|
|
||||||
|
|
||||||
|
def test_lowercase_address(acfactory) -> None:
|
||||||
|
addr, password = acfactory.get_credentials()
|
||||||
|
addr_upper = addr.upper()
|
||||||
|
account = acfactory.get_unconfigured_account()
|
||||||
|
account.add_or_update_transport(
|
||||||
|
{
|
||||||
|
"addr": addr_upper,
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert account.is_configured()
|
||||||
|
assert addr_upper != addr
|
||||||
|
assert account.get_config("configured_addr") == addr
|
||||||
|
assert account.list_transports()[0]["addr"] == addr
|
||||||
|
|
||||||
|
for param in [
|
||||||
|
account.get_info()["used_account_settings"],
|
||||||
|
account.get_info()["entered_account_settings"],
|
||||||
|
]:
|
||||||
|
assert addr in param
|
||||||
|
assert addr_upper not in param
|
||||||
|
|
||||||
|
|
||||||
def test_configure_ip(acfactory) -> None:
|
def test_configure_ip(acfactory) -> None:
|
||||||
account = acfactory.new_preconfigured_account()
|
addr, password = acfactory.get_credentials()
|
||||||
|
account = acfactory.get_unconfigured_account()
|
||||||
|
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
|
||||||
|
|
||||||
domain = account.get_config("addr").rsplit("@")[-1]
|
|
||||||
ip_address = socket.gethostbyname(domain)
|
|
||||||
|
|
||||||
# This should fail TLS check.
|
|
||||||
account.set_config("mail_server", ip_address)
|
|
||||||
with pytest.raises(JsonRpcError):
|
with pytest.raises(JsonRpcError):
|
||||||
account.configure()
|
account.add_or_update_transport(
|
||||||
|
{
|
||||||
|
"addr": addr,
|
||||||
|
"password": password,
|
||||||
|
# This should fail TLS check.
|
||||||
|
"imapServer": ip_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_configure_alternative_port(acfactory) -> None:
|
def test_configure_alternative_port(acfactory) -> None:
|
||||||
"""Test that configuration with alternative port 443 works."""
|
"""Test that configuration with alternative port 443 works."""
|
||||||
account = acfactory.new_preconfigured_account()
|
addr, password = acfactory.get_credentials()
|
||||||
|
account = acfactory.get_unconfigured_account()
|
||||||
account.set_config("mail_port", "443")
|
account.add_or_update_transport(
|
||||||
account.set_config("send_port", "443")
|
{
|
||||||
|
"addr": addr,
|
||||||
account.configure()
|
"password": password,
|
||||||
|
"imapPort": 443,
|
||||||
|
"smtpPort": 443,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert account.is_configured()
|
||||||
|
|
||||||
|
|
||||||
def test_configure_username(acfactory) -> None:
|
def test_list_transports(acfactory) -> None:
|
||||||
account = acfactory.new_preconfigured_account()
|
addr, password = acfactory.get_credentials()
|
||||||
|
account = acfactory.get_unconfigured_account()
|
||||||
addr = account.get_config("addr")
|
account.add_or_update_transport(
|
||||||
account.set_config("mail_user", addr)
|
{
|
||||||
account.configure()
|
"addr": addr,
|
||||||
|
"password": password,
|
||||||
assert account.get_config("configured_mail_user") == addr
|
"imapUser": addr,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
transports = account.list_transports()
|
||||||
|
assert len(transports) == 1
|
||||||
|
params = transports[0]
|
||||||
|
assert params["addr"] == addr
|
||||||
|
assert params["password"] == password
|
||||||
|
assert params["imapUser"] == addr
|
||||||
|
|
||||||
|
|
||||||
def test_account(acfactory) -> None:
|
def test_account(acfactory) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
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.kind == 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()
|
||||||
@@ -129,7 +171,10 @@ def test_account(acfactory) -> None:
|
|||||||
assert alice.get_size()
|
assert alice.get_size()
|
||||||
assert alice.is_configured()
|
assert alice.is_configured()
|
||||||
assert not alice.get_avatar()
|
assert not alice.get_avatar()
|
||||||
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
# get_contact_by_addr() can lookup a key contact by address:
|
||||||
|
bob_contact = alice.get_contact_by_addr(bob_addr).get_snapshot()
|
||||||
|
assert bob_contact.display_name == "Bob"
|
||||||
|
assert bob_contact.is_key_contact
|
||||||
assert alice.get_contacts()
|
assert alice.get_contacts()
|
||||||
assert alice.get_contacts(snapshot=True)
|
assert alice.get_contacts(snapshot=True)
|
||||||
assert alice.self_contact
|
assert alice.self_contact
|
||||||
@@ -174,8 +219,7 @@ def test_account(acfactory) -> None:
|
|||||||
def test_chat(acfactory) -> None:
|
def test_chat(acfactory) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
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!")
|
||||||
|
|
||||||
@@ -231,7 +275,9 @@ def test_chat(acfactory) -> None:
|
|||||||
group.get_fresh_message_count()
|
group.get_fresh_message_count()
|
||||||
group.mark_noticed()
|
group.mark_noticed()
|
||||||
assert group.get_contacts()
|
assert group.get_contacts()
|
||||||
group.remove_contact(alice_chat_bob)
|
assert group.get_past_contacts() == []
|
||||||
|
group.remove_contact(alice_contact_bob)
|
||||||
|
assert len(group.get_past_contacts()) == 1
|
||||||
group.get_locations()
|
group.get_locations()
|
||||||
|
|
||||||
|
|
||||||
@@ -239,13 +285,12 @@ def test_contact(acfactory) -> None:
|
|||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
|
||||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||||
assert repr(alice_contact_bob)
|
assert repr(alice_contact_bob)
|
||||||
alice_contact_bob.block()
|
alice_contact_bob.block()
|
||||||
alice_contact_bob.unblock()
|
alice_contact_bob.unblock()
|
||||||
alice_contact_bob.reset_encryption()
|
|
||||||
alice_contact_bob.set_name("new name")
|
alice_contact_bob.set_name("new name")
|
||||||
alice_contact_bob.get_encryption_info()
|
alice_contact_bob.get_encryption_info()
|
||||||
snapshot = alice_contact_bob.get_snapshot()
|
snapshot = alice_contact_bob.get_snapshot()
|
||||||
@@ -256,8 +301,7 @@ def test_contact(acfactory) -> None:
|
|||||||
def test_message(acfactory) -> None:
|
def test_message(acfactory) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
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!")
|
||||||
|
|
||||||
@@ -285,28 +329,74 @@ def test_message(acfactory) -> None:
|
|||||||
assert reactions == snapshot.reactions
|
assert reactions == snapshot.reactions
|
||||||
|
|
||||||
|
|
||||||
|
def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||||
|
alice = acfactory.get_online_account()
|
||||||
|
|
||||||
|
log.section("Alice adds a second device")
|
||||||
|
alice2 = alice.clone()
|
||||||
|
|
||||||
|
log.section("Second device goes online")
|
||||||
|
alice2.start_io()
|
||||||
|
|
||||||
|
log.section("First device changes avatar")
|
||||||
|
image = data.get_path("image/avatar1000x1000.jpg")
|
||||||
|
alice.set_config("selfavatar", image)
|
||||||
|
avatar_config = alice.get_config("selfavatar")
|
||||||
|
avatar_hash = os.path.basename(avatar_config)
|
||||||
|
print("Info: avatar hash is ", avatar_hash)
|
||||||
|
|
||||||
|
log.section("First device receives avatar change")
|
||||||
|
alice2.wait_for_event(EventType.SELFAVATAR_CHANGED)
|
||||||
|
avatar_config2 = alice2.get_config("selfavatar")
|
||||||
|
avatar_hash2 = os.path.basename(avatar_config2)
|
||||||
|
print("Info: avatar hash on second device is ", avatar_hash2)
|
||||||
|
assert avatar_hash == avatar_hash2
|
||||||
|
assert avatar_config != avatar_config2
|
||||||
|
|
||||||
|
|
||||||
|
def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
alice2 = alice.clone()
|
||||||
|
alice2.start_io()
|
||||||
|
|
||||||
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
|
event = bob.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
|
||||||
|
message = bob.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
snapshot.chat.accept()
|
||||||
|
message.send_reaction("😎")
|
||||||
|
for a in [alice, alice2]:
|
||||||
|
a.wait_for_event(EventType.INCOMING_REACTION)
|
||||||
|
|
||||||
|
alice2.clear_all_events()
|
||||||
|
alice_chat_bob.mark_noticed()
|
||||||
|
chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id
|
||||||
|
alice2_chat_bob = alice2.create_chat(bob)
|
||||||
|
assert chat_id == alice2_chat_bob.id
|
||||||
|
|
||||||
|
|
||||||
def test_is_bot(acfactory) -> None:
|
def test_is_bot(acfactory) -> None:
|
||||||
"""Test that we can recognize messages submitted by bots."""
|
"""Test that we can recognize messages submitted by bots."""
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
# Alice becomes a bot.
|
# Alice becomes a bot.
|
||||||
alice.set_config("bot", "1")
|
alice.set_config("bot", "1")
|
||||||
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()
|
message = bob.get_message_by_id(event.msg_id)
|
||||||
if event.kind == EventType.INCOMING_MSG:
|
snapshot = message.get_snapshot()
|
||||||
msg_id = event.msg_id
|
assert snapshot.chat_id == event.chat_id
|
||||||
message = bob.get_message_by_id(msg_id)
|
assert snapshot.text == "Hello!"
|
||||||
snapshot = message.get_snapshot()
|
assert snapshot.is_bot
|
||||||
assert snapshot.chat_id == event.chat_id
|
|
||||||
assert snapshot.text == "Hello!"
|
|
||||||
assert snapshot.is_bot
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def test_bot(acfactory) -> None:
|
def test_bot(acfactory) -> None:
|
||||||
@@ -353,9 +443,11 @@ def test_wait_next_messages(acfactory) -> None:
|
|||||||
alice = acfactory.new_configured_account()
|
alice = acfactory.new_configured_account()
|
||||||
|
|
||||||
# Create a bot account so it does not receive device messages in the beginning.
|
# Create a bot account so it does not receive device messages in the beginning.
|
||||||
bot = acfactory.new_preconfigured_account()
|
addr, password = acfactory.get_credentials()
|
||||||
|
bot = acfactory.get_unconfigured_account()
|
||||||
bot.set_config("bot", "1")
|
bot.set_config("bot", "1")
|
||||||
bot.configure()
|
bot.add_or_update_transport({"addr": addr, "password": password})
|
||||||
|
assert bot.is_configured()
|
||||||
|
|
||||||
# There are no old messages and the call returns immediately.
|
# There are no old messages and the call returns immediately.
|
||||||
assert not bot.wait_next_messages()
|
assert not bot.wait_next_messages()
|
||||||
@@ -364,14 +456,17 @@ def test_wait_next_messages(acfactory) -> None:
|
|||||||
# Bot starts waiting for messages.
|
# Bot starts waiting for messages.
|
||||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||||
|
|
||||||
bot_addr = bot.get_config("addr")
|
alice_contact_bot = alice.create_contact(bot, "Bot")
|
||||||
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!")
|
||||||
|
|
||||||
next_messages = next_messages_task.result()
|
next_messages = next_messages_task.result()
|
||||||
assert len(next_messages) == 1
|
|
||||||
snapshot = next_messages[0].get_snapshot()
|
if len(next_messages) == E2EE_INFO_MSGS:
|
||||||
|
next_messages += bot.wait_next_messages()
|
||||||
|
|
||||||
|
assert len(next_messages) == 1 + E2EE_INFO_MSGS
|
||||||
|
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
@@ -389,9 +484,7 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
|
|||||||
def test_import_export_keys(acfactory, tmp_path) -> None:
|
def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
|
||||||
alice_chat_bob.send_text("Hello Bob!")
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
|
|
||||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
@@ -441,9 +534,7 @@ def test_provider_info(rpc) -> None:
|
|||||||
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
|
|
||||||
# Bob creates chat manually so chat with Alice is accepted.
|
# Bob creates chat manually so chat with Alice is accepted.
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
@@ -467,10 +558,7 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
|||||||
|
|
||||||
# Alice reads Bob's message.
|
# Alice reads Bob's message.
|
||||||
message.mark_seen()
|
message.mark_seen()
|
||||||
while True:
|
bob.wait_for_event(EventType.MSG_READ)
|
||||||
event = bob.wait_for_event()
|
|
||||||
if event.kind == EventType.MSG_READ:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Bob sends a message to Alice, it should also be encrypted.
|
# Bob sends a message to Alice, it should also be encrypted.
|
||||||
bob_chat_alice.send_text("Hi Alice!")
|
bob_chat_alice.send_text("Hi Alice!")
|
||||||
@@ -542,9 +630,13 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
|||||||
messages they refer to and thus dropped.
|
messages they refer to and thus dropped.
|
||||||
"""
|
"""
|
||||||
(ac1,) = acfactory.get_online_accounts(1)
|
(ac1,) = acfactory.get_online_accounts(1)
|
||||||
ac2 = acfactory.new_preconfigured_account()
|
|
||||||
ac2.configure()
|
addr, password = acfactory.get_credentials()
|
||||||
|
ac2 = acfactory.get_unconfigured_account()
|
||||||
|
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||||
ac2.set_config("mvbox_move", "1")
|
ac2.set_config("mvbox_move", "1")
|
||||||
|
assert ac2.is_configured()
|
||||||
|
|
||||||
ac2.bring_online()
|
ac2.bring_online()
|
||||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
ac2.stop_io()
|
ac2.stop_io()
|
||||||
@@ -588,9 +680,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
|||||||
chat.send_text("Hello 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!"
|
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(account)
|
||||||
contact = alice.create_contact(contact_addr, "")
|
|
||||||
|
|
||||||
alice_group.add_contact(contact)
|
alice_group.add_contact(contact)
|
||||||
|
|
||||||
if n_accounts == 2:
|
if n_accounts == 2:
|
||||||
@@ -621,7 +711,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
|||||||
assert snapshot.chat == bob_chat_alice
|
assert snapshot.chat == bob_chat_alice
|
||||||
|
|
||||||
|
|
||||||
def test_markseen_contact_request(acfactory, tmp_path):
|
def test_markseen_contact_request(acfactory):
|
||||||
"""
|
"""
|
||||||
Test that seen status is synchronized for contact request messages
|
Test that seen status is synchronized for contact request messages
|
||||||
even though read receipt is not sent.
|
even though read receipt is not sent.
|
||||||
@@ -629,10 +719,7 @@ def test_markseen_contact_request(acfactory, tmp_path):
|
|||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
# Bob sets up a second device.
|
# Bob sets up a second device.
|
||||||
bob.export_backup(tmp_path)
|
bob2 = bob.clone()
|
||||||
files = list(tmp_path.glob("*.tar"))
|
|
||||||
bob2 = acfactory.get_unconfigured_account()
|
|
||||||
bob2.import_backup(files[0])
|
|
||||||
bob2.start_io()
|
bob2.start_io()
|
||||||
|
|
||||||
alice_chat_bob = alice.create_chat(bob)
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
@@ -643,13 +730,30 @@ def test_markseen_contact_request(acfactory, tmp_path):
|
|||||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||||
|
|
||||||
message.mark_seen()
|
message.mark_seen()
|
||||||
while True:
|
bob2.wait_for_event(EventType.MSGS_NOTICED)
|
||||||
event = bob2.wait_for_event()
|
|
||||||
if event.kind == EventType.MSGS_NOTICED:
|
|
||||||
break
|
|
||||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_receipt(acfactory):
|
||||||
|
"""
|
||||||
|
Test sending a read receipt and ensure it is attributed to the correct contact.
|
||||||
|
"""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
alice_contact_bob = alice.create_contact(bob)
|
||||||
|
bob.create_chat(alice) # Accept the chat
|
||||||
|
|
||||||
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
|
msg = bob.wait_for_incoming_msg()
|
||||||
|
msg.mark_seen()
|
||||||
|
|
||||||
|
read_msg = alice.get_message_by_id(alice.wait_for_event(EventType.MSG_READ).msg_id)
|
||||||
|
read_receipts = read_msg.get_read_receipts()
|
||||||
|
assert len(read_receipts) == 1
|
||||||
|
assert read_receipts[0].contact_id == alice_contact_bob.id
|
||||||
|
|
||||||
|
|
||||||
def test_get_http_response(acfactory):
|
def test_get_http_response(acfactory):
|
||||||
alice = acfactory.new_configured_account()
|
alice = acfactory.new_configured_account()
|
||||||
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
|
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
|
||||||
@@ -659,12 +763,11 @@ def test_get_http_response(acfactory):
|
|||||||
|
|
||||||
def test_configured_imap_certificate_checks(acfactory):
|
def test_configured_imap_certificate_checks(acfactory):
|
||||||
alice = acfactory.new_configured_account()
|
alice = acfactory.new_configured_account()
|
||||||
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
|
|
||||||
|
|
||||||
# Certificate checks should be configured (not None)
|
# Certificate checks should be configured (not None)
|
||||||
assert configured_certificate_checks
|
assert "cert_automatic" in alice.get_info().used_account_settings
|
||||||
|
|
||||||
# 0 is the value old Delta Chat core versions used
|
# "cert_old_automatic" is the value old Delta Chat core versions used
|
||||||
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
||||||
# and configuration failed to use strict TLS checks
|
# and configuration failed to use strict TLS checks
|
||||||
# so it switched strict TLS checks off.
|
# so it switched strict TLS checks off.
|
||||||
@@ -675,4 +778,132 @@ def test_configured_imap_certificate_checks(acfactory):
|
|||||||
#
|
#
|
||||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||||
# This test is a regression test to prevent this happening again.
|
# This test is a regression test to prevent this happening again.
|
||||||
assert configured_certificate_checks != "0"
|
assert "cert_old_automatic" not in alice.get_info().used_account_settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_old_msg_is_fresh(acfactory):
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1_clone = ac1.clone()
|
||||||
|
ac1_clone.start_io()
|
||||||
|
|
||||||
|
ac1.create_chat(ac2)
|
||||||
|
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||||
|
|
||||||
|
ac1.get_device_chat().mark_noticed()
|
||||||
|
|
||||||
|
logging.info("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||||
|
first_msg = ac2.create_chat(ac1).send_text("Hi")
|
||||||
|
ac1.wait_for_incoming_msg_event()
|
||||||
|
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
|
||||||
|
assert len(list(ac1.get_fresh_messages())) == 1
|
||||||
|
|
||||||
|
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||||
|
|
||||||
|
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||||
|
ac1_clone_chat.send_text("Hi back")
|
||||||
|
ev = ac1.wait_for_msgs_noticed_event()
|
||||||
|
|
||||||
|
assert ev.chat_id == first_msg.get_snapshot().chat_id
|
||||||
|
assert ac1.create_chat(ac2).get_fresh_message_count() == 0
|
||||||
|
assert len(list(ac1.get_fresh_messages())) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_rename_synchronization(acfactory):
|
||||||
|
"""Test synchronization of contact renaming."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
alice2 = alice.clone()
|
||||||
|
alice2.bring_online()
|
||||||
|
|
||||||
|
bob.set_config("displayname", "Bob")
|
||||||
|
bob.create_chat(alice).send_text("Hello!")
|
||||||
|
alice_msg = alice.wait_for_incoming_msg().get_snapshot()
|
||||||
|
alice2_msg = alice2.wait_for_incoming_msg().get_snapshot()
|
||||||
|
|
||||||
|
assert alice2_msg.sender.get_snapshot().display_name == "Bob"
|
||||||
|
alice_msg.sender.set_name("Bobby")
|
||||||
|
alice2.wait_for_event(EventType.CONTACTS_CHANGED)
|
||||||
|
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
|
||||||
|
|
||||||
|
|
||||||
|
def test_rename_group(acfactory):
|
||||||
|
"""Test renaming the group."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
alice_group = alice.create_group("Test group")
|
||||||
|
alice_contact_bob = alice.create_contact(bob)
|
||||||
|
alice_group.add_contact(alice_contact_bob)
|
||||||
|
alice_group.send_text("Hello!")
|
||||||
|
|
||||||
|
bob_msg = bob.wait_for_incoming_msg()
|
||||||
|
bob_chat = bob_msg.get_snapshot().chat
|
||||||
|
assert bob_chat.get_basic_snapshot().name == "Test group"
|
||||||
|
|
||||||
|
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
||||||
|
alice_group.set_name(name)
|
||||||
|
bob.wait_for_incoming_msg_event()
|
||||||
|
assert bob_chat.get_basic_snapshot().name == name
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_all_accounts_deadlock(rpc):
|
||||||
|
"""Regression test for get_all_accounts deadlock."""
|
||||||
|
for _ in range(100):
|
||||||
|
all_accounts = rpc.get_all_accounts.future()
|
||||||
|
rpc.add_account()
|
||||||
|
all_accounts()
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_deltachat_folder(acfactory, direct_imap):
|
||||||
|
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||||
|
ac1 = acfactory.new_configured_account()
|
||||||
|
ac1.set_config("mvbox_move", "1")
|
||||||
|
ac1.bring_online()
|
||||||
|
|
||||||
|
ac1_direct_imap = direct_imap(ac1)
|
||||||
|
ac1_direct_imap.conn.folder.delete("DeltaChat")
|
||||||
|
assert "DeltaChat" not in ac1_direct_imap.list_folders()
|
||||||
|
|
||||||
|
# Wait until new folder is created and UIDVALIDITY is updated.
|
||||||
|
while True:
|
||||||
|
event = ac1.wait_for_event()
|
||||||
|
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
|
||||||
|
break
|
||||||
|
|
||||||
|
ac2 = acfactory.get_online_account()
|
||||||
|
ac2.create_chat(ac1).send_text("hello")
|
||||||
|
msg = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert msg.text == "hello"
|
||||||
|
|
||||||
|
assert "DeltaChat" in ac1_direct_imap.list_folders()
|
||||||
|
|
||||||
|
|
||||||
|
def test_broadcast(acfactory):
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
alice_chat = alice.create_broadcast("My great channel")
|
||||||
|
snapshot = alice_chat.get_basic_snapshot()
|
||||||
|
assert snapshot.name == "My great channel"
|
||||||
|
assert snapshot.is_unpromoted
|
||||||
|
assert snapshot.is_encrypted
|
||||||
|
assert snapshot.chat_type == ChatType.OUT_BROADCAST
|
||||||
|
|
||||||
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
alice_chat.add_contact(alice_contact_bob)
|
||||||
|
|
||||||
|
alice_msg = alice_chat.send_message(text="hello").get_snapshot()
|
||||||
|
assert alice_msg.text == "hello"
|
||||||
|
assert alice_msg.show_padlock
|
||||||
|
|
||||||
|
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert bob_msg.text == "hello"
|
||||||
|
assert bob_msg.show_padlock
|
||||||
|
assert bob_msg.error is None
|
||||||
|
|
||||||
|
bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
|
||||||
|
bob_chat_snapshot = bob_chat.get_basic_snapshot()
|
||||||
|
assert bob_chat_snapshot.name == "My great channel"
|
||||||
|
assert not bob_chat_snapshot.is_unpromoted
|
||||||
|
assert bob_chat_snapshot.is_encrypted
|
||||||
|
assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
|
||||||
|
assert bob_chat_snapshot.is_contact_request
|
||||||
|
|
||||||
|
assert not bob_chat.can_send()
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
def test_vcard(acfactory) -> None:
|
def test_vcard(acfactory) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
||||||
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
from deltachat_rpc_client import EventType
|
|
||||||
|
|
||||||
|
|
||||||
def test_webxdc(acfactory) -> None:
|
def test_webxdc(acfactory) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||||
|
|
||||||
while True:
|
event = bob.wait_for_incoming_msg_event()
|
||||||
event = bob.wait_for_event()
|
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||||
if event.kind == EventType.INCOMING_MSG:
|
message = bob.get_message_by_id(event.msg_id)
|
||||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
|
||||||
message = bob.get_message_by_id(event.msg_id)
|
|
||||||
break
|
|
||||||
|
|
||||||
webxdc_info = message.get_webxdc_info()
|
webxdc_info = message.get_webxdc_info()
|
||||||
assert webxdc_info == {
|
assert webxdc_info == {
|
||||||
@@ -51,8 +44,7 @@ def test_webxdc(acfactory) -> None:
|
|||||||
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
|
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = bob.get_config("addr")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
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")
|
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ setenv =
|
|||||||
RUST_MIN_STACK=8388608
|
RUST_MIN_STACK=8388608
|
||||||
passenv =
|
passenv =
|
||||||
CHATMAIL_DOMAIN
|
CHATMAIL_DOMAIN
|
||||||
deps =
|
dependency_groups =
|
||||||
pytest
|
dev
|
||||||
pytest-timeout
|
|
||||||
pytest-xdist
|
|
||||||
imap-tools
|
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
@@ -24,7 +21,7 @@ skip_install = True
|
|||||||
deps =
|
deps =
|
||||||
ruff
|
ruff
|
||||||
commands =
|
commands =
|
||||||
ruff format --quiet --diff src/ examples/ tests/
|
ruff format --diff src/ examples/ tests/
|
||||||
ruff check src/ examples/ tests/
|
ruff check src/ examples/ tests/
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "1.152.2"
|
version = "2.11.0"
|
||||||
description = "DeltaChat JSON-RPC server"
|
description = "DeltaChat JSON-RPC server"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ over standard I/O.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
To download binary pre-builds check the [releases page](https://github.com/deltachat/deltachat-core-rust/releases).
|
To download binary pre-builds check the [releases page](https://github.com/chatmail/core/releases).
|
||||||
Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`.
|
Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`.
|
||||||
|
|
||||||
To install from source run:
|
To install from source run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server
|
cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
|
||||||
```
|
```
|
||||||
|
|
||||||
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
|
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
"url": "https://github.com/chatmail/core.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepack": "node scripts/update_optional_dependencies_and_version.js"
|
"prepack": "node scripts/update_optional_dependencies_and_version.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"version": "1.152.2"
|
"version": "2.11.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user