mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 23:52:11 +03:00
Compare commits
1612 Commits
link2xt/py
...
v1.138.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97d2812644 | ||
|
|
2ab713d968 | ||
|
|
b7a25d5092 | ||
|
|
8cd85fa7a4 | ||
|
|
7cfab9a931 | ||
|
|
30086038e6 | ||
|
|
eec1062619 | ||
|
|
07ceabdf85 | ||
|
|
c349bf8e0c | ||
|
|
21eb4f6648 | ||
|
|
10fed7d7de | ||
|
|
b08a283fe5 | ||
|
|
45a2805100 | ||
|
|
cc8157ecf1 | ||
|
|
0c98aca5f0 | ||
|
|
170e4b3530 | ||
|
|
5ed91e9f6e | ||
|
|
2779737c56 | ||
|
|
0d3c0a3d8f | ||
|
|
8e38e7220b | ||
|
|
acfde3cb7b | ||
|
|
b6a461e3b7 | ||
|
|
0541ecf22c | ||
|
|
77af0a2114 | ||
|
|
2f679bc21a | ||
|
|
518db9a20f | ||
|
|
edf8aafbdc | ||
|
|
ab1583eef9 | ||
|
|
e3cb9b894b | ||
|
|
c375c03d8e | ||
|
|
14aaab05b0 | ||
|
|
72c09feb64 | ||
|
|
8a4dff2212 | ||
|
|
022f836d35 | ||
|
|
636ab4a9e5 | ||
|
|
2bddefa1ab | ||
|
|
7d67100a3c | ||
|
|
1043916411 | ||
|
|
f4e58e90ae | ||
|
|
e4f10b32dd | ||
|
|
e9431888a6 | ||
|
|
1649073c0f | ||
|
|
b2cf18d8b3 | ||
|
|
2eceb4be29 | ||
|
|
ae7ff17ba2 | ||
|
|
026f678452 | ||
|
|
add8c0680f | ||
|
|
aee2b81c06 | ||
|
|
3624aad1b5 | ||
|
|
299d994d4b | ||
|
|
5e0f5ec390 | ||
|
|
c318ca5d1a | ||
|
|
38a2e07194 | ||
|
|
1ff6740938 | ||
|
|
402d5bed85 | ||
|
|
57bc046381 | ||
|
|
0617236eb0 | ||
|
|
8c5ffe0237 | ||
|
|
39f977c1e6 | ||
|
|
ec03614cae | ||
|
|
ea0b063c19 | ||
|
|
98d7a93909 | ||
|
|
49bf8414ed | ||
|
|
1e7dbea351 | ||
|
|
0412244646 | ||
|
|
bbd854d7bc | ||
|
|
ba2bb517f7 | ||
|
|
0ae831eca0 | ||
|
|
ab494ae786 | ||
|
|
8a58ae8a3a | ||
|
|
cf84255e99 | ||
|
|
462bd63065 | ||
|
|
6bfbf6547b | ||
|
|
13802bab42 | ||
|
|
adb2e4ea32 | ||
|
|
421a7b277d | ||
|
|
14d8139883 | ||
|
|
062905924c | ||
|
|
20d79970a2 | ||
|
|
f49588e64e | ||
|
|
496a8e3810 | ||
|
|
94dc65c1a2 | ||
|
|
4fe7fa3148 | ||
|
|
4cf923ccb9 | ||
|
|
56b86adf18 | ||
|
|
cfccee2ad4 | ||
|
|
37d92e3fa5 | ||
|
|
a1ee2b463f | ||
|
|
8df3b1bb1b | ||
|
|
22f240dd4d | ||
|
|
ae10ed5c40 | ||
|
|
aff6bf9402 | ||
|
|
43fc55e542 | ||
|
|
7ea05cb8a0 | ||
|
|
d036ad5853 | ||
|
|
e9280b8413 | ||
|
|
2108a8ba94 | ||
|
|
34f4ec02f6 | ||
|
|
72d5a387fb | ||
|
|
d17d89ea8f | ||
|
|
d2aa76c0ca | ||
|
|
406031773b | ||
|
|
242547f1e9 | ||
|
|
f43f5c6c0f | ||
|
|
910e4bfa37 | ||
|
|
ecf4e651ee | ||
|
|
7b724fa75a | ||
|
|
09776ae71c | ||
|
|
47bea5f8fb | ||
|
|
99cd6d10da | ||
|
|
fae4cb33bc | ||
|
|
7a3be74350 | ||
|
|
20a64ec357 | ||
|
|
92bf48684a | ||
|
|
17701b78d6 | ||
|
|
ff0d506c95 | ||
|
|
8ff3f08c2f | ||
|
|
7a32bcc1f4 | ||
|
|
65822e53e6 | ||
|
|
ac508a9e9c | ||
|
|
225112a8fe | ||
|
|
5d34b225b7 | ||
|
|
6ca6a439bd | ||
|
|
f9465f7512 | ||
|
|
489eae5d66 | ||
|
|
b6c6a63a39 | ||
|
|
c069190b68 | ||
|
|
94ac2b1097 | ||
|
|
6080a52024 | ||
|
|
0aea7d1e02 | ||
|
|
08cbc54c00 | ||
|
|
9731ec419e | ||
|
|
e9cfcd9d1b | ||
|
|
d39cbcdc8d | ||
|
|
fbbefe6b49 | ||
|
|
bab311730c | ||
|
|
b47cad7e68 | ||
|
|
a3b62b9743 | ||
|
|
9aa4c0e56b | ||
|
|
27d2b12e8d | ||
|
|
c1148e4117 | ||
|
|
295f7a291b | ||
|
|
2be28f1311 | ||
|
|
2e42243de8 | ||
|
|
00f2585d8c | ||
|
|
0b73f9cebd | ||
|
|
f5e8a04fd0 | ||
|
|
6721df7d57 | ||
|
|
18d98d643b | ||
|
|
62758658ed | ||
|
|
03bb751a9b | ||
|
|
3ebb1ea95f | ||
|
|
c1d251010f | ||
|
|
7e5959e495 | ||
|
|
823da56f2d | ||
|
|
5bcc44ca9b | ||
|
|
4304e3f0be | ||
|
|
e2e3abdf03 | ||
|
|
dcea188b62 | ||
|
|
5cf725a378 | ||
|
|
2bf0ea9d91 | ||
|
|
1df936aeac | ||
|
|
9ab2c6df16 | ||
|
|
cf11741a8c | ||
|
|
b6a12e3914 | ||
|
|
b753440a68 | ||
|
|
39abc8344c | ||
|
|
65c9e72bf4 | ||
|
|
ea4d954c77 | ||
|
|
43523a96a2 | ||
|
|
2e2fa9e74f | ||
|
|
e43ffb20a1 | ||
|
|
2f0f247e70 | ||
|
|
5bda4f0c26 | ||
|
|
d39c8a3a19 | ||
|
|
e465415039 | ||
|
|
5cef77b8e6 | ||
|
|
60e733c30c | ||
|
|
8b98816eb9 | ||
|
|
50165b3e35 | ||
|
|
0be8b5a5c4 | ||
|
|
451bb6e9db | ||
|
|
83196d4cb5 | ||
|
|
0003e55ad5 | ||
|
|
02014eda6c | ||
|
|
f1c6cd69e9 | ||
|
|
ace281ff6c | ||
|
|
c9edd525e0 | ||
|
|
3f35b442c3 | ||
|
|
87e9365016 | ||
|
|
9806509f4a | ||
|
|
91600a34b6 | ||
|
|
d16351d207 | ||
|
|
4caf638201 | ||
|
|
375fcbd63c | ||
|
|
6ff3a2cf7c | ||
|
|
a890fe3a9a | ||
|
|
2b8bf29fce | ||
|
|
26400a9e4e | ||
|
|
f8b9bb9083 | ||
|
|
42f9047a54 | ||
|
|
6433a3a5f3 | ||
|
|
4b6a03c904 | ||
|
|
ff3df01d98 | ||
|
|
cdc99854b2 | ||
|
|
e7072bcb75 | ||
|
|
7950bde3c6 | ||
|
|
a259669c98 | ||
|
|
603e6be9b4 | ||
|
|
a78c484467 | ||
|
|
e78f07b343 | ||
|
|
8abf10aacb | ||
|
|
2fef4acdd6 | ||
|
|
de27be3a36 | ||
|
|
c62e8539a1 | ||
|
|
22c0aef9c0 | ||
|
|
87805bc36d | ||
|
|
99c4d24eab | ||
|
|
7bf9c4a2d9 | ||
|
|
304e902fce | ||
|
|
0155d93622 | ||
|
|
ebd097bdbe | ||
|
|
a11d01f8a3 | ||
|
|
38491b694b | ||
|
|
e702c1a8ca | ||
|
|
1adea3c678 | ||
|
|
9af812a3e7 | ||
|
|
36bdf8a67e | ||
|
|
20b30fc70a | ||
|
|
e59ff6ca74 | ||
|
|
0e5db36205 | ||
|
|
7960944b14 | ||
|
|
71c2383cbe | ||
|
|
5f5b272726 | ||
|
|
b34fe8f118 | ||
|
|
810be4f6c7 | ||
|
|
1ebbe26ebb | ||
|
|
0f5d5dd2b2 | ||
|
|
473dbe01af | ||
|
|
069ed7afa6 | ||
|
|
9313ece3cd | ||
|
|
900168c68c | ||
|
|
0bd137b4e5 | ||
|
|
75da205ff6 | ||
|
|
67e5fbbfe3 | ||
|
|
570daf42ec | ||
|
|
fcbbb91cde | ||
|
|
c3a7fc4c8d | ||
|
|
4b4c57a480 | ||
|
|
b95d58208c | ||
|
|
c468eb088e | ||
|
|
de37135ed6 | ||
|
|
33777d8759 | ||
|
|
8cc348bfa4 | ||
|
|
76bbd5fd72 | ||
|
|
eaed2381e7 | ||
|
|
6198ed0ef5 | ||
|
|
9f4af679a3 | ||
|
|
e158b889c9 | ||
|
|
9f7defa8da | ||
|
|
e9d7fe0561 | ||
|
|
7d7289bd51 | ||
|
|
ebdc52247c | ||
|
|
36bb4a7a32 | ||
|
|
c0832af634 | ||
|
|
b6db0152b0 | ||
|
|
bc7fd4495b | ||
|
|
e67e86422f | ||
|
|
2030de11d9 | ||
|
|
2c5a0cac5f | ||
|
|
251917e602 | ||
|
|
273719ae7c | ||
|
|
e639b58c6f | ||
|
|
5addfa8d1d | ||
|
|
02d68332c7 | ||
|
|
97abb9a0a9 | ||
|
|
d0e0cfafef | ||
|
|
f630b5fb39 | ||
|
|
d9bab938d5 | ||
|
|
215ec14b20 | ||
|
|
ea728e9b62 | ||
|
|
2af9ff1d01 | ||
|
|
7502234686 | ||
|
|
863a386d0f | ||
|
|
e4b49dfdef | ||
|
|
612aa1431e | ||
|
|
781d3abdb9 | ||
|
|
78d01933ad | ||
|
|
1a1467f7cf | ||
|
|
8d09291d1e | ||
|
|
4ccd2b8d02 | ||
|
|
794596ec69 | ||
|
|
3a787519b3 | ||
|
|
c03e163ed2 | ||
|
|
6cee295a5d | ||
|
|
f0be7daae9 | ||
|
|
0b279ec84e | ||
|
|
e919de78a3 | ||
|
|
6ea675a12f | ||
|
|
b970ebe67a | ||
|
|
3c4c701f9b | ||
|
|
01ac9c8b90 | ||
|
|
f6de23738d | ||
|
|
ddc2704278 | ||
|
|
3d2b164c05 | ||
|
|
2094bc3135 | ||
|
|
acff8205e2 | ||
|
|
255400028a | ||
|
|
d7615b223f | ||
|
|
00fbf540c4 | ||
|
|
288eccf722 | ||
|
|
99ee769580 | ||
|
|
345759d653 | ||
|
|
db0143f01a | ||
|
|
4da0c19766 | ||
|
|
08247a5d37 | ||
|
|
ceadd8928e | ||
|
|
c3d96814ca | ||
|
|
c2953623b9 | ||
|
|
1907d1859e | ||
|
|
a1970e998f | ||
|
|
1e9baefca0 | ||
|
|
e16322d99d | ||
|
|
ecfe3898c6 | ||
|
|
5499ca52bf | ||
|
|
4e8979f7c8 | ||
|
|
417db31098 | ||
|
|
cd9f6c3d5b | ||
|
|
07870a6d69 | ||
|
|
b08a4d6fcf | ||
|
|
b3a82b416f | ||
|
|
4e5d7fb821 | ||
|
|
1d73f97ef3 | ||
|
|
f5601e7683 | ||
|
|
0000c09ad3 | ||
|
|
a83884d7e9 | ||
|
|
9e00e8627f | ||
|
|
85c9622675 | ||
|
|
30432d8fa5 | ||
|
|
8b9f19be70 | ||
|
|
39c317e211 | ||
|
|
36ab7bdf47 | ||
|
|
f8f0ca08da | ||
|
|
2a0a05d03c | ||
|
|
7bc2f0cb6b | ||
|
|
4355bd77a9 | ||
|
|
f0091696c2 | ||
|
|
d2e86c5852 | ||
|
|
d4a505b52e | ||
|
|
08a30031eb | ||
|
|
44686d6caa | ||
|
|
9862d40f89 | ||
|
|
256c8c13f1 | ||
|
|
0b3a56c3c4 | ||
|
|
89024bbf37 | ||
|
|
cf16671d8d | ||
|
|
671feb68a4 | ||
|
|
ccd5158109 | ||
|
|
0a18e32d62 | ||
|
|
e9fadc0785 | ||
|
|
cfa13f0669 | ||
|
|
89e43c6678 | ||
|
|
8a67797cb1 | ||
|
|
b29bc19ef4 | ||
|
|
e765066f05 | ||
|
|
67aa785a9e | ||
|
|
c88c26426d | ||
|
|
b4e9a9764f | ||
|
|
06e79e8926 | ||
|
|
9427f7b587 | ||
|
|
bce22edfe3 | ||
|
|
656d4ed506 | ||
|
|
5e3fcafb3a | ||
|
|
660cfd4f01 | ||
|
|
7a1270f861 | ||
|
|
b35b893351 | ||
|
|
f45f9263db | ||
|
|
8289dc92ed | ||
|
|
862107c708 | ||
|
|
778660a8c9 | ||
|
|
6e55f0c6e3 | ||
|
|
3b0e740c17 | ||
|
|
2dd87b6b5e | ||
|
|
cdcacf2f83 | ||
|
|
51aaaf2e8d | ||
|
|
e6438f9981 | ||
|
|
9135cffaa4 | ||
|
|
73492ca4bc | ||
|
|
fe3c1f69c3 | ||
|
|
31ee3feb57 | ||
|
|
f4ed63c54c | ||
|
|
8f88cdd826 | ||
|
|
9933a4268f | ||
|
|
8a54c228fd | ||
|
|
b5f2c747e0 | ||
|
|
ba35e83db2 | ||
|
|
61a2c551fc | ||
|
|
20c91ba2fa | ||
|
|
969f8b916b | ||
|
|
b7b7a7e95d | ||
|
|
455b108a6c | ||
|
|
645ca7741b | ||
|
|
36643c551d | ||
|
|
0fcdee8857 | ||
|
|
26ae686687 | ||
|
|
b94bd9a659 | ||
|
|
f15e7d43e3 | ||
|
|
05c256dd5b | ||
|
|
37295f6967 | ||
|
|
dfdbb91f0a | ||
|
|
72f93dca7a | ||
|
|
ec2cf31cfa | ||
|
|
ecd4d2afe0 | ||
|
|
ec9d104cf3 | ||
|
|
11214c7d1f | ||
|
|
fba27ff884 | ||
|
|
f8907e3c83 | ||
|
|
f1688d2b3f | ||
|
|
693045b542 | ||
|
|
14dfb9abec | ||
|
|
c8ed3ed73b | ||
|
|
bce5203eeb | ||
|
|
74c0c2cc38 | ||
|
|
4f25072352 | ||
|
|
91c3a39134 | ||
|
|
ae94b2a7b3 | ||
|
|
3b013a1017 | ||
|
|
80aab220b6 | ||
|
|
34c3e44b9d | ||
|
|
78d304443a | ||
|
|
d6c24eb9f6 | ||
|
|
f7fd1ef2bf | ||
|
|
af7bf5bd2b | ||
|
|
ea666f1098 | ||
|
|
5bb80f94c7 | ||
|
|
2f29c56a36 | ||
|
|
de86b8a96e | ||
|
|
060c9c8aa1 | ||
|
|
727428a965 | ||
|
|
df455bbcf5 | ||
|
|
946eea4c9e | ||
|
|
5cbc87369e | ||
|
|
5cdd5e0564 | ||
|
|
f493d6bb40 | ||
|
|
8e073b9c3e | ||
|
|
ea2a692d18 | ||
|
|
1b7c5be9c5 | ||
|
|
f7903df805 | ||
|
|
d2c61dc90e | ||
|
|
7b68098785 | ||
|
|
48f2ea717e | ||
|
|
cb3f03fd39 | ||
|
|
06f1fe18d6 | ||
|
|
1dbf924c6a | ||
|
|
3f6814f421 | ||
|
|
782828ac4f | ||
|
|
bd3759d55e | ||
|
|
672993e69e | ||
|
|
987bdaf237 | ||
|
|
7cf382a3b8 | ||
|
|
19dce9ddfa | ||
|
|
0afc0dd65a | ||
|
|
73d612a07d | ||
|
|
3b1529ef81 | ||
|
|
15187c0adb | ||
|
|
c5f31c3d03 | ||
|
|
2c17e78347 | ||
|
|
4ee646ce0b | ||
|
|
1f7b4a74fa | ||
|
|
4bc90701cc | ||
|
|
490deb9347 | ||
|
|
28d9484a13 | ||
|
|
e67e684ee0 | ||
|
|
6cfe3e6a97 | ||
|
|
99ac524905 | ||
|
|
2faf7fdb78 | ||
|
|
6a8ea8a083 | ||
|
|
e0e56cd831 | ||
|
|
bbc6febb72 | ||
|
|
7f7f42d721 | ||
|
|
589236c27b | ||
|
|
c16c5e0802 | ||
|
|
36cab40ac1 | ||
|
|
4186d78305 | ||
|
|
06cccb77f8 | ||
|
|
1895f4c556 | ||
|
|
849a873e61 | ||
|
|
b5c0372c99 | ||
|
|
1ba9b69849 | ||
|
|
6345a4f5b3 | ||
|
|
382fc75b1e | ||
|
|
92fc9ea971 | ||
|
|
de7ac2a240 | ||
|
|
7b0e5adaee | ||
|
|
406b59501b | ||
|
|
d5da2bed75 | ||
|
|
924d5b9377 | ||
|
|
bb47299ee4 | ||
|
|
20065d3daa | ||
|
|
ccb267beab | ||
|
|
32bcb59601 | ||
|
|
c708c44f0a | ||
|
|
9415a71f9d | ||
|
|
1fd42f2c53 | ||
|
|
1e52502ab3 | ||
|
|
a144d7e4f3 | ||
|
|
e855b79f9c | ||
|
|
2f8a8f9f50 | ||
|
|
b9a58bf625 | ||
|
|
c8075e53d2 | ||
|
|
ff54cf24a1 | ||
|
|
af0833e821 | ||
|
|
da11542322 | ||
|
|
3bcdd1770a | ||
|
|
4dc596e646 | ||
|
|
2e69210825 | ||
|
|
625887d249 | ||
|
|
b7c34b7794 | ||
|
|
941cf38a3e | ||
|
|
7f61896ec8 | ||
|
|
b14b49cbf0 | ||
|
|
6de3510a5d | ||
|
|
dea519095c | ||
|
|
3f8ca0cee9 | ||
|
|
1b998da57a | ||
|
|
772747d42d | ||
|
|
3998258afb | ||
|
|
4e86de98c4 | ||
|
|
2a497989e9 | ||
|
|
361b19e455 | ||
|
|
c036b26ae5 | ||
|
|
dcf6ffef12 | ||
|
|
865ede39fe | ||
|
|
a27e84ad89 | ||
|
|
b83bd26325 | ||
|
|
44227d7b86 | ||
|
|
6bcf022523 | ||
|
|
ccec26ffa7 | ||
|
|
83e159e42f | ||
|
|
cbabd4219e | ||
|
|
548afe3153 | ||
|
|
35c5f42b35 | ||
|
|
b9ff8b1d6c | ||
|
|
bb6a20dc11 | ||
|
|
e97955f5a0 | ||
|
|
35bd56ffea | ||
|
|
78affb766e | ||
|
|
9b1704e3b2 | ||
|
|
55cdbdc085 | ||
|
|
58620988d7 | ||
|
|
467f313091 | ||
|
|
091578573a | ||
|
|
62c1237024 | ||
|
|
8d41d02397 | ||
|
|
fce3f80654 | ||
|
|
2a0a51bea0 | ||
|
|
91d94d5920 | ||
|
|
c59f21230d | ||
|
|
828cc1fbd1 | ||
|
|
57f4958fc6 | ||
|
|
3aeb57b4df | ||
|
|
1b85614db9 | ||
|
|
57ecf49eb1 | ||
|
|
f279b0d1e5 | ||
|
|
32071297e6 | ||
|
|
1d98c38ff3 | ||
|
|
c09e0e2b65 | ||
|
|
0c8f967391 | ||
|
|
aca34379e0 | ||
|
|
1edd7045be | ||
|
|
c784c499c2 | ||
|
|
36c751bcc3 | ||
|
|
8a14a84bec | ||
|
|
b00703cec2 | ||
|
|
05e783564f | ||
|
|
330fb02486 | ||
|
|
1447ab8dac | ||
|
|
d574ee4edb | ||
|
|
814fe953a9 | ||
|
|
280f13b8cf | ||
|
|
a96b44a482 | ||
|
|
4286d248e9 | ||
|
|
116537019b | ||
|
|
8b37b8c1fd | ||
|
|
63b4339ca0 | ||
|
|
fdd239f61f | ||
|
|
5ca5d95c5e | ||
|
|
3fcad50924 | ||
|
|
8e40540d24 | ||
|
|
04d22bb84d | ||
|
|
5415f1bfa1 | ||
|
|
ff3bf4791a | ||
|
|
eebea216cb | ||
|
|
fbcd7f46b8 | ||
|
|
846278b18e | ||
|
|
2f2b1e18bf | ||
|
|
073c250fa4 | ||
|
|
1f336f89a6 | ||
|
|
a47fec7f6c | ||
|
|
084434d3b4 | ||
|
|
ebfbc11973 | ||
|
|
9cc9579b2d | ||
|
|
7beccd9dbc | ||
|
|
0e195bc7a2 | ||
|
|
f89efd5fce | ||
|
|
48d278fca9 | ||
|
|
c84effdaa1 | ||
|
|
e9601ef138 | ||
|
|
44c5cd5526 | ||
|
|
1c9662a8f2 | ||
|
|
5d08b2ce33 | ||
|
|
bb9d7d7ef3 | ||
|
|
766bb5c8aa | ||
|
|
84144659cf | ||
|
|
1394137436 | ||
|
|
998614b923 | ||
|
|
5b346397b8 | ||
|
|
1f99269002 | ||
|
|
160cbe8125 | ||
|
|
b9fa05c3bb | ||
|
|
4287a4d3ad | ||
|
|
37d2aafb26 | ||
|
|
4332170691 | ||
|
|
9a7c0f4737 | ||
|
|
9e7e172a7b | ||
|
|
71fbaf572a | ||
|
|
2ab29e5bfa | ||
|
|
85f8f910b9 | ||
|
|
b779d08d7f | ||
|
|
3b5634f14b | ||
|
|
f91ba357cf | ||
|
|
616faff96b | ||
|
|
5e6869403e | ||
|
|
7ff7d82959 | ||
|
|
9b751c1865 | ||
|
|
d1d31096e0 | ||
|
|
30f8522626 | ||
|
|
d3c221e061 | ||
|
|
8a421224f8 | ||
|
|
7dfce71ac9 | ||
|
|
35ba97f76a | ||
|
|
41921eaf3d | ||
|
|
03221ea86c | ||
|
|
b50761e4d1 | ||
|
|
40dea771cc | ||
|
|
e011f8f42f | ||
|
|
09d4b4354a | ||
|
|
ab151654fb | ||
|
|
ea9556b1b9 | ||
|
|
3dc6fd5c10 | ||
|
|
f39acbc037 | ||
|
|
005f7ff07e | ||
|
|
144ca7c171 | ||
|
|
7012b99d73 | ||
|
|
72bacd56f7 | ||
|
|
cc75038ccc | ||
|
|
f4810125e3 | ||
|
|
acf1faf151 | ||
|
|
255fbe94f7 | ||
|
|
b5d1eba28e | ||
|
|
1509978738 | ||
|
|
607b9e55a9 | ||
|
|
7c4c980409 | ||
|
|
b8ad3ec1b1 | ||
|
|
87dd33f66e | ||
|
|
7d8d13759a | ||
|
|
2b4f2a9171 | ||
|
|
8e869de350 | ||
|
|
b0ef082b2a | ||
|
|
bf8e74198d | ||
|
|
e77805471c | ||
|
|
45a8004b33 | ||
|
|
990f4dce9b | ||
|
|
0f36197c54 | ||
|
|
890a2bcc15 | ||
|
|
224355e83a | ||
|
|
c6ea4e389a | ||
|
|
678142b3fb | ||
|
|
ae6f83cd21 | ||
|
|
626b2be1fe | ||
|
|
ac5c789c75 | ||
|
|
ce2878f1e8 | ||
|
|
d4162899b4 | ||
|
|
e900d50e38 | ||
|
|
a438a4746a | ||
|
|
cfb819506f | ||
|
|
b86b915f40 | ||
|
|
ad5a5ad3db | ||
|
|
74081d8a36 | ||
|
|
34a434f07c | ||
|
|
dc944d8ca7 | ||
|
|
b06a7e7197 | ||
|
|
fa61d90115 | ||
|
|
7977c9ab44 | ||
|
|
6273a7d54e | ||
|
|
4d1a9c2aa1 | ||
|
|
b26ded423b | ||
|
|
e4b6eba5d7 | ||
|
|
bc225024a1 | ||
|
|
e616ecf160 | ||
|
|
f93562c6bf | ||
|
|
ac39c3699b | ||
|
|
091bc1ab13 | ||
|
|
fcbb66a788 | ||
|
|
ab2bc3bfb2 | ||
|
|
42dd6f9d08 | ||
|
|
465bcd46f8 | ||
|
|
cc88a6cb58 | ||
|
|
ba8f1bfcfd | ||
|
|
d4d6ced957 | ||
|
|
0b664e75cb | ||
|
|
1a4c2953f7 | ||
|
|
765c95de39 | ||
|
|
b2ea8f54df | ||
|
|
d7aecabcaa | ||
|
|
9ca049051c | ||
|
|
790509676f | ||
|
|
ce016eb567 | ||
|
|
57e34abe98 | ||
|
|
fd92b7c455 | ||
|
|
0ee68d1dfc | ||
|
|
1856c622a1 | ||
|
|
0a48a2effa | ||
|
|
543864f0f5 | ||
|
|
0fe94e47cc | ||
|
|
fc09210aea | ||
|
|
3e194969c0 | ||
|
|
391cffb454 | ||
|
|
47486f8bab | ||
|
|
620e363ce6 | ||
|
|
0c2276775d | ||
|
|
ad51a7cd85 | ||
|
|
28952789a4 | ||
|
|
14adcdb517 | ||
|
|
cc80590488 | ||
|
|
48416289ac | ||
|
|
003a27f625 | ||
|
|
bff4a2259f | ||
|
|
9adf856705 | ||
|
|
2215de5285 | ||
|
|
013467d6c6 | ||
|
|
1f52b8af2f | ||
|
|
ce32f76265 | ||
|
|
836f65376c | ||
|
|
99940dd28c | ||
|
|
ffeb801b58 | ||
|
|
339bbcf070 | ||
|
|
7b83bddc2d | ||
|
|
5549733a0b | ||
|
|
4e21917c0e | ||
|
|
939b4b2aab | ||
|
|
fd0770859d | ||
|
|
d5854fb3c9 | ||
|
|
3aa22a27cc | ||
|
|
10b1a2f5f5 | ||
|
|
a28a34773c | ||
|
|
7c744d14d7 | ||
|
|
6c68f2eb7e | ||
|
|
eb2d2b7313 | ||
|
|
2e50abedaa | ||
|
|
ee53136ed2 | ||
|
|
e923983dca | ||
|
|
f4753862f1 | ||
|
|
16b40f3a19 | ||
|
|
9cd3a7550b | ||
|
|
d840a7e6b9 | ||
|
|
d875691955 | ||
|
|
c600bfa8ca | ||
|
|
973ffa1a64 | ||
|
|
caffc3d93c | ||
|
|
a4dcf656f3 | ||
|
|
7bb5d48966 | ||
|
|
71b7b0b393 | ||
|
|
bd02eea66b | ||
|
|
cdcb10fb58 | ||
|
|
6cd7296001 | ||
|
|
168021523f | ||
|
|
03f2635296 | ||
|
|
e3b08fa92b | ||
|
|
79cebe66de | ||
|
|
0619e2a129 | ||
|
|
64a81e4f61 | ||
|
|
0431ae53ca | ||
|
|
89c873acd0 | ||
|
|
2e70cf9388 | ||
|
|
2efd0461d1 | ||
|
|
196a34684d | ||
|
|
402fd6850c | ||
|
|
72515f440d | ||
|
|
045d919cdc | ||
|
|
9b9108320e | ||
|
|
5efb100f12 | ||
|
|
b747dd6ae8 | ||
|
|
ed2bc9e44d | ||
|
|
9e1a2149fa | ||
|
|
22b6d8c17b | ||
|
|
3876846410 | ||
|
|
a93c79e001 | ||
|
|
6aae0276da | ||
|
|
1d80659bc3 | ||
|
|
94d5e86d4f | ||
|
|
aecf7729d8 | ||
|
|
f130d537b7 | ||
|
|
f30f862e7e | ||
|
|
81e1164358 | ||
|
|
542bd4cbb8 | ||
|
|
771b57778e | ||
|
|
9be56a5e56 | ||
|
|
da744958c2 | ||
|
|
f6bda1e480 | ||
|
|
d2e24534c7 | ||
|
|
df6f974eca | ||
|
|
2f5c6b5e16 | ||
|
|
97176b13f1 | ||
|
|
18bb7e58be | ||
|
|
c2bab44bdd | ||
|
|
53bb8a9831 | ||
|
|
1b66120e7d | ||
|
|
1478f321ae | ||
|
|
5fb92c78ad | ||
|
|
a0a792b821 | ||
|
|
3feb0e648d | ||
|
|
fa5358a5bf | ||
|
|
7399a398a7 | ||
|
|
25a78aceb9 | ||
|
|
66708454dd | ||
|
|
bb5e3d11d8 | ||
|
|
ff54db2e5f | ||
|
|
434d8fc35f | ||
|
|
12eb813bc3 | ||
|
|
88d5576150 | ||
|
|
af35e4adeb | ||
|
|
eaeacb8848 | ||
|
|
f00e68e142 | ||
|
|
113356a24e | ||
|
|
b89c134e7f | ||
|
|
3748794048 | ||
|
|
ccca12176e | ||
|
|
c89dd331f7 | ||
|
|
fa81ed5f39 | ||
|
|
6c34f6b8d9 | ||
|
|
4f21a5691d | ||
|
|
b2a839971b | ||
|
|
8ad99be322 | ||
|
|
4e771e8727 | ||
|
|
e725bdfb2b | ||
|
|
ac557f73b3 | ||
|
|
0ba3501a46 | ||
|
|
b1fe12881e | ||
|
|
c1eb33c0da | ||
|
|
03bb92c942 | ||
|
|
b0da5a54cc | ||
|
|
44e056e210 | ||
|
|
48a8680ba4 | ||
|
|
b73bcc2c22 | ||
|
|
cff42936aa | ||
|
|
d3b04004b4 | ||
|
|
5cd92f10ef | ||
|
|
22a3ab983b | ||
|
|
83d2e6b8b4 | ||
|
|
4e979c5880 | ||
|
|
71e1089139 | ||
|
|
349c154a99 | ||
|
|
54410dbe49 | ||
|
|
4e08bb7b05 | ||
|
|
934ca6a7d7 | ||
|
|
e878caebe3 | ||
|
|
088eda2983 | ||
|
|
418cd24979 | ||
|
|
b4fe9e3eec | ||
|
|
c13bbd05cd | ||
|
|
58330fe8b2 | ||
|
|
680d024b05 | ||
|
|
defcd5764b | ||
|
|
e87f785a0a | ||
|
|
0227bbc305 | ||
|
|
d05afec289 | ||
|
|
64035d3ecb | ||
|
|
21e0bb28ad | ||
|
|
c6358169ad | ||
|
|
955f4fbb19 | ||
|
|
df7c44ae42 | ||
|
|
8573649bf7 | ||
|
|
52c46c6dca | ||
|
|
54ea3ec5d6 | ||
|
|
1632035784 | ||
|
|
b239535964 | ||
|
|
0751cc50b9 | ||
|
|
da5d844ec4 | ||
|
|
2775fd1fcf | ||
|
|
a87635dcf4 | ||
|
|
e30517e62c | ||
|
|
a54f3c4b31 | ||
|
|
bda6cea0ce | ||
|
|
6fece09ed7 | ||
|
|
3917c6b2f0 | ||
|
|
96a89b5bdc | ||
|
|
b55027fe71 | ||
|
|
eacbb82399 | ||
|
|
ee279f84ad | ||
|
|
26959d5b75 | ||
|
|
ff5005fa93 | ||
|
|
8f316e12d5 | ||
|
|
5f00fc4e27 | ||
|
|
f279730b0f | ||
|
|
5a5f8b03d1 | ||
|
|
5e73e9cd72 | ||
|
|
129de9182f | ||
|
|
37383c10ac | ||
|
|
09798df7a0 | ||
|
|
b360225e08 | ||
|
|
09d5e44b13 | ||
|
|
8ba89c0fa1 | ||
|
|
f984a27379 | ||
|
|
425a2310fe | ||
|
|
95571be278 | ||
|
|
7bf44a237e | ||
|
|
a119b24eeb | ||
|
|
47dbac9b50 | ||
|
|
a49282727b | ||
|
|
0d22fc7ac1 | ||
|
|
275791595c | ||
|
|
1040bc551f | ||
|
|
5aa0205c80 | ||
|
|
210a4ebcbe | ||
|
|
7fc2b06b3f | ||
|
|
1177c19a43 | ||
|
|
8a2417f32d | ||
|
|
a154347834 | ||
|
|
d51adf2aa0 | ||
|
|
a5f0c1613e | ||
|
|
7c4bcf9004 | ||
|
|
f3fb0dc5fe | ||
|
|
24ea90bd68 | ||
|
|
ef3e94c7e1 | ||
|
|
883832f78d | ||
|
|
1f03267273 | ||
|
|
8bc2ce1c30 | ||
|
|
2ae92c06e3 | ||
|
|
4a6a214f3c | ||
|
|
c2d7011aa7 | ||
|
|
c0195ab23f | ||
|
|
e4e50d0e81 | ||
|
|
573746ce54 | ||
|
|
6b2df13cdb | ||
|
|
3166b44580 | ||
|
|
e500485c21 | ||
|
|
2dd44d5f89 | ||
|
|
f656cb29be | ||
|
|
59e5a63d5f | ||
|
|
53230b6eb0 | ||
|
|
80ca59f152 | ||
|
|
eb624e43c0 | ||
|
|
532e9cb09a | ||
|
|
ef4d2a7ed0 | ||
|
|
5daa6274e8 | ||
|
|
6d2ac30461 | ||
|
|
d109a1b27f | ||
|
|
33a203d56e | ||
|
|
a19811f379 | ||
|
|
f23023961e | ||
|
|
b463a0566e | ||
|
|
38d5743c06 | ||
|
|
6990312051 | ||
|
|
a7cf51868b | ||
|
|
815c1b9c49 | ||
|
|
88bba83383 | ||
|
|
b1d517398d | ||
|
|
4e5b41f150 | ||
|
|
56b2361f01 | ||
|
|
968cc65323 | ||
|
|
d0ee21e6dc | ||
|
|
a1345f2542 | ||
|
|
f290fe0871 | ||
|
|
c41687586c | ||
|
|
59a3bc0ff4 | ||
|
|
aa78e82fed | ||
|
|
d4e670d5e9 | ||
|
|
4553c6521f | ||
|
|
a42a6ca18c | ||
|
|
e72d527d88 | ||
|
|
f5c36043f6 | ||
|
|
b227ff87dc | ||
|
|
676f311f97 | ||
|
|
061d091c97 | ||
|
|
e7617f0abd | ||
|
|
790e867af0 | ||
|
|
f02299c06c | ||
|
|
ed781af52c | ||
|
|
67043177a9 | ||
|
|
49cc5fb673 | ||
|
|
68c95dee17 | ||
|
|
9bd7ab7280 | ||
|
|
7a359f6318 | ||
|
|
38b31aa88d | ||
|
|
e12e026bd8 | ||
|
|
212fbc125c | ||
|
|
2939de013b | ||
|
|
4a0585404a | ||
|
|
0562e23ee0 | ||
|
|
dcbf5996c2 | ||
|
|
a8551510cd | ||
|
|
087f6edd0c | ||
|
|
d6b7ee04a0 | ||
|
|
d5c5ff8b3f | ||
|
|
dc4396a699 | ||
|
|
a74b00c3f9 | ||
|
|
2fdb9f8b7e | ||
|
|
80fac3f1b8 | ||
|
|
17a6c88cc7 | ||
|
|
1ba69dbb9b | ||
|
|
ab1c7ebbe2 | ||
|
|
ee715da078 | ||
|
|
27e177dc05 | ||
|
|
7aac4bfc83 | ||
|
|
7b24f9b7a4 | ||
|
|
b36b902eeb | ||
|
|
f7a47e60cd | ||
|
|
30024abb6c | ||
|
|
1d9702e9e7 | ||
|
|
ee2eae63d6 | ||
|
|
cd477936b5 | ||
|
|
2587ebbacd | ||
|
|
dbe9d7e34e | ||
|
|
4815e9b990 | ||
|
|
f05b0ddf04 | ||
|
|
f94d34c94b | ||
|
|
e9811fb6da | ||
|
|
178fc1736d | ||
|
|
54d632adaf | ||
|
|
ae939e79da | ||
|
|
49f143e0d5 | ||
|
|
9d7bdf369d | ||
|
|
e12ef805a9 | ||
|
|
b36acb2dc0 | ||
|
|
1b883ae3fa | ||
|
|
72c94e1037 | ||
|
|
a270db1d87 | ||
|
|
0a4c993bb8 | ||
|
|
ec56134583 | ||
|
|
d8bf1c1691 | ||
|
|
8ac1754e18 | ||
|
|
e1f1143919 | ||
|
|
d9e38289c4 | ||
|
|
486050d0b8 | ||
|
|
828c90ac3d | ||
|
|
ab09ecce7e | ||
|
|
aebad2eb10 | ||
|
|
2a39a85d9e | ||
|
|
cb0270baa7 | ||
|
|
99302c9598 | ||
|
|
88ae653760 | ||
|
|
6881f9d70f | ||
|
|
60ddbe5729 | ||
|
|
5e5e557c8b | ||
|
|
bc8023644c | ||
|
|
7c7cd9cc80 | ||
|
|
83ef25e7de | ||
|
|
47d465e6e4 | ||
|
|
03d3e0578f | ||
|
|
440a442f30 | ||
|
|
1da52d7d1d | ||
|
|
9a7d1faf75 | ||
|
|
4d74f625d3 | ||
|
|
0a94fbc735 | ||
|
|
9ef34890fa | ||
|
|
3e07f2c173 | ||
|
|
ee28298d7f | ||
|
|
e59c4ee858 | ||
|
|
62aed13880 | ||
|
|
bffe934acc | ||
|
|
a520f0268f | ||
|
|
5e3b1fa540 | ||
|
|
95f29f7b63 | ||
|
|
87ffcaf03e | ||
|
|
2635146328 | ||
|
|
d727d85f6d | ||
|
|
20513475ef | ||
|
|
81a7af10c7 | ||
|
|
4a6e94f8ab | ||
|
|
146fe50e20 | ||
|
|
9bf2850fb1 | ||
|
|
ba2c36548e | ||
|
|
3b806320ec | ||
|
|
d07c743cdc | ||
|
|
c857d6e1bd | ||
|
|
94cd9a713f | ||
|
|
7676473ebd | ||
|
|
8c778b3f5c | ||
|
|
a66f8bd9fc | ||
|
|
95b2a15930 | ||
|
|
0179ec2da9 | ||
|
|
8f2313bb2a | ||
|
|
488a3d1118 | ||
|
|
9094df7bc7 | ||
|
|
16aad3fa67 | ||
|
|
3b47c3f21d | ||
|
|
987ce58926 | ||
|
|
20c88743df | ||
|
|
03395b95cb | ||
|
|
53f04a134a | ||
|
|
885f26ea8c | ||
|
|
3ab181fdf8 | ||
|
|
d70c1d48b5 | ||
|
|
a8e0cb9b5a | ||
|
|
6ea9a8988b | ||
|
|
45e35b3571 | ||
|
|
e12044e6af | ||
|
|
954067eb6d | ||
|
|
e43f9066d8 | ||
|
|
aecbebd566 | ||
|
|
3111bcde5e | ||
|
|
e6cffd537e | ||
|
|
70000d9ebb | ||
|
|
d95843b0bf | ||
|
|
13e766bc37 | ||
|
|
c34edc582e | ||
|
|
8eee389c09 | ||
|
|
8ed6d4d709 | ||
|
|
60bacbec47 | ||
|
|
af013559de | ||
|
|
b784415c57 | ||
|
|
85739ba6ad | ||
|
|
a02a593f47 | ||
|
|
bba6c8f15a | ||
|
|
67f28f501a | ||
|
|
9b9703a48e | ||
|
|
c55a3d3873 | ||
|
|
55aaec744a | ||
|
|
f27d304f3b | ||
|
|
2f24eddb7d | ||
|
|
6d51d19f01 | ||
|
|
170968dfc2 | ||
|
|
a33c91afa9 | ||
|
|
f930576fd1 | ||
|
|
d52f2883cf | ||
|
|
b872953bc5 | ||
|
|
d797de7a8d | ||
|
|
acc7bb00c5 | ||
|
|
8fb8a877be | ||
|
|
b96028cd87 | ||
|
|
682e241edb | ||
|
|
c1cb6eef08 | ||
|
|
3a63628f1f | ||
|
|
3705616cd9 | ||
|
|
200b808c27 | ||
|
|
d572d960e5 | ||
|
|
b8fcb660ad | ||
|
|
5673294623 | ||
|
|
7062bb0502 | ||
|
|
5db75128ba | ||
|
|
fbd2fc8ead | ||
|
|
bc73c16df7 | ||
|
|
659cffe0cc | ||
|
|
a1663a98e0 | ||
|
|
3de1dbc9e4 | ||
|
|
6d37e8601e | ||
|
|
0a50bad555 | ||
|
|
82c0058129 | ||
|
|
1bd307a26a | ||
|
|
740f43a2d6 | ||
|
|
d762753103 | ||
|
|
a020d5ccce | ||
|
|
c14f45a8f5 | ||
|
|
8269116dba | ||
|
|
1e28ea9bb0 | ||
|
|
17f2d33731 | ||
|
|
db941ccf88 | ||
|
|
a464cbdfe6 | ||
|
|
976797d4cf | ||
|
|
31e3169433 | ||
|
|
d2b15cb629 | ||
|
|
9cd000c4f2 | ||
|
|
ea4a0530b8 | ||
|
|
243c035b03 | ||
|
|
9d3b2d4844 | ||
|
|
c312280ab3 | ||
|
|
572b99a2e1 | ||
|
|
3992b5a063 | ||
|
|
b97cb4b55e | ||
|
|
64c218f1ea | ||
|
|
deed790950 | ||
|
|
b33ae3cd0f | ||
|
|
9480699362 | ||
|
|
94c190e844 | ||
|
|
578e47666f | ||
|
|
7eeced50d1 | ||
|
|
46e127ad27 | ||
|
|
4891849e28 | ||
|
|
e0dd83d538 | ||
|
|
aac8bb950c | ||
|
|
bf21796bc0 | ||
|
|
9cbf413064 | ||
|
|
1b57eb4d8d | ||
|
|
5152e702bd | ||
|
|
c80f1a1997 | ||
|
|
88759c815b | ||
|
|
9c68fac4b6 | ||
|
|
8e17e400b3 | ||
|
|
dae3857db8 | ||
|
|
695f71e124 | ||
|
|
2d30afd212 | ||
|
|
5fe94e8bce | ||
|
|
1351f71632 | ||
|
|
d42322b38b | ||
|
|
ce6876c418 | ||
|
|
2a6b7d9766 | ||
|
|
fa1924da2b | ||
|
|
d5214eb192 | ||
|
|
c47324d671 | ||
|
|
3f8ec5ec56 | ||
|
|
fab504b54c | ||
|
|
dd32430ade | ||
|
|
eb943625a6 | ||
|
|
32ac4a01ca | ||
|
|
f01a9d7d5c | ||
|
|
a5db7104c2 | ||
|
|
18aeb14003 | ||
|
|
4ad2d6e340 | ||
|
|
ce9cd54993 | ||
|
|
23f540f9f9 | ||
|
|
f994b2d8e4 | ||
|
|
6e42b85a36 | ||
|
|
d69e42377d | ||
|
|
de9330b52f | ||
|
|
01d1c4c04b | ||
|
|
7d98978269 | ||
|
|
5024f48609 | ||
|
|
e975568122 | ||
|
|
1f71c69325 | ||
|
|
b80ec8507c | ||
|
|
3a3f3542d9 | ||
|
|
657c5fa947 | ||
|
|
7d0b25c209 | ||
|
|
8d26303cad | ||
|
|
0d8a76593a | ||
|
|
7b49fb2eb6 | ||
|
|
efa37dd283 | ||
|
|
323e44da04 | ||
|
|
70efd0f10a | ||
|
|
fcec81b4c1 | ||
|
|
dd806b2d88 | ||
|
|
5659c1b9c2 | ||
|
|
d538d29b94 | ||
|
|
b4209fac2e | ||
|
|
4d6dfa120e | ||
|
|
f92108be1d | ||
|
|
00cb72f04d | ||
|
|
92e34d67e6 | ||
|
|
65bff8339f | ||
|
|
768f8175e6 | ||
|
|
c3f352aff1 | ||
|
|
5ac2d1b8cb | ||
|
|
8214b2b8c1 | ||
|
|
53ab8a3b35 | ||
|
|
cbe1671104 | ||
|
|
0d0e223238 | ||
|
|
4767f1ce74 | ||
|
|
1a62b6d77f | ||
|
|
915008d474 | ||
|
|
9646766793 | ||
|
|
e948ec3256 | ||
|
|
9ab9d2eb7b | ||
|
|
437f8c48c4 | ||
|
|
e6d9a49187 | ||
|
|
33a014eea4 | ||
|
|
9be871ccf6 | ||
|
|
6eb8abe535 | ||
|
|
91bf87fa80 | ||
|
|
a2599ef08a | ||
|
|
22d0a4bb32 | ||
|
|
7a160033b6 | ||
|
|
3442748be7 | ||
|
|
d451bcfbe3 | ||
|
|
b2993242e4 | ||
|
|
5eaa9eeed2 | ||
|
|
3ed2ac8f0c | ||
|
|
0145203f7b | ||
|
|
59588b319e | ||
|
|
f917c7de6b | ||
|
|
84888fa4c4 | ||
|
|
e0b1644488 | ||
|
|
4beba8ce3c | ||
|
|
bc521a685d | ||
|
|
33caa0f499 | ||
|
|
033ce41c0f | ||
|
|
88a62e1f6e | ||
|
|
dd30f6ab7d | ||
|
|
140d116d98 | ||
|
|
d96b783909 | ||
|
|
572c7f2efb | ||
|
|
bcd6c226f6 | ||
|
|
bae61746f8 | ||
|
|
31f2766074 | ||
|
|
b06c8baa9c | ||
|
|
1e479fe4a3 | ||
|
|
8ea8ee02ed | ||
|
|
55bc556bcf | ||
|
|
3b6d21301b | ||
|
|
472195c7d9 | ||
|
|
afb8b5ce55 | ||
|
|
de3c82ef43 | ||
|
|
4255ae4c2d | ||
|
|
4b4e2f700e | ||
|
|
81fde5c680 | ||
|
|
5340a7d033 | ||
|
|
fc82d728fc | ||
|
|
136e9179e9 | ||
|
|
31e19ca56c | ||
|
|
f2b02b7bb0 | ||
|
|
646ace8e7a | ||
|
|
a2495716b6 | ||
|
|
0f579c6415 | ||
|
|
3eddc9164c | ||
|
|
dd29fae49b | ||
|
|
5b435d11c7 | ||
|
|
b9b0d20e8d | ||
|
|
c68a2e3820 | ||
|
|
c7ad0b1f4f | ||
|
|
0dd9e3a77e | ||
|
|
d27e3d085e | ||
|
|
10b2aa5350 | ||
|
|
3a29a555bf | ||
|
|
f024f396bf | ||
|
|
24d52c5909 | ||
|
|
f9dc8edbcb | ||
|
|
2e6f98f4e4 | ||
|
|
50431d8cfe | ||
|
|
55fcd589db | ||
|
|
081178d623 | ||
|
|
92d5857150 | ||
|
|
bb45c249a3 | ||
|
|
8796e0472a | ||
|
|
3bd16ba045 | ||
|
|
4b7ff6f003 | ||
|
|
53449ea5b3 | ||
|
|
3b381c4862 | ||
|
|
ce729263a5 | ||
|
|
67480999c0 | ||
|
|
fb8b9f60ce | ||
|
|
9ed36d4e05 | ||
|
|
e3c01d76c4 | ||
|
|
cb7f96449d | ||
|
|
e4f4dacaf0 | ||
|
|
9fc1fe74ad | ||
|
|
991089d98e | ||
|
|
c7a250da31 | ||
|
|
c5b6bad956 | ||
|
|
c4534ff621 | ||
|
|
b2c299fa82 | ||
|
|
68aa15950a | ||
|
|
4a593a8d7e | ||
|
|
2328ba54be | ||
|
|
e216dfd655 | ||
|
|
86472aba2c | ||
|
|
15ce54edfb | ||
|
|
b7bbb3ee9a | ||
|
|
d3236e79fd | ||
|
|
aa212b2b7e | ||
|
|
a0c51b3c3a | ||
|
|
e50d7724e3 | ||
|
|
0d30e66dda | ||
|
|
5c3df7e452 | ||
|
|
3efbe5b1ef | ||
|
|
52cfba06ea | ||
|
|
afba63603e | ||
|
|
3a25d6a44e | ||
|
|
0688895022 | ||
|
|
ce0e5416e6 | ||
|
|
7918a6801e | ||
|
|
6af631e8df | ||
|
|
625ecaa9b5 | ||
|
|
24fe1b9c15 | ||
|
|
ba36d09c70 | ||
|
|
f57be7187e | ||
|
|
6a00338f79 | ||
|
|
16906210e1 | ||
|
|
f9b4540387 | ||
|
|
9755438d0d | ||
|
|
fe9534ed7d | ||
|
|
134c9ada68 | ||
|
|
a3240452ff | ||
|
|
84beb6647d | ||
|
|
ecf7e2d909 | ||
|
|
fcfcf4bbf3 | ||
|
|
c62b6d77b7 | ||
|
|
7e51b9686f | ||
|
|
542ec4cac4 | ||
|
|
64b25d9ec0 | ||
|
|
f91b6fbdf0 | ||
|
|
41445a506e | ||
|
|
798db9d019 | ||
|
|
2e860c32ab | ||
|
|
0e1faed6e5 | ||
|
|
f5de3be977 | ||
|
|
4b0a30eb66 | ||
|
|
7710467571 | ||
|
|
1687794b81 | ||
|
|
158541f05c | ||
|
|
eb28899cd0 | ||
|
|
432046225a | ||
|
|
bf958ce6c1 | ||
|
|
ea8ee4e67d | ||
|
|
edfdbbdc90 | ||
|
|
1b8bfef441 | ||
|
|
514074de8b | ||
|
|
e7aab5c67c | ||
|
|
40484e875e | ||
|
|
fc215ceb63 | ||
|
|
2701c135db | ||
|
|
3a8df3e673 | ||
|
|
fa95b269a5 | ||
|
|
0e9f8c4726 | ||
|
|
a8d4cbd5c1 | ||
|
|
f68a2fc387 | ||
|
|
ede4e8109e | ||
|
|
663df6bdfd | ||
|
|
d97bdd9fd0 | ||
|
|
a806a218bf | ||
|
|
903633f422 | ||
|
|
3c774b02e5 | ||
|
|
4aae48b0a1 | ||
|
|
a8b790a5db | ||
|
|
6a6ceb6875 | ||
|
|
37503dd3e8 | ||
|
|
3f615c8de6 | ||
|
|
2ef5f2eb52 | ||
|
|
f267f6f756 | ||
|
|
538db53887 | ||
|
|
21349abed8 | ||
|
|
d2fb2bb2ca | ||
|
|
0b832fb9de | ||
|
|
179b9ba2cb | ||
|
|
9150e9fb38 | ||
|
|
4716fcef94 | ||
|
|
2b7ee85e30 | ||
|
|
2b8888350b | ||
|
|
4dfe34eedc | ||
|
|
430a71288f | ||
|
|
350509d5d1 | ||
|
|
5403fd849c | ||
|
|
fa87d2e225 | ||
|
|
28a13e98a6 | ||
|
|
b369a30544 | ||
|
|
318ed4e6e1 | ||
|
|
c6a64e8d93 | ||
|
|
efd6937dfa | ||
|
|
36bf1fe3f6 | ||
|
|
4da0e9ac64 | ||
|
|
436766f002 | ||
|
|
d4f2507288 | ||
|
|
28fd27476f | ||
|
|
619b849ce7 | ||
|
|
f1eeb1df8c | ||
|
|
cecc080931 | ||
|
|
11e6a325a2 | ||
|
|
eed8e08145 | ||
|
|
36bec9c295 | ||
|
|
c8988f5a55 | ||
|
|
86c18fb3ae | ||
|
|
2ba4381c39 | ||
|
|
185a0193cc | ||
|
|
1b00334281 | ||
|
|
0eb2f5bf52 | ||
|
|
78d1aa46a1 | ||
|
|
26403a1599 | ||
|
|
a1f112470e | ||
|
|
2ab7a37d85 | ||
|
|
4b933ed2ef | ||
|
|
0d1c12115d | ||
|
|
3bced80b23 | ||
|
|
f46f3e939e | ||
|
|
9d8e836fdd | ||
|
|
132d34db5c | ||
|
|
16edb7b35a | ||
|
|
044478a044 | ||
|
|
f6ea48d666 | ||
|
|
6ed3ed1617 | ||
|
|
9c9c401e66 | ||
|
|
72031edfbe | ||
|
|
41456aa2ab | ||
|
|
a70f29381a | ||
|
|
3b0ad73732 | ||
|
|
8c302648bb | ||
|
|
01b888c341 | ||
|
|
f8e87a8b6c | ||
|
|
01af83946c | ||
|
|
386a9ad0b0 | ||
|
|
c6c20d8f3c | ||
|
|
40b072711e | ||
|
|
2469964a44 | ||
|
|
40e0924768 | ||
|
|
e016440fb3 | ||
|
|
a37aaa5585 | ||
|
|
1ee551d53e | ||
|
|
2341eed796 | ||
|
|
49cfd79505 | ||
|
|
9ac0d1a1ce | ||
|
|
6046dfabe9 | ||
|
|
c2269bf777 | ||
|
|
b81a958021 | ||
|
|
9e37a5643c | ||
|
|
50a213c2e1 | ||
|
|
c6e9fbd501 | ||
|
|
720b5aa7c1 | ||
|
|
7be7640d3f | ||
|
|
e4d97278ee | ||
|
|
793ad82d42 | ||
|
|
2b4d846f0e | ||
|
|
379eadab4f | ||
|
|
53a41fcffe | ||
|
|
5e597e1f09 | ||
|
|
c9879f863b | ||
|
|
e1b260d0ec | ||
|
|
480b05063d | ||
|
|
8477f7b472 | ||
|
|
91ae4eab06 | ||
|
|
f6d27516cb | ||
|
|
7b86681cb2 | ||
|
|
1de815441a | ||
|
|
d6426dc1b6 | ||
|
|
44a36025b5 | ||
|
|
00017ab653 | ||
|
|
411adc861e | ||
|
|
c5b3939ddb | ||
|
|
a9e3ea56ec | ||
|
|
6a0f17a983 | ||
|
|
28e6f457b1 | ||
|
|
fdf46054e2 | ||
|
|
47a3ee1ff1 | ||
|
|
a0b7b4b5c4 | ||
|
|
b9bc0a4047 | ||
|
|
e57abf92f3 | ||
|
|
b56a7bc139 | ||
|
|
c401780c15 | ||
|
|
df96a1daac | ||
|
|
0a2200c2c8 | ||
|
|
61b8d04418 | ||
|
|
fd7cc83537 | ||
|
|
f24843fbb1 | ||
|
|
6c57bc9438 | ||
|
|
ae5f72cf4f | ||
|
|
3e65b6f3a6 | ||
|
|
a4a53d5299 | ||
|
|
4223cac7a5 | ||
|
|
0073a09da6 | ||
|
|
e612927c5d | ||
|
|
aff951440c | ||
|
|
ef63e01632 | ||
|
|
c4cf0f12c9 | ||
|
|
68635be8a2 | ||
|
|
776df7505e | ||
|
|
d6fdc7cb67 | ||
|
|
91c10b3ac6 | ||
|
|
82ace72527 | ||
|
|
ea87c78d34 | ||
|
|
585b8ece58 | ||
|
|
a2927a6586 | ||
|
|
943c8a1ab3 | ||
|
|
3400f5641e | ||
|
|
5068648960 | ||
|
|
5be558ea68 | ||
|
|
fc25bba514 | ||
|
|
20b326415a | ||
|
|
1bf5064039 | ||
|
|
edf0c02bc8 | ||
|
|
1d42907114 | ||
|
|
070d832580 | ||
|
|
0dfec83b0f | ||
|
|
c84155cbd4 | ||
|
|
eb5ddf270f | ||
|
|
fb093253c6 | ||
|
|
1864be5c55 | ||
|
|
7138d44083 | ||
|
|
04daff0608 | ||
|
|
801250a9e0 | ||
|
|
20319b5426 | ||
|
|
9cca34bba5 | ||
|
|
530981119e | ||
|
|
6d0327d057 | ||
|
|
580ce5a9e9 | ||
|
|
6f327c950d | ||
|
|
92ad843ff2 | ||
|
|
a8059c6bff | ||
|
|
4b468a25fe | ||
|
|
1e135b649c | ||
|
|
40d32f2d0c | ||
|
|
c9ec087cd8 | ||
|
|
84d79e1479 | ||
|
|
83af248068 | ||
|
|
4f25edb1a1 | ||
|
|
ded1634b7d | ||
|
|
635c73ffc6 | ||
|
|
fcc1fe73be | ||
|
|
fa278d50f7 | ||
|
|
2f02be4c64 | ||
|
|
7add1c116c | ||
|
|
124a0e90e1 | ||
|
|
1716cdf51c | ||
|
|
3fdcffb314 | ||
|
|
f033aae25c | ||
|
|
c42d942460 | ||
|
|
0ba8201797 | ||
|
|
87252ab053 | ||
|
|
53eec521dc | ||
|
|
238570a7b9 | ||
|
|
043ae48806 | ||
|
|
fb88f2e6ab | ||
|
|
5db867cd1b | ||
|
|
ec00c160c6 | ||
|
|
616eabc613 | ||
|
|
89b32e02c5 |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -2,6 +2,14 @@
|
|||||||
# ensures this even if the user has not set core.autocrlf.
|
# ensures this even if the user has not set core.autocrlf.
|
||||||
* text=auto
|
* text=auto
|
||||||
|
|
||||||
|
# Checkout JavaScript files with LF line endings
|
||||||
|
# to prevent `prettier` from reporting errors on Windows.
|
||||||
|
*.js eol=lf
|
||||||
|
*.jsx eol=lf
|
||||||
|
*.ts eol=lf
|
||||||
|
*.tsx eol=lf
|
||||||
|
*.json eol=lf
|
||||||
|
|
||||||
# This directory contains email messages verbatim, and changing CRLF to
|
# This directory contains email messages verbatim, and changing CRLF to
|
||||||
# LF will corrupt them.
|
# LF will corrupt them.
|
||||||
test-data/** text=false
|
test-data/** text=false
|
||||||
|
|||||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -5,5 +5,5 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "monthly"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "cargo"
|
prefix: "chore(cargo)"
|
||||||
open-pull-requests-limit: 50
|
open-pull-requests-limit: 50
|
||||||
|
|||||||
26
.github/mergeable.yml
vendored
26
.github/mergeable.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
version: 2
|
|
||||||
mergeable:
|
|
||||||
- when: pull_request.*
|
|
||||||
name: "Changelog check"
|
|
||||||
validate:
|
|
||||||
- do: or
|
|
||||||
validate:
|
|
||||||
- do: description
|
|
||||||
must_include:
|
|
||||||
regex: "#skip-changelog"
|
|
||||||
- do: and
|
|
||||||
validate:
|
|
||||||
- do: dependent
|
|
||||||
changed:
|
|
||||||
file: "src/**"
|
|
||||||
required: ["CHANGELOG.md"]
|
|
||||||
- do: dependent
|
|
||||||
changed:
|
|
||||||
file: "deltachat-ffi/src/**"
|
|
||||||
required: ["CHANGELOG.md"]
|
|
||||||
fail:
|
|
||||||
- do: checks
|
|
||||||
status: "action_required"
|
|
||||||
payload:
|
|
||||||
title: Changelog might need an update
|
|
||||||
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
|
|
||||||
270
.github/workflows/ci.yml
vendored
270
.github/workflows/ci.yml
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
# GitHub Actions workflow to
|
||||||
|
# lint Rust and Python code
|
||||||
|
# and run Rust tests, Python tests and async Python tests.
|
||||||
|
|
||||||
name: Rust CI
|
name: Rust CI
|
||||||
|
|
||||||
# Cancel previously started workflow runs
|
# Cancel previously started workflow runs
|
||||||
@@ -10,19 +14,21 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint_rust:
|
||||||
name: Rustfmt and Clippy
|
name: Lint Rust
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
RUSTUP_TOOLCHAIN: 1.68.0
|
RUSTUP_TOOLCHAIN: 1.78.0
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Install rustfmt and clippy
|
- name: Install rustfmt and clippy
|
||||||
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
|
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
@@ -31,12 +37,16 @@ jobs:
|
|||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: scripts/clippy.sh
|
run: scripts/clippy.sh
|
||||||
|
- name: Check
|
||||||
|
run: cargo check --workspace --all-targets --all-features
|
||||||
|
|
||||||
cargo_deny:
|
cargo_deny:
|
||||||
name: cargo deny
|
name: cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||||
with:
|
with:
|
||||||
arguments: --all-features --workspace
|
arguments: --all-features --workspace
|
||||||
@@ -47,7 +57,9 @@ jobs:
|
|||||||
name: Check provider database
|
name: Check provider database
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Check provider database
|
- name: Check provider database
|
||||||
run: scripts/update-provider-database.sh
|
run: scripts/update-provider-database.sh
|
||||||
|
|
||||||
@@ -57,38 +69,34 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: -Dwarnings
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
- name: Rustdoc
|
- name: Rustdoc
|
||||||
run: cargo doc --document-private-items --no-deps
|
run: cargo doc --document-private-items --no-deps
|
||||||
|
|
||||||
build_and_test:
|
rust_tests:
|
||||||
name: Build and test
|
name: Rust tests
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Currently used Rust version.
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: 1.68.0
|
rust: 1.78.0
|
||||||
python: 3.9
|
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
rust: 1.68.0
|
rust: 1.78.0
|
||||||
python: false # Python bindings compilation on Windows is not supported.
|
- os: macos-latest
|
||||||
|
rust: 1.78.0
|
||||||
|
|
||||||
# Minimum Supported Rust Version = 1.64.0
|
# Minimum Supported Rust Version = 1.77.0
|
||||||
#
|
|
||||||
# Minimum Supported Python Version = 3.7
|
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
|
||||||
# built.
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: 1.64.0
|
rust: 1.77.0
|
||||||
python: 3.7
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
- name: Install Rust ${{ matrix.rust }}
|
- name: Install Rust ${{ matrix.rust }}
|
||||||
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
||||||
@@ -97,64 +105,206 @@ jobs:
|
|||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Check
|
- name: Install nextest
|
||||||
run: cargo check --workspace --bins --examples --tests --benches
|
uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: nextest
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: cargo test --workspace
|
env:
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
|
||||||
|
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
|
||||||
|
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
|
||||||
|
run: cargo nextest run --workspace
|
||||||
|
|
||||||
|
- name: Doc-Tests
|
||||||
|
env:
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
run: cargo test --workspace --doc
|
||||||
|
|
||||||
- name: Test cargo vendor
|
- name: Test cargo vendor
|
||||||
run: cargo vendor
|
run: cargo vendor
|
||||||
|
|
||||||
|
c_library:
|
||||||
|
name: Build C library
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Cache rust cargo artifacts
|
||||||
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Build C library
|
||||||
|
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||||
|
|
||||||
|
- name: Upload C library
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
|
path: target/debug/libdeltachat.a
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
rpc_server:
|
||||||
|
name: Build deltachat-rpc-server
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Cache rust cargo artifacts
|
||||||
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server
|
||||||
|
run: cargo build -p deltachat-rpc-server
|
||||||
|
|
||||||
|
- name: Upload deltachat-rpc-server
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
|
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
python_lint:
|
||||||
|
name: Python lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Install tox
|
||||||
|
run: pip install tox
|
||||||
|
|
||||||
|
- name: Lint Python bindings
|
||||||
|
working-directory: python
|
||||||
|
run: tox -e lint
|
||||||
|
|
||||||
|
- name: Lint deltachat-rpc-client
|
||||||
|
working-directory: deltachat-rpc-client
|
||||||
|
run: tox -e lint
|
||||||
|
|
||||||
|
cffi_python_tests:
|
||||||
|
name: CFFI Python tests
|
||||||
|
needs: ["c_library", "python_lint"]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Currently used Rust version.
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: 3.12
|
||||||
|
- os: macos-latest
|
||||||
|
python: 3.12
|
||||||
|
|
||||||
|
# PyPy tests
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: pypy3.10
|
||||||
|
- os: macos-latest
|
||||||
|
python: pypy3.10
|
||||||
|
|
||||||
|
# Minimum Supported Python Version = 3.7
|
||||||
|
# This is the minimum version for which manylinux Python wheels are
|
||||||
|
# built. Test it with minimum supported Rust version.
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: 3.7
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Download libdeltachat.a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
|
path: target/debug
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
if: ${{ matrix.python }}
|
uses: actions/setup-python@v5
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
if: ${{ matrix.python }}
|
|
||||||
run: pip install tox
|
run: pip install tox
|
||||||
|
|
||||||
- name: Build C library
|
|
||||||
if: ${{ matrix.python }}
|
|
||||||
run: cargo build -p deltachat_ffi --features jsonrpc
|
|
||||||
|
|
||||||
- name: Run python tests
|
- name: Run python tests
|
||||||
if: ${{ matrix.python }}
|
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
DCC_RS_TARGET: debug
|
DCC_RS_TARGET: debug
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
DCC_RS_DEV: ${{ github.workspace }}
|
||||||
working-directory: python
|
working-directory: python
|
||||||
run: tox -e lint,mypy,doc,py3
|
run: tox -e mypy,doc,py
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server
|
rpc_python_tests:
|
||||||
if: ${{ matrix.python }}
|
name: JSON-RPC Python tests
|
||||||
run: cargo build -p deltachat-rpc-server
|
needs: ["python_lint", "rpc_server"]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: 3.12
|
||||||
|
- os: macos-latest
|
||||||
|
python: 3.12
|
||||||
|
- os: windows-latest
|
||||||
|
python: 3.12
|
||||||
|
|
||||||
|
# PyPy tests
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: pypy3.10
|
||||||
|
- os: macos-latest
|
||||||
|
python: pypy3.10
|
||||||
|
|
||||||
|
# Minimum Supported Python Version = 3.7
|
||||||
|
- os: ubuntu-latest
|
||||||
|
python: 3.7
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Install python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
|
- name: Install tox
|
||||||
|
run: pip install tox
|
||||||
|
|
||||||
|
- name: Download deltachat-rpc-server
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
|
path: target/debug
|
||||||
|
|
||||||
|
- name: Make deltachat-rpc-server executable
|
||||||
|
if: ${{ matrix.os != 'windows-latest' }}
|
||||||
|
run: chmod +x target/debug/deltachat-rpc-server
|
||||||
|
|
||||||
- name: Add deltachat-rpc-server to path
|
- name: Add deltachat-rpc-server to path
|
||||||
if: ${{ matrix.python }}
|
if: ${{ matrix.os != 'windows-latest' }}
|
||||||
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
|
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Add deltachat-rpc-server to path
|
||||||
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
|
run: |
|
||||||
|
"${{ github.workspace }}/target/debug" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
- name: Run deltachat-rpc-client tests
|
- name: Run deltachat-rpc-client tests
|
||||||
if: ${{ matrix.python }}
|
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: tox -e py3,lint
|
run: tox -e py
|
||||||
|
|
||||||
- name: Install pypy
|
|
||||||
if: ${{ matrix.python }}
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "pypy${{ matrix.python }}"
|
|
||||||
|
|
||||||
- name: Run pypy tests
|
|
||||||
if: ${{ matrix.python }}
|
|
||||||
env:
|
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
|
||||||
DCC_RS_TARGET: debug
|
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
|
||||||
working-directory: python
|
|
||||||
run: tox -e pypy3
|
|
||||||
|
|||||||
426
.github/workflows/deltachat-rpc-server.yml
vendored
426
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -1,4 +1,10 @@
|
|||||||
# Manually triggered action to build deltachat-rpc-server binaries.
|
# GitHub Actions workflow
|
||||||
|
# to build `deltachat-rpc-server` binaries
|
||||||
|
# and upload them to the release.
|
||||||
|
#
|
||||||
|
# The workflow is automatically triggered on releases.
|
||||||
|
# It can also be triggered manually
|
||||||
|
# to produce binary artifacts for testing.
|
||||||
|
|
||||||
name: Build deltachat-rpc-server binaries
|
name: Build deltachat-rpc-server binaries
|
||||||
|
|
||||||
@@ -15,112 +21,378 @@ jobs:
|
|||||||
# Build a version statically linked against musl libc
|
# Build a version statically linked against musl libc
|
||||||
# to avoid problems with glibc version incompatibility.
|
# to avoid problems with glibc version incompatibility.
|
||||||
build_linux:
|
build_linux:
|
||||||
name: Cross-compile deltachat-rpc-server for x86_64, aarch64 and armv7 Linux
|
name: Linux
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: sh scripts/zig-rpc-server.sh
|
|
||||||
|
|
||||||
- name: Upload x86_64 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-x86_64
|
|
||||||
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload aarch64 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-aarch64
|
|
||||||
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload armv7 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv7
|
|
||||||
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build_windows:
|
|
||||||
name: Build deltachat-rpc-server for Windows
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||||
- os: windows-latest
|
runs-on: ubuntu-latest
|
||||||
artifact: win32.exe
|
|
||||||
path: deltachat-rpc-server.exe
|
|
||||||
target: i686-pc-windows-msvc
|
|
||||||
|
|
||||||
- os: windows-latest
|
|
||||||
artifact: win64.exe
|
|
||||||
path: deltachat-rpc-server.exe
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Setup rust target
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: rustup target add ${{ matrix.target }}
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.target }} --features vendored
|
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.artifact }}
|
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
path: target/${{ matrix.target}}/release/${{ matrix.path }}
|
path: result/bin/deltachat-rpc-server
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_windows:
|
||||||
|
name: Windows
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [win32, win64]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server binaries
|
||||||
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||||
|
path: result/bin/deltachat-rpc-server.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_macos:
|
||||||
|
name: macOS
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [x86_64, aarch64]
|
||||||
|
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Setup rust target
|
||||||
|
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||||
|
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_android:
|
||||||
|
name: Android
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server binaries
|
||||||
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
|
path: result/bin/deltachat-rpc-server
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Upload binaries to the release
|
name: Build wheels and upload binaries to the release
|
||||||
needs: ["build_linux", "build_windows"]
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
url: https://pypi.org/p/deltachat-rpc-server
|
||||||
permissions:
|
permissions:
|
||||||
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- name: Download deltachat-rpc-server-x86_64
|
- uses: actions/checkout@v4
|
||||||
uses: "actions/download-artifact@v3"
|
|
||||||
with:
|
with:
|
||||||
name: "deltachat-rpc-server-x86_64"
|
show-progress: false
|
||||||
path: "dist/deltachat-rpc-server-x86_64"
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server-aarch64
|
- name: Download Linux aarch64 binary
|
||||||
uses: "actions/download-artifact@v3"
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: "deltachat-rpc-server-aarch64"
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
path: "dist/deltachat-rpc-server-aarch64"
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server-armv7
|
- name: Download Linux armv7l binary
|
||||||
uses: "actions/download-artifact@v3"
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: "deltachat-rpc-server-armv7"
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
path: "dist/deltachat-rpc-server-armv7"
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server-win32.exe
|
- name: Download Linux armv6l binary
|
||||||
uses: "actions/download-artifact@v3"
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: "deltachat-rpc-server-win32.exe"
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
path: "dist/deltachat-rpc-server-win32.exe"
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server-win64.exe
|
- name: Download Linux i686 binary
|
||||||
uses: "actions/download-artifact@v3"
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: "deltachat-rpc-server-win64.exe"
|
name: deltachat-rpc-server-i686-linux
|
||||||
path: "dist/deltachat-rpc-server-win64.exe"
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
- name: List downloaded artifacts
|
- name: Download Linux x86_64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
|
- name: Download Win32 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win32
|
||||||
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
|
- name: Download Win64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win64
|
||||||
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for x86_64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for aarch64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
|
- name: Download Android binary for arm64-v8a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
|
- name: Download Android binary for armeabi-v7a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
|
|
||||||
|
- name: Create bin/ directory
|
||||||
|
run: |
|
||||||
|
mkdir -p bin
|
||||||
|
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-linux
|
||||||
|
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv7l-linux
|
||||||
|
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv6l-linux
|
||||||
|
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-i686-linux
|
||||||
|
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-linux
|
||||||
|
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win32.exe
|
||||||
|
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
|
||||||
|
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
|
||||||
|
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
|
||||||
|
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
|
||||||
|
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
|
||||||
|
|
||||||
|
- name: List binaries
|
||||||
|
run: ls -l bin/
|
||||||
|
|
||||||
|
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||||
|
- name: Install python 3.12
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
|
||||||
|
- name: Install wheel
|
||||||
|
run: pip install wheel
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server Python wheels and source package
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
nix build .#deltachat-rpc-server-x86_64-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-armv7l-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-armv6l-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-aarch64-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-i686-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-win64-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-win32-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-source
|
||||||
|
cp result/*.tar.gz dist/
|
||||||
|
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||||
|
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
||||||
|
mv *.whl dist/
|
||||||
|
|
||||||
|
- name: List artifacts
|
||||||
run: ls -l dist/
|
run: ls -l dist/
|
||||||
|
|
||||||
- name: Upload binaries to the GitHub release
|
- name: Upload binaries to the GitHub release
|
||||||
|
if: github.event_name == 'release'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
run: |
|
run: |
|
||||||
gh release upload ${{ github.ref_name }} \
|
gh release upload ${{ github.ref_name }} \
|
||||||
--repo ${{ github.repository }} \
|
--repo ${{ github.repository }} \
|
||||||
dist/deltachat-rpc-server-*
|
bin/* dist/*
|
||||||
|
|
||||||
|
- name: Publish deltachat-rpc-client to PyPI
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
|
publish_npm_package:
|
||||||
|
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||||
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Download Linux aarch64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux armv7l binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux armv6l binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux i686 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-i686-linux
|
||||||
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux x86_64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
|
- name: Download Win32 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win32
|
||||||
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
|
- name: Download Win64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win64
|
||||||
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for x86_64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for aarch64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
|
- name: Download Android binary for arm64-v8a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
|
- name: Download Android binary for armeabi-v7a
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
|
|
||||||
|
- name: make npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||||
|
run: |
|
||||||
|
cd deltachat-rpc-server/npm-package
|
||||||
|
|
||||||
|
python --version
|
||||||
|
|
||||||
|
python scripts/pack_binary_for_platform.py aarch64-unknown-linux-musl ../../deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py armv7-unknown-linux-musleabihf ../../deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py arm-unknown-linux-musleabihf ../../deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py i686-unknown-linux-musl ../../deltachat-rpc-server-i686-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py x86_64-unknown-linux-musl ../../deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py i686-pc-windows-gnu ../../deltachat-rpc-server-win32.d/deltachat-rpc-server.exe
|
||||||
|
python scripts/pack_binary_for_platform.py x86_64-pc-windows-gnu ../../deltachat-rpc-server-win64.d/deltachat-rpc-server.exe
|
||||||
|
python scripts/pack_binary_for_platform.py x86_64-apple-darwin ../../deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py aarch64-apple-darwin ../../deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py aarch64-linux-android ../../deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server
|
||||||
|
python scripts/pack_binary_for_platform.py armv7-linux-androideabi ../../deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server
|
||||||
|
|
||||||
|
ls -lah platform_package
|
||||||
|
|
||||||
|
for platform in ./platform_package/*; do npm pack "$platform"; done
|
||||||
|
npm pack
|
||||||
|
ls -lah
|
||||||
|
|
||||||
|
- name: Upload to artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-npm-package
|
||||||
|
path: deltachat-rpc-server/npm-package/*.tgz
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload npm packets to the GitHub release
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
run: |
|
||||||
|
gh release upload ${{ github.ref_name }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
deltachat-rpc-server/npm-package/*.tgz
|
||||||
|
|
||||||
|
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
working-directory: deltachat-rpc-server/npm-package
|
||||||
|
run: |
|
||||||
|
ls -lah platform_package
|
||||||
|
for platform in *.tgz; do npm publish --provenance "$platform"; done
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
3
.github/workflows/dependabot.yml
vendored
3
.github/workflows/dependabot.yml
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
# GitHub Actions workflow
|
||||||
|
# to automatically approve PRs made by Dependabot.
|
||||||
|
|
||||||
name: Dependabot auto-approve
|
name: Dependabot auto-approve
|
||||||
on: pull_request
|
on: pull_request
|
||||||
|
|
||||||
|
|||||||
18
.github/workflows/jsonrpc-client-npm-package.yml
vendored
18
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -13,11 +13,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Install tree
|
- name: Install tree
|
||||||
run: sudo apt install tree
|
run: sudo apt install tree
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
- name: Get tag
|
- name: Get tag
|
||||||
id: tag
|
id: tag
|
||||||
uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
@@ -38,19 +39,18 @@ jobs:
|
|||||||
node --version
|
node --version
|
||||||
echo $DELTACHAT_JSONRPC_TAR_GZ
|
echo $DELTACHAT_JSONRPC_TAR_GZ
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm install --ignore-scripts
|
||||||
npm install --ignore-scripts
|
|
||||||
- name: Package
|
- name: Package
|
||||||
shell: bash
|
shell: bash
|
||||||
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: |
|
run: |
|
||||||
cd deltachat-jsonrpc/typescript
|
|
||||||
npm run build
|
npm run build
|
||||||
npm pack .
|
npm pack .
|
||||||
ls -lah
|
ls -lah
|
||||||
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
||||||
- name: Upload Prebuild
|
- name: Upload Prebuild
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-jsonrpc-client.tgz
|
name: deltachat-jsonrpc-client.tgz
|
||||||
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||||
|
|||||||
41
.github/workflows/jsonrpc.yml
vendored
41
.github/workflows/jsonrpc.yml
vendored
@@ -2,9 +2,9 @@ name: JSON-RPC API Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [main]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -14,32 +14,29 @@ jobs:
|
|||||||
build_and_test:
|
build_and_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
show-progress: false
|
||||||
|
- name: Use Node.js 18.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
- name: Add Rust cache
|
- name: Add Rust cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm install
|
||||||
npm install
|
|
||||||
- name: Build TypeScript, run Rust tests, generate bindings
|
- name: Build TypeScript, run Rust tests, generate bindings
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm run build
|
||||||
npm run build
|
|
||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm run test
|
||||||
npm run test
|
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
- name: make sure websocket server version still builds
|
- name: make sure websocket server version still builds
|
||||||
run: |
|
working-directory: deltachat-jsonrpc
|
||||||
cd deltachat-jsonrpc
|
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||||
cargo build --bin deltachat-jsonrpc-server --features webserver
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm run prettier:check
|
||||||
npm run prettier:check
|
|
||||||
|
|||||||
31
.github/workflows/node-delete-preview.yml
vendored
31
.github/workflows/node-delete-preview.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat
|
|
||||||
name: Delete node PR previews
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
delete:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Get Pull Request ID
|
|
||||||
id: getid
|
|
||||||
run: |
|
|
||||||
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
|
|
||||||
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
|
|
||||||
- name: Renaming
|
|
||||||
run: |
|
|
||||||
# create empty file to copy it over the outdated deliverable on download.delta.chat
|
|
||||||
echo "This preview build is outdated and has been removed." > empty
|
|
||||||
cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz
|
|
||||||
- name: Replace builds with dummy files
|
|
||||||
uses: horochx/deploy-via-scp@v1.0.1
|
|
||||||
with:
|
|
||||||
user: ${{ secrets.USERNAME }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
host: "download.delta.chat"
|
|
||||||
port: 22
|
|
||||||
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
|
|
||||||
remote: "/var/www/html/download/node/preview/"
|
|
||||||
21
.github/workflows/node-docs.yml
vendored
21
.github/workflows/node-docs.yml
vendored
@@ -1,24 +1,31 @@
|
|||||||
|
# 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
|
name: Generate & upload node.js documentation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate:
|
generate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js 16.x
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Use Node.js 18.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
- name: npm install and generate documentation
|
- name: npm install and generate documentation
|
||||||
|
working-directory: node
|
||||||
run: |
|
run: |
|
||||||
cd node
|
|
||||||
npm i --ignore-scripts
|
npm i --ignore-scripts
|
||||||
npx typedoc
|
npx typedoc
|
||||||
mv docs js
|
mv docs js
|
||||||
|
|||||||
127
.github/workflows/node-package.yml
vendored
127
.github/workflows/node-package.yml
vendored
@@ -12,13 +12,14 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04, macos-latest, windows-latest]
|
os: [macos-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
- name: System info
|
- name: System info
|
||||||
run: |
|
run: |
|
||||||
rustc -vV
|
rustc -vV
|
||||||
@@ -28,7 +29,7 @@ jobs:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APPDATA }}/npm-cache
|
${{ env.APPDATA }}/npm-cache
|
||||||
@@ -36,7 +37,7 @@ jobs:
|
|||||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry/
|
~/.cargo/registry/
|
||||||
@@ -46,34 +47,104 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies & build
|
- name: Install dependencies & build
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
working-directory: node
|
||||||
cd node
|
run: npm install --verbose
|
||||||
npm install --verbose
|
|
||||||
|
|
||||||
- name: Build Prebuild
|
- name: Build Prebuild
|
||||||
|
working-directory: node
|
||||||
run: |
|
run: |
|
||||||
cd node
|
|
||||||
npm run prebuildify
|
npm run prebuildify
|
||||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||||
|
|
||||||
- name: Upload Prebuild
|
- name: Upload Prebuild
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}
|
name: ${{ matrix.os }}
|
||||||
path: node/${{ matrix.os }}.tar.gz
|
path: node/${{ matrix.os }}.tar.gz
|
||||||
|
|
||||||
|
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:
|
pack-module:
|
||||||
needs: prebuild
|
needs: [prebuild, prebuild-linux]
|
||||||
name: Package deltachat-node and upload to download.delta.chat
|
name: Package deltachat-node and upload to download.delta.chat
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install tree
|
- name: Install tree
|
||||||
run: sudo apt install tree
|
run: sudo apt install tree
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v2
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
- name: Get tag
|
- name: Get tag
|
||||||
id: tag
|
id: tag
|
||||||
uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
@@ -96,26 +167,26 @@ jobs:
|
|||||||
npm --version
|
npm --version
|
||||||
node --version
|
node --version
|
||||||
echo $DELTACHAT_NODE_TAR_GZ
|
echo $DELTACHAT_NODE_TAR_GZ
|
||||||
- name: Download Ubuntu prebuild
|
- name: Download Linux prebuild
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ubuntu-20.04
|
name: linux
|
||||||
- name: Download macOS prebuild
|
- name: Download macOS prebuild
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-latest
|
name: macos-latest
|
||||||
- name: Download Windows prebuild
|
- name: Download Windows prebuild
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-latest
|
name: windows-latest
|
||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir node/prebuilds
|
mkdir node/prebuilds
|
||||||
tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds
|
tar -xvzf linux.tar.gz -C node/prebuilds
|
||||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
tar -xvzf macos-latest.tar.gz -C node/prebuilds
|
||||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
tar -xvzf windows-latest.tar.gz -C node/prebuilds
|
||||||
tree node/prebuilds
|
tree node/prebuilds
|
||||||
rm -rf ubuntu-20.04 macos-latest windows-latest
|
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
run: |
|
run: |
|
||||||
npm install --ignore-scripts
|
npm install --ignore-scripts
|
||||||
@@ -133,7 +204,7 @@ jobs:
|
|||||||
ls -lah
|
ls -lah
|
||||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||||
- name: Upload prebuild
|
- name: Upload prebuild
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-node.tgz
|
name: deltachat-node.tgz
|
||||||
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||||
|
|||||||
40
.github/workflows/node-tests.yml
vendored
40
.github/workflows/node-tests.yml
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
# GitHub Actions workflow
|
||||||
|
# to test Node.js bindings.
|
||||||
|
|
||||||
name: "node.js tests"
|
name: "node.js tests"
|
||||||
|
|
||||||
# Cancel previously started workflow runs
|
# Cancel previously started workflow runs
|
||||||
@@ -10,7 +13,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
@@ -20,11 +23,12 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
- name: System info
|
- name: System info
|
||||||
run: |
|
run: |
|
||||||
rustc -vV
|
rustc -vV
|
||||||
@@ -34,7 +38,7 @@ jobs:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APPDATA }}/npm-cache
|
${{ env.APPDATA }}/npm-cache
|
||||||
@@ -42,7 +46,7 @@ jobs:
|
|||||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry/
|
~/.cargo/registry/
|
||||||
@@ -52,25 +56,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies & build
|
- name: Install dependencies & build
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
working-directory: node
|
||||||
cd node
|
run: npm install --verbose
|
||||||
npm install --verbose
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
if: runner.os != 'Windows'
|
working-directory: node
|
||||||
run: |
|
run: npm run test
|
||||||
cd node
|
|
||||||
npm run test
|
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
|
||||||
- name: Run tests on Windows, except lint
|
|
||||||
timeout-minutes: 10
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
run: |
|
|
||||||
cd node
|
|
||||||
npm run test:mocha
|
|
||||||
env:
|
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
|
||||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||||
|
|||||||
47
.github/workflows/publish-deltachat-rpc-client-pypi.yml
vendored
Normal file
47
.github/workflows/publish-deltachat-rpc-client-pypi.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Publish deltachat-rpc-client to PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build distribution
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: python3 -m pip install build
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
working-directory: deltachat-rpc-client
|
||||||
|
run: python3 -m build
|
||||||
|
- name: Store the distribution packages
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: deltachat-rpc-client/dist/
|
||||||
|
|
||||||
|
publish-to-pypi:
|
||||||
|
name: Publish Python distribution to PyPI
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: pypi
|
||||||
|
url: https://pypi.org/p/deltachat-rpc-client
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all the dists
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
- name: Publish deltachat-rpc-client to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
19
.github/workflows/repl.yml
vendored
19
.github/workflows/repl.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
# Manually triggered action to build a Windows repl.exe which users can
|
# Manually triggered GitHub Actions workflow
|
||||||
|
# to build a Windows repl.exe which users can
|
||||||
# download to debug complex bugs.
|
# download to debug complex bugs.
|
||||||
|
|
||||||
name: Build Windows REPL .exe
|
name: Build Windows REPL .exe
|
||||||
@@ -9,15 +10,17 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build_repl:
|
build_repl:
|
||||||
name: Build REPL example
|
name: Build REPL example
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build -p deltachat-repl --features vendored
|
run: nix build .#deltachat-repl-win64
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: repl.exe
|
name: repl.exe
|
||||||
path: "target/debug/deltachat-repl.exe"
|
path: "result/bin/deltachat-repl.exe"
|
||||||
|
|||||||
90
.github/workflows/upload-docs.yml
vendored
90
.github/workflows/upload-docs.yml
vendored
@@ -1,25 +1,91 @@
|
|||||||
name: Build & Deploy Documentation on rs.delta.chat
|
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
- docs-gh-action
|
- build_jsonrpc_docs_ci
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-rs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Build the documentation with cargo
|
- name: Build the documentation with cargo
|
||||||
run: |
|
run: |
|
||||||
cargo doc --package deltachat --no-deps --document-private-items
|
cargo doc --package deltachat --no-deps --document-private-items
|
||||||
- name: Upload to rs.delta.chat
|
- name: Upload to rs.delta.chat
|
||||||
uses: up9cloud/action-rsync@v1.3
|
run: |
|
||||||
env:
|
mkdir -p "$HOME/.ssh"
|
||||||
USER: ${{ secrets.USERNAME }}
|
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||||
KEY: ${{ secrets.KEY }}
|
chmod 600 "$HOME/.ssh/key"
|
||||||
HOST: "delta.chat"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
|
||||||
SOURCE: "target/doc"
|
|
||||||
TARGET: "/var/www/html/rs/"
|
build-python:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- name: Build Python documentation
|
||||||
|
run: nix build .#python-docs
|
||||||
|
- name: Upload to py.delta.chat
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.ssh"
|
||||||
|
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||||
|
chmod 600 "$HOME/.ssh/key"
|
||||||
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
|
||||||
|
|
||||||
|
build-c:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- name: Build C documentation
|
||||||
|
run: nix build .#docs
|
||||||
|
- name: Upload to c.delta.chat
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.ssh"
|
||||||
|
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||||
|
chmod 600 "$HOME/.ssh/key"
|
||||||
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
||||||
|
|
||||||
|
build-ts:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./deltachat-jsonrpc/typescript
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
- name: npm install
|
||||||
|
run: npm install
|
||||||
|
- name: npm run build
|
||||||
|
run: npm run build
|
||||||
|
- name: Run docs script
|
||||||
|
run: npm run docs
|
||||||
|
- name: Upload to js.jsonrpc.delta.chat
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.ssh"
|
||||||
|
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||||
|
chmod 600 "$HOME/.ssh/key"
|
||||||
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"
|
||||||
|
|||||||
23
.github/workflows/upload-ffi-docs.yml
vendored
23
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,25 +1,28 @@
|
|||||||
|
# GitHub Actions workflow
|
||||||
|
# to build `deltachat_ffi` crate documentation
|
||||||
|
# and upload it to <https://cffi.delta.chat/>
|
||||||
|
|
||||||
name: Build & Deploy Documentation on cffi.delta.chat
|
name: Build & Deploy Documentation on cffi.delta.chat
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
- docs-gh-action
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
- name: Build the documentation with cargo
|
- name: Build the documentation with cargo
|
||||||
run: |
|
run: |
|
||||||
cargo doc --package deltachat_ffi --no-deps
|
cargo doc --package deltachat_ffi --no-deps
|
||||||
- name: Upload to cffi.delta.chat
|
- name: Upload to cffi.delta.chat
|
||||||
uses: up9cloud/action-rsync@v1.3
|
run: |
|
||||||
env:
|
mkdir -p "$HOME/.ssh"
|
||||||
USER: ${{ secrets.USERNAME }}
|
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
||||||
KEY: ${{ secrets.KEY }}
|
chmod 600 "$HOME/.ssh/key"
|
||||||
HOST: "delta.chat"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
|
||||||
SOURCE: "target/doc"
|
|
||||||
TARGET: "/var/www/html/cffi/"
|
|
||||||
|
|||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
# ignore vi temporaries
|
# ignore vi temporaries
|
||||||
*~
|
*~
|
||||||
@@ -18,6 +19,9 @@ python/.eggs
|
|||||||
__pycache__
|
__pycache__
|
||||||
python/src/deltachat/capi*.so
|
python/src/deltachat/capi*.so
|
||||||
python/.venv/
|
python/.venv/
|
||||||
|
python/venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
python/liveconfig*
|
python/liveconfig*
|
||||||
|
|
||||||
@@ -29,6 +33,7 @@ deltachat-ffi/xml
|
|||||||
|
|
||||||
coverage/
|
coverage/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
python/accounts.txt
|
python/accounts.txt
|
||||||
python/all-testaccounts.txt
|
python/all-testaccounts.txt
|
||||||
@@ -40,3 +45,10 @@ node/build/
|
|||||||
node/dist/
|
node/dist/
|
||||||
node/prebuilds/
|
node/prebuilds/
|
||||||
node/.nyc_output/
|
node/.nyc_output/
|
||||||
|
|
||||||
|
# Nix symlink.
|
||||||
|
result
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.envrc
|
||||||
|
.direnv
|
||||||
1837
CHANGELOG.md
1837
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -12,26 +12,22 @@ else()
|
|||||||
set(DYNAMIC_EXT "dll")
|
set(DYNAMIC_EXT "dll")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
||||||
|
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
||||||
|
else()
|
||||||
|
set(ARCH_DIR "./")
|
||||||
|
endif()
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT
|
OUTPUT
|
||||||
"target/release/libdeltachat.a"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||||
"target/release/pkgconfig/deltachat.pc"
|
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||||
COMMAND
|
COMMAND
|
||||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||||
${CARGO} build --release --no-default-features --features jsonrpc
|
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
|
||||||
|
|
||||||
# Build in `deltachat-ffi` directory instead of using
|
|
||||||
# `--package deltachat_ffi` to avoid feature resolver version
|
|
||||||
# "1" bug which makes `--no-default-features` affect only
|
|
||||||
# `deltachat`, but not `deltachat-ffi` package.
|
|
||||||
#
|
|
||||||
# We can't enable version "2" resolver [1] because it is not
|
|
||||||
# stable yet on rust 1.50.0.
|
|
||||||
#
|
|
||||||
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
|
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,12 +35,12 @@ add_custom_target(
|
|||||||
lib_deltachat
|
lib_deltachat
|
||||||
ALL
|
ALL
|
||||||
DEPENDS
|
DEPENDS
|
||||||
"target/release/libdeltachat.a"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||||
"target/release/pkgconfig/deltachat.pc"
|
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||||
|
|||||||
126
CONTRIBUTING.md
Normal file
126
CONTRIBUTING.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Contributing guidelines
|
||||||
|
|
||||||
|
## Reporting bugs
|
||||||
|
|
||||||
|
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
|
||||||
|
If the bug you found is specific to
|
||||||
|
[Android](https://github.com/deltachat/deltachat-android/issues),
|
||||||
|
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
||||||
|
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
|
||||||
|
report it to the corresponding repository.
|
||||||
|
|
||||||
|
## Proposing features
|
||||||
|
|
||||||
|
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
|
||||||
|
|
||||||
|
## Contributing code
|
||||||
|
|
||||||
|
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||||
|
|
||||||
|
If you have write access to the repository,
|
||||||
|
push a branch named `<username>/<feature>`
|
||||||
|
so it is clear who is responsible for the branch,
|
||||||
|
and open a PR proposing to merge the change.
|
||||||
|
Otherwise fork the repository and create a branch in your fork.
|
||||||
|
|
||||||
|
You can find the list of good first issues
|
||||||
|
and a link to this guide
|
||||||
|
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
||||||
|
|
||||||
|
### Coding conventions
|
||||||
|
|
||||||
|
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
||||||
|
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
||||||
|
|
||||||
|
Commit messages follow the [Conventional Commits] notation.
|
||||||
|
We use [git-cliff] to generate the changelog from commit messages before the release.
|
||||||
|
|
||||||
|
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
|
||||||
|
|
||||||
|
The following prefix types are used:
|
||||||
|
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||||
|
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||||
|
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||||
|
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||||
|
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||||
|
- `test`: Test changes and improvements to the testing framework.
|
||||||
|
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
|
||||||
|
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
|
||||||
|
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
|
||||||
|
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
|
||||||
|
|
||||||
|
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
|
||||||
|
|
||||||
|
If you intend to squash merge the PR from the web interface,
|
||||||
|
make sure the PR title follows the conventional commits notation
|
||||||
|
as it will end up being a commit title.
|
||||||
|
Otherwise make sure each commit title follows the conventional commit notation.
|
||||||
|
|
||||||
|
#### Breaking Changes
|
||||||
|
|
||||||
|
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
|
||||||
|
|
||||||
|
Alternatively, breaking changes can go into the commit description, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
fix: Fix race condition and db corruption when a message was received during backup
|
||||||
|
|
||||||
|
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Multiple Changes in one PR
|
||||||
|
|
||||||
|
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
|
||||||
|
|
||||||
|
[Clippy]: https://doc.rust-lang.org/clippy/
|
||||||
|
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||||
|
[git-cliff]: https://git-cliff.org/
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||||
|
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
|
||||||
|
capitalize it but do not add a full stop as the contexts will be separated by `:`.
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
.with_context(|| format!("Unable to trash message {msg_id}"))
|
||||||
|
```
|
||||||
|
|
||||||
|
All errors should be handled in one of these ways:
|
||||||
|
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
|
||||||
|
- With `.log_err().ok()`.
|
||||||
|
- Bubbled up with `?`.
|
||||||
|
|
||||||
|
`backtrace` feature is enabled for `anyhow` crate
|
||||||
|
and `debug = 1` option is set in the test profile.
|
||||||
|
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||||
|
and get a backtrace with line numbers in resultified tests
|
||||||
|
which return `anyhow::Result`.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
For logging, use `info!`, `warn!` and `error!` macros.
|
||||||
|
Log messages should be capitalized and have a full stop in the end. For example:
|
||||||
|
```
|
||||||
|
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||||
|
```
|
||||||
|
|
||||||
|
Format anyhow errors with `{:#}` to print all the contexts like this:
|
||||||
|
```
|
||||||
|
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reviewing
|
||||||
|
|
||||||
|
Once a PR has an approval and passes CI, it can be merged.
|
||||||
|
|
||||||
|
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
|
||||||
|
This is to ensure that PRs are merged as intended by the author,
|
||||||
|
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
|
||||||
|
|
||||||
|
If you do not have access to the repository and created a PR from a fork,
|
||||||
|
ask the maintainers to merge the PR and say how it should be merged.
|
||||||
|
|
||||||
|
## Other ways to contribute
|
||||||
|
|
||||||
|
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
||||||
3446
Cargo.lock
generated
3446
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
124
Cargo.toml
124
Cargo.toml
@@ -1,9 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.111.0"
|
version = "1.138.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.64"
|
rust-version = "1.77"
|
||||||
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
@@ -11,6 +12,10 @@ panic = 'abort'
|
|||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
||||||
[profile.test]
|
[profile.test]
|
||||||
|
# Make anyhow `backtrace` feature useful.
|
||||||
|
# With `debug = 0` there are no line numbers in the backtrace
|
||||||
|
# produced with RUST_BACKTRACE=1.
|
||||||
|
debug = 1
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
|
|
||||||
# Always optimize dependencies.
|
# Always optimize dependencies.
|
||||||
@@ -23,87 +28,100 @@ opt-level = "z"
|
|||||||
lto = true
|
lto = true
|
||||||
panic = 'abort'
|
panic = 'abort'
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
codegen-units = 1
|
||||||
[patch.crates-io]
|
strip = true
|
||||||
default-net = { git = "https://github.com/dignifiedquire/default-net.git", branch="feat-android" }
|
|
||||||
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
|
||||||
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat_derive = { path = "./deltachat_derive" }
|
deltachat_derive = { path = "./deltachat_derive" }
|
||||||
|
deltachat-time = { path = "./deltachat-time" }
|
||||||
|
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
|
||||||
format-flowed = { path = "./format-flowed" }
|
format-flowed = { path = "./format-flowed" }
|
||||||
ratelimit = { path = "./deltachat-ratelimit" }
|
ratelimit = { path = "./deltachat-ratelimit" }
|
||||||
|
|
||||||
anyhow = "1"
|
anyhow = { workspace = true }
|
||||||
async-channel = "1.8.0"
|
async-broadcast = "0.7.0"
|
||||||
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
|
async-channel = "2.2.1"
|
||||||
|
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||||
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
|
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
base64 = "0.21"
|
base64 = "0.22"
|
||||||
bitflags = "1.3"
|
brotli = { version = "6", default-features=false, features = ["std"] }
|
||||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
chrono = { workspace = true }
|
||||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||||
escaper = "0.1"
|
escaper = "0.1"
|
||||||
fast-socks5 = "0.8"
|
fast-socks5 = "0.9"
|
||||||
|
fd-lock = "4"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-lite = "1.12.0"
|
futures-lite = "2.3.0"
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
|
hickory-resolver = "0.24"
|
||||||
humansize = "2"
|
humansize = "2"
|
||||||
image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||||
# iroh = { version = "0.3.0", default-features = false }
|
iroh = { version = "0.4.2", default-features = false }
|
||||||
iroh = { git = 'https://github.com/n0-computer/iroh', branch = "flub/ticket-multiple-addrs" }
|
kamadak-exif = "0.5.3"
|
||||||
kamadak-exif = "0.5"
|
|
||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
mailparse = "0.14"
|
mailparse = "0.15"
|
||||||
num_cpus = "1.15"
|
mime = "0.3.17"
|
||||||
num-derive = "0.3"
|
num_cpus = "1.16"
|
||||||
|
num-derive = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
once_cell = "1.17.0"
|
once_cell = { workspace = true }
|
||||||
percent-encoding = "2.2"
|
percent-encoding = "2.3"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
pgp = { version = "0.9", default-features = false }
|
pgp = { version = "0.11", default-features = false }
|
||||||
pretty_env_logger = { version = "0.4", optional = true }
|
pretty_env_logger = { version = "0.5", optional = true }
|
||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
quick-xml = "0.27"
|
quick-xml = "0.31"
|
||||||
|
quoted_printable = "0.5"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1.7"
|
regex = { workspace = true }
|
||||||
reqwest = { version = "0.11.14", features = ["json"] }
|
reqwest = { version = "0.12.2", features = ["json"] }
|
||||||
rusqlite = { version = "0.28", features = ["sqlcipher"] }
|
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||||
rust-hsluv = "0.1"
|
rust-hsluv = "0.1"
|
||||||
sanitize-filename = "0.4"
|
sanitize-filename = "0.5"
|
||||||
serde_json = "1.0"
|
serde_json = "1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha-1 = "0.10"
|
sha-1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
smallvec = "1"
|
smallvec = "1.13.2"
|
||||||
strum = "0.24"
|
strum = "0.26"
|
||||||
strum_macros = "0.24"
|
strum_macros = "0.26"
|
||||||
tagger = "4.3.4"
|
tagger = "4.3.4"
|
||||||
textwrap = "0.16.0"
|
textwrap = "0.16.1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tokio-io-timeout = "1.2.0"
|
tokio-io-timeout = "1.2.0"
|
||||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
tokio-util = "0.7.9"
|
||||||
toml = "0.7"
|
toml = "0.8"
|
||||||
trust-dns-resolver = "0.22"
|
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
|
|
||||||
|
# Pin OpenSSL to 3.1 releases.
|
||||||
|
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
|
||||||
|
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
|
||||||
|
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
|
||||||
|
# According to <https://www.openssl.org/policies/releasestrat.html>
|
||||||
|
# 3.1 branch will be supported until 2025-03-14.
|
||||||
|
openssl-src = "~300.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ansi_term = "0.12.0"
|
ansi_term = "0.12.0"
|
||||||
criterion = { version = "0.4.0", features = ["async_tokio"] }
|
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
futures-lite = "1.12"
|
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||||
|
futures-lite = "2.3.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.5"
|
||||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
testdir = "0.7.2"
|
testdir = "0.9.0"
|
||||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||||
|
pretty_assertions = "1.3.0"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
@@ -113,14 +131,11 @@ members = [
|
|||||||
"deltachat-rpc-server",
|
"deltachat-rpc-server",
|
||||||
"deltachat-ratelimit",
|
"deltachat-ratelimit",
|
||||||
"deltachat-repl",
|
"deltachat-repl",
|
||||||
|
"deltachat-time",
|
||||||
"format-flowed",
|
"format-flowed",
|
||||||
|
"deltachat-contact-tools",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "simple"
|
|
||||||
path = "examples/simple.rs"
|
|
||||||
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "create_account"
|
name = "create_account"
|
||||||
harness = false
|
harness = false
|
||||||
@@ -149,6 +164,13 @@ harness = false
|
|||||||
name = "send_events"
|
name = "send_events"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
regex = "1.10"
|
||||||
|
rusqlite = "0.31"
|
||||||
|
chrono = { version = "0.4.38", default-features=false, features = ["alloc", "clock", "std"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
internals = []
|
internals = []
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice
|
|||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
If it is not possible or desirable to put the notice in a particular
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -1,8 +1,19 @@
|
|||||||
# Delta Chat Rust
|
<p align="center">
|
||||||
|
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
> Deltachat-core written in Rust
|
<p align="center">
|
||||||
|
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||||
|
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||||
|
</a>
|
||||||
|
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
|
||||||
|
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
<p align="center">
|
||||||
|
The core library for Delta Chat, written in Rust
|
||||||
|
</p>
|
||||||
|
|
||||||
## Installing Rust and Cargo
|
## Installing Rust and Cargo
|
||||||
|
|
||||||
@@ -19,7 +30,7 @@ $ curl https://sh.rustup.rs -sSf | sh
|
|||||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ RUST_LOG=deltachat_repl=info cargo run -p deltachat-repl -- ~/deltachat-db
|
$ cargo run -p deltachat-repl -- ~/deltachat-db
|
||||||
```
|
```
|
||||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||||
|
|
||||||
@@ -113,7 +124,7 @@ $ cargo build -p deltachat_ffi --release
|
|||||||
|
|
||||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
||||||
|
|
||||||
- `RUST_LOG=deltachat_repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
- `RUST_LOG=async_imap=trace,async_smtp=trace`: enable IMAP and
|
||||||
SMTP tracing in addition to info messages.
|
SMTP tracing in addition to info messages.
|
||||||
|
|
||||||
### Expensive tests
|
### Expensive tests
|
||||||
@@ -167,8 +178,8 @@ 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**
|
- **Node.js**
|
||||||
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||||
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
- over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
||||||
- **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/)\]
|
||||||
@@ -184,6 +195,7 @@ or its language bindings:
|
|||||||
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||||
|
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
||||||
- several **Bots**
|
- several **Bots**
|
||||||
|
|
||||||
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||||
|
|||||||
24
RELEASE.md
Normal file
24
RELEASE.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Releasing a new version of DeltaChat core
|
||||||
|
|
||||||
|
For example, to release version 1.116.0 of the core, do the following steps.
|
||||||
|
|
||||||
|
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
|
||||||
|
|
||||||
|
2. Run `npm run build:core:constants` in the root of the repository
|
||||||
|
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
|
||||||
|
|
||||||
|
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||||
|
|
||||||
|
4. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||||
|
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
|
||||||
|
|
||||||
|
5. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||||
|
|
||||||
|
6. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||||
|
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||||
|
|
||||||
|
7. Tag the release: `git tag -a v1.116.0`.
|
||||||
|
|
||||||
|
8. Push the release tag: `git push origin v1.116.0`.
|
||||||
|
|
||||||
|
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 121 KiB |
@@ -8,7 +8,8 @@ async fn create_accounts(n: u32) {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let p: PathBuf = dir.path().join("accounts");
|
let p: PathBuf = dir.path().join("accounts");
|
||||||
|
|
||||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
let writable = true;
|
||||||
|
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||||
|
|
||||||
for expected_id in 2..n {
|
for expected_id in 2..n {
|
||||||
let id = accounts.add_account().await.unwrap();
|
let id = accounts.add_account().await.unwrap();
|
||||||
|
|||||||
93
cliff.toml
Normal file
93
cliff.toml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# configuration file for git-cliff
|
||||||
|
# see https://git-cliff.org/docs/configuration/
|
||||||
|
|
||||||
|
|
||||||
|
[git]
|
||||||
|
# parse the commits based on https://www.conventionalcommits.org
|
||||||
|
conventional_commits = true
|
||||||
|
# filter out the commits that are not conventional
|
||||||
|
filter_unconventional = false
|
||||||
|
# process each line of a commit as an individual commit
|
||||||
|
split_commits = false
|
||||||
|
# regex for preprocessing the commit messages
|
||||||
|
commit_preprocessors = [
|
||||||
|
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers
|
||||||
|
]
|
||||||
|
# regex for parsing and grouping commits
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "Features / Changes"},
|
||||||
|
{ message = "^fix", group = "Fixes"},
|
||||||
|
{ message = "^api", group = "API-Changes" },
|
||||||
|
{ message = "^refactor", group = "Refactor"},
|
||||||
|
{ message = "^perf", group = "Performance"},
|
||||||
|
{ message = "^test", group = "Tests"},
|
||||||
|
{ message = "^style", group = "Styling"},
|
||||||
|
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||||
|
{ message = "^chore", group = "Miscellaneous Tasks"},
|
||||||
|
{ message = "^build", group = "Build system"},
|
||||||
|
{ message = "^docs", group = "Documentation"},
|
||||||
|
{ message = "^ci", group = "CI"},
|
||||||
|
{ message = ".*", group = "Other"},
|
||||||
|
# { body = ".*security", group = "Security"},
|
||||||
|
]
|
||||||
|
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||||
|
protect_breaking_commits = true
|
||||||
|
# filter out the commits that are not matched by commit parsers
|
||||||
|
filter_commits = true
|
||||||
|
# glob pattern for matching git tags
|
||||||
|
tag_pattern = "v[0-9]*"
|
||||||
|
# regex for skipping tags
|
||||||
|
#skip_tags = "v0.1.0-beta.1"
|
||||||
|
# regex for ignoring tags
|
||||||
|
ignore_tags = ""
|
||||||
|
# sort the tags topologically
|
||||||
|
topo_order = false
|
||||||
|
# sort the commits inside sections by oldest/newest order
|
||||||
|
sort_commits = "oldest"
|
||||||
|
# limit the number of commits included in the changelog.
|
||||||
|
# limit_commits = 42
|
||||||
|
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
# changelog header
|
||||||
|
header = """
|
||||||
|
# Changelog\n
|
||||||
|
"""
|
||||||
|
# template for the changelog body
|
||||||
|
# https://keats.github.io/tera/docs/#templates
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}\
|
||||||
|
## [unreleased]
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | upper_first }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
||||||
|
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
||||||
|
{{ commit.message | upper_first }}.\
|
||||||
|
{% if commit.footers is defined %}\
|
||||||
|
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||||
|
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||||
|
{% endif %}{% endfor %}\
|
||||||
|
{% endif%}\
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}\n
|
||||||
|
"""
|
||||||
|
# remove the leading and trailing whitespace from the template
|
||||||
|
trim = true
|
||||||
|
footer = """
|
||||||
|
{% for release in releases -%}
|
||||||
|
{% if release.version -%}
|
||||||
|
{% if release.previous.version -%}
|
||||||
|
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||||
|
https://github.com/deltachat/deltachat-core-rust\
|
||||||
|
/compare/{{ release.previous.version }}..{{ release.version }}
|
||||||
|
{% endif -%}
|
||||||
|
{% else -%}
|
||||||
|
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
|
||||||
|
/compare/{{ release.previous.version }}..HEAD
|
||||||
|
{% endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
18
deltachat-contact-tools/Cargo.toml
Normal file
18
deltachat-contact-tools/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "deltachat-contact-tools"
|
||||||
|
version = "0.0.0" # No semver-stable versioning
|
||||||
|
edition = "2021"
|
||||||
|
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
||||||
|
license = "MPL-2.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||||
|
chrono = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
615
deltachat-contact-tools/src/lib.rs
Normal file
615
deltachat-contact-tools/src/lib.rs
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
//! Contact-related tools, like parsing vcards and sanitizing name and address
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![warn(
|
||||||
|
unused,
|
||||||
|
clippy::correctness,
|
||||||
|
missing_debug_implementations,
|
||||||
|
missing_docs,
|
||||||
|
clippy::all,
|
||||||
|
clippy::wildcard_imports,
|
||||||
|
clippy::needless_borrow,
|
||||||
|
clippy::cast_lossless,
|
||||||
|
clippy::unused_async,
|
||||||
|
clippy::explicit_iter_loop,
|
||||||
|
clippy::explicit_into_iter_loop,
|
||||||
|
clippy::cloned_instead_of_copied
|
||||||
|
)]
|
||||||
|
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
|
||||||
|
#![allow(
|
||||||
|
clippy::match_bool,
|
||||||
|
clippy::mixed_read_write_in_expression,
|
||||||
|
clippy::bool_assert_comparison,
|
||||||
|
clippy::manual_split_once,
|
||||||
|
clippy::format_push_string,
|
||||||
|
clippy::bool_to_int_with_if
|
||||||
|
)]
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use anyhow::Context as _;
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{DateTime, NaiveDateTime};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
// TODOs to clean up:
|
||||||
|
// - Check if sanitizing is done correctly everywhere
|
||||||
|
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// A Contact, as represented in a VCard.
|
||||||
|
pub struct VcardContact {
|
||||||
|
/// The email address, vcard property `email`
|
||||||
|
pub addr: String,
|
||||||
|
/// The contact's display name, vcard property `fn`
|
||||||
|
pub display_name: String,
|
||||||
|
/// The contact's public PGP key in Base64, vcard property `key`
|
||||||
|
pub key: Option<String>,
|
||||||
|
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
||||||
|
pub profile_image: Option<String>,
|
||||||
|
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
||||||
|
pub timestamp: Result<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 timestamp: i64 = timestamp.try_into().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 = match c.display_name.is_empty() {
|
||||||
|
false => &c.display_name,
|
||||||
|
true => &c.addr,
|
||||||
|
};
|
||||||
|
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) -> Result<Vec<VcardContact>> {
|
||||||
|
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
||||||
|
let start_of_s = s.get(..prefix.len())?;
|
||||||
|
|
||||||
|
if start_of_s.eq_ignore_ascii_case(prefix) {
|
||||||
|
s.get(prefix.len()..)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
|
||||||
|
let remainder = remove_prefix(s, property)?;
|
||||||
|
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||||
|
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||||
|
|
||||||
|
// TODO this doesn't handle the case where there are quotes around a colon
|
||||||
|
let (params, value) = remainder.split_once(':')?;
|
||||||
|
// In the example from above, `params` is now `;TYPE=work`
|
||||||
|
// and `value` is now `alice@example.com`
|
||||||
|
|
||||||
|
if params
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
// `s` started with `property`, but the next character after it was not punctuation,
|
||||||
|
// so this line's property is actually something else
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
fn parse_datetime(datetime: &str) -> Result<u64> {
|
||||||
|
// 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.try_into()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lines = vcard.lines().peekable();
|
||||||
|
let mut contacts = Vec::new();
|
||||||
|
|
||||||
|
while lines.peek().is_some() {
|
||||||
|
// Skip to the start of the vcard:
|
||||||
|
for line in lines.by_ref() {
|
||||||
|
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut display_name = None;
|
||||||
|
let mut addr = None;
|
||||||
|
let mut key = None;
|
||||||
|
let mut photo = None;
|
||||||
|
let mut datetime = None;
|
||||||
|
|
||||||
|
for line in lines.by_ref() {
|
||||||
|
if let Some(email) = vcard_property(line, "email") {
|
||||||
|
addr.get_or_insert(email);
|
||||||
|
} else if let Some(name) = vcard_property(line, "fn") {
|
||||||
|
display_name.get_or_insert(name);
|
||||||
|
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
|
||||||
|
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
|
||||||
|
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
|
||||||
|
{
|
||||||
|
key.get_or_insert(k);
|
||||||
|
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
|
||||||
|
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
|
||||||
|
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
|
||||||
|
.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 (display_name, addr) =
|
||||||
|
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||||
|
|
||||||
|
contacts.push(VcardContact {
|
||||||
|
display_name,
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(contacts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valid contact address.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ContactAddress(String);
|
||||||
|
|
||||||
|
impl Deref for ContactAddress {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for ContactAddress {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ContactAddress {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContactAddress {
|
||||||
|
/// Constructs a new contact address from string,
|
||||||
|
/// normalizing and validating it.
|
||||||
|
pub fn new(s: &str) -> Result<Self> {
|
||||||
|
let addr = addr_normalize(s);
|
||||||
|
if !may_be_valid_addr(&addr) {
|
||||||
|
bail!("invalid address {:?}", s);
|
||||||
|
}
|
||||||
|
Ok(Self(addr.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||||
|
impl rusqlite::types::ToSql for ContactAddress {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
|
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||||
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make the name and address
|
||||||
|
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||||
|
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||||
|
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||||
|
(
|
||||||
|
if name.is_empty() {
|
||||||
|
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
|
||||||
|
} else {
|
||||||
|
strip_rtlo_characters(name)
|
||||||
|
},
|
||||||
|
captures
|
||||||
|
.get(2)
|
||||||
|
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
strip_rtlo_characters(&normalize_name(name)),
|
||||||
|
addr.to_string(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut name = normalize_name(&name);
|
||||||
|
|
||||||
|
// If the 'display name' is just the address, remove it:
|
||||||
|
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||||
|
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||||
|
if name == addr {
|
||||||
|
name = "".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
(name, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalize a name.
|
||||||
|
///
|
||||||
|
/// - Remove quotes (come from some bad MUA implementations)
|
||||||
|
/// - Trims the resulting string
|
||||||
|
///
|
||||||
|
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||||
|
pub fn normalize_name(full_name: &str) -> String {
|
||||||
|
let full_name = full_name.trim();
|
||||||
|
if full_name.is_empty() {
|
||||||
|
return full_name.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
match full_name.as_bytes() {
|
||||||
|
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||||
|
.get(1..full_name.len() - 1)
|
||||||
|
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||||
|
_ => full_name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
|
||||||
|
/// This method strips all occurrences of the RTLO Unicode character.
|
||||||
|
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
|
||||||
|
pub fn strip_rtlo_characters(input_str: &str) -> String {
|
||||||
|
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns false if addr is an invalid address, otherwise true.
|
||||||
|
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||||
|
let res = EmailAddress::new(addr);
|
||||||
|
res.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns address lowercased,
|
||||||
|
/// with whitespace trimmed and `mailto:` prefix removed.
|
||||||
|
pub fn addr_normalize(addr: &str) -> String {
|
||||||
|
let norm = addr.trim().to_lowercase();
|
||||||
|
|
||||||
|
if norm.starts_with("mailto:") {
|
||||||
|
norm.get(7..).unwrap_or(&norm).to_string()
|
||||||
|
} else {
|
||||||
|
norm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compares two email addresses, normalizing them beforehand.
|
||||||
|
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
||||||
|
let norm1 = addr_normalize(addr1);
|
||||||
|
let norm2 = addr_normalize(addr2);
|
||||||
|
|
||||||
|
norm1 == norm2
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Represents an email address, right now just the `name@domain` portion.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use deltachat_contact_tools::EmailAddress;
|
||||||
|
/// let email = match EmailAddress::new("someone@example.com") {
|
||||||
|
/// Ok(addr) => addr,
|
||||||
|
/// Err(e) => panic!("Error parsing address, error was {}", e),
|
||||||
|
/// };
|
||||||
|
/// assert_eq!(&email.local, "someone");
|
||||||
|
/// assert_eq!(&email.domain, "example.com");
|
||||||
|
/// assert_eq!(email.to_string(), "someone@example.com");
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub struct EmailAddress {
|
||||||
|
/// Local part of the email address.
|
||||||
|
pub local: String,
|
||||||
|
|
||||||
|
/// Email address domain.
|
||||||
|
pub domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for EmailAddress {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}@{}", self.local, self.domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailAddress {
|
||||||
|
/// Performs a dead-simple parse of an email address.
|
||||||
|
pub fn new(input: &str) -> Result<EmailAddress> {
|
||||||
|
if input.is_empty() {
|
||||||
|
bail!("empty string is not valid");
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
||||||
|
|
||||||
|
if input
|
||||||
|
.chars()
|
||||||
|
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||||
|
{
|
||||||
|
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
||||||
|
}
|
||||||
|
|
||||||
|
match &parts[..] {
|
||||||
|
[domain, local] => {
|
||||||
|
if local.is_empty() {
|
||||||
|
bail!("empty string is not valid for local part in {:?}", input);
|
||||||
|
}
|
||||||
|
if domain.is_empty() {
|
||||||
|
bail!("missing domain after '@' in {:?}", input);
|
||||||
|
}
|
||||||
|
if domain.ends_with('.') {
|
||||||
|
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||||
|
}
|
||||||
|
Ok(EmailAddress {
|
||||||
|
local: (*local).to_string(),
|
||||||
|
domain: (*domain).to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => bail!("Email {:?} must contain '@' character", input),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rusqlite::types::ToSql for EmailAddress {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||||
|
let val = rusqlite::types::Value::Text(self.to_string());
|
||||||
|
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::TimeZone;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcard_thunderbird() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
"BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN:'Alice Mueller'
|
||||||
|
EMAIL;PREF=1:alice.mueller@posteo.de
|
||||||
|
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
|
||||||
|
END:VCARD
|
||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:4.0
|
||||||
|
FN:'bobzzz@freenet.de'
|
||||||
|
EMAIL;PREF=1:bobzzz@freenet.de
|
||||||
|
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||||
|
END:VCARD
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
||||||
|
assert_eq!(contacts[0].display_name, "Alice Mueller".to_string());
|
||||||
|
assert_eq!(contacts[0].key, None);
|
||||||
|
assert_eq!(contacts[0].profile_image, None);
|
||||||
|
assert!(contacts[0].timestamp.is_err());
|
||||||
|
|
||||||
|
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
|
||||||
|
assert_eq!(contacts[1].display_name, "".to_string());
|
||||||
|
assert_eq!(contacts[1].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",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||||
|
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||||
|
assert_eq!(contacts[0].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(),
|
||||||
|
display_name: "Alice Wonderland".to_string(),
|
||||||
|
key: Some("[base64-data]".to_string()),
|
||||||
|
profile_image: Some("image in Base64".to_string()),
|
||||||
|
timestamp: Ok(1713465762),
|
||||||
|
},
|
||||||
|
VcardContact {
|
||||||
|
addr: "bob@example.com".to_string(),
|
||||||
|
display_name: "".to_string(),
|
||||||
|
key: None,
|
||||||
|
profile_image: None,
|
||||||
|
timestamp: Ok(0),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for len in 0..=contacts.len() {
|
||||||
|
let contacts = &contacts[0..len];
|
||||||
|
let vcard = make_vcard(contacts);
|
||||||
|
let parsed = parse_vcard(&vcard).unwrap();
|
||||||
|
assert_eq!(parsed.len(), contacts.len());
|
||||||
|
for i in 0..parsed.len() {
|
||||||
|
assert_eq!(parsed[i].addr, contacts[i].addr);
|
||||||
|
assert_eq!(parsed[i].display_name, contacts[i].display_name);
|
||||||
|
assert_eq!(parsed[i].key, contacts[i].key);
|
||||||
|
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
|
||||||
|
assert_eq!(
|
||||||
|
parsed[i].timestamp.as_ref().unwrap(),
|
||||||
|
contacts[i].timestamp.as_ref().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_contact_address() -> Result<()> {
|
||||||
|
let alice_addr = "alice@example.org";
|
||||||
|
let contact_address = ContactAddress::new(alice_addr)?;
|
||||||
|
assert_eq!(contact_address.as_ref(), alice_addr);
|
||||||
|
|
||||||
|
let invalid_addr = "<> foobar";
|
||||||
|
assert!(ContactAddress::new(invalid_addr).is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_emailaddress_parse() {
|
||||||
|
assert_eq!(EmailAddress::new("").is_ok(), false);
|
||||||
|
assert_eq!(
|
||||||
|
EmailAddress::new("user@domain.tld").unwrap(),
|
||||||
|
EmailAddress {
|
||||||
|
local: "user".into(),
|
||||||
|
domain: "domain.tld".into(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
EmailAddress::new("user@localhost").unwrap(),
|
||||||
|
EmailAddress {
|
||||||
|
local: "user".into(),
|
||||||
|
domain: "localhost".into()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
|
||||||
|
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
||||||
|
assert!(EmailAddress::new("tt.dd@uu").is_ok());
|
||||||
|
assert!(EmailAddress::new("u@d").is_ok());
|
||||||
|
assert!(EmailAddress::new("u@d.").is_err());
|
||||||
|
assert!(EmailAddress::new("u@d.t").is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
EmailAddress::new("u@d.tt").unwrap(),
|
||||||
|
EmailAddress {
|
||||||
|
local: "u".into(),
|
||||||
|
domain: "d.tt".into(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(EmailAddress::new("u@tt").is_ok());
|
||||||
|
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcard_android() {
|
||||||
|
let contacts = parse_vcard(
|
||||||
|
"BEGIN:VCARD
|
||||||
|
VERSION:2.1
|
||||||
|
N:;Bob;;;
|
||||||
|
FN:Bob
|
||||||
|
TEL;CELL:+1-234-567-890
|
||||||
|
EMAIL;HOME:bob@example.org
|
||||||
|
END:VCARD
|
||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:2.1
|
||||||
|
N:;Alice;;;
|
||||||
|
FN:Alice
|
||||||
|
EMAIL;HOME:alice@example.org
|
||||||
|
END:VCARD
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||||
|
assert_eq!(contacts[0].display_name, "Bob".to_string());
|
||||||
|
assert_eq!(contacts[0].key, None);
|
||||||
|
assert_eq!(contacts[0].profile_image, None);
|
||||||
|
|
||||||
|
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
||||||
|
assert_eq!(contacts[1].display_name, "Alice".to_string());
|
||||||
|
assert_eq!(contacts[1].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",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(contacts.len(), 1);
|
||||||
|
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
|
||||||
|
assert_eq!(contacts[0].display_name, "Alice Wonderland".to_string());
|
||||||
|
assert_eq!(
|
||||||
|
*contacts[0].timestamp.as_ref().unwrap(),
|
||||||
|
chrono::offset::Local
|
||||||
|
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
|
||||||
|
.unwrap()
|
||||||
|
.timestamp()
|
||||||
|
.try_into()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.111.0"
|
version = "1.138.2"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -17,14 +17,15 @@ crate-type = ["cdylib", "staticlib"]
|
|||||||
deltachat = { path = "../", default-features = false }
|
deltachat = { path = "../", default-features = false }
|
||||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
human-panic = { version = "1", default-features = false }
|
human-panic = { version = "2", default-features = false }
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
rand = "0.7"
|
rand = "0.8"
|
||||||
once_cell = "1.17.0"
|
once_cell = "1.18.0"
|
||||||
|
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
|
|||||||
# exclude all test directories use the pattern */test/*
|
# exclude all test directories use the pattern */test/*
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
|
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t
|
||||||
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
|
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
|
||||||
EXCLUDE_SYMBOLS += _dc_* jsmn*
|
EXCLUDE_SYMBOLS += _dc_* jsmn*
|
||||||
######################################################
|
######################################################
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<tab type="hierarchy" visible="no" title="" intro=""/>
|
<tab type="hierarchy" visible="no" title="" intro=""/>
|
||||||
<tab type="classmembers" visible="no" title="" intro=""/>
|
<tab type="classmembers" visible="no" title="" intro=""/>
|
||||||
</tab>
|
</tab>
|
||||||
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||||
<tab type="pages" visible="yes" title="" intro=""/>
|
<tab type="pages" visible="yes" title="" intro=""/>
|
||||||
<tab type="namespaces" visible="yes" title="">
|
<tab type="namespaces" visible="yes" title="">
|
||||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
deltachat-jsonrpc/.gitignore
vendored
1
deltachat-jsonrpc/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
openrpc/openrpc.json
|
||||||
accounts/
|
accounts/
|
||||||
|
|
||||||
.cargo
|
.cargo
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "1.111.0"
|
version = "1.138.2"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "deltachat-jsonrpc-server"
|
default-run = "deltachat-jsonrpc-server"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "deltachat-jsonrpc-server"
|
name = "deltachat-jsonrpc-server"
|
||||||
@@ -15,25 +16,26 @@ required-features = ["webserver"]
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
deltachat = { path = ".." }
|
deltachat = { path = ".." }
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
|
schemars = "0.8.19"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.10.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
async-channel = { version = "1.8.0" }
|
async-channel = { version = "2.2.1" }
|
||||||
futures = { version = "0.3.26" }
|
futures = { version = "0.3.30" }
|
||||||
serde_json = "1.0.91"
|
serde_json = "1"
|
||||||
yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
|
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||||
tokio = { version = "1.25.0" }
|
tokio = { version = "1.37.0" }
|
||||||
sanitize-filename = "0.4"
|
sanitize-filename = "0.5"
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.5.0"
|
||||||
base64 = "0.21"
|
base64 = "0.22"
|
||||||
|
|
||||||
# optional dependencies
|
# optional dependencies
|
||||||
axum = { version = "0.6.11", optional = true, features = ["ws"] }
|
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||||
env_logger = { version = "0.10.0", optional = true }
|
env_logger = { version = "0.11.3", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.25.0", features = ["full", "rt-multi-thread"] }
|
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ This will build the `deltachat-jsonrpc-server` binary and then run a test suite
|
|||||||
|
|
||||||
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
|
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
|
||||||
|
|
||||||
Then, set the `DCC_NEW_TMP_EMAIL` environment variable to your mailadm token before running the tests.
|
Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
|
||||||
|
|
||||||
```
|
```
|
||||||
DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=yourtoken npm run test
|
CHATMAIL_DOMAIN=chat.example.org npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Test Coverage
|
#### Test Coverage
|
||||||
|
|||||||
@@ -1,54 +1,56 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use std::{collections::HashMap, str::FromStr};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||||
pub use deltachat::accounts::Accounts;
|
pub use deltachat::accounts::Accounts;
|
||||||
use deltachat::qr::Qr;
|
use deltachat::chat::{
|
||||||
use deltachat::{
|
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||||
chat::{
|
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
ProtectionStatus,
|
||||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
|
||||||
ProtectionStatus,
|
|
||||||
},
|
|
||||||
chatlist::Chatlist,
|
|
||||||
config::Config,
|
|
||||||
constants::DC_MSG_ID_DAYMARKER,
|
|
||||||
contact::{may_be_valid_addr, Contact, ContactId, Origin},
|
|
||||||
context::get_info,
|
|
||||||
ephemeral::Timer,
|
|
||||||
imex, location,
|
|
||||||
message::{
|
|
||||||
self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
|
||||||
},
|
|
||||||
provider::get_provider_info,
|
|
||||||
qr,
|
|
||||||
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
|
|
||||||
reaction::send_reaction,
|
|
||||||
securejoin,
|
|
||||||
stock_str::StockMessage,
|
|
||||||
webxdc::StatusUpdateSerial,
|
|
||||||
};
|
};
|
||||||
|
use deltachat::chatlist::Chatlist;
|
||||||
|
use deltachat::config::Config;
|
||||||
|
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||||
|
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||||
|
use deltachat::context::get_info;
|
||||||
|
use deltachat::ephemeral::Timer;
|
||||||
|
use deltachat::imex;
|
||||||
|
use deltachat::location;
|
||||||
|
use deltachat::message::get_msg_read_receipts;
|
||||||
|
use deltachat::message::{
|
||||||
|
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||||
|
};
|
||||||
|
use deltachat::provider::get_provider_info;
|
||||||
|
use deltachat::qr::{self, Qr};
|
||||||
|
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||||
|
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||||
|
use deltachat::securejoin;
|
||||||
|
use deltachat::stock_str::StockMessage;
|
||||||
|
use deltachat::webxdc::StatusUpdateSerial;
|
||||||
|
use deltachat::EventEmitter;
|
||||||
use 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 walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use yerpc::rpc;
|
use yerpc::rpc;
|
||||||
|
|
||||||
pub mod events;
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use types::account::Account;
|
use types::account::Account;
|
||||||
use types::chat::FullChat;
|
use types::chat::FullChat;
|
||||||
use types::chat_list::ChatListEntry;
|
|
||||||
use types::contact::ContactObject;
|
use types::contact::ContactObject;
|
||||||
use types::message::MessageData;
|
use types::events::Event;
|
||||||
use types::message::MessageObject;
|
use types::http::HttpResponse;
|
||||||
|
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||||
use types::provider_info::ProviderInfo;
|
use types::provider_info::ProviderInfo;
|
||||||
|
use types::reactions::JSONRPCReactions;
|
||||||
use types::webxdc::WebxdcMessageInfo;
|
use types::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
use self::types::message::MessageLoadResult;
|
use self::types::message::{MessageInfo, MessageLoadResult};
|
||||||
use self::types::{
|
use self::types::{
|
||||||
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
||||||
location::JsonrpcLocation,
|
location::JsonrpcLocation,
|
||||||
@@ -63,14 +65,14 @@ use crate::api::types::qr::QrObject;
|
|||||||
struct AccountState {
|
struct AccountState {
|
||||||
/// The Qr code for current [`CommandApi::provide_backup`] call.
|
/// The Qr code for current [`CommandApi::provide_backup`] call.
|
||||||
///
|
///
|
||||||
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
|
/// If there is currently is a call to [`CommandApi::provide_backup`] this will be
|
||||||
/// `Pending` or `Ready`, otherwise `NoProvider`.
|
/// `Some`, otherwise `None`.
|
||||||
backup_provider_qr: watch::Sender<ProviderQr>,
|
backup_provider_qr: watch::Sender<Option<Qr>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AccountState {
|
impl Default for AccountState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
|
let tx = watch::Sender::new(None);
|
||||||
Self {
|
Self {
|
||||||
backup_provider_qr: tx,
|
backup_provider_qr: tx,
|
||||||
}
|
}
|
||||||
@@ -81,21 +83,30 @@ impl Default for AccountState {
|
|||||||
pub struct CommandApi {
|
pub struct CommandApi {
|
||||||
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
pub(crate) accounts: Arc<RwLock<Accounts>>,
|
||||||
|
|
||||||
|
/// Receiver side of the event channel.
|
||||||
|
///
|
||||||
|
/// Events from it can be received by calling `get_next_event` method.
|
||||||
|
event_emitter: Arc<EventEmitter>,
|
||||||
|
|
||||||
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandApi {
|
impl CommandApi {
|
||||||
pub fn new(accounts: Accounts) -> Self {
|
pub fn new(accounts: Accounts) -> Self {
|
||||||
|
let event_emitter = Arc::new(accounts.get_event_emitter());
|
||||||
CommandApi {
|
CommandApi {
|
||||||
accounts: Arc::new(RwLock::new(accounts)),
|
accounts: Arc::new(RwLock::new(accounts)),
|
||||||
|
event_emitter,
|
||||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
pub async fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
|
||||||
|
let event_emitter = Arc::new(accounts.read().await.get_event_emitter());
|
||||||
CommandApi {
|
CommandApi {
|
||||||
accounts,
|
accounts,
|
||||||
|
event_emitter,
|
||||||
states: Arc::new(Mutex::new(BTreeMap::new())),
|
states: Arc::new(Mutex::new(BTreeMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,21 +135,13 @@ impl CommandApi {
|
|||||||
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
|
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let val: ProviderQr = receiver.borrow_and_update().clone();
|
loop {
|
||||||
match val {
|
if let Some(qr) = receiver.borrow_and_update().clone() {
|
||||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
return Ok(qr);
|
||||||
ProviderQr::Pending => loop {
|
}
|
||||||
if receiver.changed().await.is_err() {
|
if receiver.changed().await.is_err() {
|
||||||
bail!("No backup being provided (account state dropped)");
|
bail!("No backup being provided (account state dropped)");
|
||||||
}
|
}
|
||||||
let val: ProviderQr = receiver.borrow().clone();
|
|
||||||
match val {
|
|
||||||
ProviderQr::NoProvider => bail!("No backup being provided"),
|
|
||||||
ProviderQr::Pending => continue,
|
|
||||||
ProviderQr::Ready(qr) => break Ok(qr),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
ProviderQr::Ready(qr) => Ok(qr),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,16 +157,25 @@ impl CommandApi {
|
|||||||
// Misc top level functions
|
// Misc top level functions
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
/// Check if an email address is valid.
|
/// Checks if an email address is valid.
|
||||||
async fn check_email_validity(&self, email: String) -> bool {
|
async fn check_email_validity(&self, email: String) -> bool {
|
||||||
may_be_valid_addr(&email)
|
may_be_valid_addr(&email)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get general system info.
|
/// Returns general system info.
|
||||||
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
|
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
|
||||||
get_info()
|
get_info()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the next event.
|
||||||
|
async fn get_next_event(&self) -> Result<Event> {
|
||||||
|
self.event_emitter
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map(|event| event.into())
|
||||||
|
.context("event channel is closed")
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// Account Management
|
// Account Management
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
@@ -205,20 +217,34 @@ impl CommandApi {
|
|||||||
let context_option = self.accounts.read().await.get_account(id);
|
let context_option = self.accounts.read().await.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?)
|
||||||
} else {
|
|
||||||
println!("account with id {id} doesn't exist anymore");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(accounts)
|
Ok(accounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts background tasks for all accounts.
|
||||||
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
||||||
self.accounts.read().await.start_io().await;
|
self.accounts.write().await.start_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stops background tasks for all accounts.
|
||||||
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
||||||
self.accounts.read().await.stop_io().await;
|
self.accounts.write().await.stop_io().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||||
|
///
|
||||||
|
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
|
||||||
|
/// Process all events until you get this one and you can safely return to the background
|
||||||
|
/// without forgetting to create notifications caused by timing race conditions.
|
||||||
|
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||||
|
self.accounts
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
|
||||||
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,14 +252,16 @@ impl CommandApi {
|
|||||||
// Methods that work on individual accounts
|
// Methods that work on individual accounts
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
async fn start_io(&self, id: u32) -> Result<()> {
|
/// Starts background tasks for a single account.
|
||||||
let ctx = self.get_context(id).await?;
|
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.start_io().await;
|
ctx.start_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_io(&self, id: u32) -> Result<()> {
|
/// Stops background tasks for a single account.
|
||||||
let ctx = self.get_context(id).await?;
|
async fn stop_io(&self, account_id: u32) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.stop_io().await;
|
ctx.stop_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -300,11 +328,18 @@ impl CommandApi {
|
|||||||
ctx.get_info().await
|
ctx.get_info().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
Ok(ctx.draft_self_report().await?.to_u32())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the given configuration key.
|
||||||
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
set_config(&ctx, &key, value.as_deref()).await
|
set_config(&ctx, &key, value.as_deref()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates a batch of configuration values.
|
||||||
async fn batch_set_config(
|
async fn batch_set_config(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -336,6 +371,7 @@ impl CommandApi {
|
|||||||
Ok(qr_object)
|
Ok(qr_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns configuration value for the given key.
|
||||||
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
get_config(&ctx, &key).await
|
get_config(&ctx, &key).await
|
||||||
@@ -453,6 +489,49 @@ impl CommandApi {
|
|||||||
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
|
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets messages to be processed by the bot and returns their IDs.
|
||||||
|
///
|
||||||
|
/// Only messages with database ID higher than `last_msg_id` config value
|
||||||
|
/// are returned. After processing the messages, the bot should
|
||||||
|
/// update `last_msg_id` by calling [`markseen_msgs`]
|
||||||
|
/// or manually updating the value to avoid getting already
|
||||||
|
/// processed messages.
|
||||||
|
///
|
||||||
|
/// [`markseen_msgs`]: Self::markseen_msgs
|
||||||
|
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let msg_ids = ctx
|
||||||
|
.get_next_msgs()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|msg_id| msg_id.to_u32())
|
||||||
|
.collect();
|
||||||
|
Ok(msg_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for messages to be processed by the bot and returns their IDs.
|
||||||
|
///
|
||||||
|
/// This function is similar to [`get_next_msgs`],
|
||||||
|
/// but waits for internal new message notification before returning.
|
||||||
|
/// New message notification is sent when new message is added to the database,
|
||||||
|
/// on initialization, when I/O is started and when I/O is stopped.
|
||||||
|
/// This allows bots to use `wait_next_msgs` in a loop to process
|
||||||
|
/// old messages after initialization and during the bot runtime.
|
||||||
|
/// To shutdown the bot, stopping I/O can be used to interrupt
|
||||||
|
/// pending or next `wait_next_msgs` call.
|
||||||
|
///
|
||||||
|
/// [`get_next_msgs`]: Self::get_next_msgs
|
||||||
|
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let msg_ids = ctx
|
||||||
|
.wait_next_msgs()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|msg_id| msg_id.to_u32())
|
||||||
|
.collect();
|
||||||
|
Ok(msg_ids)
|
||||||
|
}
|
||||||
|
|
||||||
/// Estimate the number of messages that will be deleted
|
/// Estimate the number of messages that will be deleted
|
||||||
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
|
/// by the set_config()-options `delete_device_after` or `delete_server_after`.
|
||||||
/// This is typically used to show the estimated impact to the user
|
/// This is typically used to show the estimated impact to the user
|
||||||
@@ -496,7 +575,7 @@ impl CommandApi {
|
|||||||
list_flags: Option<u32>,
|
list_flags: Option<u32>,
|
||||||
query_string: Option<String>,
|
query_string: Option<String>,
|
||||||
query_contact_id: Option<u32>,
|
query_contact_id: Option<u32>,
|
||||||
) -> Result<Vec<ChatListEntry>> {
|
) -> Result<Vec<u32>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let list = Chatlist::try_load(
|
let list = Chatlist::try_load(
|
||||||
&ctx,
|
&ctx,
|
||||||
@@ -505,32 +584,43 @@ impl CommandApi {
|
|||||||
query_contact_id.map(ContactId::new),
|
query_contact_id.map(ContactId::new),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
|
let mut l: Vec<u32> = Vec::with_capacity(list.len());
|
||||||
for i in 0..list.len() {
|
for i in 0..list.len() {
|
||||||
l.push(ChatListEntry(
|
l.push(list.get_chat_id(i)?.to_u32());
|
||||||
list.get_chat_id(i)?.to_u32(),
|
|
||||||
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Ok(l)
|
Ok(l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns chats similar to the given one.
|
||||||
|
///
|
||||||
|
/// Experimental API, subject to change without notice.
|
||||||
|
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let chat_id = ChatId::new(chat_id);
|
||||||
|
let list = chat_id
|
||||||
|
.get_similar_chat_ids(&ctx)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(chat_id, _metric)| chat_id.to_u32())
|
||||||
|
.collect();
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_chatlist_items_by_entries(
|
async fn get_chatlist_items_by_entries(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
entries: Vec<ChatListEntry>,
|
entries: Vec<u32>,
|
||||||
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
|
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
|
||||||
// todo custom json deserializer for ChatListEntry?
|
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut result: HashMap<u32, ChatListItemFetchResult> =
|
let mut result: HashMap<u32, ChatListItemFetchResult> =
|
||||||
HashMap::with_capacity(entries.len());
|
HashMap::with_capacity(entries.len());
|
||||||
for entry in entries.iter() {
|
for &entry in entries.iter() {
|
||||||
result.insert(
|
result.insert(
|
||||||
entry.0,
|
entry,
|
||||||
match get_chat_list_item_by_id(&ctx, entry).await {
|
match get_chat_list_item_by_id(&ctx, entry).await {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(err) => ChatListItemFetchResult::Error {
|
Err(err) => ChatListItemFetchResult::Error {
|
||||||
id: entry.0,
|
id: entry,
|
||||||
error: format!("{err:#}"),
|
error: format!("{err:#}"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -610,8 +700,7 @@ impl CommandApi {
|
|||||||
/// the Verified-Group-Invite protocol is offered in the QR code;
|
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||||
/// works for protected groups as well as for normal groups.
|
/// works for protected groups as well as for normal groups.
|
||||||
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
||||||
/// See https://countermitm.readthedocs.io/en/latest/new.html
|
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||||
/// for details about both protocols.
|
|
||||||
///
|
///
|
||||||
/// return format: `[code, svg]`
|
/// return format: `[code, svg]`
|
||||||
async fn get_chat_securejoin_qr_code_svg(
|
async fn get_chat_securejoin_qr_code_svg(
|
||||||
@@ -639,8 +728,7 @@ impl CommandApi {
|
|||||||
///
|
///
|
||||||
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||||
///
|
///
|
||||||
/// See https://countermitm.readthedocs.io/en/latest/new.html
|
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||||
/// for details about both protocols.
|
|
||||||
///
|
///
|
||||||
/// **qr**: The text of the scanned QR code. Typically, the same string as given
|
/// **qr**: The text of the scanned QR code. Typically, the same string as given
|
||||||
/// to `check_qr()`.
|
/// to `check_qr()`.
|
||||||
@@ -747,24 +835,12 @@ impl CommandApi {
|
|||||||
/// Create a new broadcast list.
|
/// Create a new broadcast list.
|
||||||
///
|
///
|
||||||
/// Broadcast lists are similar to groups on the sending device,
|
/// Broadcast lists are similar to groups on the sending device,
|
||||||
/// however, recipients get the messages in normal one-to-one chats
|
/// however, recipients get the messages in a read-only chat
|
||||||
/// and will not be aware of other members.
|
/// and will see who the other members are.
|
||||||
///
|
///
|
||||||
/// Replies to broadcasts go only to the sender
|
/// For historical reasons, this function does not take a name directly,
|
||||||
/// and not to all broadcast recipients.
|
/// instead you have to set the name using dc_set_chat_name()
|
||||||
/// Moreover, replies will not appear in the broadcast list
|
/// after creating the broadcast list.
|
||||||
/// but in the one-to-one chat with the person answering.
|
|
||||||
///
|
|
||||||
/// The name and the image of the broadcast list is set automatically
|
|
||||||
/// and is visible to the sender only.
|
|
||||||
/// Not asking for these data allows more focused creation
|
|
||||||
/// and we bypass the question who will get which data.
|
|
||||||
/// Also, many users will have at most one broadcast list
|
|
||||||
/// so, a generic name and image is sufficient at the first place.
|
|
||||||
///
|
|
||||||
/// Later on, however, the name can be changed using dc_set_chat_name().
|
|
||||||
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
|
||||||
/// All in all, this is also what other messengers are doing here.
|
|
||||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
chat::create_broadcast_list(&ctx)
|
chat::create_broadcast_list(&ctx)
|
||||||
@@ -840,19 +916,35 @@ impl CommandApi {
|
|||||||
.to_u32())
|
.to_u32())
|
||||||
}
|
}
|
||||||
|
|
||||||
// for now only text messages, because we only used text messages in desktop thusfar
|
/// Add a message to the device-chat.
|
||||||
|
/// Device-messages usually contain update information
|
||||||
|
/// and some hints that are added during the program runs, multi-device etc.
|
||||||
|
/// The device-message may be defined by a label;
|
||||||
|
/// if a message with the same label was added or skipped before,
|
||||||
|
/// the message is not added again, even if the message was deleted in between.
|
||||||
|
/// If needed, the device-chat is created before.
|
||||||
|
///
|
||||||
|
/// Sends the `MsgsChanged` event on success.
|
||||||
|
///
|
||||||
|
/// Setting msg to None will prevent the device message with this label from being added in the future.
|
||||||
async fn add_device_message(
|
async fn add_device_message(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
label: String,
|
label: String,
|
||||||
text: String,
|
msg: Option<MessageData>,
|
||||||
) -> Result<u32> {
|
) -> Result<Option<u32>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
if let Some(msg) = msg {
|
||||||
msg.set_text(Some(text));
|
let mut message = msg.create_message(&ctx).await?;
|
||||||
let message_id =
|
let message_id =
|
||||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut message)).await?;
|
||||||
Ok(message_id.to_u32())
|
if !message_id.is_unset() {
|
||||||
|
return Ok(Some(message_id.to_u32()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deltachat::chat::add_device_msg(&ctx, Some(&label), None).await?;
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark all messages in a chat as _noticed_.
|
/// Mark all messages in a chat as _noticed_.
|
||||||
@@ -944,6 +1036,11 @@ impl CommandApi {
|
|||||||
/// Moreover, timer is started for incoming ephemeral messages.
|
/// Moreover, timer is started for incoming ephemeral messages.
|
||||||
/// This also happens for contact requests chats.
|
/// This also happens for contact requests chats.
|
||||||
///
|
///
|
||||||
|
/// This function updates `last_msg_id` configuration value
|
||||||
|
/// to the maximum of the current value and IDs passed to this function.
|
||||||
|
/// Bots which mark messages as seen can rely on this side effect
|
||||||
|
/// to avoid updating `last_msg_id` value manually.
|
||||||
|
///
|
||||||
/// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
/// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||||
async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<()> {
|
async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
@@ -1001,9 +1098,12 @@ impl CommandApi {
|
|||||||
.collect::<Vec<JSONRPCMessageListItem>>())
|
.collect::<Vec<JSONRPCMessageListItem>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
|
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
MessageObject::from_message_id(&ctx, message_id).await
|
let msg_id = MsgId::new(msg_id);
|
||||||
|
MessageObject::from_msg_id(&ctx, msg_id)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
|
||||||
@@ -1023,7 +1123,7 @@ impl CommandApi {
|
|||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
|
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
|
||||||
for message_id in message_ids {
|
for message_id in message_ids {
|
||||||
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
|
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
|
||||||
messages.insert(
|
messages.insert(
|
||||||
message_id,
|
message_id,
|
||||||
match message_result {
|
match message_result {
|
||||||
@@ -1062,7 +1162,35 @@ impl CommandApi {
|
|||||||
/// max. text returned by dc_msg_get_text() (about 30000 characters).
|
/// max. text returned by dc_msg_get_text() (about 30000 characters).
|
||||||
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
|
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
get_msg_info(&ctx, MsgId::new(message_id)).await
|
MsgId::new(message_id).get_info(&ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns additional information for single message.
|
||||||
|
async fn get_message_info_object(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
message_id: u32,
|
||||||
|
) -> Result<MessageInfo> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
MessageInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns contacts that sent read receipts and the time of reading.
|
||||||
|
async fn get_message_read_receipts(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
message_id: u32,
|
||||||
|
) -> Result<Vec<MessageReadReceipt>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let receipts = get_msg_read_receipts(&ctx, MsgId::new(message_id))
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|(contact_id, ts)| MessageReadReceipt {
|
||||||
|
contact_id: contact_id.to_u32(),
|
||||||
|
timestamp: *ts,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(receipts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asks the core to start downloading a message fully.
|
/// Asks the core to start downloading a message fully.
|
||||||
@@ -1082,17 +1210,17 @@ impl CommandApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Search messages containing the given query string.
|
/// Search messages containing the given query string.
|
||||||
/// Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
|
/// Searching can be done globally (chat_id=None) or in a specified chat only (chat_id set).
|
||||||
///
|
///
|
||||||
/// Global chat results are typically displayed using dc_msg_get_summary(), chat
|
/// Global search results are typically displayed using dc_msg_get_summary(), chat
|
||||||
/// search results may just hilite the corresponding messages and present a
|
/// search results may just highlight the corresponding messages and present a
|
||||||
/// prev/next button.
|
/// prev/next button.
|
||||||
///
|
///
|
||||||
/// For global search, result is limited to 1000 messages,
|
/// For the global search, the result is limited to 1000 messages,
|
||||||
/// this allows incremental search done fast.
|
/// this allows an incremental search done fast.
|
||||||
/// So, when getting exactly 1000 results, the result may be truncated;
|
/// So, when getting exactly 1000 messages, the result actually may be truncated;
|
||||||
/// the UIs may display sth. as "1000+ messages found" in this case.
|
/// the UIs may display sth. like "1000+ messages found" in this case.
|
||||||
/// Chat search (if a chat_id is set) is not limited.
|
/// The chat search (if chat_id is set) is not limited.
|
||||||
async fn search_messages(
|
async fn search_messages(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1265,7 +1393,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::load_from_db(&ctx, contact_id).await?;
|
let contact = Contact::get_by_id(&ctx, contact_id).await?;
|
||||||
let addr = contact.get_addr();
|
let addr = contact.get_addr();
|
||||||
Contact::create(&ctx, &name, addr).await?;
|
Contact::create(&ctx, &name, addr).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1302,6 +1430,19 @@ impl CommandApi {
|
|||||||
// chat
|
// chat
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
|
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||||
|
///
|
||||||
|
/// If it does not exist, `None` is returned.
|
||||||
|
async fn get_chat_id_by_contact_id(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
contact_id: u32,
|
||||||
|
) -> Result<Option<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let chat_id = ChatId::lookup_by_contact(&ctx, ContactId::new(contact_id)).await?;
|
||||||
|
Ok(chat_id.map(|id| id.to_u32()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns all message IDs of the given types in a chat.
|
/// Returns all message IDs of the given types in a chat.
|
||||||
/// Typically used to show a gallery.
|
/// Typically used to show a gallery.
|
||||||
///
|
///
|
||||||
@@ -1339,6 +1480,10 @@ impl CommandApi {
|
|||||||
///
|
///
|
||||||
/// one combined call for getting chat::get_next_media for both directions
|
/// one combined call for getting chat::get_next_media for both directions
|
||||||
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||||
|
///
|
||||||
|
/// Deprecated 2023-10-03, use `get_chat_media` method
|
||||||
|
/// and navigate the returned array instead.
|
||||||
|
#[allow(deprecated)]
|
||||||
async fn get_neighboring_chat_media(
|
async fn get_neighboring_chat_media(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1425,20 +1570,21 @@ impl CommandApi {
|
|||||||
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
/// Returns once a remote device has retrieved the backup, or is cancelled.
|
||||||
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
async fn provide_backup(&self, account_id: u32) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
self.with_state(account_id, |state| {
|
|
||||||
state.backup_provider_qr.send_replace(ProviderQr::Pending);
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let provider = imex::BackupProvider::prepare(&ctx).await?;
|
let provider = imex::BackupProvider::prepare(&ctx).await?;
|
||||||
self.with_state(account_id, |state| {
|
self.with_state(account_id, |state| {
|
||||||
state
|
state.backup_provider_qr.send_replace(Some(provider.qr()));
|
||||||
.backup_provider_qr
|
|
||||||
.send_replace(ProviderQr::Ready(provider.qr()));
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
provider.await
|
let res = provider.await;
|
||||||
|
|
||||||
|
self.with_state(account_id, |state| {
|
||||||
|
state.backup_provider_qr.send_replace(None);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
|
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
|
||||||
@@ -1446,11 +1592,17 @@ impl CommandApi {
|
|||||||
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
|
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
|
||||||
/// retrieve the backup and setup this second device.
|
/// retrieve the backup and setup this second device.
|
||||||
///
|
///
|
||||||
/// This call will fail if there is currently no concurrent call to
|
/// This call will block until the QR code is ready,
|
||||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||||
/// ready.
|
/// but will fail after 10 seconds to avoid deadlocks.
|
||||||
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
|
||||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
let qr = tokio::time::timeout(
|
||||||
|
Duration::from_secs(10),
|
||||||
|
self.inner_get_backup_qr(account_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Backup provider did not start in time")?
|
||||||
|
.context("Failed to get backup QR code")?;
|
||||||
qr::format_backup(&qr)
|
qr::format_backup(&qr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1459,14 +1611,20 @@ impl CommandApi {
|
|||||||
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
|
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
|
||||||
/// retrieve the backup and setup this second device.
|
/// retrieve the backup and setup this second device.
|
||||||
///
|
///
|
||||||
/// This call will fail if there is currently no concurrent call to
|
/// This call will block until the QR code is ready,
|
||||||
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
|
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
|
||||||
/// ready.
|
/// but will fail after 10 seconds to avoid deadlocks.
|
||||||
///
|
///
|
||||||
/// Returns the QR code rendered as an SVG image.
|
/// Returns the QR code rendered as an SVG image.
|
||||||
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let qr = self.inner_get_backup_qr(account_id).await?;
|
let qr = tokio::time::timeout(
|
||||||
|
Duration::from_secs(10),
|
||||||
|
self.inner_get_backup_qr(account_id),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Backup provider did not start in time")?
|
||||||
|
.context("Failed to get backup QR code")?;
|
||||||
generate_backup_qr(&ctx, &qr).await
|
generate_backup_qr(&ctx, &qr).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1476,6 +1634,9 @@ impl CommandApi {
|
|||||||
/// the current device.
|
/// the current device.
|
||||||
///
|
///
|
||||||
/// Can be cancelled by stopping the ongoing process.
|
/// Can be cancelled by stopping the ongoing process.
|
||||||
|
///
|
||||||
|
/// Do not forget to call start_io on the account after a successful import,
|
||||||
|
/// otherwise it will not connect to the email server.
|
||||||
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
|
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let qr = qr::check_qr(&ctx, &qr_text).await?;
|
let qr = qr::check_qr(&ctx, &qr_text).await?;
|
||||||
@@ -1610,6 +1771,38 @@ impl CommandApi {
|
|||||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets Webxdc file as integration.
|
||||||
|
/// `file` is the .xdc to use as Webxdc integration.
|
||||||
|
async fn set_webxdc_integration(&self, account_id: u32, file_path: String) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
ctx.set_webxdc_integration(&file_path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns Webxdc instance used for optional integrations.
|
||||||
|
/// UI can open the Webxdc as usual.
|
||||||
|
/// Returns `None` if there is no integration; the caller can add one using `set_webxdc_integration` then.
|
||||||
|
/// `integrate_for` is the chat to get the integration for.
|
||||||
|
async fn init_webxdc_integration(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
chat_id: Option<u32>,
|
||||||
|
) -> Result<Option<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
Ok(ctx
|
||||||
|
.init_webxdc_integration(chat_id.map(ChatId::new))
|
||||||
|
.await?
|
||||||
|
.map(|msg_id| msg_id.to_u32()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes an HTTP GET request and returns a response.
|
||||||
|
///
|
||||||
|
/// `url` is the HTTP or HTTPS URL.
|
||||||
|
async fn get_http_response(&self, account_id: u32, url: String) -> Result<HttpResponse> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let response = deltachat::net::read_url_blob(&ctx, &url).await?.into();
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
/// Forward messages to another chat.
|
/// Forward messages to another chat.
|
||||||
///
|
///
|
||||||
/// All types of messages can be forwarded,
|
/// All types of messages can be forwarded,
|
||||||
@@ -1627,6 +1820,20 @@ impl CommandApi {
|
|||||||
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resend messages and make information available for newly added chat members.
|
||||||
|
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||||
|
/// Clients that already have the original message can still ignore the resent message as
|
||||||
|
/// they have tracked the state by dedicated updates.
|
||||||
|
///
|
||||||
|
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
|
||||||
|
///
|
||||||
|
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
|
||||||
|
async fn resend_messages(&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::resend_msgs(&ctx, &message_ids).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_sticker(
|
async fn send_sticker(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1638,6 +1845,9 @@ impl CommandApi {
|
|||||||
let mut msg = Message::new(Viewtype::Sticker);
|
let mut msg = Message::new(Viewtype::Sticker);
|
||||||
msg.set_file(&sticker_path, None);
|
msg.set_file(&sticker_path, None);
|
||||||
|
|
||||||
|
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
||||||
|
msg.force_sticker();
|
||||||
|
|
||||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
}
|
}
|
||||||
@@ -1659,48 +1869,48 @@ impl CommandApi {
|
|||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns reactions to the message.
|
||||||
|
async fn get_message_reactions(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
message_id: u32,
|
||||||
|
) -> Result<Option<JSONRPCReactions>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
|
||||||
|
if reactions.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some(reactions.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
|
let mut message = data.create_message(&ctx).await?;
|
||||||
viewtype.into()
|
|
||||||
} else if data.file.is_some() {
|
|
||||||
Viewtype::File
|
|
||||||
} else {
|
|
||||||
Viewtype::Text
|
|
||||||
});
|
|
||||||
if data.text.is_some() {
|
|
||||||
message.set_text(data.text);
|
|
||||||
}
|
|
||||||
if data.html.is_some() {
|
|
||||||
message.set_html(data.html);
|
|
||||||
}
|
|
||||||
if data.override_sender_name.is_some() {
|
|
||||||
message.set_override_sender_name(data.override_sender_name);
|
|
||||||
}
|
|
||||||
if let Some(file) = data.file {
|
|
||||||
message.set_file(file, None);
|
|
||||||
}
|
|
||||||
if let Some((latitude, longitude)) = data.location {
|
|
||||||
message.set_location(latitude, longitude);
|
|
||||||
}
|
|
||||||
if let Some(id) = data.quoted_message_id {
|
|
||||||
message
|
|
||||||
.set_quote(
|
|
||||||
&ctx,
|
|
||||||
Some(
|
|
||||||
&Message::load_from_db(&ctx, MsgId::new(id))
|
|
||||||
.await
|
|
||||||
.context("message to quote could not be loaded")?,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
||||||
.await?
|
.await?
|
||||||
.to_u32();
|
.to_u32();
|
||||||
Ok(msg_id)
|
Ok(msg_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if messages can be sent to a given chat.
|
||||||
|
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let chat_id = ChatId::new(chat_id);
|
||||||
|
let chat = Chat::load_from_db(&ctx, chat_id).await?;
|
||||||
|
let can_send = chat.can_send(&ctx).await?;
|
||||||
|
Ok(can_send)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves a file copy at the user-provided path.
|
||||||
|
///
|
||||||
|
/// Fails if file already exists at the provided path.
|
||||||
|
async fn save_msg_file(&self, account_id: u32, msg_id: u32, path: String) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||||
|
message.save_file(&ctx, Path::new(&path)).await
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// functions for the composer
|
// functions for the composer
|
||||||
// the composer is the message input field
|
// the composer is the message input field
|
||||||
@@ -1749,7 +1959,7 @@ impl CommandApi {
|
|||||||
.context("path conversion to string failed")
|
.context("path conversion to string failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// save a sticker to a collection/folder in the account's sticker folder
|
/// Saves a sticker to a collection/folder in the account's sticker folder.
|
||||||
async fn misc_save_sticker(
|
async fn misc_save_sticker(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1773,19 +1983,21 @@ impl CommandApi {
|
|||||||
);
|
);
|
||||||
let destination_path = account_folder.join("stickers").join(collection);
|
let destination_path = account_folder.join("stickers").join(collection);
|
||||||
fs::create_dir_all(&destination_path).await?;
|
fs::create_dir_all(&destination_path).await?;
|
||||||
let file = message.get_file(&ctx).context("no file")?;
|
let file = message.get_filename().context("no file?")?;
|
||||||
fs::copy(
|
message
|
||||||
&file,
|
.save_file(
|
||||||
destination_path.join(format!(
|
&ctx,
|
||||||
"{}.{}",
|
&destination_path.join(format!(
|
||||||
msg_id,
|
"{}.{}",
|
||||||
file.extension()
|
msg_id,
|
||||||
.unwrap_or_default()
|
Path::new(&file)
|
||||||
.to_str()
|
.extension()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
)),
|
.to_str()
|
||||||
)
|
.unwrap_or_default()
|
||||||
.await?;
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1842,7 +2054,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::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_text(Some(text));
|
msg.set_text(text);
|
||||||
|
|
||||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
@@ -1865,9 +2077,7 @@ impl CommandApi {
|
|||||||
} else {
|
} else {
|
||||||
Viewtype::Text
|
Viewtype::Text
|
||||||
});
|
});
|
||||||
if text.is_some() {
|
message.set_text(text.unwrap_or_default());
|
||||||
message.set_text(text);
|
|
||||||
}
|
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
message.set_file(file, None);
|
message.set_file(file, None);
|
||||||
}
|
}
|
||||||
@@ -1886,11 +2096,9 @@ impl CommandApi {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
|
||||||
.await?
|
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
|
||||||
.to_u32();
|
Ok((msg_id.to_u32(), message))
|
||||||
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
|
|
||||||
Ok((msg_id, message))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
|
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
|
||||||
@@ -1904,16 +2112,20 @@ impl CommandApi {
|
|||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
quoted_message_id: Option<u32>,
|
quoted_message_id: Option<u32>,
|
||||||
|
view_type: Option<MessageViewtype>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut draft = Message::new(if file.is_some() {
|
let mut draft = Message::new(view_type.map_or_else(
|
||||||
Viewtype::File
|
|| {
|
||||||
} else {
|
if file.is_some() {
|
||||||
Viewtype::Text
|
Viewtype::File
|
||||||
});
|
} else {
|
||||||
if text.is_some() {
|
Viewtype::Text
|
||||||
draft.set_text(text);
|
}
|
||||||
}
|
},
|
||||||
|
|v| v.into(),
|
||||||
|
));
|
||||||
|
draft.set_text(text.unwrap_or_default());
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
draft.set_file(file, None);
|
draft.set_file(file, None);
|
||||||
}
|
}
|
||||||
@@ -1932,6 +2144,23 @@ impl CommandApi {
|
|||||||
|
|
||||||
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send the chat's current set draft
|
||||||
|
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
|
||||||
|
let mut draft = draft;
|
||||||
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
|
||||||
|
.await?
|
||||||
|
.to_u32();
|
||||||
|
Ok(msg_id)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"chat with id {} doesn't have draft message",
|
||||||
|
chat_id
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions (to prevent code duplication)
|
// Helper functions (to prevent code duplication)
|
||||||
@@ -1948,13 +2177,6 @@ async fn set_config(
|
|||||||
value,
|
value,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match key {
|
|
||||||
"sentbox_watch" | "mvbox_move" | "only_fetch_mvbox" => {
|
|
||||||
ctx.restart_io_if_running().await;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1970,15 +2192,3 @@ async fn get_config(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a QR code for a BackupProvider is currently available.
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
enum ProviderQr {
|
|
||||||
/// There is no provider, asking for a QR is an error.
|
|
||||||
NoProvider,
|
|
||||||
/// There is a provider, the QR code is pending.
|
|
||||||
Pending,
|
|
||||||
/// There is a provider and QR code.
|
|
||||||
Ready(Qr),
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,8 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "kind")]
|
||||||
pub enum Account {
|
pub enum Account {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Configured {
|
Configured {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{bail, Context as _, Result};
|
||||||
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
||||||
use deltachat::chat::{Chat, ChatId};
|
use deltachat::chat::{Chat, ChatId};
|
||||||
use deltachat::constants::Chattype;
|
use deltachat::constants::Chattype;
|
||||||
@@ -13,11 +13,22 @@ 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;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FullChat {
|
pub struct FullChat {
|
||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
|
/// True if the chat is protected.
|
||||||
|
///
|
||||||
|
/// UI should display a green checkmark
|
||||||
|
/// in the chat title,
|
||||||
|
/// in the chat profile title and
|
||||||
|
/// in the chatlist item
|
||||||
|
/// if chat protection is enabled.
|
||||||
|
/// UI should also display a green checkmark
|
||||||
|
/// in the contact profile
|
||||||
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
@@ -31,6 +42,7 @@ pub struct FullChat {
|
|||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
// is_group - please check over chat.type in frontend instead
|
// is_group - please check over chat.type in frontend instead
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
is_protection_broken: bool,
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
self_in_group: bool,
|
self_in_group: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
@@ -53,7 +65,9 @@ impl FullChat {
|
|||||||
contacts.push(
|
contacts.push(
|
||||||
ContactObject::try_from_dc_contact(
|
ContactObject::try_from_dc_contact(
|
||||||
context,
|
context,
|
||||||
Contact::load_from_db(context, *contact_id).await?,
|
Contact::get_by_id(context, *contact_id)
|
||||||
|
.await
|
||||||
|
.context("failed to load contact")?,
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
@@ -71,9 +85,10 @@ impl FullChat {
|
|||||||
let can_send = chat.can_send(context).await?;
|
let can_send = chat.can_send(context).await?;
|
||||||
|
|
||||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||||
match contact_ids.get(0) {
|
match contact_ids.first() {
|
||||||
Some(contact) => Contact::load_from_db(context, *contact)
|
Some(contact) => Contact::get_by_id(context, *contact)
|
||||||
.await?
|
.await
|
||||||
|
.context("failed to load contact for was_seen_recently")?
|
||||||
.was_seen_recently(),
|
.was_seen_recently(),
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
@@ -89,10 +104,7 @@ impl FullChat {
|
|||||||
is_protected: chat.is_protected(),
|
is_protected: chat.is_protected(),
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
chat_type: chat
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
.get_type()
|
|
||||||
.to_u32()
|
|
||||||
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
|
|
||||||
is_unpromoted: chat.is_unpromoted(),
|
is_unpromoted: chat.is_unpromoted(),
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
contacts,
|
contacts,
|
||||||
@@ -100,6 +112,7 @@ impl FullChat {
|
|||||||
color,
|
color,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_protection_broken: chat.is_protection_broken(),
|
||||||
is_device_chat: chat.is_device_talk(),
|
is_device_chat: chat.is_device_talk(),
|
||||||
self_in_group: contact_ids.contains(&ContactId::SELF),
|
self_in_group: contact_ids.contains(&ContactId::SELF),
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
@@ -121,11 +134,22 @@ impl FullChat {
|
|||||||
/// - can_send
|
/// - can_send
|
||||||
///
|
///
|
||||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BasicChat {
|
pub struct BasicChat {
|
||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
|
/// True if the chat is protected.
|
||||||
|
///
|
||||||
|
/// UI should display a green checkmark
|
||||||
|
/// in the chat title,
|
||||||
|
/// in the chat profile title and
|
||||||
|
/// in the chatlist item
|
||||||
|
/// if chat protection is enabled.
|
||||||
|
/// UI should also display a green checkmark
|
||||||
|
/// in the contact profile
|
||||||
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
@@ -134,6 +158,7 @@ pub struct BasicChat {
|
|||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
color: String,
|
color: String,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
is_protection_broken: bool,
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
}
|
}
|
||||||
@@ -155,25 +180,24 @@ impl BasicChat {
|
|||||||
is_protected: chat.is_protected(),
|
is_protected: chat.is_protected(),
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
chat_type: chat
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
.get_type()
|
|
||||||
.to_u32()
|
|
||||||
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
|
|
||||||
is_unpromoted: chat.is_unpromoted(),
|
is_unpromoted: chat.is_unpromoted(),
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
color,
|
color,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_protection_broken: chat.is_protection_broken(),
|
||||||
is_device_chat: chat.is_device_talk(),
|
is_device_chat: chat.is_device_talk(),
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
pub enum MuteDuration {
|
pub enum MuteDuration {
|
||||||
NotMuted,
|
NotMuted,
|
||||||
Forever,
|
Forever,
|
||||||
Until(i64),
|
Until { duration: i64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MuteDuration {
|
impl MuteDuration {
|
||||||
@@ -181,20 +205,20 @@ impl MuteDuration {
|
|||||||
match self {
|
match self {
|
||||||
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
||||||
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
||||||
MuteDuration::Until(n) => {
|
MuteDuration::Until { duration } => {
|
||||||
if n <= 0 {
|
if duration <= 0 {
|
||||||
bail!("failed to read mute duration")
|
bail!("failed to read mute duration")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SystemTime::now()
|
Ok(SystemTime::now()
|
||||||
.checked_add(Duration::from_secs(n as u64))
|
.checked_add(Duration::from_secs(duration as u64))
|
||||||
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "ChatVisibility")]
|
#[serde(rename = "ChatVisibility")]
|
||||||
pub enum JSONRPCChatVisibility {
|
pub enum JSONRPCChatVisibility {
|
||||||
Normal,
|
Normal,
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
|
use deltachat::chat::{Chat, ChatId};
|
||||||
|
use deltachat::chatlist::get_last_message_for_chat;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::{Contact, ContactId};
|
use deltachat::contact::{Contact, ContactId};
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
chat::{get_chat_contacts, ChatVisibility},
|
chat::{get_chat_contacts, ChatVisibility},
|
||||||
chatlist::Chatlist,
|
chatlist::Chatlist,
|
||||||
};
|
};
|
||||||
use deltachat::{
|
|
||||||
chat::{Chat, ChatId},
|
|
||||||
message::MsgId,
|
|
||||||
};
|
|
||||||
use num_traits::cast::ToPrimitive;
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
use super::message::MessageViewtype;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
pub struct ChatListEntry(pub u32, pub u32);
|
#[serde(tag = "kind")]
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum ChatListItemFetchResult {
|
pub enum ChatListItemFetchResult {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatListItem {
|
ChatListItem {
|
||||||
@@ -31,6 +27,8 @@ pub enum ChatListItemFetchResult {
|
|||||||
summary_text1: String,
|
summary_text1: String,
|
||||||
summary_text2: String,
|
summary_text2: String,
|
||||||
summary_status: u32,
|
summary_status: u32,
|
||||||
|
/// showing preview if last chat message is image
|
||||||
|
summary_preview_image: Option<String>,
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
is_group: bool,
|
is_group: bool,
|
||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
@@ -47,6 +45,8 @@ pub enum ChatListItemFetchResult {
|
|||||||
/// 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,
|
||||||
|
last_message_type: Option<MessageViewtype>,
|
||||||
|
last_message_id: Option<u32>,
|
||||||
},
|
},
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ArchiveLink { fresh_message_counter: usize },
|
ArchiveLink { fresh_message_counter: usize },
|
||||||
@@ -56,14 +56,9 @@ pub enum ChatListItemFetchResult {
|
|||||||
|
|
||||||
pub(crate) async fn get_chat_list_item_by_id(
|
pub(crate) async fn get_chat_list_item_by_id(
|
||||||
ctx: &deltachat::context::Context,
|
ctx: &deltachat::context::Context,
|
||||||
entry: &ChatListEntry,
|
entry: u32,
|
||||||
) -> Result<ChatListItemFetchResult> {
|
) -> Result<ChatListItemFetchResult> {
|
||||||
let chat_id = ChatId::new(entry.0);
|
let chat_id = ChatId::new(entry);
|
||||||
let last_msgid = match entry.1 {
|
|
||||||
0 => None,
|
|
||||||
_ => Some(MsgId::new(entry.1)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
||||||
|
|
||||||
if chat_id.is_archived_link() {
|
if chat_id.is_archived_link() {
|
||||||
@@ -72,12 +67,18 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let chat = Chat::load_from_db(ctx, chat_id).await?;
|
let last_msgid = get_last_message_for_chat(ctx, chat_id).await?;
|
||||||
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
|
|
||||||
|
let chat = Chat::load_from_db(ctx, chat_id).await.context("chat")?;
|
||||||
|
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat))
|
||||||
|
.await
|
||||||
|
.context("summary")?;
|
||||||
|
|
||||||
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
|
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
|
||||||
let summary_text2 = summary.text.to_owned();
|
let summary_text2 = summary.text.to_owned();
|
||||||
|
|
||||||
|
let summary_preview_image = summary.thumbnail_path;
|
||||||
|
|
||||||
let visibility = chat.get_visibility();
|
let visibility = chat.get_visibility();
|
||||||
|
|
||||||
let avatar_path = chat
|
let avatar_path = chat
|
||||||
@@ -85,12 +86,15 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
.await?
|
.await?
|
||||||
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
|
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
|
||||||
|
|
||||||
let last_updated = match last_msgid {
|
let (last_updated, message_type) = match last_msgid {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
|
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
|
||||||
Some(last_message.get_timestamp() * 1000)
|
(
|
||||||
|
Some(last_message.get_timestamp() * 1000),
|
||||||
|
Some(last_message.get_viewtype().into()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
None => None,
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||||
@@ -98,10 +102,11 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
||||||
|
|
||||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||||
let contact = chat_contacts.get(0);
|
let contact = chat_contacts.first();
|
||||||
let was_seen_recently = match contact {
|
let was_seen_recently = match contact {
|
||||||
Some(contact) => Contact::load_from_db(ctx, *contact)
|
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||||
.await?
|
.await
|
||||||
|
.context("contact")?
|
||||||
.was_seen_recently(),
|
.was_seen_recently(),
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
@@ -124,6 +129,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
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,
|
||||||
is_protected: chat.is_protected(),
|
is_protected: chat.is_protected(),
|
||||||
is_group: chat.get_type() == Chattype::Group,
|
is_group: chat.get_type() == Chattype::Group,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
@@ -138,5 +144,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
is_broadcast: chat.get_type() == Chattype::Broadcast,
|
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_id: last_msgid.map(|id| id.to_u32()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use deltachat::contact::VerifiedStatus;
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||||
pub struct ContactObject {
|
pub struct ContactObject {
|
||||||
address: String,
|
address: String,
|
||||||
@@ -19,14 +18,36 @@ pub struct ContactObject {
|
|||||||
profile_image: Option<String>, // BLOBS
|
profile_image: Option<String>, // BLOBS
|
||||||
name_and_addr: String,
|
name_and_addr: String,
|
||||||
is_blocked: bool,
|
is_blocked: bool,
|
||||||
|
|
||||||
|
/// True if the contact can be added to verified groups.
|
||||||
|
///
|
||||||
|
/// If this is true
|
||||||
|
/// UI should display green checkmark after the contact name
|
||||||
|
/// in contact list items,
|
||||||
|
/// in chat member list items
|
||||||
|
/// and in profiles if no chat with the contact exist.
|
||||||
is_verified: bool,
|
is_verified: bool,
|
||||||
/// the address that verified this contact
|
|
||||||
verifier_addr: Option<String>,
|
/// True if the contact profile title should have a green checkmark.
|
||||||
/// the id of the contact that verified this contact
|
///
|
||||||
|
/// This indicates whether 1:1 chat has a green checkmark
|
||||||
|
/// or will have a green checkmark if created.
|
||||||
|
is_profile_verified: bool,
|
||||||
|
|
||||||
|
/// The ID of the contact that verified this contact.
|
||||||
|
///
|
||||||
|
/// If this is present,
|
||||||
|
/// display a green checkmark and "Introduced by ..."
|
||||||
|
/// string followed by the verifier contact name and address
|
||||||
|
/// in the contact profile.
|
||||||
verifier_id: Option<u32>,
|
verifier_id: Option<u32>,
|
||||||
|
|
||||||
/// the contact's last seen timestamp
|
/// the contact's last seen timestamp
|
||||||
last_seen: i64,
|
last_seen: i64,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
|
|
||||||
|
/// If the contact is a bot.
|
||||||
|
is_bot: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContactObject {
|
impl ContactObject {
|
||||||
@@ -38,19 +59,13 @@ impl ContactObject {
|
|||||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
|
let is_verified = contact.is_verified(context).await?;
|
||||||
|
let is_profile_verified = contact.is_profile_verified(context).await?;
|
||||||
|
|
||||||
let (verifier_addr, verifier_id) = if is_verified {
|
let verifier_id = contact
|
||||||
(
|
.get_verifier_id(context)
|
||||||
contact.get_verifier_addr(context).await?,
|
.await?
|
||||||
contact
|
.map(|contact_id| contact_id.to_u32());
|
||||||
.get_verifier_id(context)
|
|
||||||
.await?
|
|
||||||
.map(|contact_id| contact_id.to_u32()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ContactObject {
|
Ok(ContactObject {
|
||||||
address: contact.get_addr().to_owned(),
|
address: contact.get_addr().to_owned(),
|
||||||
@@ -64,10 +79,11 @@ impl ContactObject {
|
|||||||
name_and_addr: contact.get_name_n_addr(),
|
name_and_addr: contact.get_name_n_addr(),
|
||||||
is_blocked: contact.is_blocked(),
|
is_blocked: contact.is_blocked(),
|
||||||
is_verified,
|
is_verified,
|
||||||
verifier_addr,
|
is_profile_verified,
|
||||||
verifier_id,
|
verifier_id,
|
||||||
last_seen: contact.last_seen(),
|
last_seen: contact.last_seen(),
|
||||||
was_seen_recently: contact.was_seen_recently(),
|
was_seen_recently: contact.was_seen_recently(),
|
||||||
|
is_bot: contact.is_bot(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,64 @@
|
|||||||
use deltachat::{Event, EventType};
|
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{json, Value};
|
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
pub fn event_to_json_rpc_notification(event: Event) -> Value {
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
let id: JSONRPCEventType = event.typ.into();
|
#[serde(rename_all = "camelCase")]
|
||||||
json!({
|
pub struct Event {
|
||||||
"event": id,
|
/// Event payload.
|
||||||
"contextId": event.id,
|
event: EventType,
|
||||||
})
|
|
||||||
|
/// Account ID.
|
||||||
|
context_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
impl From<CoreEvent> for Event {
|
||||||
#[serde(tag = "type", rename = "Event")]
|
fn from(event: CoreEvent) -> Self {
|
||||||
pub enum JSONRPCEventType {
|
Event {
|
||||||
|
event: event.typ.into(),
|
||||||
|
context_id: event.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
|
pub enum EventType {
|
||||||
/// The library-user may write an informational string to the log.
|
/// The library-user may write an informational string to the log.
|
||||||
///
|
///
|
||||||
/// This event should *not* be reported to the end-user using a popup or something like
|
/// This event should *not* be reported to the end-user using a popup or something like
|
||||||
/// that.
|
/// that.
|
||||||
Info {
|
Info { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when SMTP connection is established and login was successful.
|
/// Emitted when SMTP connection is established and login was successful.
|
||||||
SmtpConnected {
|
SmtpConnected { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when IMAP connection is established and login was successful.
|
/// Emitted when IMAP connection is established and login was successful.
|
||||||
ImapConnected {
|
ImapConnected { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when a message was successfully sent to the SMTP server.
|
/// Emitted when a message was successfully sent to the SMTP server.
|
||||||
SmtpMessageSent {
|
SmtpMessageSent { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when an IMAP message has been marked as deleted
|
/// Emitted when an IMAP message has been marked as deleted
|
||||||
ImapMessageDeleted {
|
ImapMessageDeleted { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when an IMAP message has been moved
|
/// Emitted when an IMAP message has been moved
|
||||||
ImapMessageMoved {
|
ImapMessageMoved { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
/// Emitted before going into IDLE on the Inbox folder.
|
||||||
|
ImapInboxIdle,
|
||||||
|
|
||||||
/// Emitted when an new file in the $BLOBDIR was created
|
/// Emitted when an new file in the $BLOBDIR was created
|
||||||
NewBlobFile {
|
NewBlobFile { file: String },
|
||||||
file: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Emitted when an file in the $BLOBDIR was deleted
|
/// Emitted when an file in the $BLOBDIR was deleted
|
||||||
DeletedBlobFile {
|
DeletedBlobFile { file: String },
|
||||||
file: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The library-user should write a warning string to the log.
|
/// The library-user should write a warning string to the log.
|
||||||
///
|
///
|
||||||
/// This event should *not* be reported to the end-user using a popup or something like
|
/// This event should *not* be reported to the end-user using a popup or something like
|
||||||
/// that.
|
/// that.
|
||||||
Warning {
|
Warning { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The library-user should report an error to the end-user.
|
/// The library-user should report an error to the end-user.
|
||||||
///
|
///
|
||||||
@@ -75,18 +70,14 @@ pub enum JSONRPCEventType {
|
|||||||
/// it might be better to delay showing these events until the function has really
|
/// it might be better to delay showing these events until the function has really
|
||||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||||
/// in a messasge box then.
|
/// in a messasge box then.
|
||||||
Error {
|
Error { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// An action cannot be performed because the user is not in the group.
|
/// An action cannot be performed because the user is not in the group.
|
||||||
/// Reported eg. after a call to
|
/// Reported eg. after a call to
|
||||||
/// setChatName(), setChatProfileImage(),
|
/// setChatName(), setChatProfileImage(),
|
||||||
/// addContactToChat(), removeContactFromChat(),
|
/// addContactToChat(), removeContactFromChat(),
|
||||||
/// and messages sending functions.
|
/// and messages sending functions.
|
||||||
ErrorSelfNotInGroup {
|
ErrorSelfNotInGroup { msg: String },
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Messages or chats changed. One or more messages or chats changed for various
|
/// Messages or chats changed. One or more messages or chats changed for various
|
||||||
/// reasons in the database:
|
/// reasons in the database:
|
||||||
@@ -97,10 +88,7 @@ pub enum JSONRPCEventType {
|
|||||||
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||||
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgsChanged {
|
MsgsChanged { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Reactions for the message changed.
|
/// Reactions for the message changed.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -113,53 +101,39 @@ pub enum JSONRPCEventType {
|
|||||||
/// There is a fresh message. Typically, the user will show an notification
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
IncomingMsg {
|
IncomingMsg { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Downloading a bunch of messages just finished. This is an experimental
|
/// Downloading a bunch of messages just finished. This is an
|
||||||
/// event to allow the UI to only show one notification per message bunch,
|
/// event to allow the UI to only show one notification per message bunch,
|
||||||
/// instead of cluttering the user with many notifications.
|
/// instead of cluttering the user with many notifications.
|
||||||
///
|
|
||||||
/// msg_ids contains the message ids.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
IncomingMsgBunch {
|
IncomingMsgBunch,
|
||||||
msg_ids: Vec<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Messages were seen or noticed.
|
/// Messages were seen or noticed.
|
||||||
/// chat id is always set.
|
/// chat id is always set.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgsNoticed {
|
MsgsNoticed { chat_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||||
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgDelivered {
|
MsgDelivered { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||||
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgFailed {
|
MsgFailed { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||||
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgRead {
|
MsgRead { chat_id: u32, msg_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
msg_id: u32,
|
/// A single message is deleted.
|
||||||
},
|
#[serde(rename_all = "camelCase")]
|
||||||
|
MsgDeleted { chat_id: u32, msg_id: u32 },
|
||||||
|
|
||||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||||
/// Or the verify state of a chat has changed.
|
/// Or the verify state of a chat has changed.
|
||||||
@@ -169,24 +143,17 @@ pub enum JSONRPCEventType {
|
|||||||
/// This event does not include ephemeral timer modification, which
|
/// This event does not include ephemeral timer modification, which
|
||||||
/// is a separate event.
|
/// is a separate event.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatModified {
|
ChatModified { chat_id: u32 },
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Chat ephemeral timer changed.
|
/// Chat ephemeral timer changed.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatEphemeralTimerModified {
|
ChatEphemeralTimerModified { chat_id: u32, timer: u32 },
|
||||||
chat_id: u32,
|
|
||||||
timer: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Contact(s) created, renamed, blocked or deleted.
|
/// Contact(s) created, renamed, blocked or deleted.
|
||||||
///
|
///
|
||||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ContactsChanged {
|
ContactsChanged { contact_id: Option<u32> },
|
||||||
contact_id: Option<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Location of one or more contact has changed.
|
/// Location of one or more contact has changed.
|
||||||
///
|
///
|
||||||
@@ -194,9 +161,7 @@ pub enum JSONRPCEventType {
|
|||||||
/// If the locations of several contacts have been changed,
|
/// If the locations of several contacts have been changed,
|
||||||
/// this parameter is set to `None`.
|
/// this parameter is set to `None`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
LocationChanged {
|
LocationChanged { contact_id: Option<u32> },
|
||||||
contact_id: Option<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Inform about the configuration progress started by configure().
|
/// Inform about the configuration progress started by configure().
|
||||||
ConfigureProgress {
|
ConfigureProgress {
|
||||||
@@ -214,9 +179,7 @@ pub enum JSONRPCEventType {
|
|||||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
/// @param data2 0
|
/// @param data2 0
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexProgress {
|
ImexProgress { progress: usize },
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A file has been exported. A file has been written by imex().
|
/// A file has been exported. A file has been written by imex().
|
||||||
/// This event may be sent multiple times by a single call to imex().
|
/// This event may be sent multiple times by a single call to imex().
|
||||||
@@ -226,9 +189,7 @@ pub enum JSONRPCEventType {
|
|||||||
///
|
///
|
||||||
/// @param data2 0
|
/// @param data2 0
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexFileWritten {
|
ImexFileWritten { path: String },
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Progress information of a secure-join handshake from the view of the inviter
|
/// Progress information of a secure-join handshake from the view of the inviter
|
||||||
/// (Alice, the person who shows the QR code).
|
/// (Alice, the person who shows the QR code).
|
||||||
@@ -243,10 +204,7 @@ pub enum JSONRPCEventType {
|
|||||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||||
/// 1000=Protocol finished for this contact.
|
/// 1000=Protocol finished for this contact.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
SecurejoinInviterProgress {
|
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
||||||
contact_id: u32,
|
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Progress information of a secure-join handshake from the view of the joiner
|
/// Progress information of a secure-join handshake from the view of the joiner
|
||||||
/// (Bob, the person who scans the QR code).
|
/// (Bob, the person who scans the QR code).
|
||||||
@@ -257,10 +215,7 @@ pub enum JSONRPCEventType {
|
|||||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
SecurejoinJoinerProgress {
|
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||||
contact_id: u32,
|
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The connectivity to the server changed.
|
/// The connectivity to the server changed.
|
||||||
/// This means that you should refresh the connectivity view
|
/// This means that you should refresh the connectivity view
|
||||||
@@ -268,8 +223,17 @@ pub enum JSONRPCEventType {
|
|||||||
/// getConnectivityHtml() for details.
|
/// getConnectivityHtml() for details.
|
||||||
ConnectivityChanged,
|
ConnectivityChanged,
|
||||||
|
|
||||||
|
/// Deprecated by `ConfigSynced`.
|
||||||
SelfavatarChanged,
|
SelfavatarChanged,
|
||||||
|
|
||||||
|
/// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
|
||||||
|
/// uniformity this is emitted on the source device too. The value isn't here, otherwise it
|
||||||
|
/// would be logged which might not be good for privacy.
|
||||||
|
ConfigSynced {
|
||||||
|
/// Configuration key.
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcStatusUpdate {
|
WebxdcStatusUpdate {
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
@@ -278,31 +242,46 @@ pub enum JSONRPCEventType {
|
|||||||
|
|
||||||
/// Inform that a message containing a webxdc instance has been deleted
|
/// Inform that a message containing a webxdc instance has been deleted
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcInstanceDeleted {
|
WebxdcInstanceDeleted { msg_id: u32 },
|
||||||
msg_id: u32,
|
|
||||||
},
|
/// Tells that the Background fetch was completed (or timed out).
|
||||||
|
/// This event acts as a marker, when you reach this event you can be sure
|
||||||
|
/// that all events emitted during the background fetch were processed.
|
||||||
|
///
|
||||||
|
/// This event is only emitted by the account manager
|
||||||
|
AccountsBackgroundFetchDone,
|
||||||
|
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||||
|
///
|
||||||
|
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
|
||||||
|
ChatlistChanged,
|
||||||
|
|
||||||
|
/// Inform that a single chat list item changed and needs to be rerendered.
|
||||||
|
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
ChatlistItemChanged { chat_id: Option<u32> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<EventType> for JSONRPCEventType {
|
impl From<CoreEventType> for EventType {
|
||||||
fn from(event: EventType) -> Self {
|
fn from(event: CoreEventType) -> Self {
|
||||||
use JSONRPCEventType::*;
|
use EventType::*;
|
||||||
match event {
|
match event {
|
||||||
EventType::Info(msg) => Info { msg },
|
CoreEventType::Info(msg) => Info { msg },
|
||||||
EventType::SmtpConnected(msg) => SmtpConnected { msg },
|
CoreEventType::SmtpConnected(msg) => SmtpConnected { msg },
|
||||||
EventType::ImapConnected(msg) => ImapConnected { msg },
|
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
|
||||||
EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
||||||
EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
||||||
EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
||||||
EventType::NewBlobFile(file) => NewBlobFile { file },
|
CoreEventType::ImapInboxIdle => ImapInboxIdle,
|
||||||
EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
|
||||||
EventType::Warning(msg) => Warning { msg },
|
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
||||||
EventType::Error(msg) => Error { msg },
|
CoreEventType::Warning(msg) => Warning { msg },
|
||||||
EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
|
CoreEventType::Error(msg) => Error { msg },
|
||||||
EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
|
CoreEventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
|
||||||
|
CoreEventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::ReactionsChanged {
|
CoreEventType::ReactionsChanged {
|
||||||
chat_id,
|
chat_id,
|
||||||
msg_id,
|
msg_id,
|
||||||
contact_id,
|
contact_id,
|
||||||
@@ -311,92 +290,86 @@ impl From<EventType> for JSONRPCEventType {
|
|||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
CoreEventType::IncomingMsgBunch => IncomingMsgBunch,
|
||||||
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||||
},
|
|
||||||
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
|
CoreEventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
|
CoreEventType::MsgFailed { chat_id, msg_id } => MsgFailed {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::MsgRead { chat_id, msg_id } => MsgRead {
|
CoreEventType::MsgRead { chat_id, msg_id } => MsgRead {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::ChatModified(chat_id) => ChatModified {
|
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
|
msg_id: msg_id.to_u32(),
|
||||||
|
},
|
||||||
|
CoreEventType::ChatModified(chat_id) => ChatModified {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::ChatEphemeralTimerModified { chat_id, timer } => {
|
CoreEventType::ChatEphemeralTimerModified { chat_id, timer } => {
|
||||||
ChatEphemeralTimerModified {
|
ChatEphemeralTimerModified {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
timer: timer.to_u32(),
|
timer: timer.to_u32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventType::ContactsChanged(contact) => ContactsChanged {
|
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
||||||
contact_id: contact.map(|c| c.to_u32()),
|
contact_id: contact.map(|c| c.to_u32()),
|
||||||
},
|
},
|
||||||
EventType::LocationChanged(contact) => LocationChanged {
|
CoreEventType::LocationChanged(contact) => LocationChanged {
|
||||||
contact_id: contact.map(|c| c.to_u32()),
|
contact_id: contact.map(|c| c.to_u32()),
|
||||||
},
|
},
|
||||||
EventType::ConfigureProgress { progress, comment } => {
|
CoreEventType::ConfigureProgress { progress, comment } => {
|
||||||
ConfigureProgress { progress, comment }
|
ConfigureProgress { progress, comment }
|
||||||
}
|
}
|
||||||
EventType::ImexProgress(progress) => ImexProgress { progress },
|
CoreEventType::ImexProgress(progress) => ImexProgress { progress },
|
||||||
EventType::ImexFileWritten(path) => ImexFileWritten {
|
CoreEventType::ImexFileWritten(path) => ImexFileWritten {
|
||||||
path: path.to_str().unwrap_or_default().to_owned(),
|
path: path.to_str().unwrap_or_default().to_owned(),
|
||||||
},
|
},
|
||||||
EventType::SecurejoinInviterProgress {
|
CoreEventType::SecurejoinInviterProgress {
|
||||||
contact_id,
|
contact_id,
|
||||||
progress,
|
progress,
|
||||||
} => SecurejoinInviterProgress {
|
} => SecurejoinInviterProgress {
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
progress,
|
progress,
|
||||||
},
|
},
|
||||||
EventType::SecurejoinJoinerProgress {
|
CoreEventType::SecurejoinJoinerProgress {
|
||||||
contact_id,
|
contact_id,
|
||||||
progress,
|
progress,
|
||||||
} => SecurejoinJoinerProgress {
|
} => SecurejoinJoinerProgress {
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
progress,
|
progress,
|
||||||
},
|
},
|
||||||
EventType::ConnectivityChanged => ConnectivityChanged,
|
CoreEventType::ConnectivityChanged => ConnectivityChanged,
|
||||||
EventType::SelfavatarChanged => SelfavatarChanged,
|
CoreEventType::SelfavatarChanged => SelfavatarChanged,
|
||||||
EventType::WebxdcStatusUpdate {
|
CoreEventType::ConfigSynced { key } => ConfigSynced {
|
||||||
|
key: key.to_string(),
|
||||||
|
},
|
||||||
|
CoreEventType::WebxdcStatusUpdate {
|
||||||
msg_id,
|
msg_id,
|
||||||
status_update_serial,
|
status_update_serial,
|
||||||
} => WebxdcStatusUpdate {
|
} => WebxdcStatusUpdate {
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
status_update_serial: status_update_serial.to_u32(),
|
status_update_serial: status_update_serial.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
|
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
|
||||||
|
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
|
||||||
|
chat_id: chat_id.map(|id| id.to_u32()),
|
||||||
|
},
|
||||||
|
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[test]
|
|
||||||
fn generate_events_ts_types_definition() {
|
|
||||||
let events = {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
let options = typescript_type_def::DefinitionFileOptions {
|
|
||||||
root_namespace: None,
|
|
||||||
..typescript_type_def::DefinitionFileOptions::default()
|
|
||||||
};
|
|
||||||
typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options)
|
|
||||||
.unwrap();
|
|
||||||
String::from_utf8(buf).unwrap()
|
|
||||||
};
|
|
||||||
std::fs::write("typescript/generated/events.ts", events).unwrap();
|
|
||||||
}
|
|
||||||
29
deltachat-jsonrpc/src/api/types/http.rs
Normal file
29
deltachat-jsonrpc/src/api/types/http.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use deltachat::net::HttpResponse as CoreHttpResponse;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
pub struct HttpResponse {
|
||||||
|
/// base64-encoded response body.
|
||||||
|
blob: String,
|
||||||
|
|
||||||
|
/// MIME type, e.g. "text/plain" or "text/html".
|
||||||
|
mimetype: Option<String>,
|
||||||
|
|
||||||
|
/// Encoding, e.g. "utf-8".
|
||||||
|
encoding: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CoreHttpResponse> for HttpResponse {
|
||||||
|
fn from(response: CoreHttpResponse) -> Self {
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
|
||||||
|
let mimetype = response.mimetype;
|
||||||
|
let encoding = response.encoding;
|
||||||
|
HttpResponse {
|
||||||
|
blob,
|
||||||
|
mimetype,
|
||||||
|
encoding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use deltachat::location::Location;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Location", rename_all = "camelCase")]
|
#[serde(rename = "Location", rename_all = "camelCase")]
|
||||||
pub struct JsonrpcLocation {
|
pub struct JsonrpcLocation {
|
||||||
pub location_id: u32,
|
pub location_id: u32,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use deltachat::chat::Chat;
|
use deltachat::chat::Chat;
|
||||||
use deltachat::chat::ChatItem;
|
use deltachat::chat::ChatItem;
|
||||||
use deltachat::constants::Chattype;
|
use deltachat::chat::ChatVisibility;
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::Contact;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::download;
|
use deltachat::download;
|
||||||
@@ -10,8 +10,7 @@ use deltachat::message::MsgId;
|
|||||||
use deltachat::message::Viewtype;
|
use deltachat::message::Viewtype;
|
||||||
use deltachat::reaction::get_msg_reactions;
|
use deltachat::reaction::get_msg_reactions;
|
||||||
use num_traits::cast::ToPrimitive;
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde::Serialize;
|
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
@@ -19,14 +18,14 @@ use super::contact::ContactObject;
|
|||||||
use super::reactions::JSONRPCReactions;
|
use super::reactions::JSONRPCReactions;
|
||||||
use super::webxdc::WebxdcMessageInfo;
|
use super::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", tag = "variant")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
pub enum MessageLoadResult {
|
pub enum MessageLoadResult {
|
||||||
Message(MessageObject),
|
Message(MessageObject),
|
||||||
LoadingError { error: String },
|
LoadingError { error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Message", rename_all = "camelCase")]
|
#[serde(rename = "Message", rename_all = "camelCase")]
|
||||||
pub struct MessageObject {
|
pub struct MessageObject {
|
||||||
id: u32,
|
id: u32,
|
||||||
@@ -35,7 +34,11 @@ pub struct MessageObject {
|
|||||||
quote: Option<MessageQuote>,
|
quote: Option<MessageQuote>,
|
||||||
parent_id: Option<u32>,
|
parent_id: Option<u32>,
|
||||||
|
|
||||||
text: Option<String>,
|
text: String,
|
||||||
|
|
||||||
|
/// Check if a message has a POI location bound to it.
|
||||||
|
/// These locations are also returned by `get_locations` method.
|
||||||
|
/// The UI may decide to display a special icon beside such messages.
|
||||||
has_location: bool,
|
has_location: bool,
|
||||||
has_html: bool,
|
has_html: bool,
|
||||||
view_type: MessageViewtype,
|
view_type: MessageViewtype,
|
||||||
@@ -86,7 +89,7 @@ pub struct MessageObject {
|
|||||||
reactions: Option<JSONRPCReactions>,
|
reactions: Option<JSONRPCReactions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
enum MessageQuote {
|
enum MessageQuote {
|
||||||
JustText {
|
JustText {
|
||||||
@@ -106,16 +109,15 @@ enum MessageQuote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageObject {
|
impl MessageObject {
|
||||||
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
|
|
||||||
let msg_id = MsgId::new(message_id);
|
|
||||||
Self::from_msg_id(context, msg_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||||
let message = Message::load_from_db(context, msg_id).await?;
|
let message = Message::load_from_db(context, msg_id).await?;
|
||||||
|
|
||||||
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
|
let sender_contact = Contact::get_by_id(context, message.get_from_id())
|
||||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
|
.await
|
||||||
|
.context("failed to load sender contact")?;
|
||||||
|
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
|
||||||
|
.await
|
||||||
|
.context("failed to load sender contact object")?;
|
||||||
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();
|
||||||
|
|
||||||
@@ -132,7 +134,9 @@ impl MessageObject {
|
|||||||
let quote = if let Some(quoted_text) = message.quoted_text() {
|
let quote = if let Some(quoted_text) = message.quoted_text() {
|
||||||
match message.quoted_message(context).await? {
|
match message.quoted_message(context).await? {
|
||||||
Some(quote) => {
|
Some(quote) => {
|
||||||
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
|
let quote_author = Contact::get_by_id(context, quote.get_from_id())
|
||||||
|
.await
|
||||||
|
.context("failed to load quote author contact")?;
|
||||||
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(),
|
||||||
@@ -160,7 +164,9 @@ impl MessageObject {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let reactions = get_msg_reactions(context, msg_id).await?;
|
let reactions = get_msg_reactions(context, msg_id)
|
||||||
|
.await
|
||||||
|
.context("failed to load message reactions")?;
|
||||||
let reactions = if reactions.is_empty() {
|
let reactions = if reactions.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -180,7 +186,7 @@ impl MessageObject {
|
|||||||
state: message
|
state: message
|
||||||
.get_state()
|
.get_state()
|
||||||
.to_u32()
|
.to_u32()
|
||||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
.context("state conversion to number failed")?,
|
||||||
error: message.error(),
|
error: message.error(),
|
||||||
|
|
||||||
timestamp: message.get_timestamp(),
|
timestamp: message.get_timestamp(),
|
||||||
@@ -203,7 +209,7 @@ impl MessageObject {
|
|||||||
videochat_type: match message.get_videochat_type() {
|
videochat_type: match message.get_videochat_type() {
|
||||||
Some(vct) => Some(
|
Some(vct) => Some(
|
||||||
vct.to_u32()
|
vct.to_u32()
|
||||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
.context("videochat type conversion to number failed")?,
|
||||||
),
|
),
|
||||||
None => None,
|
None => None,
|
||||||
},
|
},
|
||||||
@@ -230,7 +236,7 @@ impl MessageObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, TypeDef)]
|
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Viewtype")]
|
#[serde(rename = "Viewtype")]
|
||||||
pub enum MessageViewtype {
|
pub enum MessageViewtype {
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -306,11 +312,12 @@ impl From<MessageViewtype> for Viewtype {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
pub enum DownloadState {
|
pub enum DownloadState {
|
||||||
Done,
|
Done,
|
||||||
Available,
|
Available,
|
||||||
Failure,
|
Failure,
|
||||||
|
Undecipherable,
|
||||||
InProgress,
|
InProgress,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,12 +327,13 @@ impl From<download::DownloadState> for DownloadState {
|
|||||||
download::DownloadState::Done => DownloadState::Done,
|
download::DownloadState::Done => DownloadState::Done,
|
||||||
download::DownloadState::Available => DownloadState::Available,
|
download::DownloadState::Available => DownloadState::Available,
|
||||||
download::DownloadState::Failure => DownloadState::Failure,
|
download::DownloadState::Failure => DownloadState::Failure,
|
||||||
|
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
|
||||||
download::DownloadState::InProgress => DownloadState::InProgress,
|
download::DownloadState::InProgress => DownloadState::InProgress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
pub enum SystemMessageType {
|
pub enum SystemMessageType {
|
||||||
Unknown,
|
Unknown,
|
||||||
GroupNameChanged,
|
GroupNameChanged,
|
||||||
@@ -336,6 +344,15 @@ pub enum SystemMessageType {
|
|||||||
SecurejoinMessage,
|
SecurejoinMessage,
|
||||||
LocationStreamingEnabled,
|
LocationStreamingEnabled,
|
||||||
LocationOnly,
|
LocationOnly,
|
||||||
|
InvalidUnencryptedMail,
|
||||||
|
|
||||||
|
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
|
||||||
|
/// to complete.
|
||||||
|
SecurejoinWait,
|
||||||
|
|
||||||
|
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
|
||||||
|
/// send messages.
|
||||||
|
SecurejoinWaitTimeout,
|
||||||
|
|
||||||
/// Chat ephemeral message timer is changed.
|
/// Chat ephemeral message timer is changed.
|
||||||
EphemeralTimerChanged,
|
EphemeralTimerChanged,
|
||||||
@@ -376,11 +393,14 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||||
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
||||||
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
||||||
|
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
|
||||||
|
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
||||||
|
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageNotificationInfo {
|
pub struct MessageNotificationInfo {
|
||||||
id: u32,
|
id: u32,
|
||||||
@@ -438,14 +458,22 @@ impl MessageNotificationInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageSearchResult {
|
pub struct MessageSearchResult {
|
||||||
id: u32,
|
id: u32,
|
||||||
author_profile_image: Option<String>,
|
author_profile_image: Option<String>,
|
||||||
|
/// if sender name if overridden it will show it as ~alias
|
||||||
author_name: String,
|
author_name: String,
|
||||||
author_color: String,
|
author_color: String,
|
||||||
chat_name: Option<String>,
|
author_id: u32,
|
||||||
|
chat_profile_image: Option<String>,
|
||||||
|
chat_color: String,
|
||||||
|
chat_name: String,
|
||||||
|
chat_type: u32,
|
||||||
|
is_chat_protected: bool,
|
||||||
|
is_chat_contact_request: bool,
|
||||||
|
is_chat_archived: bool,
|
||||||
message: String,
|
message: String,
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
}
|
}
|
||||||
@@ -454,30 +482,44 @@ impl MessageSearchResult {
|
|||||||
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 chat = Chat::load_from_db(context, message.get_chat_id()).await?;
|
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
|
||||||
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
|
let sender = Contact::get_by_id(context, message.get_from_id()).await?;
|
||||||
|
|
||||||
let profile_image = match sender.get_profile_image(context).await? {
|
let profile_image = match sender.get_profile_image(context).await? {
|
||||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
let chat_profile_image = match chat.get_profile_image(context).await? {
|
||||||
|
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let author_name = if let Some(name) = message.get_override_sender_name() {
|
||||||
|
format!("~{name}")
|
||||||
|
} else {
|
||||||
|
sender.get_display_name().to_owned()
|
||||||
|
};
|
||||||
|
let chat_color = color_int_to_hex_string(chat.get_color(context).await?);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: msg_id.to_u32(),
|
id: msg_id.to_u32(),
|
||||||
author_profile_image: profile_image,
|
author_profile_image: profile_image,
|
||||||
author_name: sender.get_display_name().to_owned(),
|
author_name,
|
||||||
author_color: color_int_to_hex_string(sender.get_color()),
|
author_color: color_int_to_hex_string(sender.get_color()),
|
||||||
chat_name: if chat.get_type() == Chattype::Single {
|
author_id: sender.id.to_u32(),
|
||||||
Some(chat.get_name().to_owned())
|
chat_name: chat.get_name().to_owned(),
|
||||||
} else {
|
chat_color,
|
||||||
None
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
},
|
chat_profile_image,
|
||||||
message: message.get_text().unwrap_or_default(),
|
is_chat_protected: chat.is_protected(),
|
||||||
|
is_chat_contact_request: chat.is_contact_request(),
|
||||||
|
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||||
|
message: message.get_text(),
|
||||||
timestamp: message.get_timestamp(),
|
timestamp: message.get_timestamp(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
||||||
pub enum JSONRPCMessageListItem {
|
pub enum JSONRPCMessageListItem {
|
||||||
Message {
|
Message {
|
||||||
@@ -503,7 +545,7 @@ impl From<ChatItem> for JSONRPCMessageListItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, TypeDef)]
|
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageData {
|
pub struct MessageData {
|
||||||
pub text: Option<String>,
|
pub text: Option<String>,
|
||||||
@@ -514,3 +556,116 @@ pub struct MessageData {
|
|||||||
pub override_sender_name: Option<String>,
|
pub override_sender_name: Option<String>,
|
||||||
pub quoted_message_id: Option<u32>,
|
pub quoted_message_id: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MessageData {
|
||||||
|
pub(crate) async fn create_message(self, context: &Context) -> Result<Message> {
|
||||||
|
let mut message = Message::new(if let Some(viewtype) = self.viewtype {
|
||||||
|
viewtype.into()
|
||||||
|
} else if self.file.is_some() {
|
||||||
|
Viewtype::File
|
||||||
|
} else {
|
||||||
|
Viewtype::Text
|
||||||
|
});
|
||||||
|
message.set_text(self.text.unwrap_or_default());
|
||||||
|
if self.html.is_some() {
|
||||||
|
message.set_html(self.html);
|
||||||
|
}
|
||||||
|
if self.override_sender_name.is_some() {
|
||||||
|
message.set_override_sender_name(self.override_sender_name);
|
||||||
|
}
|
||||||
|
if let Some(file) = self.file {
|
||||||
|
message.set_file(file, None);
|
||||||
|
}
|
||||||
|
if let Some((latitude, longitude)) = self.location {
|
||||||
|
message.set_location(latitude, longitude);
|
||||||
|
}
|
||||||
|
if let Some(id) = self.quoted_message_id {
|
||||||
|
message
|
||||||
|
.set_quote(
|
||||||
|
context,
|
||||||
|
Some(
|
||||||
|
&Message::load_from_db(context, MsgId::new(id))
|
||||||
|
.await
|
||||||
|
.context("message to quote could not be loaded")?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MessageReadReceipt {
|
||||||
|
pub contact_id: u32,
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MessageInfo {
|
||||||
|
rawtext: String,
|
||||||
|
ephemeral_timer: EphemeralTimer,
|
||||||
|
/// When message is ephemeral this contains the timestamp of the message expiry
|
||||||
|
ephemeral_timestamp: Option<i64>,
|
||||||
|
error: Option<String>,
|
||||||
|
rfc724_mid: String,
|
||||||
|
server_urls: Vec<String>,
|
||||||
|
hop_info: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageInfo {
|
||||||
|
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||||
|
let message = Message::load_from_db(context, msg_id).await?;
|
||||||
|
let rawtext = msg_id.rawtext(context).await?;
|
||||||
|
let ephemeral_timer = message.get_ephemeral_timer().into();
|
||||||
|
let ephemeral_timestamp = match message.get_ephemeral_timer() {
|
||||||
|
deltachat::ephemeral::Timer::Disabled => None,
|
||||||
|
deltachat::ephemeral::Timer::Enabled { .. } => Some(message.get_ephemeral_timestamp()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let server_urls =
|
||||||
|
MsgId::get_info_server_urls(context, message.rfc724_mid().to_owned()).await?;
|
||||||
|
|
||||||
|
let hop_info = msg_id.hop_info(context).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
rawtext,
|
||||||
|
ephemeral_timer,
|
||||||
|
ephemeral_timestamp,
|
||||||
|
error: message.error(),
|
||||||
|
rfc724_mid: message.rfc724_mid().to_owned(),
|
||||||
|
server_urls,
|
||||||
|
hop_info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
|
pub enum EphemeralTimer {
|
||||||
|
/// Timer is disabled.
|
||||||
|
Disabled,
|
||||||
|
|
||||||
|
/// Timer is enabled.
|
||||||
|
Enabled {
|
||||||
|
/// Timer duration in seconds.
|
||||||
|
///
|
||||||
|
/// The value cannot be 0.
|
||||||
|
duration: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
|
||||||
|
fn from(value: deltachat::ephemeral::Timer) -> Self {
|
||||||
|
match value {
|
||||||
|
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
|
||||||
|
deltachat::ephemeral::Timer::Enabled { duration } => {
|
||||||
|
EphemeralTimer::Enabled { duration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ pub mod account;
|
|||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod chat_list;
|
pub mod chat_list;
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
|
pub mod events;
|
||||||
|
pub mod http;
|
||||||
pub mod location;
|
pub mod location;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod provider_info;
|
pub mod provider_info;
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ use num_traits::cast::ToPrimitive;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ProviderInfo {
|
pub struct ProviderInfo {
|
||||||
|
/// Unique ID, corresponding to provider database filename.
|
||||||
|
pub id: String,
|
||||||
pub before_login_hint: String,
|
pub before_login_hint: String,
|
||||||
pub overview_page: String,
|
pub overview_page: String,
|
||||||
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
|
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
|
||||||
@@ -14,6 +16,7 @@ pub struct ProviderInfo {
|
|||||||
impl ProviderInfo {
|
impl ProviderInfo {
|
||||||
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
|
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
|
||||||
provider.map(|p| ProviderInfo {
|
provider.map(|p| ProviderInfo {
|
||||||
|
id: p.id.to_owned(),
|
||||||
before_login_hint: p.before_login_hint.to_owned(),
|
before_login_hint: p.before_login_hint.to_owned(),
|
||||||
overview_page: p.overview_page.to_owned(),
|
overview_page: p.overview_page.to_owned(),
|
||||||
status: p.status.to_u32().unwrap(),
|
status: p.status.to_u32().unwrap(),
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ use deltachat::qr::Qr;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "kind")]
|
||||||
pub enum QrObject {
|
pub enum QrObject {
|
||||||
AskVerifyContact {
|
AskVerifyContact {
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
|
|||||||
@@ -1,23 +1,37 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use deltachat::contact::ContactId;
|
||||||
use deltachat::reaction::Reactions;
|
use deltachat::reaction::Reactions;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
/// A single reaction emoji.
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||||
|
pub struct JSONRPCReaction {
|
||||||
|
/// Emoji.
|
||||||
|
emoji: String,
|
||||||
|
|
||||||
|
/// Emoji frequency.
|
||||||
|
count: usize,
|
||||||
|
|
||||||
|
/// True if we reacted with this emoji.
|
||||||
|
is_from_self: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Structure representing all reactions to a particular message.
|
/// Structure representing all reactions to a particular message.
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||||
pub struct JSONRPCReactions {
|
pub struct JSONRPCReactions {
|
||||||
/// Map from a contact to it's reaction to message.
|
/// Map from a contact to it's reaction to message.
|
||||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||||
/// Unique reactions and their count
|
/// Unique reactions and their count, sorted in descending order.
|
||||||
reactions: BTreeMap<String, u32>,
|
reactions: Vec<JSONRPCReaction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Reactions> for JSONRPCReactions {
|
impl From<Reactions> for JSONRPCReactions {
|
||||||
fn from(reactions: Reactions) -> Self {
|
fn from(reactions: Reactions) -> Self {
|
||||||
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||||
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
|
|
||||||
|
|
||||||
for contact_id in reactions.contacts() {
|
for contact_id in reactions.contacts() {
|
||||||
let reaction = reactions.get(contact_id);
|
let reaction = reactions.get(contact_id);
|
||||||
@@ -30,18 +44,29 @@ impl From<Reactions> for JSONRPCReactions {
|
|||||||
.map(|emoji| emoji.to_owned())
|
.map(|emoji| emoji.to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
||||||
for emoji in emojis {
|
}
|
||||||
if let Some(x) = unique_reactions.get_mut(&emoji) {
|
|
||||||
*x += 1;
|
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
||||||
} else {
|
|
||||||
unique_reactions.insert(emoji, 1);
|
let mut reactions_v = Vec::new();
|
||||||
}
|
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
||||||
}
|
let is_from_self = if let Some(self_reactions) = self_reactions {
|
||||||
|
self_reactions.contains(&emoji)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let reaction = JSONRPCReaction {
|
||||||
|
emoji,
|
||||||
|
count,
|
||||||
|
is_from_self,
|
||||||
|
};
|
||||||
|
reactions_v.push(reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONRPCReactions {
|
JSONRPCReactions {
|
||||||
reactions_by_contact,
|
reactions_by_contact,
|
||||||
reactions: unique_reactions,
|
reactions: reactions_v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
use super::maybe_empty_string_to_option;
|
use super::maybe_empty_string_to_option;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
|
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
|
||||||
pub struct WebxdcMessageInfo {
|
pub struct WebxdcMessageInfo {
|
||||||
/// The name of the app.
|
/// The name of the app.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
pub use api::events;
|
|
||||||
pub use yerpc;
|
pub use yerpc;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -14,10 +13,11 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||||
let accounts = Accounts::new(tmp_dir).await?;
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||||
let api = CommandApi::new(accounts);
|
let api = CommandApi::new(accounts);
|
||||||
|
|
||||||
let (sender, mut receiver) = unbounded::<String>();
|
let (sender, receiver) = unbounded::<String>();
|
||||||
|
|
||||||
let (client, mut rx) = RpcClient::new();
|
let (client, mut rx) = RpcClient::new();
|
||||||
let session = RpcSession::new(client, api);
|
let session = RpcSession::new(client, api);
|
||||||
@@ -36,17 +36,17 @@ mod tests {
|
|||||||
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.next().await;
|
let result = receiver.recv().await?;
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
assert_eq!(result, Some(response.to_owned()));
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
|
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.next().await;
|
let result = receiver.recv().await?;
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
assert_eq!(result, Some(response.to_owned()));
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -55,10 +55,11 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_batch_set_config() -> anyhow::Result<()> {
|
async fn test_batch_set_config() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||||
let accounts = Accounts::new(tmp_dir).await?;
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||||
let api = CommandApi::new(accounts);
|
let api = CommandApi::new(accounts);
|
||||||
|
|
||||||
let (sender, mut receiver) = unbounded::<String>();
|
let (sender, receiver) = unbounded::<String>();
|
||||||
|
|
||||||
let (client, mut rx) = RpcClient::new();
|
let (client, mut rx) = RpcClient::new();
|
||||||
let session = RpcSession::new(client, api);
|
let session = RpcSession::new(client, api);
|
||||||
@@ -77,15 +78,15 @@ mod tests {
|
|||||||
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.next().await;
|
let result = receiver.recv().await?;
|
||||||
assert_eq!(result, Some(response.to_owned()));
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.next().await;
|
let result = receiver.recv().await?;
|
||||||
assert_eq!(result, Some(response.to_owned()));
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use yerpc::axum::handle_ws_rpc;
|
|||||||
use yerpc::{RpcClient, RpcSession};
|
use yerpc::{RpcClient, RpcSession};
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
use api::events::event_to_json_rpc_notification;
|
|
||||||
use api::{Accounts, CommandApi};
|
use api::{Accounts, CommandApi};
|
||||||
|
|
||||||
const DEFAULT_PORT: u16 = 20808;
|
const DEFAULT_PORT: u16 = 20808;
|
||||||
@@ -20,7 +19,8 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
||||||
.unwrap_or(DEFAULT_PORT);
|
.unwrap_or(DEFAULT_PORT);
|
||||||
log::info!("Starting with accounts directory `{path}`.");
|
log::info!("Starting with accounts directory `{path}`.");
|
||||||
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
|
||||||
let state = CommandApi::new(accounts);
|
let state = CommandApi::new(accounts);
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -28,15 +28,13 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
.layer(Extension(state.clone()));
|
.layer(Extension(state.clone()));
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
state.accounts.read().await.start_io().await;
|
state.accounts.write().await.start_io().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||||
log::info!("JSON-RPC WebSocket server listening on {}", addr);
|
log::info!("JSON-RPC WebSocket server listening on {}", addr);
|
||||||
axum::Server::bind(&addr)
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
.serve(app.into_make_service())
|
axum::serve(listener, app).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -44,12 +42,5 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
||||||
let (client, out_receiver) = RpcClient::new();
|
let (client, out_receiver) = RpcClient::new();
|
||||||
let session = RpcSession::new(client.clone(), api.clone());
|
let session = RpcSession::new(client.clone(), api.clone());
|
||||||
tokio::spawn(async move {
|
|
||||||
let events = api.accounts.read().await.get_event_emitter();
|
|
||||||
while let Some(event) = events.recv().await {
|
|
||||||
let event = event_to_json_rpc_notification(event);
|
|
||||||
client.send_notification("event", Some(event)).await.ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
handle_ws_rpc(ws, out_receiver, session).await
|
handle_ws_rpc(ws, out_receiver, session).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async function run() {
|
|||||||
const accounts = await client.rpc.getAllAccounts();
|
const accounts = await client.rpc.getAllAccounts();
|
||||||
console.log("accounts loaded", accounts);
|
console.log("accounts loaded", accounts);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
if (account.type === "Configured") {
|
if (account.kind === "Configured") {
|
||||||
write(
|
write(
|
||||||
$head,
|
$head,
|
||||||
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
||||||
@@ -57,7 +57,7 @@ async function run() {
|
|||||||
clear($main);
|
clear($main);
|
||||||
const selectedAccount = SELECTED_ACCOUNT;
|
const selectedAccount = SELECTED_ACCOUNT;
|
||||||
const info = await client.rpc.getAccountInfo(selectedAccount);
|
const info = await client.rpc.getAccountInfo(selectedAccount);
|
||||||
if (info.type !== "Configured") {
|
if (info.kind !== "Configured") {
|
||||||
return write($main, "Account is not configured");
|
return write($main, "Account is not configured");
|
||||||
}
|
}
|
||||||
write($main, `<h2>${info.addr!}</h2>`);
|
write($main, `<h2>${info.addr!}</h2>`);
|
||||||
@@ -67,7 +67,7 @@ async function run() {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
for (const [chatId, _messageId] of chats) {
|
for (const chatId of chats) {
|
||||||
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
|
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
|
||||||
write($main, `<h3>${chat.name}</h3>`);
|
write($main, `<h3>${chat.name}</h3>`);
|
||||||
const messageIds = await client.rpc.getMessageIds(
|
const messageIds = await client.rpc.getMessageIds(
|
||||||
@@ -81,8 +81,7 @@ async function run() {
|
|||||||
messageIds
|
messageIds
|
||||||
);
|
);
|
||||||
for (const [_messageId, message] of Object.entries(messages)) {
|
for (const [_messageId, message] of Object.entries(messages)) {
|
||||||
if (message.variant === "message")
|
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
|
||||||
write($main, `<p>${message.text}</p>`);
|
|
||||||
else write($main, `<p>loading error: ${message.error}</p>`);
|
else write($main, `<p>loading error: ${message.error}</p>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +92,9 @@ async function run() {
|
|||||||
$side,
|
$side,
|
||||||
`
|
`
|
||||||
<p class="message">
|
<p class="message">
|
||||||
[<strong>${event.type}</strong> on account ${accountId}]<br>
|
[<strong>${event.kind}</strong> on account ${accountId}]<br>
|
||||||
<em>f1:</em> ${JSON.stringify(
|
<em>f1:</em> ${JSON.stringify(
|
||||||
Object.assign({}, event, { type: undefined })
|
Object.assign({}, event, { kind: undefined })
|
||||||
)}
|
)}
|
||||||
</p>`
|
</p>`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"@types/chai": "^4.2.21",
|
"@types/chai": "^4.2.21",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/node-fetch": "^2.5.7",
|
|
||||||
"@types/ws": "^7.2.4",
|
"@types/ws": "^7.2.4",
|
||||||
"c8": "^7.10.0",
|
"c8": "^7.10.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
@@ -17,7 +16,6 @@
|
|||||||
"esbuild": "^0.17.9",
|
"esbuild": "^0.17.9",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"mocha": "^9.1.1",
|
"mocha": "^9.1.1",
|
||||||
"node-fetch": "^2.6.1",
|
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"typedoc": "^0.23.2",
|
"typedoc": "^0.23.2",
|
||||||
@@ -27,7 +25,8 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/deltachat.js",
|
"import": "./dist/deltachat.js",
|
||||||
"require": "./dist/deltachat.cjs"
|
"require": "./dist/deltachat.cjs",
|
||||||
|
"types": "./dist/deltachat.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
@@ -55,5 +54,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "1.111.0"
|
"version": "1.138.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
import * as T from "../generated/types.js";
|
import * as T 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 { Event } from "../generated/events.js";
|
|
||||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||||
|
|
||||||
type DCWireEvent<T extends Event> = {
|
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||||
event: T;
|
[Property in EventType["kind"]]: (
|
||||||
contextId: number;
|
|
||||||
};
|
|
||||||
// export type Events = Record<
|
|
||||||
// Event["type"] | "ALL",
|
|
||||||
// (event: DeltaChatEvent<Event>) => void
|
|
||||||
// >;
|
|
||||||
|
|
||||||
type Events = { ALL: (accountId: number, event: Event) => void } & {
|
|
||||||
[Property in Event["type"]]: (
|
|
||||||
accountId: number,
|
accountId: number,
|
||||||
event: Extract<Event, { type: Property }>
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextEvents = { ALL: (event: Event) => void } & {
|
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||||
[Property in Event["type"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
event: Extract<Event, { type: Property }>
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DcEvent = Event;
|
export type DcEvent = EventType;
|
||||||
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
|
export type DcEventType<T extends EventType["kind"]> = Extract<
|
||||||
|
EventType,
|
||||||
|
{ kind: T }
|
||||||
|
>;
|
||||||
|
|
||||||
export class BaseDeltaChat<
|
export class BaseDeltaChat<
|
||||||
Transport extends BaseTransport<any>
|
Transport extends BaseTransport<any>
|
||||||
@@ -36,27 +30,34 @@ export class BaseDeltaChat<
|
|||||||
rpc: RawClient;
|
rpc: RawClient;
|
||||||
account?: T.Account;
|
account?: T.Account;
|
||||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||||
constructor(public transport: Transport) {
|
|
||||||
|
//@ts-ignore
|
||||||
|
private eventTask: Promise<void>;
|
||||||
|
|
||||||
|
constructor(public transport: Transport, startEventLoop: boolean) {
|
||||||
super();
|
super();
|
||||||
this.rpc = new RawClient(this.transport);
|
this.rpc = new RawClient(this.transport);
|
||||||
this.transport.on("request", (request: Request) => {
|
if (startEventLoop) {
|
||||||
const method = request.method;
|
this.eventTask = this.eventLoop();
|
||||||
if (method === "event") {
|
}
|
||||||
const event = request.params! as DCWireEvent<Event>;
|
}
|
||||||
//@ts-ignore
|
|
||||||
this.emit(event.event.type, event.contextId, event.event as any);
|
|
||||||
this.emit("ALL", event.contextId, event.event as any);
|
|
||||||
|
|
||||||
if (this.contextEmitters[event.contextId]) {
|
async eventLoop(): Promise<void> {
|
||||||
this.contextEmitters[event.contextId].emit(
|
while (true) {
|
||||||
event.event.type,
|
const event = await this.rpc.getNextEvent();
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
event.event as any
|
this.emit(event.event.kind, event.contextId, event.event);
|
||||||
);
|
this.emit("ALL", event.contextId, event.event);
|
||||||
this.contextEmitters[event.contextId].emit("ALL", event.event);
|
|
||||||
}
|
if (this.contextEmitters[event.contextId]) {
|
||||||
|
this.contextEmitters[event.contextId].emit(
|
||||||
|
event.event.kind,
|
||||||
|
//@ts-ignore
|
||||||
|
event.event as any
|
||||||
|
);
|
||||||
|
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listAccounts(): Promise<T.Account[]> {
|
async listAccounts(): Promise<T.Account[]> {
|
||||||
@@ -75,10 +76,12 @@ export class BaseDeltaChat<
|
|||||||
|
|
||||||
export type Opts = {
|
export type Opts = {
|
||||||
url: string;
|
url: string;
|
||||||
|
startEventLoop: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_OPTS: Opts = {
|
export const DEFAULT_OPTS: Opts = {
|
||||||
url: "ws://localhost:20808/ws",
|
url: "ws://localhost:20808/ws",
|
||||||
|
startEventLoop: true,
|
||||||
};
|
};
|
||||||
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||||
opts: Opts;
|
opts: Opts;
|
||||||
@@ -86,20 +89,24 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
|||||||
this.transport.close();
|
this.transport.close();
|
||||||
}
|
}
|
||||||
constructor(opts?: Opts | string) {
|
constructor(opts?: Opts | string) {
|
||||||
if (typeof opts === "string") opts = { url: opts };
|
if (typeof opts === "string") {
|
||||||
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
|
opts = { ...DEFAULT_OPTS, url: opts };
|
||||||
else opts = { ...DEFAULT_OPTS };
|
} else if (opts) {
|
||||||
|
opts = { ...DEFAULT_OPTS, ...opts };
|
||||||
|
} else {
|
||||||
|
opts = { ...DEFAULT_OPTS };
|
||||||
|
}
|
||||||
const transport = new WebsocketTransport(opts.url);
|
const transport = new WebsocketTransport(opts.url);
|
||||||
super(transport);
|
super(transport, opts.startEventLoop);
|
||||||
this.opts = opts;
|
this.opts = opts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||||
close() {}
|
close() {}
|
||||||
constructor(input: any, output: any) {
|
constructor(input: any, output: any, startEventLoop: boolean) {
|
||||||
const transport = new StdioTransport(input, output);
|
const transport = new StdioTransport(input, output);
|
||||||
super(transport);
|
super(transport, startEventLoop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export * as RPC from "../generated/jsonrpc.js";
|
export * as RPC from "../generated/jsonrpc.js";
|
||||||
export * as T from "../generated/types.js";
|
export * as T from "../generated/types.js";
|
||||||
export * from "../generated/events.js";
|
|
||||||
export { RawClient } from "../generated/client.js";
|
export { RawClient } from "../generated/client.js";
|
||||||
export * from "./client.js";
|
export * from "./client.js";
|
||||||
export * as yerpc from "yerpc";
|
export * as yerpc from "yerpc";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("basic tests", () => {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
serverHandle = await startServer();
|
serverHandle = await startServer();
|
||||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
|
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||||
// dc.on("ALL", (event) => {
|
// dc.on("ALL", (event) => {
|
||||||
//console.log("event", event);
|
//console.log("event", event);
|
||||||
// });
|
// });
|
||||||
@@ -79,6 +79,9 @@ describe("basic tests", () => {
|
|||||||
accountId = await dc.rpc.addAccount();
|
accountId = await dc.rpc.addAccount();
|
||||||
});
|
});
|
||||||
it("should block and unblock contact", async function () {
|
it("should block and unblock contact", async function () {
|
||||||
|
// Cannot send sync messages to self as we do not have a self address.
|
||||||
|
await dc.rpc.setConfig(accountId, "sync_msgs", "0");
|
||||||
|
|
||||||
const contactId = await dc.rpc.createContact(
|
const contactId = await dc.rpc.createContact(
|
||||||
accountId,
|
accountId,
|
||||||
"example@delta.chat",
|
"example@delta.chat",
|
||||||
|
|||||||
@@ -13,27 +13,27 @@ describe("online tests", function () {
|
|||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(60000);
|
this.timeout(60000);
|
||||||
if (!process.env.DCC_NEW_TMP_EMAIL) {
|
if (!process.env.CHATMAIL_DOMAIN) {
|
||||||
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
||||||
console.error(
|
console.error(
|
||||||
"CAN NOT RUN COVERAGE correctly: Missing DCC_NEW_TMP_EMAIL environment variable!\n\n",
|
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
|
||||||
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
|
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
"Missing DCC_NEW_TMP_EMAIL environment variable!, skip integration tests"
|
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
serverHandle = await startServer();
|
serverHandle = await startServer();
|
||||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
|
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||||
|
|
||||||
dc.on("ALL", (contextId, { type }) => {
|
dc.on("ALL", (contextId, { kind }) => {
|
||||||
if (type !== "Info") console.log(contextId, type);
|
if (kind !== "Info") console.log(contextId, kind);
|
||||||
});
|
});
|
||||||
|
|
||||||
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||||
if (!account1 || !account1.email || !account1.password) {
|
if (!account1 || !account1.email || !account1.password) {
|
||||||
console.log(
|
console.log(
|
||||||
"We didn't got back an account from the api, skip integration tests"
|
"We didn't got back an account from the api, skip integration tests"
|
||||||
@@ -41,7 +41,7 @@ describe("online tests", function () {
|
|||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
|
|
||||||
account2 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||||
if (!account2 || !account2.email || !account2.password) {
|
if (!account2 || !account2.email || !account2.password) {
|
||||||
console.log(
|
console.log(
|
||||||
"We didn't got back an account2 from the api, skip integration tests"
|
"We didn't got back an account2 from the api, skip integration tests"
|
||||||
@@ -148,7 +148,7 @@ describe("online tests", function () {
|
|||||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||||
]);
|
]);
|
||||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||||
// Check if answer arives at A and if it is encrypted
|
// Check if answer arrives at A and if it is encrypted
|
||||||
await eventPromise2;
|
await eventPromise2;
|
||||||
|
|
||||||
const messageId = (
|
const messageId = (
|
||||||
@@ -177,12 +177,12 @@ describe("online tests", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function waitForEvent<T extends DcEvent["type"]>(
|
async function waitForEvent<T extends DcEvent["kind"]>(
|
||||||
dc: DeltaChat,
|
dc: DeltaChat,
|
||||||
eventType: T,
|
eventType: T,
|
||||||
accountId: number,
|
accountId: number,
|
||||||
timeout: number = EVENT_TIMEOUT
|
timeout: number = EVENT_TIMEOUT
|
||||||
): Promise<Extract<DcEvent, { type: T }>> {
|
): Promise<Extract<DcEvent, { kind: T }>> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const rejectTimeout = setTimeout(
|
const rejectTimeout = setTimeout(
|
||||||
() => reject(new Error("Timeout reached before event came in")),
|
() => reject(new Error("Timeout reached before event came in")),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { tmpdir } from "os";
|
|||||||
import { join, resolve } from "path";
|
import { join, resolve } from "path";
|
||||||
import { mkdtemp, rm } from "fs/promises";
|
import { mkdtemp, rm } from "fs/promises";
|
||||||
import { spawn, exec } from "child_process";
|
import { spawn, exec } from "child_process";
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { Readable, Writable } from "node:stream";
|
import { Readable, Writable } from "node:stream";
|
||||||
|
|
||||||
export type RpcServerHandle = {
|
export type RpcServerHandle = {
|
||||||
@@ -57,15 +56,14 @@ export async function startServer(): Promise<RpcServerHandle> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTempUser(url: string) {
|
export function createTempUser(chatmailDomain: String) {
|
||||||
const response = await fetch(url, {
|
const charset = "2345789acdefghjkmnpqrstuvwxyz";
|
||||||
method: "POST",
|
let user = "ci-";
|
||||||
headers: {
|
for (let i = 0; i < 6; i++) {
|
||||||
"cache-control": "no-cache",
|
user += charset[Math.floor(Math.random() * charset.length)];
|
||||||
},
|
}
|
||||||
});
|
const email = user + "@" + chatmailDomain;
|
||||||
if (!response.ok) throw new Error("Received invalid response");
|
return { email: email, password: user + "$" + user };
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTargetDir(): Promise<string> {
|
function getTargetDir(): Promise<string> {
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "1.111.0"
|
version = "1.138.2"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.12.1"
|
ansi_term = "0.12.1"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
deltachat = { path = "..", features = ["internals"]}
|
deltachat = { path = "..", features = ["internals"]}
|
||||||
dirs = "4"
|
dirs = "5"
|
||||||
log = "0.4.16"
|
log = "0.4.21"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.5"
|
||||||
rusqlite = "0.28"
|
rusqlite = "0.31"
|
||||||
rustyline = "11"
|
rustyline = "14"
|
||||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ extern crate dirs;
|
|||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use deltachat::chat::{
|
use deltachat::chat::{
|
||||||
@@ -18,6 +18,7 @@ use deltachat::imex::*;
|
|||||||
use deltachat::location;
|
use deltachat::location;
|
||||||
use deltachat::log::LogExt;
|
use deltachat::log::LogExt;
|
||||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||||
|
use deltachat::mimeparser::SystemMessage;
|
||||||
use deltachat::peerstate::*;
|
use deltachat::peerstate::*;
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
use deltachat::reaction::send_reaction;
|
use deltachat::reaction::send_reaction;
|
||||||
@@ -32,14 +33,6 @@ use tokio::fs;
|
|||||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||||
async fn reset_tables(context: &Context, bits: i32) {
|
async fn reset_tables(context: &Context, bits: i32) {
|
||||||
println!("Resetting tables ({bits})...");
|
println!("Resetting tables ({bits})...");
|
||||||
if 0 != bits & 1 {
|
|
||||||
context
|
|
||||||
.sql()
|
|
||||||
.execute("DELETE FROM jobs;", ())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
println!("(1) Jobs reset.");
|
|
||||||
}
|
|
||||||
if 0 != bits & 2 {
|
if 0 != bits & 2 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
@@ -138,11 +131,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
/* import a directory */
|
/* import a directory */
|
||||||
let dir_name = std::path::Path::new(&real_spec);
|
let dir_name = std::path::Path::new(&real_spec);
|
||||||
let dir = fs::read_dir(dir_name).await;
|
let dir = fs::read_dir(dir_name).await;
|
||||||
if dir.is_err() {
|
if let Ok(mut dir) = dir {
|
||||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
let mut dir = dir.unwrap();
|
|
||||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||||
let name_f = entry.file_name();
|
let name_f = entry.file_name();
|
||||||
let name = name_f.to_string_lossy();
|
let name = name_f.to_string_lossy();
|
||||||
@@ -154,6 +143,9 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||||
@@ -187,6 +179,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
DownloadState::Available => " [⬇ Download available]",
|
DownloadState::Available => " [⬇ Download available]",
|
||||||
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
||||||
DownloadState::Failure => " [⬇ Download failed]",
|
DownloadState::Failure => " [⬇ Download failed]",
|
||||||
|
DownloadState::Undecipherable => " [⬇ Decryption failed]",
|
||||||
};
|
};
|
||||||
|
|
||||||
let temp2 = timestamp_to_str(msg.get_timestamp());
|
let temp2 = timestamp_to_str(msg.get_timestamp());
|
||||||
@@ -199,7 +192,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
if msg.has_location() { "📍" } else { "" },
|
if msg.has_location() { "📍" } else { "" },
|
||||||
&contact_name,
|
&contact_name,
|
||||||
contact_id,
|
contact_id,
|
||||||
msgtext.unwrap_or_default(),
|
msgtext,
|
||||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||||
if msg.get_from_id() == ContactId::SELF {
|
if msg.get_from_id() == ContactId::SELF {
|
||||||
""
|
""
|
||||||
@@ -210,7 +203,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
} else {
|
} else {
|
||||||
"[FRESH]"
|
"[FRESH]"
|
||||||
},
|
},
|
||||||
if msg.is_info() { "[INFO]" } else { "" },
|
if msg.is_info() {
|
||||||
|
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
|
||||||
|
"[INFO 🛡️]"
|
||||||
|
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
|
||||||
|
"[INFO 🛡️❌]"
|
||||||
|
} else {
|
||||||
|
"[INFO]"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||||
format!(
|
format!(
|
||||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||||
@@ -273,13 +276,8 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
|||||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||||
let name = contact.get_display_name();
|
let name = contact.get_display_name();
|
||||||
let addr = contact.get_addr();
|
let addr = contact.get_addr();
|
||||||
let verified_state = contact.is_verified(context).await?;
|
let verified_str = if contact.is_verified(context).await? {
|
||||||
let verified_str = if VerifiedStatus::Unverified != verified_state {
|
" √"
|
||||||
if verified_state == VerifiedStatus::BidirectVerified {
|
|
||||||
" √√"
|
|
||||||
} else {
|
|
||||||
" √"
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
@@ -395,8 +393,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
unpin <chat-id>\n\
|
unpin <chat-id>\n\
|
||||||
mute <chat-id> [<seconds>]\n\
|
mute <chat-id> [<seconds>]\n\
|
||||||
unmute <chat-id>\n\
|
unmute <chat-id>\n\
|
||||||
protect <chat-id>\n\
|
|
||||||
unprotect <chat-id>\n\
|
|
||||||
delchat <chat-id>\n\
|
delchat <chat-id>\n\
|
||||||
accept <chat-id>\n\
|
accept <chat-id>\n\
|
||||||
decline <chat-id>\n\
|
decline <chat-id>\n\
|
||||||
@@ -563,7 +559,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
context.maybe_network().await;
|
context.maybe_network().await;
|
||||||
}
|
}
|
||||||
"housekeeping" => {
|
"housekeeping" => {
|
||||||
sql::housekeeping(&context).await.ok_or_log(&context);
|
sql::housekeeping(&context).await.log_err(&context).ok();
|
||||||
}
|
}
|
||||||
"listchats" | "listarchived" | "chats" => {
|
"listchats" | "listarchived" | "chats" => {
|
||||||
let listflags = if arg0 == "listarchived" {
|
let listflags = if arg0 == "listarchived" {
|
||||||
@@ -805,15 +801,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"chatinfo" => {
|
"chatinfo" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
|
let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
|
||||||
|
|
||||||
let contacts =
|
let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
|
||||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
|
||||||
println!("Memberlist:");
|
println!("Memberlist:");
|
||||||
|
|
||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
|
println!("{} contacts", contacts.len());
|
||||||
|
|
||||||
|
let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
|
||||||
|
if !similar_chats.is_empty() {
|
||||||
|
println!("Similar chats: ");
|
||||||
|
for (similar_chat_id, metric) in similar_chats {
|
||||||
|
let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
|
||||||
|
println!(
|
||||||
|
"{} (#{}) {:.1}",
|
||||||
|
similar_chat.name,
|
||||||
|
similar_chat_id,
|
||||||
|
100.0 * metric
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{} contacts\nLocation streaming: {}",
|
"Location streaming: {}",
|
||||||
contacts.len(),
|
|
||||||
location::is_sending_locations_to_chat(
|
location::is_sending_locations_to_chat(
|
||||||
&context,
|
&context,
|
||||||
Some(sel_chat.as_ref().unwrap().get_id())
|
Some(sel_chat.as_ref().unwrap().get_id())
|
||||||
@@ -878,7 +889,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let latitude = arg1.parse()?;
|
let latitude = arg1.parse()?;
|
||||||
let longitude = arg2.parse()?;
|
let longitude = arg2.parse()?;
|
||||||
|
|
||||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
|
||||||
if continue_streaming {
|
if continue_streaming {
|
||||||
println!("Success, streaming should be continued.");
|
println!("Success, streaming should be continued.");
|
||||||
} else {
|
} else {
|
||||||
@@ -912,9 +923,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
Viewtype::File
|
Viewtype::File
|
||||||
});
|
});
|
||||||
msg.set_file(arg1, None);
|
msg.set_file(arg1, None);
|
||||||
if !arg2.is_empty() {
|
msg.set_text(arg2.to_string());
|
||||||
msg.set_text(Some(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?;
|
||||||
}
|
}
|
||||||
"sendhtml" => {
|
"sendhtml" => {
|
||||||
@@ -926,11 +935,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_html(Some(html.to_string()));
|
msg.set_html(Some(html.to_string()));
|
||||||
msg.set_text(Some(if arg2.is_empty() {
|
msg.set_text(if arg2.is_empty() {
|
||||||
path.file_name().unwrap().to_string_lossy().to_string()
|
path.file_name().unwrap().to_string_lossy().to_string()
|
||||||
} else {
|
} else {
|
||||||
arg2.to_string()
|
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?;
|
||||||
}
|
}
|
||||||
"sendsyncmsg" => match context.send_sync_msg().await? {
|
"sendsyncmsg" => match context.send_sync_msg().await? {
|
||||||
@@ -979,7 +988,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
if !arg1.is_empty() {
|
if !arg1.is_empty() {
|
||||||
let mut draft = Message::new(Viewtype::Text);
|
let mut draft = Message::new(Viewtype::Text);
|
||||||
draft.set_text(Some(arg1.to_string()));
|
draft.set_text(arg1.to_string());
|
||||||
sel_chat
|
sel_chat
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1003,7 +1012,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"Please specify text to add as device message."
|
"Please specify text to add as device message."
|
||||||
);
|
);
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_text(Some(arg1.to_string()));
|
msg.set_text(arg1.to_string());
|
||||||
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||||
}
|
}
|
||||||
"listmedia" => {
|
"listmedia" => {
|
||||||
@@ -1058,20 +1067,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
};
|
};
|
||||||
chat::set_muted(&context, chat_id, duration).await?;
|
chat::set_muted(&context, chat_id, duration).await?;
|
||||||
}
|
}
|
||||||
"protect" | "unprotect" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
|
||||||
let chat_id = ChatId::new(arg1.parse()?);
|
|
||||||
chat_id
|
|
||||||
.set_protection(
|
|
||||||
&context,
|
|
||||||
match arg0 {
|
|
||||||
"protect" => ProtectionStatus::Protected,
|
|
||||||
"unprotect" => ProtectionStatus::Unprotected,
|
|
||||||
_ => unreachable!("arg0={:?}", arg0),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
"delchat" => {
|
"delchat" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||||
let chat_id = ChatId::new(arg1.parse()?);
|
let chat_id = ChatId::new(arg1.parse()?);
|
||||||
@@ -1090,7 +1085,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"msginfo" => {
|
"msginfo" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||||
let id = MsgId::new(arg1.parse()?);
|
let id = MsgId::new(arg1.parse()?);
|
||||||
let res = message::get_msg_info(&context, id).await?;
|
let res = id.get_info(&context).await?;
|
||||||
println!("{res}");
|
println!("{res}");
|
||||||
}
|
}
|
||||||
"download" => {
|
"download" => {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ extern crate deltachat;
|
|||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use ansi_term::Color;
|
use ansi_term::Color;
|
||||||
@@ -20,8 +19,7 @@ use deltachat::context::*;
|
|||||||
use deltachat::oauth2::*;
|
use deltachat::oauth2::*;
|
||||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||||
use deltachat::securejoin::*;
|
use deltachat::securejoin::*;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::EventType;
|
||||||
use deltachat::{EventType, Events};
|
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
@@ -299,8 +297,8 @@ impl Highlighter for DcHelper {
|
|||||||
self.highlighter.highlight(line, pos)
|
self.highlighter.highlight(line, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_char(&self, line: &str, pos: usize) -> bool {
|
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
|
||||||
self.highlighter.highlight_char(line, pos)
|
self.highlighter.highlight_char(line, pos, forced)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +310,10 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
println!("Error: Bad arguments, expected [db-name].");
|
println!("Error: Bad arguments, expected [db-name].");
|
||||||
bail!("No db-name specified");
|
bail!("No db-name specified");
|
||||||
}
|
}
|
||||||
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
|
let context = ContextBuilder::new(args[1].clone().into())
|
||||||
|
.with_id(1)
|
||||||
|
.open()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let events = context.get_event_emitter();
|
let events = context.get_event_emitter();
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
@@ -481,7 +482,10 @@ async fn handle_cmd(
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Error> {
|
async fn main() -> Result<(), Error> {
|
||||||
let _ = pretty_env_logger::try_init();
|
pretty_env_logger::formatted_timed_builder()
|
||||||
|
.parse_default_env()
|
||||||
|
.filter_module("deltachat_repl", log::LevelFilter::Info)
|
||||||
|
.init();
|
||||||
|
|
||||||
let args = std::env::args().collect();
|
let args = std::env::args().collect();
|
||||||
start(args).await?;
|
start(args).await?;
|
||||||
|
|||||||
373
deltachat-rpc-client/LICENSE
Normal file
373
deltachat-rpc-client/LICENSE
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -5,13 +5,27 @@ and provides asynchronous interface to it.
|
|||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`
|
||||||
|
or download a prebuilt release.
|
||||||
Install it anywhere in your `PATH`.
|
Install it anywhere in your `PATH`.
|
||||||
|
|
||||||
|
[Create a virtual environment](https://docs.python.org/3/library/venv.html)
|
||||||
|
if you don't have one already and activate it.
|
||||||
|
```
|
||||||
|
$ python -m venv env
|
||||||
|
$ . env/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Install `deltachat-rpc-client` from source:
|
||||||
|
```
|
||||||
|
$ cd deltachat-rpc-client
|
||||||
|
$ pip install .
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||||
2. Run `PATH="../target/debug:$PATH" tox`.
|
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||||
|
|
||||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||||
|
|
||||||
@@ -23,19 +37,14 @@ $ tox --devenv env
|
|||||||
$ . env/bin/activate
|
$ . env/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
It is recommended to use IPython, because it supports using `await` directly
|
|
||||||
from the REPL.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$ pip install ipython
|
$ python
|
||||||
$ PATH="../target/debug:$PATH" ipython
|
>>> from deltachat_rpc_client import *
|
||||||
...
|
>>> rpc = Rpc()
|
||||||
In [1]: from deltachat_rpc_client import *
|
>>> rpc.start()
|
||||||
In [2]: rpc = Rpc()
|
>>> dc = DeltaChat(rpc)
|
||||||
In [3]: await rpc.start()
|
>>> system_info = dc.get_system_info()
|
||||||
In [4]: dc = DeltaChat(rpc)
|
>>> system_info["level"]
|
||||||
In [5]: system_info = await dc.get_system_info()
|
'awesome'
|
||||||
In [6]: system_info["level"]
|
>>> rpc.close()
|
||||||
Out[6]: 'awesome'
|
|
||||||
In [7]: await rpc.close()
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
||||||
Pass --help to the CLI to see available options.
|
Pass --help to the CLI to see available options.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from deltachat_rpc_client import events, run_bot_cli
|
from deltachat_rpc_client import events, run_bot_cli
|
||||||
|
|
||||||
@@ -12,15 +11,15 @@ hooks = events.HookCollection()
|
|||||||
|
|
||||||
|
|
||||||
@hooks.on(events.RawEvent)
|
@hooks.on(events.RawEvent)
|
||||||
async def log_event(event):
|
def log_event(event):
|
||||||
print(event)
|
print(event)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.NewMessage)
|
@hooks.on(events.NewMessage)
|
||||||
async def echo(event):
|
def echo(event):
|
||||||
snapshot = event.message_snapshot
|
snapshot = event.message_snapshot
|
||||||
await snapshot.chat.send_text(snapshot.text)
|
snapshot.chat.send_text(snapshot.text)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(run_bot_cli(hooks))
|
run_bot_cli(hooks)
|
||||||
|
|||||||
45
deltachat-rpc-client/examples/echobot_advanced.py
Normal file → Executable file
45
deltachat-rpc-client/examples/echobot_advanced.py
Normal file → Executable file
@@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
it will echo back any message that has non-empty text and also supports the /help command.
|
it will echo back any message that has non-empty text and also supports the /help command.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
||||||
|
|
||||||
@@ -13,62 +14,62 @@ hooks = events.HookCollection()
|
|||||||
|
|
||||||
|
|
||||||
@hooks.on(events.RawEvent)
|
@hooks.on(events.RawEvent)
|
||||||
async def log_event(event):
|
def log_event(event):
|
||||||
if event.type == EventType.INFO:
|
if event.kind == EventType.INFO:
|
||||||
logging.info(event.msg)
|
logging.info(event.msg)
|
||||||
elif event.type == EventType.WARNING:
|
elif event.kind == EventType.WARNING:
|
||||||
logging.warning(event.msg)
|
logging.warning(event.msg)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.RawEvent(EventType.ERROR))
|
@hooks.on(events.RawEvent(EventType.ERROR))
|
||||||
async def log_error(event):
|
def log_error(event):
|
||||||
logging.error(event.msg)
|
logging.error(event.msg)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.MemberListChanged)
|
@hooks.on(events.MemberListChanged)
|
||||||
async def on_memberlist_changed(event):
|
def on_memberlist_changed(event):
|
||||||
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.GroupImageChanged)
|
@hooks.on(events.GroupImageChanged)
|
||||||
async def on_group_image_changed(event):
|
def on_group_image_changed(event):
|
||||||
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
|
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.GroupNameChanged)
|
@hooks.on(events.GroupNameChanged)
|
||||||
async def on_group_name_changed(event):
|
def on_group_name_changed(event):
|
||||||
logging.info("group name changed, old name: %s", event.old_name)
|
logging.info("group name changed, old name: %s", event.old_name)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.NewMessage(func=lambda e: not e.command))
|
@hooks.on(events.NewMessage(func=lambda e: not e.command))
|
||||||
async def echo(event):
|
def echo(event):
|
||||||
snapshot = event.message_snapshot
|
snapshot = event.message_snapshot
|
||||||
if snapshot.text or snapshot.file:
|
if snapshot.text or snapshot.file:
|
||||||
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.NewMessage(command="/help"))
|
@hooks.on(events.NewMessage(command="/help"))
|
||||||
async def help_command(event):
|
def help_command(event):
|
||||||
snapshot = event.message_snapshot
|
snapshot = event.message_snapshot
|
||||||
await snapshot.chat.send_text("Send me any message and I will echo it back")
|
snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
def main():
|
||||||
async with Rpc() as rpc:
|
with Rpc() as rpc:
|
||||||
deltachat = DeltaChat(rpc)
|
deltachat = DeltaChat(rpc)
|
||||||
system_info = await deltachat.get_system_info()
|
system_info = deltachat.get_system_info()
|
||||||
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
|
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
|
||||||
|
|
||||||
accounts = await deltachat.get_all_accounts()
|
accounts = deltachat.get_all_accounts()
|
||||||
account = accounts[0] if accounts else await deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
|
|
||||||
bot = Bot(account, hooks)
|
bot = Bot(account, hooks)
|
||||||
if not await bot.is_configured():
|
if not bot.is_configured():
|
||||||
# Save a reference to avoid garbage collection of the task.
|
configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
|
||||||
_configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
|
configure_thread.start()
|
||||||
await bot.run_forever()
|
bot.run_forever()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
asyncio.run(main())
|
main()
|
||||||
|
|||||||
54
deltachat-rpc-client/examples/echobot_no_hooks.py
Normal file → Executable file
54
deltachat-rpc-client/examples/echobot_no_hooks.py
Normal file → Executable file
@@ -2,56 +2,56 @@
|
|||||||
"""
|
"""
|
||||||
Example echo bot without using hooks
|
Example echo bot without using hooks
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc
|
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
def main():
|
||||||
async with Rpc() as rpc:
|
with Rpc() as rpc:
|
||||||
deltachat = DeltaChat(rpc)
|
deltachat = DeltaChat(rpc)
|
||||||
system_info = await deltachat.get_system_info()
|
system_info = deltachat.get_system_info()
|
||||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||||
|
|
||||||
accounts = await deltachat.get_all_accounts()
|
accounts = deltachat.get_all_accounts()
|
||||||
account = accounts[0] if accounts else await deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
|
|
||||||
await account.set_config("bot", "1")
|
account.set_config("bot", "1")
|
||||||
if not await account.is_configured():
|
if not account.is_configured():
|
||||||
logging.info("Account is not configured, configuring")
|
logging.info("Account is not configured, configuring")
|
||||||
await account.set_config("addr", sys.argv[1])
|
account.set_config("addr", sys.argv[1])
|
||||||
await account.set_config("mail_pw", sys.argv[2])
|
account.set_config("mail_pw", sys.argv[2])
|
||||||
await account.configure()
|
account.configure()
|
||||||
logging.info("Configured")
|
logging.info("Configured")
|
||||||
else:
|
else:
|
||||||
logging.info("Account is already configured")
|
logging.info("Account is already configured")
|
||||||
await deltachat.start_io()
|
deltachat.start_io()
|
||||||
|
|
||||||
async def process_messages():
|
def process_messages():
|
||||||
for message in await account.get_fresh_messages_in_arrival_order():
|
for message in account.get_next_messages():
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
if not snapshot.is_bot and not snapshot.is_info:
|
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||||
await snapshot.chat.send_text(snapshot.text)
|
snapshot.chat.send_text(snapshot.text)
|
||||||
await snapshot.message.mark_seen()
|
snapshot.message.mark_seen()
|
||||||
|
|
||||||
# Process old messages.
|
# Process old messages.
|
||||||
await process_messages()
|
process_messages()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
if event["type"] == EventType.INFO:
|
if event["kind"] == EventType.INFO:
|
||||||
logging.info("%s", event["msg"])
|
logging.info("%s", event["msg"])
|
||||||
elif event["type"] == EventType.WARNING:
|
elif event["kind"] == EventType.WARNING:
|
||||||
logging.warning("%s", event["msg"])
|
logging.warning("%s", event["msg"])
|
||||||
elif event["type"] == EventType.ERROR:
|
elif event["kind"] == EventType.ERROR:
|
||||||
logging.error("%s", event["msg"])
|
logging.error("%s", event["msg"])
|
||||||
elif event["type"] == EventType.INCOMING_MSG:
|
elif event["kind"] == EventType.INCOMING_MSG:
|
||||||
logging.info("Got an incoming message")
|
logging.info("Got an incoming message")
|
||||||
await process_messages()
|
process_messages()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
asyncio.run(main())
|
main()
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
|
requires = ["setuptools>=45"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deltachat-rpc-client"
|
name = "deltachat-rpc-client"
|
||||||
|
version = "1.138.2"
|
||||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||||
dependencies = [
|
classifiers = [
|
||||||
"aiohttp",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"aiodns"
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
"Operating System :: MacOS :: MacOS X",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Topic :: Communications :: Chat",
|
||||||
|
"Topic :: Communications :: Email"
|
||||||
]
|
]
|
||||||
dynamic = [
|
readme = "README.md"
|
||||||
"version"
|
dependencies = [
|
||||||
|
"imap-tools",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
@@ -25,7 +37,7 @@ deltachat_rpc_client = [
|
|||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = [
|
lint.select = [
|
||||||
"E", "W", # pycodestyle
|
"E", "W", # pycodestyle
|
||||||
"F", # Pyflakes
|
"F", # Pyflakes
|
||||||
"N", # pep8-naming
|
"N", # pep8-naming
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"""Delta Chat asynchronous high-level API"""
|
"""Delta Chat JSON-RPC high-level API"""
|
||||||
|
|
||||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||||
from .account import Account
|
from .account import Account
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
from .client import Bot, Client
|
from .client import Bot, Client
|
||||||
from .const import EventType
|
from .const import EventType, SpecialContactId
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
from .deltachat import DeltaChat
|
from .deltachat import DeltaChat
|
||||||
from .message import Message
|
from .message import Message
|
||||||
@@ -19,6 +20,7 @@ __all__ = [
|
|||||||
"DeltaChat",
|
"DeltaChat",
|
||||||
"EventType",
|
"EventType",
|
||||||
"Message",
|
"Message",
|
||||||
|
"SpecialContactId",
|
||||||
"Rpc",
|
"Rpc",
|
||||||
"run_bot_cli",
|
"run_bot_cli",
|
||||||
"run_client_cli",
|
"run_client_cli",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from threading import Thread
|
||||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
|
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -43,7 +43,7 @@ class AttrDict(dict):
|
|||||||
super().__setattr__(attr, val)
|
super().__setattr__(attr, val)
|
||||||
|
|
||||||
|
|
||||||
async def run_client_cli(
|
def run_client_cli(
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -54,10 +54,10 @@ async def run_client_cli(
|
|||||||
"""
|
"""
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
|
||||||
await _run_cli(Client, hooks, argv, **kwargs)
|
_run_cli(Client, hooks, argv, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def run_bot_cli(
|
def run_bot_cli(
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -68,10 +68,10 @@ async def run_bot_cli(
|
|||||||
"""
|
"""
|
||||||
from .client import Bot
|
from .client import Bot
|
||||||
|
|
||||||
await _run_cli(Bot, hooks, argv, **kwargs)
|
_run_cli(Bot, hooks, argv, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def _run_cli(
|
def _run_cli(
|
||||||
client_type: Type["Client"],
|
client_type: Type["Client"],
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
@@ -93,20 +93,24 @@ async def _run_cli(
|
|||||||
parser.add_argument("--password", action="store", help="password")
|
parser.add_argument("--password", action="store", help="password")
|
||||||
args = parser.parse_args(argv[1:])
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||||
deltachat = DeltaChat(rpc)
|
deltachat = DeltaChat(rpc)
|
||||||
core_version = (await deltachat.get_system_info()).deltachat_core_version
|
core_version = (deltachat.get_system_info()).deltachat_core_version
|
||||||
accounts = await deltachat.get_all_accounts()
|
accounts = deltachat.get_all_accounts()
|
||||||
account = accounts[0] if accounts else await deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
|
|
||||||
client = client_type(account, hooks)
|
client = client_type(account, hooks)
|
||||||
client.logger.debug("Running deltachat core %s", core_version)
|
client.logger.debug("Running deltachat core %s", core_version)
|
||||||
if not await client.is_configured():
|
if not client.is_configured():
|
||||||
assert args.email, "Account is not configured and email must be provided"
|
assert args.email, "Account is not configured and email must be provided"
|
||||||
assert args.password, "Account is not configured and password must be provided"
|
assert args.password, "Account is not configured and password must be provided"
|
||||||
# Save a reference to avoid garbage collection of the task.
|
configure_thread = Thread(
|
||||||
_configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
|
target=client.configure,
|
||||||
await client.run_forever()
|
daemon=True,
|
||||||
|
kwargs={"email": args.email, "password": args.password},
|
||||||
|
)
|
||||||
|
configure_thread.start()
|
||||||
|
client.run_forever()
|
||||||
|
|
||||||
|
|
||||||
def extract_addr(text: str) -> str:
|
def extract_addr(text: str) -> str:
|
||||||
@@ -168,3 +172,33 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
|||||||
return "removed", addr, addr
|
return "removed", addr, addr
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class futuremethod: # noqa: N801
|
||||||
|
"""Decorator for async methods."""
|
||||||
|
|
||||||
|
def __init__(self, func):
|
||||||
|
self._func = func
|
||||||
|
|
||||||
|
def __get__(self, instance, owner=None):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def future(*args):
|
||||||
|
generator = self._func(instance, *args)
|
||||||
|
res = next(generator)
|
||||||
|
|
||||||
|
def f():
|
||||||
|
try:
|
||||||
|
generator.send(res())
|
||||||
|
except StopIteration as e:
|
||||||
|
return e.value
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
def wrapper(*args):
|
||||||
|
f = future(*args)
|
||||||
|
return f()
|
||||||
|
|
||||||
|
wrapper.future = future
|
||||||
|
return wrapper
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from dataclasses import dataclass
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from ._utils import AttrDict, futuremethod
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
from .const import ChatlistFlag, ContactFlag, SpecialContactId
|
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
from .message import Message
|
from .message import Message
|
||||||
|
|
||||||
@@ -23,63 +26,82 @@ class Account:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.manager.rpc
|
return self.manager.rpc
|
||||||
|
|
||||||
async def wait_for_event(self) -> AttrDict:
|
def wait_for_event(self) -> AttrDict:
|
||||||
"""Wait until the next event and return it."""
|
"""Wait until the next event and return it."""
|
||||||
return AttrDict(await self._rpc.wait_for_event(self.id))
|
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||||
|
|
||||||
async def remove(self) -> None:
|
def clear_all_events(self):
|
||||||
|
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||||
|
self._rpc.clear_all_events(self.id)
|
||||||
|
|
||||||
|
def remove(self) -> None:
|
||||||
"""Remove the account."""
|
"""Remove the account."""
|
||||||
await self._rpc.remove_account(self.id)
|
self._rpc.remove_account(self.id)
|
||||||
|
|
||||||
async def start_io(self) -> None:
|
def start_io(self) -> None:
|
||||||
"""Start the account I/O."""
|
"""Start the account I/O."""
|
||||||
await self._rpc.start_io(self.id)
|
self._rpc.start_io(self.id)
|
||||||
|
|
||||||
async def stop_io(self) -> None:
|
def stop_io(self) -> None:
|
||||||
"""Stop the account I/O."""
|
"""Stop the account I/O."""
|
||||||
await self._rpc.stop_io(self.id)
|
self._rpc.stop_io(self.id)
|
||||||
|
|
||||||
async def get_info(self) -> AttrDict:
|
def get_info(self) -> AttrDict:
|
||||||
"""Return dictionary of this account configuration parameters."""
|
"""Return dictionary of this account configuration parameters."""
|
||||||
return AttrDict(await self._rpc.get_info(self.id))
|
return AttrDict(self._rpc.get_info(self.id))
|
||||||
|
|
||||||
async def get_size(self) -> int:
|
def get_size(self) -> int:
|
||||||
"""Get the combined filesize of an account in bytes."""
|
"""Get the combined filesize of an account in bytes."""
|
||||||
return await self._rpc.get_account_file_size(self.id)
|
return self._rpc.get_account_file_size(self.id)
|
||||||
|
|
||||||
async def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
"""Return True if this account is configured."""
|
"""Return True if this account is configured."""
|
||||||
return await self._rpc.is_configured(self.id)
|
return self._rpc.is_configured(self.id)
|
||||||
|
|
||||||
async def set_config(self, key: str, value: Optional[str] = None) -> None:
|
def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||||
"""Set configuration value."""
|
"""Set configuration value."""
|
||||||
await self._rpc.set_config(self.id, key, value)
|
self._rpc.set_config(self.id, key, value)
|
||||||
|
|
||||||
async def get_config(self, key: str) -> Optional[str]:
|
def get_config(self, key: str) -> Optional[str]:
|
||||||
"""Get configuration value."""
|
"""Get configuration value."""
|
||||||
return await self._rpc.get_config(self.id, key)
|
return self._rpc.get_config(self.id, key)
|
||||||
|
|
||||||
async 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():
|
||||||
await self.set_config(key, value)
|
self.set_config(key, value)
|
||||||
|
|
||||||
async def set_avatar(self, img_path: Optional[str] = None) -> None:
|
def set_avatar(self, img_path: Optional[str] = None) -> None:
|
||||||
"""Set self avatar.
|
"""Set self avatar.
|
||||||
|
|
||||||
Passing None will discard the currently set avatar.
|
Passing None will discard the currently set avatar.
|
||||||
"""
|
"""
|
||||||
await self.set_config("selfavatar", img_path)
|
self.set_config("selfavatar", img_path)
|
||||||
|
|
||||||
async def get_avatar(self) -> Optional[str]:
|
def get_avatar(self) -> Optional[str]:
|
||||||
"""Get self avatar."""
|
"""Get self avatar."""
|
||||||
return await self.get_config("selfavatar")
|
return self.get_config("selfavatar")
|
||||||
|
|
||||||
async def configure(self) -> None:
|
def check_qr(self, qr):
|
||||||
|
return self._rpc.check_qr(self.id, qr)
|
||||||
|
|
||||||
|
def set_config_from_qr(self, qr: str):
|
||||||
|
self._rpc.set_config_from_qr(self.id, qr)
|
||||||
|
|
||||||
|
@futuremethod
|
||||||
|
def configure(self):
|
||||||
"""Configure an account."""
|
"""Configure an account."""
|
||||||
await self._rpc.configure(self.id)
|
yield self._rpc.configure.future(self.id)
|
||||||
|
|
||||||
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
def bring_online(self):
|
||||||
|
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||||
|
self.start_io()
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||||
|
break
|
||||||
|
|
||||||
|
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||||
"""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
|
||||||
@@ -93,30 +115,49 @@ class Account:
|
|||||||
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 = (await obj.get_snapshot()).address
|
obj = obj.get_snapshot().address
|
||||||
return Contact(self, await self._rpc.create_contact(self.id, obj, name))
|
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||||
|
|
||||||
|
def create_chat(self, account: "Account") -> Chat:
|
||||||
|
addr = account.get_config("addr")
|
||||||
|
contact = self.create_contact(addr)
|
||||||
|
return contact.create_chat()
|
||||||
|
|
||||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||||
"""Return Contact instance for the given contact ID."""
|
"""Return Contact instance for the given contact ID."""
|
||||||
return Contact(self, contact_id)
|
return Contact(self, contact_id)
|
||||||
|
|
||||||
async 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."""
|
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||||
contact_id = await 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)
|
||||||
|
|
||||||
async def get_blocked_contacts(self) -> List[AttrDict]:
|
def get_blocked_contacts(self) -> list[AttrDict]:
|
||||||
"""Return a list with snapshots of all blocked contacts."""
|
"""Return a list with snapshots of all blocked contacts."""
|
||||||
contacts = await self._rpc.get_blocked_contacts(self.id)
|
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||||
|
|
||||||
async def get_contacts(
|
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
||||||
|
"""Return 1:1 chat for a contact if it exists."""
|
||||||
|
if isinstance(contact, Contact):
|
||||||
|
assert contact.account == self
|
||||||
|
contact_id = contact.id
|
||||||
|
elif isinstance(contact, int):
|
||||||
|
contact_id = contact
|
||||||
|
else:
|
||||||
|
raise ValueError(f"{contact!r} is not a contact")
|
||||||
|
chat_id = self._rpc.get_chat_id_by_contact_id(self.id, contact_id)
|
||||||
|
if chat_id:
|
||||||
|
return Chat(self, chat_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_contacts(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
with_self: bool = False,
|
with_self: bool = False,
|
||||||
verified_only: bool = False,
|
verified_only: bool = False,
|
||||||
snapshot: bool = False,
|
snapshot: bool = False,
|
||||||
) -> Union[List[Contact], List[AttrDict]]:
|
) -> Union[list[Contact], list[AttrDict]]:
|
||||||
"""Get a filtered list of contacts.
|
"""Get a filtered list of contacts.
|
||||||
|
|
||||||
:param query: if a string is specified, only return contacts
|
:param query: if a string is specified, only return contacts
|
||||||
@@ -132,9 +173,9 @@ class Account:
|
|||||||
flags |= ContactFlag.ADD_SELF
|
flags |= ContactFlag.ADD_SELF
|
||||||
|
|
||||||
if snapshot:
|
if snapshot:
|
||||||
contacts = await self._rpc.get_contacts(self.id, flags, query)
|
contacts = self._rpc.get_contacts(self.id, flags, query)
|
||||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||||
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
|
contacts = self._rpc.get_contact_ids(self.id, flags, query)
|
||||||
return [Contact(self, contact_id) for contact_id in contacts]
|
return [Contact(self, contact_id) for contact_id in contacts]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -142,7 +183,7 @@ class Account:
|
|||||||
"""This account's identity as a Contact."""
|
"""This account's identity as a Contact."""
|
||||||
return Contact(self, SpecialContactId.SELF)
|
return Contact(self, SpecialContactId.SELF)
|
||||||
|
|
||||||
async def get_chatlist(
|
def get_chatlist(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
contact: Optional[Contact] = None,
|
contact: Optional[Contact] = None,
|
||||||
@@ -151,7 +192,7 @@ class Account:
|
|||||||
no_specials: bool = False,
|
no_specials: bool = False,
|
||||||
alldone_hint: bool = False,
|
alldone_hint: bool = False,
|
||||||
snapshot: bool = False,
|
snapshot: bool = False,
|
||||||
) -> Union[List[Chat], List[AttrDict]]:
|
) -> Union[list[Chat], list[AttrDict]]:
|
||||||
"""Return list of chats.
|
"""Return list of chats.
|
||||||
|
|
||||||
:param query: if a string is specified only chats matching this query are returned.
|
:param query: if a string is specified only chats matching this query are returned.
|
||||||
@@ -174,72 +215,130 @@ class Account:
|
|||||||
if alldone_hint:
|
if alldone_hint:
|
||||||
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
||||||
|
|
||||||
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||||
if not snapshot:
|
if not snapshot:
|
||||||
return [Chat(self, entry[0]) for entry in entries]
|
return [Chat(self, entry) for entry in entries]
|
||||||
|
|
||||||
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||||
chats = []
|
chats = []
|
||||||
for item in items.values():
|
for item in items.values():
|
||||||
item["chat"] = Chat(self, item["id"])
|
item["chat"] = Chat(self, item["id"])
|
||||||
chats.append(AttrDict(item))
|
chats.append(AttrDict(item))
|
||||||
return chats
|
return chats
|
||||||
|
|
||||||
async 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 and is in unpromoted state.
|
||||||
"""
|
"""
|
||||||
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
|
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
async 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.
|
||||||
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||||
See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
|
See https://securejoin.delta.chat/ for protocol details.
|
||||||
|
|
||||||
:param qrdata: The text of the scanned QR code.
|
:param qrdata: The text of the scanned QR code.
|
||||||
"""
|
"""
|
||||||
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
|
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||||
|
|
||||||
async def get_qr_code(self) -> Tuple[str, str]:
|
def get_qr_code(self) -> tuple[str, str]:
|
||||||
"""Get Setup-Contact QR Code text and SVG data.
|
"""Get Setup-Contact QR Code text and SVG data.
|
||||||
|
|
||||||
this data needs to be transferred to another Delta Chat account
|
this data needs to be transferred to another Delta Chat account
|
||||||
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
||||||
"""
|
"""
|
||||||
return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||||
|
|
||||||
def get_message_by_id(self, msg_id: int) -> Message:
|
def get_message_by_id(self, msg_id: int) -> Message:
|
||||||
"""Return the Message instance with the given ID."""
|
"""Return the Message instance with the given ID."""
|
||||||
return Message(self, msg_id)
|
return Message(self, msg_id)
|
||||||
|
|
||||||
async def mark_seen_messages(self, messages: List[Message]) -> None:
|
def mark_seen_messages(self, messages: list[Message]) -> None:
|
||||||
"""Mark the given set of messages as seen."""
|
"""Mark the given set of messages as seen."""
|
||||||
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||||
|
|
||||||
async def delete_messages(self, messages: List[Message]) -> None:
|
def delete_messages(self, messages: list[Message]) -> None:
|
||||||
"""Delete messages (local and remote)."""
|
"""Delete messages (local and remote)."""
|
||||||
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||||
|
|
||||||
async def get_fresh_messages(self) -> List[Message]:
|
def get_fresh_messages(self) -> list[Message]:
|
||||||
"""Return the list of fresh messages, newest messages first.
|
"""Return the list of fresh messages, newest messages first.
|
||||||
|
|
||||||
This call is intended for displaying notifications.
|
This call is intended for displaying notifications.
|
||||||
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
||||||
to process oldest messages first.
|
to process oldest messages first.
|
||||||
"""
|
"""
|
||||||
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
|
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||||
|
|
||||||
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
def get_next_messages(self) -> list[Message]:
|
||||||
|
"""Return a list of next messages."""
|
||||||
|
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||||
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
|
def wait_next_messages(self) -> list[Message]:
|
||||||
|
"""Wait for new messages and return a list of them."""
|
||||||
|
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||||
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
|
def wait_for_incoming_msg_event(self):
|
||||||
|
"""Wait for incoming message event and return it."""
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
|
return event
|
||||||
|
|
||||||
|
def wait_for_securejoin_inviter_success(self):
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||||
|
break
|
||||||
|
|
||||||
|
def wait_for_securejoin_joiner_success(self):
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||||
|
break
|
||||||
|
|
||||||
|
def wait_for_reactions_changed(self):
|
||||||
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
|
if event.kind == EventType.REACTIONS_CHANGED:
|
||||||
|
return event
|
||||||
|
|
||||||
|
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
|
warn(
|
||||||
|
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
|
||||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||||
|
|
||||||
|
def export_backup(self, path, passphrase: str = "") -> None:
|
||||||
|
"""Export backup."""
|
||||||
|
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||||
|
|
||||||
|
def import_backup(self, path, passphrase: str = "") -> None:
|
||||||
|
"""Import backup."""
|
||||||
|
self._rpc.import_backup(self.id, str(path), passphrase)
|
||||||
|
|
||||||
|
def export_self_keys(self, path) -> None:
|
||||||
|
"""Export keys."""
|
||||||
|
passphrase = "" # Setting passphrase is currently not supported.
|
||||||
|
self._rpc.export_self_keys(self.id, str(path), passphrase)
|
||||||
|
|
||||||
|
def import_self_keys(self, path) -> None:
|
||||||
|
"""Import keys."""
|
||||||
|
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||||
|
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict
|
||||||
from .const import ChatVisibility, ViewType
|
from .const import ChatVisibility, ViewType
|
||||||
@@ -25,7 +27,7 @@ class Chat:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
async def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
"""Delete this chat and all its messages.
|
"""Delete this chat and all its messages.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@@ -33,85 +35,91 @@ class Chat:
|
|||||||
- does not delete messages on server
|
- does not delete messages on server
|
||||||
- the chat or contact is not blocked, new message will arrive
|
- the chat or contact is not blocked, new message will arrive
|
||||||
"""
|
"""
|
||||||
await self._rpc.delete_chat(self.account.id, self.id)
|
self._rpc.delete_chat(self.account.id, self.id)
|
||||||
|
|
||||||
async def block(self) -> None:
|
def block(self) -> None:
|
||||||
"""Block this chat."""
|
"""Block this chat."""
|
||||||
await self._rpc.block_chat(self.account.id, self.id)
|
self._rpc.block_chat(self.account.id, self.id)
|
||||||
|
|
||||||
async def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
"""Accept this contact request chat."""
|
"""Accept this contact request chat."""
|
||||||
await self._rpc.accept_chat(self.account.id, self.id)
|
self._rpc.accept_chat(self.account.id, self.id)
|
||||||
|
|
||||||
async def leave(self) -> None:
|
def leave(self) -> None:
|
||||||
"""Leave this chat."""
|
"""Leave this chat."""
|
||||||
await self._rpc.leave_group(self.account.id, self.id)
|
self._rpc.leave_group(self.account.id, self.id)
|
||||||
|
|
||||||
async def mute(self, duration: Optional[int] = None) -> None:
|
def mute(self, duration: Optional[int] = None) -> None:
|
||||||
"""Mute this chat, if a duration is not provided the chat is muted forever.
|
"""Mute this chat, if a duration is not provided the chat is muted forever.
|
||||||
|
|
||||||
:param duration: mute duration from now in seconds. Must be greater than zero.
|
:param duration: mute duration from now in seconds. Must be greater than zero.
|
||||||
"""
|
"""
|
||||||
if duration is not None:
|
if duration is not None:
|
||||||
assert duration > 0, "Invalid duration"
|
assert duration > 0, "Invalid duration"
|
||||||
dur: Union[str, dict] = {"Until": duration}
|
dur: dict = {"kind": "Until", "duration": duration}
|
||||||
else:
|
else:
|
||||||
dur = "Forever"
|
dur = {"kind": "Forever"}
|
||||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||||
|
|
||||||
async def unmute(self) -> None:
|
def unmute(self) -> None:
|
||||||
"""Unmute this chat."""
|
"""Unmute this chat."""
|
||||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
|
self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
|
||||||
|
|
||||||
async def pin(self) -> None:
|
def pin(self) -> None:
|
||||||
"""Pin this chat."""
|
"""Pin this chat."""
|
||||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||||
|
|
||||||
async def unpin(self) -> None:
|
def unpin(self) -> None:
|
||||||
"""Unpin this chat."""
|
"""Unpin this chat."""
|
||||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||||
|
|
||||||
async def archive(self) -> None:
|
def archive(self) -> None:
|
||||||
"""Archive this chat."""
|
"""Archive this chat."""
|
||||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||||
|
|
||||||
async def unarchive(self) -> None:
|
def unarchive(self) -> None:
|
||||||
"""Unarchive this chat."""
|
"""Unarchive this chat."""
|
||||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||||
|
|
||||||
async def set_name(self, name: str) -> None:
|
def set_name(self, name: str) -> None:
|
||||||
"""Set name of this chat."""
|
"""Set name of this chat."""
|
||||||
await self._rpc.set_chat_name(self.account.id, self.id, name)
|
self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||||
|
|
||||||
async def set_ephemeral_timer(self, timer: int) -> None:
|
def set_ephemeral_timer(self, timer: int) -> None:
|
||||||
"""Set ephemeral timer of this chat."""
|
"""Set ephemeral timer of this chat in seconds.
|
||||||
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
|
||||||
|
|
||||||
async def get_encryption_info(self) -> str:
|
0 means the timer is disabled, use 1 for immediate deletion."""
|
||||||
|
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||||
|
|
||||||
|
def get_encryption_info(self) -> str:
|
||||||
"""Return encryption info for this chat."""
|
"""Return encryption info for this chat."""
|
||||||
return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||||
|
|
||||||
async def get_qr_code(self) -> Tuple[str, str]:
|
def get_qr_code(self) -> tuple[str, str]:
|
||||||
"""Get Join-Group QR code text and SVG data."""
|
"""Get Join-Group QR code text and SVG data."""
|
||||||
return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||||
|
|
||||||
async def get_basic_snapshot(self) -> AttrDict:
|
def get_basic_snapshot(self) -> AttrDict:
|
||||||
"""Get a chat snapshot with basic info about this chat."""
|
"""Get a chat snapshot with basic info about this chat."""
|
||||||
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
|
info = self._rpc.get_basic_chat_info(self.account.id, self.id)
|
||||||
return AttrDict(chat=self, **info)
|
return AttrDict(chat=self, **info)
|
||||||
|
|
||||||
async def get_full_snapshot(self) -> AttrDict:
|
def get_full_snapshot(self) -> AttrDict:
|
||||||
"""Get a full snapshot of this chat."""
|
"""Get a full snapshot of this chat."""
|
||||||
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||||
return AttrDict(chat=self, **info)
|
return AttrDict(chat=self, **info)
|
||||||
|
|
||||||
async def send_message(
|
def can_send(self) -> bool:
|
||||||
|
"""Return true if messages can be sent to the chat."""
|
||||||
|
return self._rpc.can_send(self.account.id, self.id)
|
||||||
|
|
||||||
|
def send_message(
|
||||||
self,
|
self,
|
||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
viewtype: Optional[ViewType] = None,
|
viewtype: Optional[ViewType] = None,
|
||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
location: Optional[Tuple[float, float]] = None,
|
location: Optional[tuple[float, float]] = None,
|
||||||
override_sender_name: Optional[str] = None,
|
override_sender_name: Optional[str] = None,
|
||||||
quoted_msg: Optional[Union[int, Message]] = None,
|
quoted_msg: Optional[Union[int, Message]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
@@ -126,49 +134,54 @@ class Chat:
|
|||||||
"file": file,
|
"file": file,
|
||||||
"location": location,
|
"location": location,
|
||||||
"overrideSenderName": override_sender_name,
|
"overrideSenderName": override_sender_name,
|
||||||
"quotedMsg": quoted_msg,
|
"quotedMessageId": quoted_msg,
|
||||||
}
|
}
|
||||||
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
|
msg_id = self._rpc.send_msg(self.account.id, self.id, draft)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
async def send_text(self, text: str) -> Message:
|
def send_text(self, text: str) -> Message:
|
||||||
"""Send a text message and return the resulting Message instance."""
|
"""Send a text message and return the resulting Message instance."""
|
||||||
msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
async def send_videochat_invitation(self) -> Message:
|
def send_file(self, path):
|
||||||
|
"""Send a file and return the resulting Message instance."""
|
||||||
|
return self.send_message(file=path)
|
||||||
|
|
||||||
|
def send_videochat_invitation(self) -> Message:
|
||||||
"""Send a videochat invitation and return the resulting Message instance."""
|
"""Send a videochat invitation and return the resulting Message instance."""
|
||||||
msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
|
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
async def send_sticker(self, path: str) -> Message:
|
def send_sticker(self, path: str) -> Message:
|
||||||
"""Send an sticker and return the resulting Message instance."""
|
"""Send an sticker and return the resulting Message instance."""
|
||||||
msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
|
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
async def forward_messages(self, messages: List[Message]) -> None:
|
def forward_messages(self, messages: list[Message]) -> None:
|
||||||
"""Forward a list of messages to this chat."""
|
"""Forward a list of messages to this chat."""
|
||||||
msg_ids = [msg.id for msg in messages]
|
msg_ids = [msg.id for msg in messages]
|
||||||
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||||
|
|
||||||
async def set_draft(
|
def set_draft(
|
||||||
self,
|
self,
|
||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
quoted_msg: Optional[int] = None,
|
quoted_msg: Optional[int] = None,
|
||||||
|
viewtype: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set draft message."""
|
"""Set draft message."""
|
||||||
if isinstance(quoted_msg, Message):
|
if isinstance(quoted_msg, Message):
|
||||||
quoted_msg = quoted_msg.id
|
quoted_msg = quoted_msg.id
|
||||||
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype)
|
||||||
|
|
||||||
async def remove_draft(self) -> None:
|
def remove_draft(self) -> None:
|
||||||
"""Remove draft message."""
|
"""Remove draft message."""
|
||||||
await self._rpc.remove_draft(self.account.id, self.id)
|
self._rpc.remove_draft(self.account.id, self.id)
|
||||||
|
|
||||||
async def get_draft(self) -> Optional[AttrDict]:
|
def get_draft(self) -> Optional[AttrDict]:
|
||||||
"""Get draft message."""
|
"""Get draft message."""
|
||||||
snapshot = await self._rpc.get_draft(self.account.id, self.id)
|
snapshot = self._rpc.get_draft(self.account.id, self.id)
|
||||||
if not snapshot:
|
if not snapshot:
|
||||||
return None
|
return None
|
||||||
snapshot = AttrDict(snapshot)
|
snapshot = AttrDict(snapshot)
|
||||||
@@ -177,74 +190,74 @@ class Chat:
|
|||||||
snapshot["message"] = Message(self.account, snapshot.id)
|
snapshot["message"] = Message(self.account, snapshot.id)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
async 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 = await 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]
|
||||||
|
|
||||||
async 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 await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||||
|
|
||||||
async def mark_noticed(self) -> None:
|
def mark_noticed(self) -> None:
|
||||||
"""Mark all messages in this chat as noticed."""
|
"""Mark all messages in this chat as noticed."""
|
||||||
await self._rpc.marknoticed_chat(self.account.id, self.id)
|
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||||
|
|
||||||
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||||
"""Add contacts to this group."""
|
"""Add contacts to this group."""
|
||||||
for cnt in contact:
|
for cnt in contact:
|
||||||
if isinstance(cnt, str):
|
if isinstance(cnt, str):
|
||||||
contact_id = (await self.account.create_contact(cnt)).id
|
contact_id = self.account.create_contact(cnt).id
|
||||||
elif not isinstance(cnt, int):
|
elif not isinstance(cnt, int):
|
||||||
contact_id = cnt.id
|
contact_id = cnt.id
|
||||||
else:
|
else:
|
||||||
contact_id = cnt
|
contact_id = cnt
|
||||||
await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||||
|
|
||||||
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||||
"""Remove members from this group."""
|
"""Remove members from this group."""
|
||||||
for cnt in contact:
|
for cnt in contact:
|
||||||
if isinstance(cnt, str):
|
if isinstance(cnt, str):
|
||||||
contact_id = (await self.account.create_contact(cnt)).id
|
contact_id = self.account.create_contact(cnt).id
|
||||||
elif not isinstance(cnt, int):
|
elif not isinstance(cnt, int):
|
||||||
contact_id = cnt.id
|
contact_id = cnt.id
|
||||||
else:
|
else:
|
||||||
contact_id = cnt
|
contact_id = cnt
|
||||||
await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||||
|
|
||||||
async def get_contacts(self) -> List[Contact]:
|
def get_contacts(self) -> list[Contact]:
|
||||||
"""Get the contacts belonging to this chat.
|
"""Get the contacts belonging to this chat.
|
||||||
|
|
||||||
For single/direct chats self-address is not included.
|
For single/direct chats self-address is not included.
|
||||||
"""
|
"""
|
||||||
contacts = await 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]
|
||||||
|
|
||||||
async 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.
|
||||||
|
|
||||||
:param path: Full path of the image to use as the group image.
|
:param path: Full path of the image to use as the group image.
|
||||||
"""
|
"""
|
||||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
||||||
|
|
||||||
async def remove_image(self) -> None:
|
def remove_image(self) -> None:
|
||||||
"""Remove profile image of this chat."""
|
"""Remove profile image of this chat."""
|
||||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||||
|
|
||||||
async def get_locations(
|
def get_locations(
|
||||||
self,
|
self,
|
||||||
contact: Optional[Contact] = None,
|
contact: Optional[Contact] = None,
|
||||||
timestamp_from: Optional["datetime"] = None,
|
timestamp_from: Optional["datetime"] = None,
|
||||||
timestamp_to: Optional["datetime"] = None,
|
timestamp_to: Optional["datetime"] = None,
|
||||||
) -> List[AttrDict]:
|
) -> list[AttrDict]:
|
||||||
"""Get list of location snapshots for the given contact in the given timespan."""
|
"""Get list of location snapshots for the given contact in the given timespan."""
|
||||||
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||||
contact_id = contact.id if contact else 0
|
contact_id = contact.id if contact else 0
|
||||||
|
|
||||||
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||||
locations = []
|
locations = []
|
||||||
contacts: Dict[int, Contact] = {}
|
contacts: dict[int, Contact] = {}
|
||||||
for loc in result:
|
for loc in result:
|
||||||
location = AttrDict(loc)
|
location = AttrDict(loc)
|
||||||
location["chat"] = self
|
location["chat"] = self
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
"""Event loop implementations offering high level event handling/hooking."""
|
"""Event loop implementations offering high level event handling/hooking."""
|
||||||
import inspect
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
Iterable,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
|
||||||
Tuple,
|
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
@@ -20,7 +18,7 @@ from ._utils import (
|
|||||||
parse_system_image_changed,
|
parse_system_image_changed,
|
||||||
parse_system_title_changed,
|
parse_system_title_changed,
|
||||||
)
|
)
|
||||||
from .const import COMMAND_PREFIX, EventType, SystemMessageType
|
from .const import COMMAND_PREFIX, EventType, SpecialContactId, SystemMessageType
|
||||||
from .events import (
|
from .events import (
|
||||||
EventFilter,
|
EventFilter,
|
||||||
GroupImageChanged,
|
GroupImageChanged,
|
||||||
@@ -40,16 +38,16 @@ class Client:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
account: "Account",
|
account: "Account",
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
|
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||||
logger: Optional[logging.Logger] = None,
|
logger: Optional[logging.Logger] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.account = account
|
self.account = account
|
||||||
self.logger = logger or logging
|
self.logger = logger or logging
|
||||||
self._hooks: Dict[type, Set[tuple]] = {}
|
self._hooks: dict[type, set[tuple]] = {}
|
||||||
self._should_process_messages = 0
|
self._should_process_messages = 0
|
||||||
self.add_hooks(hooks or [])
|
self.add_hooks(hooks or [])
|
||||||
|
|
||||||
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
|
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||||
for hook, event in hooks:
|
for hook, event in hooks:
|
||||||
self.add_hook(hook, event)
|
self.add_hook(hook, event)
|
||||||
|
|
||||||
@@ -78,22 +76,22 @@ class Client:
|
|||||||
)
|
)
|
||||||
self._hooks.get(type(event), set()).remove((hook, event))
|
self._hooks.get(type(event), set()).remove((hook, event))
|
||||||
|
|
||||||
async def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
return await self.account.is_configured()
|
return self.account.is_configured()
|
||||||
|
|
||||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
await self.account.set_config("addr", email)
|
self.account.set_config("addr", email)
|
||||||
await 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():
|
||||||
await self.account.set_config(key, value)
|
self.account.set_config(key, value)
|
||||||
await self.account.configure()
|
self.account.configure()
|
||||||
self.logger.debug("Account configured")
|
self.logger.debug("Account configured")
|
||||||
|
|
||||||
async def run_forever(self) -> None:
|
def run_forever(self) -> None:
|
||||||
"""Process events forever."""
|
"""Process events forever."""
|
||||||
await self.run_until(lambda _: False)
|
self.run_until(lambda _: False)
|
||||||
|
|
||||||
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||||
"""Process events until the given callable evaluates to True.
|
"""Process events until the given callable evaluates to True.
|
||||||
|
|
||||||
The callable should accept an AttrDict object representing the
|
The callable should accept an AttrDict object representing the
|
||||||
@@ -101,39 +99,37 @@ class Client:
|
|||||||
evaluates to True.
|
evaluates to True.
|
||||||
"""
|
"""
|
||||||
self.logger.debug("Listening to incoming events...")
|
self.logger.debug("Listening to incoming events...")
|
||||||
if await self.is_configured():
|
if self.is_configured():
|
||||||
await self.account.start_io()
|
self.account.start_io()
|
||||||
await self._process_messages() # Process old messages.
|
self._process_messages() # Process old messages.
|
||||||
while True:
|
while True:
|
||||||
event = await self.account.wait_for_event()
|
event = self.account.wait_for_event()
|
||||||
event["type"] = EventType(event.type)
|
event["kind"] = EventType(event.kind)
|
||||||
event["account"] = self.account
|
event["account"] = self.account
|
||||||
await self._on_event(event)
|
self._on_event(event)
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
await self._process_messages()
|
self._process_messages()
|
||||||
|
|
||||||
stop = func(event)
|
stop = func(event)
|
||||||
if inspect.isawaitable(stop):
|
|
||||||
stop = await stop
|
|
||||||
if stop:
|
if stop:
|
||||||
return event
|
return event
|
||||||
|
|
||||||
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||||
if await evfilter.filter(event):
|
if evfilter.filter(event):
|
||||||
try:
|
try:
|
||||||
await hook(event)
|
hook(event)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.logger.exception(ex)
|
self.logger.exception(ex)
|
||||||
|
|
||||||
async def _parse_command(self, event: AttrDict) -> None:
|
def _parse_command(self, event: AttrDict) -> None:
|
||||||
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
||||||
parts = event.message_snapshot.text.split(maxsplit=1)
|
parts = event.message_snapshot.text.split(maxsplit=1)
|
||||||
payload = parts[1] if len(parts) > 1 else ""
|
payload = parts[1] if len(parts) > 1 else ""
|
||||||
cmd = parts.pop(0)
|
cmd = parts.pop(0)
|
||||||
|
|
||||||
if "@" in cmd:
|
if "@" in cmd:
|
||||||
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
|
suffix = "@" + self.account.self_contact.get_snapshot().address
|
||||||
if cmd.endswith(suffix):
|
if cmd.endswith(suffix):
|
||||||
cmd = cmd[: -len(suffix)]
|
cmd = cmd[: -len(suffix)]
|
||||||
else:
|
else:
|
||||||
@@ -153,32 +149,32 @@ class Client:
|
|||||||
|
|
||||||
event["command"], event["payload"] = cmd, payload
|
event["command"], event["payload"] = cmd, payload
|
||||||
|
|
||||||
async def _on_new_msg(self, snapshot: AttrDict) -> None:
|
def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||||
event = AttrDict(command="", payload="", message_snapshot=snapshot)
|
event = AttrDict(command="", payload="", message_snapshot=snapshot)
|
||||||
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
|
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
|
||||||
await self._parse_command(event)
|
self._parse_command(event)
|
||||||
await self._on_event(event, NewMessage)
|
self._on_event(event, NewMessage)
|
||||||
|
|
||||||
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||||
event = AttrDict(message_snapshot=snapshot)
|
event = AttrDict(message_snapshot=snapshot)
|
||||||
|
|
||||||
img_changed = parse_system_image_changed(snapshot.text)
|
img_changed = parse_system_image_changed(snapshot.text)
|
||||||
if img_changed:
|
if img_changed:
|
||||||
_, event["image_deleted"] = img_changed
|
_, event["image_deleted"] = img_changed
|
||||||
await self._on_event(event, GroupImageChanged)
|
self._on_event(event, GroupImageChanged)
|
||||||
return
|
return
|
||||||
|
|
||||||
title_changed = parse_system_title_changed(snapshot.text)
|
title_changed = parse_system_title_changed(snapshot.text)
|
||||||
if title_changed:
|
if title_changed:
|
||||||
_, event["old_name"] = title_changed
|
_, event["old_name"] = title_changed
|
||||||
await self._on_event(event, GroupNameChanged)
|
self._on_event(event, GroupNameChanged)
|
||||||
return
|
return
|
||||||
|
|
||||||
members_changed = parse_system_add_remove(snapshot.text)
|
members_changed = parse_system_add_remove(snapshot.text)
|
||||||
if members_changed:
|
if members_changed:
|
||||||
action, event["member"], _ = members_changed
|
action, event["member"], _ = members_changed
|
||||||
event["member_added"] = action == "added"
|
event["member_added"] = action == "added"
|
||||||
await self._on_event(event, MemberListChanged)
|
self._on_event(event, MemberListChanged)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
@@ -187,19 +183,20 @@ class Client:
|
|||||||
snapshot.text,
|
snapshot.text,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _process_messages(self) -> None:
|
def _process_messages(self) -> None:
|
||||||
if self._should_process_messages:
|
if self._should_process_messages:
|
||||||
for message in await self.account.get_fresh_messages_in_arrival_order():
|
for message in self.account.get_next_messages():
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
await self._on_new_msg(snapshot)
|
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
|
||||||
|
self._on_new_msg(snapshot)
|
||||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||||
await self._handle_info_msg(snapshot)
|
self._handle_info_msg(snapshot)
|
||||||
await snapshot.message.mark_seen()
|
snapshot.message.mark_seen()
|
||||||
|
|
||||||
|
|
||||||
class Bot(Client):
|
class Bot(Client):
|
||||||
"""Simple bot implementation that listent to events of a single account."""
|
"""Simple bot implementation that listens to events of a single account."""
|
||||||
|
|
||||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
kwargs.setdefault("bot", "1")
|
kwargs.setdefault("bot", "1")
|
||||||
await super().configure(email, password, **kwargs)
|
super().configure(email, password, **kwargs)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class EventType(str, Enum):
|
|||||||
SMTP_MESSAGE_SENT = "SmtpMessageSent"
|
SMTP_MESSAGE_SENT = "SmtpMessageSent"
|
||||||
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
|
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
|
||||||
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
|
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
|
||||||
|
IMAP_INBOX_IDLE = "ImapInboxIdle"
|
||||||
NEW_BLOB_FILE = "NewBlobFile"
|
NEW_BLOB_FILE = "NewBlobFile"
|
||||||
DELETED_BLOB_FILE = "DeletedBlobFile"
|
DELETED_BLOB_FILE = "DeletedBlobFile"
|
||||||
WARNING = "Warning"
|
WARNING = "Warning"
|
||||||
@@ -44,6 +45,7 @@ class EventType(str, Enum):
|
|||||||
MSG_DELIVERED = "MsgDelivered"
|
MSG_DELIVERED = "MsgDelivered"
|
||||||
MSG_FAILED = "MsgFailed"
|
MSG_FAILED = "MsgFailed"
|
||||||
MSG_READ = "MsgRead"
|
MSG_READ = "MsgRead"
|
||||||
|
MSG_DELETED = "MsgDeleted"
|
||||||
CHAT_MODIFIED = "ChatModified"
|
CHAT_MODIFIED = "ChatModified"
|
||||||
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
||||||
CONTACTS_CHANGED = "ContactsChanged"
|
CONTACTS_CHANGED = "ContactsChanged"
|
||||||
@@ -57,6 +59,18 @@ class EventType(str, Enum):
|
|||||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||||
|
CHATLIST_CHANGED = "ChatlistChanged"
|
||||||
|
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||||
|
CONFIG_SYNCED = "ConfigSynced"
|
||||||
|
|
||||||
|
|
||||||
|
class ChatId(IntEnum):
|
||||||
|
"""Special chat ids"""
|
||||||
|
|
||||||
|
TRASH = 3
|
||||||
|
ARCHIVED_LINK = 6
|
||||||
|
ALLDONE_HINT = 7
|
||||||
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
class ChatType(IntEnum):
|
class ChatType(IntEnum):
|
||||||
@@ -120,3 +134,107 @@ class SystemMessageType(str, Enum):
|
|||||||
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
|
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
|
||||||
MULTI_DEVICE_SYNC = "MultiDeviceSync"
|
MULTI_DEVICE_SYNC = "MultiDeviceSync"
|
||||||
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
|
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
|
||||||
|
|
||||||
|
|
||||||
|
class MessageState(IntEnum):
|
||||||
|
"""State of the message."""
|
||||||
|
|
||||||
|
UNDEFINED = 0
|
||||||
|
IN_FRESH = 10
|
||||||
|
IN_NOTICED = 13
|
||||||
|
IN_SEEN = 16
|
||||||
|
OUT_PREPARING = 18
|
||||||
|
OUT_DRAFT = 19
|
||||||
|
OUT_PENDING = 20
|
||||||
|
OUT_FAILED = 24
|
||||||
|
OUT_DELIVERED = 26
|
||||||
|
OUT_MDN_RCVD = 28
|
||||||
|
|
||||||
|
|
||||||
|
class MessageId(IntEnum):
|
||||||
|
"""Special message ids"""
|
||||||
|
|
||||||
|
DAYMARKER = 9
|
||||||
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
|
class CertificateChecks(IntEnum):
|
||||||
|
"""Certificate checks mode"""
|
||||||
|
|
||||||
|
AUTOMATIC = 0
|
||||||
|
STRICT = 1
|
||||||
|
ACCEPT_INVALID_CERTIFICATES = 3
|
||||||
|
|
||||||
|
|
||||||
|
class Connectivity(IntEnum):
|
||||||
|
"""Connectivity states"""
|
||||||
|
|
||||||
|
NOT_CONNECTED = 1000
|
||||||
|
CONNECTING = 2000
|
||||||
|
WORKING = 3000
|
||||||
|
CONNECTED = 4000
|
||||||
|
|
||||||
|
|
||||||
|
class KeyGenType(IntEnum):
|
||||||
|
"""Type of the key to generate"""
|
||||||
|
|
||||||
|
DEFAULT = 0
|
||||||
|
RSA2048 = 1
|
||||||
|
ED25519 = 2
|
||||||
|
RSA4096 = 3
|
||||||
|
|
||||||
|
|
||||||
|
# "Lp" means "login parameters"
|
||||||
|
class LpAuthFlag(IntEnum):
|
||||||
|
"""Authorization flags"""
|
||||||
|
|
||||||
|
OAUTH2 = 0x2
|
||||||
|
NORMAL = 0x4
|
||||||
|
|
||||||
|
|
||||||
|
class MediaQuality(IntEnum):
|
||||||
|
"""Media quality setting"""
|
||||||
|
|
||||||
|
BALANCED = 0
|
||||||
|
WORSE = 1
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderStatus(IntEnum):
|
||||||
|
"""Provider status according to manual testing"""
|
||||||
|
|
||||||
|
OK = 1
|
||||||
|
PREPARATION = 2
|
||||||
|
BROKEN = 3
|
||||||
|
|
||||||
|
|
||||||
|
class PushNotifyState(IntEnum):
|
||||||
|
"""Push notifications state"""
|
||||||
|
|
||||||
|
NOT_CONNECTED = 0
|
||||||
|
HEARTBEAT = 1
|
||||||
|
CONNECTED = 2
|
||||||
|
|
||||||
|
|
||||||
|
class ShowEmails(IntEnum):
|
||||||
|
"""Show emails mode"""
|
||||||
|
|
||||||
|
OFF = 0
|
||||||
|
ACCEPTED_CONTACTS = 1
|
||||||
|
ALL = 2
|
||||||
|
|
||||||
|
|
||||||
|
class SocketSecurity(IntEnum):
|
||||||
|
"""Socket security"""
|
||||||
|
|
||||||
|
AUTOMATIC = 0
|
||||||
|
SSL = 1
|
||||||
|
STARTTLS = 2
|
||||||
|
PLAIN = 3
|
||||||
|
|
||||||
|
|
||||||
|
class VideochatType(IntEnum):
|
||||||
|
"""Video chat URL type"""
|
||||||
|
|
||||||
|
UNKNOWN = 0
|
||||||
|
BASICWEBRTC = 1
|
||||||
|
JITSI = 2
|
||||||
|
|||||||
@@ -24,39 +24,39 @@ class Contact:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
async def block(self) -> None:
|
def block(self) -> None:
|
||||||
"""Block contact."""
|
"""Block contact."""
|
||||||
await self._rpc.block_contact(self.account.id, self.id)
|
self._rpc.block_contact(self.account.id, self.id)
|
||||||
|
|
||||||
async def unblock(self) -> None:
|
def unblock(self) -> None:
|
||||||
"""Unblock contact."""
|
"""Unblock contact."""
|
||||||
await self._rpc.unblock_contact(self.account.id, self.id)
|
self._rpc.unblock_contact(self.account.id, self.id)
|
||||||
|
|
||||||
async def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
"""Delete contact."""
|
"""Delete contact."""
|
||||||
await self._rpc.delete_contact(self.account.id, self.id)
|
self._rpc.delete_contact(self.account.id, self.id)
|
||||||
|
|
||||||
async 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."""
|
||||||
await self._rpc.change_contact_name(self.account.id, self.id, name)
|
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||||
|
|
||||||
async 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, containing your fingerprint and
|
||||||
the fingerprint of the contact.
|
the fingerprint of the contact.
|
||||||
"""
|
"""
|
||||||
return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||||
|
|
||||||
async def get_snapshot(self) -> AttrDict:
|
def get_snapshot(self) -> AttrDict:
|
||||||
"""Return a dictionary with a snapshot of all contact properties."""
|
"""Return a dictionary with a snapshot of all contact properties."""
|
||||||
snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
|
snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id))
|
||||||
snapshot["contact"] = self
|
snapshot["contact"] = self
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
async def create_chat(self) -> "Chat":
|
def create_chat(self) -> "Chat":
|
||||||
"""Create or get an existing 1:1 chat for this contact."""
|
"""Create or get an existing 1:1 chat for this contact."""
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
|
|
||||||
return Chat(
|
return Chat(
|
||||||
self.account,
|
self.account,
|
||||||
await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from typing import TYPE_CHECKING, Dict, List
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict
|
||||||
from .account import Account
|
from .account import Account
|
||||||
@@ -16,34 +18,34 @@ class DeltaChat:
|
|||||||
def __init__(self, rpc: "Rpc") -> None:
|
def __init__(self, rpc: "Rpc") -> None:
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
|
|
||||||
async def add_account(self) -> Account:
|
def add_account(self) -> Account:
|
||||||
"""Create a new account database."""
|
"""Create a new account database."""
|
||||||
account_id = await self.rpc.add_account()
|
account_id = self.rpc.add_account()
|
||||||
return Account(self, account_id)
|
return Account(self, account_id)
|
||||||
|
|
||||||
async def get_all_accounts(self) -> List[Account]:
|
def get_all_accounts(self) -> list[Account]:
|
||||||
"""Return a list of all available accounts."""
|
"""Return a list of all available accounts."""
|
||||||
account_ids = await self.rpc.get_all_account_ids()
|
account_ids = self.rpc.get_all_account_ids()
|
||||||
return [Account(self, account_id) for account_id in account_ids]
|
return [Account(self, account_id) for account_id in account_ids]
|
||||||
|
|
||||||
async def start_io(self) -> None:
|
def start_io(self) -> None:
|
||||||
"""Start the I/O of all accounts."""
|
"""Start the I/O of all accounts."""
|
||||||
await self.rpc.start_io_for_all_accounts()
|
self.rpc.start_io_for_all_accounts()
|
||||||
|
|
||||||
async def stop_io(self) -> None:
|
def stop_io(self) -> None:
|
||||||
"""Stop the I/O of all accounts."""
|
"""Stop the I/O of all accounts."""
|
||||||
await self.rpc.stop_io_for_all_accounts()
|
self.rpc.stop_io_for_all_accounts()
|
||||||
|
|
||||||
async 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 likely has come back or just that the network
|
||||||
conditions might have changed.
|
conditions might have changed.
|
||||||
"""
|
"""
|
||||||
await self.rpc.maybe_network()
|
self.rpc.maybe_network()
|
||||||
|
|
||||||
async def get_system_info(self) -> AttrDict:
|
def get_system_info(self) -> AttrDict:
|
||||||
"""Get information about the Delta Chat core in this system."""
|
"""Get information about the Delta Chat core in this system."""
|
||||||
return AttrDict(await self.rpc.get_system_info())
|
return AttrDict(self.rpc.get_system_info())
|
||||||
|
|
||||||
async def set_translations(self, translations: Dict[str, str]) -> None:
|
def set_translations(self, translations: dict[str, str]) -> None:
|
||||||
"""Set stock translation strings."""
|
"""Set stock translation strings."""
|
||||||
await self.rpc.set_stock_strings(translations)
|
self.rpc.set_stock_strings(translations)
|
||||||
|
|||||||
226
deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py
Normal file
226
deltachat-rpc-client/src/deltachat_rpc_client/direct_imap.py
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
Internal Python-level IMAP handling used by the tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import imaplib
|
||||||
|
import io
|
||||||
|
import pathlib
|
||||||
|
import ssl
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from imap_tools import (
|
||||||
|
AND,
|
||||||
|
Header,
|
||||||
|
MailBox,
|
||||||
|
MailBoxTls,
|
||||||
|
MailMessage,
|
||||||
|
MailMessageFlags,
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import Account, const
|
||||||
|
|
||||||
|
FLAGS = b"FLAGS"
|
||||||
|
FETCH = b"FETCH"
|
||||||
|
ALL = "1:*"
|
||||||
|
|
||||||
|
|
||||||
|
class DirectImap:
|
||||||
|
def __init__(self, account: Account) -> None:
|
||||||
|
self.account = account
|
||||||
|
self.logid = account.get_config("displayname") or id(account)
|
||||||
|
self._idling = False
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
host = self.account.get_config("configured_mail_server")
|
||||||
|
port = int(self.account.get_config("configured_mail_port"))
|
||||||
|
security = int(self.account.get_config("configured_mail_security"))
|
||||||
|
|
||||||
|
user = self.account.get_config("addr")
|
||||||
|
pw = self.account.get_config("mail_pw")
|
||||||
|
|
||||||
|
if security == const.SocketSecurity.PLAIN:
|
||||||
|
ssl_context = None
|
||||||
|
else:
|
||||||
|
ssl_context = ssl.create_default_context()
|
||||||
|
|
||||||
|
# don't check if certificate hostname doesn't match target hostname
|
||||||
|
ssl_context.check_hostname = False
|
||||||
|
|
||||||
|
# don't check if the certificate is trusted by a certificate authority
|
||||||
|
ssl_context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
if security == const.SocketSecurity.STARTTLS:
|
||||||
|
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||||
|
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
|
||||||
|
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||||
|
self.conn.login(user, pw)
|
||||||
|
|
||||||
|
self.select_folder("INBOX")
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
try:
|
||||||
|
self.conn.logout()
|
||||||
|
except (OSError, imaplib.IMAP4.abort):
|
||||||
|
print("Could not logout direct_imap conn")
|
||||||
|
|
||||||
|
def create_folder(self, foldername):
|
||||||
|
try:
|
||||||
|
self.conn.folder.create(foldername)
|
||||||
|
except errors.MailboxFolderCreateError as e:
|
||||||
|
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||||
|
|
||||||
|
def select_folder(self, foldername: str) -> tuple:
|
||||||
|
assert not self._idling
|
||||||
|
return self.conn.folder.set(foldername)
|
||||||
|
|
||||||
|
def select_config_folder(self, config_name: str):
|
||||||
|
"""Return info about selected folder if it is
|
||||||
|
configured, otherwise None.
|
||||||
|
"""
|
||||||
|
if "_" not in config_name:
|
||||||
|
config_name = f"configured_{config_name}_folder"
|
||||||
|
foldername = self.account.get_config(config_name)
|
||||||
|
if foldername:
|
||||||
|
return self.select_folder(foldername)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_folders(self) -> list[str]:
|
||||||
|
"""return list of all existing folder names."""
|
||||||
|
assert not self._idling
|
||||||
|
return [folder.name for folder in self.conn.folder.list()]
|
||||||
|
|
||||||
|
def delete(self, uid_list: str, expunge=True):
|
||||||
|
"""delete a range of messages (imap-syntax).
|
||||||
|
If expunge is true, perform the expunge-operation
|
||||||
|
to make sure the messages are really gone and not
|
||||||
|
just flagged as deleted.
|
||||||
|
"""
|
||||||
|
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
|
||||||
|
if expunge:
|
||||||
|
self.conn.expunge()
|
||||||
|
|
||||||
|
def get_all_messages(self) -> list[MailMessage]:
|
||||||
|
assert not self._idling
|
||||||
|
return list(self.conn.fetch())
|
||||||
|
|
||||||
|
def get_unread_messages(self) -> list[str]:
|
||||||
|
assert not self._idling
|
||||||
|
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||||
|
|
||||||
|
def mark_all_read(self):
|
||||||
|
messages = self.get_unread_messages()
|
||||||
|
if messages:
|
||||||
|
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||||
|
print("marked seen:", messages, res)
|
||||||
|
|
||||||
|
def get_unread_cnt(self) -> int:
|
||||||
|
return len(self.get_unread_messages())
|
||||||
|
|
||||||
|
def dump_imap_structures(self, dir, logfile):
|
||||||
|
assert not self._idling
|
||||||
|
stream = io.StringIO()
|
||||||
|
|
||||||
|
def log(*args, **kwargs):
|
||||||
|
kwargs["file"] = stream
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
empty_folders = []
|
||||||
|
for imapfolder in self.list_folders():
|
||||||
|
self.select_folder(imapfolder)
|
||||||
|
messages = list(self.get_all_messages())
|
||||||
|
if not messages:
|
||||||
|
empty_folders.append(imapfolder)
|
||||||
|
continue
|
||||||
|
|
||||||
|
log("---------", imapfolder, len(messages), "messages ---------")
|
||||||
|
# get message content without auto-marking it as seen
|
||||||
|
# fetching 'RFC822' would mark it as seen.
|
||||||
|
for msg in self.conn.fetch(mark_seen=False):
|
||||||
|
body = getattr(msg.obj, "text", None)
|
||||||
|
if not body:
|
||||||
|
body = getattr(msg.obj, "html", None)
|
||||||
|
if not body:
|
||||||
|
log("Message", msg.uid, "has empty body")
|
||||||
|
continue
|
||||||
|
|
||||||
|
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
fn = path.joinpath(str(msg.uid))
|
||||||
|
fn.write_bytes(body)
|
||||||
|
log("Message", msg.uid, fn)
|
||||||
|
log(
|
||||||
|
"Message",
|
||||||
|
msg.uid,
|
||||||
|
msg.flags,
|
||||||
|
"Message-Id:",
|
||||||
|
msg.obj.get("Message-Id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if empty_folders:
|
||||||
|
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||||
|
|
||||||
|
print(stream.getvalue(), file=logfile)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def idle(self):
|
||||||
|
"""return Idle ContextManager."""
|
||||||
|
idle_manager = IdleManager(self)
|
||||||
|
try:
|
||||||
|
yield idle_manager
|
||||||
|
finally:
|
||||||
|
idle_manager.done()
|
||||||
|
|
||||||
|
def append(self, folder: str, msg: str):
|
||||||
|
"""Upload a message to *folder*.
|
||||||
|
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||||
|
"""
|
||||||
|
if msg.startswith("\n"):
|
||||||
|
msg = msg[1:]
|
||||||
|
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
|
||||||
|
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||||
|
|
||||||
|
def get_uid_by_message_id(self, message_id) -> str:
|
||||||
|
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||||
|
if len(msgs) == 0:
|
||||||
|
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||||
|
return msgs[0]
|
||||||
|
|
||||||
|
|
||||||
|
class IdleManager:
|
||||||
|
def __init__(self, direct_imap) -> None:
|
||||||
|
self.direct_imap = direct_imap
|
||||||
|
self.log = direct_imap.account.log
|
||||||
|
# fetch latest messages before starting idle so that it only
|
||||||
|
# returns messages that arrive anew
|
||||||
|
self.direct_imap.conn.fetch("1:*")
|
||||||
|
self.direct_imap.conn.idle.start()
|
||||||
|
|
||||||
|
def check(self, timeout=None) -> list[bytes]:
|
||||||
|
"""(blocking) wait for next idle message from server."""
|
||||||
|
self.log("imap-direct: calling idle_check")
|
||||||
|
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||||
|
self.log(f"imap-direct: idle_check returned {res!r}")
|
||||||
|
return res
|
||||||
|
|
||||||
|
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||||
|
while True:
|
||||||
|
for item in self.check(timeout=timeout):
|
||||||
|
if b"EXISTS" in item or b"RECENT" in item:
|
||||||
|
return item
|
||||||
|
|
||||||
|
def wait_for_seen(self, timeout=None) -> int:
|
||||||
|
"""Return first message with SEEN flag from a running idle-stream."""
|
||||||
|
while True:
|
||||||
|
for item in self.check(timeout=timeout):
|
||||||
|
if FETCH in item:
|
||||||
|
self.log(str(item))
|
||||||
|
if FLAGS in item and rb"\Seen" in item:
|
||||||
|
return int(item.split(b" ")[1])
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
"""send idle-done to server if we are currently in idle mode."""
|
||||||
|
return self.direct_imap.conn.idle.stop()
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
"""High-level classes for event processing and filtering."""
|
"""High-level classes for event processing and filtering."""
|
||||||
import inspect
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union
|
||||||
|
|
||||||
from .const import EventType
|
from .const import EventType
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ def _tuple_of(obj, type_: type) -> tuple:
|
|||||||
class EventFilter(ABC):
|
class EventFilter(ABC):
|
||||||
"""The base event filter.
|
"""The base event filter.
|
||||||
|
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -43,16 +45,13 @@ class EventFilter(ABC):
|
|||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self == other
|
return not self == other
|
||||||
|
|
||||||
async def _call_func(self, event) -> bool:
|
def _call_func(self, event) -> bool:
|
||||||
if not self.func:
|
if not self.func:
|
||||||
return True
|
return True
|
||||||
res = self.func(event)
|
return self.func(event)
|
||||||
if inspect.isawaitable(res):
|
|
||||||
return await res
|
|
||||||
return res
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async 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 and should be
|
||||||
used, or False-like value otherwise.
|
used, or False-like value otherwise.
|
||||||
"""
|
"""
|
||||||
@@ -62,7 +61,7 @@ class RawEvent(EventFilter):
|
|||||||
"""Matches raw core events.
|
"""Matches raw core events.
|
||||||
|
|
||||||
:param types: The types of event to match.
|
:param types: The types of event to match.
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -82,10 +81,10 @@ class RawEvent(EventFilter):
|
|||||||
return (self.types, self.func) == (other.types, other.func)
|
return (self.types, self.func) == (other.types, other.func)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
if self.types and event.type not in self.types:
|
if self.types and event.kind not in self.types:
|
||||||
return False
|
return False
|
||||||
return await self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class NewMessage(EventFilter):
|
class NewMessage(EventFilter):
|
||||||
@@ -104,7 +103,7 @@ class NewMessage(EventFilter):
|
|||||||
:param is_info: If set to True only match info/system messages, if set to False
|
: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
|
only match messages that are not info/system messages. If omitted
|
||||||
info/system messages as well as normal messages will be matched.
|
info/system messages as well as normal messages will be matched.
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -159,7 +158,7 @@ class NewMessage(EventFilter):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
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:
|
||||||
@@ -168,11 +167,9 @@ class NewMessage(EventFilter):
|
|||||||
return False
|
return False
|
||||||
if self.pattern:
|
if self.pattern:
|
||||||
match = self.pattern(event.message_snapshot.text)
|
match = self.pattern(event.message_snapshot.text)
|
||||||
if inspect.isawaitable(match):
|
|
||||||
match = await match
|
|
||||||
if not match:
|
if not match:
|
||||||
return False
|
return False
|
||||||
return await super()._call_func(event)
|
return super()._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class MemberListChanged(EventFilter):
|
class MemberListChanged(EventFilter):
|
||||||
@@ -184,7 +181,7 @@ class MemberListChanged(EventFilter):
|
|||||||
:param added: If set to True only match if a member was added, if set to False
|
:param added: If set to True only match if a member was added, if set to False
|
||||||
only match if a member was removed. If omitted both, member additions
|
only match if a member was removed. If omitted both, member additions
|
||||||
and removals, will be matched.
|
and removals, will be matched.
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -201,10 +198,10 @@ class MemberListChanged(EventFilter):
|
|||||||
return (self.added, self.func) == (other.added, other.func)
|
return (self.added, self.func) == (other.added, other.func)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
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 await self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class GroupImageChanged(EventFilter):
|
class GroupImageChanged(EventFilter):
|
||||||
@@ -216,7 +213,7 @@ class GroupImageChanged(EventFilter):
|
|||||||
:param deleted: If set to True only match if the image was deleted, if set to False
|
:param deleted: If set to True only match if the image was deleted, if set to False
|
||||||
only match if a new image was set. If omitted both, image changes and
|
only match if a new image was set. If omitted both, image changes and
|
||||||
removals, will be matched.
|
removals, will be matched.
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -233,10 +230,10 @@ class GroupImageChanged(EventFilter):
|
|||||||
return (self.deleted, self.func) == (other.deleted, other.func)
|
return (self.deleted, self.func) == (other.deleted, other.func)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
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 await self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class GroupNameChanged(EventFilter):
|
class GroupNameChanged(EventFilter):
|
||||||
@@ -245,7 +242,7 @@ class GroupNameChanged(EventFilter):
|
|||||||
Warning: registering a handler for this event will cause the messages
|
Warning: registering a handler for this event will cause the messages
|
||||||
to be marked as read. Its usage is mainly intended for bots.
|
to be marked as read. Its usage is mainly intended for bots.
|
||||||
|
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -258,8 +255,8 @@ class GroupNameChanged(EventFilter):
|
|||||||
return self.func == other.func
|
return self.func == other.func
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
return await self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class HookCollection:
|
class HookCollection:
|
||||||
@@ -268,9 +265,9 @@ class HookCollection:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
|
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
|
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]:
|
||||||
return iter(self._hooks)
|
return iter(self._hooks)
|
||||||
|
|
||||||
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict
|
||||||
|
from .const import EventType
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -21,32 +22,51 @@ class Message:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
async def send_reaction(self, *reaction: str):
|
def send_reaction(self, *reaction: str) -> "Message":
|
||||||
"""Send a reaction to this message."""
|
"""Send a reaction to this message."""
|
||||||
await self._rpc.send_reaction(self.account.id, self.id, reaction)
|
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||||
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
async def get_snapshot(self) -> AttrDict:
|
def get_snapshot(self) -> AttrDict:
|
||||||
"""Get a snapshot with the properties of this message."""
|
"""Get a snapshot with the properties of this message."""
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
|
|
||||||
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
|
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
|
||||||
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
|
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
|
||||||
snapshot["sender"] = Contact(self.account, snapshot.from_id)
|
snapshot["sender"] = Contact(self.account, snapshot.from_id)
|
||||||
snapshot["message"] = self
|
snapshot["message"] = self
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
async def mark_seen(self) -> None:
|
def get_reactions(self) -> Optional[AttrDict]:
|
||||||
"""Mark the message as seen."""
|
"""Get message reactions."""
|
||||||
await self._rpc.markseen_msgs(self.account.id, [self.id])
|
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||||
|
if reactions:
|
||||||
|
return AttrDict(reactions)
|
||||||
|
return None
|
||||||
|
|
||||||
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
def get_sender_contact(self) -> Contact:
|
||||||
|
from_id = self.get_snapshot().from_id
|
||||||
|
return self.account.get_contact_by_id(from_id)
|
||||||
|
|
||||||
|
def mark_seen(self) -> None:
|
||||||
|
"""Mark the message as seen."""
|
||||||
|
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||||
|
|
||||||
|
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):
|
||||||
update = json.dumps(update)
|
update = json.dumps(update)
|
||||||
await 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)
|
||||||
|
|
||||||
async 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 json.loads(await 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))
|
||||||
|
|
||||||
async def get_webxdc_info(self) -> dict:
|
def get_webxdc_info(self) -> dict:
|
||||||
return await self._rpc.get_webxdc_info(self.account.id, self.id)
|
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||||
|
|
||||||
|
def wait_until_delivered(self) -> None:
|
||||||
|
"""Consume events until the message is delivered."""
|
||||||
|
while True:
|
||||||
|
event = self.account.wait_for_event()
|
||||||
|
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||||
|
break
|
||||||
|
|||||||
@@ -1,66 +1,83 @@
|
|||||||
import asyncio
|
from __future__ import annotations
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from typing import AsyncGenerator, List, Optional
|
import random
|
||||||
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
import aiohttp
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||||
|
from ._utils import futuremethod
|
||||||
from .rpc import Rpc
|
from .rpc import Rpc
|
||||||
|
|
||||||
|
|
||||||
async def get_temp_credentials() -> dict:
|
def get_temp_credentials() -> dict:
|
||||||
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||||
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||||
|
password = f"{username}${username}"
|
||||||
# Replace default 5 minute timeout with a 1 minute timeout.
|
addr = f"{username}@{domain}"
|
||||||
timeout = aiohttp.ClientTimeout(total=60)
|
return {"email": addr, "password": password}
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(url, timeout=timeout) as response:
|
|
||||||
return json.loads(await response.text())
|
|
||||||
|
|
||||||
|
|
||||||
class ACFactory:
|
class ACFactory:
|
||||||
def __init__(self, deltachat: DeltaChat) -> None:
|
def __init__(self, deltachat: DeltaChat) -> None:
|
||||||
self.deltachat = deltachat
|
self.deltachat = deltachat
|
||||||
|
|
||||||
async def get_unconfigured_account(self) -> Account:
|
def get_unconfigured_account(self) -> Account:
|
||||||
return await self.deltachat.add_account()
|
account = self.deltachat.add_account()
|
||||||
|
account.set_config("verified_one_on_one_chats", "1")
|
||||||
|
return account
|
||||||
|
|
||||||
async def get_unconfigured_bot(self) -> Bot:
|
def get_unconfigured_bot(self) -> Bot:
|
||||||
return Bot(await self.get_unconfigured_account())
|
return Bot(self.get_unconfigured_account())
|
||||||
|
|
||||||
async def new_preconfigured_account(self) -> Account:
|
def new_preconfigured_account(self) -> Account:
|
||||||
"""Make a new account with configuration options set, but configuration not started."""
|
"""Make a new account with configuration options set, but configuration not started."""
|
||||||
credentials = await get_temp_credentials()
|
credentials = get_temp_credentials()
|
||||||
account = await self.get_unconfigured_account()
|
account = self.get_unconfigured_account()
|
||||||
await account.set_config("addr", credentials["email"])
|
account.set_config("addr", credentials["email"])
|
||||||
await account.set_config("mail_pw", credentials["password"])
|
account.set_config("mail_pw", credentials["password"])
|
||||||
assert not await account.is_configured()
|
assert not account.is_configured()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
async def new_configured_account(self) -> Account:
|
@futuremethod
|
||||||
account = await self.new_preconfigured_account()
|
def new_configured_account(self):
|
||||||
await account.configure()
|
account = self.new_preconfigured_account()
|
||||||
assert await account.is_configured()
|
yield account.configure.future()
|
||||||
|
assert account.is_configured()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
async def new_configured_bot(self) -> Bot:
|
def new_configured_bot(self) -> Bot:
|
||||||
credentials = await get_temp_credentials()
|
credentials = get_temp_credentials()
|
||||||
bot = await self.get_unconfigured_bot()
|
bot = self.get_unconfigured_bot()
|
||||||
await bot.configure(credentials["email"], credentials["password"])
|
bot.configure(credentials["email"], credentials["password"])
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
async def get_online_account(self) -> Account:
|
@futuremethod
|
||||||
account = await self.new_configured_account()
|
def get_online_account(self):
|
||||||
await account.start_io()
|
account = yield self.new_configured_account.future()
|
||||||
|
account.bring_online()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
async def get_online_accounts(self, num: int) -> List[Account]:
|
def get_online_accounts(self, num: int) -> list[Account]:
|
||||||
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
|
futures = [self.get_online_account.future() for _ in range(num)]
|
||||||
|
return [f() for f in futures]
|
||||||
|
|
||||||
async def send_message(
|
def resetup_account(self, ac: Account) -> Account:
|
||||||
|
"""Resetup account from scratch, losing the encryption key."""
|
||||||
|
ac.stop_io()
|
||||||
|
ac_clone = self.get_unconfigured_account()
|
||||||
|
for i in ["addr", "mail_pw"]:
|
||||||
|
ac_clone.set_config(i, ac.get_config(i))
|
||||||
|
ac.remove()
|
||||||
|
ac_clone.configure()
|
||||||
|
return ac_clone
|
||||||
|
|
||||||
|
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||||
|
ac2.create_chat(ac1)
|
||||||
|
return ac1.create_chat(ac2)
|
||||||
|
|
||||||
|
def send_message(
|
||||||
self,
|
self,
|
||||||
to_account: Account,
|
to_account: Account,
|
||||||
from_account: Optional[Account] = None,
|
from_account: Optional[Account] = None,
|
||||||
@@ -69,16 +86,16 @@ class ACFactory:
|
|||||||
group: Optional[str] = None,
|
group: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
if not from_account:
|
if not from_account:
|
||||||
from_account = (await self.get_online_accounts(1))[0]
|
from_account = (self.get_online_accounts(1))[0]
|
||||||
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
|
to_contact = from_account.create_contact(to_account.get_config("addr"))
|
||||||
if group:
|
if group:
|
||||||
to_chat = await from_account.create_group(group)
|
to_chat = from_account.create_group(group)
|
||||||
await to_chat.add_contact(to_contact)
|
to_chat.add_contact(to_contact)
|
||||||
else:
|
else:
|
||||||
to_chat = await to_contact.create_chat()
|
to_chat = to_contact.create_chat()
|
||||||
return await to_chat.send_message(text=text, file=file)
|
return to_chat.send_message(text=text, file=file)
|
||||||
|
|
||||||
async def process_message(
|
def process_message(
|
||||||
self,
|
self,
|
||||||
to_client: Client,
|
to_client: Client,
|
||||||
from_account: Optional[Account] = None,
|
from_account: Optional[Account] = None,
|
||||||
@@ -86,7 +103,7 @@ class ACFactory:
|
|||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
group: Optional[str] = None,
|
group: Optional[str] = None,
|
||||||
) -> AttrDict:
|
) -> AttrDict:
|
||||||
await self.send_message(
|
self.send_message(
|
||||||
to_account=to_client.account,
|
to_account=to_client.account,
|
||||||
from_account=from_account,
|
from_account=from_account,
|
||||||
text=text,
|
text=text,
|
||||||
@@ -94,16 +111,16 @@ class ACFactory:
|
|||||||
group=group,
|
group=group,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest.fixture()
|
||||||
async def rpc(tmp_path) -> AsyncGenerator:
|
def rpc(tmp_path) -> AsyncGenerator:
|
||||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||||
async with rpc_server:
|
with rpc_server:
|
||||||
yield rpc_server
|
yield rpc_server
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest.fixture()
|
||||||
async def acfactory(rpc) -> AsyncGenerator:
|
def acfactory(rpc) -> AsyncGenerator:
|
||||||
yield ACFactory(DeltaChat(rpc))
|
return ACFactory(DeltaChat(rpc))
|
||||||
|
|||||||
@@ -1,16 +1,65 @@
|
|||||||
import asyncio
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, Optional
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from queue import Empty, Queue
|
||||||
|
from threading import Event, Thread
|
||||||
|
from typing import Any, Iterator, Optional
|
||||||
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RpcFuture:
|
||||||
|
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||||
|
self.rpc = rpc
|
||||||
|
self.request_id = request_id
|
||||||
|
self.event = event
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self.event.wait()
|
||||||
|
response = self.rpc.request_results.pop(self.request_id)
|
||||||
|
if "error" in response:
|
||||||
|
raise JsonRpcError(response["error"])
|
||||||
|
if "result" in response:
|
||||||
|
return response["result"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class RpcMethod:
|
||||||
|
def __init__(self, rpc: "Rpc", name: str):
|
||||||
|
self.rpc = rpc
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __call__(self, *args) -> Any:
|
||||||
|
"""Synchronously calls JSON-RPC method."""
|
||||||
|
future = self.future(*args)
|
||||||
|
return future()
|
||||||
|
|
||||||
|
def future(self, *args) -> Any:
|
||||||
|
"""Asynchronously calls JSON-RPC method."""
|
||||||
|
request_id = next(self.rpc.id_iterator)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": self.name,
|
||||||
|
"params": args,
|
||||||
|
"id": request_id,
|
||||||
|
}
|
||||||
|
event = Event()
|
||||||
|
self.rpc.request_events[request_id] = event
|
||||||
|
self.rpc.request_queue.put(request)
|
||||||
|
|
||||||
|
return RpcFuture(self.rpc, request_id, event)
|
||||||
|
|
||||||
|
|
||||||
class Rpc:
|
class Rpc:
|
||||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||||
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
|
"""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),
|
||||||
@@ -18,85 +67,135 @@ class Rpc:
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
self.process: asyncio.subprocess.Process
|
self.process: subprocess.Popen
|
||||||
self.id: int
|
self.id_iterator: Iterator[int]
|
||||||
self.event_queues: Dict[int, asyncio.Queue]
|
self.event_queues: dict[int, Queue]
|
||||||
# Map from request ID to `asyncio.Future` returning the response.
|
# Map from request ID to `threading.Event`.
|
||||||
self.request_events: Dict[int, asyncio.Future]
|
self.request_events: dict[int, Event]
|
||||||
self.reader_task: asyncio.Task
|
# Map from request ID to the result.
|
||||||
|
self.request_results: dict[int, Any]
|
||||||
|
self.request_queue: Queue[Any]
|
||||||
|
self.closing: bool
|
||||||
|
self.reader_thread: Thread
|
||||||
|
self.writer_thread: Thread
|
||||||
|
self.events_thread: Thread
|
||||||
|
|
||||||
async def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.process = await asyncio.create_subprocess_exec(
|
if sys.version_info >= (3, 11):
|
||||||
"deltachat-rpc-server",
|
self.process = subprocess.Popen(
|
||||||
stdin=asyncio.subprocess.PIPE,
|
"deltachat-rpc-server",
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
**self._kwargs,
|
stdout=subprocess.PIPE,
|
||||||
)
|
# Prevent subprocess from capturing SIGINT.
|
||||||
self.id = 0
|
process_group=0,
|
||||||
|
**self._kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.process = subprocess.Popen(
|
||||||
|
"deltachat-rpc-server",
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
# `process_group` is not supported before Python 3.11.
|
||||||
|
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||||
|
**self._kwargs,
|
||||||
|
)
|
||||||
|
self.id_iterator = itertools.count(start=1)
|
||||||
self.event_queues = {}
|
self.event_queues = {}
|
||||||
self.request_events = {}
|
self.request_events = {}
|
||||||
self.reader_task = asyncio.create_task(self.reader_loop())
|
self.request_results = {}
|
||||||
|
self.request_queue = Queue()
|
||||||
|
self.closing = False
|
||||||
|
self.reader_thread = Thread(target=self.reader_loop)
|
||||||
|
self.reader_thread.start()
|
||||||
|
self.writer_thread = Thread(target=self.writer_loop)
|
||||||
|
self.writer_thread.start()
|
||||||
|
self.events_thread = Thread(target=self.events_loop)
|
||||||
|
self.events_thread.start()
|
||||||
|
|
||||||
async def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||||
self.process.terminate()
|
self.closing = True
|
||||||
await self.reader_task
|
self.stop_io_for_all_accounts()
|
||||||
|
self.events_thread.join()
|
||||||
|
self.process.stdin.close()
|
||||||
|
self.reader_thread.join()
|
||||||
|
self.request_queue.put(None)
|
||||||
|
self.writer_thread.join()
|
||||||
|
|
||||||
async def __aenter__(self):
|
def __enter__(self):
|
||||||
await self.start()
|
self.start()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, _exc_type, _exc, _tb):
|
def __exit__(self, _exc_type, _exc, _tb):
|
||||||
await self.close()
|
self.close()
|
||||||
|
|
||||||
async def reader_loop(self) -> None:
|
def reader_loop(self) -> None:
|
||||||
while True:
|
try:
|
||||||
line = await self.process.stdout.readline() # noqa
|
while True:
|
||||||
if not line: # EOF
|
line = self.process.stdout.readline()
|
||||||
break
|
if not line: # EOF
|
||||||
response = json.loads(line)
|
break
|
||||||
if "id" in response:
|
response = json.loads(line)
|
||||||
fut = self.request_events.pop(response["id"])
|
if "id" in response:
|
||||||
fut.set_result(response)
|
response_id = response["id"]
|
||||||
elif response["method"] == "event":
|
event = self.request_events.pop(response_id)
|
||||||
# An event notification.
|
self.request_results[response_id] = response
|
||||||
params = response["params"]
|
event.set()
|
||||||
account_id = params["contextId"]
|
else:
|
||||||
if account_id not in self.event_queues:
|
logging.warning("Got a response without ID: %s", response)
|
||||||
self.event_queues[account_id] = asyncio.Queue()
|
except Exception:
|
||||||
await self.event_queues[account_id].put(params["event"])
|
# Log an exception if the reader loop dies.
|
||||||
else:
|
logging.exception("Exception in the reader loop")
|
||||||
print(response)
|
|
||||||
|
|
||||||
async def wait_for_event(self, account_id: int) -> Optional[dict]:
|
def writer_loop(self) -> None:
|
||||||
|
"""Writer loop ensuring only a single thread writes requests."""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
request = self.request_queue.get()
|
||||||
|
if not request:
|
||||||
|
break
|
||||||
|
data = (json.dumps(request) + "\n").encode()
|
||||||
|
self.process.stdin.write(data)
|
||||||
|
self.process.stdin.flush()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# Log an exception if the writer loop dies.
|
||||||
|
logging.exception("Exception in the writer loop")
|
||||||
|
|
||||||
|
def get_queue(self, account_id: int) -> Queue:
|
||||||
|
if account_id not in self.event_queues:
|
||||||
|
self.event_queues[account_id] = Queue()
|
||||||
|
return self.event_queues[account_id]
|
||||||
|
|
||||||
|
def events_loop(self) -> None:
|
||||||
|
"""Requests new events and distributes them between queues."""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if self.closing:
|
||||||
|
return
|
||||||
|
event = self.get_next_event()
|
||||||
|
account_id = event["contextId"]
|
||||||
|
queue = self.get_queue(account_id)
|
||||||
|
event = event["event"]
|
||||||
|
logging.debug("account_id=%d got an event %s", account_id, event)
|
||||||
|
queue.put(event)
|
||||||
|
except Exception:
|
||||||
|
# Log an exception if the event loop dies.
|
||||||
|
logging.exception("Exception in the event loop")
|
||||||
|
|
||||||
|
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||||
"""Waits for the next event from the given account and returns it."""
|
"""Waits for the next event from the given account and returns it."""
|
||||||
if account_id in self.event_queues:
|
queue = self.get_queue(account_id)
|
||||||
return await self.event_queues[account_id].get()
|
return queue.get()
|
||||||
return None
|
|
||||||
|
def clear_all_events(self, account_id: int):
|
||||||
|
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||||
|
queue = self.get_queue(account_id)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
queue.get_nowait()
|
||||||
|
except Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
def __getattr__(self, attr: str):
|
def __getattr__(self, attr: str):
|
||||||
async def method(*args, **kwargs) -> Any:
|
return RpcMethod(self, attr)
|
||||||
self.id += 1
|
|
||||||
request_id = self.id
|
|
||||||
|
|
||||||
assert not (args and kwargs), "Mixing positional and keyword arguments"
|
|
||||||
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": attr,
|
|
||||||
"params": kwargs or args,
|
|
||||||
"id": self.id,
|
|
||||||
}
|
|
||||||
data = (json.dumps(request) + "\n").encode()
|
|
||||||
self.process.stdin.write(data) # noqa
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
fut = loop.create_future()
|
|
||||||
self.request_events[request_id] = fut
|
|
||||||
response = await fut
|
|
||||||
if "error" in response:
|
|
||||||
raise JsonRpcError(response["error"])
|
|
||||||
if "result" in response:
|
|
||||||
return response["result"]
|
|
||||||
return None
|
|
||||||
|
|
||||||
return method
|
|
||||||
|
|||||||
218
deltachat-rpc-client/tests/test_chatlist_events.py
Normal file
218
deltachat-rpc-client/tests/test_chatlist_events.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from deltachat_rpc_client import Account, EventType, const
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_chatlist_and_specific_item(account, chat_id):
|
||||||
|
first_event = ""
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.CHATLIST_CHANGED:
|
||||||
|
first_event = "change"
|
||||||
|
break
|
||||||
|
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||||
|
first_event = "item_change"
|
||||||
|
break
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.CHATLIST_CHANGED and first_event == "item_change":
|
||||||
|
break
|
||||||
|
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id and first_event == "change":
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_chatlist_specific_item(account, chat_id):
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_chatlist(account):
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.CHATLIST_CHANGED:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def test_delivery_status(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test change status on chatlistitem when status changes (delivered, read)
|
||||||
|
"""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
bob.stop_io()
|
||||||
|
alice.stop_io()
|
||||||
|
alice_chat_bob.send_text("hi")
|
||||||
|
wait_for_chatlist_and_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
alice.start_io()
|
||||||
|
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||||
|
|
||||||
|
bob.clear_all_events()
|
||||||
|
bob.start_io()
|
||||||
|
|
||||||
|
event = bob.wait_for_incoming_msg_event()
|
||||||
|
msg = bob.get_message_by_id(event.msg_id)
|
||||||
|
msg.get_snapshot().chat.accept()
|
||||||
|
msg.mark_seen()
|
||||||
|
|
||||||
|
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||||
|
assert chat_item["summaryStatus"] == const.MessageState.OUT_DELIVERED
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event = alice.wait_for_event()
|
||||||
|
if event.kind == EventType.MSG_READ:
|
||||||
|
break
|
||||||
|
|
||||||
|
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
|
||||||
|
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
|
||||||
|
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
|
||||||
|
|
||||||
|
|
||||||
|
def test_delivery_status_failed(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test change status on chatlistitem when status changes failed
|
||||||
|
"""
|
||||||
|
(alice,) = acfactory.get_online_accounts(1)
|
||||||
|
|
||||||
|
invalid_contact = alice.create_contact("example@example.com", "invalid address")
|
||||||
|
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
|
||||||
|
failing_message = invalid_chat.send_text("test")
|
||||||
|
|
||||||
|
wait_for_chatlist_and_specific_item(alice, invalid_chat.id)
|
||||||
|
|
||||||
|
assert failing_message.get_snapshot().state == const.MessageState.OUT_PENDING
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event = alice.wait_for_event()
|
||||||
|
if event.kind == EventType.MSG_FAILED:
|
||||||
|
break
|
||||||
|
|
||||||
|
wait_for_chatlist_specific_item(alice, invalid_chat.id)
|
||||||
|
|
||||||
|
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test if download on demand emits chatlist update events.
|
||||||
|
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
|
||||||
|
"""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("hi")
|
||||||
|
|
||||||
|
alice.set_config("download_limit", "1")
|
||||||
|
|
||||||
|
event = bob.wait_for_incoming_msg_event()
|
||||||
|
msg = bob.get_message_by_id(event.msg_id)
|
||||||
|
chat_id = msg.get_snapshot().chat_id
|
||||||
|
msg.get_snapshot().chat.accept()
|
||||||
|
bob.get_chat_by_id(chat_id).send_message(
|
||||||
|
"Hello World, this message is bigger than 5 bytes",
|
||||||
|
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
msg_id = alice.wait_for_incoming_msg_event().msg_id
|
||||||
|
|
||||||
|
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
|
||||||
|
alice._rpc.download_full_message(alice.id, msg_id)
|
||||||
|
|
||||||
|
wait_for_chatlist_specific_item(alice, chat_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("hi")
|
||||||
|
|
||||||
|
bob.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
|
alice_second_device: Account = acfactory.get_unconfigured_account()
|
||||||
|
|
||||||
|
alice._rpc.provide_backup.future(alice.id)
|
||||||
|
backup_code = alice._rpc.get_backup_qr(alice.id)
|
||||||
|
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
|
||||||
|
alice_second_device.start_io()
|
||||||
|
alice.clear_all_events()
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
bob.clear_all_events()
|
||||||
|
return [alice, alice_second_device, bob, alice_chat_bob]
|
||||||
|
|
||||||
|
|
||||||
|
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test that chatlist changed events are emitted for the second device
|
||||||
|
when the message is marked as read on the first device
|
||||||
|
"""
|
||||||
|
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||||
|
|
||||||
|
alice_chat_bob.send_text("hello")
|
||||||
|
|
||||||
|
event = bob.wait_for_incoming_msg_event()
|
||||||
|
msg = bob.get_message_by_id(event.msg_id)
|
||||||
|
bob_chat_id = msg.get_snapshot().chat_id
|
||||||
|
msg.get_snapshot().chat.accept()
|
||||||
|
|
||||||
|
alice.clear_all_events()
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
bob.get_chat_by_id(bob_chat_id).send_text("hello")
|
||||||
|
|
||||||
|
# make sure alice_second_device already received the message
|
||||||
|
alice_second_device.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
|
event = alice.wait_for_incoming_msg_event()
|
||||||
|
msg = alice.get_message_by_id(event.msg_id)
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
msg.mark_seen()
|
||||||
|
|
||||||
|
wait_for_chatlist_specific_item(bob, bob_chat_id)
|
||||||
|
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
|
||||||
|
"""
|
||||||
|
Test multidevice sync: syncing chat visibility and muting across multiple devices
|
||||||
|
"""
|
||||||
|
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||||
|
|
||||||
|
alice_chat_bob.archive()
|
||||||
|
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||||
|
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().archived
|
||||||
|
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
alice_chat_bob.pin()
|
||||||
|
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||||
|
|
||||||
|
alice_second_device.clear_all_events()
|
||||||
|
alice_chat_bob.mute()
|
||||||
|
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
|
||||||
|
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().is_muted
|
||||||
618
deltachat-rpc-client/tests/test_securejoin.py
Normal file
618
deltachat-rpc-client/tests/test_securejoin.py
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||||
|
|
||||||
|
|
||||||
|
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
qr_code, _svg = alice.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
|
||||||
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# Test that Alice verified Bob's profile.
|
||||||
|
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||||
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# Test that Bob verified Alice's profile.
|
||||||
|
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||||
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
# Test that if Bob changes the key, backwards verification is lost.
|
||||||
|
logging.info("Bob 2 is created")
|
||||||
|
bob2 = acfactory.new_configured_account()
|
||||||
|
bob2.export_self_keys(tmp_path)
|
||||||
|
|
||||||
|
logging.info("Bob imports a key")
|
||||||
|
bob.import_self_keys(tmp_path / "private-key-default.asc")
|
||||||
|
|
||||||
|
assert bob.get_config("key_id") == "2"
|
||||||
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
|
assert not bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("protect", [True, False])
|
||||||
|
def test_qr_securejoin(acfactory, protect):
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
logging.info("Alice creates a verified group")
|
||||||
|
alice_chat = alice.create_group("Verified group", protect=protect)
|
||||||
|
assert alice_chat.get_basic_snapshot().is_protected == protect
|
||||||
|
|
||||||
|
logging.info("Bob joins verified group")
|
||||||
|
qr_code, _svg = alice_chat.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
|
||||||
|
# Check that at least some of the handshake messages are deleted.
|
||||||
|
for ac in [alice, bob]:
|
||||||
|
while True:
|
||||||
|
event = ac.wait_for_event()
|
||||||
|
if event["kind"] == "ImapMessageDeleted":
|
||||||
|
break
|
||||||
|
|
||||||
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# Test that Alice verified Bob's profile.
|
||||||
|
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||||
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
||||||
|
|
||||||
|
# Test that Bob verified Alice's profile.
|
||||||
|
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||||
|
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||||
|
assert bob_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||||
|
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello!"
|
||||||
|
bob_chat_alice = snapshot.chat
|
||||||
|
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
|
alice_chat = alice.create_group("Verified group", protect=True)
|
||||||
|
logging.info("Bob joins verified group")
|
||||||
|
qr_code, _svg = alice_chat.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
while True:
|
||||||
|
event = bob.wait_for_event()
|
||||||
|
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Chat stays being a contact request.
|
||||||
|
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
|
|
||||||
|
def test_qr_readreceipt(acfactory) -> None:
|
||||||
|
alice, bob, charlie = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
logging.info("Bob and Charlie setup contact with Alice")
|
||||||
|
qr_code, _svg = alice.get_qr_code()
|
||||||
|
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
charlie.secure_join(qr_code)
|
||||||
|
|
||||||
|
for joiner in [bob, charlie]:
|
||||||
|
joiner.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
logging.info("Alice creates a verified group")
|
||||||
|
group = alice.create_group("Group", protect=True)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
charlie_addr = charlie.get_config("addr")
|
||||||
|
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
|
||||||
|
|
||||||
|
group.add_contact(alice_contact_bob)
|
||||||
|
group.add_contact(alice_contact_charlie)
|
||||||
|
|
||||||
|
# Promote a group.
|
||||||
|
group.send_message(text="Hello")
|
||||||
|
|
||||||
|
logging.info("Bob and Charlie receive a group")
|
||||||
|
|
||||||
|
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
|
||||||
|
bob_message = bob.get_message_by_id(bob_msg_id)
|
||||||
|
bob_snapshot = bob_message.get_snapshot()
|
||||||
|
assert bob_snapshot.text == "Hello"
|
||||||
|
|
||||||
|
# Charlie receives the same "Hello" message as Bob.
|
||||||
|
charlie.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
|
logging.info("Bob sends a message to the group")
|
||||||
|
|
||||||
|
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
|
||||||
|
|
||||||
|
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
|
||||||
|
charlie_message = charlie.get_message_by_id(charlie_msg_id)
|
||||||
|
charlie_snapshot = charlie_message.get_snapshot()
|
||||||
|
assert charlie_snapshot.text == "Hi from Bob!"
|
||||||
|
|
||||||
|
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
|
||||||
|
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||||
|
|
||||||
|
logging.info("Charlie reads Bob's message")
|
||||||
|
charlie_message.mark_seen()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
event = bob.wait_for_event()
|
||||||
|
if event["kind"] == "MsgRead" and event["msg_id"] == bob_out_message.id:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Receiving a read receipt from Charlie
|
||||||
|
# should not unblock hidden chat with Charlie for Bob.
|
||||||
|
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_contact_resetup(acfactory) -> None:
|
||||||
|
"""Tests that setup contact works after Alice resets the device and changes the key."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
qr_code, _svg = alice.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
alice = acfactory.resetup_account(alice)
|
||||||
|
|
||||||
|
qr_code, _svg = alice.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
|
||||||
|
def test_verified_group_recovery(acfactory) -> None:
|
||||||
|
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||||
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
logging.info("ac1 creates verified group")
|
||||||
|
chat = ac1.create_group("Verified group", protect=True)
|
||||||
|
assert chat.get_basic_snapshot().is_protected
|
||||||
|
|
||||||
|
logging.info("ac2 joins verified group")
|
||||||
|
qr_code, _svg = chat.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 has ac2 directly verified.
|
||||||
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||||
|
|
||||||
|
logging.info("ac3 joins verified group")
|
||||||
|
ac3_chat = ac3.secure_join(qr_code)
|
||||||
|
ac3.wait_for_securejoin_joiner_success()
|
||||||
|
ac3.wait_for_incoming_msg_event() # Member added
|
||||||
|
|
||||||
|
logging.info("ac2 logs in on a new device")
|
||||||
|
ac2 = acfactory.resetup_account(ac2)
|
||||||
|
|
||||||
|
logging.info("ac2 reverifies with ac3")
|
||||||
|
qr_code, _svg = ac3.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
logging.info("ac3 sends a message to the group")
|
||||||
|
assert len(ac3_chat.get_contacts()) == 3
|
||||||
|
ac3_chat.send_text("Hi!")
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
|
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
|
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
|
||||||
|
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
|
||||||
|
assert ac1_contact.get_snapshot().is_verified
|
||||||
|
|
||||||
|
# ac2 can write messages to the group.
|
||||||
|
snapshot.chat.send_text("Works again!")
|
||||||
|
|
||||||
|
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
ac1_chat_messages = snapshot.chat.get_messages()
|
||||||
|
ac2_addr = ac2.get_config("addr")
|
||||||
|
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
|
||||||
|
|
||||||
|
# ac2 is now verified by ac3 for ac1
|
||||||
|
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||||
|
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
|
||||||
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
logging.info("ac1 creates verified group")
|
||||||
|
chat = ac1.create_group("Verified group", protect=True)
|
||||||
|
assert chat.get_basic_snapshot().is_protected
|
||||||
|
|
||||||
|
logging.info("ac2 joins verified group")
|
||||||
|
qr_code, _svg = chat.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 has ac2 directly verified.
|
||||||
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||||
|
|
||||||
|
logging.info("ac3 joins verified group")
|
||||||
|
ac3_chat = ac3.secure_join(qr_code)
|
||||||
|
ac3.wait_for_securejoin_joiner_success()
|
||||||
|
ac3.wait_for_incoming_msg_event() # Member added
|
||||||
|
|
||||||
|
logging.info("ac2 logs in on a new device")
|
||||||
|
ac2 = acfactory.resetup_account(ac2)
|
||||||
|
|
||||||
|
logging.info("ac2 reverifies with ac3")
|
||||||
|
qr_code, _svg = ac3.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
logging.info("ac3 sends a message to the group")
|
||||||
|
assert len(ac3_chat.get_contacts()) == 3
|
||||||
|
ac3_chat.send_text("Hi!")
|
||||||
|
|
||||||
|
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
logging.info("Received message %s", snapshot.text)
|
||||||
|
assert snapshot.text == "Hi!"
|
||||||
|
|
||||||
|
ac1.wait_for_incoming_msg_event() # Hi!
|
||||||
|
|
||||||
|
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
|
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||||
|
ac3_chat.add_contact(ac3_contact_ac2)
|
||||||
|
|
||||||
|
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert "removed" in snapshot.text
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert "removed" in snapshot.text
|
||||||
|
|
||||||
|
event = ac2.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
chat_id = event.chat_id
|
||||||
|
message = ac2.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
logging.info("ac2 got event message: %s", snapshot.text)
|
||||||
|
assert "added" in snapshot.text
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert "added" in snapshot.text
|
||||||
|
|
||||||
|
chat = Chat(ac2, chat_id)
|
||||||
|
chat.send_text("Works again!")
|
||||||
|
|
||||||
|
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
||||||
|
message = ac3.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
|
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||||
|
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||||
|
assert ac1_contact_ac2_snapshot.is_verified
|
||||||
|
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
|
||||||
|
|
||||||
|
# ac2 is now verified by ac3 for ac1
|
||||||
|
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||||
|
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||||
|
"""Regression test for
|
||||||
|
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
|
||||||
|
"""
|
||||||
|
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||||
|
|
||||||
|
logging.info("ac3: verify with ac2")
|
||||||
|
qr_code, _svg = ac2.get_qr_code()
|
||||||
|
ac3.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# in order for ac2 to have pending bobstate with a verified group
|
||||||
|
# we first create a fully joined verified group, and then start
|
||||||
|
# joining a second time but interrupt it, to create pending bob state
|
||||||
|
|
||||||
|
logging.info("ac1: create verified group that ac2 fully joins")
|
||||||
|
ch1 = ac1.create_group("Group", protect=True)
|
||||||
|
qr_code, _svg = ch1.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||||
|
ch1.send_text("ac1 says hello")
|
||||||
|
while 1:
|
||||||
|
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
if snapshot.text == "ac1 says hello":
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||||
|
break
|
||||||
|
|
||||||
|
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||||
|
qr_code, _svg = ch1.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac1.remove()
|
||||||
|
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||||
|
|
||||||
|
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||||
|
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
|
||||||
|
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
|
||||||
|
|
||||||
|
logging.info("ac3: create a verified group VG with ac2")
|
||||||
|
vg = ac3.create_group("ac3-created", protect=True)
|
||||||
|
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
|
||||||
|
|
||||||
|
# ensure ac2 receives message in VG
|
||||||
|
vg.send_text("hello")
|
||||||
|
while 1:
|
||||||
|
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
if msg.text == "hello":
|
||||||
|
assert msg.chat.get_basic_snapshot().is_protected
|
||||||
|
break
|
||||||
|
|
||||||
|
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||||
|
qr_code, _svg = vg.get_qr_code()
|
||||||
|
ac4.secure_join(qr_code)
|
||||||
|
ac3.wait_for_securejoin_inviter_success()
|
||||||
|
while 1:
|
||||||
|
ev = ac2.wait_for_event()
|
||||||
|
if "added by unrelated SecureJoin" in str(ev):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def test_qr_new_group_unblocked(acfactory):
|
||||||
|
"""Regression test for a bug introduced in core v1.113.0.
|
||||||
|
ac2 scans a verified group QR code created by ac1.
|
||||||
|
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||||
|
but ac1 contact is not blocked on ac2.
|
||||||
|
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||||
|
ac2 should receive a message and create a contact request for the group.
|
||||||
|
Due to a bug previously ac2 created a blocked group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1_chat = ac1.create_group("Group for joining", protect=True)
|
||||||
|
qr_code, _svg = ac1_chat.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
|
||||||
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
ac1_new_chat = ac1.create_group("Another group")
|
||||||
|
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
|
||||||
|
# Receive "Member added" message.
|
||||||
|
ac2.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
|
ac1_new_chat.send_text("Hello!")
|
||||||
|
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert ac2_msg.text == "Hello!"
|
||||||
|
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
|
|
||||||
|
def test_aeap_flow_verified(acfactory):
|
||||||
|
"""Test that a new address is added to a contact when it changes its address."""
|
||||||
|
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||||
|
chat = ac1.create_group("hello", protect=True)
|
||||||
|
assert chat.get_basic_snapshot().is_protected
|
||||||
|
qr_code, _svg = chat.get_qr_code()
|
||||||
|
logging.info("ac2: start QR-code based join-group protocol")
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
logging.info("sending first message")
|
||||||
|
msg_out = chat.send_text("old address").get_snapshot()
|
||||||
|
|
||||||
|
logging.info("receiving first message")
|
||||||
|
ac2.wait_for_incoming_msg_event() # member added message
|
||||||
|
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert msg_in_1.text == msg_out.text
|
||||||
|
|
||||||
|
logging.info("changing email account")
|
||||||
|
ac1.set_config("addr", ac1new.get_config("addr"))
|
||||||
|
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
||||||
|
ac1.stop_io()
|
||||||
|
ac1.configure()
|
||||||
|
ac1.start_io()
|
||||||
|
|
||||||
|
logging.info("sending second message")
|
||||||
|
msg_out = chat.send_text("changed address").get_snapshot()
|
||||||
|
|
||||||
|
logging.info("receiving second message")
|
||||||
|
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||||
|
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||||
|
assert msg_in_2_snapshot.text == msg_out.text
|
||||||
|
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
||||||
|
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
|
||||||
|
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
|
||||||
|
assert ac1new.get_config("addr") in [
|
||||||
|
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_gossip_verification(acfactory) -> None:
|
||||||
|
alice, bob, carol = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
# Bob verifies Alice.
|
||||||
|
qr_code, _svg = alice.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# Bob verifies Carol.
|
||||||
|
qr_code, _svg = carol.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
|
||||||
|
bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
|
||||||
|
carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
|
||||||
|
|
||||||
|
logging.info("Bob creates an Autocrypt group")
|
||||||
|
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||||
|
assert not bob_group_chat.get_basic_snapshot().is_protected
|
||||||
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
|
bob_group_chat.send_message(text="Hello Autocrypt group")
|
||||||
|
|
||||||
|
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Autocrypt group"
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
# Autocrypt group does not propagate verification.
|
||||||
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
|
assert not carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
logging.info("Bob creates a Securejoin group")
|
||||||
|
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
|
||||||
|
assert bob_group_chat.get_basic_snapshot().is_protected
|
||||||
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
|
bob_group_chat.send_message(text="Hello Securejoin group")
|
||||||
|
|
||||||
|
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Securejoin group"
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
# Securejoin propagates verification.
|
||||||
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
|
assert carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||||
|
"""
|
||||||
|
Regression test for a bug that prevented joining verified group with a QR code
|
||||||
|
if the group is already created and contains
|
||||||
|
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
|
||||||
|
"""
|
||||||
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
# ac3 creates protected group with ac1.
|
||||||
|
ac3_chat = ac3.create_group("Verified group", protect=True)
|
||||||
|
|
||||||
|
# ac1 joins ac3 group.
|
||||||
|
ac3_qr_code, _svg = ac3_chat.get_qr_code()
|
||||||
|
ac1.secure_join(ac3_qr_code)
|
||||||
|
ac1.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 waits for member added message and creates a QR code.
|
||||||
|
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
ac1_qr_code, _svg = snapshot.chat.get_qr_code()
|
||||||
|
|
||||||
|
# ac2 verifies ac1
|
||||||
|
qr_code, _svg = ac1.get_qr_code()
|
||||||
|
ac2.secure_join(qr_code)
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# ac1 is verified for ac2.
|
||||||
|
ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "")
|
||||||
|
assert ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
|
# ac1 resetups the account.
|
||||||
|
ac1 = acfactory.resetup_account(ac1)
|
||||||
|
|
||||||
|
# ac1 sends a message to ac2.
|
||||||
|
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||||
|
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||||
|
ac1_chat_ac2.send_text("Hello!")
|
||||||
|
|
||||||
|
# ac2 receives a message.
|
||||||
|
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello!"
|
||||||
|
|
||||||
|
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
|
||||||
|
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
|
# ac1 goes offline.
|
||||||
|
ac1.remove()
|
||||||
|
|
||||||
|
# Scanning a QR code results in creating an unprotected group with an inviter.
|
||||||
|
# In this case inviter is ac1 which has an inconsistent key state.
|
||||||
|
# Normally inviter becomes verified as a result of Securejoin protocol
|
||||||
|
# and then the group chat becomes verified when "Member added" is received,
|
||||||
|
# but in this case ac1 is offline and this Securejoin process will never finish.
|
||||||
|
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
|
||||||
|
ac2.secure_join(ac1_qr_code)
|
||||||
|
|
||||||
|
logging.info("ac2 scans ac3 QR code")
|
||||||
|
ac2.secure_join(ac3_qr_code)
|
||||||
|
|
||||||
|
logging.info("ac2 waits for joiner success")
|
||||||
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
# Wait for member added.
|
||||||
|
logging.info("ac2 waits for member added message")
|
||||||
|
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.is_info
|
||||||
|
ac2_chat = snapshot.chat
|
||||||
|
assert ac2_chat.get_basic_snapshot().is_protected
|
||||||
|
assert len(ac2_chat.get_contacts()) == 3
|
||||||
|
|
||||||
|
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||||
|
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def test_withdraw_securejoin_qr(acfactory):
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
logging.info("Alice creates a verified group")
|
||||||
|
alice_chat = alice.create_group("Verified group", protect=True)
|
||||||
|
assert alice_chat.get_basic_snapshot().is_protected
|
||||||
|
logging.info("Bob joins verified group")
|
||||||
|
|
||||||
|
qr_code, _svg = alice_chat.get_qr_code()
|
||||||
|
bob_chat = bob.secure_join(qr_code)
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||||
|
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||||
|
bob_chat.leave()
|
||||||
|
|
||||||
|
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||||
|
|
||||||
|
logging.info("Alice withdraws QR code.")
|
||||||
|
qr = alice.check_qr(qr_code)
|
||||||
|
assert qr["kind"] == "withdrawVerifyGroup"
|
||||||
|
alice.set_config_from_qr(qr_code)
|
||||||
|
|
||||||
|
logging.info("Bob scans withdrawn QR code.")
|
||||||
|
bob_chat = bob.secure_join(qr_code)
|
||||||
|
|
||||||
|
logging.info("Bob scanned withdrawn QR code")
|
||||||
|
while True:
|
||||||
|
event = alice.wait_for_event()
|
||||||
|
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
|
||||||
|
break
|
||||||
|
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))
|
||||||
@@ -1,31 +1,38 @@
|
|||||||
import asyncio
|
import concurrent.futures
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from deltachat_rpc_client import EventType, events
|
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||||
|
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||||
|
from deltachat_rpc_client.direct_imap import DirectImap
|
||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_system_info(rpc) -> None:
|
||||||
async def test_system_info(rpc) -> None:
|
system_info = rpc.get_system_info()
|
||||||
system_info = await rpc.get_system_info()
|
|
||||||
assert "arch" in system_info
|
assert "arch" in system_info
|
||||||
assert "deltachat_core_version" in system_info
|
assert "deltachat_core_version" in system_info
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_sleep(rpc) -> None:
|
||||||
async def test_sleep(rpc) -> None:
|
|
||||||
"""Test that long-running task does not block short-running task from completion."""
|
"""Test that long-running task does not block short-running task from completion."""
|
||||||
sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
|
sleep_5_future = executor.submit(rpc.sleep, 5.0)
|
||||||
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
|
sleep_3_future = executor.submit(rpc.sleep, 3.0)
|
||||||
assert sleep_3_task in done
|
done, pending = concurrent.futures.wait(
|
||||||
assert sleep_5_task in pending
|
[sleep_5_future, sleep_3_future],
|
||||||
sleep_5_task.cancel()
|
return_when=concurrent.futures.FIRST_COMPLETED,
|
||||||
|
)
|
||||||
|
assert sleep_3_future in done
|
||||||
|
assert sleep_5_future in pending
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_email_address_validity(rpc) -> None:
|
||||||
async def test_email_address_validity(rpc) -> None:
|
|
||||||
valid_addresses = [
|
valid_addresses = [
|
||||||
"email@example.com",
|
"email@example.com",
|
||||||
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
||||||
@@ -33,17 +40,16 @@ async def test_email_address_validity(rpc) -> None:
|
|||||||
invalid_addresses = ["email@", "example.com", "emai221"]
|
invalid_addresses = ["email@", "example.com", "emai221"]
|
||||||
|
|
||||||
for addr in valid_addresses:
|
for addr in valid_addresses:
|
||||||
assert await rpc.check_email_validity(addr)
|
assert rpc.check_email_validity(addr)
|
||||||
for addr in invalid_addresses:
|
for addr in invalid_addresses:
|
||||||
assert not await rpc.check_email_validity(addr)
|
assert not rpc.check_email_validity(addr)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_acfactory(acfactory) -> None:
|
||||||
async def test_acfactory(acfactory) -> None:
|
account = acfactory.new_configured_account()
|
||||||
account = await acfactory.new_configured_account()
|
|
||||||
while True:
|
while True:
|
||||||
event = await account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
if event.type == EventType.CONFIGURE_PROGRESS:
|
if event.kind == EventType.CONFIGURE_PROGRESS:
|
||||||
assert event.progress != 0 # Progress 0 indicates error.
|
assert event.progress != 0 # Progress 0 indicates error.
|
||||||
if event.progress == 1000: # Success
|
if event.progress == 1000: # Success
|
||||||
break
|
break
|
||||||
@@ -52,232 +58,235 @@ async def test_acfactory(acfactory) -> None:
|
|||||||
print("Successful configuration")
|
print("Successful configuration")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_configure_starttls(acfactory) -> None:
|
||||||
async def test_configure_starttls(acfactory) -> None:
|
account = acfactory.new_preconfigured_account()
|
||||||
account = await acfactory.new_preconfigured_account()
|
|
||||||
|
|
||||||
# Use STARTTLS
|
# Use STARTTLS
|
||||||
await account.set_config("mail_security", "2")
|
account.set_config("mail_security", "2")
|
||||||
await account.set_config("send_security", "2")
|
account.set_config("send_security", "2")
|
||||||
await account.configure()
|
account.configure()
|
||||||
assert await account.is_configured()
|
assert account.is_configured()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_account(acfactory) -> None:
|
||||||
async def test_account(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
await alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
chat_id = event.chat_id
|
chat_id = event.chat_id
|
||||||
msg_id = event.msg_id
|
msg_id = event.msg_id
|
||||||
break
|
break
|
||||||
|
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == chat_id
|
assert snapshot.chat_id == chat_id
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
await bob.mark_seen_messages([message])
|
bob.mark_seen_messages([message])
|
||||||
|
|
||||||
assert alice != bob
|
assert alice != bob
|
||||||
assert repr(alice)
|
assert repr(alice)
|
||||||
assert (await alice.get_info()).level
|
assert alice.get_info().level
|
||||||
assert await alice.get_size()
|
assert alice.get_size()
|
||||||
assert await alice.is_configured()
|
assert alice.is_configured()
|
||||||
assert not await alice.get_avatar()
|
assert not alice.get_avatar()
|
||||||
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||||
assert await alice.get_contacts()
|
assert alice.get_contacts()
|
||||||
assert await alice.get_contacts(snapshot=True)
|
assert alice.get_contacts(snapshot=True)
|
||||||
assert alice.self_contact
|
assert alice.self_contact
|
||||||
assert await alice.get_chatlist()
|
assert alice.get_chatlist()
|
||||||
assert await alice.get_chatlist(snapshot=True)
|
assert alice.get_chatlist(snapshot=True)
|
||||||
assert await alice.get_qr_code()
|
assert alice.get_qr_code()
|
||||||
await alice.get_fresh_messages()
|
assert alice.get_fresh_messages()
|
||||||
await alice.get_fresh_messages_in_arrival_order()
|
assert alice.get_next_messages()
|
||||||
|
|
||||||
group = await alice.create_group("test group")
|
# Test sending empty message.
|
||||||
await group.add_contact(alice_contact_bob)
|
assert len(bob.wait_next_messages()) == 0
|
||||||
group_msg = await group.send_message(text="hello")
|
alice_chat_bob.send_text("")
|
||||||
|
messages = bob.wait_next_messages()
|
||||||
|
assert len(messages) == 1
|
||||||
|
message = messages[0]
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.text == ""
|
||||||
|
bob.mark_seen_messages([message])
|
||||||
|
|
||||||
|
group = alice.create_group("test group")
|
||||||
|
group.add_contact(alice_contact_bob)
|
||||||
|
group_msg = group.send_message(text="hello")
|
||||||
assert group_msg == alice.get_message_by_id(group_msg.id)
|
assert group_msg == alice.get_message_by_id(group_msg.id)
|
||||||
assert group == alice.get_chat_by_id(group.id)
|
assert group == alice.get_chat_by_id(group.id)
|
||||||
await alice.delete_messages([group_msg])
|
alice.delete_messages([group_msg])
|
||||||
|
|
||||||
await alice.set_config("selfstatus", "test")
|
alice.set_config("selfstatus", "test")
|
||||||
assert await alice.get_config("selfstatus") == "test"
|
assert alice.get_config("selfstatus") == "test"
|
||||||
await alice.update_config(selfstatus="test2")
|
alice.update_config(selfstatus="test2")
|
||||||
assert await alice.get_config("selfstatus") == "test2"
|
assert alice.get_config("selfstatus") == "test2"
|
||||||
|
|
||||||
assert not await alice.get_blocked_contacts()
|
assert not alice.get_blocked_contacts()
|
||||||
await alice_contact_bob.block()
|
alice_contact_bob.block()
|
||||||
blocked_contacts = await alice.get_blocked_contacts()
|
blocked_contacts = alice.get_blocked_contacts()
|
||||||
assert blocked_contacts
|
assert blocked_contacts
|
||||||
assert blocked_contacts[0].contact == alice_contact_bob
|
assert blocked_contacts[0].contact == alice_contact_bob
|
||||||
|
|
||||||
await bob.remove()
|
bob.remove()
|
||||||
await alice.stop_io()
|
alice.stop_io()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_chat(acfactory) -> None:
|
||||||
async def test_chat(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
await alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
event = bob.wait_for_incoming_msg_event()
|
||||||
event = await bob.wait_for_event()
|
chat_id = event.chat_id
|
||||||
if event.type == EventType.INCOMING_MSG:
|
msg_id = event.msg_id
|
||||||
chat_id = event.chat_id
|
|
||||||
msg_id = event.msg_id
|
|
||||||
break
|
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == chat_id
|
assert snapshot.chat_id == chat_id
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
bob_chat_alice = bob.get_chat_by_id(chat_id)
|
bob_chat_alice = bob.get_chat_by_id(chat_id)
|
||||||
|
|
||||||
assert alice_chat_bob != bob_chat_alice
|
assert alice_chat_bob != bob_chat_alice
|
||||||
assert repr(alice_chat_bob)
|
assert repr(alice_chat_bob)
|
||||||
await alice_chat_bob.delete()
|
alice_chat_bob.delete()
|
||||||
await bob_chat_alice.accept()
|
assert not bob_chat_alice.can_send()
|
||||||
await bob_chat_alice.block()
|
bob_chat_alice.accept()
|
||||||
bob_chat_alice = await snapshot.sender.create_chat()
|
assert bob_chat_alice.can_send()
|
||||||
await bob_chat_alice.mute()
|
bob_chat_alice.block()
|
||||||
await bob_chat_alice.unmute()
|
bob_chat_alice = snapshot.sender.create_chat()
|
||||||
await bob_chat_alice.pin()
|
bob_chat_alice.mute()
|
||||||
await bob_chat_alice.unpin()
|
bob_chat_alice.unmute()
|
||||||
await bob_chat_alice.archive()
|
bob_chat_alice.pin()
|
||||||
await bob_chat_alice.unarchive()
|
bob_chat_alice.unpin()
|
||||||
|
bob_chat_alice.archive()
|
||||||
|
bob_chat_alice.unarchive()
|
||||||
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||||
await bob_chat_alice.set_name("test")
|
bob_chat_alice.set_name("test")
|
||||||
await bob_chat_alice.set_ephemeral_timer(300)
|
bob_chat_alice.set_ephemeral_timer(300)
|
||||||
await bob_chat_alice.get_encryption_info()
|
bob_chat_alice.get_encryption_info()
|
||||||
|
|
||||||
group = await alice.create_group("test group")
|
group = alice.create_group("test group")
|
||||||
await group.add_contact(alice_contact_bob)
|
group.add_contact(alice_contact_bob)
|
||||||
await group.get_qr_code()
|
group.get_qr_code()
|
||||||
|
|
||||||
snapshot = await group.get_basic_snapshot()
|
snapshot = group.get_basic_snapshot()
|
||||||
assert snapshot.name == "test group"
|
assert snapshot.name == "test group"
|
||||||
await group.set_name("new name")
|
group.set_name("new name")
|
||||||
snapshot = await group.get_full_snapshot()
|
snapshot = group.get_full_snapshot()
|
||||||
assert snapshot.name == "new name"
|
assert snapshot.name == "new name"
|
||||||
|
|
||||||
msg = await group.send_message(text="hi")
|
msg = group.send_message(text="hi")
|
||||||
assert (await msg.get_snapshot()).text == "hi"
|
assert (msg.get_snapshot()).text == "hi"
|
||||||
await group.forward_messages([msg])
|
group.forward_messages([msg])
|
||||||
|
|
||||||
await group.set_draft(text="test draft")
|
group.set_draft(text="test draft")
|
||||||
draft = await group.get_draft()
|
draft = group.get_draft()
|
||||||
assert draft.text == "test draft"
|
assert draft.text == "test draft"
|
||||||
await group.remove_draft()
|
group.remove_draft()
|
||||||
assert not await group.get_draft()
|
assert not group.get_draft()
|
||||||
|
|
||||||
assert await group.get_messages()
|
assert group.get_messages()
|
||||||
await group.get_fresh_message_count()
|
group.get_fresh_message_count()
|
||||||
await group.mark_noticed()
|
group.mark_noticed()
|
||||||
assert await group.get_contacts()
|
assert group.get_contacts()
|
||||||
await group.remove_contact(alice_chat_bob)
|
group.remove_contact(alice_chat_bob)
|
||||||
await group.get_locations()
|
group.get_locations()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_contact(acfactory) -> None:
|
||||||
async def test_contact(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "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)
|
||||||
await alice_contact_bob.block()
|
alice_contact_bob.block()
|
||||||
await alice_contact_bob.unblock()
|
alice_contact_bob.unblock()
|
||||||
await alice_contact_bob.set_name("new name")
|
alice_contact_bob.set_name("new name")
|
||||||
await alice_contact_bob.get_encryption_info()
|
alice_contact_bob.get_encryption_info()
|
||||||
snapshot = await alice_contact_bob.get_snapshot()
|
snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert snapshot.address == bob_addr
|
assert snapshot.address == bob_addr
|
||||||
await alice_contact_bob.create_chat()
|
alice_contact_bob.create_chat()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_message(acfactory) -> None:
|
||||||
async def test_message(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
await alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
event = bob.wait_for_incoming_msg_event()
|
||||||
event = await bob.wait_for_event()
|
chat_id = event.chat_id
|
||||||
if event.type == EventType.INCOMING_MSG:
|
msg_id = event.msg_id
|
||||||
chat_id = event.chat_id
|
|
||||||
msg_id = event.msg_id
|
|
||||||
break
|
|
||||||
|
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == chat_id
|
assert snapshot.chat_id == chat_id
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
assert not snapshot.is_bot
|
assert not snapshot.is_bot
|
||||||
assert repr(message)
|
assert repr(message)
|
||||||
|
|
||||||
with pytest.raises(JsonRpcError): # chat is not accepted
|
with pytest.raises(JsonRpcError): # chat is not accepted
|
||||||
await snapshot.chat.send_text("hi")
|
snapshot.chat.send_text("hi")
|
||||||
await snapshot.chat.accept()
|
snapshot.chat.accept()
|
||||||
await snapshot.chat.send_text("hi")
|
snapshot.chat.send_text("hi")
|
||||||
|
|
||||||
await message.mark_seen()
|
message.mark_seen()
|
||||||
await message.send_reaction("😎")
|
message.send_reaction("😎")
|
||||||
|
reactions = message.get_reactions()
|
||||||
|
assert reactions
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert reactions == snapshot.reactions
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_is_bot(acfactory) -> None:
|
||||||
async 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 = await acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
# Alice becomes a bot.
|
# Alice becomes a bot.
|
||||||
await alice.set_config("bot", "1")
|
alice.set_config("bot", "1")
|
||||||
await alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
msg_id = event.msg_id
|
msg_id = event.msg_id
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == event.chat_id
|
assert snapshot.chat_id == event.chat_id
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
assert snapshot.is_bot
|
assert snapshot.is_bot
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_bot(acfactory) -> None:
|
||||||
async def test_bot(acfactory) -> None:
|
|
||||||
mock = MagicMock()
|
mock = MagicMock()
|
||||||
user = (await acfactory.get_online_accounts(1))[0]
|
user = (acfactory.get_online_accounts(1))[0]
|
||||||
bot = await acfactory.new_configured_bot()
|
bot = acfactory.new_configured_bot()
|
||||||
bot2 = await acfactory.new_configured_bot()
|
bot2 = acfactory.new_configured_bot()
|
||||||
|
|
||||||
assert await bot.is_configured()
|
assert bot.is_configured()
|
||||||
assert await bot.account.get_config("bot") == "1"
|
assert bot.account.get_config("bot") == "1"
|
||||||
|
|
||||||
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
||||||
bot.add_hook(*hook)
|
bot.add_hook(*hook)
|
||||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||||
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
||||||
assert not snapshot.is_bot
|
assert not snapshot.is_bot
|
||||||
mock.hook.assert_called_once_with(event.msg_id)
|
mock.hook.assert_called_once_with(event.msg_id)
|
||||||
bot.remove_hook(*hook)
|
bot.remove_hook(*hook)
|
||||||
@@ -289,17 +298,318 @@ async def test_bot(acfactory) -> None:
|
|||||||
hook = track, events.NewMessage(r"hello")
|
hook = track, events.NewMessage(r"hello")
|
||||||
bot.add_hook(*hook)
|
bot.add_hook(*hook)
|
||||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||||
mock.hook.assert_called_with(event.msg_id)
|
mock.hook.assert_called_with(event.msg_id)
|
||||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||||
mock.hook.assert_called_with(event.msg_id)
|
mock.hook.assert_called_with(event.msg_id)
|
||||||
await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
||||||
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
|
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
|
||||||
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||||
assert len(mock.hook.mock_calls) == 2
|
assert len(mock.hook.mock_calls) == 2
|
||||||
bot.remove_hook(*hook)
|
bot.remove_hook(*hook)
|
||||||
|
|
||||||
mock.hook.reset_mock()
|
mock.hook.reset_mock()
|
||||||
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||||
mock.hook.assert_called_once_with(event.msg_id)
|
mock.hook.assert_called_once_with(event.msg_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wait_next_messages(acfactory) -> None:
|
||||||
|
alice = acfactory.new_configured_account()
|
||||||
|
|
||||||
|
# Create a bot account so it does not receive device messages in the beginning.
|
||||||
|
bot = acfactory.new_preconfigured_account()
|
||||||
|
bot.set_config("bot", "1")
|
||||||
|
bot.configure()
|
||||||
|
|
||||||
|
# There are no old messages and the call returns immediately.
|
||||||
|
assert not bot.wait_next_messages()
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
|
# Bot starts waiting for messages.
|
||||||
|
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||||
|
|
||||||
|
bot_addr = bot.get_config("addr")
|
||||||
|
alice_contact_bot = alice.create_contact(bot_addr, "Bot")
|
||||||
|
alice_chat_bot = alice_contact_bot.create_chat()
|
||||||
|
alice_chat_bot.send_text("Hello!")
|
||||||
|
|
||||||
|
next_messages = next_messages_task.result()
|
||||||
|
assert len(next_messages) == 1
|
||||||
|
snapshot = next_messages[0].get_snapshot()
|
||||||
|
assert snapshot.text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||||
|
alice = acfactory.new_configured_account()
|
||||||
|
alice.export_backup(tmp_path)
|
||||||
|
|
||||||
|
files = list(tmp_path.glob("*.tar"))
|
||||||
|
alice2 = acfactory.get_unconfigured_account()
|
||||||
|
alice2.import_backup(files[0])
|
||||||
|
|
||||||
|
assert alice2.manager.get_system_info()
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
|
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Bob!"
|
||||||
|
|
||||||
|
# Alice resetups account, but keeps the key.
|
||||||
|
alice_keys_path = tmp_path / "alice_keys"
|
||||||
|
alice_keys_path.mkdir()
|
||||||
|
alice.export_self_keys(alice_keys_path)
|
||||||
|
alice = acfactory.resetup_account(alice)
|
||||||
|
alice.import_self_keys(alice_keys_path)
|
||||||
|
|
||||||
|
snapshot.chat.accept()
|
||||||
|
snapshot.chat.send_text("Hello Alice!")
|
||||||
|
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "Hello Alice!"
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrpc_command_line() -> None:
|
||||||
|
"""Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
|
||||||
|
out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
|
||||||
|
openrpc = json.loads(out)
|
||||||
|
assert "openrpc" in openrpc
|
||||||
|
assert "methods" in openrpc
|
||||||
|
|
||||||
|
|
||||||
|
def test_provider_info(rpc) -> None:
|
||||||
|
account_id = rpc.add_account()
|
||||||
|
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "example.org")
|
||||||
|
assert provider_info["id"] == "example.com"
|
||||||
|
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
|
||||||
|
assert provider_info is None
|
||||||
|
|
||||||
|
# Test MX record resolution.
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||||
|
assert provider_info["id"] == "gmail"
|
||||||
|
|
||||||
|
# Disable MX record resolution.
|
||||||
|
rpc.set_config(account_id, "socks5_enabled", "1")
|
||||||
|
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||||
|
assert provider_info is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
|
||||||
|
# Bob creates chat manually so chat with Alice is accepted.
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
|
# Alice sends a message to Bob.
|
||||||
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
|
event = bob.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
message = bob.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
|
||||||
|
# Bob sends a message to Alice.
|
||||||
|
bob_chat_alice = snapshot.chat
|
||||||
|
bob_chat_alice.accept()
|
||||||
|
bob_chat_alice.send_text("Hello Alice!")
|
||||||
|
event = alice.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
message = alice.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
# Alice reads Bob's message.
|
||||||
|
message.mark_seen()
|
||||||
|
while True:
|
||||||
|
event = bob.wait_for_event()
|
||||||
|
if event.kind == EventType.MSG_READ:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Bob sends a message to Alice, it should also be encrypted.
|
||||||
|
bob_chat_alice.send_text("Hi Alice!")
|
||||||
|
event = alice.wait_for_incoming_msg_event()
|
||||||
|
msg_id = event.msg_id
|
||||||
|
message = alice.get_message_by_id(msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
|
|
||||||
|
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
|
||||||
|
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||||
|
messages are received out of order".
|
||||||
|
|
||||||
|
If the Inbox contains X small messages followed by Y large messages followed by Z small
|
||||||
|
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
|
||||||
|
|
||||||
|
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
|
||||||
|
with online test as follows:
|
||||||
|
- Bob enables download limit and goes offline.
|
||||||
|
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
|
||||||
|
- Bob goes online
|
||||||
|
- Bob first processes a reaction message and throws it away because there is no corresponding
|
||||||
|
message, then processes a partially downloaded message.
|
||||||
|
- As a result, Bob does not see a reaction
|
||||||
|
"""
|
||||||
|
download_limit = 300000
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1_addr = ac1.get_config("addr")
|
||||||
|
chat = ac1.create_chat(ac2)
|
||||||
|
ac2.set_config("download_limit", str(download_limit))
|
||||||
|
ac2.stop_io()
|
||||||
|
|
||||||
|
logging.info("sending small+large messages from ac1 to ac2")
|
||||||
|
msgs = []
|
||||||
|
msgs.append(chat.send_text("hi"))
|
||||||
|
path = tmp_path / "large"
|
||||||
|
path.write_bytes(os.urandom(download_limit + 1))
|
||||||
|
msgs.append(chat.send_file(str(path)))
|
||||||
|
for m in msgs:
|
||||||
|
m.wait_until_delivered()
|
||||||
|
|
||||||
|
logging.info("sending a reaction to the large message from ac1 to ac2")
|
||||||
|
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
|
||||||
|
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
|
||||||
|
# have a later INTERNALDATE.
|
||||||
|
time.sleep(1.1)
|
||||||
|
react_str = "\N{THUMBS UP SIGN}"
|
||||||
|
msgs.append(msgs[-1].send_reaction(react_str))
|
||||||
|
msgs[-1].wait_until_delivered()
|
||||||
|
|
||||||
|
ac2.start_io()
|
||||||
|
|
||||||
|
logging.info("wait for ac2 to receive a reaction")
|
||||||
|
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||||
|
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
|
||||||
|
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
|
||||||
|
reactions = msg2.get_reactions()
|
||||||
|
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||||
|
assert len(contacts) == 1
|
||||||
|
assert contacts[0].get_snapshot().address == ac1_addr
|
||||||
|
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reactions_for_a_reordering_move(acfactory):
|
||||||
|
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
|
||||||
|
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
|
||||||
|
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
|
||||||
|
messages they refer to and thus dropped.
|
||||||
|
"""
|
||||||
|
(ac1,) = acfactory.get_online_accounts(1)
|
||||||
|
ac2 = acfactory.new_preconfigured_account()
|
||||||
|
ac2.configure()
|
||||||
|
ac2.set_config("mvbox_move", "1")
|
||||||
|
ac2.bring_online()
|
||||||
|
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
|
ac2.stop_io()
|
||||||
|
|
||||||
|
logging.info("sending message + reaction from ac1 to ac2")
|
||||||
|
msg1 = chat1.send_text("hi")
|
||||||
|
msg1.wait_until_delivered()
|
||||||
|
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
|
||||||
|
# order by DC, and most (if not all) mail servers provide only seconds precision.
|
||||||
|
time.sleep(1.1)
|
||||||
|
react_str = "\N{THUMBS UP SIGN}"
|
||||||
|
msg1.send_reaction(react_str).wait_until_delivered()
|
||||||
|
|
||||||
|
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
|
||||||
|
ac2_direct_imap = DirectImap(ac2)
|
||||||
|
ac2_direct_imap.connect()
|
||||||
|
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
|
||||||
|
ac2_direct_imap.conn.move(uid, "DeltaChat")
|
||||||
|
|
||||||
|
logging.info("receiving messages by ac2")
|
||||||
|
ac2.start_io()
|
||||||
|
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
|
||||||
|
assert msg2.get_snapshot().text == msg1.get_snapshot().text
|
||||||
|
reactions = msg2.get_reactions()
|
||||||
|
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
|
||||||
|
assert len(contacts) == 1
|
||||||
|
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
|
||||||
|
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("n_accounts", [3, 2])
|
||||||
|
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||||
|
download_limit = 300000
|
||||||
|
|
||||||
|
alice, *others = acfactory.get_online_accounts(n_accounts)
|
||||||
|
bob = others[0]
|
||||||
|
|
||||||
|
alice_group = alice.create_group("test group")
|
||||||
|
for account in others:
|
||||||
|
chat = account.create_chat(alice)
|
||||||
|
chat.send_text("Hello Alice!")
|
||||||
|
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
|
||||||
|
|
||||||
|
contact_addr = account.get_config("addr")
|
||||||
|
contact = alice.create_contact(contact_addr, "")
|
||||||
|
|
||||||
|
alice_group.add_contact(contact)
|
||||||
|
|
||||||
|
if n_accounts == 2:
|
||||||
|
bob_chat_alice = bob.create_chat(alice)
|
||||||
|
bob.set_config("download_limit", str(download_limit))
|
||||||
|
|
||||||
|
alice_group.send_text("hi")
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.text == "hi"
|
||||||
|
bob_group = snapshot.chat
|
||||||
|
|
||||||
|
path = tmp_path / "large"
|
||||||
|
path.write_bytes(os.urandom(download_limit + 1))
|
||||||
|
|
||||||
|
for i in range(10):
|
||||||
|
logging.info("Sending message %s", i)
|
||||||
|
alice_group.send_file(str(path))
|
||||||
|
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||||
|
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||||
|
if n_accounts > 2:
|
||||||
|
assert snapshot.chat == bob_group
|
||||||
|
else:
|
||||||
|
# Group contains only Alice and Bob,
|
||||||
|
# so partially downloaded messages are
|
||||||
|
# hard to distinguish from private replies to group messages.
|
||||||
|
#
|
||||||
|
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
|
||||||
|
assert snapshot.chat == bob_chat_alice
|
||||||
|
|
||||||
|
|
||||||
|
def test_markseen_contact_request(acfactory, tmp_path):
|
||||||
|
"""
|
||||||
|
Test that seen status is synchronized for contact request messages
|
||||||
|
even though read receipt is not sent.
|
||||||
|
"""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
# Bob sets up a second device.
|
||||||
|
bob.export_backup(tmp_path)
|
||||||
|
files = list(tmp_path.glob("*.tar"))
|
||||||
|
bob2 = acfactory.get_unconfigured_account()
|
||||||
|
bob2.import_backup(files[0])
|
||||||
|
bob2.start_io()
|
||||||
|
|
||||||
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
alice_chat_bob.send_text("Hello Bob!")
|
||||||
|
|
||||||
|
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
|
||||||
|
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
|
||||||
|
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||||
|
|
||||||
|
message.mark_seen()
|
||||||
|
while True:
|
||||||
|
event = bob2.wait_for_event()
|
||||||
|
if event.kind == EventType.MSGS_NOTICED:
|
||||||
|
break
|
||||||
|
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import pytest
|
|
||||||
from deltachat_rpc_client import EventType
|
from deltachat_rpc_client import EventType
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_webxdc(acfactory) -> None:
|
||||||
async def test_webxdc(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
await 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:
|
while True:
|
||||||
event = await bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||||
message = bob.get_message_by_id(event.msg_id)
|
message = bob.get_message_by_id(event.msg_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
webxdc_info = await message.get_webxdc_info()
|
webxdc_info = message.get_webxdc_info()
|
||||||
assert webxdc_info == {
|
assert webxdc_info == {
|
||||||
"document": None,
|
"document": None,
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
@@ -28,20 +26,32 @@ async def test_webxdc(acfactory) -> None:
|
|||||||
"summary": None,
|
"summary": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
status_updates = await message.get_webxdc_status_updates()
|
status_updates = message.get_webxdc_status_updates()
|
||||||
assert status_updates == []
|
assert status_updates == []
|
||||||
|
|
||||||
await bob_chat_alice.accept()
|
bob_chat_alice.accept()
|
||||||
await message.send_webxdc_status_update({"payload": 42}, "")
|
message.send_webxdc_status_update({"payload": 42}, "")
|
||||||
await message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
||||||
|
|
||||||
status_updates = await message.get_webxdc_status_updates()
|
status_updates = message.get_webxdc_status_updates()
|
||||||
assert status_updates == [
|
assert status_updates == [
|
||||||
{"payload": 42, "serial": 1, "max_serial": 2},
|
{"payload": 42, "serial": 1, "max_serial": 2},
|
||||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||||
]
|
]
|
||||||
|
|
||||||
status_updates = await message.get_webxdc_status_updates(1)
|
status_updates = message.get_webxdc_status_updates(1)
|
||||||
assert status_updates == [
|
assert status_updates == [
|
||||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("addr")
|
||||||
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||||
|
|
||||||
|
for i in range(2000):
|
||||||
|
message.send_webxdc_status_update({"payload": str(i)}, "description")
|
||||||
|
|||||||
@@ -6,24 +6,27 @@ envlist =
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
pytest --exitfirst {posargs}
|
pytest -n6 {posargs}
|
||||||
setenv =
|
setenv =
|
||||||
# Avoid stack overflow when Rust core is built without optimizations.
|
# Avoid stack overflow when Rust core is built without optimizations.
|
||||||
RUST_MIN_STACK=8388608
|
RUST_MIN_STACK=8388608
|
||||||
passenv =
|
passenv =
|
||||||
DCC_NEW_TMP_EMAIL
|
CHATMAIL_DOMAIN
|
||||||
deps =
|
deps =
|
||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
pytest-timeout
|
||||||
aiohttp
|
pytest-xdist
|
||||||
aiodns
|
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
skip_install = True
|
skip_install = True
|
||||||
deps =
|
deps =
|
||||||
ruff
|
ruff
|
||||||
black
|
|
||||||
commands =
|
commands =
|
||||||
black --quiet --check --diff src/ examples/ tests/
|
ruff format --quiet --diff src/ examples/ tests/
|
||||||
ruff src/ examples/ tests/
|
ruff check src/ examples/ tests/
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
timeout = 300
|
||||||
|
log_cli = true
|
||||||
|
log_level = debug
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "1.111.0"
|
version = "1.138.2"
|
||||||
description = "DeltaChat JSON-RPC server"
|
description = "DeltaChat JSON-RPC server"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -9,20 +9,19 @@ license = "MPL-2.0"
|
|||||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||||
categories = ["cryptography", "std", "email"]
|
categories = ["cryptography", "std", "email"]
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "deltachat-rpc-server"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
||||||
|
deltachat = { path = "..", default-features = false }
|
||||||
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
env_logger = { version = "0.10.0" }
|
env_logger = { version = "0.11.3" }
|
||||||
futures-lite = "1.12.0"
|
futures-lite = "2.3.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde_json = "1.0.91"
|
serde_json = "1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tokio = { version = "1.25.0", features = ["io-std"] }
|
tokio = { version = "1.37.0", features = ["io-std"] }
|
||||||
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
|
tokio-util = "0.7.9"
|
||||||
|
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -30,5 +30,8 @@ deltachat-rpc-server
|
|||||||
The common use case for this program is to create bindings to use Delta Chat core from programming
|
The common use case for this program is to create bindings to use Delta Chat core from programming
|
||||||
languages other than Rust, for example:
|
languages other than Rust, for example:
|
||||||
|
|
||||||
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
|
1. Python: https://pypi.org/project/deltachat-rpc-client/
|
||||||
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
||||||
|
|
||||||
|
Run `deltachat-rpc-server --version` to check the version of the server.
|
||||||
|
Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.
|
||||||
|
|||||||
2
deltachat-rpc-server/npm-package/.gitignore
vendored
Normal file
2
deltachat-rpc-server/npm-package/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
platform_package
|
||||||
|
*.tgz
|
||||||
3
deltachat-rpc-server/npm-package/.npmignore
Normal file
3
deltachat-rpc-server/npm-package/.npmignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
platform_package/*
|
||||||
|
scripts/
|
||||||
|
*.tgz
|
||||||
77
deltachat-rpc-server/npm-package/README.md
Normal file
77
deltachat-rpc-server/npm-package/README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
## npm package for deltachat-rpc-server
|
||||||
|
|
||||||
|
This is the successor of `deltachat-node`,
|
||||||
|
it does not use NAPI bindings but instead uses stdio executables
|
||||||
|
to let you talk to core over jsonrpc over stdio.
|
||||||
|
This simplifies cross-compilation and even reduces binary size (no CFFI layer and no NAPI layer).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
> The **minimum** nodejs version for this package is `20.11`
|
||||||
|
|
||||||
|
```
|
||||||
|
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
||||||
|
import { C } from "@deltachat/jsonrpc-client";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const dc = await startDeltaChat("deltachat-data");
|
||||||
|
console.log(await dc.rpc.getSystemInfo());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
|
||||||
|
|
||||||
|
## How to use on an unsupported platform
|
||||||
|
|
||||||
|
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
|
||||||
|
|
||||||
|
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
|
||||||
|
|
||||||
|
## How does it work when you install it
|
||||||
|
|
||||||
|
NPM automatically installs platform dependent optional dependencies when `os` and `cpu` fields are set correctly.
|
||||||
|
|
||||||
|
references:
|
||||||
|
|
||||||
|
- https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages, [webarchive version](https://web.archive.org/web/20240309234250/https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages)
|
||||||
|
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#cpu
|
||||||
|
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#os
|
||||||
|
|
||||||
|
When you import this package it searches for the rpc server in the following locations and order:
|
||||||
|
|
||||||
|
1. `DELTA_CHAT_RPC_SERVER` environment variable
|
||||||
|
2. in PATH
|
||||||
|
- unless `DELTA_CHAT_SKIP_PATH=1` is specified
|
||||||
|
- searches in .cargo/bin directory first
|
||||||
|
- but there an additional version check is performed
|
||||||
|
3. prebuilds in npm packages
|
||||||
|
|
||||||
|
## How do you built this package in CI
|
||||||
|
|
||||||
|
- To build platform packages, run the `build_platform_package.py` script:
|
||||||
|
```
|
||||||
|
python3 build_platform_package.py <cargo-target>
|
||||||
|
# example
|
||||||
|
python3 build_platform_package.py x86_64-apple-darwin
|
||||||
|
```
|
||||||
|
- Then pass it as an artifact to the last CI action that publishes the main package.
|
||||||
|
- upload all packages from `deltachat-rpc-server/npm-package/platform_package`.
|
||||||
|
- then publish `deltachat-rpc-server/npm-package`,
|
||||||
|
- this will run `update_optional_dependencies_and_version.js` (in the `prepack` script),
|
||||||
|
which puts all platform packages into `optionalDependencies` and updates the `version` in `package.json`
|
||||||
|
|
||||||
|
## How to build a version you can use localy on your host machine for development
|
||||||
|
|
||||||
|
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have seperate scripts for making it work for local installation.
|
||||||
|
|
||||||
|
- If you just need your host platform run `python scripts/make_local_dev_version.py`
|
||||||
|
- note: this clears the `platform_package` folder
|
||||||
|
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple plaftorms with `build_platform_package.py`
|
||||||
|
|
||||||
|
## Thanks to nlnet
|
||||||
|
|
||||||
|
The initial work on this package was funded by nlnet as part of the [Delta Tauri](https://nlnet.nl/project/DeltaTauri/) Project.
|
||||||
39
deltachat-rpc-server/npm-package/index.d.ts
vendored
Normal file
39
deltachat-rpc-server/npm-package/index.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||||
|
|
||||||
|
export interface SearchOptions {
|
||||||
|
/** whether to disable looking for deltachat-rpc-server inside of $PATH */
|
||||||
|
skipSearchInPath: boolean;
|
||||||
|
|
||||||
|
/** whether to disable the DELTA_CHAT_RPC_SERVER environment variable */
|
||||||
|
disableEnvPath: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns absolute path to deltachat-rpc-server binary
|
||||||
|
* @throws when it is not found
|
||||||
|
*/
|
||||||
|
export function getRPCServerPath(
|
||||||
|
options?: Partial<SearchOptions>
|
||||||
|
): Promise<string>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type DeltaChatOverJsonRpcServer = StdioDeltaChat & {
|
||||||
|
shutdown: () => Promise<void>;
|
||||||
|
readonly pathToServerBinary: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param directory directory for accounts folder
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export function startDeltaChat(directory: string, options?: Partial<SearchOptions> ): Promise<DeltaChatOverJsonRpcServer>
|
||||||
|
|
||||||
|
|
||||||
|
export namespace FnTypes {
|
||||||
|
export type getRPCServerPath = typeof getRPCServerPath
|
||||||
|
export type startDeltaChat = typeof startDeltaChat
|
||||||
|
}
|
||||||
143
deltachat-rpc-server/npm-package/index.js
Normal file
143
deltachat-rpc-server/npm-package/index.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
//@ts-check
|
||||||
|
import { execFile, spawn } from "node:child_process";
|
||||||
|
import { stat, readdir } from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import { join, basename } from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import {
|
||||||
|
ENV_VAR_NAME,
|
||||||
|
PATH_EXECUTABLE_NAME,
|
||||||
|
SKIP_SEARCH_IN_PATH,
|
||||||
|
} from "./src/const.js";
|
||||||
|
import {
|
||||||
|
ENV_VAR_LOCATION_NOT_FOUND,
|
||||||
|
FAILED_TO_START_SERVER_EXECUTABLE,
|
||||||
|
NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR,
|
||||||
|
NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR,
|
||||||
|
} from "./src/errors.js";
|
||||||
|
|
||||||
|
// Because this is not compiled by typescript, esm needs this stuff (` with { type: "json" };`,
|
||||||
|
// nodejs still complains about it being experimental, but deno also uses it, so treefit bets taht it will become standard)
|
||||||
|
import package_json from "./package.json" with { type: "json" };
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
// exports
|
||||||
|
// - [ ] a raw starter that has a stdin/out handle thingie like desktop uses
|
||||||
|
// - [X] a function that already wraps the stdio handle from above into the deltachat jsonrpc bindings
|
||||||
|
|
||||||
|
function findRPCServerInNodeModules() {
|
||||||
|
const arch = os.arch();
|
||||||
|
const operating_system = process.platform;
|
||||||
|
const package_name = `@deltachat/stdio-rpc-server-${operating_system}-${arch}`;
|
||||||
|
try {
|
||||||
|
const { resolve } = createRequire(import.meta.url);
|
||||||
|
return resolve(package_name);
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("findRpcServerInNodeModules", error);
|
||||||
|
if (Object.keys(package_json.optionalDependencies).includes(package_name)) {
|
||||||
|
throw new Error(NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name));
|
||||||
|
} else {
|
||||||
|
throw new Error(NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("./index").FnTypes.getRPCServerPath} */
|
||||||
|
export async function getRPCServerPath(
|
||||||
|
options = { skipSearchInPath: false, disableEnvPath: false }
|
||||||
|
) {
|
||||||
|
// @TODO: improve confusing naming of these options
|
||||||
|
const { skipSearchInPath, disableEnvPath } = options;
|
||||||
|
// 1. check if it is set as env var
|
||||||
|
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
|
||||||
|
try {
|
||||||
|
if (!(await stat(process.env[ENV_VAR_NAME])).isFile()) {
|
||||||
|
throw new Error(
|
||||||
|
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(ENV_VAR_LOCATION_NOT_FOUND());
|
||||||
|
}
|
||||||
|
return process.env[ENV_VAR_NAME];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. check if it can be found in PATH
|
||||||
|
if (!process.env[SKIP_SEARCH_IN_PATH] && !skipSearchInPath) {
|
||||||
|
const exec = promisify(execFile);
|
||||||
|
|
||||||
|
const { stdout: executable } =
|
||||||
|
os.platform() !== "win32"
|
||||||
|
? await exec("command", ["-v", PATH_EXECUTABLE_NAME])
|
||||||
|
: await exec("where", [PATH_EXECUTABLE_NAME]);
|
||||||
|
|
||||||
|
// by just trying to execute it and then use "command -v deltachat-rpc-server" (unix) or "where deltachat-rpc-server" (windows) to get the path to the executable
|
||||||
|
if (executable.length > 1) {
|
||||||
|
// test if it is the right version
|
||||||
|
try {
|
||||||
|
// for some unknown reason it is in stderr and not in stdout
|
||||||
|
const { stderr } = await promisify(execFile)(executable, ["--version"]);
|
||||||
|
const version = stderr.slice(0, stderr.indexOf("\n"));
|
||||||
|
if (package_json.version !== version) {
|
||||||
|
throw new Error(
|
||||||
|
`version mismatch: (npm package: ${package_json.version}) (installed ${PATH_EXECUTABLE_NAME} version: ${version})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return executable;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Found executable in PATH, but there was an error: " + error
|
||||||
|
);
|
||||||
|
console.error("So falling back to using prebuild...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. check for prebuilds
|
||||||
|
|
||||||
|
return findRPCServerInNodeModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||||
|
|
||||||
|
/** @type {import("./index").FnTypes.startDeltaChat} */
|
||||||
|
export async function startDeltaChat(directory, options) {
|
||||||
|
const pathToServerBinary = await getRPCServerPath(options);
|
||||||
|
const server = spawn(pathToServerBinary, {
|
||||||
|
env: {
|
||||||
|
RUST_LOG: process.env.RUST_LOG || "info",
|
||||||
|
DC_ACCOUNTS_PATH: directory,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("error", (err) => {
|
||||||
|
throw new Error(FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err));
|
||||||
|
});
|
||||||
|
let shouldClose = false;
|
||||||
|
|
||||||
|
server.on("exit", () => {
|
||||||
|
if (shouldClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error("Server quit");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.stderr.pipe(process.stderr);
|
||||||
|
|
||||||
|
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
|
||||||
|
//@ts-expect-error
|
||||||
|
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
|
||||||
|
|
||||||
|
dc.shutdown = async () => {
|
||||||
|
shouldClose = true;
|
||||||
|
if (!server.kill()) {
|
||||||
|
console.log("server termination failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
dc.pathToServerBinary = pathToServerBinary;
|
||||||
|
|
||||||
|
return dc;
|
||||||
|
}
|
||||||
15
deltachat-rpc-server/npm-package/package.json
Normal file
15
deltachat-rpc-server/npm-package/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"name": "@deltachat/stdio-rpc-server",
|
||||||
|
"optionalDependencies": {},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@deltachat/jsonrpc-client": "*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepack": "node scripts/update_optional_dependencies_and_version.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"version": "1.138.2"
|
||||||
|
}
|
||||||
53
deltachat-rpc-server/npm-package/scripts/build_platform_package.py
Executable file
53
deltachat-rpc-server/npm-package/scripts/build_platform_package.py
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import subprocess
|
||||||
|
from sys import argv
|
||||||
|
from os import path, makedirs, chdir
|
||||||
|
from shutil import copy
|
||||||
|
from src.make_package import write_package_json
|
||||||
|
|
||||||
|
# ensure correct working directory
|
||||||
|
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
|
||||||
|
|
||||||
|
if len(argv) < 2:
|
||||||
|
print("First argument should be target architecture as required by cargo")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
target = argv[1].strip()
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
["cargo", "build", "--release", "-p", "deltachat-rpc-server", "--target", target],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
newpath = "platform_package"
|
||||||
|
if not path.exists(newpath):
|
||||||
|
makedirs(newpath)
|
||||||
|
|
||||||
|
# make new folder
|
||||||
|
|
||||||
|
platform_path = "platform_package/" + target
|
||||||
|
if not path.exists(platform_path):
|
||||||
|
makedirs(platform_path)
|
||||||
|
|
||||||
|
# copy binary it over
|
||||||
|
|
||||||
|
|
||||||
|
def binary_path(binary_name):
|
||||||
|
return "../../target/" + target + "/release/" + binary_name
|
||||||
|
|
||||||
|
|
||||||
|
my_binary_name = "deltachat-rpc-server"
|
||||||
|
|
||||||
|
if not path.isfile(binary_path("deltachat-rpc-server")):
|
||||||
|
my_binary_name = "deltachat-rpc-server.exe"
|
||||||
|
if not path.isfile(binary_path("deltachat-rpc-server.exe")):
|
||||||
|
print("Did not find the build")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
my_binary_path = binary_path(my_binary_name)
|
||||||
|
|
||||||
|
copy(my_binary_path, platform_path + "/" + my_binary_name)
|
||||||
|
|
||||||
|
# make a package.json for it
|
||||||
|
|
||||||
|
write_package_json(platform_path, target, my_binary_name)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# This script is for making a version of the npm packet that you can install locally
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from sys import argv
|
||||||
|
from os import path, makedirs, chdir
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import tomllib
|
||||||
|
from shutil import copy, rmtree
|
||||||
|
|
||||||
|
# ensure correct working directory
|
||||||
|
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
|
||||||
|
|
||||||
|
# get host target with "rustc -vV"
|
||||||
|
output = subprocess.run(["rustc", "-vV"], capture_output=True)
|
||||||
|
host_target = re.search('host: ([-\\w]*)', output.stdout.decode("utf-8")).group(1)
|
||||||
|
print("host target to build for is:", host_target)
|
||||||
|
|
||||||
|
# clean platform_package folder
|
||||||
|
newpath = r'platform_package'
|
||||||
|
if not path.exists(newpath):
|
||||||
|
makedirs(newpath)
|
||||||
|
else:
|
||||||
|
rmtree(path.join(path.dirname(path.abspath(__file__)), "../platform_package/"))
|
||||||
|
makedirs(newpath)
|
||||||
|
|
||||||
|
# run build_platform_package.py with the host's target to build it
|
||||||
|
subprocess.run(["python", "scripts/build_platform_package.py", host_target], capture_output=False, check=True)
|
||||||
|
|
||||||
|
# run update_optional_dependencies_and_version.js to adjust the package / make it installable locally
|
||||||
|
subprocess.run(["node", "scripts/update_optional_dependencies_and_version.js", "--local"], capture_output=False, check=True)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import subprocess
|
||||||
|
from sys import argv
|
||||||
|
from os import path, makedirs, chdir, chmod, stat
|
||||||
|
import json
|
||||||
|
from shutil import copy
|
||||||
|
from src.make_package import write_package_json
|
||||||
|
|
||||||
|
# ensure correct working directory
|
||||||
|
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
|
||||||
|
|
||||||
|
if len(argv) < 3:
|
||||||
|
print("First argument should be target architecture as required by cargo")
|
||||||
|
print("Second argument should be the location of th built binary (binary_path)")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
target = argv[1].strip()
|
||||||
|
binary_path = argv[2].strip()
|
||||||
|
|
||||||
|
output = subprocess.run(["rustc","--print","target-list"], capture_output=True, check=True)
|
||||||
|
available_targets = output.stdout.decode("utf-8")
|
||||||
|
|
||||||
|
if available_targets.find(target) == -1:
|
||||||
|
print("target", target, "is not known / not valid")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
newpath = r'platform_package'
|
||||||
|
if not path.exists(newpath):
|
||||||
|
makedirs(newpath)
|
||||||
|
|
||||||
|
# make new folder
|
||||||
|
|
||||||
|
platform_path = 'platform_package/' + target
|
||||||
|
if not path.exists(platform_path):
|
||||||
|
makedirs(platform_path)
|
||||||
|
|
||||||
|
# copy binary it over
|
||||||
|
|
||||||
|
my_binary_name = path.basename(binary_path)
|
||||||
|
new_binary_path = platform_path + "/" + my_binary_name
|
||||||
|
copy(binary_path, new_binary_path)
|
||||||
|
chmod(new_binary_path, 0o555) # everyone can read & execute, nobody can write
|
||||||
|
|
||||||
|
# make a package.json for it
|
||||||
|
|
||||||
|
write_package_json(platform_path, target, my_binary_name)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
def convert_cpu_arch_to_npm_cpu_arch(arch):
|
||||||
|
if arch == "x86_64":
|
||||||
|
return "x64"
|
||||||
|
if arch == "i686":
|
||||||
|
return "i32"
|
||||||
|
if arch == "aarch64":
|
||||||
|
return "arm64"
|
||||||
|
if arch == "armv7" or arch == "arm":
|
||||||
|
return "arm"
|
||||||
|
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.arch':", arch)
|
||||||
|
return arch
|
||||||
|
|
||||||
|
def convert_os_to_npm_os(os):
|
||||||
|
if os == "windows":
|
||||||
|
return "win32"
|
||||||
|
if os == "darwin" or os == "linux":
|
||||||
|
return os
|
||||||
|
if os.startswith("android"):
|
||||||
|
return "android"
|
||||||
|
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.platform':", os)
|
||||||
|
return os
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user