mirror of
https://github.com/chatmail/core.git
synced 2026-05-09 18:06:29 +03:00
Compare commits
1005 Commits
v2.2.0
...
iequidoo/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a18bf74620 | ||
|
|
ca70fb9b3a | ||
|
|
045b586569 | ||
|
|
18e1ecbb94 | ||
|
|
6fdee2b92d | ||
|
|
9ebd4769f5 | ||
|
|
741d1beed8 | ||
|
|
ac8b2d2fca | ||
|
|
d75c05e717 | ||
|
|
35ceb51ffc | ||
|
|
8492f84b13 | ||
|
|
e713c231eb | ||
|
|
80c9ca44ca | ||
|
|
6e04993e75 | ||
|
|
4529ed2240 | ||
|
|
b5ebd6f686 | ||
|
|
4d537544ef | ||
|
|
4a16c0c3dd | ||
|
|
4c01802982 | ||
|
|
4b528e426b | ||
|
|
585de7d18b | ||
|
|
0598fdcab3 | ||
|
|
903e736fa2 | ||
|
|
f20907d597 | ||
|
|
804590c7f3 | ||
|
|
62d4cf4ed8 | ||
|
|
0d772d4dba | ||
|
|
408afa5656 | ||
|
|
1a6249c10f | ||
|
|
daea820fe5 | ||
|
|
b806efa096 | ||
|
|
0580056b62 | ||
|
|
63f96d9138 | ||
|
|
1204a94252 | ||
|
|
73e8bee120 | ||
|
|
8c927c7f86 | ||
|
|
7f9c184659 | ||
|
|
287d730556 | ||
|
|
82bb77b056 | ||
|
|
aa1f129a48 | ||
|
|
0ad58f7a59 | ||
|
|
6d61f7e071 | ||
|
|
fa68c1f0e4 | ||
|
|
5f1d54100f | ||
|
|
25cd7b65fd | ||
|
|
63596a4940 | ||
|
|
8bc84e13de | ||
|
|
ba8c39ff5b | ||
|
|
7de58f5329 | ||
|
|
1fd4a19e56 | ||
|
|
1ab6645bbc | ||
|
|
c17d067a1a | ||
|
|
3aeb2d44b7 | ||
|
|
d069e75cd8 | ||
|
|
ad5e904d1c | ||
|
|
38affa2c62 | ||
|
|
6dfc6f8780 | ||
|
|
8cca0cf75d | ||
|
|
b81f50be8f | ||
|
|
970222f376 | ||
|
|
83e31a5f17 | ||
|
|
31fabb24df | ||
|
|
66df0d2a3c | ||
|
|
5a6b1c62dd | ||
|
|
18d878378f | ||
|
|
3c25e4b726 | ||
|
|
8cd06bb785 | ||
|
|
bb816ff398 | ||
|
|
9fcb26c849 | ||
|
|
d9474a678e | ||
|
|
f1e1a240ac | ||
|
|
82924952fb | ||
|
|
7daa6cc8d9 | ||
|
|
b8cfee7e9e | ||
|
|
03fc2d26ee | ||
|
|
942172a31a | ||
|
|
04c0e7da16 | ||
|
|
4178671839 | ||
|
|
49e8065b4c | ||
|
|
1c24ad91eb | ||
|
|
8f7777b843 | ||
|
|
caeddcd57b | ||
|
|
418fd92d6d | ||
|
|
a70924a1d6 | ||
|
|
8e91eecb3d | ||
|
|
89f948028d | ||
|
|
9adb71bf8f | ||
|
|
fe2ba05804 | ||
|
|
072fc34c77 | ||
|
|
cb00bd7043 | ||
|
|
f766c11075 | ||
|
|
89e450894d | ||
|
|
6ef1f7d52b | ||
|
|
33dc3d20ad | ||
|
|
b8a0c37efe | ||
|
|
ad7f873c68 | ||
|
|
3236c8bbf4 | ||
|
|
dab7ca19fe | ||
|
|
520cd0ede8 | ||
|
|
5d5deedec3 | ||
|
|
f33e21ccb9 | ||
|
|
00c06c490b | ||
|
|
8b58b16cb5 | ||
|
|
d6971ee4ac | ||
|
|
e3bf6bf352 | ||
|
|
4b81cd2fc8 | ||
|
|
be920bf3bf | ||
|
|
602f0a088e | ||
|
|
a2bb8962cb | ||
|
|
795fe9a38b | ||
|
|
60bc4011f7 | ||
|
|
f552cf93b4 | ||
|
|
f75a7986b5 | ||
|
|
3b8f1934f3 | ||
|
|
c8716f50aa | ||
|
|
d2097d3523 | ||
|
|
1219cbe1a3 | ||
|
|
bc48b17e93 | ||
|
|
7233b4b811 | ||
|
|
d1e0088201 | ||
|
|
a5e41b0b49 | ||
|
|
2f76fd98dd | ||
|
|
6235f2a01a | ||
|
|
ec5117a6c2 | ||
|
|
d6e3a8829b | ||
|
|
2340818488 | ||
|
|
f175d2fed9 | ||
|
|
d318bbb0f4 | ||
|
|
a0f14a5978 | ||
|
|
7e49033f92 | ||
|
|
626ac8161a | ||
|
|
28cce5e31d | ||
|
|
3b87e27f34 | ||
|
|
24b21c0588 | ||
|
|
eb666d4cc3 | ||
|
|
ef265689dd | ||
|
|
49223792f9 | ||
|
|
920da083d1 | ||
|
|
8f1bf963b4 | ||
|
|
e33d50b4e0 | ||
|
|
f1dc03a4ee | ||
|
|
5d90cc7a2a | ||
|
|
68e630eb82 | ||
|
|
ef718bb869 | ||
|
|
f1860f90d4 | ||
|
|
a947f4296f | ||
|
|
8c3139f7a2 | ||
|
|
3dd7defaa1 | ||
|
|
3096dd6027 | ||
|
|
ee62d2d281 | ||
|
|
6095971f67 | ||
|
|
32ff5b7a6b | ||
|
|
b87805ab24 | ||
|
|
c8716ad85a | ||
|
|
4dd0ba2c72 | ||
|
|
a24248a90b | ||
|
|
af16fc9038 | ||
|
|
c99b8a4482 | ||
|
|
76e2c36d85 | ||
|
|
1b8bf4ed23 | ||
|
|
c553357c60 | ||
|
|
ebe8550c52 | ||
|
|
2637c3bea4 | ||
|
|
d1f1633c60 | ||
|
|
98b55ec15f | ||
|
|
6a3ef20a99 | ||
|
|
59be03a7eb | ||
|
|
8528184fa3 | ||
|
|
5ab1fdca2e | ||
|
|
f616d1bd6c | ||
|
|
e885e052c3 | ||
|
|
6b1e62faba | ||
|
|
7b9e7ae611 | ||
|
|
aedc60f1cc | ||
|
|
017099215c | ||
|
|
e86b170969 | ||
|
|
452ac8a1bc | ||
|
|
5d06ca3c8e | ||
|
|
bdc9e7ce56 | ||
|
|
e30d833c94 | ||
|
|
16668b45e9 | ||
|
|
b148be2618 | ||
|
|
191e6c2821 | ||
|
|
7b700591f4 | ||
|
|
98f03743c6 | ||
|
|
bcaf1284e2 | ||
|
|
fba4e63961 | ||
|
|
810dab12dc | ||
|
|
c0cc2ae816 | ||
|
|
528305e12b | ||
|
|
6e0586058d | ||
|
|
296ed6d74a | ||
|
|
8116460f14 | ||
|
|
52f4293bc5 | ||
|
|
cff0192e38 | ||
|
|
6f17a86903 | ||
|
|
4eb77d5a83 | ||
|
|
e06372c954 | ||
|
|
50cd2514cd | ||
|
|
ba00251572 | ||
|
|
e690186236 | ||
|
|
e14151d6cc | ||
|
|
c6cdccdb97 | ||
|
|
822a99ea9c | ||
|
|
bf02785a36 | ||
|
|
01b2aa0f66 | ||
|
|
fb46c34b55 | ||
|
|
9393753190 | ||
|
|
d9056fd187 | ||
|
|
7b17b1f8b8 | ||
|
|
d8d7f12af0 | ||
|
|
0150d38ddd | ||
|
|
11b6a108f5 | ||
|
|
54858361a9 | ||
|
|
6a705a3ef6 | ||
|
|
a23e41ea6d | ||
|
|
bdca3e5c09 | ||
|
|
a61a25f139 | ||
|
|
5404e683eb | ||
|
|
80acc9d467 | ||
|
|
3c5af7a559 | ||
|
|
f7e9973fb4 | ||
|
|
c0a3d77301 | ||
|
|
9891c2a531 | ||
|
|
f85c625799 | ||
|
|
b30f93a57d | ||
|
|
a95bf77868 | ||
|
|
d26fa715b5 | ||
|
|
1b43aac356 | ||
|
|
53acfaa054 | ||
|
|
874e38c146 | ||
|
|
cce8e3bc5a | ||
|
|
1e20055523 | ||
|
|
abb93cd79d | ||
|
|
5f84be718a | ||
|
|
d1c3a679a0 | ||
|
|
0c4e32363e | ||
|
|
89b5675b83 | ||
|
|
8ff8ba7416 | ||
|
|
e3a7d555a8 | ||
|
|
964bbad53e | ||
|
|
a1eb376131 | ||
|
|
3c4ce17f1e | ||
|
|
0622289420 | ||
|
|
c928015f20 | ||
|
|
b10acd194e | ||
|
|
b94792706a | ||
|
|
bfae2296b7 | ||
|
|
e7625ca231 | ||
|
|
ab08a47298 | ||
|
|
b85fa84a37 | ||
|
|
ccd3caf4a7 | ||
|
|
5f248954dc | ||
|
|
a6c7958739 | ||
|
|
c724e2981c | ||
|
|
ffd9f80f8b | ||
|
|
42cb9fe890 | ||
|
|
914486cb32 | ||
|
|
526b3b0271 | ||
|
|
1c439b5ef4 | ||
|
|
f97c75f146 | ||
|
|
76a36a35bf | ||
|
|
dc4249a2ff | ||
|
|
957c0b7c56 | ||
|
|
8df9b9e4d9 | ||
|
|
692e1019b0 | ||
|
|
2511b03726 | ||
|
|
c39651a8d4 | ||
|
|
8230336936 | ||
|
|
e1e8407905 | ||
|
|
ffce0dfc9a | ||
|
|
e2eec2f1f8 | ||
|
|
072c0061ee | ||
|
|
cb783ffc12 | ||
|
|
af182a85a3 | ||
|
|
7d8989a068 | ||
|
|
d7bf10d7a4 | ||
|
|
f1e90c73cd | ||
|
|
c39d2f42ef | ||
|
|
e60f4ff70a | ||
|
|
ba64d8d19b | ||
|
|
4041d9a54e | ||
|
|
bbf9a86bce | ||
|
|
cdb0e0ce29 | ||
|
|
0e7f3c8238 | ||
|
|
16c85a9585 | ||
|
|
ff7023580f | ||
|
|
58d457140e | ||
|
|
b531a3c012 | ||
|
|
f055f6226c | ||
|
|
e95dca87bd | ||
|
|
0d9442458a | ||
|
|
60cf483270 | ||
|
|
598d759b8d | ||
|
|
10b93b3943 | ||
|
|
5a06d08613 | ||
|
|
85de4bf678 | ||
|
|
624fc394d9 | ||
|
|
9deba0cf2a | ||
|
|
b95d28b2d9 | ||
|
|
2131f5e9c0 | ||
|
|
a63f695b85 | ||
|
|
de25eb90ff | ||
|
|
3fdda6f3b8 | ||
|
|
c475882727 | ||
|
|
166e259b18 | ||
|
|
cc38298163 | ||
|
|
983f43c33c | ||
|
|
5028842fd5 | ||
|
|
e78b509d0a | ||
|
|
583979c6fc | ||
|
|
5bfd8dd517 | ||
|
|
32b0ca81f8 | ||
|
|
8dd7e5c5dd | ||
|
|
5bb0b86f6a | ||
|
|
ed2b0e8f03 | ||
|
|
8152ff518e | ||
|
|
cbcfb7087e | ||
|
|
396104af47 | ||
|
|
69f6727751 | ||
|
|
b72a677f4c | ||
|
|
00e78eecf6 | ||
|
|
8b0621b724 | ||
|
|
63bf4c4f33 | ||
|
|
d6bce56d18 | ||
|
|
c8dec0dcdd | ||
|
|
509644ea5f | ||
|
|
3e95239e71 | ||
|
|
74d4b823d2 | ||
|
|
1bcfb90b90 | ||
|
|
411ee511ed | ||
|
|
e5a30c341c | ||
|
|
3d409c37a1 | ||
|
|
b46c86c9b7 | ||
|
|
e5e268f503 | ||
|
|
633536bb13 | ||
|
|
94ee485155 | ||
|
|
ec0dc8bcad | ||
|
|
49296e3014 | ||
|
|
2b93e856e4 | ||
|
|
c5be7df1d7 | ||
|
|
6b74cb6539 | ||
|
|
de2ac8cca2 | ||
|
|
085fcd2751 | ||
|
|
83f30e4a54 | ||
|
|
e79b4baa09 | ||
|
|
1e0c0d8efa | ||
|
|
378fb09c80 | ||
|
|
ff2fbebff0 | ||
|
|
50a73666fd | ||
|
|
61a8eff2ad | ||
|
|
cbd379fdf0 | ||
|
|
fe826f762e | ||
|
|
2019debe99 | ||
|
|
6c4f4bfd19 | ||
|
|
44b0736216 | ||
|
|
3b29469102 | ||
|
|
6325a35b5b | ||
|
|
c08644490a | ||
|
|
955f79923a | ||
|
|
c9026bff2c | ||
|
|
4fc0d0f53d | ||
|
|
1bf24618fa | ||
|
|
3f98e45c29 | ||
|
|
26ddcfaaed | ||
|
|
f0a12d493c | ||
|
|
c848ea7eda | ||
|
|
7c55356271 | ||
|
|
f4ee01ecca | ||
|
|
448c0d2268 | ||
|
|
3325270896 | ||
|
|
b563064b26 | ||
|
|
8d32d3ae0c | ||
|
|
c5f19f67a9 | ||
|
|
baeb31b5fa | ||
|
|
5d3bc00fd5 | ||
|
|
424928b660 | ||
|
|
1b8c732611 | ||
|
|
2531dfea1d | ||
|
|
9003b248aa | ||
|
|
35875f9b32 | ||
|
|
008e6c4af3 | ||
|
|
a6baba1852 | ||
|
|
a6b2a54e46 | ||
|
|
99aa99eb5b | ||
|
|
566395f1fa | ||
|
|
4ccd3cb665 | ||
|
|
f5e1e2678b | ||
|
|
c3a5e3ac0d | ||
|
|
b2f31c8148 | ||
|
|
29c57ad065 | ||
|
|
82a0d6b0ab | ||
|
|
5ff323ce15 | ||
|
|
a67a5299bf | ||
|
|
659d21aa9d | ||
|
|
8f604e74ec | ||
|
|
e1ebf3e96d | ||
|
|
76171aea2e | ||
|
|
96b8d1720e | ||
|
|
47b49fd02e | ||
|
|
f50e3d6ffa | ||
|
|
2ecb537307 | ||
|
|
ccae73f6db | ||
|
|
fce91f3ee0 | ||
|
|
d446a16fc6 | ||
|
|
c3a6e48882 | ||
|
|
46ec3a469b | ||
|
|
fe3b1ea16d | ||
|
|
ed300b6f97 | ||
|
|
e456be4e21 | ||
|
|
ba4055b7df | ||
|
|
c06f53cb86 | ||
|
|
13dafa46b5 | ||
|
|
d552250dc4 | ||
|
|
1383e790c3 | ||
|
|
b536902827 | ||
|
|
2631745a57 | ||
|
|
46bbe5f077 | ||
|
|
0f14edd5d9 | ||
|
|
fe6e942191 | ||
|
|
67aac12995 | ||
|
|
f2fb59f0cc | ||
|
|
55ab1b86f7 | ||
|
|
ceba687df3 | ||
|
|
7e811469b3 | ||
|
|
cdacad235e | ||
|
|
c766397abc | ||
|
|
14a59afd5d | ||
|
|
9c883e6424 | ||
|
|
9d7db20225 | ||
|
|
fdb583b5e9 | ||
|
|
8d6f4b0354 | ||
|
|
284469363e | ||
|
|
6078c79020 | ||
|
|
161e5ae358 | ||
|
|
a66859ebf2 | ||
|
|
de902babc3 | ||
|
|
98a8679779 | ||
|
|
8ce3ecc809 | ||
|
|
50a1f907a5 | ||
|
|
c2bf2a32b5 | ||
|
|
84710384fe | ||
|
|
f60fce22ed | ||
|
|
0d2f2b3266 | ||
|
|
516f0a1a98 | ||
|
|
25750de4e1 | ||
|
|
a89ce8ce7a | ||
|
|
9ac64ea6b9 | ||
|
|
294e23d82d | ||
|
|
184736723f | ||
|
|
cea528ed61 | ||
|
|
9b11f53da6 | ||
|
|
5c339efb70 | ||
|
|
d71c163c7d | ||
|
|
19fde9594f | ||
|
|
20b3a06adf | ||
|
|
b0127fa381 | ||
|
|
6a293aebe2 | ||
|
|
fd90493766 | ||
|
|
b1883c802b | ||
|
|
71ee32b8b7 | ||
|
|
84161f4202 | ||
|
|
4af9463a91 | ||
|
|
ddd4fc49a2 | ||
|
|
7d5bedde4d | ||
|
|
e34fee72a0 | ||
|
|
7ba4a43253 | ||
|
|
a09fd4577a | ||
|
|
525a3539d2 | ||
|
|
fbcdd45015 | ||
|
|
1ea8ed6442 | ||
|
|
f6817131b8 | ||
|
|
28fc1d2ff2 | ||
|
|
5925f72316 | ||
|
|
8dfa5fc37e | ||
|
|
49b04e8789 | ||
|
|
d87d87f467 | ||
|
|
bf72b3ad49 | ||
|
|
30f2981259 | ||
|
|
121bfd1fa8 | ||
|
|
9e2a4325e9 | ||
|
|
4509c1bd06 | ||
|
|
3133d89dcc | ||
|
|
99775458c4 | ||
|
|
e432960246 | ||
|
|
58cd133b5c | ||
|
|
3d234e7fc7 | ||
|
|
595258ae05 | ||
|
|
06b2a890da | ||
|
|
95ed31391d | ||
|
|
98944efdb8 | ||
|
|
3f27be9bcb | ||
|
|
5902fe2cbe | ||
|
|
73e0f81e83 | ||
|
|
cbe842735e | ||
|
|
72bc9f0ae4 | ||
|
|
0defa117a0 | ||
|
|
3821cfab0c | ||
|
|
09f159991e | ||
|
|
014d2ace76 | ||
|
|
646728372b | ||
|
|
7c30aef2ed | ||
|
|
c38d02728e | ||
|
|
dea1b414db | ||
|
|
aa5ee19340 | ||
|
|
9271ecd208 | ||
|
|
952f6735a2 | ||
|
|
a50aa3b6e9 | ||
|
|
23d95df66a | ||
|
|
6db2cf6144 | ||
|
|
47c1e54219 | ||
|
|
b41c309e21 | ||
|
|
f7ae2abe52 | ||
|
|
3a7f82c66e | ||
|
|
d75a78d446 | ||
|
|
676132457f | ||
|
|
8bce137e06 | ||
|
|
f359a9c451 | ||
|
|
0d97a5b511 | ||
|
|
7ccc021aea | ||
|
|
08e9cdc487 | ||
|
|
12cee23924 | ||
|
|
5fb118e5a3 | ||
|
|
1ec3f45dc1 | ||
|
|
e4e19b57b3 | ||
|
|
2efb128fec | ||
|
|
4a5d5bdeb1 | ||
|
|
cde4a61be7 | ||
|
|
ca2b4d7a6f | ||
|
|
ef61c0c408 | ||
|
|
dc5f939ac6 | ||
|
|
2c0092738f | ||
|
|
b1e6cf2052 | ||
|
|
343dca87f7 | ||
|
|
4d06f5a8ae | ||
|
|
a4bec7dc70 | ||
|
|
1f32c5ab40 | ||
|
|
43e8d5cc6c | ||
|
|
7e4547582e | ||
|
|
721b9cebef | ||
|
|
0d50d8703f | ||
|
|
9aba299c75 | ||
|
|
2854f87a9d | ||
|
|
145a5813e8 | ||
|
|
4cb129a67e | ||
|
|
7bf7ec3d32 | ||
|
|
8a7498a9a8 | ||
|
|
c41a69ea1e | ||
|
|
38a547dfda | ||
|
|
7c998af973 | ||
|
|
6b6ec2a4b7 | ||
|
|
b1fa1055d7 | ||
|
|
15ce05b0c7 | ||
|
|
8112183429 | ||
|
|
b9ae74fab2 | ||
|
|
fdff90eba4 | ||
|
|
a4a54d3648 | ||
|
|
98fb760f49 | ||
|
|
f553c094eb | ||
|
|
bbb4bed996 | ||
|
|
25088f2dcb | ||
|
|
552e9f4052 | ||
|
|
b3616a013f | ||
|
|
c378b1218d | ||
|
|
21f6e7c676 | ||
|
|
ac543ad251 | ||
|
|
183898b137 | ||
|
|
56204ae701 | ||
|
|
7906405400 | ||
|
|
531e0bc914 | ||
|
|
3637fe67a7 | ||
|
|
8eef79f95d | ||
|
|
6077499f07 | ||
|
|
94d2d8cfd7 | ||
|
|
ba3cad6ad6 | ||
|
|
c9c362d5ff | ||
|
|
6514b4ca7f | ||
|
|
e7e31d7914 | ||
|
|
51d6855e0d | ||
|
|
2f90b55309 | ||
|
|
be3e202470 | ||
|
|
57aadfbbf6 | ||
|
|
849cde9757 | ||
|
|
b4cd99fc56 | ||
|
|
9305a0676c | ||
|
|
39c9ba19ef | ||
|
|
af574279fd | ||
|
|
713c929e03 | ||
|
|
c83c131a37 | ||
|
|
0d0602a4a5 | ||
|
|
abfb556377 | ||
|
|
72788daca0 | ||
|
|
16bd87c78f | ||
|
|
d44e2420bc | ||
|
|
88d213fcdb | ||
|
|
fb14acb0fb | ||
|
|
a5c470fbae | ||
|
|
6bdba33d32 | ||
|
|
c6ace749e3 | ||
|
|
22ebd6436f | ||
|
|
cdfe436124 | ||
|
|
e8823fcf35 | ||
|
|
0136cfaf6a | ||
|
|
07069c348b | ||
|
|
26f6b85ff9 | ||
|
|
10b6dd1f11 | ||
|
|
cae642b024 | ||
|
|
54a2e94525 | ||
|
|
9d4ad00fc0 | ||
|
|
102b72aadd | ||
|
|
1c4d2dd78e | ||
|
|
cd50c263e8 | ||
|
|
1dbcd7f1f4 | ||
|
|
c6894f56b2 | ||
|
|
e2ae6ae013 | ||
|
|
966ea28f83 | ||
|
|
6611a9fa02 | ||
|
|
dc4ea1865a | ||
|
|
4b1dff601d | ||
|
|
a66808e25a | ||
|
|
7b54954401 | ||
|
|
d39ed9d0f1 | ||
|
|
c499dabbe1 | ||
|
|
e70307af1f | ||
|
|
69a3a31554 | ||
|
|
1cb0a25e16 | ||
|
|
fdea6c8af3 | ||
|
|
2e9fd1c25d | ||
|
|
1b1a5f170e | ||
|
|
1946603be6 | ||
|
|
c43b622c23 | ||
|
|
73bf6983b9 | ||
|
|
aaa0f8e245 | ||
|
|
5a1e0e8824 | ||
|
|
cf5b145ce0 | ||
|
|
dd11a0e29a | ||
|
|
3d86cb5953 | ||
|
|
75eb94e44f | ||
|
|
7fef812b1e | ||
|
|
5f174ceaf2 | ||
|
|
06b038ab5d | ||
|
|
b20da3cb0e | ||
|
|
a3328ea2de | ||
|
|
ee75094bef | ||
|
|
a40fd288fc | ||
|
|
81ba2d20d6 | ||
|
|
f04c881b8c | ||
|
|
ee6b9075aa | ||
|
|
9c2a13b88e | ||
|
|
1db6ea70cc | ||
|
|
da2d9620cd | ||
|
|
d1dcb739f2 | ||
|
|
e34687ba42 | ||
|
|
5034449009 | ||
|
|
997e8216bf | ||
|
|
7f059140be | ||
|
|
c9b3da4a1a | ||
|
|
098084b9a7 | ||
|
|
9bc2aeebb8 | ||
|
|
56370c2f90 | ||
|
|
59959259bf | ||
|
|
08f8f488b1 | ||
|
|
f34311d5c4 | ||
|
|
885a5efa39 | ||
|
|
8b4c718b6b | ||
|
|
2ada3cd613 | ||
|
|
b920552fc3 | ||
|
|
92c31903c6 | ||
|
|
145145f0fb | ||
|
|
05ba206c5a | ||
|
|
9f0d106818 | ||
|
|
21caf87119 | ||
|
|
4abc695790 | ||
|
|
df1a7ca386 | ||
|
|
a06ba35ce1 | ||
|
|
18445c09c2 | ||
|
|
f428033d95 | ||
|
|
0e30dd895f | ||
|
|
c001a9a983 | ||
|
|
5f3948b462 | ||
|
|
45a1d81805 | ||
|
|
19d7799324 | ||
|
|
24e18c1485 | ||
|
|
3eb1a7dfac | ||
|
|
2f2a147efb | ||
|
|
f4938465c3 | ||
|
|
129137b5de | ||
|
|
ec3f765727 | ||
|
|
a743ad9490 | ||
|
|
89315b8ef2 | ||
|
|
e7348a4fd8 | ||
|
|
c68244692d | ||
|
|
3c93f61b4d | ||
|
|
51b9e86d71 | ||
|
|
347938a9f9 | ||
|
|
9897ef2e9b | ||
|
|
2f34a740c7 | ||
|
|
fc81cef113 | ||
|
|
04c2585c27 | ||
|
|
59fac54f7b | ||
|
|
65b61efb31 | ||
|
|
afc74b0829 | ||
|
|
2481a0f48e | ||
|
|
6c24edb40d | ||
|
|
e4178789da | ||
|
|
b417ba86bc | ||
|
|
498a831873 | ||
|
|
c6722d36de | ||
|
|
90f0d5c060 | ||
|
|
90ec2f2518 | ||
|
|
5b66535134 | ||
|
|
eea848f72b | ||
|
|
214a1d3e2d | ||
|
|
e270a502d1 | ||
|
|
b863345600 | ||
|
|
61b49a9339 | ||
|
|
41c80cf3f2 | ||
|
|
6fd3645360 | ||
|
|
b812d0a7f7 | ||
|
|
e8a4c9237d | ||
|
|
5256013615 | ||
|
|
9826c28581 | ||
|
|
9ceceebdc3 | ||
|
|
187d913f84 | ||
|
|
4a0b180d86 | ||
|
|
6fa6055912 | ||
|
|
667995cde4 | ||
|
|
1e0def87fd | ||
|
|
a219e5ee8c | ||
|
|
8070dfcc82 | ||
|
|
176a89bd03 | ||
|
|
dd8dd2f95c | ||
|
|
eb1bd1d200 | ||
|
|
460d2f3c2a | ||
|
|
0ab10f99fd | ||
|
|
377f57f1c3 | ||
|
|
caf5f1f619 | ||
|
|
d9ff85a202 | ||
|
|
f180a7c024 | ||
|
|
7fac9332e1 | ||
|
|
8dd7c42f69 | ||
|
|
b542eeecc0 | ||
|
|
bee8295daa | ||
|
|
ab9fd3d5ed | ||
|
|
cc54a3feda | ||
|
|
94984f35ec | ||
|
|
0e47e89d63 | ||
|
|
2d7dc7a1be | ||
|
|
4d76a5b599 | ||
|
|
87035ff744 | ||
|
|
e0d123f732 | ||
|
|
8eddcfc9d2 | ||
|
|
af58b86b60 | ||
|
|
00ae7ce33c | ||
|
|
0bc9fe841a | ||
|
|
e37920ed4e | ||
|
|
6a7466df93 | ||
|
|
1bb966e5a8 | ||
|
|
34e631395f | ||
|
|
080ddde68d | ||
|
|
209a8026fb | ||
|
|
23bfa4fc43 | ||
|
|
58d40c118c | ||
|
|
9d39769445 | ||
|
|
bfc08abe88 | ||
|
|
6a7b097273 | ||
|
|
8f2390ac99 | ||
|
|
481f5cae22 | ||
|
|
b9068b95b8 | ||
|
|
df2c35b551 | ||
|
|
3cd4152a3c | ||
|
|
2534510f0b | ||
|
|
3f8aa4635e | ||
|
|
ada59e8205 | ||
|
|
9ec0332483 | ||
|
|
d509b0cf5c | ||
|
|
4d624d8c3a | ||
|
|
9f0ba4b9c2 | ||
|
|
a930ae27be | ||
|
|
38e4919be1 | ||
|
|
a668047f75 | ||
|
|
c2ea2cda4c | ||
|
|
f3c3a2c301 | ||
|
|
0da7e587a7 | ||
|
|
e6e686aaf4 | ||
|
|
58e1fa5c36 | ||
|
|
42549526c7 | ||
|
|
9fe1c8fe80 | ||
|
|
b8dbcb3dbd | ||
|
|
7c5675670a | ||
|
|
291945a4fd | ||
|
|
439e8827bd | ||
|
|
a745cf78ee | ||
|
|
af69756df0 | ||
|
|
46c42ab6e4 | ||
|
|
33a127187b | ||
|
|
24ddbdd251 | ||
|
|
0122a98eea | ||
|
|
406545c1f1 | ||
|
|
a1b593027b | ||
|
|
eae1ba258a | ||
|
|
d2db30eabc | ||
|
|
9fb7c52217 | ||
|
|
6cab1786d3 | ||
|
|
362328167c | ||
|
|
570a9993f7 | ||
|
|
5adc68cf0b | ||
|
|
1b1757ebf2 | ||
|
|
d8950fb7d1 | ||
|
|
ba2e573c23 | ||
|
|
31391fc074 | ||
|
|
f94b2c3794 | ||
|
|
eb0a5fed8e | ||
|
|
eaa47d175f | ||
|
|
e968000a89 | ||
|
|
1ba448fe19 | ||
|
|
a5c82425f4 | ||
|
|
1bd31f6b8e | ||
|
|
c0ea0e52b3 | ||
|
|
e6a3daacb3 | ||
|
|
09dabda4a3 | ||
|
|
f523d912af | ||
|
|
90b0ca79ea | ||
|
|
a506e2d5a2 | ||
|
|
4c66518a68 | ||
|
|
42b4b83f8e | ||
|
|
7477ebbdd7 | ||
|
|
738dc5ce19 | ||
|
|
3680467e14 | ||
|
|
c5ada9b203 | ||
|
|
3d2805bc78 | ||
|
|
2dde286d68 | ||
|
|
2260156c40 | ||
|
|
129e970727 | ||
|
|
66271db8c0 | ||
|
|
09d33e62bd | ||
|
|
bf3dfa4ab6 | ||
|
|
40b866117e | ||
|
|
cb5f9f3051 | ||
|
|
80f97cf9bd | ||
|
|
6d860f7eae | ||
|
|
545643b610 | ||
|
|
7ee6f2c36a | ||
|
|
5d9b887624 | ||
|
|
12c0e298f5 | ||
|
|
f9aec7af0d | ||
|
|
b181d78dd5 | ||
|
|
b9ff40c6b5 | ||
|
|
0684810d38 | ||
|
|
1cc7ce6e27 | ||
|
|
82bc1bf0b1 | ||
|
|
75bcf8660b | ||
|
|
5e1d945198 | ||
|
|
e047184ede | ||
|
|
307a2eb6ec | ||
|
|
ab8aedf06e | ||
|
|
b6ab13f1de | ||
|
|
53a3e51920 | ||
|
|
4033566b4a | ||
|
|
bed1623dcb | ||
|
|
d4704977bc | ||
|
|
838eed94bc | ||
|
|
9870725d1f | ||
|
|
ba827283be | ||
|
|
1e37cb8c3c | ||
|
|
1991e01641 | ||
|
|
d7e87b6336 | ||
|
|
fde490ba15 | ||
|
|
cf5a16d967 | ||
|
|
e8dde9c63d | ||
|
|
667a935665 | ||
|
|
28cea706fa | ||
|
|
209a990444 | ||
|
|
6365a46fac | ||
|
|
a81496e9ab | ||
|
|
ca05733b9d | ||
|
|
dfb5348a78 | ||
|
|
602e52490c | ||
|
|
740b24e8a4 | ||
|
|
44a09ffd12 | ||
|
|
054c42cbc2 | ||
|
|
34263a70e2 | ||
|
|
7ea6ca35d7 | ||
|
|
a9aad497fc | ||
|
|
7da8489635 | ||
|
|
683561374d | ||
|
|
66c9982822 | ||
|
|
1b6450b210 | ||
|
|
aa8a13adb2 | ||
|
|
5888541c05 | ||
|
|
f893487dc0 | ||
|
|
b84beaf974 | ||
|
|
75a3c55e70 | ||
|
|
854a09e12f | ||
|
|
40412fd4a9 | ||
|
|
57fc084795 | ||
|
|
143ba6d5e7 | ||
|
|
6b338a923c | ||
|
|
e6ab1e3df5 | ||
|
|
5da6976bf9 | ||
|
|
bd15d90e77 | ||
|
|
61633cf23b | ||
|
|
9f1107c0e7 | ||
|
|
ff0d5ce179 | ||
|
|
0bbd910883 | ||
|
|
4258088fb4 | ||
|
|
6372b677d2 | ||
|
|
9af00af70f | ||
|
|
4010c60e7b | ||
|
|
aaa83a8f52 | ||
|
|
776408c564 | ||
|
|
d0cb2110e6 | ||
|
|
11e3480fe8 | ||
|
|
2cd54b72b0 | ||
|
|
c34ccafb2e | ||
|
|
6837874d43 | ||
|
|
3656337d41 | ||
|
|
a89b6321f1 | ||
|
|
ac10103b18 | ||
|
|
b696a242fc | ||
|
|
7e4822c8ca | ||
|
|
a955cb5400 | ||
|
|
2e2cfc4cb3 | ||
|
|
4157d1986f | ||
|
|
d13eb2f580 | ||
|
|
5476f69179 | ||
|
|
dcdf30da35 | ||
|
|
55746c8c19 | ||
|
|
dbdf5f2746 | ||
|
|
b4e28deed3 | ||
|
|
f4a604dcfb | ||
|
|
b3c5787ec8 | ||
|
|
471d0469dd | ||
|
|
113eda575f | ||
|
|
45f1da82fe | ||
|
|
5f45ff77e4 | ||
|
|
1c0201ee3d | ||
|
|
c7340e04ec | ||
|
|
0a32476dc5 | ||
|
|
e02bc6ffb5 | ||
|
|
f41a3970b2 | ||
|
|
6c536f3a9b | ||
|
|
4b24b6a848 | ||
|
|
5f254a929f | ||
|
|
8df1a01ace | ||
|
|
27b5ffb34f | ||
|
|
80af012962 | ||
|
|
615c80bef4 | ||
|
|
f5f4026dbb | ||
|
|
b431206ede | ||
|
|
c4878e9b49 | ||
|
|
aa452971a6 | ||
|
|
2d798f7cfe | ||
|
|
08bb0484eb | ||
|
|
b0b7337f5a | ||
|
|
93241a4beb | ||
|
|
4f1bf1f13c | ||
|
|
2d0b7b5bd8 | ||
|
|
8fe3ce5cab | ||
|
|
59a0f1d94f | ||
|
|
5175dc3450 | ||
|
|
9a22ccd058 | ||
|
|
c06ed49a2a | ||
|
|
2e51a5a454 | ||
|
|
75cc353528 | ||
|
|
3977580426 | ||
|
|
3a1370e174 | ||
|
|
c218c05b96 | ||
|
|
db247d9f9a | ||
|
|
78b7715ea6 | ||
|
|
ba76944d75 | ||
|
|
4a1a2122f0 | ||
|
|
d80b749dec | ||
|
|
039a8b7c36 | ||
|
|
779f58ab16 | ||
|
|
b9183fe5eb | ||
|
|
9d342671d5 | ||
|
|
4e47ebd5fc | ||
|
|
d5c418e909 | ||
|
|
85414558c5 | ||
|
|
d6af8d2526 | ||
|
|
1209e95e34 | ||
|
|
51f9279e67 | ||
|
|
f27d54f7fa | ||
|
|
7f3648f8ae | ||
|
|
49fc258578 | ||
|
|
0c51b4fe41 | ||
|
|
dbad714539 | ||
|
|
edd8008650 | ||
|
|
615a1b3f4e | ||
|
|
fe6044e1aa | ||
|
|
46b275bfab | ||
|
|
25f44c517a | ||
|
|
cac04f8ee4 | ||
|
|
45d8566ec0 | ||
|
|
29a98ba13b | ||
|
|
e3973f6448 | ||
|
|
7b41425fe4 | ||
|
|
2c7d51f98f | ||
|
|
a2df29515a | ||
|
|
6df1d165dd | ||
|
|
e03e2d9a68 | ||
|
|
8fc6ea19b4 | ||
|
|
c5c947e175 | ||
|
|
6d8dff54a7 |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -7,6 +7,8 @@ updates:
|
|||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(cargo)"
|
prefix: "chore(cargo)"
|
||||||
open-pull-requests-limit: 50
|
open-pull-requests-limit: 50
|
||||||
|
cooldown:
|
||||||
|
default-days: 7
|
||||||
|
|
||||||
# Keep GitHub Actions up to date.
|
# Keep GitHub Actions up to date.
|
||||||
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
|
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
|
||||||
@@ -14,3 +16,5 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
cooldown:
|
||||||
|
default-days: 7
|
||||||
|
|||||||
130
.github/workflows/ci.yml
vendored
130
.github/workflows/ci.yml
vendored
@@ -20,17 +20,18 @@ permissions: {}
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
RUST_VERSION: 1.88.0
|
RUST_VERSION: 1.95.0
|
||||||
|
|
||||||
# Minimum Supported Rust Version
|
# Minimum Supported Rust Version
|
||||||
MSRV: 1.85.0
|
MSRV: 1.89.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint_rust:
|
lint_rust:
|
||||||
name: Lint Rust
|
name: Lint Rust
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -39,7 +40,9 @@ jobs:
|
|||||||
- run: rustup override set $RUST_VERSION
|
- run: rustup override set $RUST_VERSION
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
|
with:
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
- name: Run rustfmt
|
- name: Run rustfmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
@@ -52,40 +55,47 @@ jobs:
|
|||||||
cargo_deny:
|
cargo_deny:
|
||||||
name: cargo deny
|
name: cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb
|
||||||
with:
|
with:
|
||||||
arguments: --all-features --workspace
|
arguments: --workspace --all-features --locked
|
||||||
command: check
|
command: check
|
||||||
command-arguments: "-Dwarnings"
|
command-arguments: "-Dwarnings"
|
||||||
|
|
||||||
provider_database:
|
provider_database:
|
||||||
name: Check provider database
|
name: Check provider database
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
- name: Install rustfmt
|
||||||
|
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
|
||||||
- name: Check provider database
|
- name: Check provider database
|
||||||
run: scripts/update-provider-database.sh
|
run: scripts/update-provider-database.sh
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
name: Rust doc comments
|
name: Rust doc comments
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: -Dwarnings
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
|
with:
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
- name: Rustdoc
|
- name: Rustdoc
|
||||||
run: cargo doc --document-private-items --no-deps
|
run: cargo doc --document-private-items --no-deps
|
||||||
|
|
||||||
@@ -105,6 +115,7 @@ jobs:
|
|||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: minimum
|
rust: minimum
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- run:
|
- run:
|
||||||
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
||||||
@@ -115,7 +126,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
if: matrix.rust == 'latest'
|
if: matrix.rust == 'latest'
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -127,22 +138,24 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
|
with:
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Install nextest
|
- name: Install nextest
|
||||||
uses: taiki-e/install-action@v2
|
uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458
|
||||||
with:
|
with:
|
||||||
tool: nextest
|
tool: nextest
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
run: cargo nextest run --workspace
|
run: cargo nextest run --workspace --locked
|
||||||
|
|
||||||
- name: Doc-Tests
|
- name: Doc-Tests
|
||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
run: cargo test --workspace --doc
|
run: cargo test --workspace --locked --doc
|
||||||
|
|
||||||
- name: Test cargo vendor
|
- name: Test cargo vendor
|
||||||
run: cargo vendor
|
run: cargo vendor
|
||||||
@@ -153,20 +166,23 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
|
with:
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build C library
|
- name: Build C library
|
||||||
run: cargo build -p deltachat_ffi
|
run: cargo build -p deltachat_ffi
|
||||||
|
|
||||||
- name: Upload C library
|
- name: Upload C library
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug/libdeltachat.a
|
path: target/debug/libdeltachat.a
|
||||||
@@ -178,20 +194,23 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
|
with:
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server
|
- name: Build deltachat-rpc-server
|
||||||
run: cargo build -p deltachat-rpc-server
|
run: cargo build -p deltachat-rpc-server
|
||||||
|
|
||||||
- name: Upload deltachat-rpc-server
|
- name: Upload deltachat-rpc-server
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||||
@@ -200,8 +219,9 @@ jobs:
|
|||||||
python_lint:
|
python_lint:
|
||||||
name: Python lint
|
name: Python lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -217,6 +237,38 @@ jobs:
|
|||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: tox -e lint
|
run: tox -e lint
|
||||||
|
|
||||||
|
# mypy does not work with PyPy since mypy 1.19
|
||||||
|
# as it introduced native `librt` dependency
|
||||||
|
# that uses CPython internals.
|
||||||
|
# We only run mypy with CPython because of this.
|
||||||
|
cffi_python_mypy:
|
||||||
|
name: CFFI Python mypy
|
||||||
|
needs: ["c_library", "python_lint"]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Download libdeltachat.a
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: ubuntu-latest-libdeltachat.a
|
||||||
|
path: target/debug
|
||||||
|
|
||||||
|
- name: Install tox
|
||||||
|
run: pip install tox
|
||||||
|
|
||||||
|
- name: Run mypy
|
||||||
|
env:
|
||||||
|
DCC_RS_TARGET: debug
|
||||||
|
DCC_RS_DEV: ${{ github.workspace }}
|
||||||
|
working-directory: python
|
||||||
|
run: tox -e mypy
|
||||||
|
|
||||||
|
|
||||||
cffi_python_tests:
|
cffi_python_tests:
|
||||||
name: CFFI Python tests
|
name: CFFI Python tests
|
||||||
needs: ["c_library", "python_lint"]
|
needs: ["c_library", "python_lint"]
|
||||||
@@ -226,9 +278,9 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
# Currently used Rust version.
|
# Currently used Rust version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.13
|
python: 3.14
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.13
|
python: 3.14
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -236,27 +288,28 @@ jobs:
|
|||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.10
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.8
|
# Minimum Supported Python Version = 3.10
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
# This is the minimum version for which manylinux Python wheels are
|
||||||
# built. Test it with minimum supported Rust version.
|
# built. Test it with minimum supported Rust version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.8
|
python: "3.10"
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download libdeltachat.a
|
- name: Download libdeltachat.a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug
|
path: target/debug
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
@@ -269,7 +322,7 @@ jobs:
|
|||||||
DCC_RS_TARGET: debug
|
DCC_RS_TARGET: debug
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
DCC_RS_DEV: ${{ github.workspace }}
|
||||||
working-directory: python
|
working-directory: python
|
||||||
run: tox -e mypy,doc,py
|
run: tox -e doc,py
|
||||||
|
|
||||||
rpc_python_tests:
|
rpc_python_tests:
|
||||||
name: JSON-RPC Python tests
|
name: JSON-RPC Python tests
|
||||||
@@ -279,11 +332,11 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.13
|
python: 3.14
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.13
|
python: 3.14
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python: 3.13
|
python: 3.14
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -291,19 +344,20 @@ jobs:
|
|||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.10
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.8
|
# Minimum Supported Python Version = 3.10
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.8
|
python: "3.10"
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
@@ -311,7 +365,7 @@ jobs:
|
|||||||
run: pip install tox
|
run: pip install tox
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server
|
- name: Download deltachat-rpc-server
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: target/debug
|
path: target/debug
|
||||||
|
|||||||
257
.github/workflows/deltachat-rpc-server.yml
vendored
257
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -30,22 +30,46 @@ jobs:
|
|||||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
path: result/bin/deltachat-rpc-server
|
path: result/bin/deltachat-rpc-server
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_linux_wheel:
|
||||||
|
name: Linux wheel
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server wheels
|
||||||
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||||
|
|
||||||
|
- name: Upload wheel
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
||||||
|
path: result/*.whl
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
name: Windows
|
name: Windows
|
||||||
strategy:
|
strategy:
|
||||||
@@ -54,22 +78,46 @@ jobs:
|
|||||||
arch: [win32, win64]
|
arch: [win32, win64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||||
path: result/bin/deltachat-rpc-server.exe
|
path: result/bin/deltachat-rpc-server.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_windows_wheel:
|
||||||
|
name: Windows wheel
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [win32, win64]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server wheels
|
||||||
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||||
|
|
||||||
|
- name: Upload wheel
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
|
||||||
|
path: result/*.whl
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
build_macos:
|
build_macos:
|
||||||
name: macOS
|
name: macOS
|
||||||
strategy:
|
strategy:
|
||||||
@@ -79,7 +127,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -91,7 +139,7 @@ jobs:
|
|||||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||||
@@ -105,25 +153,49 @@ jobs:
|
|||||||
arch: [arm64-v8a, armeabi-v7a]
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
path: result/bin/deltachat-rpc-server
|
path: result/bin/deltachat-rpc-server
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_android_wheel:
|
||||||
|
name: Android wheel
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server wheels
|
||||||
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||||
|
|
||||||
|
- name: Upload wheel
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
||||||
|
path: result/*.whl
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Build wheels and upload binaries to the release
|
name: Build wheels and upload binaries to the release
|
||||||
needs: ["build_linux", "build_windows", "build_macos"]
|
needs: ["build_linux", "build_linux_wheel", "build_windows", "build_windows_wheel", "build_macos", "build_android", "build_android_wheel"]
|
||||||
environment:
|
environment:
|
||||||
name: pypi
|
name: pypi
|
||||||
url: https://pypi.org/p/deltachat-rpc-server
|
url: https://pypi.org/p/deltachat-rpc-server
|
||||||
@@ -132,78 +204,132 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
- name: Download Linux aarch64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-linux
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
path: deltachat-rpc-server-aarch64-linux.d
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux aarch64 wheel
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-linux-wheel
|
||||||
|
path: deltachat-rpc-server-aarch64-linux-wheel.d
|
||||||
|
|
||||||
- name: Download Linux armv7l binary
|
- name: Download Linux armv7l binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv7l-linux
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
path: deltachat-rpc-server-armv7l-linux.d
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux armv7l wheel
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armv7l-linux-wheel
|
||||||
|
path: deltachat-rpc-server-armv7l-linux-wheel.d
|
||||||
|
|
||||||
- name: Download Linux armv6l binary
|
- name: Download Linux armv6l binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv6l-linux
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
path: deltachat-rpc-server-armv6l-linux.d
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux armv6l wheel
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armv6l-linux-wheel
|
||||||
|
path: deltachat-rpc-server-armv6l-linux-wheel.d
|
||||||
|
|
||||||
- name: Download Linux i686 binary
|
- name: Download Linux i686 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-i686-linux
|
name: deltachat-rpc-server-i686-linux
|
||||||
path: deltachat-rpc-server-i686-linux.d
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux i686 wheel
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-i686-linux-wheel
|
||||||
|
path: deltachat-rpc-server-i686-linux-wheel.d
|
||||||
|
|
||||||
- name: Download Linux x86_64 binary
|
- name: Download Linux x86_64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-linux
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
path: deltachat-rpc-server-x86_64-linux.d
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
|
- name: Download Linux x86_64 wheel
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-x86_64-linux-wheel
|
||||||
|
path: deltachat-rpc-server-x86_64-linux-wheel.d
|
||||||
|
|
||||||
- name: Download Win32 binary
|
- name: Download Win32 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win32
|
name: deltachat-rpc-server-win32
|
||||||
path: deltachat-rpc-server-win32.d
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
|
- name: Download Win32 wheel
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win32-wheel
|
||||||
|
path: deltachat-rpc-server-win32-wheel.d
|
||||||
|
|
||||||
- name: Download Win64 binary
|
- name: Download Win64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win64
|
name: deltachat-rpc-server-win64
|
||||||
path: deltachat-rpc-server-win64.d
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
|
- name: Download Win64 wheel
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win64-wheel
|
||||||
|
path: deltachat-rpc-server-win64-wheel.d
|
||||||
|
|
||||||
- name: Download macOS binary for x86_64
|
- name: Download macOS binary for x86_64
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
path: deltachat-rpc-server-x86_64-macos.d
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
- name: Download macOS binary for aarch64
|
- name: Download macOS binary for aarch64
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-macos
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
path: deltachat-rpc-server-aarch64-macos.d
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
- name: Download Android binary for arm64-v8a
|
- name: Download Android binary for arm64-v8a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-arm64-v8a-android
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
|
- name: Download Android wheel for arm64-v8a
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-arm64-v8a-android-wheel
|
||||||
|
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
|
||||||
|
|
||||||
- name: Download Android binary for armeabi-v7a
|
- name: Download Android binary for armeabi-v7a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
|
|
||||||
|
- name: Download Android wheel for armeabi-v7a
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||||
|
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
|
||||||
|
|
||||||
- name: Create bin/ directory
|
- name: Create bin/ directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
@@ -222,38 +348,21 @@ jobs:
|
|||||||
- name: List binaries
|
- name: List binaries
|
||||||
run: ls -l bin/
|
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
|
- name: Install wheel
|
||||||
run: pip install wheel
|
run: pip install wheel
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server Python wheels and source package
|
- name: Build deltachat-rpc-server Python wheels
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
nix build .#deltachat-rpc-server-x86_64-linux-wheel
|
mv deltachat-rpc-server-aarch64-linux-wheel.d/*.whl dist/
|
||||||
cp result/*.whl dist/
|
mv deltachat-rpc-server-armv7l-linux-wheel.d/*.whl dist/
|
||||||
nix build .#deltachat-rpc-server-armv7l-linux-wheel
|
mv deltachat-rpc-server-armv6l-linux-wheel.d/*.whl dist/
|
||||||
cp result/*.whl dist/
|
mv deltachat-rpc-server-i686-linux-wheel.d/*.whl dist/
|
||||||
nix build .#deltachat-rpc-server-armv6l-linux-wheel
|
mv deltachat-rpc-server-x86_64-linux-wheel.d/*.whl dist/
|
||||||
cp result/*.whl dist/
|
mv deltachat-rpc-server-win64-wheel.d/*.whl dist/
|
||||||
nix build .#deltachat-rpc-server-aarch64-linux-wheel
|
mv deltachat-rpc-server-win32-wheel.d/*.whl dist/
|
||||||
cp result/*.whl dist/
|
mv deltachat-rpc-server-arm64-v8a-android-wheel.d/*.whl dist/
|
||||||
nix build .#deltachat-rpc-server-i686-linux-wheel
|
mv deltachat-rpc-server-armeabi-v7a-android-wheel.d/*.whl dist/
|
||||||
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-arm64-v8a-android-wheel
|
|
||||||
cp result/*.whl dist/
|
|
||||||
nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel
|
|
||||||
cp result/*.whl dist/
|
|
||||||
nix build .#deltachat-rpc-server-source
|
|
||||||
cp result/*.tar.gz dist/
|
|
||||||
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||||
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
||||||
mv *.whl dist/
|
mv *.whl dist/
|
||||||
@@ -271,90 +380,93 @@ jobs:
|
|||||||
--repo ${{ github.repository }} \
|
--repo ${{ github.repository }} \
|
||||||
bin/* dist/*
|
bin/* dist/*
|
||||||
|
|
||||||
- name: Publish deltachat-rpc-client to PyPI
|
- name: Publish deltachat-rpc-server to PyPI
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||||
|
|
||||||
publish_npm_package:
|
publish_npm_package:
|
||||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
||||||
needs: ["build_linux", "build_windows", "build_macos"]
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
environment:
|
||||||
|
name: npm-stdio-rpc-server
|
||||||
|
url: https://www.npmjs.com/package/@deltachat/stdio-rpc-server
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
# Needed to publish the binaries to the release.
|
# Needed to publish the binaries to the release.
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
- name: Download Linux aarch64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-linux
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
path: deltachat-rpc-server-aarch64-linux.d
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv7l binary
|
- name: Download Linux armv7l binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv7l-linux
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
path: deltachat-rpc-server-armv7l-linux.d
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv6l binary
|
- name: Download Linux armv6l binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv6l-linux
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
path: deltachat-rpc-server-armv6l-linux.d
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
- name: Download Linux i686 binary
|
- name: Download Linux i686 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-i686-linux
|
name: deltachat-rpc-server-i686-linux
|
||||||
path: deltachat-rpc-server-i686-linux.d
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
- name: Download Linux x86_64 binary
|
- name: Download Linux x86_64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-linux
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
path: deltachat-rpc-server-x86_64-linux.d
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
- name: Download Win32 binary
|
- name: Download Win32 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win32
|
name: deltachat-rpc-server-win32
|
||||||
path: deltachat-rpc-server-win32.d
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
- name: Download Win64 binary
|
- name: Download Win64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win64
|
name: deltachat-rpc-server-win64
|
||||||
path: deltachat-rpc-server-win64.d
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
- name: Download macOS binary for x86_64
|
- name: Download macOS binary for x86_64
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
path: deltachat-rpc-server-x86_64-macos.d
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
- name: Download macOS binary for aarch64
|
- name: Download macOS binary for aarch64
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-macos
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
path: deltachat-rpc-server-aarch64-macos.d
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
- name: Download Android binary for arm64-v8a
|
- name: Download Android binary for arm64-v8a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-arm64-v8a-android
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
- name: Download Android binary for armeabi-v7a
|
- name: Download Android binary for armeabi-v7a
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
@@ -384,7 +496,7 @@ jobs:
|
|||||||
ls -lah
|
ls -lah
|
||||||
|
|
||||||
- name: Upload to artifacts
|
- name: Upload to artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-npm-package
|
name: deltachat-rpc-server-npm-package
|
||||||
path: deltachat-rpc-server/npm-package/*.tgz
|
path: deltachat-rpc-server/npm-package/*.tgz
|
||||||
@@ -401,16 +513,19 @@ jobs:
|
|||||||
deltachat-rpc-server/npm-package/*.tgz
|
deltachat-rpc-server/npm-package/*.tgz
|
||||||
|
|
||||||
# Configure Node.js for publishing.
|
# Configure Node.js for publishing.
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
# Ensure npm 11.5.1 or later is installed.
|
||||||
|
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||||
|
- name: Update npm
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
working-directory: deltachat-rpc-server/npm-package
|
working-directory: deltachat-rpc-server/npm-package
|
||||||
run: |
|
run: |
|
||||||
ls -lah platform_package
|
ls -lah platform_package
|
||||||
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|||||||
4
.github/workflows/dependabot.yml
vendored
4
.github/workflows/dependabot.yml
vendored
@@ -10,11 +10,11 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
dependabot:
|
dependabot:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == github.event.pull_request.head.repo.full_name
|
||||||
steps:
|
steps:
|
||||||
- name: Dependabot metadata
|
- name: Dependabot metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@v2.4.0
|
uses: dependabot/fetch-metadata@v3.0.0
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
- name: Approve a PR
|
- name: Approve a PR
|
||||||
|
|||||||
23
.github/workflows/dev-version.yml
vendored
Normal file
23
.github/workflows/dev-version.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Check that PRs are made against the -dev version.
|
||||||
|
#
|
||||||
|
# If this fails, push commit to update the version to -dev to main.
|
||||||
|
|
||||||
|
name: Check for -dev version
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_dev_version:
|
||||||
|
name: Check that current version ends with -dev
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run version-checking script
|
||||||
|
run: scripts/check-dev-version.py
|
||||||
14
.github/workflows/jsonrpc-client-npm-package.yml
vendored
14
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -10,20 +10,28 @@ jobs:
|
|||||||
pack-module:
|
pack-module:
|
||||||
name: "Publish @deltachat/jsonrpc-client"
|
name: "Publish @deltachat/jsonrpc-client"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: npm-jsonrpc-client
|
||||||
|
url: https://www.npmjs.com/package/@deltachat/jsonrpc-client
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
# Ensure npm 11.5.1 or later is installed.
|
||||||
|
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
||||||
|
- name: Update npm
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm install --ignore-scripts
|
run: npm install --ignore-scripts
|
||||||
@@ -37,5 +45,3 @@ jobs:
|
|||||||
- name: Publish
|
- name: Publish
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|||||||
6
.github/workflows/jsonrpc.yml
vendored
6
.github/workflows/jsonrpc.yml
vendored
@@ -16,16 +16,16 @@ jobs:
|
|||||||
build_and_test:
|
build_and_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
- name: Add Rust cache
|
- name: Add Rust cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
|
||||||
- name: npm install
|
- name: npm install
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|||||||
24
.github/workflows/nix.yml
vendored
24
.github/workflows/nix.yml
vendored
@@ -5,10 +5,12 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- flake.lock
|
- flake.lock
|
||||||
|
- .github/workflows/nix.yml
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- flake.lock
|
- flake.lock
|
||||||
|
- .github/workflows/nix.yml
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
@@ -19,15 +21,12 @@ jobs:
|
|||||||
name: check flake formatting
|
name: check flake formatting
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
- run: nix fmt
|
- run: nix fmt flake.nix -- --check
|
||||||
|
|
||||||
# Check that formatting does not change anything.
|
|
||||||
- run: git diff --exit-code
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: nix build
|
name: nix build
|
||||||
@@ -81,11 +80,11 @@ jobs:
|
|||||||
#- deltachat-rpc-server-x86_64-android
|
#- deltachat-rpc-server-x86_64-android
|
||||||
#- deltachat-rpc-server-x86-android
|
#- deltachat-rpc-server-x86-android
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
- run: nix build .#${{ matrix.installable }}
|
- run: nix build .#${{ matrix.installable }}
|
||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
@@ -96,14 +95,15 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
installable:
|
installable:
|
||||||
- deltachat-rpc-server
|
- deltachat-rpc-server
|
||||||
|
- deltachat-rpc-server-x86_64-darwin
|
||||||
|
|
||||||
# Fails to bulid
|
# Fails to build
|
||||||
|
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
||||||
# - deltachat-rpc-server-aarch64-darwin
|
# - deltachat-rpc-server-aarch64-darwin
|
||||||
# - deltachat-rpc-server-x86_64-darwin
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
- run: nix build .#${{ matrix.installable }}
|
- run: nix build .#${{ matrix.installable }}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: python3 -m build
|
run: python3 -m build
|
||||||
- name: Store the distribution packages
|
- name: Store the distribution packages
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: deltachat-rpc-client/dist/
|
path: deltachat-rpc-client/dist/
|
||||||
@@ -42,9 +42,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download all the dists
|
- name: Download all the dists
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: dist/
|
path: dist/
|
||||||
- name: Publish deltachat-rpc-client to PyPI
|
- name: Publish deltachat-rpc-client to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b
|
||||||
|
|||||||
6
.github/workflows/repl.yml
vendored
6
.github/workflows/repl.yml
vendored
@@ -14,15 +14,15 @@ jobs:
|
|||||||
name: Build REPL example
|
name: Build REPL example
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
- name: Build
|
- name: Build
|
||||||
run: nix build .#deltachat-repl-win64
|
run: nix build .#deltachat-repl-win64
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: repl.exe
|
name: repl.exe
|
||||||
path: "result/bin/deltachat-repl.exe"
|
path: "result/bin/deltachat-repl.exe"
|
||||||
|
|||||||
67
.github/workflows/upload-docs.yml
vendored
67
.github/workflows/upload-docs.yml
vendored
@@ -1,19 +1,21 @@
|
|||||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, py.delta.chat and cffi.delta.chat
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- build_jsonrpc_docs_ci
|
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-rs:
|
build-rs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: rs.delta.chat
|
||||||
|
url: https://rs.delta.chat/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -23,62 +25,72 @@ jobs:
|
|||||||
- name: Upload to rs.delta.chat
|
- name: Upload to rs.delta.chat
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.RS_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.RS_DOCS_SSH_USER }}@rs.delta.chat:/var/www/html/rs.delta.chat/"
|
||||||
|
|
||||||
build-python:
|
build-python:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: py.delta.chat
|
||||||
|
url: https://py.delta.chat/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
- name: Build Python documentation
|
- name: Build Python documentation
|
||||||
run: nix build .#python-docs
|
run: nix build .#python-docs
|
||||||
- name: Upload to py.delta.chat
|
- name: Upload to py.delta.chat
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.PY_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$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"
|
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.PY_DOCS_SSH_USER }}@py.delta.chat:/var/www/html/py.delta.chat"
|
||||||
|
|
||||||
build-c:
|
build-c:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: c.delta.chat
|
||||||
|
url: https://c.delta.chat/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- uses: DeterminateSystems/nix-installer-action@main
|
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
|
||||||
- name: Build C documentation
|
- name: Build C documentation
|
||||||
run: nix build .#docs
|
run: nix build .#docs
|
||||||
- name: Upload to c.delta.chat
|
- name: Upload to c.delta.chat
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.C_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$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"
|
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "${{ secrets.C_DOCS_SSH_USER }}@c.delta.chat:/var/www/html/c.delta.chat"
|
||||||
|
|
||||||
build-ts:
|
build-ts:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: js.jsonrpc.delta.chat
|
||||||
|
url: https://js.jsonrpc.delta.chat/
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./deltachat-jsonrpc/typescript
|
working-directory: ./deltachat-jsonrpc/typescript
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
- name: npm install
|
- name: npm install
|
||||||
@@ -90,6 +102,27 @@ jobs:
|
|||||||
- name: Upload to js.jsonrpc.delta.chat
|
- name: Upload to js.jsonrpc.delta.chat
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$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/"
|
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
|
||||||
|
|
||||||
|
build-cffi:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: cffi.delta.chat
|
||||||
|
url: https://cffi.delta.chat/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Build the documentation with cargo
|
||||||
|
run: |
|
||||||
|
cargo doc --package deltachat_ffi --no-deps
|
||||||
|
- name: Upload to cffi.delta.chat
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.ssh"
|
||||||
|
echo "${{ secrets.CFFI_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
|
||||||
|
chmod 600 "$HOME/.ssh/key"
|
||||||
|
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.CFFI_DOCS_SSH_USER }}@delta.chat:/var/www/html/cffi.delta.chat/"
|
||||||
|
|||||||
31
.github/workflows/upload-ffi-docs.yml
vendored
31
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Build the documentation with cargo
|
|
||||||
run: |
|
|
||||||
cargo doc --package deltachat_ffi --no-deps
|
|
||||||
- name: Upload to cffi.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/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/"
|
|
||||||
21
.github/workflows/zizmor-scan.yml
vendored
21
.github/workflows/zizmor-scan.yml
vendored
@@ -6,26 +6,21 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: ["**"]
|
branches: ["**"]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zizmor:
|
zizmor:
|
||||||
name: zizmor latest via PyPI
|
name: Run zizmor
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write
|
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
|
||||||
uses: astral-sh/setup-uv@v6
|
|
||||||
|
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
run: uvx zizmor --format sarif . > results.sarif
|
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||||
|
|
||||||
- name: Upload SARIF file
|
|
||||||
uses: github/codeql-action/upload-sarif@v3
|
|
||||||
with:
|
|
||||||
sarif_file: results.sarif
|
|
||||||
category: zizmor
|
|
||||||
|
|||||||
6
.github/zizmor.yml
vendored
Normal file
6
.github/zizmor.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
rules:
|
||||||
|
unpinned-uses:
|
||||||
|
config:
|
||||||
|
policies:
|
||||||
|
actions/*: ref-pin
|
||||||
|
dependabot/*: ref-pin
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,6 +36,7 @@ deltachat-ffi/xml
|
|||||||
coverage/
|
coverage/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
|
.zed
|
||||||
python/accounts.txt
|
python/accounts.txt
|
||||||
python/all-testaccounts.txt
|
python/all-testaccounts.txt
|
||||||
tmp/
|
tmp/
|
||||||
|
|||||||
1635
CHANGELOG.md
1635
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
# Contributing to Delta Chat
|
# Contributing to chatmail core
|
||||||
|
|
||||||
## Bug reports
|
## Bug reports
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ If you want to contribute a code, follow this guide.
|
|||||||
|
|
||||||
The following prefix types are used:
|
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`.
|
- `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"
|
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is canceled"
|
||||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
- `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()`"
|
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||||
|
|||||||
1229
Cargo.lock
generated
1229
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
70
Cargo.toml
70
Cargo.toml
@@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "2.2.0"
|
version = "2.50.0-dev"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.85"
|
rust-version = "1.89"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/chatmail/core"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
@@ -44,32 +44,33 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-broadcast = "0.7.2"
|
async-broadcast = "0.7.2"
|
||||||
async-channel = { workspace = true }
|
async-channel = { workspace = true }
|
||||||
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
|
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
|
blake3 = "1.8.2"
|
||||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||||
|
colorutils-rs = { version = "0.8.0", default-features = false }
|
||||||
data-encoding = "2.9.0"
|
data-encoding = "2.9.0"
|
||||||
escaper = "0.1"
|
escaper = "0.1"
|
||||||
fast-socks5 = "0.10"
|
fast-socks5 = "1"
|
||||||
fd-lock = "4"
|
fd-lock = "4"
|
||||||
futures-lite = { workspace = true }
|
futures-lite = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
hickory-resolver = "0.25.2"
|
|
||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
humansize = "2"
|
humansize = "2"
|
||||||
hyper = "1"
|
hyper = "1"
|
||||||
hyper-util = "0.1.14"
|
hyper-util = "0.1.16"
|
||||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||||
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||||
iroh = { version = "0.35", default-features = false }
|
iroh = { version = "0.35", default-features = false }
|
||||||
kamadak-exif = "0.6.1"
|
kamadak-exif = "0.6.1"
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
mail-builder = { version = "0.4.3", default-features = false }
|
mail-builder = { version = "0.4.4", default-features = false }
|
||||||
mailparse = { workspace = true }
|
mailparse = { workspace = true }
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
num_cpus = "1.17"
|
num_cpus = "1.17"
|
||||||
@@ -77,18 +78,16 @@ num-derive = "0.4"
|
|||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
parking_lot = "0.12.4"
|
parking_lot = "0.12.4"
|
||||||
percent-encoding = "2.3"
|
percent-encoding = "2.3"
|
||||||
pgp = { version = "0.16.0", default-features = false }
|
pgp = { version = "0.19.0", default-features = false }
|
||||||
pin-project = "1"
|
pin-project = "1"
|
||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
quick-xml = "0.37"
|
quick-xml = { version = "0.39", features = ["escape-html"] }
|
||||||
quoted_printable = "0.5"
|
rand-old = { package = "rand", version = "0.8" }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||||
rust-hsluv = "0.1"
|
|
||||||
rustls-pki-types = "1.12.0"
|
|
||||||
rustls = { version = "0.23.22", default-features = false }
|
|
||||||
sanitize-filename = { workspace = true }
|
sanitize-filename = { workspace = true }
|
||||||
|
sdp = "0.17.1"
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
@@ -96,27 +95,27 @@ sha-1 = "0.10"
|
|||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||||
smallvec = "1.15.1"
|
smallvec = "1.15.1"
|
||||||
strum = "0.27"
|
strum = "0.28"
|
||||||
strum_macros = "0.27"
|
strum_macros = "0.28"
|
||||||
tagger = "4.3.4"
|
tagger = "4.3.4"
|
||||||
textwrap = "0.16.2"
|
textwrap = "0.16.2"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio-io-timeout = "1.2.0"
|
tokio-io-timeout = "1.2.1"
|
||||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
astral-tokio-tar = { version = "0.6.1", default-features = false }
|
||||||
tokio-util = { workspace = true }
|
tokio-util = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
toml = "0.8"
|
toml = "0.9"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
|
walkdir = "2.5.0"
|
||||||
webpki-roots = "0.26.8"
|
webpki-roots = "0.26.8"
|
||||||
blake3 = "1.8.2"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
criterion = { version = "0.6.0", features = ["async_tokio"] }
|
criterion = { version = "0.8.1", features = ["async_tokio"] }
|
||||||
futures-lite = { workspace = true }
|
futures-lite = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
nu-ansi-term = { workspace = true }
|
nu-ansi-term = { workspace = true }
|
||||||
@@ -157,6 +156,11 @@ name = "receive_emails"
|
|||||||
required-features = ["internals"]
|
required-features = ["internals"]
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "decrypting"
|
||||||
|
required-features = ["internals"]
|
||||||
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "get_chat_msgs"
|
name = "get_chat_msgs"
|
||||||
harness = false
|
harness = false
|
||||||
@@ -175,29 +179,29 @@ harness = false
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-channel = "2.3.1"
|
async-channel = "2.5.0"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
chrono = { version = "0.4.41", default-features = false }
|
chrono = { version = "0.4.44", default-features = false }
|
||||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||||
deltachat = { path = ".", default-features = false }
|
deltachat = { path = ".", default-features = false }
|
||||||
futures = "0.3.31"
|
futures = "0.3.32"
|
||||||
futures-lite = "2.6.0"
|
futures-lite = "2.6.1"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mailparse = "0.16.1"
|
mailparse = "0.16.1"
|
||||||
nu-ansi-term = "0.46"
|
nu-ansi-term = "0.50"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.9"
|
||||||
regex = "1.10"
|
regex = "1.12"
|
||||||
rusqlite = "0.36"
|
rusqlite = "0.37"
|
||||||
sanitize-filename = "0.5"
|
sanitize-filename = "0.6"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.27.0"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = "1"
|
tokio = "1"
|
||||||
tokio-util = "0.7.14"
|
tokio-util = "0.7.18"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
yerpc = "0.6.4"
|
yerpc = "0.6.4"
|
||||||
|
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -80,30 +80,41 @@ Connect to your mail server (if already configured):
|
|||||||
> connect
|
> connect
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a contact:
|
Export your public key to a vCard file:
|
||||||
|
|
||||||
|
```
|
||||||
|
> make-vcard my.vcard 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Create contacts by address or vCard file:
|
||||||
|
|
||||||
```
|
```
|
||||||
> addcontact yourfriends@email.org
|
> addcontact yourfriends@email.org
|
||||||
Command executed successfully.
|
> import-vcard key-contact.vcard
|
||||||
```
|
```
|
||||||
|
|
||||||
List contacts:
|
List contacts:
|
||||||
|
|
||||||
```
|
```
|
||||||
> listcontacts
|
> listcontacts
|
||||||
Contact#10: <name unset> <yourfriends@email.org>
|
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
|
||||||
Contact#1: Me √√ <your@email.org>
|
Contact#Contact#Self: Me √ <your@email.org>
|
||||||
|
2 key contacts.
|
||||||
|
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
||||||
|
1 address contacts.
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a chat with your friend and send a message:
|
Create a chat with your friend and send a message:
|
||||||
|
|
||||||
```
|
```
|
||||||
> createchat 10
|
> createchat 10
|
||||||
Single#10 created successfully.
|
Single#Chat#12 created successfully.
|
||||||
> chat 10
|
> chat 12
|
||||||
Single#10: yourfriends@email.org [yourfriends@email.org]
|
Selecting chat Chat#12
|
||||||
|
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
|
||||||
|
0 messages.
|
||||||
|
81.252µs to create this list, 123.625µs to mark all messages as noticed.
|
||||||
> send hi
|
> send hi
|
||||||
Message sent.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
List messages when inside a chat:
|
List messages when inside a chat:
|
||||||
@@ -186,12 +197,10 @@ and then run the script.
|
|||||||
Language bindings are available for:
|
Language bindings are available for:
|
||||||
|
|
||||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||||
|
- -> libdeltachat is going to be deprecated and only exists because Android, iOS and Ubuntu Touch are still using it. If you build a new project, then please use the jsonrpc api instead.
|
||||||
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
||||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||||
- **Go**
|
- **Go** \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||||
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
|
||||||
- over cffi[^1]: \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
|
||||||
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
|
||||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||||
|
|
||||||
The following "frontend" projects make use of the Rust-library
|
The following "frontend" projects make use of the Rust-library
|
||||||
@@ -204,5 +213,3 @@ or its language bindings:
|
|||||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||||
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
- [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.
|
|
||||||
|
|||||||
55
RELEASE.md
55
RELEASE.md
@@ -1,4 +1,4 @@
|
|||||||
# Releasing a new version of DeltaChat core
|
# Releasing a new version of chatmail core
|
||||||
|
|
||||||
For example, to release version 1.116.0 of the core, do the following steps.
|
For example, to release version 1.116.0 of the core, do the following steps.
|
||||||
|
|
||||||
@@ -14,8 +14,55 @@ For example, to release version 1.116.0 of the core, do the following steps.
|
|||||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||||
|
|
||||||
6. Tag the release: `git tag --annotate v1.116.0`.
|
6. Push the commit to the `main` branch.
|
||||||
|
|
||||||
7. Push the release tag: `git push origin v1.116.0`.
|
7. Once the commit is on the `main` branch and passed CI, tag the release: `git tag --annotate v1.116.0`.
|
||||||
|
|
||||||
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
8. Push the release tag: `git push origin v1.116.0`.
|
||||||
|
|
||||||
|
9. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
||||||
|
|
||||||
|
10. Update the version to the next development version:
|
||||||
|
`scripts/set_core_version.py 1.117.0-dev`.
|
||||||
|
|
||||||
|
11. Commit and push the change:
|
||||||
|
`git commit -m "chore: bump version to 1.117.0-dev" && git push origin main`.
|
||||||
|
|
||||||
|
12. Once the binaries are generated and [published](https://github.com/chatmail/core/releases),
|
||||||
|
check Windows binaries for false positive detections at [VirusTotal].
|
||||||
|
Either upload the binaries directly or submit a direct link to the artifact.
|
||||||
|
You can use [old browsers interface](https://www.virustotal.com/old-browsers/)
|
||||||
|
if there are problems with using the default website.
|
||||||
|
If you submit a direct link and get to the page saying
|
||||||
|
"No security vendors flagged this URL as malicious",
|
||||||
|
it does not mean that the file itself is not detected.
|
||||||
|
You need to go to the "details" tab
|
||||||
|
and click on the SHA-256 hash in the "Body SHA-256" section.
|
||||||
|
If any false positive is detected,
|
||||||
|
open an issue to track removing it.
|
||||||
|
See <https://github.com/chatmail/core/issues/7847>
|
||||||
|
for an example of false positive detection issue.
|
||||||
|
If there is a false positive "Microsoft" detection,
|
||||||
|
mark the issue as a blocker.
|
||||||
|
|
||||||
|
[VirusTotal]: https://www.virustotal.com/
|
||||||
|
|
||||||
|
## Dealing with antivirus false positives
|
||||||
|
|
||||||
|
If Windows release is incorrectly detected by some antivirus, submit requests to remove detection.
|
||||||
|
|
||||||
|
"Microsoft" antivirus is built in Windows and will break user setups so removing its detection should be highest priority.
|
||||||
|
To submit false positive to Microsoft, go to <https://www.microsoft.com/en-us/wdsi/filesubmission> and select "Submit file as a ... Software developer" option.
|
||||||
|
|
||||||
|
False positive contacts for other vendors can be found at <https://docs.virustotal.com/docs/false-positive-contacts>.
|
||||||
|
Not all of them may be up to date, so check the links below first.
|
||||||
|
Previously we successfully used the following contacts:
|
||||||
|
- [ESET-NOD32](mailto:samples@eset.com)
|
||||||
|
- [Symantec](https://symsubmit.symantec.com/)
|
||||||
|
|
||||||
|
## Dealing with failed releases
|
||||||
|
|
||||||
|
Once you make a GitHub release,
|
||||||
|
CI will try to build and publish [PyPI](https://pypi.org/) and [npm](https://www.npmjs.com/) packages.
|
||||||
|
If this fails for some reason, do not modify the failed tag, do not delete it and do not force-push to the `main` branch.
|
||||||
|
Fix the build process and tag a new release instead.
|
||||||
|
|||||||
63
STYLE.md
63
STYLE.md
@@ -16,11 +16,12 @@ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||||||
text TEXT DEFAULT '' NOT NULL -- message text
|
text TEXT DEFAULT '' NOT NULL -- message text
|
||||||
) STRICT",
|
) STRICT",
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.context("CREATE TABLE messages")?;
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
||||||
or [`indoc!](https://docs.rs/indoc).
|
or [`indoc!`](https://docs.rs/indoc).
|
||||||
Do not escape newlines like this:
|
Do not escape newlines like this:
|
||||||
```
|
```
|
||||||
sql.execute(
|
sql.execute(
|
||||||
@@ -29,7 +30,8 @@ id INTEGER PRIMARY KEY AUTOINCREMENT, \
|
|||||||
text TEXT DEFAULT '' NOT NULL \
|
text TEXT DEFAULT '' NOT NULL \
|
||||||
) STRICT",
|
) STRICT",
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.context("CREATE TABLE messages")?;
|
||||||
```
|
```
|
||||||
Escaping newlines
|
Escaping newlines
|
||||||
is prone to errors like this if space before backslash is missing:
|
is prone to errors like this if space before backslash is missing:
|
||||||
@@ -63,6 +65,15 @@ an older version. Also don't change the column type, consider adding a new colum
|
|||||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
||||||
keyword doesn't help here.
|
keyword doesn't help here.
|
||||||
|
|
||||||
|
Consider adding context to `anyhow` errors for SQL statements using `.context()` so that it's
|
||||||
|
possible to understand from logs which statement failed. See [Errors](#errors) for more info.
|
||||||
|
|
||||||
|
When changing complex SQL queries, test them on a new database with `EXPLAIN QUERY PLAN`
|
||||||
|
to make sure that indexes are used and large tables are not going to be scanned.
|
||||||
|
Never run `ANALYZE` on the databases,
|
||||||
|
this makes query planner unpredictable
|
||||||
|
and may make performance significantly worse: <https://github.com/chatmail/core/issues/6585>
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||||
@@ -78,6 +89,27 @@ All errors should be handled in one of these ways:
|
|||||||
- With `.log_err().ok()`.
|
- With `.log_err().ok()`.
|
||||||
- Bubbled up with `?`.
|
- Bubbled up with `?`.
|
||||||
|
|
||||||
|
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
|
||||||
|
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
|
||||||
|
```
|
||||||
|
while let Some(event) = stream.try_next().await? {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
instead of
|
||||||
|
```
|
||||||
|
while let Some(event_res) = stream.next().await {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
as it allows bubbling up the error early with `?`
|
||||||
|
with no way to accidentally skip error processing
|
||||||
|
with early `continue` or `break`.
|
||||||
|
Some streams reading from a connection
|
||||||
|
return infinite number of `Some(Err(_))`
|
||||||
|
items when connection breaks and not processing
|
||||||
|
errors may result in infinite loop.
|
||||||
|
|
||||||
`backtrace` feature is enabled for `anyhow` crate
|
`backtrace` feature is enabled for `anyhow` crate
|
||||||
and `debug = 1` option is set in the test profile.
|
and `debug = 1` option is set in the test profile.
|
||||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||||
@@ -91,6 +123,18 @@ Follow
|
|||||||
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
|
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
|
||||||
for `.expect` message style.
|
for `.expect` message style.
|
||||||
|
|
||||||
|
## BTreeMap vs HashMap
|
||||||
|
|
||||||
|
Prefer [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html)
|
||||||
|
over [HashMap](https://doc.rust-lang.org/std/collections/struct.HashMap.html)
|
||||||
|
and [BTreeSet](https://doc.rust-lang.org/std/collections/struct.BTreeSet.html)
|
||||||
|
over [HashSet](https://doc.rust-lang.org/std/collections/struct.HashSet.html)
|
||||||
|
as iterating over these structures returns items in deterministic order.
|
||||||
|
|
||||||
|
Non-deterministic code may result in difficult to reproduce bugs,
|
||||||
|
flaky tests, regression tests that miss bugs
|
||||||
|
or different behavior on different devices when processing the same messages.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
For logging, use `info!`, `warn!` and `error!` macros.
|
For logging, use `info!`, `warn!` and `error!` macros.
|
||||||
@@ -117,3 +161,16 @@ are documented.
|
|||||||
|
|
||||||
Follow Rust guidelines for the documentation comments:
|
Follow Rust guidelines for the documentation comments:
|
||||||
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
||||||
|
|
||||||
|
## Do not use `into()`, `try_into()` or `parse()`
|
||||||
|
|
||||||
|
For internal types, implementing `From`, `TryFrom` or `FromStr` is discouraged.
|
||||||
|
Instead, a `new()` function is recommended.
|
||||||
|
|
||||||
|
For external types, prefer using `Type::from()`, `Type::try_from()` or `Type::from_str()`
|
||||||
|
over `into()`, `try_into()` or `parse()`.
|
||||||
|
|
||||||
|
Calling `into()`, `try_into()` or `parse()`
|
||||||
|
creates an indirection,
|
||||||
|
which is hard to follow for people who are not familiar with Rust,
|
||||||
|
or who are not using rust-analyzer.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
142
benches/decrypting.rs
Normal file
142
benches/decrypting.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
//! Benchmarks for message decryption,
|
||||||
|
//! comparing decryption of symmetrically-encrypted messages
|
||||||
|
//! to decryption of asymmetrically-encrypted messages.
|
||||||
|
//!
|
||||||
|
//! Call with
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! cargo bench --bench decrypting --features="internals"
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! or, if you want to only run e.g. the 'Decrypt and parse a symmetrically encrypted message' benchmark:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse a symmetrically encrypted message'
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! You can also pass a substring:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! cargo bench --bench decrypting --features="internals" -- 'symmetrically'
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Symmetric decryption has to try out all known secrets,
|
||||||
|
//! You can benchmark this by adapting the `NUM_SECRETS` variable.
|
||||||
|
|
||||||
|
use std::hint::black_box;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use deltachat::internals_for_benches::create_broadcast_secret;
|
||||||
|
use deltachat::internals_for_benches::save_broadcast_secret;
|
||||||
|
use deltachat::securejoin::get_securejoin_qr;
|
||||||
|
use deltachat::{
|
||||||
|
Events, chat::ChatId, config::Config, context::Context, internals_for_benches::key_from_asc,
|
||||||
|
internals_for_benches::parse_and_get_text, internals_for_benches::store_self_keypair,
|
||||||
|
stock_str::StockStrings,
|
||||||
|
};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
static NUM_BROADCAST_SECRETS: LazyLock<usize> = LazyLock::new(|| {
|
||||||
|
std::env::var("NUM_BROADCAST_SECRETS")
|
||||||
|
.unwrap_or("500".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
static NUM_AUTH_TOKENS: LazyLock<usize> = LazyLock::new(|| {
|
||||||
|
std::env::var("NUM_AUTH_TOKENS")
|
||||||
|
.unwrap_or("5000".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
async fn create_context() -> Context {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let dbfile = dir.path().join("db.sqlite");
|
||||||
|
let context = Context::new(dbfile.as_path(), 100, Events::new(), StockStrings::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
context
|
||||||
|
.set_config(Config::ConfiguredAddr, Some("bob@example.net"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
||||||
|
store_self_keypair(&context, &secret)
|
||||||
|
.await
|
||||||
|
.expect("Failed to save key");
|
||||||
|
|
||||||
|
context
|
||||||
|
}
|
||||||
|
|
||||||
|
fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("Decrypt");
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
// Benchmarks for the whole parsing pipeline, incl. decryption (but excl. receive_imf())
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let mut secrets = generate_secrets();
|
||||||
|
|
||||||
|
// "secret" is the shared secret that was used to encrypt text_symmetrically_encrypted.eml.
|
||||||
|
// Put it into the middle of our secrets:
|
||||||
|
secrets[*NUM_BROADCAST_SECRETS / 2] = "secret".to_string();
|
||||||
|
|
||||||
|
let context = rt.block_on(async {
|
||||||
|
let context = create_context().await;
|
||||||
|
for (i, secret) in secrets.iter().enumerate() {
|
||||||
|
save_broadcast_secret(&context, ChatId::new(10 + i as u32), secret)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
for _i in 0..*NUM_AUTH_TOKENS {
|
||||||
|
get_securejoin_qr(&context, None).await.unwrap();
|
||||||
|
}
|
||||||
|
println!("NUM_AUTH_TOKENS={}", *NUM_AUTH_TOKENS);
|
||||||
|
context
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("Decrypt and parse a symmetrically encrypted message", |b| {
|
||||||
|
b.to_async(&rt).iter(|| {
|
||||||
|
let ctx = context.clone();
|
||||||
|
async move {
|
||||||
|
let text = parse_and_get_text(
|
||||||
|
&ctx,
|
||||||
|
include_bytes!("../test-data/message/text_symmetrically_encrypted.eml"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(black_box(text), "Symmetrically encrypted message");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("Decrypt and parse a public-key encrypted message", |b| {
|
||||||
|
b.to_async(&rt).iter(|| {
|
||||||
|
let ctx = context.clone();
|
||||||
|
async move {
|
||||||
|
let text = parse_and_get_text(
|
||||||
|
&ctx,
|
||||||
|
include_bytes!("../test-data/message/text_from_alice_encrypted.eml"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(black_box(text), "hi");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_secrets() -> Vec<String> {
|
||||||
|
let secrets: Vec<String> = (0..*NUM_BROADCAST_SECRETS)
|
||||||
|
.map(|_| create_broadcast_secret())
|
||||||
|
.collect();
|
||||||
|
println!("NUM_BROADCAST_SECRETS={}", *NUM_BROADCAST_SECRETS);
|
||||||
|
secrets
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, criterion_benchmark);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -66,7 +66,7 @@ body = """
|
|||||||
{% for commit in commits %}
|
{% for commit in commits %}
|
||||||
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
||||||
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
||||||
{{ commit.message | upper_first }}.\
|
{{ commit.message }}.\
|
||||||
{% if commit.footers is defined %}\
|
{% if commit.footers is defined %}\
|
||||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ impl ContactAddress {
|
|||||||
pub fn new(s: &str) -> Result<Self> {
|
pub fn new(s: &str) -> Result<Self> {
|
||||||
let addr = addr_normalize(s);
|
let addr = addr_normalize(s);
|
||||||
if !may_be_valid_addr(&addr) {
|
if !may_be_valid_addr(&addr) {
|
||||||
bail!("invalid address {:?}", s);
|
bail!("invalid address {s:?}");
|
||||||
}
|
}
|
||||||
Ok(Self(addr.to_string()))
|
Ok(Self(addr.to_string()))
|
||||||
}
|
}
|
||||||
@@ -257,16 +257,16 @@ impl EmailAddress {
|
|||||||
.chars()
|
.chars()
|
||||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
||||||
{
|
{
|
||||||
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
|
bail!("Email {input:?} must not contain whitespaces, '>' or '<'");
|
||||||
}
|
}
|
||||||
|
|
||||||
match &parts[..] {
|
match &parts[..] {
|
||||||
[domain, local] => {
|
[domain, local] => {
|
||||||
if local.is_empty() {
|
if local.is_empty() {
|
||||||
bail!("empty string is not valid for local part in {:?}", input);
|
bail!("empty string is not valid for local part in {input:?}");
|
||||||
}
|
}
|
||||||
if domain.is_empty() {
|
if domain.is_empty() {
|
||||||
bail!("missing domain after '@' in {:?}", input);
|
bail!("missing domain after '@' in {input:?}");
|
||||||
}
|
}
|
||||||
if domain.ends_with('.') {
|
if domain.ends_with('.') {
|
||||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||||
@@ -276,7 +276,7 @@ impl EmailAddress {
|
|||||||
domain: (*domain).to_string(),
|
domain: (*domain).to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => bail!("Email {:?} must contain '@' character", input),
|
_ => bail!("Email {input:?} must contain '@' character"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,45 @@ impl VcardContact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn escape(s: &str) -> String {
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
||||||
|
s
|
||||||
|
// backslash must be first!
|
||||||
|
.replace(r"\", r"\\")
|
||||||
|
.replace(',', r"\,")
|
||||||
|
.replace(';', r"\;")
|
||||||
|
.replace('\n', r"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unescape(s: &str) -> String {
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
||||||
|
let mut out = String::new();
|
||||||
|
|
||||||
|
let mut chars = s.chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '\\' {
|
||||||
|
if let Some(next) = chars.next() {
|
||||||
|
match next {
|
||||||
|
'\\' | ',' | ';' => out.push(next),
|
||||||
|
'n' | 'N' => out.push('\n'),
|
||||||
|
_ => {
|
||||||
|
// Invalid escape sequence (keep unchanged)
|
||||||
|
out.push('\\');
|
||||||
|
out.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Invalid escape sequence (keep unchanged)
|
||||||
|
out.push('\\');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a vCard containing given contacts.
|
/// Returns a vCard containing given contacts.
|
||||||
///
|
///
|
||||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||||
@@ -46,10 +85,6 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
|||||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape(s: &str) -> String {
|
|
||||||
s.replace(',', "\\,")
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res = "".to_string();
|
let mut res = "".to_string();
|
||||||
for c in contacts {
|
for c in contacts {
|
||||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
// Mustn't contain ',', but it's easier to escape than to error out.
|
||||||
@@ -124,7 +159,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|||||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
||||||
let (params, value) = vcard_property_raw(line, property)?;
|
let (params, value) = vcard_property_raw(line, property)?;
|
||||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
// Some fields can't contain commas, but unescape them everywhere for safety.
|
||||||
Some((params, value.replace("\\,", ",")))
|
Some((params, unescape(value)))
|
||||||
}
|
}
|
||||||
fn base64_key(line: &str) -> Option<&str> {
|
fn base64_key(line: &str) -> Option<&str> {
|
||||||
let (params, value) = vcard_property_raw(line, "key")?;
|
let (params, value) = vcard_property_raw(line, "key")?;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ fn test_make_and_parse_vcard() {
|
|||||||
authname: "Alice Wonderland".to_string(),
|
authname: "Alice Wonderland".to_string(),
|
||||||
key: Some("[base64-data]".to_string()),
|
key: Some("[base64-data]".to_string()),
|
||||||
profile_image: Some("image in Base64".to_string()),
|
profile_image: Some("image in Base64".to_string()),
|
||||||
biography: Some("Hi, I'm Alice".to_string()),
|
biography: Some("Hi,\nI'm Alice; and this is a backslash: \\".to_string()),
|
||||||
timestamp: Ok(1713465762),
|
timestamp: Ok(1713465762),
|
||||||
},
|
},
|
||||||
VcardContact {
|
VcardContact {
|
||||||
@@ -110,7 +110,7 @@ fn test_make_and_parse_vcard() {
|
|||||||
FN:Alice Wonderland\r\n\
|
FN:Alice Wonderland\r\n\
|
||||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
||||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
||||||
NOTE:Hi\\, I'm Alice\r\n\
|
NOTE:Hi\\,\\nI'm Alice\\; and this is a backslash: \\\\\r\n\
|
||||||
REV:20240418T184242Z\r\n\
|
REV:20240418T184242Z\r\n\
|
||||||
END:VCARD\r\n",
|
END:VCARD\r\n",
|
||||||
"BEGIN:VCARD\r\n\
|
"BEGIN:VCARD\r\n\
|
||||||
@@ -276,3 +276,14 @@ END:VCARD",
|
|||||||
assert!(contacts[0].timestamp.is_err());
|
assert!(contacts[0].timestamp.is_err());
|
||||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcard_value_escape_unescape() {
|
||||||
|
let original = "Text, with; chars and a \\ and a newline\nand a literal newline \\n";
|
||||||
|
let expected_escaped = r"Text\, with\; chars and a \\ and a newline\nand a literal newline \\n";
|
||||||
|
|
||||||
|
let escaped = escape(original);
|
||||||
|
assert_eq!(escaped, expected_escaped);
|
||||||
|
let unescaped = unescape(&escaped);
|
||||||
|
assert_eq!(original, unescaped);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "2.2.0"
|
version = "2.50.0-dev"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,14 +15,14 @@ use std::collections::BTreeMap;
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::ops::Deref;
|
use std::mem::ManuallyDrop;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock, Mutex};
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
|
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
|
||||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||||
use deltachat::contact::{Contact, ContactId, Origin};
|
use deltachat::contact::{Contact, ContactId, Origin};
|
||||||
use deltachat::context::{Context, ContextBuilder};
|
use deltachat::context::{Context, ContextBuilder};
|
||||||
@@ -39,7 +39,6 @@ use deltachat_jsonrpc::api::CommandApi;
|
|||||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||||
use message::Viewtype;
|
use message::Viewtype;
|
||||||
use num_traits::{FromPrimitive, ToPrimitive};
|
use num_traits::{FromPrimitive, ToPrimitive};
|
||||||
use rand::Rng;
|
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
@@ -61,7 +60,6 @@ use self::string::*;
|
|||||||
// - finally, this behaviour matches the old core-c API and UIs already depend on it
|
// - finally, this behaviour matches the old core-c API and UIs already depend on it
|
||||||
|
|
||||||
const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
const DC_GCM_ADDDAYMARKER: u32 = 0x01;
|
||||||
const DC_GCM_INFO_ONLY: u32 = 0x02;
|
|
||||||
|
|
||||||
// dc_context_t
|
// dc_context_t
|
||||||
|
|
||||||
@@ -101,7 +99,7 @@ pub unsafe extern "C" fn dc_context_new(
|
|||||||
|
|
||||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||||
// generate random ID as this functionality is not yet available on the C-api.
|
// generate random ID as this functionality is not yet available on the C-api.
|
||||||
let id = rand::thread_rng().gen();
|
let id = rand::random();
|
||||||
block_on(
|
block_on(
|
||||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||||
.with_id(id)
|
.with_id(id)
|
||||||
@@ -129,7 +127,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
|
|||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = rand::thread_rng().gen();
|
let id = rand::random();
|
||||||
match block_on(
|
match block_on(
|
||||||
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
ContextBuilder::new(as_path(dbfile).to_path_buf())
|
||||||
.with_id(id)
|
.with_id(id)
|
||||||
@@ -307,20 +305,17 @@ pub unsafe extern "C" fn dc_set_stock_translation(
|
|||||||
let msg = to_string_lossy(stock_msg);
|
let msg = to_string_lossy(stock_msg);
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(async move {
|
match StockMessage::from_u32(stock_id)
|
||||||
match StockMessage::from_u32(stock_id)
|
.with_context(|| format!("Invalid stock message ID {stock_id}"))
|
||||||
.with_context(|| format!("Invalid stock message ID {stock_id}"))
|
.log_err(ctx)
|
||||||
|
{
|
||||||
|
Ok(id) => ctx
|
||||||
|
.set_stock_translation(id, msg)
|
||||||
|
.context("set_stock_translation failed")
|
||||||
.log_err(ctx)
|
.log_err(ctx)
|
||||||
{
|
.is_ok() as libc::c_int,
|
||||||
Ok(id) => ctx
|
Err(_) => 0,
|
||||||
.set_stock_translation(id, msg)
|
}
|
||||||
.await
|
|
||||||
.context("set_stock_translation failed")
|
|
||||||
.log_err(ctx)
|
|
||||||
.is_ok() as libc::c_int,
|
|
||||||
Err(_) => 0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -375,7 +370,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
block_on(ctx.get_connectivity()) as u32 as libc::c_int
|
ctx.get_connectivity() as u32 as libc::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -556,6 +551,11 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
|||||||
EventType::AccountsChanged => 2302,
|
EventType::AccountsChanged => 2302,
|
||||||
EventType::AccountsItemChanged => 2303,
|
EventType::AccountsItemChanged => 2303,
|
||||||
EventType::EventChannelOverflow { .. } => 2400,
|
EventType::EventChannelOverflow { .. } => 2400,
|
||||||
|
EventType::IncomingCall { .. } => 2550,
|
||||||
|
EventType::IncomingCallAccepted { .. } => 2560,
|
||||||
|
EventType::OutgoingCallAccepted { .. } => 2570,
|
||||||
|
EventType::CallEnded { .. } => 2580,
|
||||||
|
EventType::TransportsModified => 2600,
|
||||||
#[allow(unreachable_patterns)]
|
#[allow(unreachable_patterns)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||||
@@ -590,7 +590,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::AccountsBackgroundFetchDone
|
| EventType::AccountsBackgroundFetchDone
|
||||||
| EventType::ChatlistChanged
|
| EventType::ChatlistChanged
|
||||||
| EventType::AccountsChanged
|
| EventType::AccountsChanged
|
||||||
| EventType::AccountsItemChanged => 0,
|
| EventType::AccountsItemChanged
|
||||||
|
| EventType::TransportsModified => 0,
|
||||||
EventType::IncomingReaction { contact_id, .. }
|
EventType::IncomingReaction { contact_id, .. }
|
||||||
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
|
||||||
EventType::MsgsChanged { chat_id, .. }
|
EventType::MsgsChanged { chat_id, .. }
|
||||||
@@ -619,7 +620,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
|||||||
EventType::WebxdcRealtimeData { msg_id, .. }
|
EventType::WebxdcRealtimeData { msg_id, .. }
|
||||||
| EventType::WebxdcStatusUpdate { msg_id, .. }
|
| EventType::WebxdcStatusUpdate { msg_id, .. }
|
||||||
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
|
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
|
||||||
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
| EventType::WebxdcInstanceDeleted { msg_id, .. }
|
||||||
|
| EventType::IncomingCall { msg_id, .. }
|
||||||
|
| EventType::IncomingCallAccepted { msg_id, .. }
|
||||||
|
| EventType::OutgoingCallAccepted { msg_id, .. }
|
||||||
|
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||||
EventType::ChatlistItemChanged { chat_id } => {
|
EventType::ChatlistItemChanged { chat_id } => {
|
||||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||||
}
|
}
|
||||||
@@ -671,7 +676,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::ChatModified(_)
|
| EventType::ChatModified(_)
|
||||||
| EventType::ChatDeleted { .. }
|
| EventType::ChatDeleted { .. }
|
||||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||||
| EventType::EventChannelOverflow { .. } => 0,
|
| EventType::OutgoingCallAccepted { .. }
|
||||||
|
| EventType::CallEnded { .. }
|
||||||
|
| EventType::EventChannelOverflow { .. }
|
||||||
|
| EventType::TransportsModified => 0,
|
||||||
EventType::MsgsChanged { msg_id, .. }
|
EventType::MsgsChanged { msg_id, .. }
|
||||||
| EventType::ReactionsChanged { msg_id, .. }
|
| EventType::ReactionsChanged { msg_id, .. }
|
||||||
| EventType::IncomingReaction { msg_id, .. }
|
| EventType::IncomingReaction { msg_id, .. }
|
||||||
@@ -689,6 +697,11 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
|||||||
..
|
..
|
||||||
} => status_update_serial.to_u32() as libc::c_int,
|
} => status_update_serial.to_u32() as libc::c_int,
|
||||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||||
|
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
|
||||||
|
EventType::IncomingCallAccepted {
|
||||||
|
from_this_device, ..
|
||||||
|
} => *from_this_device as libc::c_int,
|
||||||
|
|
||||||
#[allow(unreachable_patterns)]
|
#[allow(unreachable_patterns)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||||
@@ -767,8 +780,22 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
|||||||
| EventType::ChatlistChanged
|
| EventType::ChatlistChanged
|
||||||
| EventType::AccountsChanged
|
| EventType::AccountsChanged
|
||||||
| EventType::AccountsItemChanged
|
| EventType::AccountsItemChanged
|
||||||
|
| EventType::IncomingCallAccepted { .. }
|
||||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
| EventType::TransportsModified => ptr::null_mut(),
|
||||||
|
EventType::IncomingCall {
|
||||||
|
place_call_info, ..
|
||||||
|
} => {
|
||||||
|
let data2 = place_call_info.to_c_string().unwrap_or_default();
|
||||||
|
data2.into_raw()
|
||||||
|
}
|
||||||
|
EventType::OutgoingCallAccepted {
|
||||||
|
accept_call_info, ..
|
||||||
|
} => {
|
||||||
|
let data2 = accept_call_info.to_c_string().unwrap_or_default();
|
||||||
|
data2.into_raw()
|
||||||
|
}
|
||||||
|
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||||
EventType::ConfigureProgress { comment, .. } => {
|
EventType::ConfigureProgress { comment, .. } => {
|
||||||
if let Some(comment) = comment {
|
if let Some(comment) = comment {
|
||||||
comment.to_c_string().unwrap_or_default().into_raw()
|
comment.to_c_string().unwrap_or_default().into_raw()
|
||||||
@@ -1072,25 +1099,6 @@ pub unsafe extern "C" fn dc_send_delete_request(
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
|
||||||
context: *mut dc_context_t,
|
|
||||||
chat_id: u32,
|
|
||||||
) -> u32 {
|
|
||||||
if context.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
|
|
||||||
block_on(async move {
|
|
||||||
chat::send_videochat_invitation(ctx, ChatId::new(chat_id))
|
|
||||||
.await
|
|
||||||
.map(|msg_id| msg_id.to_u32())
|
|
||||||
.unwrap_or_log_default(ctx, "Failed to send video chat invitation")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -1167,6 +1175,62 @@ pub unsafe extern "C" fn dc_init_webxdc_integration(
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_place_outgoing_call(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
chat_id: u32,
|
||||||
|
place_call_info: *const libc::c_char,
|
||||||
|
has_video: bool,
|
||||||
|
) -> u32 {
|
||||||
|
if context.is_null() || chat_id == 0 {
|
||||||
|
eprintln!("ignoring careless call to dc_place_outgoing_call()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
let chat_id = ChatId::new(chat_id);
|
||||||
|
let place_call_info = to_string_lossy(place_call_info);
|
||||||
|
|
||||||
|
block_on(ctx.place_outgoing_call(chat_id, place_call_info, has_video))
|
||||||
|
.context("Failed to place call")
|
||||||
|
.log_err(ctx)
|
||||||
|
.map(|msg_id| msg_id.to_u32())
|
||||||
|
.unwrap_or_log_default(ctx, "Failed to place call")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_accept_incoming_call(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
msg_id: u32,
|
||||||
|
accept_call_info: *const libc::c_char,
|
||||||
|
) -> libc::c_int {
|
||||||
|
if context.is_null() || msg_id == 0 {
|
||||||
|
eprintln!("ignoring careless call to dc_accept_incoming_call()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
let msg_id = MsgId::new(msg_id);
|
||||||
|
let accept_call_info = to_string_lossy(accept_call_info);
|
||||||
|
|
||||||
|
block_on(ctx.accept_incoming_call(msg_id, accept_call_info))
|
||||||
|
.context("Failed to accept call")
|
||||||
|
.is_ok() as libc::c_int
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_end_call(context: *mut dc_context_t, msg_id: u32) -> libc::c_int {
|
||||||
|
if context.is_null() || msg_id == 0 {
|
||||||
|
eprintln!("ignoring careless call to dc_end_call()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
let msg_id = MsgId::new(msg_id);
|
||||||
|
|
||||||
|
block_on(ctx.end_call(msg_id))
|
||||||
|
.context("Failed to end call")
|
||||||
|
.log_err(ctx)
|
||||||
|
.is_ok() as libc::c_int
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_set_draft(
|
pub unsafe extern "C" fn dc_set_draft(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -1273,17 +1337,13 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
|||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
let info_only = (flags & DC_GCM_INFO_ONLY) != 0;
|
|
||||||
let add_daymarker = (flags & DC_GCM_ADDDAYMARKER) != 0;
|
let add_daymarker = (flags & DC_GCM_ADDDAYMARKER) != 0;
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
Box::into_raw(Box::new(
|
Box::into_raw(Box::new(
|
||||||
chat::get_chat_msgs_ex(
|
chat::get_chat_msgs_ex(
|
||||||
ctx,
|
ctx,
|
||||||
ChatId::new(chat_id),
|
ChatId::new(chat_id),
|
||||||
MessageListOptions {
|
MessageListOptions { add_daymarker },
|
||||||
info_only,
|
|
||||||
add_daymarker,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_log_default(ctx, "failed to get chat msgs")
|
.unwrap_or_log_default(ctx, "failed to get chat msgs")
|
||||||
@@ -1455,6 +1515,23 @@ pub unsafe extern "C" fn dc_marknoticed_chat(context: *mut dc_context_t, chat_id
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_markfresh_chat(context: *mut dc_context_t, chat_id: u32) {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_markfresh_chat()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
|
||||||
|
block_on(async move {
|
||||||
|
chat::markfresh_chat(ctx, ChatId::new(chat_id))
|
||||||
|
.await
|
||||||
|
.context("Failed markfresh chat")
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn from_prim<S, T>(s: S) -> Option<T>
|
fn from_prim<S, T>(s: S) -> Option<T>
|
||||||
where
|
where
|
||||||
T: FromPrimitive,
|
T: FromPrimitive,
|
||||||
@@ -1659,7 +1736,7 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_create_group_chat(
|
pub unsafe extern "C" fn dc_create_group_chat(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
protect: libc::c_int,
|
_protect: libc::c_int,
|
||||||
name: *const libc::c_char,
|
name: *const libc::c_char,
|
||||||
) -> u32 {
|
) -> u32 {
|
||||||
if context.is_null() || name.is_null() {
|
if context.is_null() || name.is_null() {
|
||||||
@@ -1667,22 +1744,12 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
let Some(protect) = ProtectionStatus::from_i32(protect)
|
|
||||||
.context("Bad protect-value for dc_create_group_chat()")
|
|
||||||
.log_err(ctx)
|
|
||||||
.ok()
|
|
||||||
else {
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
block_on(async move {
|
block_on(chat::create_group(ctx, &to_string_lossy(name)))
|
||||||
chat::create_group_chat(ctx, protect, &to_string_lossy(name))
|
.context("Failed to create group chat")
|
||||||
.await
|
.log_err(ctx)
|
||||||
.context("Failed to create group chat")
|
.map(|id| id.to_u32())
|
||||||
.log_err(ctx)
|
.unwrap_or(0)
|
||||||
.map(|id| id.to_u32())
|
|
||||||
.unwrap_or(0)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -2206,22 +2273,6 @@ pub unsafe extern "C" fn dc_get_contacts(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc::c_int {
|
|
||||||
if context.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_get_blocked_cnt()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
|
|
||||||
block_on(async move {
|
|
||||||
Contact::get_all_blocked(ctx)
|
|
||||||
.await
|
|
||||||
.unwrap_or_log_default(ctx, "failed to get blocked count")
|
|
||||||
.len() as libc::c_int
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_get_blocked_contacts(
|
pub unsafe extern "C" fn dc_get_blocked_contacts(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -2387,45 +2438,6 @@ pub unsafe extern "C" fn dc_imex_has_backup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) -> *mut libc::c_char {
|
|
||||||
if context.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_initiate_key_transfer()");
|
|
||||||
return ptr::null_mut(); // NULL explicitly defined as "error"
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
|
|
||||||
match block_on(imex::initiate_key_transfer(ctx))
|
|
||||||
.context("dc_initiate_key_transfer()")
|
|
||||||
.log_err(ctx)
|
|
||||||
{
|
|
||||||
Ok(res) => res.strdup(),
|
|
||||||
Err(_) => ptr::null_mut(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_continue_key_transfer(
|
|
||||||
context: *mut dc_context_t,
|
|
||||||
msg_id: u32,
|
|
||||||
setup_code: *const libc::c_char,
|
|
||||||
) -> libc::c_int {
|
|
||||||
if context.is_null() || msg_id <= constants::DC_MSG_ID_LAST_SPECIAL || setup_code.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_continue_key_transfer()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
|
|
||||||
block_on(imex::continue_key_transfer(
|
|
||||||
ctx,
|
|
||||||
MsgId::new(msg_id),
|
|
||||||
&to_string_lossy(setup_code),
|
|
||||||
))
|
|
||||||
.context("dc_continue_key_transfer")
|
|
||||||
.log_err(ctx)
|
|
||||||
.is_ok() as libc::c_int
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_stop_ongoing_process(context: *mut dc_context_t) {
|
pub unsafe extern "C" fn dc_stop_ongoing_process(context: *mut dc_context_t) {
|
||||||
if context.is_null() {
|
if context.is_null() {
|
||||||
@@ -2529,7 +2541,7 @@ pub unsafe extern "C" fn dc_send_locations_to_chat(
|
|||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(location::send_locations_to_chat(
|
block_on(location::send_to_chat(
|
||||||
ctx,
|
ctx,
|
||||||
ChatId::new(chat_id),
|
ChatId::new(chat_id),
|
||||||
seconds as i64,
|
seconds as i64,
|
||||||
@@ -2549,14 +2561,14 @@ pub unsafe extern "C" fn dc_is_sending_locations_to_chat(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
let chat_id = if chat_id == 0 {
|
if chat_id == 0 {
|
||||||
None
|
block_on(location::is_sending(ctx))
|
||||||
|
.unwrap_or_log_default(ctx, "Failed is_sending_locations()") as libc::c_int
|
||||||
} else {
|
} else {
|
||||||
Some(ChatId::new(chat_id))
|
block_on(location::is_sending_to_chat(ctx, ChatId::new(chat_id)))
|
||||||
};
|
.unwrap_or_log_default(ctx, "Failed is_sending_locations_to_chat()")
|
||||||
|
as libc::c_int
|
||||||
block_on(location::is_sending_locations_to_chat(ctx, chat_id))
|
}
|
||||||
.unwrap_or_log_default(ctx, "Failed dc_is_sending_locations_to_chat()") as libc::c_int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -2572,12 +2584,9 @@ pub unsafe extern "C" fn dc_set_location(
|
|||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(location::set(ctx, latitude, longitude, accuracy))
|
||||||
location::set(ctx, latitude, longitude, accuracy)
|
.log_err(ctx)
|
||||||
.await
|
.unwrap_or_default() as libc::c_int
|
||||||
.log_err(ctx)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}) as libc::c_int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -2612,23 +2621,6 @@ pub unsafe extern "C" fn dc_get_locations(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_delete_all_locations(context: *mut dc_context_t) {
|
|
||||||
if context.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_delete_all_locations()");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
|
|
||||||
block_on(async move {
|
|
||||||
location::delete_all(ctx)
|
|
||||||
.await
|
|
||||||
.context("Failed to delete locations")
|
|
||||||
.log_err(ctx)
|
|
||||||
.ok()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
|
pub unsafe extern "C" fn dc_create_qr_svg(payload: *const libc::c_char) -> *mut libc::c_char {
|
||||||
if payload.is_null() {
|
if payload.is_null() {
|
||||||
@@ -2809,7 +2801,7 @@ pub unsafe extern "C" fn dc_array_search_id(
|
|||||||
// Returns 1 if location belongs to the track of the user,
|
// Returns 1 if location belongs to the track of the user,
|
||||||
// 0 if location was reported independently.
|
// 0 if location was reported independently.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe fn dc_array_is_independent(
|
pub unsafe extern "C" fn dc_array_is_independent(
|
||||||
array: *const dc_array_t,
|
array: *const dc_array_t,
|
||||||
index: libc::size_t,
|
index: libc::size_t,
|
||||||
) -> libc::c_int {
|
) -> libc::c_int {
|
||||||
@@ -3144,13 +3136,8 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_int {
|
pub extern "C" fn dc_chat_is_protected(_chat: *mut dc_chat_t) -> libc::c_int {
|
||||||
if chat.is_null() {
|
0
|
||||||
eprintln!("ignoring careless call to dc_chat_is_protected()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ffi_chat = &*chat;
|
|
||||||
ffi_chat.chat.is_protected() as libc::c_int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -3165,16 +3152,6 @@ pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_i
|
|||||||
.unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
|
.unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
|
|
||||||
if chat.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ffi_chat = &*chat;
|
|
||||||
ffi_chat.chat.is_protection_broken() as libc::c_int
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
||||||
if chat.is_null() {
|
if chat.is_null() {
|
||||||
@@ -3763,16 +3740,6 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc
|
|||||||
ffi_msg.message.get_webxdc_href().strdup()
|
ffi_msg.message.get_webxdc_href().strdup()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_int {
|
|
||||||
if msg.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_msg_is_setupmessage()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ffi_msg = &*msg;
|
|
||||||
ffi_msg.message.is_setupmessage().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
@@ -3783,45 +3750,6 @@ pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
|
|||||||
ffi_msg.message.has_html().into()
|
ffi_msg.message.has_html().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
|
||||||
if msg.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
|
|
||||||
return "".strdup();
|
|
||||||
}
|
|
||||||
let ffi_msg = &*msg;
|
|
||||||
|
|
||||||
ffi_msg
|
|
||||||
.message
|
|
||||||
.get_videochat_url()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
|
|
||||||
if msg.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ffi_msg = &*msg;
|
|
||||||
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
|
||||||
if msg.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_msg_get_setupcodebegin()");
|
|
||||||
return "".strdup();
|
|
||||||
}
|
|
||||||
let ffi_msg = &*msg;
|
|
||||||
let ctx = &*ffi_msg.context;
|
|
||||||
|
|
||||||
block_on(ffi_msg.message.get_setupcodebegin(ctx))
|
|
||||||
.unwrap_or_default()
|
|
||||||
.strdup()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::c_char) {
|
pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::c_char) {
|
||||||
if msg.is_null() {
|
if msg.is_null() {
|
||||||
@@ -4101,16 +4029,6 @@ pub unsafe extern "C" fn dc_msg_get_saved_msg_id(msg: *const dc_msg_t) -> u32 {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
|
|
||||||
if msg.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_msg_force_plaintext()");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ffi_msg = &mut *msg;
|
|
||||||
ffi_msg.message.force_plaintext();
|
|
||||||
}
|
|
||||||
|
|
||||||
// dc_contact_t
|
// dc_contact_t
|
||||||
|
|
||||||
/// FFI struct for [dc_contact_t]
|
/// FFI struct for [dc_contact_t]
|
||||||
@@ -4229,7 +4147,17 @@ pub unsafe extern "C" fn dc_contact_get_color(contact: *mut dc_contact_t) -> u32
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ffi_contact = &*contact;
|
let ffi_contact = &*contact;
|
||||||
ffi_contact.contact.get_color()
|
let ctx = &*ffi_contact.context;
|
||||||
|
block_on(async move {
|
||||||
|
ffi_contact
|
||||||
|
.contact
|
||||||
|
// We don't want any UIs displaying gray self-color.
|
||||||
|
.get_or_gen_color(ctx)
|
||||||
|
.await
|
||||||
|
.context("Contact::get_color()")
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or(0)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -4634,13 +4562,9 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
|||||||
|
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
match block_on(provider::get_provider_info_by_addr(
|
match provider::get_provider_info_by_addr(addr.as_str())
|
||||||
ctx,
|
.log_err(ctx)
|
||||||
addr.as_str(),
|
.unwrap_or_default()
|
||||||
true,
|
|
||||||
))
|
|
||||||
.log_err(ctx)
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
{
|
||||||
Some(provider) => provider,
|
Some(provider) => provider,
|
||||||
None => ptr::null_mut(),
|
None => ptr::null_mut(),
|
||||||
@@ -4659,25 +4583,13 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
|||||||
let addr = to_string_lossy(addr);
|
let addr = to_string_lossy(addr);
|
||||||
|
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
|
|
||||||
.context("Can't get config")
|
|
||||||
.log_err(ctx);
|
|
||||||
|
|
||||||
match proxy_enabled {
|
match provider::get_provider_info_by_addr(addr.as_str())
|
||||||
Ok(proxy_enabled) => {
|
.log_err(ctx)
|
||||||
match block_on(provider::get_provider_info_by_addr(
|
.unwrap_or_default()
|
||||||
ctx,
|
{
|
||||||
addr.as_str(),
|
Some(provider) => provider,
|
||||||
proxy_enabled,
|
None => ptr::null_mut(),
|
||||||
))
|
|
||||||
.log_err(ctx)
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
|
||||||
Some(provider) => provider,
|
|
||||||
None => ptr::null_mut(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => ptr::null_mut(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4730,33 +4642,13 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
|||||||
|
|
||||||
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
||||||
/// `dc_accounts_t` in multiple threads at once.
|
/// `dc_accounts_t` in multiple threads at once.
|
||||||
pub struct AccountsWrapper {
|
pub type dc_accounts_t = RwLock<Accounts>;
|
||||||
inner: Arc<RwLock<Accounts>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for AccountsWrapper {
|
|
||||||
type Target = Arc<RwLock<Accounts>>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AccountsWrapper {
|
|
||||||
fn new(accounts: Accounts) -> Self {
|
|
||||||
let inner = Arc::new(RwLock::new(accounts));
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Struct representing a list of deltachat accounts.
|
|
||||||
pub type dc_accounts_t = AccountsWrapper;
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_new(
|
pub unsafe extern "C" fn dc_accounts_new(
|
||||||
dir: *const libc::c_char,
|
dir: *const libc::c_char,
|
||||||
writable: libc::c_int,
|
writable: libc::c_int,
|
||||||
) -> *mut dc_accounts_t {
|
) -> *const dc_accounts_t {
|
||||||
setup_panic!();
|
setup_panic!();
|
||||||
|
|
||||||
if dir.is_null() {
|
if dir.is_null() {
|
||||||
@@ -4767,7 +4659,99 @@ pub unsafe extern "C" fn dc_accounts_new(
|
|||||||
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
|
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
|
||||||
|
|
||||||
match accs {
|
match accs {
|
||||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
|
||||||
|
Err(err) => {
|
||||||
|
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||||
|
eprintln!("failed to create accounts: {err:#}");
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type dc_event_channel_t = Mutex<Option<Events>>;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_event_channel_new() -> *mut dc_event_channel_t {
|
||||||
|
Box::into_raw(Box::new(Mutex::new(Some(Events::new()))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release the events channel structure.
|
||||||
|
///
|
||||||
|
/// This function releases the memory of the `dc_event_channel_t` structure.
|
||||||
|
///
|
||||||
|
/// you can call it after calling dc_accounts_new_with_event_channel,
|
||||||
|
/// which took the events channel out of it already, so this just frees the underlying option.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_event_channel_unref(event_channel: *mut dc_event_channel_t) {
|
||||||
|
if event_channel.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_event_channel_unref()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(Box::from_raw(event_channel))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_event_channel_get_event_emitter(
|
||||||
|
event_channel: *mut dc_event_channel_t,
|
||||||
|
) -> *mut dc_event_emitter_t {
|
||||||
|
if event_channel.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_event_channel_get_event_emitter()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(event_channel) = &*(*event_channel)
|
||||||
|
.lock()
|
||||||
|
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
|
||||||
|
else {
|
||||||
|
eprintln!(
|
||||||
|
"ignoring careless call to dc_event_channel_get_event_emitter()
|
||||||
|
-> channel was already consumed, make sure you call this before dc_accounts_new_with_event_channel"
|
||||||
|
);
|
||||||
|
return ptr::null_mut();
|
||||||
|
};
|
||||||
|
|
||||||
|
let emitter = event_channel.get_emitter();
|
||||||
|
|
||||||
|
Box::into_raw(Box::new(emitter))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_accounts_new_with_event_channel(
|
||||||
|
dir: *const libc::c_char,
|
||||||
|
writable: libc::c_int,
|
||||||
|
event_channel: *mut dc_event_channel_t,
|
||||||
|
) -> *const dc_accounts_t {
|
||||||
|
setup_panic!();
|
||||||
|
|
||||||
|
if dir.is_null() || event_channel.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_accounts_new_with_event_channel()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
// consuming channel enforce that you need to get the event emitter
|
||||||
|
// before initializing the account manager,
|
||||||
|
// so that you don't miss events/errors during initialisation.
|
||||||
|
// It also prevents you from using the same channel on multiple account managers.
|
||||||
|
let Some(event_channel) = (*event_channel)
|
||||||
|
.lock()
|
||||||
|
.expect("call to dc_event_channel_get_event_emitter() failed: mutex is poisoned")
|
||||||
|
.take()
|
||||||
|
else {
|
||||||
|
eprintln!(
|
||||||
|
"ignoring careless call to dc_accounts_new_with_event_channel()
|
||||||
|
-> channel was already consumed"
|
||||||
|
);
|
||||||
|
return ptr::null_mut();
|
||||||
|
};
|
||||||
|
|
||||||
|
let accs = block_on(Accounts::new_with_events(
|
||||||
|
as_path(dir).into(),
|
||||||
|
writable != 0,
|
||||||
|
event_channel,
|
||||||
|
));
|
||||||
|
|
||||||
|
match accs {
|
||||||
|
Ok(accs) => Arc::into_raw(Arc::new(RwLock::new(accs))),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||||
eprintln!("failed to create accounts: {err:#}");
|
eprintln!("failed to create accounts: {err:#}");
|
||||||
@@ -4780,17 +4764,17 @@ pub unsafe extern "C" fn dc_accounts_new(
|
|||||||
///
|
///
|
||||||
/// This function releases the memory of the `dc_accounts_t` structure.
|
/// This function releases the memory of the `dc_accounts_t` structure.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_unref(accounts: *mut dc_accounts_t) {
|
pub unsafe extern "C" fn dc_accounts_unref(accounts: *const dc_accounts_t) {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_unref()");
|
eprintln!("ignoring careless call to dc_accounts_unref()");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let _ = Box::from_raw(accounts);
|
drop(Arc::from_raw(accounts));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_get_account(
|
pub unsafe extern "C" fn dc_accounts_get_account(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *const dc_accounts_t,
|
||||||
id: u32,
|
id: u32,
|
||||||
) -> *mut dc_context_t {
|
) -> *mut dc_context_t {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
@@ -4807,7 +4791,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *const dc_accounts_t,
|
||||||
) -> *mut dc_context_t {
|
) -> *mut dc_context_t {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
|
eprintln!("ignoring careless call to dc_accounts_get_selected_account()");
|
||||||
@@ -4823,7 +4807,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_select_account(
|
pub unsafe extern "C" fn dc_accounts_select_account(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *const dc_accounts_t,
|
||||||
id: u32,
|
id: u32,
|
||||||
) -> libc::c_int {
|
) -> libc::c_int {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
@@ -4847,13 +4831,13 @@ pub unsafe extern "C" fn dc_accounts_select_account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -> u32 {
|
pub unsafe extern "C" fn dc_accounts_add_account(accounts: *const dc_accounts_t) -> u32 {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_add_account()");
|
eprintln!("ignoring careless call to dc_accounts_add_account()");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts = &mut *accounts;
|
let accounts = &*accounts;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
let mut accounts = accounts.write().await;
|
let mut accounts = accounts.write().await;
|
||||||
@@ -4868,13 +4852,13 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
|
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *const dc_accounts_t) -> u32 {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
|
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts = &mut *accounts;
|
let accounts = &*accounts;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
let mut accounts = accounts.write().await;
|
let mut accounts = accounts.write().await;
|
||||||
@@ -4890,7 +4874,7 @@ pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accoun
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_remove_account(
|
pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *const dc_accounts_t,
|
||||||
id: u32,
|
id: u32,
|
||||||
) -> libc::c_int {
|
) -> libc::c_int {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
@@ -4898,7 +4882,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts = &mut *accounts;
|
let accounts = &*accounts;
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
let mut accounts = accounts.write().await;
|
let mut accounts = accounts.write().await;
|
||||||
@@ -4916,7 +4900,7 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_migrate_account(
|
pub unsafe extern "C" fn dc_accounts_migrate_account(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *const dc_accounts_t,
|
||||||
dbfile: *const libc::c_char,
|
dbfile: *const libc::c_char,
|
||||||
) -> u32 {
|
) -> u32 {
|
||||||
if accounts.is_null() || dbfile.is_null() {
|
if accounts.is_null() || dbfile.is_null() {
|
||||||
@@ -4924,7 +4908,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts = &mut *accounts;
|
let accounts = &*accounts;
|
||||||
let dbfile = to_string_lossy(dbfile);
|
let dbfile = to_string_lossy(dbfile);
|
||||||
|
|
||||||
block_on(async move {
|
block_on(async move {
|
||||||
@@ -4945,7 +4929,7 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *mut dc_array_t {
|
pub unsafe extern "C" fn dc_accounts_get_all(accounts: *const dc_accounts_t) -> *mut dc_array_t {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_get_all()");
|
eprintln!("ignoring careless call to dc_accounts_get_all()");
|
||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
@@ -4959,18 +4943,18 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *const dc_accounts_t) {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_start_io()");
|
eprintln!("ignoring careless call to dc_accounts_start_io()");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let accounts = &mut *accounts;
|
let accounts = &*accounts;
|
||||||
block_on(async move { accounts.write().await.start_io().await });
|
block_on(async move { accounts.write().await.start_io().await });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
|
pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *const dc_accounts_t) {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_stop_io()");
|
eprintln!("ignoring careless call to dc_accounts_stop_io()");
|
||||||
return;
|
return;
|
||||||
@@ -4981,7 +4965,7 @@ pub unsafe extern "C" fn dc_accounts_stop_io(accounts: *mut dc_accounts_t) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t) {
|
pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *const dc_accounts_t) {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_maybe_network()");
|
eprintln!("ignoring careless call to dc_accounts_maybe_network()");
|
||||||
return;
|
return;
|
||||||
@@ -4992,7 +4976,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network(accounts: *mut dc_accounts_t)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accounts_t) {
|
pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *const dc_accounts_t) {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_maybe_network_lost()");
|
eprintln!("ignoring careless call to dc_accounts_maybe_network_lost()");
|
||||||
return;
|
return;
|
||||||
@@ -5004,7 +4988,7 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_background_fetch(
|
pub unsafe extern "C" fn dc_accounts_background_fetch(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *const dc_accounts_t,
|
||||||
timeout_in_seconds: u64,
|
timeout_in_seconds: u64,
|
||||||
) -> libc::c_int {
|
) -> libc::c_int {
|
||||||
if accounts.is_null() || timeout_in_seconds <= 2 {
|
if accounts.is_null() || timeout_in_seconds <= 2 {
|
||||||
@@ -5022,9 +5006,20 @@ pub unsafe extern "C" fn dc_accounts_background_fetch(
|
|||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_accounts_stop_background_fetch(accounts: *const dc_accounts_t) {
|
||||||
|
if accounts.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_accounts_stop_background_fetch()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accounts = &*accounts;
|
||||||
|
block_on(accounts.read()).stop_background_fetch();
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *const dc_accounts_t,
|
||||||
token: *const libc::c_char,
|
token: *const libc::c_char,
|
||||||
) {
|
) {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
@@ -5047,7 +5042,7 @@ pub unsafe extern "C" fn dc_accounts_set_push_device_token(
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||||
accounts: *mut dc_accounts_t,
|
accounts: *const dc_accounts_t,
|
||||||
) -> *mut dc_event_emitter_t {
|
) -> *mut dc_event_emitter_t {
|
||||||
if accounts.is_null() {
|
if accounts.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
|
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
|
||||||
@@ -5067,17 +5062,17 @@ pub struct dc_jsonrpc_instance_t {
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_jsonrpc_init(
|
pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||||
account_manager: *mut dc_accounts_t,
|
account_manager: *const dc_accounts_t,
|
||||||
) -> *mut dc_jsonrpc_instance_t {
|
) -> *mut dc_jsonrpc_instance_t {
|
||||||
if account_manager.is_null() {
|
if account_manager.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
}
|
}
|
||||||
|
|
||||||
let account_manager = &*account_manager;
|
let account_manager = ManuallyDrop::new(Arc::from_raw(account_manager));
|
||||||
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
|
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(Arc::clone(
|
||||||
account_manager.inner.clone(),
|
&account_manager,
|
||||||
));
|
)));
|
||||||
|
|
||||||
let (request_handle, receiver) = RpcClient::new();
|
let (request_handle, receiver) = RpcClient::new();
|
||||||
let handle = RpcSession::new(request_handle, cmd_api);
|
let handle = RpcSession::new(request_handle, cmd_api);
|
||||||
|
|||||||
@@ -45,21 +45,23 @@ impl Lot {
|
|||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { .. } => None,
|
Qr::AskVerifyContact { .. } => None,
|
||||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||||
|
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||||
Qr::FprOk { .. } => None,
|
Qr::FprOk { .. } => None,
|
||||||
Qr::FprMismatch { .. } => None,
|
Qr::FprMismatch { .. } => None,
|
||||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||||
Qr::Backup2 { .. } => None,
|
Qr::Backup2 { .. } => None,
|
||||||
Qr::BackupTooNew { .. } => None,
|
Qr::BackupTooNew { .. } => None,
|
||||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
|
||||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||||
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
||||||
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
||||||
Qr::WithdrawVerifyContact { .. } => None,
|
Qr::WithdrawVerifyContact { .. } => None,
|
||||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||||
|
Qr::WithdrawJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||||
Qr::ReviveVerifyContact { .. } => None,
|
Qr::ReviveVerifyContact { .. } => None,
|
||||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||||
|
Qr::ReviveJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||||
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
||||||
},
|
},
|
||||||
Self::Error(err) => Some(Cow::Borrowed(err)),
|
Self::Error(err) => Some(Cow::Borrowed(err)),
|
||||||
@@ -99,21 +101,23 @@ impl Lot {
|
|||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
||||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||||
|
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
|
||||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||||
Qr::Account { .. } => LotState::QrAccount,
|
Qr::Account { .. } => LotState::QrAccount,
|
||||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
|
||||||
Qr::Proxy { .. } => LotState::QrProxy,
|
Qr::Proxy { .. } => LotState::QrProxy,
|
||||||
Qr::Addr { .. } => LotState::QrAddr,
|
Qr::Addr { .. } => LotState::QrAddr,
|
||||||
Qr::Url { .. } => LotState::QrUrl,
|
Qr::Url { .. } => LotState::QrUrl,
|
||||||
Qr::Text { .. } => LotState::QrText,
|
Qr::Text { .. } => LotState::QrText,
|
||||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
||||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||||
|
Qr::WithdrawJoinBroadcast { .. } => LotState::QrWithdrawJoinBroadcast,
|
||||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||||
|
Qr::ReviveJoinBroadcast { .. } => LotState::QrReviveJoinBroadcast,
|
||||||
Qr::Login { .. } => LotState::QrLogin,
|
Qr::Login { .. } => LotState::QrLogin,
|
||||||
},
|
},
|
||||||
Self::Error(_err) => LotState::QrError,
|
Self::Error(_err) => LotState::QrError,
|
||||||
@@ -126,21 +130,23 @@ impl Lot {
|
|||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||||
|
Qr::AskJoinBroadcast { .. } => Default::default(),
|
||||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
||||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||||
Qr::Account { .. } => Default::default(),
|
Qr::Account { .. } => Default::default(),
|
||||||
Qr::Backup2 { .. } => Default::default(),
|
Qr::Backup2 { .. } => Default::default(),
|
||||||
Qr::BackupTooNew { .. } => Default::default(),
|
Qr::BackupTooNew { .. } => Default::default(),
|
||||||
Qr::WebrtcInstance { .. } => Default::default(),
|
|
||||||
Qr::Proxy { .. } => Default::default(),
|
Qr::Proxy { .. } => Default::default(),
|
||||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::Url { .. } => Default::default(),
|
Qr::Url { .. } => Default::default(),
|
||||||
Qr::Text { .. } => Default::default(),
|
Qr::Text { .. } => Default::default(),
|
||||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
Qr::WithdrawVerifyGroup { .. } | Qr::WithdrawJoinBroadcast { .. } => {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
Qr::ReviveVerifyGroup { .. } | Qr::ReviveJoinBroadcast { .. } => Default::default(),
|
||||||
Qr::Login { .. } => Default::default(),
|
Qr::Login { .. } => Default::default(),
|
||||||
},
|
},
|
||||||
Self::Error(_) => Default::default(),
|
Self::Error(_) => Default::default(),
|
||||||
@@ -169,6 +175,9 @@ pub enum LotState {
|
|||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrAskVerifyGroup = 202,
|
QrAskVerifyGroup = 202,
|
||||||
|
|
||||||
|
/// text1=broadcast_name
|
||||||
|
QrAskJoinBroadcast = 204,
|
||||||
|
|
||||||
/// id=contact
|
/// id=contact
|
||||||
QrFprOk = 210,
|
QrFprOk = 210,
|
||||||
|
|
||||||
@@ -185,9 +194,6 @@ pub enum LotState {
|
|||||||
|
|
||||||
QrBackupTooNew = 255,
|
QrBackupTooNew = 255,
|
||||||
|
|
||||||
/// text1=domain, text2=instance pattern
|
|
||||||
QrWebrtcInstance = 260,
|
|
||||||
|
|
||||||
/// text1=address, text2=protocol
|
/// text1=address, text2=protocol
|
||||||
QrProxy = 271,
|
QrProxy = 271,
|
||||||
|
|
||||||
@@ -207,11 +213,15 @@ pub enum LotState {
|
|||||||
|
|
||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrWithdrawVerifyGroup = 502,
|
QrWithdrawVerifyGroup = 502,
|
||||||
|
/// text1=broadcast channel name
|
||||||
|
QrWithdrawJoinBroadcast = 504,
|
||||||
|
|
||||||
QrReviveVerifyContact = 510,
|
QrReviveVerifyContact = 510,
|
||||||
|
|
||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrReviveVerifyGroup = 512,
|
QrReviveVerifyGroup = 512,
|
||||||
|
/// text1=groupname
|
||||||
|
QrReviveJoinBroadcast = 514,
|
||||||
|
|
||||||
/// text1=email_address
|
/// text1=email_address
|
||||||
QrLogin = 520,
|
QrLogin = 520,
|
||||||
@@ -220,7 +230,6 @@ pub enum LotState {
|
|||||||
MsgInFresh = 10,
|
MsgInFresh = 10,
|
||||||
MsgInNoticed = 13,
|
MsgInNoticed = 13,
|
||||||
MsgInSeen = 16,
|
MsgInSeen = 16,
|
||||||
MsgOutPreparing = 18,
|
|
||||||
MsgOutDraft = 19,
|
MsgOutDraft = 19,
|
||||||
MsgOutPending = 20,
|
MsgOutPending = 20,
|
||||||
MsgOutFailed = 24,
|
MsgOutFailed = 24,
|
||||||
@@ -236,7 +245,6 @@ impl From<MessageState> for LotState {
|
|||||||
InFresh => LotState::MsgInFresh,
|
InFresh => LotState::MsgInFresh,
|
||||||
InNoticed => LotState::MsgInNoticed,
|
InNoticed => LotState::MsgInNoticed,
|
||||||
InSeen => LotState::MsgInSeen,
|
InSeen => LotState::MsgInSeen,
|
||||||
OutPreparing => LotState::MsgOutPreparing,
|
|
||||||
OutDraft => LotState::MsgOutDraft,
|
OutDraft => LotState::MsgOutDraft,
|
||||||
OutPending => LotState::MsgOutPending,
|
OutPending => LotState::MsgOutPending,
|
||||||
OutFailed => LotState::MsgOutFailed,
|
OutFailed => LotState::MsgOutFailed,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "2.2.0"
|
version = "2.50.0-dev"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -19,7 +19,6 @@ yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
|||||||
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
sanitize-filename = { workspace = true }
|
sanitize-filename = { workspace = true }
|
||||||
walkdir = "2.5.0"
|
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ pub enum Account {
|
|||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
addr: Option<String>,
|
addr: Option<String>,
|
||||||
// size: u32,
|
// size: u32,
|
||||||
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
|
profile_image: Option<String>,
|
||||||
color: String,
|
color: String,
|
||||||
/// Optional tag as "Work", "Family".
|
/// Optional tag as "Work", "Family".
|
||||||
/// Meant to help profile owner to differ between profiles with similar names.
|
/// Meant to help profile owner to differ between profiles with similar names.
|
||||||
@@ -32,7 +32,10 @@ impl Account {
|
|||||||
let addr = ctx.get_config(Config::Addr).await?;
|
let addr = ctx.get_config(Config::Addr).await?;
|
||||||
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
||||||
let color = color_int_to_hex_string(
|
let color = color_int_to_hex_string(
|
||||||
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
|
Contact::get_by_id(ctx, ContactId::SELF)
|
||||||
|
.await?
|
||||||
|
.get_or_gen_color(ctx)
|
||||||
|
.await?,
|
||||||
);
|
);
|
||||||
let private_tag = ctx.get_config(Config::PrivateTag).await?;
|
let private_tag = ctx.get_config(Config::PrivateTag).await?;
|
||||||
Ok(Account::Configured {
|
Ok(Account::Configured {
|
||||||
|
|||||||
97
deltachat-jsonrpc/src/api/types/calls.rs
Normal file
97
deltachat-jsonrpc/src/api/types/calls.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
|
||||||
|
use deltachat::calls::{call_state, CallState};
|
||||||
|
use deltachat::context::Context;
|
||||||
|
use deltachat::message::MsgId;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename = "CallInfo", rename_all = "camelCase")]
|
||||||
|
pub struct JsonrpcCallInfo {
|
||||||
|
/// SDP offer.
|
||||||
|
///
|
||||||
|
/// Can be used to manually answer the call
|
||||||
|
/// even if incoming call event was missed.
|
||||||
|
pub sdp_offer: String,
|
||||||
|
|
||||||
|
/// True if the call is started as a video call.
|
||||||
|
pub has_video: bool,
|
||||||
|
|
||||||
|
/// Call state.
|
||||||
|
///
|
||||||
|
/// For example, if the call is accepted, active, canceled, declined etc.
|
||||||
|
pub state: JsonrpcCallState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonrpcCallInfo {
|
||||||
|
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallInfo> {
|
||||||
|
let call_info = context.load_call_by_id(msg_id).await?.with_context(|| {
|
||||||
|
format!("Attempting to get call state of non-call message {msg_id}")
|
||||||
|
})?;
|
||||||
|
let sdp_offer = call_info.place_call_info.clone();
|
||||||
|
let has_video = call_info.has_video_initially();
|
||||||
|
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
|
||||||
|
|
||||||
|
Ok(JsonrpcCallInfo {
|
||||||
|
sdp_offer,
|
||||||
|
has_video,
|
||||||
|
state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename = "CallState", tag = "kind")]
|
||||||
|
pub enum JsonrpcCallState {
|
||||||
|
/// Fresh incoming or outgoing call that is still ringing.
|
||||||
|
///
|
||||||
|
/// There is no separate state for outgoing call
|
||||||
|
/// that has been dialled but not ringing on the other side yet
|
||||||
|
/// as we don't know whether the other side received our call.
|
||||||
|
Alerting,
|
||||||
|
|
||||||
|
/// Active call.
|
||||||
|
Active,
|
||||||
|
|
||||||
|
/// Completed call that was once active
|
||||||
|
/// and then was terminated for any reason.
|
||||||
|
Completed {
|
||||||
|
/// Call duration in seconds.
|
||||||
|
duration: i64,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Incoming call that was not picked up within a timeout
|
||||||
|
/// or was explicitly ended by the caller before we picked up.
|
||||||
|
Missed,
|
||||||
|
|
||||||
|
/// Incoming call that was explicitly ended on our side
|
||||||
|
/// before picking up or outgoing call
|
||||||
|
/// that was declined before the timeout.
|
||||||
|
Declined,
|
||||||
|
|
||||||
|
/// Outgoing call that has been canceled on our side
|
||||||
|
/// before receiving a response.
|
||||||
|
///
|
||||||
|
/// Incoming calls cannot be canceled,
|
||||||
|
/// on the receiver side canceled calls
|
||||||
|
/// usually result in missed calls.
|
||||||
|
Canceled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonrpcCallState {
|
||||||
|
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallState> {
|
||||||
|
let call_state = call_state(context, msg_id).await?;
|
||||||
|
|
||||||
|
let jsonrpc_call_state = match call_state {
|
||||||
|
CallState::Alerting => JsonrpcCallState::Alerting,
|
||||||
|
CallState::Active => JsonrpcCallState::Active,
|
||||||
|
CallState::Completed { duration } => JsonrpcCallState::Completed { duration },
|
||||||
|
CallState::Missed => JsonrpcCallState::Missed,
|
||||||
|
CallState::Declined => JsonrpcCallState::Declined,
|
||||||
|
CallState::Canceled => JsonrpcCallState::Canceled,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(jsonrpc_call_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,10 @@ use deltachat::chat::{Chat, ChatId};
|
|||||||
use deltachat::constants::Chattype;
|
use deltachat::constants::Chattype;
|
||||||
use deltachat::contact::{Contact, ContactId};
|
use deltachat::contact::{Contact, ContactId};
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use num_traits::cast::ToPrimitive;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, 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::contact::ContactObject;
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -19,17 +17,6 @@ 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,
|
|
||||||
/// True if the chat is encrypted.
|
/// True if the chat is encrypted.
|
||||||
/// This means that all messages in the chat are encrypted,
|
/// This means that all messages in the chat are encrypted,
|
||||||
/// and all contacts in the chat are "key-contacts",
|
/// and all contacts in the chat are "key-contacts",
|
||||||
@@ -57,10 +44,9 @@ pub struct FullChat {
|
|||||||
archived: bool,
|
archived: bool,
|
||||||
pinned: bool,
|
pinned: bool,
|
||||||
// subtitle - will be moved to frontend because it uses translation functions
|
// subtitle - will be moved to frontend because it uses translation functions
|
||||||
chat_type: u32,
|
chat_type: JsonrpcChatType,
|
||||||
is_unpromoted: bool,
|
is_unpromoted: bool,
|
||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
contacts: Vec<ContactObject>,
|
|
||||||
contact_ids: Vec<u32>,
|
contact_ids: Vec<u32>,
|
||||||
|
|
||||||
/// Contact IDs of the past chat members.
|
/// Contact IDs of the past chat members.
|
||||||
@@ -70,11 +56,18 @@ 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,
|
||||||
|
/// Note that this is different from
|
||||||
|
/// [`ChatListItem::is_self_in_group`](`crate::api::types::chat_list::ChatListItemFetchResult::ChatListItem::is_self_in_group`).
|
||||||
|
/// This property should only be accessed
|
||||||
|
/// when [`FullChat::chat_type`] is [`Chattype::Group`].
|
||||||
|
//
|
||||||
|
// We could utilize [`Chat::is_self_in_chat`],
|
||||||
|
// but that would be an extra DB query.
|
||||||
self_in_group: bool,
|
self_in_group: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
|
ephemeral_timer: u32,
|
||||||
can_send: bool,
|
can_send: bool,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
mailing_list_address: Option<String>,
|
mailing_list_address: Option<String>,
|
||||||
@@ -88,20 +81,6 @@ impl FullChat {
|
|||||||
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
||||||
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
||||||
|
|
||||||
let mut contacts = Vec::with_capacity(contact_ids.len());
|
|
||||||
|
|
||||||
for contact_id in &contact_ids {
|
|
||||||
contacts.push(
|
|
||||||
ContactObject::try_from_dc_contact(
|
|
||||||
context,
|
|
||||||
Contact::get_by_id(context, *contact_id)
|
|
||||||
.await
|
|
||||||
.context("failed to load contact")?,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let profile_image = match chat.get_profile_image(context).await? {
|
let profile_image = match chat.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,
|
||||||
@@ -130,21 +109,18 @@ impl FullChat {
|
|||||||
Ok(FullChat {
|
Ok(FullChat {
|
||||||
id: chat_id,
|
id: chat_id,
|
||||||
name: chat.name.clone(),
|
name: chat.name.clone(),
|
||||||
is_protected: chat.is_protected(),
|
|
||||||
is_encrypted: chat.is_encrypted(context).await?,
|
is_encrypted: chat.is_encrypted(context).await?,
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
chat_type: chat.get_type().into(),
|
||||||
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,
|
|
||||||
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||||
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||||
color,
|
color,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
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(),
|
||||||
@@ -157,7 +133,6 @@ impl FullChat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// cheaper version of fullchat, omits:
|
/// cheaper version of fullchat, omits:
|
||||||
/// - contacts
|
|
||||||
/// - contact_ids
|
/// - contact_ids
|
||||||
/// - fresh_message_counter
|
/// - fresh_message_counter
|
||||||
/// - ephemeral_timer
|
/// - ephemeral_timer
|
||||||
@@ -172,18 +147,6 @@ 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,
|
|
||||||
|
|
||||||
/// True if the chat is encrypted.
|
/// True if the chat is encrypted.
|
||||||
/// This means that all messages in the chat are encrypted,
|
/// This means that all messages in the chat are encrypted,
|
||||||
/// and all contacts in the chat are "key-contacts",
|
/// and all contacts in the chat are "key-contacts",
|
||||||
@@ -210,12 +173,12 @@ pub struct BasicChat {
|
|||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
pinned: bool,
|
pinned: bool,
|
||||||
chat_type: u32,
|
chat_type: JsonrpcChatType,
|
||||||
is_unpromoted: bool,
|
is_unpromoted: bool,
|
||||||
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,
|
||||||
}
|
}
|
||||||
@@ -234,17 +197,15 @@ impl BasicChat {
|
|||||||
Ok(BasicChat {
|
Ok(BasicChat {
|
||||||
id: chat_id,
|
id: chat_id,
|
||||||
name: chat.name.clone(),
|
name: chat.name.clone(),
|
||||||
is_protected: chat.is_protected(),
|
|
||||||
is_encrypted: chat.is_encrypted(context).await?,
|
is_encrypted: chat.is_encrypted(context).await?,
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
||||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
chat_type: chat.get_type().into(),
|
||||||
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(),
|
||||||
})
|
})
|
||||||
@@ -279,18 +240,52 @@ impl MuteDuration {
|
|||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "ChatVisibility")]
|
#[serde(rename = "ChatVisibility")]
|
||||||
pub enum JSONRPCChatVisibility {
|
pub enum JsonrpcChatVisibility {
|
||||||
Normal,
|
Normal,
|
||||||
Archived,
|
Archived,
|
||||||
Pinned,
|
Pinned,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JSONRPCChatVisibility {
|
impl JsonrpcChatVisibility {
|
||||||
pub fn into_core_type(self) -> ChatVisibility {
|
pub fn into_core_type(self) -> ChatVisibility {
|
||||||
match self {
|
match self {
|
||||||
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
|
JsonrpcChatVisibility::Normal => ChatVisibility::Normal,
|
||||||
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
|
JsonrpcChatVisibility::Archived => ChatVisibility::Archived,
|
||||||
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
|
JsonrpcChatVisibility::Pinned => ChatVisibility::Pinned,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, PartialEq, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename = "ChatType")]
|
||||||
|
pub enum JsonrpcChatType {
|
||||||
|
Single,
|
||||||
|
Group,
|
||||||
|
Mailinglist,
|
||||||
|
OutBroadcast,
|
||||||
|
InBroadcast,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Chattype> for JsonrpcChatType {
|
||||||
|
fn from(chattype: Chattype) -> Self {
|
||||||
|
match chattype {
|
||||||
|
Chattype::Single => JsonrpcChatType::Single,
|
||||||
|
Chattype::Group => JsonrpcChatType::Group,
|
||||||
|
Chattype::Mailinglist => JsonrpcChatType::Mailinglist,
|
||||||
|
Chattype::OutBroadcast => JsonrpcChatType::OutBroadcast,
|
||||||
|
Chattype::InBroadcast => JsonrpcChatType::InBroadcast,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JsonrpcChatType> for Chattype {
|
||||||
|
fn from(chattype: JsonrpcChatType) -> Self {
|
||||||
|
match chattype {
|
||||||
|
JsonrpcChatType::Single => Chattype::Single,
|
||||||
|
JsonrpcChatType::Group => Chattype::Group,
|
||||||
|
JsonrpcChatType::Mailinglist => Chattype::Mailinglist,
|
||||||
|
JsonrpcChatType::OutBroadcast => Chattype::OutBroadcast,
|
||||||
|
JsonrpcChatType::InBroadcast => Chattype::InBroadcast,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
|||||||
use deltachat::chat::{Chat, ChatId};
|
use deltachat::chat::{Chat, ChatId};
|
||||||
use deltachat::chatlist::get_last_message_for_chat;
|
use deltachat::chatlist::get_last_message_for_chat;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::{Contact, ContactId};
|
use deltachat::contact::Contact;
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
chat::{get_chat_contacts, ChatVisibility},
|
chat::{get_chat_contacts, ChatVisibility},
|
||||||
chatlist::Chatlist,
|
chatlist::Chatlist,
|
||||||
@@ -11,6 +11,7 @@ use num_traits::cast::ToPrimitive;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
use super::chat::JsonrpcChatType;
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::message::MessageViewtype;
|
use super::message::MessageViewtype;
|
||||||
|
|
||||||
@@ -23,14 +24,13 @@ pub enum ChatListItemFetchResult {
|
|||||||
name: String,
|
name: String,
|
||||||
avatar_path: Option<String>,
|
avatar_path: Option<String>,
|
||||||
color: String,
|
color: String,
|
||||||
chat_type: u32,
|
chat_type: JsonrpcChatType,
|
||||||
last_updated: Option<i64>,
|
last_updated: Option<i64>,
|
||||||
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
|
/// showing preview if last chat message is image
|
||||||
summary_preview_image: Option<String>,
|
summary_preview_image: Option<String>,
|
||||||
is_protected: bool,
|
|
||||||
|
|
||||||
/// True if the chat is encrypted.
|
/// True if the chat is encrypted.
|
||||||
/// This means that all messages in the chat are encrypted,
|
/// This means that all messages in the chat are encrypted,
|
||||||
@@ -127,11 +127,8 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
None => (None, None),
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
|
||||||
|
|
||||||
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 chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||||
let contact = chat_contacts.first();
|
let contact = chat_contacts.first();
|
||||||
let was_seen_recently = match contact {
|
let was_seen_recently = match contact {
|
||||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||||
@@ -155,19 +152,18 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
name: chat.get_name().to_owned(),
|
name: chat.get_name().to_owned(),
|
||||||
avatar_path,
|
avatar_path,
|
||||||
color,
|
color,
|
||||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
chat_type: chat.get_type().into(),
|
||||||
last_updated,
|
last_updated,
|
||||||
summary_text1,
|
summary_text1,
|
||||||
summary_text2,
|
summary_text2,
|
||||||
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
||||||
summary_preview_image,
|
summary_preview_image,
|
||||||
is_protected: chat.is_protected(),
|
|
||||||
is_encrypted: chat.is_encrypted(ctx).await?,
|
is_encrypted: chat.is_encrypted(ctx).await?,
|
||||||
is_group: chat.get_type() == Chattype::Group,
|
is_group: chat.get_type() == Chattype::Group,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
is_device_talk: chat.is_device_talk(),
|
is_device_talk: chat.is_device_talk(),
|
||||||
is_self_in_group: self_in_group,
|
is_self_in_group: chat.is_self_in_chat(ctx).await?,
|
||||||
is_sending_location: chat.is_sending_locations(),
|
is_sending_location: chat.is_sending_locations(),
|
||||||
is_archived: visibility == ChatVisibility::Archived,
|
is_archived: visibility == ChatVisibility::Archived,
|
||||||
is_pinned: visibility == ChatVisibility::Pinned,
|
is_pinned: visibility == ChatVisibility::Pinned,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use deltachat::color;
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
|
use deltachat::key::{DcKey, SignedPublicKey};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
@@ -31,27 +31,35 @@ pub struct ContactObject {
|
|||||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||||
e2ee_avail: bool,
|
e2ee_avail: bool,
|
||||||
|
|
||||||
/// True if the contact can be added to verified groups.
|
/// True if the contact
|
||||||
|
/// can be added to protected chats
|
||||||
|
/// because SELF and contact have verified their fingerprints in both directions.
|
||||||
///
|
///
|
||||||
/// If this is true
|
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||||
/// UI should display green checkmark after the contact name
|
|
||||||
/// in contact list items,
|
|
||||||
/// in chat member list items
|
|
||||||
/// and in profiles if no chat with the contact exist.
|
|
||||||
is_verified: bool,
|
is_verified: bool,
|
||||||
|
|
||||||
/// True if the contact profile title should have a green checkmark.
|
/// The contact ID that verified a contact.
|
||||||
///
|
///
|
||||||
/// This indicates whether 1:1 chat has a green checkmark
|
/// As verifier may be unknown,
|
||||||
/// or will have a green checkmark if created.
|
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||||
is_profile_verified: bool,
|
|
||||||
|
|
||||||
/// The ID of the contact that verified this contact.
|
|
||||||
///
|
///
|
||||||
/// If this is present,
|
/// UI should display the information in the contact's profile as follows:
|
||||||
/// display a green checkmark and "Introduced by ..."
|
///
|
||||||
/// string followed by the verifier contact name and address
|
/// - If `verifierId` != 0,
|
||||||
/// in the contact profile.
|
/// display text "Introduced by ..."
|
||||||
|
/// with the name of the contact.
|
||||||
|
/// Prefix the text by a green checkmark.
|
||||||
|
///
|
||||||
|
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||||
|
/// display "Introduced" prefixed by a green checkmark.
|
||||||
|
///
|
||||||
|
/// - if `verifierId` == 0 and `isVerified` == 0,
|
||||||
|
/// display nothing
|
||||||
|
///
|
||||||
|
/// This contains the contact ID of the verifier.
|
||||||
|
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
||||||
|
/// If it is None/Null, we don't have verifier information or
|
||||||
|
/// the contact is not verified.
|
||||||
verifier_id: Option<u32>,
|
verifier_id: Option<u32>,
|
||||||
|
|
||||||
/// the contact's last seen timestamp
|
/// the contact's last seen timestamp
|
||||||
@@ -72,7 +80,6 @@ impl ContactObject {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let is_verified = contact.is_verified(context).await?;
|
let is_verified = contact.is_verified(context).await?;
|
||||||
let is_profile_verified = contact.is_profile_verified(context).await?;
|
|
||||||
|
|
||||||
let verifier_id = contact
|
let verifier_id = contact
|
||||||
.get_verifier_id(context)
|
.get_verifier_id(context)
|
||||||
@@ -94,7 +101,6 @@ impl ContactObject {
|
|||||||
is_key_contact: contact.is_key_contact(),
|
is_key_contact: contact.is_key_contact(),
|
||||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||||
is_verified,
|
is_verified,
|
||||||
is_profile_verified,
|
|
||||||
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(),
|
||||||
@@ -123,7 +129,13 @@ pub struct VcardContact {
|
|||||||
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
|
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
|
||||||
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
|
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
|
||||||
let display_name = vc.display_name().to_string();
|
let display_name = vc.display_name().to_string();
|
||||||
let color = color::str_to_color(&vc.addr.to_lowercase());
|
let is_self = false;
|
||||||
|
let fpr = vc.key.as_deref().and_then(|k| {
|
||||||
|
SignedPublicKey::from_base64(k)
|
||||||
|
.ok()
|
||||||
|
.map(|k| k.dc_fingerprint())
|
||||||
|
});
|
||||||
|
let color = deltachat::contact::get_color(is_self, &vc.addr, &fpr);
|
||||||
Self {
|
Self {
|
||||||
addr: vc.addr,
|
addr: vc.addr,
|
||||||
display_name,
|
display_name,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
use super::chat::JsonrpcChatType;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
@@ -224,7 +226,6 @@ pub enum EventType {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||||
/// Or the verify state of a chat has changed.
|
|
||||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||||
/// and removeContactFromChat().
|
/// and removeContactFromChat().
|
||||||
///
|
///
|
||||||
@@ -270,7 +271,7 @@ pub enum EventType {
|
|||||||
/// Progress.
|
/// Progress.
|
||||||
///
|
///
|
||||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
progress: usize,
|
progress: u16,
|
||||||
|
|
||||||
/// Progress comment or error, something to display to the user.
|
/// Progress comment or error, something to display to the user.
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
@@ -281,7 +282,7 @@ pub enum EventType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexProgress {
|
ImexProgress {
|
||||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
progress: usize,
|
progress: u16,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// A file has been exported. A file has been written by imex().
|
/// A file has been exported. A file has been written by imex().
|
||||||
@@ -294,8 +295,8 @@ pub enum EventType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexFileWritten { path: String },
|
ImexFileWritten { path: String },
|
||||||
|
|
||||||
/// Progress information of a secure-join handshake from the view of the inviter
|
/// Progress event sent when SecureJoin protocol has finished
|
||||||
/// (Alice, the person who shows the QR code).
|
/// from the view of the inviter (Alice, the person who shows the QR code).
|
||||||
///
|
///
|
||||||
/// These events are typically sent after a joiner has scanned the QR code
|
/// These events are typically sent after a joiner has scanned the QR code
|
||||||
/// generated by getChatSecurejoinQrCodeSvg().
|
/// generated by getChatSecurejoinQrCodeSvg().
|
||||||
@@ -304,12 +305,15 @@ pub enum EventType {
|
|||||||
/// ID of the contact that wants to join.
|
/// ID of the contact that wants to join.
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
|
|
||||||
/// Progress as:
|
/// The type of the joined chat.
|
||||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
/// This can take the same values
|
||||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
|
||||||
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
|
chat_type: JsonrpcChatType,
|
||||||
/// 1000=Protocol finished for this contact.
|
/// ID of the chat in case of success.
|
||||||
progress: usize,
|
chat_id: u32,
|
||||||
|
|
||||||
|
/// Progress, always 1000.
|
||||||
|
progress: u16,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -325,7 +329,7 @@ pub enum EventType {
|
|||||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
/// 1000=vg-member-added/vc-contact-confirm received
|
/// 1000=vg-member-added/vc-contact-confirm received
|
||||||
progress: usize,
|
progress: u16,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// The connectivity to the server changed.
|
/// The connectivity to the server changed.
|
||||||
@@ -417,6 +421,56 @@ pub enum EventType {
|
|||||||
/// Number of events skipped.
|
/// Number of events skipped.
|
||||||
n: u64,
|
n: u64,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Incoming call.
|
||||||
|
IncomingCall {
|
||||||
|
/// ID of the info message referring to the call.
|
||||||
|
msg_id: u32,
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
|
chat_id: u32,
|
||||||
|
/// User-defined info as passed to place_outgoing_call()
|
||||||
|
place_call_info: String,
|
||||||
|
/// True if incoming call is a video call.
|
||||||
|
has_video: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Incoming call accepted.
|
||||||
|
/// This is esp. interesting to stop ringing on other devices.
|
||||||
|
IncomingCallAccepted {
|
||||||
|
/// ID of the info message referring to the call.
|
||||||
|
msg_id: u32,
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
|
chat_id: u32,
|
||||||
|
/// The call was accepted from this device (process).
|
||||||
|
from_this_device: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Outgoing call accepted.
|
||||||
|
OutgoingCallAccepted {
|
||||||
|
/// ID of the info message referring to the call.
|
||||||
|
msg_id: u32,
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
|
chat_id: u32,
|
||||||
|
/// User-defined info passed to dc_accept_incoming_call(
|
||||||
|
accept_call_info: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Call ended.
|
||||||
|
CallEnded {
|
||||||
|
/// ID of the info message referring to the call.
|
||||||
|
msg_id: u32,
|
||||||
|
/// ID of the chat which the message belongs to.
|
||||||
|
chat_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// One or more transports has changed.
|
||||||
|
///
|
||||||
|
/// UI should update the list.
|
||||||
|
///
|
||||||
|
/// This event is emitted when transport
|
||||||
|
/// synchronization messages arrives,
|
||||||
|
/// but not when the UI modifies the transport list by itself.
|
||||||
|
TransportsModified,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CoreEventType> for EventType {
|
impl From<CoreEventType> for EventType {
|
||||||
@@ -523,9 +577,13 @@ impl From<CoreEventType> for EventType {
|
|||||||
},
|
},
|
||||||
CoreEventType::SecurejoinInviterProgress {
|
CoreEventType::SecurejoinInviterProgress {
|
||||||
contact_id,
|
contact_id,
|
||||||
|
chat_type,
|
||||||
|
chat_id,
|
||||||
progress,
|
progress,
|
||||||
} => SecurejoinInviterProgress {
|
} => SecurejoinInviterProgress {
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
|
chat_type: chat_type.into(),
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
progress,
|
progress,
|
||||||
},
|
},
|
||||||
CoreEventType::SecurejoinJoinerProgress {
|
CoreEventType::SecurejoinJoinerProgress {
|
||||||
@@ -567,6 +625,41 @@ impl From<CoreEventType> for EventType {
|
|||||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||||
CoreEventType::AccountsChanged => AccountsChanged,
|
CoreEventType::AccountsChanged => AccountsChanged,
|
||||||
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
||||||
|
CoreEventType::IncomingCall {
|
||||||
|
msg_id,
|
||||||
|
chat_id,
|
||||||
|
place_call_info,
|
||||||
|
has_video,
|
||||||
|
} => IncomingCall {
|
||||||
|
msg_id: msg_id.to_u32(),
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
|
place_call_info,
|
||||||
|
has_video,
|
||||||
|
},
|
||||||
|
CoreEventType::IncomingCallAccepted {
|
||||||
|
msg_id,
|
||||||
|
chat_id,
|
||||||
|
from_this_device,
|
||||||
|
} => IncomingCallAccepted {
|
||||||
|
msg_id: msg_id.to_u32(),
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
|
from_this_device,
|
||||||
|
},
|
||||||
|
CoreEventType::OutgoingCallAccepted {
|
||||||
|
msg_id,
|
||||||
|
chat_id,
|
||||||
|
accept_call_info,
|
||||||
|
} => OutgoingCallAccepted {
|
||||||
|
msg_id: msg_id.to_u32(),
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
|
accept_call_info,
|
||||||
|
},
|
||||||
|
CoreEventType::CallEnded { msg_id, chat_id } => CallEnded {
|
||||||
|
msg_id: msg_id.to_u32(),
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
|
},
|
||||||
|
CoreEventType::TransportsModified => TransportsModified,
|
||||||
|
|
||||||
#[allow(unreachable_patterns)]
|
#[allow(unreachable_patterns)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ use serde::Deserialize;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use yerpc::TypeDef;
|
use yerpc::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TransportListEntry {
|
||||||
|
/// The login data entered by the user.
|
||||||
|
pub param: EnteredLoginParam,
|
||||||
|
/// Whether this transport is set to 'unpublished'.
|
||||||
|
/// See `set_transport_unpublished` / `setTransportUnpublished` for details.
|
||||||
|
pub is_unpublished: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Login parameters entered by the user.
|
/// Login parameters entered by the user.
|
||||||
///
|
///
|
||||||
/// Usually it will be enough to only set `addr` and `password`,
|
/// Usually it will be enough to only set `addr` and `password`,
|
||||||
@@ -23,6 +33,12 @@ pub struct EnteredLoginParam {
|
|||||||
/// Imap server port.
|
/// Imap server port.
|
||||||
pub imap_port: Option<u16>,
|
pub imap_port: Option<u16>,
|
||||||
|
|
||||||
|
/// IMAP server folder.
|
||||||
|
///
|
||||||
|
/// Defaults to "INBOX" if not set.
|
||||||
|
/// Should not be an empty string.
|
||||||
|
pub imap_folder: Option<String>,
|
||||||
|
|
||||||
/// Imap socket security.
|
/// Imap socket security.
|
||||||
pub imap_security: Option<Socket>,
|
pub imap_security: Option<Socket>,
|
||||||
|
|
||||||
@@ -56,6 +72,15 @@ pub struct EnteredLoginParam {
|
|||||||
pub oauth2: Option<bool>,
|
pub oauth2: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<dc::TransportListEntry> for TransportListEntry {
|
||||||
|
fn from(transport: dc::TransportListEntry) -> Self {
|
||||||
|
TransportListEntry {
|
||||||
|
param: transport.param.into(),
|
||||||
|
is_unpublished: transport.is_unpublished,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||||
let imap_security: Socket = param.imap.security.into();
|
let imap_security: Socket = param.imap.security.into();
|
||||||
@@ -66,6 +91,7 @@ impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
|||||||
password: param.imap.password,
|
password: param.imap.password,
|
||||||
imap_server: param.imap.server.into_option(),
|
imap_server: param.imap.server.into_option(),
|
||||||
imap_port: param.imap.port.into_option(),
|
imap_port: param.imap.port.into_option(),
|
||||||
|
imap_folder: param.imap.folder.into_option(),
|
||||||
imap_security: imap_security.into_option(),
|
imap_security: imap_security.into_option(),
|
||||||
imap_user: param.imap.user.into_option(),
|
imap_user: param.imap.user.into_option(),
|
||||||
smtp_server: param.smtp.server.into_option(),
|
smtp_server: param.smtp.server.into_option(),
|
||||||
@@ -85,14 +111,15 @@ impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
|||||||
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
addr: param.addr,
|
addr: param.addr,
|
||||||
imap: dc::EnteredServerLoginParam {
|
imap: dc::EnteredImapLoginParam {
|
||||||
server: param.imap_server.unwrap_or_default(),
|
server: param.imap_server.unwrap_or_default(),
|
||||||
port: param.imap_port.unwrap_or_default(),
|
port: param.imap_port.unwrap_or_default(),
|
||||||
|
folder: param.imap_folder.unwrap_or_default(),
|
||||||
security: param.imap_security.unwrap_or_default().into(),
|
security: param.imap_security.unwrap_or_default().into(),
|
||||||
user: param.imap_user.unwrap_or_default(),
|
user: param.imap_user.unwrap_or_default(),
|
||||||
password: param.password,
|
password: param.password,
|
||||||
},
|
},
|
||||||
smtp: dc::EnteredServerLoginParam {
|
smtp: dc::EnteredSmtpLoginParam {
|
||||||
server: param.smtp_server.unwrap_or_default(),
|
server: param.smtp_server.unwrap_or_default(),
|
||||||
port: param.smtp_port.unwrap_or_default(),
|
port: param.smtp_port.unwrap_or_default(),
|
||||||
security: param.smtp_security.unwrap_or_default().into(),
|
security: param.smtp_security.unwrap_or_default().into(),
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ use num_traits::cast::ToPrimitive;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
use super::chat::JsonrpcChatType;
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::contact::ContactObject;
|
use super::contact::ContactObject;
|
||||||
use super::reactions::JSONRPCReactions;
|
use super::reactions::JsonrpcReactions;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
@@ -67,7 +68,6 @@ pub struct MessageObject {
|
|||||||
/// if `show_padlock` is `false`,
|
/// if `show_padlock` is `false`,
|
||||||
/// and nothing if it is `true`.
|
/// and nothing if it is `true`.
|
||||||
show_padlock: bool,
|
show_padlock: bool,
|
||||||
is_setupmessage: bool,
|
|
||||||
is_info: bool,
|
is_info: bool,
|
||||||
is_forwarded: bool,
|
is_forwarded: bool,
|
||||||
|
|
||||||
@@ -84,16 +84,14 @@ pub struct MessageObject {
|
|||||||
dimensions_height: i32,
|
dimensions_height: i32,
|
||||||
dimensions_width: i32,
|
dimensions_width: i32,
|
||||||
|
|
||||||
videochat_type: Option<u32>,
|
|
||||||
videochat_url: Option<String>,
|
|
||||||
|
|
||||||
override_sender_name: Option<String>,
|
override_sender_name: Option<String>,
|
||||||
sender: ContactObject,
|
sender: ContactObject,
|
||||||
|
|
||||||
setup_code_begin: Option<String>,
|
|
||||||
|
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
file_mime: Option<String>,
|
file_mime: Option<String>,
|
||||||
|
|
||||||
|
/// The size of the file in bytes, if applicable.
|
||||||
|
/// If message is a pre-message, then this is the size of the file to be downloaded.
|
||||||
file_bytes: u64,
|
file_bytes: u64,
|
||||||
file_name: Option<String>,
|
file_name: Option<String>,
|
||||||
|
|
||||||
@@ -105,7 +103,7 @@ pub struct MessageObject {
|
|||||||
|
|
||||||
saved_message_id: Option<u32>,
|
saved_message_id: Option<u32>,
|
||||||
|
|
||||||
reactions: Option<JSONRPCReactions>,
|
reactions: Option<JsonrpcReactions>,
|
||||||
|
|
||||||
vcard_contact: Option<VcardContact>,
|
vcard_contact: Option<VcardContact>,
|
||||||
}
|
}
|
||||||
@@ -225,7 +223,6 @@ impl MessageObject {
|
|||||||
|
|
||||||
subject: message.get_subject().to_owned(),
|
subject: message.get_subject().to_owned(),
|
||||||
show_padlock: message.get_showpadlock(),
|
show_padlock: message.get_showpadlock(),
|
||||||
is_setupmessage: message.is_setupmessage(),
|
|
||||||
is_info: message.is_info(),
|
is_info: message.is_info(),
|
||||||
is_forwarded: message.is_forwarded(),
|
is_forwarded: message.is_forwarded(),
|
||||||
is_bot: message.is_bot(),
|
is_bot: message.is_bot(),
|
||||||
@@ -239,20 +236,9 @@ impl MessageObject {
|
|||||||
dimensions_height: message.get_height(),
|
dimensions_height: message.get_height(),
|
||||||
dimensions_width: message.get_width(),
|
dimensions_width: message.get_width(),
|
||||||
|
|
||||||
videochat_type: match message.get_videochat_type() {
|
|
||||||
Some(vct) => Some(
|
|
||||||
vct.to_u32()
|
|
||||||
.context("videochat type conversion to number failed")?,
|
|
||||||
),
|
|
||||||
None => None,
|
|
||||||
},
|
|
||||||
videochat_url: message.get_videochat_url(),
|
|
||||||
|
|
||||||
override_sender_name,
|
override_sender_name,
|
||||||
sender,
|
sender,
|
||||||
|
|
||||||
setup_code_begin: message.get_setupcodebegin(context).await,
|
|
||||||
|
|
||||||
file: match message.get_file(context) {
|
file: match message.get_file(context) {
|
||||||
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,
|
||||||
@@ -301,8 +287,6 @@ pub enum MessageViewtype {
|
|||||||
Gif,
|
Gif,
|
||||||
|
|
||||||
/// Message containing a sticker, similar to image.
|
/// Message containing a sticker, similar to image.
|
||||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
|
||||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
|
||||||
///
|
///
|
||||||
/// If possible, the ui should display the image without borders in a transparent way.
|
/// If possible, the ui should display the image without borders in a transparent way.
|
||||||
/// A click on a sticker will offer to install the sticker set in some future.
|
/// A click on a sticker will offer to install the sticker set in some future.
|
||||||
@@ -321,8 +305,8 @@ pub enum MessageViewtype {
|
|||||||
/// Message containing any file, eg. a PDF.
|
/// Message containing any file, eg. a PDF.
|
||||||
File,
|
File,
|
||||||
|
|
||||||
/// Message is an invitation to a videochat.
|
/// Message is a call.
|
||||||
VideochatInvitation,
|
Call,
|
||||||
|
|
||||||
/// Message is an webxdc instance.
|
/// Message is an webxdc instance.
|
||||||
Webxdc,
|
Webxdc,
|
||||||
@@ -345,7 +329,7 @@ impl From<Viewtype> for MessageViewtype {
|
|||||||
Viewtype::Voice => MessageViewtype::Voice,
|
Viewtype::Voice => MessageViewtype::Voice,
|
||||||
Viewtype::Video => MessageViewtype::Video,
|
Viewtype::Video => MessageViewtype::Video,
|
||||||
Viewtype::File => MessageViewtype::File,
|
Viewtype::File => MessageViewtype::File,
|
||||||
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
Viewtype::Call => MessageViewtype::Call,
|
||||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
Viewtype::Vcard => MessageViewtype::Vcard,
|
||||||
}
|
}
|
||||||
@@ -364,7 +348,7 @@ impl From<MessageViewtype> for Viewtype {
|
|||||||
MessageViewtype::Voice => Viewtype::Voice,
|
MessageViewtype::Voice => Viewtype::Voice,
|
||||||
MessageViewtype::Video => Viewtype::Video,
|
MessageViewtype::Video => Viewtype::Video,
|
||||||
MessageViewtype::File => Viewtype::File,
|
MessageViewtype::File => Viewtype::File,
|
||||||
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
MessageViewtype::Call => Viewtype::Call,
|
||||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
MessageViewtype::Vcard => Viewtype::Vcard,
|
||||||
}
|
}
|
||||||
@@ -396,6 +380,7 @@ impl From<download::DownloadState> for DownloadState {
|
|||||||
pub enum SystemMessageType {
|
pub enum SystemMessageType {
|
||||||
Unknown,
|
Unknown,
|
||||||
GroupNameChanged,
|
GroupNameChanged,
|
||||||
|
GroupDescriptionChanged,
|
||||||
GroupImageChanged,
|
GroupImageChanged,
|
||||||
MemberAddedToGroup,
|
MemberAddedToGroup,
|
||||||
MemberRemovedFromGroup,
|
MemberRemovedFromGroup,
|
||||||
@@ -416,6 +401,9 @@ pub enum SystemMessageType {
|
|||||||
/// Chat ephemeral message timer is changed.
|
/// Chat ephemeral message timer is changed.
|
||||||
EphemeralTimerChanged,
|
EphemeralTimerChanged,
|
||||||
|
|
||||||
|
// Chat is e2ee
|
||||||
|
ChatE2ee,
|
||||||
|
|
||||||
// Chat protection state changed
|
// Chat protection state changed
|
||||||
ChatProtectionEnabled,
|
ChatProtectionEnabled,
|
||||||
ChatProtectionDisabled,
|
ChatProtectionDisabled,
|
||||||
@@ -434,6 +422,9 @@ pub enum SystemMessageType {
|
|||||||
|
|
||||||
/// This message contains a users iroh node address.
|
/// This message contains a users iroh node address.
|
||||||
IrohNodeAddr,
|
IrohNodeAddr,
|
||||||
|
|
||||||
|
CallAccepted,
|
||||||
|
CallEnded,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||||
@@ -442,6 +433,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
match system_message_type {
|
match system_message_type {
|
||||||
SystemMessage::Unknown => SystemMessageType::Unknown,
|
SystemMessage::Unknown => SystemMessageType::Unknown,
|
||||||
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
||||||
|
SystemMessage::GroupDescriptionChanged => SystemMessageType::GroupDescriptionChanged,
|
||||||
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
||||||
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
||||||
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
||||||
@@ -450,6 +442,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||||
|
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
|
||||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||||
@@ -459,6 +452,8 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
||||||
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
||||||
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
||||||
|
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
|
||||||
|
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,8 +529,7 @@ pub struct MessageSearchResult {
|
|||||||
chat_profile_image: Option<String>,
|
chat_profile_image: Option<String>,
|
||||||
chat_color: String,
|
chat_color: String,
|
||||||
chat_name: String,
|
chat_name: String,
|
||||||
chat_type: u32,
|
chat_type: JsonrpcChatType,
|
||||||
is_chat_protected: bool,
|
|
||||||
is_chat_contact_request: bool,
|
is_chat_contact_request: bool,
|
||||||
is_chat_archived: bool,
|
is_chat_archived: bool,
|
||||||
message: String,
|
message: String,
|
||||||
@@ -573,9 +567,8 @@ impl MessageSearchResult {
|
|||||||
chat_id: chat.id.to_u32(),
|
chat_id: chat.id.to_u32(),
|
||||||
chat_name: chat.get_name().to_owned(),
|
chat_name: chat.get_name().to_owned(),
|
||||||
chat_color,
|
chat_color,
|
||||||
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
chat_type: chat.get_type().into(),
|
||||||
chat_profile_image,
|
chat_profile_image,
|
||||||
is_chat_protected: chat.is_protected(),
|
|
||||||
is_chat_contact_request: chat.is_contact_request(),
|
is_chat_contact_request: chat.is_contact_request(),
|
||||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||||
message: message.get_text(),
|
message: message.get_text(),
|
||||||
@@ -586,7 +579,7 @@ impl MessageSearchResult {
|
|||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[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 {
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
},
|
},
|
||||||
@@ -599,13 +592,13 @@ pub enum JSONRPCMessageListItem {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ChatItem> for JSONRPCMessageListItem {
|
impl From<ChatItem> for JsonrpcMessageListItem {
|
||||||
fn from(item: ChatItem) -> Self {
|
fn from(item: ChatItem) -> Self {
|
||||||
match item {
|
match item {
|
||||||
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
|
ChatItem::Message { msg_id } => JsonrpcMessageListItem::Message {
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
ChatItem::DayMarker { timestamp } => JSONRPCMessageListItem::DayMarker { timestamp },
|
ChatItem::DayMarker { timestamp } => JsonrpcMessageListItem::DayMarker { timestamp },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod account;
|
pub mod account;
|
||||||
|
pub mod calls;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod chat_list;
|
pub mod chat_list;
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
@@ -7,6 +8,7 @@ pub mod http;
|
|||||||
pub mod location;
|
pub mod location;
|
||||||
pub mod login_param;
|
pub mod login_param;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
|
pub mod notify_state;
|
||||||
pub mod provider_info;
|
pub mod provider_info;
|
||||||
pub mod qr;
|
pub mod qr;
|
||||||
pub mod reactions;
|
pub mod reactions;
|
||||||
|
|||||||
26
deltachat-jsonrpc/src/api/types/notify_state.rs
Normal file
26
deltachat-jsonrpc/src/api/types/notify_state.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use deltachat::push::NotifyState;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename = "NotifyState")]
|
||||||
|
pub enum JsonrpcNotifyState {
|
||||||
|
/// Not subscribed to push notifications.
|
||||||
|
NotConnected,
|
||||||
|
|
||||||
|
/// Subscribed to heartbeat push notifications.
|
||||||
|
Heartbeat,
|
||||||
|
|
||||||
|
/// Subscribed to push notifications for new messages.
|
||||||
|
Connected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NotifyState> for JsonrpcNotifyState {
|
||||||
|
fn from(state: NotifyState) -> Self {
|
||||||
|
match state {
|
||||||
|
NotifyState::NotConnected => Self::NotConnected,
|
||||||
|
NotifyState::Heartbeat => Self::Heartbeat,
|
||||||
|
NotifyState::Connected => Self::Connected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use deltachat::qr::Qr;
|
use deltachat::qr::Qr;
|
||||||
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ pub enum QrObject {
|
|||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
/// Authentication code.
|
||||||
authcode: String,
|
authcode: String,
|
||||||
|
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||||
|
is_v3: bool,
|
||||||
},
|
},
|
||||||
/// Ask the user whether to join the group.
|
/// Ask the user whether to join the group.
|
||||||
AskVerifyGroup {
|
AskVerifyGroup {
|
||||||
@@ -33,6 +36,30 @@ pub enum QrObject {
|
|||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
/// Authentication code.
|
||||||
authcode: String,
|
authcode: String,
|
||||||
|
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||||
|
is_v3: bool,
|
||||||
|
},
|
||||||
|
/// Ask the user whether to join the broadcast channel.
|
||||||
|
AskJoinBroadcast {
|
||||||
|
/// The user-visible name of this broadcast channel
|
||||||
|
name: String,
|
||||||
|
/// A string of random characters,
|
||||||
|
/// uniquely identifying this broadcast channel across all databases/clients.
|
||||||
|
/// Called `grpid` for historic reasons:
|
||||||
|
/// The id of multi-user chats is always called `grpid` in the database
|
||||||
|
/// because groups were once the only multi-user chats.
|
||||||
|
grpid: String,
|
||||||
|
/// ID of the contact who owns the broadcast channel and created the QR code.
|
||||||
|
contact_id: u32,
|
||||||
|
/// Fingerprint of the broadcast channel owner's key as scanned from the QR code.
|
||||||
|
fingerprint: String,
|
||||||
|
|
||||||
|
/// Invite number.
|
||||||
|
invitenumber: String,
|
||||||
|
/// Authentication code.
|
||||||
|
authcode: String,
|
||||||
|
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||||
|
is_v3: bool,
|
||||||
},
|
},
|
||||||
/// Contact fingerprint is verified.
|
/// Contact fingerprint is verified.
|
||||||
///
|
///
|
||||||
@@ -136,6 +163,21 @@ pub enum QrObject {
|
|||||||
/// Authentication code.
|
/// Authentication code.
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
|
/// Ask the user if they want to withdraw their own broadcast channel invite QR code.
|
||||||
|
WithdrawJoinBroadcast {
|
||||||
|
/// Broadcast name.
|
||||||
|
name: String,
|
||||||
|
/// ID, uniquely identifying this chat. Called grpid for historic reasons.
|
||||||
|
grpid: String,
|
||||||
|
/// Contact ID. Always `ContactId::SELF`.
|
||||||
|
contact_id: u32,
|
||||||
|
/// Fingerprint of the contact key as scanned from the QR code.
|
||||||
|
fingerprint: String,
|
||||||
|
/// Invite number.
|
||||||
|
invitenumber: String,
|
||||||
|
/// Authentication code.
|
||||||
|
authcode: String,
|
||||||
|
},
|
||||||
/// Ask the user if they want to revive their own QR code.
|
/// Ask the user if they want to revive their own QR code.
|
||||||
ReviveVerifyContact {
|
ReviveVerifyContact {
|
||||||
/// Contact ID.
|
/// Contact ID.
|
||||||
@@ -162,6 +204,21 @@ pub enum QrObject {
|
|||||||
/// Authentication code.
|
/// Authentication code.
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
|
/// Ask the user if they want to revive their own broadcast channel invite QR code.
|
||||||
|
ReviveJoinBroadcast {
|
||||||
|
/// Broadcast name.
|
||||||
|
name: String,
|
||||||
|
/// Globally unique chat ID. Called grpid for historic reasons.
|
||||||
|
grpid: String,
|
||||||
|
/// Contact ID. Always `ContactId::SELF`.
|
||||||
|
contact_id: u32,
|
||||||
|
/// Fingerprint of the contact key as scanned from the QR code.
|
||||||
|
fingerprint: String,
|
||||||
|
/// Invite number.
|
||||||
|
invitenumber: String,
|
||||||
|
/// Authentication code.
|
||||||
|
authcode: String,
|
||||||
|
},
|
||||||
/// `dclogin:` scheme parameters.
|
/// `dclogin:` scheme parameters.
|
||||||
///
|
///
|
||||||
/// Ask the user if they want to login with the email address.
|
/// Ask the user if they want to login with the email address.
|
||||||
@@ -178,14 +235,16 @@ impl From<Qr> for QrObject {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
|
is_v3,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.to_string();
|
let fingerprint = fingerprint.human_readable();
|
||||||
QrObject::AskVerifyContact {
|
QrObject::AskVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
|
is_v3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::AskVerifyGroup {
|
Qr::AskVerifyGroup {
|
||||||
@@ -195,9 +254,10 @@ impl From<Qr> for QrObject {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
|
is_v3,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.to_string();
|
let fingerprint = fingerprint.human_readable();
|
||||||
QrObject::AskVerifyGroup {
|
QrObject::AskVerifyGroup {
|
||||||
grpname,
|
grpname,
|
||||||
grpid,
|
grpid,
|
||||||
@@ -205,6 +265,28 @@ impl From<Qr> for QrObject {
|
|||||||
fingerprint,
|
fingerprint,
|
||||||
invitenumber,
|
invitenumber,
|
||||||
authcode,
|
authcode,
|
||||||
|
is_v3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qr::AskJoinBroadcast {
|
||||||
|
name,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
authcode,
|
||||||
|
invitenumber,
|
||||||
|
is_v3,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.human_readable();
|
||||||
|
QrObject::AskJoinBroadcast {
|
||||||
|
name,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
authcode,
|
||||||
|
invitenumber,
|
||||||
|
is_v3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::FprOk { contact_id } => {
|
Qr::FprOk { contact_id } => {
|
||||||
@@ -225,13 +307,6 @@ impl From<Qr> for QrObject {
|
|||||||
auth_token,
|
auth_token,
|
||||||
},
|
},
|
||||||
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
||||||
Qr::WebrtcInstance {
|
|
||||||
domain,
|
|
||||||
instance_pattern,
|
|
||||||
} => QrObject::WebrtcInstance {
|
|
||||||
domain,
|
|
||||||
instance_pattern,
|
|
||||||
},
|
|
||||||
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
||||||
Qr::Addr { contact_id, draft } => {
|
Qr::Addr { contact_id, draft } => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
@@ -246,7 +321,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.to_string();
|
let fingerprint = fingerprint.human_readable();
|
||||||
QrObject::WithdrawVerifyContact {
|
QrObject::WithdrawVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
@@ -263,7 +338,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.to_string();
|
let fingerprint = fingerprint.human_readable();
|
||||||
QrObject::WithdrawVerifyGroup {
|
QrObject::WithdrawVerifyGroup {
|
||||||
grpname,
|
grpname,
|
||||||
grpid,
|
grpid,
|
||||||
@@ -273,6 +348,25 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Qr::WithdrawJoinBroadcast {
|
||||||
|
name,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.human_readable();
|
||||||
|
QrObject::WithdrawJoinBroadcast {
|
||||||
|
name,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
Qr::ReviveVerifyContact {
|
Qr::ReviveVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
@@ -280,7 +374,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.to_string();
|
let fingerprint = fingerprint.human_readable();
|
||||||
QrObject::ReviveVerifyContact {
|
QrObject::ReviveVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
@@ -297,7 +391,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
} => {
|
} => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
let fingerprint = fingerprint.to_string();
|
let fingerprint = fingerprint.human_readable();
|
||||||
QrObject::ReviveVerifyGroup {
|
QrObject::ReviveVerifyGroup {
|
||||||
grpname,
|
grpname,
|
||||||
grpid,
|
grpid,
|
||||||
@@ -307,7 +401,76 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Qr::ReviveJoinBroadcast {
|
||||||
|
name,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
} => {
|
||||||
|
let contact_id = contact_id.to_u32();
|
||||||
|
let fingerprint = fingerprint.human_readable();
|
||||||
|
QrObject::ReviveJoinBroadcast {
|
||||||
|
name,
|
||||||
|
grpid,
|
||||||
|
contact_id,
|
||||||
|
fingerprint,
|
||||||
|
invitenumber,
|
||||||
|
authcode,
|
||||||
|
}
|
||||||
|
}
|
||||||
Qr::Login { address, .. } => QrObject::Login { address },
|
Qr::Login { address, .. } => QrObject::Login { address },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
pub enum SecurejoinSource {
|
||||||
|
/// Because of some problem, it is unknown where the QR code came from.
|
||||||
|
Unknown,
|
||||||
|
/// The user opened a link somewhere outside Delta Chat
|
||||||
|
ExternalLink,
|
||||||
|
/// The user clicked on a link in a message inside Delta Chat
|
||||||
|
InternalLink,
|
||||||
|
/// The user clicked "Paste from Clipboard" in the QR scan activity
|
||||||
|
Clipboard,
|
||||||
|
/// The user clicked "Load QR code as image" in the QR scan activity
|
||||||
|
ImageLoaded,
|
||||||
|
/// The user scanned a QR code
|
||||||
|
Scan,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
pub enum SecurejoinUiPath {
|
||||||
|
/// The UI path is unknown, or the user didn't open the QR code screen at all.
|
||||||
|
Unknown,
|
||||||
|
/// The user directly clicked on the QR icon in the main screen
|
||||||
|
QrIcon,
|
||||||
|
/// The user first clicked on the `+` button in the main screen,
|
||||||
|
/// and then on "New Contact"
|
||||||
|
NewContact,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SecurejoinSource> for deltachat::SecurejoinSource {
|
||||||
|
fn from(value: SecurejoinSource) -> Self {
|
||||||
|
match value {
|
||||||
|
SecurejoinSource::Unknown => deltachat::SecurejoinSource::Unknown,
|
||||||
|
SecurejoinSource::ExternalLink => deltachat::SecurejoinSource::ExternalLink,
|
||||||
|
SecurejoinSource::InternalLink => deltachat::SecurejoinSource::InternalLink,
|
||||||
|
SecurejoinSource::Clipboard => deltachat::SecurejoinSource::Clipboard,
|
||||||
|
SecurejoinSource::ImageLoaded => deltachat::SecurejoinSource::ImageLoaded,
|
||||||
|
SecurejoinSource::Scan => deltachat::SecurejoinSource::Scan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SecurejoinUiPath> for deltachat::SecurejoinUiPath {
|
||||||
|
fn from(value: SecurejoinUiPath) -> Self {
|
||||||
|
match value {
|
||||||
|
SecurejoinUiPath::Unknown => deltachat::SecurejoinUiPath::Unknown,
|
||||||
|
SecurejoinUiPath::QrIcon => deltachat::SecurejoinUiPath::QrIcon,
|
||||||
|
SecurejoinUiPath::NewContact => deltachat::SecurejoinUiPath::NewContact,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
|
|||||||
/// A single reaction emoji.
|
/// A single reaction emoji.
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||||
pub struct JSONRPCReaction {
|
pub struct JsonrpcReaction {
|
||||||
/// Emoji.
|
/// Emoji.
|
||||||
emoji: String,
|
emoji: String,
|
||||||
|
|
||||||
@@ -22,41 +22,32 @@ pub struct JSONRPCReaction {
|
|||||||
/// Structure representing all reactions to a particular message.
|
/// Structure representing all reactions to a particular message.
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[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.
|
||||||
|
/// There is only a single reaction per contact,
|
||||||
|
/// but this contains a list of reactions for historical reasons.
|
||||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||||
/// Unique reactions and their count, sorted in descending order.
|
/// Unique reactions and their count, sorted in descending order.
|
||||||
reactions: Vec<JSONRPCReaction>,
|
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 reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
|
||||||
|
.iter()
|
||||||
for contact_id in reactions.contacts() {
|
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
|
||||||
let reaction = reactions.get(contact_id);
|
.collect();
|
||||||
if reaction.is_empty() {
|
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let emojis: Vec<String> = reaction
|
|
||||||
.emojis()
|
|
||||||
.into_iter()
|
|
||||||
.map(|emoji| emoji.to_owned())
|
|
||||||
.collect();
|
|
||||||
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
|
||||||
|
|
||||||
let mut reactions_v = Vec::new();
|
let mut reactions_v = Vec::new();
|
||||||
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
||||||
let is_from_self = if let Some(self_reactions) = self_reactions {
|
let is_from_self = if let Some(self_reaction) = self_reaction {
|
||||||
self_reactions.contains(&emoji)
|
self_reaction.contains(&emoji)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let reaction = JSONRPCReaction {
|
let reaction = JsonrpcReaction {
|
||||||
emoji,
|
emoji,
|
||||||
count,
|
count,
|
||||||
is_from_self,
|
is_from_self,
|
||||||
@@ -64,7 +55,7 @@ impl From<Reactions> for JSONRPCReactions {
|
|||||||
reactions_v.push(reaction)
|
reactions_v.push(reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONRPCReactions {
|
JsonrpcReactions {
|
||||||
reactions_by_contact,
|
reactions_by_contact,
|
||||||
reactions: reactions_v,
|
reactions: reactions_v,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ pub struct WebxdcMessageInfo {
|
|||||||
internet_access: bool,
|
internet_access: bool,
|
||||||
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
||||||
self_addr: String,
|
self_addr: String,
|
||||||
|
/// Define if the local user is the one who initially shared the webxdc application in the chat.
|
||||||
|
is_app_sender: bool,
|
||||||
|
/// Define if the app runs in a broadcasting context.
|
||||||
|
is_broadcast: bool,
|
||||||
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
||||||
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
||||||
send_update_interval: usize,
|
send_update_interval: usize,
|
||||||
@@ -60,6 +64,8 @@ impl WebxdcMessageInfo {
|
|||||||
request_integration: _,
|
request_integration: _,
|
||||||
internet_access,
|
internet_access,
|
||||||
self_addr,
|
self_addr,
|
||||||
|
is_app_sender,
|
||||||
|
is_broadcast,
|
||||||
send_update_interval,
|
send_update_interval,
|
||||||
send_update_max_size,
|
send_update_max_size,
|
||||||
} = message.get_webxdc_info(context).await?;
|
} = message.get_webxdc_info(context).await?;
|
||||||
@@ -72,6 +78,8 @@ impl WebxdcMessageInfo {
|
|||||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||||
internet_access,
|
internet_access,
|
||||||
self_addr,
|
self_addr,
|
||||||
|
is_app_sender,
|
||||||
|
is_broadcast,
|
||||||
send_update_interval,
|
send_update_interval,
|
||||||
send_update_max_size,
|
send_update_max_size,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ mod tests {
|
|||||||
assert_eq!(result, 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":""}]}"#;
|
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":""}]}"#;
|
||||||
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.recv().await?;
|
let result = receiver.recv().await?;
|
||||||
|
|||||||
@@ -54,5 +54,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "2.2.0"
|
"version": "2.50.0-dev"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,15 +40,35 @@ const constants = data
|
|||||||
key.startsWith("DC_DOWNLOAD") ||
|
key.startsWith("DC_DOWNLOAD") ||
|
||||||
key.startsWith("DC_INFO_") ||
|
key.startsWith("DC_INFO_") ||
|
||||||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
||||||
key.startsWith("DC_QR_")
|
key.startsWith("DC_QR_") ||
|
||||||
|
key.startsWith("DC_CERTCK_") ||
|
||||||
|
key.startsWith("DC_SOCKET_") ||
|
||||||
|
key.startsWith("DC_LP_AUTH_") ||
|
||||||
|
key.startsWith("DC_PUSH_") ||
|
||||||
|
key.startsWith("DC_TEXT1_") ||
|
||||||
|
key.startsWith("DC_CHAT_TYPE")
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
return ` ${row.key}: ${row.value}`;
|
return ` export const ${row.key} = ${row.value};`;
|
||||||
})
|
})
|
||||||
.join(",\n");
|
.join("\n");
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
resolve(__dirname, "../generated/constants.ts"),
|
resolve(__dirname, "../generated/constants.ts"),
|
||||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
|
`// Generated!
|
||||||
|
|
||||||
|
export namespace C {
|
||||||
|
${constants}
|
||||||
|
/** @deprecated 10-8-2025 compare string directly with \`== "Group"\` */
|
||||||
|
export const DC_CHAT_TYPE_GROUP = "Group";
|
||||||
|
/** @deprecated 10-8-2025 compare string directly with \`== "InBroadcast"\`*/
|
||||||
|
export const DC_CHAT_TYPE_IN_BROADCAST = "InBroadcast";
|
||||||
|
/** @deprecated 10-8-2025 compare string directly with \`== "Mailinglist"\` */
|
||||||
|
export const DC_CHAT_TYPE_MAILINGLIST = "Mailinglist";
|
||||||
|
/** @deprecated 10-8-2025 compare string directly with \`== "OutBroadcast"\` */
|
||||||
|
export const DC_CHAT_TYPE_OUT_BROADCAST = "OutBroadcast";
|
||||||
|
/** @deprecated 10-8-2025 compare string directly with \`== "Single"\` */
|
||||||
|
export const DC_CHAT_TYPE_SINGLE = "Single";
|
||||||
|
}\n`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export class BaseDeltaChat<
|
|||||||
Transport extends BaseTransport<any>,
|
Transport extends BaseTransport<any>,
|
||||||
> extends TinyEmitter<Events> {
|
> extends TinyEmitter<Events> {
|
||||||
rpc: RawClient;
|
rpc: RawClient;
|
||||||
account?: T.Account;
|
|
||||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -36,6 +35,10 @@ export class BaseDeltaChat<
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public transport: Transport,
|
public transport: Transport,
|
||||||
|
/**
|
||||||
|
* Whether to start calling {@linkcode RawClient.getNextEvent}
|
||||||
|
* and emitting the respective events on this class.
|
||||||
|
*/
|
||||||
startEventLoop: boolean,
|
startEventLoop: boolean,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@@ -45,28 +48,39 @@ export class BaseDeltaChat<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see the constructor's `startEventLoop`
|
||||||
|
*/
|
||||||
async eventLoop(): Promise<void> {
|
async eventLoop(): Promise<void> {
|
||||||
while (true) {
|
while (true) {
|
||||||
const event = await this.rpc.getNextEvent();
|
for (const event of await this.rpc.getNextEventBatch()) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.emit(event.event.kind, event.contextId, event.event);
|
this.emit(event.event.kind, event.contextId, event.event);
|
||||||
this.emit("ALL", event.contextId, event.event);
|
this.emit("ALL", event.contextId, event.event);
|
||||||
|
|
||||||
if (this.contextEmitters[event.contextId]) {
|
if (this.contextEmitters[event.contextId]) {
|
||||||
this.contextEmitters[event.contextId].emit(
|
this.contextEmitters[event.contextId].emit(
|
||||||
event.event.kind,
|
event.event.kind,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
event.event as any,
|
event.event as any,
|
||||||
);
|
);
|
||||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use {@linkcode BaseDeltaChat.rpc.getAllAccounts} instead.
|
||||||
|
*/
|
||||||
async listAccounts(): Promise<T.Account[]> {
|
async listAccounts(): Promise<T.Account[]> {
|
||||||
return await this.rpc.getAllAccounts();
|
return await this.rpc.getAllAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience function to listen on events binned by `account_id`
|
||||||
|
* (see {@linkcode RawClient.getAllAccounts}).
|
||||||
|
*/
|
||||||
getContextEvents(account_id: number) {
|
getContextEvents(account_id: number) {
|
||||||
if (this.contextEmitters[account_id]) {
|
if (this.contextEmitters[account_id]) {
|
||||||
return this.contextEmitters[account_id];
|
return this.contextEmitters[account_id];
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ describe("online tests", function () {
|
|||||||
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
||||||
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
||||||
await dc.rpc.configure(accountId1);
|
await dc.rpc.configure(accountId1);
|
||||||
|
await waitForEvent(dc, "ImapInboxIdle", accountId1);
|
||||||
|
|
||||||
accountId2 = await dc.rpc.addAccount();
|
accountId2 = await dc.rpc.addAccount();
|
||||||
await dc.rpc.batchSetConfig(accountId2, {
|
await dc.rpc.batchSetConfig(accountId2, {
|
||||||
@@ -71,6 +72,7 @@ describe("online tests", function () {
|
|||||||
mail_pw: account2.password,
|
mail_pw: account2.password,
|
||||||
});
|
});
|
||||||
await dc.rpc.configure(accountId2);
|
await dc.rpc.configure(accountId2);
|
||||||
|
await waitForEvent(dc, "ImapInboxIdle", accountId2);
|
||||||
accountsConfigured = true;
|
accountsConfigured = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,8 +97,10 @@ describe("online tests", function () {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(messageList).have.length(1);
|
// There are 2 messages in the chat:
|
||||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
|
||||||
|
expect(messageList).have.length(2);
|
||||||
|
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
|
||||||
expect(message.text).equal("Hello");
|
expect(message.text).equal("Hello");
|
||||||
expect(message.showPadlock).equal(true);
|
expect(message.showPadlock).equal(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "2.2.0"
|
version = "2.50.0-dev"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/chatmail/core"
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use deltachat::chat::{
|
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
||||||
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
|
||||||
};
|
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
@@ -72,11 +70,6 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
context.sql().config_cache().write().await.clear();
|
context.sql().config_cache().write().await.clear();
|
||||||
context
|
|
||||||
.sql()
|
|
||||||
.execute("DELETE FROM leftgrps;", ())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
println!("(8) Rest but server config reset.");
|
println!("(8) Rest but server config reset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +80,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
|||||||
let data = read_file(context, filename).await?;
|
let data = read_file(context, filename).await?;
|
||||||
|
|
||||||
if let Err(err) = receive_imf(context, &data, false).await {
|
if let Err(err) = receive_imf(context, &data, false).await {
|
||||||
println!("receive_imf errored: {err:?}");
|
eprintln!("receive_imf errored: {err:?}");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -210,13 +203,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
if msg.get_viewtype() == Viewtype::Webxdc {
|
||||||
format!(
|
|
||||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
|
||||||
msg.get_videochat_url().unwrap_or_default(),
|
|
||||||
msg.get_videochat_type().unwrap_or_default()
|
|
||||||
)
|
|
||||||
} else if msg.get_viewtype() == Viewtype::Webxdc {
|
|
||||||
match msg.get_webxdc_info(context).await {
|
match msg.get_webxdc_info(context).await {
|
||||||
Ok(info) => format!(
|
Ok(info) => format!(
|
||||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||||
@@ -315,9 +302,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
// TODO: reuse commands definition in main.rs.
|
// TODO: reuse commands definition in main.rs.
|
||||||
"imex" => println!(
|
"imex" => println!(
|
||||||
"====================Import/Export commands==\n\
|
"====================Import/Export commands==\n\
|
||||||
initiate-key-transfer\n\
|
|
||||||
get-setupcodebegin <msg-id>\n\
|
|
||||||
continue-key-transfer <msg-id> <setup-code>\n\
|
|
||||||
has-backup\n\
|
has-backup\n\
|
||||||
export-backup\n\
|
export-backup\n\
|
||||||
import-backup <backup-file>\n\
|
import-backup <backup-file>\n\
|
||||||
@@ -353,17 +337,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
createchat <contact-id>\n\
|
createchat <contact-id>\n\
|
||||||
creategroup <name>\n\
|
creategroup <name>\n\
|
||||||
createbroadcast <name>\n\
|
createbroadcast <name>\n\
|
||||||
createprotected <name>\n\
|
|
||||||
addmember <contact-id>\n\
|
addmember <contact-id>\n\
|
||||||
removemember <contact-id>\n\
|
removemember <contact-id>\n\
|
||||||
groupname <name>\n\
|
groupname <name>\n\
|
||||||
|
groupdescription <description>\n\
|
||||||
groupimage <image>\n\
|
groupimage <image>\n\
|
||||||
chatinfo\n\
|
chatinfo\n\
|
||||||
sendlocations <seconds>\n\
|
sendlocations <seconds>\n\
|
||||||
setlocation <lat> <lng>\n\
|
setlocation <lat> <lng>\n\
|
||||||
dellocations\n\
|
|
||||||
getlocations [<contact-id>]\n\
|
getlocations [<contact-id>]\n\
|
||||||
send <text>\n\
|
send <text>\n\
|
||||||
|
send-sync <text>\n\
|
||||||
sendempty\n\
|
sendempty\n\
|
||||||
sendimage <file> [<text>]\n\
|
sendimage <file> [<text>]\n\
|
||||||
sendsticker <file> [<text>]\n\
|
sendsticker <file> [<text>]\n\
|
||||||
@@ -371,7 +355,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||||
sendsyncmsg\n\
|
sendsyncmsg\n\
|
||||||
sendupdate <msg-id> <json status update>\n\
|
sendupdate <msg-id> <json status update>\n\
|
||||||
videochat\n\
|
|
||||||
draft [<text>]\n\
|
draft [<text>]\n\
|
||||||
devicemsg <text>\n\
|
devicemsg <text>\n\
|
||||||
listmedia\n\
|
listmedia\n\
|
||||||
@@ -403,6 +386,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
block <contact-id>\n\
|
block <contact-id>\n\
|
||||||
unblock <contact-id>\n\
|
unblock <contact-id>\n\
|
||||||
listblocked\n\
|
listblocked\n\
|
||||||
|
import-vcard <file>\n\
|
||||||
|
make-vcard <file> <contact-id> [contact-id ...]\n\
|
||||||
======================================Misc.==\n\
|
======================================Misc.==\n\
|
||||||
getqr [<chat-id>]\n\
|
getqr [<chat-id>]\n\
|
||||||
getqrsvg [<chat-id>]\n\
|
getqrsvg [<chat-id>]\n\
|
||||||
@@ -419,34 +404,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
============================================="
|
============================================="
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"initiate-key-transfer" => match initiate_key_transfer(&context).await {
|
|
||||||
Ok(setup_code) => {
|
|
||||||
println!("Setup code for the transferred setup message: {setup_code}",)
|
|
||||||
}
|
|
||||||
Err(err) => bail!("Failed to generate setup code: {}", err),
|
|
||||||
},
|
|
||||||
"get-setupcodebegin" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
|
||||||
let msg_id: MsgId = MsgId::new(arg1.parse()?);
|
|
||||||
let msg = Message::load_from_db(&context, msg_id).await?;
|
|
||||||
if msg.is_setupmessage() {
|
|
||||||
let setupcodebegin = msg.get_setupcodebegin(&context).await;
|
|
||||||
println!(
|
|
||||||
"The setup code for setup message {} starts with: {}",
|
|
||||||
msg_id,
|
|
||||||
setupcodebegin.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
bail!("{} is no setup message.", msg_id,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"continue-key-transfer" => {
|
|
||||||
ensure!(
|
|
||||||
!arg1.is_empty() && !arg2.is_empty(),
|
|
||||||
"Arguments <msg-id> <setup-code> expected"
|
|
||||||
);
|
|
||||||
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
|
|
||||||
}
|
|
||||||
"has-backup" => {
|
"has-backup" => {
|
||||||
has_backup(&context, blobdir).await?;
|
has_backup(&context, blobdir).await?;
|
||||||
}
|
}
|
||||||
@@ -532,7 +489,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Report written to: {file:#?}");
|
println!("Report written to: {file:#?}");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("Failed to get connectivity html: {}", err);
|
bail!("Failed to get connectivity html: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,7 +524,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
for i in (0..cnt).rev() {
|
for i in (0..cnt).rev() {
|
||||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||||
println!(
|
println!(
|
||||||
"{}#{}: {} [{} fresh] {}{}{}{}",
|
"{}#{}: {} [{} fresh] {}{}{}",
|
||||||
chat_prefix(&chat),
|
chat_prefix(&chat),
|
||||||
chat.get_id(),
|
chat.get_id(),
|
||||||
chat.get_name(),
|
chat.get_name(),
|
||||||
@@ -578,7 +535,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ChatVisibility::Archived => "📦",
|
ChatVisibility::Archived => "📦",
|
||||||
ChatVisibility::Pinned => "📌",
|
ChatVisibility::Pinned => "📌",
|
||||||
},
|
},
|
||||||
if chat.is_protected() { "🛡️" } else { "" },
|
|
||||||
if chat.is_contact_request() {
|
if chat.is_contact_request() {
|
||||||
"🆕"
|
"🆕"
|
||||||
} else {
|
} else {
|
||||||
@@ -617,11 +573,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if location::is_sending_locations_to_chat(&context, None).await? {
|
if location::is_sending(&context).await? {
|
||||||
println!("Location streaming enabled.");
|
println!("Location streaming enabled.");
|
||||||
}
|
}
|
||||||
println!("{cnt} chats");
|
println!("{cnt} chats");
|
||||||
println!("{time_needed:?} to create this list");
|
eprintln!("{time_needed:?} to create this list");
|
||||||
}
|
}
|
||||||
"start-realtime" => {
|
"start-realtime" => {
|
||||||
if arg1.is_empty() {
|
if arg1.is_empty() {
|
||||||
@@ -666,7 +622,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
&context,
|
&context,
|
||||||
sel_chat.get_id(),
|
sel_chat.get_id(),
|
||||||
chat::MessageListOptions {
|
chat::MessageListOptions {
|
||||||
info_only: false,
|
|
||||||
add_daymarker: true,
|
add_daymarker: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -693,7 +648,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
format!("{} member(s)", members.len())
|
format!("{} member(s)", members.len())
|
||||||
};
|
};
|
||||||
println!(
|
println!(
|
||||||
"{}#{}: {} [{}]{}{}{} {}",
|
"{}#{}: {} [{}]{}{}{}",
|
||||||
chat_prefix(sel_chat),
|
chat_prefix(sel_chat),
|
||||||
sel_chat.get_id(),
|
sel_chat.get_id(),
|
||||||
sel_chat.get_name(),
|
sel_chat.get_name(),
|
||||||
@@ -711,11 +666,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
},
|
},
|
||||||
_ => "".to_string(),
|
_ => "".to_string(),
|
||||||
},
|
},
|
||||||
if sel_chat.is_protected() {
|
|
||||||
"🛡️"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
log_msglist(&context, &msglist).await?;
|
log_msglist(&context, &msglist).await?;
|
||||||
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
||||||
@@ -731,7 +681,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||||
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
||||||
|
|
||||||
println!(
|
eprintln!(
|
||||||
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -744,8 +694,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"creategroup" => {
|
"creategroup" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||||
let chat_id =
|
let chat_id = chat::create_group(&context, arg1).await?;
|
||||||
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
|
|
||||||
|
|
||||||
println!("Group#{chat_id} created successfully.");
|
println!("Group#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
@@ -755,13 +704,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
println!("Broadcast#{chat_id} created successfully.");
|
println!("Broadcast#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
"createprotected" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
|
||||||
let chat_id =
|
|
||||||
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
|
|
||||||
|
|
||||||
println!("Group#{chat_id} created and protected successfully.");
|
|
||||||
}
|
|
||||||
"addmember" => {
|
"addmember" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected");
|
ensure!(sel_chat.is_some(), "No chat selected");
|
||||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||||
@@ -796,6 +738,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
println!("Chat name set");
|
println!("Chat name set");
|
||||||
}
|
}
|
||||||
|
"groupdescription" => {
|
||||||
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <description> missing.");
|
||||||
|
chat::set_chat_description(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||||
|
|
||||||
|
println!("Chat description set");
|
||||||
|
}
|
||||||
"groupimage" => {
|
"groupimage" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
||||||
@@ -831,11 +780,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Location streaming: {}",
|
"Location streaming: {}",
|
||||||
location::is_sending_locations_to_chat(
|
location::is_sending_to_chat(&context, sel_chat.as_ref().unwrap().get_id()).await?,
|
||||||
&context,
|
|
||||||
Some(sel_chat.as_ref().unwrap().get_id())
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
"getlocations" => {
|
"getlocations" => {
|
||||||
@@ -875,12 +820,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ensure!(!arg1.is_empty(), "No timeout given.");
|
ensure!(!arg1.is_empty(), "No timeout given.");
|
||||||
|
|
||||||
let seconds = arg1.parse()?;
|
let seconds = arg1.parse()?;
|
||||||
location::send_locations_to_chat(
|
location::send_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), seconds).await?;
|
||||||
&context,
|
|
||||||
sel_chat.as_ref().unwrap().get_id(),
|
|
||||||
seconds,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
println!(
|
println!(
|
||||||
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
"Locations will be sent to Chat#{} for {} seconds. Use 'setlocation <lat> <lng>' to play around.",
|
||||||
sel_chat.as_ref().unwrap().get_id(),
|
sel_chat.as_ref().unwrap().get_id(),
|
||||||
@@ -902,9 +842,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Success, streaming can be stopped.");
|
println!("Success, streaming can be stopped.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"dellocations" => {
|
|
||||||
location::delete_all(&context).await?;
|
|
||||||
}
|
|
||||||
"send" => {
|
"send" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
ensure!(!arg1.is_empty(), "No message text given.");
|
ensure!(!arg1.is_empty(), "No message text given.");
|
||||||
@@ -913,6 +850,23 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
|
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
|
||||||
}
|
}
|
||||||
|
"send-sync" => {
|
||||||
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
|
ensure!(!arg1.is_empty(), "No message text given.");
|
||||||
|
|
||||||
|
// Send message over a dedicated SMTP connection
|
||||||
|
// and measure time.
|
||||||
|
//
|
||||||
|
// This can be used to benchmark SMTP connection establishment.
|
||||||
|
let time_start = std::time::Instant::now();
|
||||||
|
|
||||||
|
let msg = format!("{arg1} {arg2}");
|
||||||
|
let mut msg = Message::new_text(msg);
|
||||||
|
chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||||
|
|
||||||
|
let time_needed = time_start.elapsed();
|
||||||
|
println!("Sent message in {time_needed:?}.");
|
||||||
|
}
|
||||||
"sendempty" => {
|
"sendempty" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
||||||
@@ -960,10 +914,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let msg_id = MsgId::new(arg1.parse()?);
|
let msg_id = MsgId::new(arg1.parse()?);
|
||||||
context.send_webxdc_status_update(msg_id, arg2).await?;
|
context.send_webxdc_status_update(msg_id, arg2).await?;
|
||||||
}
|
}
|
||||||
"videochat" => {
|
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
|
||||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
|
||||||
}
|
|
||||||
"listmsgs" => {
|
"listmsgs" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||||
|
|
||||||
@@ -985,7 +935,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
},
|
},
|
||||||
query,
|
query,
|
||||||
);
|
);
|
||||||
println!("{time_needed:?} to create this list");
|
eprintln!("{time_needed:?} to create this list");
|
||||||
}
|
}
|
||||||
"draft" => {
|
"draft" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
@@ -1151,7 +1101,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"listcontacts" | "contacts" => {
|
"listcontacts" | "contacts" => {
|
||||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
println!("{} contacts.", contacts.len());
|
println!("{} key contacts.", contacts.len());
|
||||||
|
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
|
||||||
|
log_contactlist(&context, &addrcontacts).await?;
|
||||||
|
println!("{} address contacts.", addrcontacts.len());
|
||||||
}
|
}
|
||||||
"addcontact" => {
|
"addcontact" => {
|
||||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||||
@@ -1215,6 +1168,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
println!("{} blocked contacts.", contacts.len());
|
println!("{} blocked contacts.", contacts.len());
|
||||||
}
|
}
|
||||||
|
"import-vcard" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||||
|
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
|
||||||
|
let contacts = import_vcard(&context, &vcard_content).await?;
|
||||||
|
println!("vCard contacts imported:");
|
||||||
|
log_contactlist(&context, &contacts).await?;
|
||||||
|
}
|
||||||
|
"make-vcard" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||||
|
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
|
||||||
|
let mut contact_ids = vec![];
|
||||||
|
for x in arg2.split_whitespace() {
|
||||||
|
contact_ids.push(ContactId::new(x.parse()?))
|
||||||
|
}
|
||||||
|
let vcard_content = make_vcard(&context, &contact_ids).await?;
|
||||||
|
fs::write(&arg1.to_string(), vcard_content).await?;
|
||||||
|
println!("vCard written to: {arg1}");
|
||||||
|
}
|
||||||
"checkqr" => {
|
"checkqr" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||||
let qr = check_qr(&context, arg1).await?;
|
let qr = check_qr(&context, arg1).await?;
|
||||||
@@ -1223,8 +1194,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"setqr" => {
|
"setqr" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||||
match set_config_from_qr(&context, arg1).await {
|
match set_config_from_qr(&context, arg1).await {
|
||||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
Ok(()) => eprintln!("Config set from the QR code."),
|
||||||
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"createqrsvg" => {
|
"createqrsvg" => {
|
||||||
@@ -1236,10 +1207,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"providerinfo" => {
|
"providerinfo" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||||
let proxy_enabled = context
|
match provider::get_provider_info(arg1) {
|
||||||
.get_config_bool(config::Config::ProxyEnabled)
|
|
||||||
.await?;
|
|
||||||
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
|
|
||||||
Some(info) => {
|
Some(info) => {
|
||||||
println!("Information for provider belonging to {arg1}:");
|
println!("Information for provider belonging to {arg1}:");
|
||||||
println!("status: {}", info.status as u32);
|
println!("status: {}", info.status as u32);
|
||||||
@@ -1275,7 +1243,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
"" => (),
|
"" => (),
|
||||||
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -149,10 +149,7 @@ impl Completer for DcHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMEX_COMMANDS: [&str; 13] = [
|
const IMEX_COMMANDS: [&str; 10] = [
|
||||||
"initiate-key-transfer",
|
|
||||||
"get-setupcodebegin",
|
|
||||||
"continue-key-transfer",
|
|
||||||
"has-backup",
|
"has-backup",
|
||||||
"export-backup",
|
"export-backup",
|
||||||
"import-backup",
|
"import-backup",
|
||||||
@@ -192,13 +189,14 @@ const CHAT_COMMANDS: [&str; 39] = [
|
|||||||
"addmember",
|
"addmember",
|
||||||
"removemember",
|
"removemember",
|
||||||
"groupname",
|
"groupname",
|
||||||
|
"groupdescription",
|
||||||
"groupimage",
|
"groupimage",
|
||||||
"chatinfo",
|
"chatinfo",
|
||||||
"sendlocations",
|
"sendlocations",
|
||||||
"setlocation",
|
"setlocation",
|
||||||
"dellocations",
|
|
||||||
"getlocations",
|
"getlocations",
|
||||||
"send",
|
"send",
|
||||||
|
"send-sync",
|
||||||
"sendempty",
|
"sendempty",
|
||||||
"sendimage",
|
"sendimage",
|
||||||
"sendsticker",
|
"sendsticker",
|
||||||
@@ -206,7 +204,6 @@ const CHAT_COMMANDS: [&str; 39] = [
|
|||||||
"sendhtml",
|
"sendhtml",
|
||||||
"sendsyncmsg",
|
"sendsyncmsg",
|
||||||
"sendupdate",
|
"sendupdate",
|
||||||
"videochat",
|
|
||||||
"draft",
|
"draft",
|
||||||
"devicemsg",
|
"devicemsg",
|
||||||
"listmedia",
|
"listmedia",
|
||||||
@@ -232,7 +229,7 @@ const MESSAGE_COMMANDS: [&str; 10] = [
|
|||||||
"delmsg",
|
"delmsg",
|
||||||
"react",
|
"react",
|
||||||
];
|
];
|
||||||
const CONTACT_COMMANDS: [&str; 7] = [
|
const CONTACT_COMMANDS: [&str; 9] = [
|
||||||
"listcontacts",
|
"listcontacts",
|
||||||
"addcontact",
|
"addcontact",
|
||||||
"contactinfo",
|
"contactinfo",
|
||||||
@@ -240,6 +237,8 @@ const CONTACT_COMMANDS: [&str; 7] = [
|
|||||||
"block",
|
"block",
|
||||||
"unblock",
|
"unblock",
|
||||||
"listblocked",
|
"listblocked",
|
||||||
|
"import-vcard",
|
||||||
|
"make-vcard",
|
||||||
];
|
];
|
||||||
const MISC_COMMANDS: [&str; 14] = [
|
const MISC_COMMANDS: [&str; 14] = [
|
||||||
"getqr",
|
"getqr",
|
||||||
@@ -311,7 +310,7 @@ impl Validator for DcHelper {}
|
|||||||
|
|
||||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
println!("Error: Bad arguments, expected [db-name].");
|
eprintln!("Error: Bad arguments, expected [db-name].");
|
||||||
bail!("No db-name specified");
|
bail!("No db-name specified");
|
||||||
}
|
}
|
||||||
let context = ContextBuilder::new(args[1].clone().into())
|
let context = ContextBuilder::new(args[1].clone().into())
|
||||||
@@ -366,7 +365,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error: {err:#}");
|
eprintln!("Error: {err:#}");
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,7 +380,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error: {err:#}");
|
eprintln!("Error: {err:#}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,12 +427,12 @@ async fn handle_cmd(
|
|||||||
}
|
}
|
||||||
"oauth2" => {
|
"oauth2" => {
|
||||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
||||||
let oauth2_url =
|
if let Some(oauth2_url) =
|
||||||
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?
|
||||||
if oauth2_url.is_none() {
|
{
|
||||||
println!("OAuth2 not available for {}.", &addr);
|
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{oauth2_url}");
|
||||||
} else {
|
} else {
|
||||||
println!("Open the following url, set mail_pw to the generated token and server_flags to 2:\n{}", oauth2_url.unwrap());
|
println!("OAuth2 not available for {}.", &addr);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("oauth2: set addr first.");
|
println!("oauth2: set addr first.");
|
||||||
@@ -465,7 +464,7 @@ async fn handle_cmd(
|
|||||||
println!("QR code svg written to: {file:#?}");
|
println!("QR code svg written to: {file:#?}");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("Failed to get QR code svg: {}", err);
|
bail!("Failed to get QR code svg: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
|
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
|
||||||
and provides asynchronous interface to it.
|
and provides asynchronous interface to it.
|
||||||
|
`rpc.start()` performs a health-check RPC call to verify the server
|
||||||
|
started successfully and will raise an error if startup fails
|
||||||
|
(e.g. if the accounts directory could not be used).
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -30,6 +33,15 @@ $ pip install .
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
||||||
|
## Activating current checkout of deltachat-rpc-client and -server for development
|
||||||
|
|
||||||
|
Go to root repository directory and run:
|
||||||
|
```
|
||||||
|
$ scripts/make-rpc-testenv.sh
|
||||||
|
$ source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
## Using in REPL
|
## Using in REPL
|
||||||
|
|
||||||
Setup a development environment:
|
Setup a development environment:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ def main():
|
|||||||
with Rpc() as rpc:
|
with Rpc() as rpc:
|
||||||
deltachat = DeltaChat(rpc)
|
deltachat = DeltaChat(rpc)
|
||||||
system_info = deltachat.get_system_info()
|
system_info = deltachat.get_system_info()
|
||||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
logging.info(f"Running deltachat core {system_info['deltachat_core_version']}")
|
||||||
|
|
||||||
accounts = deltachat.get_all_accounts()
|
accounts = deltachat.get_all_accounts()
|
||||||
account = accounts[0] if accounts else deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
@@ -21,36 +21,30 @@ def main():
|
|||||||
account.set_config("bot", "1")
|
account.set_config("bot", "1")
|
||||||
if not account.is_configured():
|
if not account.is_configured():
|
||||||
logging.info("Account is not configured, configuring")
|
logging.info("Account is not configured, configuring")
|
||||||
account.set_config("addr", sys.argv[1])
|
account.add_or_update_transport({"addr": sys.argv[1], "password": sys.argv[2]})
|
||||||
account.set_config("mail_pw", sys.argv[2])
|
|
||||||
account.configure()
|
|
||||||
logging.info("Configured")
|
logging.info("Configured")
|
||||||
else:
|
else:
|
||||||
logging.info("Account is already configured")
|
logging.info("Account is already configured")
|
||||||
deltachat.start_io()
|
deltachat.start_io()
|
||||||
|
|
||||||
def process_messages():
|
qr = account.get_qr_code()
|
||||||
for message in account.get_next_messages():
|
logging.info(f"Invite link: {qr}")
|
||||||
|
while True:
|
||||||
|
event = account.wait_for_event()
|
||||||
|
if event.kind == EventType.INFO:
|
||||||
|
logging.info(event["msg"])
|
||||||
|
elif event.kind == EventType.WARNING:
|
||||||
|
logging.warning(event["msg"])
|
||||||
|
elif event.kind == EventType.ERROR:
|
||||||
|
logging.error(event["msg"])
|
||||||
|
elif event.kind == EventType.INCOMING_MSG:
|
||||||
|
logging.info("Got an incoming message")
|
||||||
|
message = account.get_message_by_id(event.msg_id)
|
||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
if snapshot.from_id != SpecialContactId.SELF and 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:
|
||||||
snapshot.chat.send_text(snapshot.text)
|
snapshot.chat.send_text(snapshot.text)
|
||||||
snapshot.message.mark_seen()
|
snapshot.message.mark_seen()
|
||||||
|
|
||||||
# Process old messages.
|
|
||||||
process_messages()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
event = account.wait_for_event()
|
|
||||||
if event["kind"] == EventType.INFO:
|
|
||||||
logging.info("%s", event["msg"])
|
|
||||||
elif event["kind"] == EventType.WARNING:
|
|
||||||
logging.warning("%s", event["msg"])
|
|
||||||
elif event["kind"] == EventType.ERROR:
|
|
||||||
logging.error("%s", event["msg"])
|
|
||||||
elif event["kind"] == EventType.INCOMING_MSG:
|
|
||||||
logging.info("Got an incoming message")
|
|
||||||
process_messages()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=45"]
|
requires = ["setuptools>=77"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deltachat-rpc-client"
|
name = "deltachat-rpc-client"
|
||||||
version = "2.2.0"
|
version = "2.50.0-dev"
|
||||||
|
license = "MPL-2.0"
|
||||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
|
||||||
"Operating System :: POSIX :: Linux",
|
"Operating System :: POSIX :: Linux",
|
||||||
"Operating System :: MacOS :: MacOS X",
|
"Operating System :: MacOS :: MacOS X",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13",
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14",
|
||||||
"Topic :: Communications :: Chat",
|
"Topic :: Communications :: Chat",
|
||||||
"Topic :: Communications :: Email"
|
"Topic :: Communications :: Email"
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
deltachat_rpc_client = [
|
deltachat_rpc_client = [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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
|
||||||
from .rpc import Rpc
|
from .rpc import JsonRpcError, Rpc
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Account",
|
"Account",
|
||||||
@@ -19,6 +19,7 @@ __all__ = [
|
|||||||
"Contact",
|
"Contact",
|
||||||
"DeltaChat",
|
"DeltaChat",
|
||||||
"EventType",
|
"EventType",
|
||||||
|
"JsonRpcError",
|
||||||
"Message",
|
"Message",
|
||||||
"SpecialContactId",
|
"SpecialContactId",
|
||||||
"Rpc",
|
"Rpc",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -44,8 +45,13 @@ class AttrDict(dict):
|
|||||||
super().__setattr__(attr, val)
|
super().__setattr__(attr, val)
|
||||||
|
|
||||||
|
|
||||||
|
def _forever(_event: AttrDict) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
|
until: Callable[[AttrDict], bool] = _forever,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -55,10 +61,11 @@ def run_client_cli(
|
|||||||
"""
|
"""
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
|
||||||
_run_cli(Client, hooks, argv, **kwargs)
|
_run_cli(Client, until, hooks, argv, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def run_bot_cli(
|
def run_bot_cli(
|
||||||
|
until: Callable[[AttrDict], bool] = _forever,
|
||||||
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,
|
||||||
@@ -69,11 +76,12 @@ def run_bot_cli(
|
|||||||
"""
|
"""
|
||||||
from .client import Bot
|
from .client import Bot
|
||||||
|
|
||||||
_run_cli(Bot, hooks, argv, **kwargs)
|
_run_cli(Bot, until, hooks, argv, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _run_cli(
|
def _run_cli(
|
||||||
client_type: Type["Client"],
|
client_type: Type["Client"],
|
||||||
|
until: Callable[[AttrDict], bool] = _forever,
|
||||||
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,
|
||||||
@@ -111,7 +119,7 @@ def _run_cli(
|
|||||||
kwargs={"email": args.email, "password": args.password},
|
kwargs={"email": args.email, "password": args.password},
|
||||||
)
|
)
|
||||||
configure_thread.start()
|
configure_thread.start()
|
||||||
client.run_forever()
|
client.run_until(until)
|
||||||
|
|
||||||
|
|
||||||
def extract_addr(text: str) -> str:
|
def extract_addr(text: str) -> str:
|
||||||
@@ -182,9 +190,6 @@ class futuremethod: # noqa: N801
|
|||||||
self._func = func
|
self._func = func
|
||||||
|
|
||||||
def __get__(self, instance, owner=None):
|
def __get__(self, instance, owner=None):
|
||||||
if instance is None:
|
|
||||||
return self
|
|
||||||
|
|
||||||
def future(*args):
|
def future(*args):
|
||||||
generator = self._func(instance, *args)
|
generator = self._func(instance, *args)
|
||||||
res = next(generator)
|
res = next(generator)
|
||||||
@@ -197,6 +202,7 @@ class futuremethod: # noqa: N801
|
|||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
@functools.wraps(self._func)
|
||||||
def wrapper(*args):
|
def wrapper(*args):
|
||||||
f = future(*args)
|
f = future(*args)
|
||||||
return f()
|
return f()
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
from ._utils import AttrDict, futuremethod
|
from ._utils import AttrDict, futuremethod
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
@@ -124,6 +124,15 @@ class Account:
|
|||||||
"""Add a new transport."""
|
"""Add a new transport."""
|
||||||
yield self._rpc.add_or_update_transport.future(self.id, params)
|
yield self._rpc.add_or_update_transport.future(self.id, params)
|
||||||
|
|
||||||
|
@futuremethod
|
||||||
|
def add_transport_from_qr(self, qr: str):
|
||||||
|
"""Add a new transport using a QR code."""
|
||||||
|
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||||
|
|
||||||
|
def delete_transport(self, addr: str):
|
||||||
|
"""Delete a transport."""
|
||||||
|
self._rpc.delete_transport(self.id, addr)
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def list_transports(self):
|
def list_transports(self):
|
||||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||||
@@ -185,7 +194,21 @@ class Account:
|
|||||||
return Contact(self, contact_id)
|
return Contact(self, contact_id)
|
||||||
|
|
||||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||||
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
"""Looks up a known and unblocked contact with a given e-mail address.
|
||||||
|
To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||||
|
|
||||||
|
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||||
|
(e.g. an address-contact and a key-contact),
|
||||||
|
this looks up the most recently seen contact,
|
||||||
|
i.e. which contact is returned depends on which contact last sent a message.
|
||||||
|
If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||||
|
But **DO NOT** internally represent contacts by their email address
|
||||||
|
and do not use this function to look them up;
|
||||||
|
otherwise this function will sometimes look up the wrong contact.
|
||||||
|
Instead, you should internally represent contacts by their ids.
|
||||||
|
|
||||||
|
To validate an e-mail address independently of the contact database
|
||||||
|
use check_email_validity()."""
|
||||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||||
return contact_id and Contact(self, contact_id)
|
return contact_id and Contact(self, contact_id)
|
||||||
|
|
||||||
@@ -285,7 +308,7 @@ class Account:
|
|||||||
chats.append(AttrDict(item))
|
chats.append(AttrDict(item))
|
||||||
return chats
|
return chats
|
||||||
|
|
||||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
def create_group(self, name: str) -> Chat:
|
||||||
"""Create a new group chat.
|
"""Create a new group chat.
|
||||||
|
|
||||||
After creation,
|
After creation,
|
||||||
@@ -302,15 +325,11 @@ class Account:
|
|||||||
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
|
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
|
||||||
(see `get_full_snapshot()` / `get_basic_snapshot()`).
|
(see `get_full_snapshot()` / `get_basic_snapshot()`).
|
||||||
This may be useful if you want to show some help for just created groups.
|
This may be useful if you want to show some help for just created groups.
|
||||||
|
|
||||||
:param protect: If set to 1 the function creates group with protection initially enabled.
|
|
||||||
Only verified members are allowed in these groups
|
|
||||||
and end-to-end-encryption is always enabled.
|
|
||||||
"""
|
"""
|
||||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
return Chat(self, self._rpc.create_group_chat(self.id, name, False))
|
||||||
|
|
||||||
def create_broadcast(self, name: str) -> Chat:
|
def create_broadcast(self, name: str) -> Chat:
|
||||||
"""Create a new **broadcast channel**
|
"""Create a new, outgoing **broadcast channel**
|
||||||
(called "Channel" in the UI).
|
(called "Channel" in the UI).
|
||||||
|
|
||||||
Broadcast channels are similar to groups on the sending device,
|
Broadcast channels are similar to groups on the sending device,
|
||||||
@@ -372,8 +391,7 @@ class Account:
|
|||||||
"""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, process "incoming message" events instead.
|
||||||
to process oldest messages first.
|
|
||||||
"""
|
"""
|
||||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||||
@@ -383,9 +401,18 @@ class Account:
|
|||||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
|
@futuremethod
|
||||||
def wait_next_messages(self) -> list[Message]:
|
def wait_next_messages(self) -> list[Message]:
|
||||||
"""Wait for new messages and return a list of them."""
|
"""(deprecated) Wait for new messages and return a list of them. Meant for bots.
|
||||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
|
||||||
|
Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
|
||||||
|
even if it is not fully downloaded yet.
|
||||||
|
The bot needs to wait for the message to be fully downloaded.
|
||||||
|
Since this is usually not the desired behavior,
|
||||||
|
bots should instead use the `EventType.INCOMING_MSG`
|
||||||
|
event for getting notified about new messages.
|
||||||
|
"""
|
||||||
|
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
def wait_for_incoming_msg_event(self):
|
def wait_for_incoming_msg_event(self):
|
||||||
@@ -400,12 +427,21 @@ class Account:
|
|||||||
"""Wait for messages noticed event and return it."""
|
"""Wait for messages noticed event and return it."""
|
||||||
return self.wait_for_event(EventType.MSGS_NOTICED)
|
return self.wait_for_event(EventType.MSGS_NOTICED)
|
||||||
|
|
||||||
|
def wait_for_msg(self, event_type) -> Message:
|
||||||
|
"""Wait for an event about the message.
|
||||||
|
|
||||||
|
Consumes all events before the matching event.
|
||||||
|
Returns a message corresponding to the msg_id field of the event.
|
||||||
|
"""
|
||||||
|
event = self.wait_for_event(event_type)
|
||||||
|
return self.get_message_by_id(event.msg_id)
|
||||||
|
|
||||||
def wait_for_incoming_msg(self):
|
def wait_for_incoming_msg(self):
|
||||||
"""Wait for incoming message and return it.
|
"""Wait for incoming message and return it.
|
||||||
|
|
||||||
Consumes all events before the next incoming message event.
|
Consumes all events before the next incoming message event.
|
||||||
"""
|
"""
|
||||||
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
return self.wait_for_msg(EventType.INCOMING_MSG)
|
||||||
|
|
||||||
def wait_for_securejoin_inviter_success(self):
|
def wait_for_securejoin_inviter_success(self):
|
||||||
"""Wait until SecureJoin process finishes successfully on the inviter side."""
|
"""Wait until SecureJoin process finishes successfully on the inviter side."""
|
||||||
@@ -425,16 +461,6 @@ class Account:
|
|||||||
"""Wait for reaction change event."""
|
"""Wait for reaction change event."""
|
||||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||||
|
|
||||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
|
||||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
|
||||||
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]
|
|
||||||
|
|
||||||
def export_backup(self, path, passphrase: str = "") -> None:
|
def export_backup(self, path, passphrase: str = "") -> None:
|
||||||
"""Export backup."""
|
"""Export backup."""
|
||||||
self._rpc.export_backup(self.id, str(path), passphrase)
|
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||||
@@ -453,6 +479,11 @@ class Account:
|
|||||||
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||||
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||||
|
|
||||||
def initiate_autocrypt_key_transfer(self) -> None:
|
def ice_servers(self) -> list:
|
||||||
"""Send Autocrypt Setup Message."""
|
"""Return ICE servers for WebRTC configuration."""
|
||||||
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
ice_servers_json = self._rpc.ice_servers(self.id)
|
||||||
|
return json.loads(ice_servers_json)
|
||||||
|
|
||||||
|
def is_sending_locations(self) -> bool:
|
||||||
|
"""Return True if sending locations to any chat."""
|
||||||
|
return self._rpc.is_sending_locations(self.id)
|
||||||
|
|||||||
@@ -164,10 +164,15 @@ class Chat:
|
|||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
def send_sticker(self, path: str) -> Message:
|
def send_sticker(self, path: str) -> Message:
|
||||||
"""Send an sticker and return the resulting Message instance."""
|
"""Deprecated as of 2026-04; use `send_message` with `Viewtype.STICKER` instead."""
|
||||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
|
def resend_messages(self, messages: list[Message]) -> None:
|
||||||
|
"""Resend a list of messages to this chat."""
|
||||||
|
msg_ids = [msg.id for msg in messages]
|
||||||
|
self._rpc.resend_messages(self.account.id, msg_ids)
|
||||||
|
|
||||||
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]
|
||||||
@@ -201,9 +206,9 @@ class Chat:
|
|||||||
snapshot["message"] = Message(self.account, snapshot.id)
|
snapshot["message"] = Message(self.account, snapshot.id)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
def get_messages(self, add_daymarker: bool = False) -> list[Message]:
|
||||||
"""Get the list of messages in this chat."""
|
"""Get the list of messages in this chat."""
|
||||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
msgs = self._rpc.get_message_ids(self.account.id, self.id, False, add_daymarker)
|
||||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||||
|
|
||||||
def get_fresh_message_count(self) -> int:
|
def get_fresh_message_count(self) -> int:
|
||||||
@@ -214,10 +219,16 @@ class Chat:
|
|||||||
"""Mark all messages in this chat as noticed."""
|
"""Mark all messages in this chat as noticed."""
|
||||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||||
|
|
||||||
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
def mark_fresh(self) -> None:
|
||||||
|
"""Mark the last incoming message in the chat as fresh."""
|
||||||
|
self._rpc.markfresh_chat(self.account.id, self.id)
|
||||||
|
|
||||||
|
def add_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
||||||
"""Add contacts to this group."""
|
"""Add contacts to this group."""
|
||||||
|
from .account import Account
|
||||||
|
|
||||||
for cnt in contact:
|
for cnt in contact:
|
||||||
if isinstance(cnt, str):
|
if isinstance(cnt, (str, Account)):
|
||||||
contact_id = 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
|
||||||
@@ -225,10 +236,12 @@ class Chat:
|
|||||||
contact_id = cnt
|
contact_id = cnt
|
||||||
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)
|
||||||
|
|
||||||
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
def remove_contact(self, *contact: Union[int, str, Contact, "Account"]) -> None:
|
||||||
"""Remove members from this group."""
|
"""Remove members from this group."""
|
||||||
|
from .account import Account
|
||||||
|
|
||||||
for cnt in contact:
|
for cnt in contact:
|
||||||
if isinstance(cnt, str):
|
if isinstance(cnt, (str, Account)):
|
||||||
contact_id = 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
|
||||||
@@ -244,6 +257,10 @@ class Chat:
|
|||||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||||
|
|
||||||
|
def num_contacts(self) -> int:
|
||||||
|
"""Return number of contacts in this chat."""
|
||||||
|
return len(self.get_contacts())
|
||||||
|
|
||||||
def get_past_contacts(self) -> list[Contact]:
|
def get_past_contacts(self) -> list[Contact]:
|
||||||
"""Get past contacts for this chat."""
|
"""Get past contacts for this chat."""
|
||||||
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
||||||
@@ -260,6 +277,16 @@ class Chat:
|
|||||||
"""Remove profile image of this chat."""
|
"""Remove profile image of this chat."""
|
||||||
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||||
|
|
||||||
|
def send_locations(self, seconds) -> None:
|
||||||
|
"""Enable location streaming in the chat for the given number of seconds.
|
||||||
|
|
||||||
|
Pass 0 to disable location streaming."""
|
||||||
|
self._rpc.send_locations_to_chat(self.account.id, self.id, seconds)
|
||||||
|
|
||||||
|
def is_sending_locations(self) -> bool:
|
||||||
|
"""Return True if sending locations to this chat."""
|
||||||
|
return self._rpc.is_sending_locations_to_chat(self.account.id, self.id)
|
||||||
|
|
||||||
def get_locations(
|
def get_locations(
|
||||||
self,
|
self,
|
||||||
contact: Optional[Contact] = None,
|
contact: Optional[Contact] = None,
|
||||||
@@ -289,3 +316,8 @@ class Chat:
|
|||||||
f.write(vcard.encode())
|
f.write(vcard.encode())
|
||||||
f.flush()
|
f.flush()
|
||||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
||||||
|
|
||||||
|
def place_outgoing_call(self, place_call_info: str, has_video_initially: bool) -> Message:
|
||||||
|
"""Starts an outgoing call."""
|
||||||
|
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info, has_video_initially)
|
||||||
|
return Message(self.account, msg_id)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from typing import (
|
|||||||
|
|
||||||
from ._utils import (
|
from ._utils import (
|
||||||
AttrDict,
|
AttrDict,
|
||||||
|
_forever,
|
||||||
parse_system_add_remove,
|
parse_system_add_remove,
|
||||||
parse_system_image_changed,
|
parse_system_image_changed,
|
||||||
parse_system_title_changed,
|
parse_system_title_changed,
|
||||||
@@ -83,28 +84,36 @@ class Client:
|
|||||||
|
|
||||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
"""Configure the client."""
|
"""Configure the client."""
|
||||||
self.account.set_config("addr", email)
|
|
||||||
self.account.set_config("mail_pw", password)
|
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self.account.set_config(key, value)
|
self.account.set_config(key, value)
|
||||||
self.account.configure()
|
params = {"addr": email, "password": password}
|
||||||
|
self.account.add_or_update_transport(params)
|
||||||
self.logger.debug("Account configured")
|
self.logger.debug("Account configured")
|
||||||
|
|
||||||
def run_forever(self) -> None:
|
def run_forever(self) -> None:
|
||||||
"""Process events forever."""
|
"""Process events forever."""
|
||||||
self.run_until(lambda _: False)
|
self.run_until(_forever)
|
||||||
|
|
||||||
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||||
"""Process events until the given callable evaluates to True.
|
"""Start the event processing loop."""
|
||||||
|
|
||||||
The callable should accept an AttrDict object representing the
|
|
||||||
last processed event. The event is returned when the callable
|
|
||||||
evaluates to True.
|
|
||||||
"""
|
|
||||||
self.logger.debug("Listening to incoming events...")
|
self.logger.debug("Listening to incoming events...")
|
||||||
if self.is_configured():
|
if self.is_configured():
|
||||||
self.account.start_io()
|
self.account.start_io()
|
||||||
self._process_messages() # Process old messages.
|
self._process_messages() # Process old messages.
|
||||||
|
return self._process_events(until_func=func) # Loop over incoming events
|
||||||
|
|
||||||
|
def _process_events(
|
||||||
|
self,
|
||||||
|
until_func: Callable[[AttrDict], bool] = _forever,
|
||||||
|
until_event: EventType = False,
|
||||||
|
) -> AttrDict:
|
||||||
|
"""Process events until the given callable evaluates to True,
|
||||||
|
or until a certain event happens.
|
||||||
|
|
||||||
|
The until_func callable should accept an AttrDict object representing
|
||||||
|
the last processed event. The event is returned when the callable
|
||||||
|
evaluates to True.
|
||||||
|
"""
|
||||||
while True:
|
while True:
|
||||||
event = self.account.wait_for_event()
|
event = self.account.wait_for_event()
|
||||||
event["kind"] = EventType(event.kind)
|
event["kind"] = EventType(event.kind)
|
||||||
@@ -113,10 +122,13 @@ class Client:
|
|||||||
if event.kind == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
self._process_messages()
|
self._process_messages()
|
||||||
|
|
||||||
stop = func(event)
|
stop = until_func(event)
|
||||||
if stop:
|
if stop:
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
if event.kind == until_event:
|
||||||
|
return event
|
||||||
|
|
||||||
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 evfilter.filter(event):
|
if evfilter.filter(event):
|
||||||
|
|||||||
@@ -73,9 +73,14 @@ class EventType(str, Enum):
|
|||||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||||
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
||||||
|
INCOMING_CALL = "IncomingCall"
|
||||||
|
INCOMING_CALL_ACCEPTED = "IncomingCallAccepted"
|
||||||
|
OUTGOING_CALL_ACCEPTED = "OutgoingCallAccepted"
|
||||||
|
CALL_ENDED = "CallEnded"
|
||||||
CONFIG_SYNCED = "ConfigSynced"
|
CONFIG_SYNCED = "ConfigSynced"
|
||||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
||||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
||||||
|
TRANSPORTS_MODIFIED = "TransportsModified"
|
||||||
|
|
||||||
|
|
||||||
class ChatId(IntEnum):
|
class ChatId(IntEnum):
|
||||||
@@ -87,19 +92,17 @@ class ChatId(IntEnum):
|
|||||||
LAST_SPECIAL = 9
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
class ChatType(IntEnum):
|
class ChatType(str, Enum):
|
||||||
"""Chat type."""
|
"""Chat type."""
|
||||||
|
|
||||||
UNDEFINED = 0
|
SINGLE = "Single"
|
||||||
|
|
||||||
SINGLE = 100
|
|
||||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||||
|
|
||||||
GROUP = 120
|
GROUP = "Group"
|
||||||
|
|
||||||
MAILINGLIST = 140
|
MAILINGLIST = "Mailinglist"
|
||||||
|
|
||||||
OUT_BROADCAST = 160
|
OUT_BROADCAST = "OutBroadcast"
|
||||||
"""Outgoing broadcast channel, called "Channel" in the UI.
|
"""Outgoing broadcast channel, called "Channel" in the UI.
|
||||||
|
|
||||||
The user can send into this channel,
|
The user can send into this channel,
|
||||||
@@ -111,7 +114,7 @@ class ChatType(IntEnum):
|
|||||||
which would make it hard to grep for it.
|
which would make it hard to grep for it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
IN_BROADCAST = 165
|
IN_BROADCAST = "InBroadcast"
|
||||||
"""Incoming broadcast channel, called "Channel" in the UI.
|
"""Incoming broadcast channel, called "Channel" in the UI.
|
||||||
|
|
||||||
This channel is read-only,
|
This channel is read-only,
|
||||||
@@ -156,7 +159,6 @@ class ViewType(str, Enum):
|
|||||||
VOICE = "Voice"
|
VOICE = "Voice"
|
||||||
VIDEO = "Video"
|
VIDEO = "Video"
|
||||||
FILE = "File"
|
FILE = "File"
|
||||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
|
||||||
WEBXDC = "Webxdc"
|
WEBXDC = "Webxdc"
|
||||||
VCARD = "Vcard"
|
VCARD = "Vcard"
|
||||||
|
|
||||||
@@ -188,7 +190,6 @@ class MessageState(IntEnum):
|
|||||||
IN_FRESH = 10
|
IN_FRESH = 10
|
||||||
IN_NOTICED = 13
|
IN_NOTICED = 13
|
||||||
IN_SEEN = 16
|
IN_SEEN = 16
|
||||||
OUT_PREPARING = 18
|
|
||||||
OUT_DRAFT = 19
|
OUT_DRAFT = 19
|
||||||
OUT_PENDING = 20
|
OUT_PENDING = 20
|
||||||
OUT_FAILED = 24
|
OUT_FAILED = 24
|
||||||
@@ -275,11 +276,3 @@ class SocketSecurity(IntEnum):
|
|||||||
SSL = 1
|
SSL = 1
|
||||||
STARTTLS = 2
|
STARTTLS = 2
|
||||||
PLAIN = 3
|
PLAIN = 3
|
||||||
|
|
||||||
|
|
||||||
class VideochatType(IntEnum):
|
|
||||||
"""Video chat URL type."""
|
|
||||||
|
|
||||||
UNKNOWN = 0
|
|
||||||
BASICWEBRTC = 1
|
|
||||||
JITSI = 2
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict, futuremethod
|
||||||
from .account import Account
|
from .account import Account
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -39,6 +39,15 @@ class DeltaChat:
|
|||||||
"""Stop the I/O of all accounts."""
|
"""Stop the I/O of all accounts."""
|
||||||
self.rpc.stop_io_for_all_accounts()
|
self.rpc.stop_io_for_all_accounts()
|
||||||
|
|
||||||
|
@futuremethod
|
||||||
|
def background_fetch(self, timeout_in_seconds: int) -> None:
|
||||||
|
"""Run background fetch for all accounts."""
|
||||||
|
yield self.rpc.background_fetch.future(timeout_in_seconds)
|
||||||
|
|
||||||
|
def stop_background_fetch(self) -> None:
|
||||||
|
"""Stop ongoing background fetch."""
|
||||||
|
self.rpc.stop_background_fetch()
|
||||||
|
|
||||||
def maybe_network(self) -> None:
|
def maybe_network(self) -> None:
|
||||||
"""Indicate that the network conditions might have changed."""
|
"""Indicate that the network conditions might have changed."""
|
||||||
self.rpc.maybe_network()
|
self.rpc.maybe_network()
|
||||||
@@ -50,3 +59,11 @@ class DeltaChat:
|
|||||||
def set_translations(self, translations: dict[str, str]) -> None:
|
def set_translations(self, translations: dict[str, str]) -> None:
|
||||||
"""Set stock translation strings."""
|
"""Set stock translation strings."""
|
||||||
self.rpc.set_stock_strings(translations)
|
self.rpc.set_stock_strings(translations)
|
||||||
|
|
||||||
|
def set_location(self, latitude, longitude, accuracy) -> bool:
|
||||||
|
"""Set location, return True if location streaming should continue."""
|
||||||
|
return self.rpc.set_location(latitude, longitude, accuracy)
|
||||||
|
|
||||||
|
def stop_sending_locations(self) -> None:
|
||||||
|
"""Stop sending locations to all chats."""
|
||||||
|
return self.rpc.stop_sending_locations()
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ class Message:
|
|||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
def send_reaction(self, *reaction: str) -> "Message":
|
def send_reaction(self, *reaction: str) -> "Message":
|
||||||
"""Send a reaction to this message."""
|
"""
|
||||||
|
Sends a reaction to message.
|
||||||
|
|
||||||
|
A reaction is a string that represents an emoji.
|
||||||
|
You can call this function again to change the emoji;
|
||||||
|
the last sent reaction overrides all previously sent reactions.
|
||||||
|
It is possible to remove the reaction by sending an empty string.
|
||||||
|
"""
|
||||||
msg_id = 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)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
@@ -44,6 +51,14 @@ class Message:
|
|||||||
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
|
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
|
||||||
return [AttrDict(read_receipt) for read_receipt in read_receipts]
|
return [AttrDict(read_receipt) for read_receipt in read_receipts]
|
||||||
|
|
||||||
|
def get_read_receipt_count(self) -> int:
|
||||||
|
"""
|
||||||
|
Returns count of read receipts on message.
|
||||||
|
|
||||||
|
This view count is meant as a feedback measure for the channel owner only.
|
||||||
|
"""
|
||||||
|
return self._rpc.get_message_read_receipt_count(self.account.id, self.id)
|
||||||
|
|
||||||
def get_reactions(self) -> Optional[AttrDict]:
|
def get_reactions(self) -> Optional[AttrDict]:
|
||||||
"""Get message reactions."""
|
"""Get message reactions."""
|
||||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||||
@@ -60,13 +75,9 @@ class Message:
|
|||||||
"""Mark the message as seen."""
|
"""Mark the message as seen."""
|
||||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||||
|
|
||||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
def exists(self) -> bool:
|
||||||
"""Continue the Autocrypt Setup Message key transfer.
|
"""Return True if the message exists."""
|
||||||
|
return bool(self._rpc.get_existing_msg_ids(self.account.id, [self.id]))
|
||||||
This function can be called on received Autocrypt Setup Message
|
|
||||||
to import the key encrypted with the provided setup code.
|
|
||||||
"""
|
|
||||||
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
|
|
||||||
|
|
||||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||||
"""Send a webxdc status update. This message must be a webxdc."""
|
"""Send a webxdc status update. This message must be a webxdc."""
|
||||||
@@ -93,6 +104,17 @@ class Message:
|
|||||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def resend(self) -> None:
|
||||||
|
"""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.
|
||||||
|
"""
|
||||||
|
self._rpc.resend_messages(self.account.id, [self.id])
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def send_webxdc_realtime_advertisement(self):
|
def send_webxdc_realtime_advertisement(self):
|
||||||
"""Send an advertisement to join the realtime channel."""
|
"""Send an advertisement to join the realtime channel."""
|
||||||
@@ -102,3 +124,15 @@ class Message:
|
|||||||
def send_webxdc_realtime_data(self, data) -> None:
|
def send_webxdc_realtime_data(self, data) -> None:
|
||||||
"""Send data to the realtime channel."""
|
"""Send data to the realtime channel."""
|
||||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||||
|
|
||||||
|
def accept_incoming_call(self, accept_call_info):
|
||||||
|
"""Accepts an incoming call."""
|
||||||
|
self._rpc.accept_incoming_call(self.account.id, self.id, accept_call_info)
|
||||||
|
|
||||||
|
def end_call(self):
|
||||||
|
"""Ends incoming or outgoing call."""
|
||||||
|
self._rpc.end_call(self.account.id, self.id)
|
||||||
|
|
||||||
|
def get_call_info(self) -> AttrDict:
|
||||||
|
"""Return information about the call."""
|
||||||
|
return AttrDict(self._rpc.call_info(self.account.id, self.id))
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
|
import platform
|
||||||
import random
|
import random
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
from typing import AsyncGenerator, Optional
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
|
import execnet
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -13,6 +19,24 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
|
|||||||
from ._utils import futuremethod
|
from ._utils import futuremethod
|
||||||
from .rpc import Rpc
|
from .rpc import Rpc
|
||||||
|
|
||||||
|
E2EE_INFO_MSGS = 1
|
||||||
|
"""
|
||||||
|
The number of info messages added to new e2ee chats.
|
||||||
|
Currently this is "Messages are end-to-end encrypted."
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header():
|
||||||
|
for base in os.get_exec_path():
|
||||||
|
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
|
||||||
|
if fn.exists():
|
||||||
|
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
|
||||||
|
proc.wait()
|
||||||
|
version = proc.stderr.read().decode().strip()
|
||||||
|
return f"deltachat-rpc-server: {fn} [{version}]"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ACFactory:
|
class ACFactory:
|
||||||
"""Test account factory."""
|
"""Test account factory."""
|
||||||
@@ -22,9 +46,7 @@ class ACFactory:
|
|||||||
|
|
||||||
def get_unconfigured_account(self) -> Account:
|
def get_unconfigured_account(self) -> Account:
|
||||||
"""Create a new unconfigured account."""
|
"""Create a new unconfigured account."""
|
||||||
account = self.deltachat.add_account()
|
return self.deltachat.add_account()
|
||||||
account.set_config("verified_one_on_one_chats", "1")
|
|
||||||
return account
|
|
||||||
|
|
||||||
def get_unconfigured_bot(self) -> Bot:
|
def get_unconfigured_bot(self) -> Bot:
|
||||||
"""Create a new unconfigured bot."""
|
"""Create a new unconfigured bot."""
|
||||||
@@ -32,17 +54,21 @@ class ACFactory:
|
|||||||
|
|
||||||
def get_credentials(self) -> (str, str):
|
def get_credentials(self) -> (str, str):
|
||||||
"""Generate new credentials for chatmail account."""
|
"""Generate new credentials for chatmail account."""
|
||||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||||
return f"{username}@{domain}", f"{username}${username}"
|
return f"{username}@{domain}", f"{username}${username}"
|
||||||
|
|
||||||
|
def get_account_qr(self):
|
||||||
|
"""Return "dcaccount:" QR code for testing chatmail relay."""
|
||||||
|
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||||
|
return f"dcaccount:{domain}"
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
def new_configured_account(self):
|
def new_configured_account(self):
|
||||||
"""Create a new configured account."""
|
"""Create a new configured account."""
|
||||||
addr, password = self.get_credentials()
|
|
||||||
account = self.get_unconfigured_account()
|
account = self.get_unconfigured_account()
|
||||||
params = {"addr": addr, "password": password}
|
qr = self.get_account_qr()
|
||||||
yield account.add_or_update_transport.future(params)
|
yield account.add_transport_from_qr.future(qr)
|
||||||
|
|
||||||
assert account.is_configured()
|
assert account.is_configured()
|
||||||
return account
|
return account
|
||||||
@@ -69,11 +95,12 @@ class ACFactory:
|
|||||||
def resetup_account(self, ac: Account) -> Account:
|
def resetup_account(self, ac: Account) -> Account:
|
||||||
"""Resetup account from scratch, losing the encryption key."""
|
"""Resetup account from scratch, losing the encryption key."""
|
||||||
ac.stop_io()
|
ac.stop_io()
|
||||||
ac_clone = self.get_unconfigured_account()
|
transports = ac.list_transports()
|
||||||
for i in ["addr", "mail_pw"]:
|
|
||||||
ac_clone.set_config(i, ac.get_config(i))
|
|
||||||
ac.remove()
|
ac.remove()
|
||||||
ac_clone.configure()
|
ac_clone = self.get_unconfigured_account()
|
||||||
|
for transport in transports:
|
||||||
|
ac_clone.add_or_update_transport(transport)
|
||||||
|
ac_clone.bring_online()
|
||||||
return ac_clone
|
return ac_clone
|
||||||
|
|
||||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||||
@@ -132,9 +159,15 @@ def rpc(tmp_path) -> AsyncGenerator:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def acfactory(rpc) -> AsyncGenerator:
|
def dc(rpc) -> DeltaChat:
|
||||||
|
"""Return account manager."""
|
||||||
|
return DeltaChat(rpc)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def acfactory(dc) -> AsyncGenerator:
|
||||||
"""Return account factory fixture."""
|
"""Return account factory fixture."""
|
||||||
return ACFactory(DeltaChat(rpc))
|
return ACFactory(dc)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -172,13 +205,143 @@ def log():
|
|||||||
|
|
||||||
class Printer:
|
class Printer:
|
||||||
def section(self, msg: str) -> None:
|
def section(self, msg: str) -> None:
|
||||||
print()
|
logging.info("\n%s %s %s", "=" * 10, msg, "=" * 10)
|
||||||
print("=" * 10, msg, "=" * 10)
|
|
||||||
|
|
||||||
def step(self, msg: str) -> None:
|
def step(self, msg: str) -> None:
|
||||||
print("-" * 5, "step " + msg, "-" * 5)
|
logging.info("%s step %s %s", "-" * 5, msg, "-" * 5)
|
||||||
|
|
||||||
def indent(self, msg: str) -> None:
|
def indent(self, msg: str) -> None:
|
||||||
print(" " + msg)
|
logging.info(" " + msg)
|
||||||
|
|
||||||
return Printer()
|
return Printer()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# support for testing against different deltachat-rpc-server/clients
|
||||||
|
# installed into a temporary virtualenv and connected via 'execnet' channels
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
def find_path(venv, name):
|
||||||
|
is_windows = platform.system() == "Windows"
|
||||||
|
bin = venv / ("bin" if not is_windows else "Scripts")
|
||||||
|
|
||||||
|
tryadd = [""]
|
||||||
|
if is_windows:
|
||||||
|
tryadd += os.environ["PATHEXT"].split(os.pathsep)
|
||||||
|
for ext in tryadd:
|
||||||
|
p = bin.joinpath(name + ext)
|
||||||
|
if p.exists():
|
||||||
|
return str(p)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def get_core_python_env(tmp_path_factory):
|
||||||
|
"""Return a factory to create virtualenv environments with rpc server/client packages
|
||||||
|
installed.
|
||||||
|
|
||||||
|
The factory takes a version and returns a (python_path, rpc_server_path) tuple
|
||||||
|
of the respective binaries in the virtualenv.
|
||||||
|
"""
|
||||||
|
|
||||||
|
envs = {}
|
||||||
|
|
||||||
|
def get_versioned_venv(core_version):
|
||||||
|
venv = envs.get(core_version)
|
||||||
|
if not venv:
|
||||||
|
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
|
||||||
|
subprocess.check_call([sys.executable, "-m", "venv", venv])
|
||||||
|
|
||||||
|
python = find_path(venv, "python")
|
||||||
|
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}", "pytest"]
|
||||||
|
subprocess.check_call([python, "-m", "pip", "install"] + pkgs)
|
||||||
|
|
||||||
|
envs[core_version] = venv
|
||||||
|
python = find_path(venv, "python")
|
||||||
|
rpc_server_path = find_path(venv, "deltachat-rpc-server")
|
||||||
|
logging.info(f"Paths:\npython={python}\nrpc_server={rpc_server_path}")
|
||||||
|
return python, rpc_server_path
|
||||||
|
|
||||||
|
return get_versioned_venv
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
|
||||||
|
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
|
||||||
|
|
||||||
|
The 'eval' function allows to remote-execute arbitrary expressions
|
||||||
|
that can use the `bob` online account, and the `bob_contact_alice`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def factory(core_version):
|
||||||
|
python, rpc_server_path = get_core_python_env(core_version)
|
||||||
|
gw = execnet.makegateway(f"popen//python={python}")
|
||||||
|
|
||||||
|
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
|
||||||
|
channel = gw.remote_exec(remote_bob_loop)
|
||||||
|
cm = os.environ.get("CHATMAIL_DOMAIN")
|
||||||
|
|
||||||
|
# trigger getting an online account on bob's side
|
||||||
|
channel.send((accounts_dir, str(rpc_server_path), cm))
|
||||||
|
|
||||||
|
# meanwhile get a local alice account
|
||||||
|
alice = acfactory.get_online_account()
|
||||||
|
channel.send(alice.self_contact.make_vcard())
|
||||||
|
|
||||||
|
# wait for bob to have started
|
||||||
|
sysinfo = channel.receive()
|
||||||
|
assert sysinfo == f"v{core_version}"
|
||||||
|
bob_vcard = channel.receive()
|
||||||
|
[alice_contact_bob] = alice.import_vcard(bob_vcard)
|
||||||
|
|
||||||
|
def eval(eval_str):
|
||||||
|
channel.send(eval_str)
|
||||||
|
return channel.receive()
|
||||||
|
|
||||||
|
return alice, alice_contact_bob, eval
|
||||||
|
|
||||||
|
return factory
|
||||||
|
|
||||||
|
|
||||||
|
def remote_bob_loop(channel):
|
||||||
|
# This function executes with versioned
|
||||||
|
# deltachat-rpc-client/server packages
|
||||||
|
# installed into the virtualenv.
|
||||||
|
#
|
||||||
|
# The "channel" argument is a send/receive pipe
|
||||||
|
# to the process that runs the corresponding remote_exec(remote_bob_loop)
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from deltachat_rpc_client import DeltaChat, Rpc
|
||||||
|
from deltachat_rpc_client.pytestplugin import ACFactory
|
||||||
|
|
||||||
|
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
|
||||||
|
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
|
||||||
|
|
||||||
|
# older core versions don't support specifying rpc_server_path
|
||||||
|
# so we can't just pass `rpc_server_path` argument to Rpc constructor
|
||||||
|
basepath = os.path.dirname(rpc_server_path)
|
||||||
|
os.environ["PATH"] = os.pathsep.join([basepath, os.environ["PATH"]])
|
||||||
|
rpc = Rpc(accounts_dir=accounts_dir)
|
||||||
|
|
||||||
|
with rpc:
|
||||||
|
dc = DeltaChat(rpc)
|
||||||
|
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
|
||||||
|
acfactory = ACFactory(dc)
|
||||||
|
bob = acfactory.get_online_account()
|
||||||
|
alice_vcard = channel.receive()
|
||||||
|
[alice_contact] = bob.import_vcard(alice_vcard)
|
||||||
|
ns = {"bob": bob, "bob_contact_alice": alice_contact}
|
||||||
|
channel.send(bob.self_contact.make_vcard())
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
eval_str = channel.receive()
|
||||||
|
res = eval(eval_str, ns)
|
||||||
|
try:
|
||||||
|
channel.send(res)
|
||||||
|
except Exception:
|
||||||
|
# some unserializable result
|
||||||
|
channel.send(None)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from queue import Empty, Queue
|
from queue import Empty, Queue
|
||||||
from threading import Event, Thread
|
from threading import Thread
|
||||||
from typing import Any, Iterator, Optional
|
from typing import Any, Iterator, Optional
|
||||||
|
|
||||||
|
|
||||||
@@ -17,25 +17,6 @@ class JsonRpcError(Exception):
|
|||||||
"""JSON-RPC error."""
|
"""JSON-RPC error."""
|
||||||
|
|
||||||
|
|
||||||
class RpcFuture:
|
|
||||||
"""RPC future waiting for RPC call result."""
|
|
||||||
|
|
||||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
|
||||||
self.rpc = rpc
|
|
||||||
self.request_id = request_id
|
|
||||||
self.event = event
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
"""Wait for the future to return the result."""
|
|
||||||
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:
|
class RpcMethod:
|
||||||
"""RPC method."""
|
"""RPC method."""
|
||||||
|
|
||||||
@@ -57,20 +38,31 @@ class RpcMethod:
|
|||||||
"params": args,
|
"params": args,
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
}
|
}
|
||||||
event = Event()
|
self.rpc.request_results[request_id] = queue = Queue()
|
||||||
self.rpc.request_events[request_id] = event
|
|
||||||
self.rpc.request_queue.put(request)
|
self.rpc.request_queue.put(request)
|
||||||
|
|
||||||
return RpcFuture(self.rpc, request_id, event)
|
def rpc_future():
|
||||||
|
"""Wait for the request to receive a result."""
|
||||||
|
response = queue.get()
|
||||||
|
if "error" in response:
|
||||||
|
raise JsonRpcError(response["error"])
|
||||||
|
return response.get("result", None)
|
||||||
|
|
||||||
|
return rpc_future
|
||||||
|
|
||||||
|
|
||||||
class Rpc:
|
class Rpc:
|
||||||
"""RPC client."""
|
"""RPC client."""
|
||||||
|
|
||||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
accounts_dir: Optional[str] = None,
|
||||||
|
rpc_server_path="deltachat-rpc-server",
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""Initialize RPC client.
|
"""Initialize RPC client.
|
||||||
|
|
||||||
The given arguments will be passed to subprocess.Popen().
|
The 'kwargs' arguments will be passed to subprocess.Popen().
|
||||||
"""
|
"""
|
||||||
if accounts_dir:
|
if accounts_dir:
|
||||||
kwargs["env"] = {
|
kwargs["env"] = {
|
||||||
@@ -79,13 +71,12 @@ class Rpc:
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
|
self.rpc_server_path = rpc_server_path
|
||||||
self.process: subprocess.Popen
|
self.process: subprocess.Popen
|
||||||
self.id_iterator: Iterator[int]
|
self.id_iterator: Iterator[int]
|
||||||
self.event_queues: dict[int, Queue]
|
self.event_queues: dict[int, Queue]
|
||||||
# Map from request ID to `threading.Event`.
|
# Map from request ID to a Queue which provides a single result
|
||||||
self.request_events: dict[int, Event]
|
self.request_results: dict[int, Queue]
|
||||||
# Map from request ID to the result.
|
|
||||||
self.request_results: dict[int, Any]
|
|
||||||
self.request_queue: Queue[Any]
|
self.request_queue: Queue[Any]
|
||||||
self.closing: bool
|
self.closing: bool
|
||||||
self.reader_thread: Thread
|
self.reader_thread: Thread
|
||||||
@@ -93,28 +84,27 @@ class Rpc:
|
|||||||
self.events_thread: Thread
|
self.events_thread: Thread
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Start RPC server subprocess."""
|
"""Start RPC server subprocess and wait for successful initialization.
|
||||||
|
|
||||||
|
This method blocks until the RPC server responds to an initial
|
||||||
|
health-check RPC call (get_system_info).
|
||||||
|
If the server fails to start
|
||||||
|
(e.g., due to an invalid accounts directory),
|
||||||
|
a JsonRpcError is raised.
|
||||||
|
"""
|
||||||
|
popen_kwargs = {"stdin": subprocess.PIPE, "stdout": subprocess.PIPE, "stderr": subprocess.PIPE}
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
self.process = subprocess.Popen(
|
# Prevent subprocess from capturing SIGINT.
|
||||||
"deltachat-rpc-server",
|
popen_kwargs["process_group"] = 0
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
# Prevent subprocess from capturing SIGINT.
|
|
||||||
process_group=0,
|
|
||||||
**self._kwargs,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.process = subprocess.Popen(
|
# `process_group` is not supported before Python 3.11.
|
||||||
"deltachat-rpc-server",
|
popen_kwargs["preexec_fn"] = os.setpgrp # noqa: PLW1509
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
popen_kwargs.update(self._kwargs)
|
||||||
# `process_group` is not supported before Python 3.11.
|
self.process = subprocess.Popen(self.rpc_server_path, **popen_kwargs)
|
||||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
|
||||||
**self._kwargs,
|
|
||||||
)
|
|
||||||
self.id_iterator = itertools.count(start=1)
|
self.id_iterator = itertools.count(start=1)
|
||||||
self.event_queues = {}
|
self.event_queues = {}
|
||||||
self.request_events = {}
|
|
||||||
self.request_results = {}
|
self.request_results = {}
|
||||||
self.request_queue = Queue()
|
self.request_queue = Queue()
|
||||||
self.closing = False
|
self.closing = False
|
||||||
@@ -125,6 +115,22 @@ class Rpc:
|
|||||||
self.events_thread = Thread(target=self.events_loop)
|
self.events_thread = Thread(target=self.events_loop)
|
||||||
self.events_thread.start()
|
self.events_thread.start()
|
||||||
|
|
||||||
|
# Perform a health-check RPC call to ensure the server started
|
||||||
|
# successfully and the accounts directory is usable.
|
||||||
|
try:
|
||||||
|
system_info = self.get_system_info()
|
||||||
|
except (JsonRpcError, Exception) as e:
|
||||||
|
# The reader_loop already saw EOF on stdout, so the process
|
||||||
|
# has exited and stderr is available.
|
||||||
|
stderr = self.process.stderr.read().decode(errors="replace").strip()
|
||||||
|
if stderr:
|
||||||
|
raise JsonRpcError(f"RPC server failed to start: {stderr}") from e
|
||||||
|
raise JsonRpcError(f"RPC server startup check failed: {e}") from e
|
||||||
|
logging.info(
|
||||||
|
"RPC server ready. Core version: %s",
|
||||||
|
system_info.get("deltachat_core_version", "unknown"),
|
||||||
|
)
|
||||||
|
|
||||||
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.closing = True
|
self.closing = True
|
||||||
@@ -149,14 +155,16 @@ class Rpc:
|
|||||||
response = json.loads(line)
|
response = json.loads(line)
|
||||||
if "id" in response:
|
if "id" in response:
|
||||||
response_id = response["id"]
|
response_id = response["id"]
|
||||||
event = self.request_events.pop(response_id)
|
self.request_results.pop(response_id).put(response)
|
||||||
self.request_results[response_id] = response
|
|
||||||
event.set()
|
|
||||||
else:
|
else:
|
||||||
logging.warning("Got a response without ID: %s", response)
|
logging.warning("Got a response without ID: %s", response)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Log an exception if the reader loop dies.
|
# Log an exception if the reader loop dies.
|
||||||
logging.exception("Exception in the reader loop")
|
logging.exception("Exception in the reader loop")
|
||||||
|
finally:
|
||||||
|
# Unblock any pending requests when the server closes stdout.
|
||||||
|
for _request_id, queue in self.request_results.items():
|
||||||
|
queue.put({"error": {"code": -32000, "message": "RPC server closed"}})
|
||||||
|
|
||||||
def writer_loop(self) -> None:
|
def writer_loop(self) -> None:
|
||||||
"""Writer loop ensuring only a single thread writes requests."""
|
"""Writer loop ensuring only a single thread writes requests."""
|
||||||
@@ -165,7 +173,6 @@ class Rpc:
|
|||||||
data = (json.dumps(request) + "\n").encode()
|
data = (json.dumps(request) + "\n").encode()
|
||||||
self.process.stdin.write(data)
|
self.process.stdin.write(data)
|
||||||
self.process.stdin.flush()
|
self.process.stdin.flush()
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# Log an exception if the writer loop dies.
|
# Log an exception if the writer loop dies.
|
||||||
logging.exception("Exception in the writer loop")
|
logging.exception("Exception in the writer loop")
|
||||||
@@ -179,15 +186,15 @@ class Rpc:
|
|||||||
def events_loop(self) -> None:
|
def events_loop(self) -> None:
|
||||||
"""Request new events and distributes them between queues."""
|
"""Request new events and distributes them between queues."""
|
||||||
try:
|
try:
|
||||||
while True:
|
while events := self.get_next_event_batch():
|
||||||
|
for event in events:
|
||||||
|
account_id = event["contextId"]
|
||||||
|
queue = self.get_queue(account_id)
|
||||||
|
payload = event["event"]
|
||||||
|
logging.debug("account_id=%d got an event %s", account_id, payload)
|
||||||
|
queue.put(payload)
|
||||||
if self.closing:
|
if self.closing:
|
||||||
return
|
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:
|
except Exception:
|
||||||
# Log an exception if the event loop dies.
|
# Log an exception if the event loop dies.
|
||||||
logging.exception("Exception in the event loop")
|
logging.exception("Exception in the event loop")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import imaplib
|
import imaplib
|
||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
import pathlib
|
import pathlib
|
||||||
import ssl
|
import ssl
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
@@ -45,13 +46,13 @@ class DirectImap:
|
|||||||
try:
|
try:
|
||||||
self.conn.logout()
|
self.conn.logout()
|
||||||
except (OSError, imaplib.IMAP4.abort):
|
except (OSError, imaplib.IMAP4.abort):
|
||||||
print("Could not logout direct_imap conn")
|
logging.warning("Could not logout direct_imap conn")
|
||||||
|
|
||||||
def create_folder(self, foldername):
|
def create_folder(self, foldername):
|
||||||
try:
|
try:
|
||||||
self.conn.folder.create(foldername)
|
self.conn.folder.create(foldername)
|
||||||
except errors.MailboxFolderCreateError as e:
|
except errors.MailboxFolderCreateError as e:
|
||||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
logging.warning(f"Cannot create '{foldername}', probably it already exists: {str(e)}")
|
||||||
|
|
||||||
def select_folder(self, foldername: str) -> tuple:
|
def select_folder(self, foldername: str) -> tuple:
|
||||||
assert not self._idling
|
assert not self._idling
|
||||||
@@ -85,17 +86,17 @@ class DirectImap:
|
|||||||
|
|
||||||
def get_all_messages(self) -> list[MailMessage]:
|
def get_all_messages(self) -> list[MailMessage]:
|
||||||
assert not self._idling
|
assert not self._idling
|
||||||
return list(self.conn.fetch())
|
return list(self.conn.fetch(mark_seen=False))
|
||||||
|
|
||||||
def get_unread_messages(self) -> list[str]:
|
def get_unread_messages(self) -> list[str]:
|
||||||
assert not self._idling
|
assert not self._idling
|
||||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
return [msg.uid for msg in self.conn.fetch(AND(seen=False), mark_seen=False)]
|
||||||
|
|
||||||
def mark_all_read(self):
|
def mark_all_read(self):
|
||||||
messages = self.get_unread_messages()
|
messages = self.get_unread_messages()
|
||||||
if messages:
|
if messages:
|
||||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||||
print("marked seen:", messages, res)
|
logging.info(f"Marked seen: {messages} {res}")
|
||||||
|
|
||||||
def get_unread_cnt(self) -> int:
|
def get_unread_cnt(self) -> int:
|
||||||
return len(self.get_unread_messages())
|
return len(self.get_unread_messages())
|
||||||
@@ -173,7 +174,6 @@ class DirectImap:
|
|||||||
class IdleManager:
|
class IdleManager:
|
||||||
def __init__(self, direct_imap) -> None:
|
def __init__(self, direct_imap) -> None:
|
||||||
self.direct_imap = direct_imap
|
self.direct_imap = direct_imap
|
||||||
self.log = direct_imap.account.log
|
|
||||||
# fetch latest messages before starting idle so that it only
|
# fetch latest messages before starting idle so that it only
|
||||||
# returns messages that arrive anew
|
# returns messages that arrive anew
|
||||||
self.direct_imap.conn.fetch("1:*")
|
self.direct_imap.conn.fetch("1:*")
|
||||||
@@ -181,14 +181,11 @@ class IdleManager:
|
|||||||
|
|
||||||
def check(self, timeout=None) -> list[bytes]:
|
def check(self, timeout=None) -> list[bytes]:
|
||||||
"""(blocking) wait for next idle message from server."""
|
"""(blocking) wait for next idle message from server."""
|
||||||
self.log("imap-direct: calling idle_check")
|
return self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||||
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:
|
def wait_for_new_message(self) -> bytes:
|
||||||
while True:
|
while True:
|
||||||
for item in self.check(timeout=timeout):
|
for item in self.check():
|
||||||
if b"EXISTS" in item or b"RECENT" in item:
|
if b"EXISTS" in item or b"RECENT" in item:
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@@ -196,10 +193,8 @@ class IdleManager:
|
|||||||
"""Return first message with SEEN flag from a running idle-stream."""
|
"""Return first message with SEEN flag from a running idle-stream."""
|
||||||
while True:
|
while True:
|
||||||
for item in self.check(timeout=timeout):
|
for item in self.check(timeout=timeout):
|
||||||
if FETCH in item:
|
if FETCH in item and FLAGS in item and rb"\Seen" in item:
|
||||||
self.log(str(item))
|
return int(item.split(b" ")[1])
|
||||||
if FLAGS in item and rb"\Seen" in item:
|
|
||||||
return int(item.split(b" ")[1])
|
|
||||||
|
|
||||||
def done(self):
|
def done(self):
|
||||||
"""send idle-done to server if we are currently in idle mode."""
|
"""send idle-done to server if we are currently in idle mode."""
|
||||||
|
|||||||
146
deltachat-rpc-client/tests/test_calls.py
Normal file
146
deltachat-rpc-client/tests/test_calls.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from deltachat_rpc_client import EventType, Message
|
||||||
|
|
||||||
|
|
||||||
|
def test_calls(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
place_call_info = "offer"
|
||||||
|
accept_call_info = "answer"
|
||||||
|
|
||||||
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||||
|
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info, has_video_initially=True)
|
||||||
|
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
|
||||||
|
|
||||||
|
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||||
|
assert incoming_call_event.place_call_info == place_call_info
|
||||||
|
assert incoming_call_event.has_video
|
||||||
|
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||||
|
assert incoming_call_message.get_call_info().state.kind == "Alerting"
|
||||||
|
assert incoming_call_message.get_call_info().has_video
|
||||||
|
|
||||||
|
incoming_call_message.accept_incoming_call(accept_call_info)
|
||||||
|
assert incoming_call_message.get_call_info().sdp_offer == place_call_info
|
||||||
|
assert incoming_call_message.get_call_info().state.kind == "Active"
|
||||||
|
outgoing_call_accepted_event = alice.wait_for_event(EventType.OUTGOING_CALL_ACCEPTED)
|
||||||
|
assert outgoing_call_accepted_event.accept_call_info == accept_call_info
|
||||||
|
assert outgoing_call_message.get_call_info().state.kind == "Active"
|
||||||
|
|
||||||
|
outgoing_call_message.end_call()
|
||||||
|
assert outgoing_call_message.get_call_info().state.kind == "Completed"
|
||||||
|
|
||||||
|
end_call_event = bob.wait_for_event(EventType.CALL_ENDED)
|
||||||
|
assert end_call_event.msg_id == outgoing_call_message.id
|
||||||
|
assert incoming_call_message.get_call_info().state.kind == "Completed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_video_call(acfactory) -> None:
|
||||||
|
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
|
||||||
|
# with `s= ` replaced with `s=-`.
|
||||||
|
#
|
||||||
|
# `s=` cannot be empty according to RFC 3264,
|
||||||
|
# so it is more clear as `s=-`.
|
||||||
|
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||||
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||||
|
|
||||||
|
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||||
|
assert incoming_call_event.place_call_info == "offer"
|
||||||
|
assert incoming_call_event.has_video
|
||||||
|
|
||||||
|
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||||
|
assert incoming_call_message.get_call_info().has_video
|
||||||
|
|
||||||
|
|
||||||
|
def test_audio_call(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob.create_chat(alice) # Accept the chat so incoming call causes a notification.
|
||||||
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
alice_chat_bob.place_outgoing_call("offer", has_video_initially=False)
|
||||||
|
|
||||||
|
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||||
|
assert incoming_call_event.place_call_info == "offer"
|
||||||
|
assert not incoming_call_event.has_video
|
||||||
|
|
||||||
|
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||||
|
assert not incoming_call_message.get_call_info().has_video
|
||||||
|
|
||||||
|
|
||||||
|
def test_ice_servers(acfactory) -> None:
|
||||||
|
alice = acfactory.get_online_account()
|
||||||
|
|
||||||
|
ice_servers = alice.ice_servers()
|
||||||
|
assert len(ice_servers) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_contact_request_call(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||||
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
|
# Notification for "Hello!" message should arrive
|
||||||
|
# without the call ringing.
|
||||||
|
while True:
|
||||||
|
event = bob.wait_for_event()
|
||||||
|
|
||||||
|
# There should be no incoming call notification.
|
||||||
|
assert event.kind != EventType.INCOMING_CALL
|
||||||
|
|
||||||
|
if event.kind == EventType.MSGS_CHANGED:
|
||||||
|
msg = bob.get_message_by_id(event.msg_id)
|
||||||
|
if msg.get_snapshot().text == "Hello!":
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def test_who_can_call_me_nobody(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
# Bob sets "who can call me" to "nobody" (2)
|
||||||
|
bob.set_config("who_can_call_me", "2")
|
||||||
|
|
||||||
|
# Bob even accepts Alice in advance so the chat does not appear as contact request.
|
||||||
|
bob.create_chat(alice)
|
||||||
|
|
||||||
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||||
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
|
# Notification for "Hello!" message should arrive
|
||||||
|
# without the call ringing.
|
||||||
|
while True:
|
||||||
|
event = bob.wait_for_event()
|
||||||
|
|
||||||
|
# There should be no incoming call notification.
|
||||||
|
assert event.kind != EventType.INCOMING_CALL
|
||||||
|
|
||||||
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
|
msg = bob.get_message_by_id(event.msg_id)
|
||||||
|
if msg.get_snapshot().text == "Hello!":
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def test_who_can_call_me_everybody(acfactory) -> None:
|
||||||
|
"""Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
# Bob sets "who can call me" to "nobody" (0)
|
||||||
|
bob.set_config("who_can_call_me", "0")
|
||||||
|
|
||||||
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
alice_chat_bob.place_outgoing_call("offer", has_video_initially=True)
|
||||||
|
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
|
||||||
|
|
||||||
|
incoming_call_message = Message(bob, incoming_call_event.msg_id)
|
||||||
|
|
||||||
|
# Even with the call arriving, the chat is still in the contact request mode.
|
||||||
|
incoming_chat = incoming_call_message.get_snapshot().chat
|
||||||
|
assert incoming_chat.get_basic_snapshot().is_contact_request
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from deltachat_rpc_client import Account, EventType, const
|
from deltachat_rpc_client import Account, EventType, const
|
||||||
@@ -129,7 +127,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
|||||||
msg.get_snapshot().chat.accept()
|
msg.get_snapshot().chat.accept()
|
||||||
bob.get_chat_by_id(chat_id).send_message(
|
bob.get_chat_by_id(chat_id).send_message(
|
||||||
"Hello World, this message is bigger than 5 bytes",
|
"Hello World, this message is bigger than 5 bytes",
|
||||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
file="../test-data/image/screenshot.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
message = alice.wait_for_incoming_msg()
|
message = alice.wait_for_incoming_msg()
|
||||||
@@ -169,6 +167,8 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
|||||||
"""
|
"""
|
||||||
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
|
||||||
|
|
||||||
|
bob.create_chat(alice)
|
||||||
|
|
||||||
alice_chat_bob.send_text("hello")
|
alice_chat_bob.send_text("hello")
|
||||||
|
|
||||||
msg = bob.wait_for_incoming_msg()
|
msg = bob.wait_for_incoming_msg()
|
||||||
|
|||||||
57
deltachat-rpc-client/tests/test_cross_core.py
Normal file
57
deltachat-rpc-client/tests/test_cross_core.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from deltachat_rpc_client import DeltaChat, Rpc
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
|
||||||
|
python, rpc_server_path = get_core_python_env("2.24.0")
|
||||||
|
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.24.0"])
|
||||||
|
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=rpc_server_path)
|
||||||
|
|
||||||
|
with rpc:
|
||||||
|
dc = DeltaChat(rpc)
|
||||||
|
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.24.0"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("version", ["2.24.0"])
|
||||||
|
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
|
||||||
|
"""Test other-core Bob profile can do securejoin with Alice on current core."""
|
||||||
|
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||||
|
|
||||||
|
qr_code = alice.get_qr_code()
|
||||||
|
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||||
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
|
||||||
|
# Test that Alice verified Bob's profile.
|
||||||
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
|
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||||
|
|
||||||
|
# Test that Bob verified Alice's profile.
|
||||||
|
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_and_receive_message(alice_and_remote_bob) -> None:
|
||||||
|
"""Test other-core Bob profile can send a message to Alice on current core."""
|
||||||
|
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||||
|
|
||||||
|
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
|
||||||
|
|
||||||
|
msg = alice.wait_for_incoming_msg()
|
||||||
|
assert msg.get_snapshot().text == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_device(acfactory, alice_and_remote_bob) -> None:
|
||||||
|
"""Test setting up current version as a second device for old version."""
|
||||||
|
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
|
||||||
|
|
||||||
|
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
|
||||||
|
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")
|
||||||
|
new_account = acfactory.get_unconfigured_account()
|
||||||
|
new_account._rpc.get_backup(new_account.id, qr)
|
||||||
|
remote_eval("locals()['future']()")
|
||||||
|
|
||||||
|
assert new_account.get_config("addr") == remote_eval("bob.get_config('addr')")
|
||||||
125
deltachat-rpc-client/tests/test_folders.py
Normal file
125
deltachat-rpc-client/tests/test_folders.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from imap_tools import AND, U
|
||||||
|
|
||||||
|
from deltachat_rpc_client import EventType
|
||||||
|
|
||||||
|
|
||||||
|
def test_moved_markseen(acfactory, direct_imap, log):
|
||||||
|
"""Test that message already moved to DeltaChat folder is marked as seen."""
|
||||||
|
ac1 = acfactory.get_online_account()
|
||||||
|
|
||||||
|
addr, password = acfactory.get_credentials()
|
||||||
|
ac2 = acfactory.get_unconfigured_account()
|
||||||
|
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||||
|
ac2.bring_online()
|
||||||
|
|
||||||
|
log.section("ac2: creating DeltaChat folder")
|
||||||
|
ac2_direct_imap = direct_imap(ac2)
|
||||||
|
ac2_direct_imap.create_folder("DeltaChat")
|
||||||
|
ac2.set_config("delete_server_after", "0")
|
||||||
|
ac2.set_config("sync_msgs", "0") # Do not send a sync message when accepting a contact request.
|
||||||
|
|
||||||
|
ac2.add_or_update_transport({"addr": addr, "password": password, "imapFolder": "DeltaChat"})
|
||||||
|
ac2.bring_online()
|
||||||
|
|
||||||
|
ac2.stop_io()
|
||||||
|
ac2_direct_imap = direct_imap(ac2)
|
||||||
|
with ac2_direct_imap.idle() as idle2:
|
||||||
|
ac1.create_chat(ac2).send_text("Hello!")
|
||||||
|
idle2.wait_for_new_message()
|
||||||
|
|
||||||
|
# Emulate moving of the message to DeltaChat folder by Sieve rule.
|
||||||
|
log.section("ac2: moving message into DeltaChat folder")
|
||||||
|
ac2_direct_imap.conn.move(["*"], "DeltaChat")
|
||||||
|
ac2_direct_imap.select_folder("DeltaChat")
|
||||||
|
assert len(list(ac2_direct_imap.conn.fetch("*", mark_seen=False))) == 1
|
||||||
|
|
||||||
|
with ac2_direct_imap.idle() as idle2:
|
||||||
|
ac2.start_io()
|
||||||
|
|
||||||
|
ev = ac2.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
|
msg = ac2.get_message_by_id(ev.msg_id)
|
||||||
|
assert msg.get_snapshot().text == "Messages are end-to-end encrypted."
|
||||||
|
|
||||||
|
ev = ac2.wait_for_event(EventType.INCOMING_MSG)
|
||||||
|
msg = ac2.get_message_by_id(ev.msg_id)
|
||||||
|
chat = ac2.get_chat_by_id(ev.chat_id)
|
||||||
|
|
||||||
|
# Accept the contact request.
|
||||||
|
chat.accept()
|
||||||
|
msg.mark_seen()
|
||||||
|
idle2.wait_for_seen()
|
||||||
|
|
||||||
|
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True, uid=U(1, "*")), mark_seen=False))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_markseen_message_and_mdn(acfactory, direct_imap):
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
for ac in ac1, ac2:
|
||||||
|
ac.set_config("delete_server_after", "0")
|
||||||
|
|
||||||
|
# Do not send BCC to self, we only want to test MDN on ac1.
|
||||||
|
ac1.set_config("bcc_self", "0")
|
||||||
|
|
||||||
|
acfactory.get_accepted_chat(ac1, ac2).send_text("hi")
|
||||||
|
msg = ac2.wait_for_incoming_msg()
|
||||||
|
msg.mark_seen()
|
||||||
|
|
||||||
|
rex = re.compile("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||||
|
|
||||||
|
for ac in ac1, ac2:
|
||||||
|
while True:
|
||||||
|
event = ac.wait_for_event()
|
||||||
|
if event.kind == EventType.INFO and rex.search(event.msg):
|
||||||
|
break
|
||||||
|
|
||||||
|
ac1_direct_imap = direct_imap(ac1)
|
||||||
|
ac2_direct_imap = direct_imap(ac2)
|
||||||
|
|
||||||
|
ac1_direct_imap.select_folder("INBOX")
|
||||||
|
ac2_direct_imap.select_folder("INBOX")
|
||||||
|
|
||||||
|
# Check that the mdn is marked as seen
|
||||||
|
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||||
|
# Check original message is marked as seen
|
||||||
|
assert len(list(ac2_direct_imap.conn.fetch(AND(seen=True), mark_seen=False))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_trash_multiple_messages(acfactory, direct_imap, log):
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac2.stop_io()
|
||||||
|
|
||||||
|
ac2.set_config("delete_server_after", "0")
|
||||||
|
ac2.set_config("sync_msgs", "0")
|
||||||
|
|
||||||
|
ac2.start_io()
|
||||||
|
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||||
|
|
||||||
|
log.section("ac1: sending 3 messages")
|
||||||
|
texts = ["first", "second", "third"]
|
||||||
|
for text in texts:
|
||||||
|
chat12.send_text(text)
|
||||||
|
|
||||||
|
log.section("ac2: waiting for all messages on the other side")
|
||||||
|
to_delete = []
|
||||||
|
for text in texts:
|
||||||
|
msg = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert msg.text in texts
|
||||||
|
if text != "second":
|
||||||
|
to_delete.append(msg)
|
||||||
|
|
||||||
|
log.section("ac2: deleting all messages except second")
|
||||||
|
assert len(to_delete) == len(texts) - 1
|
||||||
|
ac2.delete_messages(to_delete)
|
||||||
|
|
||||||
|
log.section("ac2: test that only one message is left")
|
||||||
|
ac2_direct_imap = direct_imap(ac2)
|
||||||
|
while 1:
|
||||||
|
ac2.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||||
|
ac2_direct_imap.select_config_folder("inbox")
|
||||||
|
nr_msgs = len(ac2_direct_imap.get_all_messages())
|
||||||
|
assert nr_msgs > 0
|
||||||
|
if nr_msgs == 1:
|
||||||
|
break
|
||||||
@@ -24,6 +24,13 @@ def path_to_webxdc(request):
|
|||||||
return str(p)
|
return str(p)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def path_to_large_webxdc(request):
|
||||||
|
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/realtime-check.xdc")
|
||||||
|
assert p.exists()
|
||||||
|
return str(p)
|
||||||
|
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
logging.info(msg)
|
logging.info(msg)
|
||||||
|
|
||||||
@@ -84,7 +91,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
|||||||
|
|
||||||
# share a webxdc app between ac1 and ac2
|
# share a webxdc app between ac1 and ac2
|
||||||
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||||
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||||
assert snapshot.text == "play"
|
assert snapshot.text == "play"
|
||||||
|
|
||||||
@@ -94,7 +101,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
|||||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||||
|
|
||||||
log("waiting for incoming message on ac2")
|
log("waiting for incoming message on ac2")
|
||||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "ping1"
|
assert snapshot.text == "ping1"
|
||||||
|
|
||||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||||
@@ -102,7 +109,7 @@ def test_realtime_sequentially(acfactory, path_to_webxdc):
|
|||||||
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||||
|
|
||||||
log("waiting for incoming message on ac1")
|
log("waiting for incoming message on ac1")
|
||||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "ping2"
|
assert snapshot.text == "ping2"
|
||||||
|
|
||||||
log("sending realtime data ac1 -> ac2")
|
log("sending realtime data ac1 -> ac2")
|
||||||
@@ -214,7 +221,9 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
|||||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
|
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="WebXDC", file=path_to_webxdc)
|
||||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||||
assert ac2_webxdc_msg.get_snapshot().text == "WebXDC"
|
ac2_webxdc_msg_snapshot = ac2_webxdc_msg.get_snapshot()
|
||||||
|
assert ac2_webxdc_msg_snapshot.text == "WebXDC"
|
||||||
|
ac2_webxdc_msg_snapshot.chat.accept()
|
||||||
|
|
||||||
ac1_ac2_chat.send_text("Hello!")
|
ac1_ac2_chat.send_text("Hello!")
|
||||||
ac2_hello_msg = ac2.wait_for_incoming_msg()
|
ac2_hello_msg = ac2.wait_for_incoming_msg()
|
||||||
@@ -225,3 +234,29 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
|||||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||||
assert event.msg_id == ac1_webxdc_msg.id
|
assert event.msg_id == ac1_webxdc_msg.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_realtime_large_webxdc(acfactory, path_to_large_webxdc):
|
||||||
|
"""Tests initializing realtime channel on a large webxdc.
|
||||||
|
|
||||||
|
This is a regression test for a bug that existed in version 2.42.0.
|
||||||
|
Large webxdc is split into pre- and post- message,
|
||||||
|
and this previously resulted in failure to initialize realtime.
|
||||||
|
"""
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||||
|
|
||||||
|
ac2.create_chat(ac1)
|
||||||
|
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||||
|
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="realtime check", file=path_to_large_webxdc)
|
||||||
|
|
||||||
|
# Receive pre-message.
|
||||||
|
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||||
|
|
||||||
|
# Receive post-message.
|
||||||
|
ac2_webxdc_msg = ac2.wait_for_msg(EventType.MSGS_CHANGED)
|
||||||
|
|
||||||
|
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||||
|
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||||
|
assert event.msg_id == ac1_webxdc_msg.id
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from deltachat_rpc_client import EventType
|
|
||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_autocrypt_setup_message(account):
|
|
||||||
while True:
|
|
||||||
event = account.wait_for_event()
|
|
||||||
if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0:
|
|
||||||
msg_id = event.msg_id
|
|
||||||
msg = account.get_message_by_id(msg_id)
|
|
||||||
if msg.get_snapshot().is_setupmessage:
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def test_autocrypt_setup_message_key_transfer(acfactory):
|
|
||||||
alice1 = acfactory.get_online_account()
|
|
||||||
|
|
||||||
alice2 = acfactory.get_unconfigured_account()
|
|
||||||
alice2.set_config("addr", alice1.get_config("addr"))
|
|
||||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
|
||||||
alice2.configure()
|
|
||||||
alice2.bring_online()
|
|
||||||
|
|
||||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
|
||||||
msg = wait_for_autocrypt_setup_message(alice2)
|
|
||||||
|
|
||||||
# Test that entering wrong code returns an error.
|
|
||||||
with pytest.raises(JsonRpcError):
|
|
||||||
msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756")
|
|
||||||
|
|
||||||
msg.continue_autocrypt_key_transfer(setup_code)
|
|
||||||
|
|
||||||
|
|
||||||
def test_ac_setup_message_twice(acfactory):
|
|
||||||
alice1 = acfactory.get_online_account()
|
|
||||||
|
|
||||||
alice2 = acfactory.get_unconfigured_account()
|
|
||||||
alice2.set_config("addr", alice1.get_config("addr"))
|
|
||||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
|
||||||
alice2.configure()
|
|
||||||
alice2.bring_online()
|
|
||||||
|
|
||||||
# Send the first Autocrypt Setup Message and ignore it.
|
|
||||||
_setup_code = alice1.initiate_autocrypt_key_transfer()
|
|
||||||
wait_for_autocrypt_setup_message(alice2)
|
|
||||||
|
|
||||||
# Send the second Autocrypt Setup Message and import it.
|
|
||||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
|
||||||
msg = wait_for_autocrypt_setup_message(alice2)
|
|
||||||
|
|
||||||
msg.continue_autocrypt_key_transfer(setup_code)
|
|
||||||
32
deltachat-rpc-client/tests/test_location.py
Normal file
32
deltachat-rpc-client/tests/test_location.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
def test_set_location(dc, acfactory) -> None:
|
||||||
|
# Try setting location without any accounts.
|
||||||
|
assert not dc.set_location(1.0, 2.0, 0.1)
|
||||||
|
|
||||||
|
# Create one account that does not stream,
|
||||||
|
# set location.
|
||||||
|
acfactory.new_configured_account()
|
||||||
|
assert not dc.set_location(3.0, 4.0, 0.1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_locations_to_chat(dc, acfactory):
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
assert not alice.is_sending_locations()
|
||||||
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
assert not alice_chat_bob.is_sending_locations()
|
||||||
|
|
||||||
|
# Test starting and stopping location streaming in a chat.
|
||||||
|
alice_chat_bob.send_locations(3600)
|
||||||
|
assert alice.is_sending_locations()
|
||||||
|
assert alice_chat_bob.is_sending_locations()
|
||||||
|
alice_chat_bob.send_locations(0)
|
||||||
|
assert not alice.is_sending_locations()
|
||||||
|
assert not alice_chat_bob.is_sending_locations()
|
||||||
|
|
||||||
|
# Test stop_sending_locations() for all accounts and chats.
|
||||||
|
alice_chat_bob.send_locations(3600)
|
||||||
|
assert alice.is_sending_locations()
|
||||||
|
assert alice_chat_bob.is_sending_locations()
|
||||||
|
dc.stop_sending_locations()
|
||||||
|
assert not alice.is_sending_locations()
|
||||||
|
assert not alice_chat_bob.is_sending_locations()
|
||||||
@@ -4,6 +4,41 @@ from deltachat_rpc_client import EventType
|
|||||||
from deltachat_rpc_client.const import MessageState
|
from deltachat_rpc_client.const import MessageState
|
||||||
|
|
||||||
|
|
||||||
|
def test_bcc_self_delete_server_after_defaults(acfactory):
|
||||||
|
"""Test default values for bcc_self and delete_server_after."""
|
||||||
|
ac = acfactory.get_online_account()
|
||||||
|
|
||||||
|
# Initially after getting online
|
||||||
|
# the setting bcc_self is set to 0 because there is only one device
|
||||||
|
# and delete_server_after is "1", meaning immediate deletion.
|
||||||
|
assert ac.get_config("bcc_self") == "0"
|
||||||
|
assert ac.get_config("delete_server_after") == "1"
|
||||||
|
|
||||||
|
# Setup a second device.
|
||||||
|
ac_clone = ac.clone()
|
||||||
|
ac_clone.bring_online()
|
||||||
|
|
||||||
|
# Second device setup
|
||||||
|
# enables bcc_self and changes default delete_server_after.
|
||||||
|
assert ac.get_config("bcc_self") == "1"
|
||||||
|
assert ac.get_config("delete_server_after") == "0"
|
||||||
|
|
||||||
|
assert ac_clone.get_config("bcc_self") == "1"
|
||||||
|
assert ac_clone.get_config("delete_server_after") == "0"
|
||||||
|
|
||||||
|
# Manually disabling bcc_self
|
||||||
|
# also restores the default for delete_server_after.
|
||||||
|
ac.set_config("bcc_self", "0")
|
||||||
|
assert ac.get_config("bcc_self") == "0"
|
||||||
|
assert ac.get_config("delete_server_after") == "1"
|
||||||
|
|
||||||
|
# Cloning the account again enables bcc_self
|
||||||
|
# even though it was manually disabled.
|
||||||
|
ac_clone = ac.clone()
|
||||||
|
assert ac.get_config("bcc_self") == "1"
|
||||||
|
assert ac.get_config("delete_server_after") == "0"
|
||||||
|
|
||||||
|
|
||||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_clone = ac1.clone()
|
ac1_clone = ac1.clone()
|
||||||
@@ -36,6 +71,9 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
|||||||
assert ac1.get_config("bcc_self") == "1"
|
assert ac1.get_config("bcc_self") == "1"
|
||||||
|
|
||||||
# Second client receives only second message, but not the first.
|
# Second client receives only second message, but not the first.
|
||||||
|
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
|
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
|
||||||
|
|
||||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||||
|
|
||||||
|
|||||||
313
deltachat-rpc-client/tests/test_multitransport.py
Normal file
313
deltachat-rpc-client/tests/test_multitransport.py
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from deltachat_rpc_client import EventType
|
||||||
|
from deltachat_rpc_client.const import ChatType, DownloadState
|
||||||
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_second_address(acfactory) -> None:
|
||||||
|
account = acfactory.new_configured_account()
|
||||||
|
assert len(account.list_transports()) == 1
|
||||||
|
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
account.add_transport_from_qr(qr)
|
||||||
|
assert len(account.list_transports()) == 2
|
||||||
|
|
||||||
|
account.add_transport_from_qr(qr)
|
||||||
|
assert len(account.list_transports()) == 3
|
||||||
|
|
||||||
|
first_addr = account.list_transports()[0]["addr"]
|
||||||
|
second_addr = account.list_transports()[1]["addr"]
|
||||||
|
|
||||||
|
# Cannot delete the first address.
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
account.delete_transport(first_addr)
|
||||||
|
|
||||||
|
account.delete_transport(second_addr)
|
||||||
|
assert len(account.list_transports()) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_address(acfactory) -> None:
|
||||||
|
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_addr = bob.get_config("configured_addr")
|
||||||
|
bob.create_chat(alice)
|
||||||
|
|
||||||
|
alice_chat_bob = alice.create_chat(bob)
|
||||||
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
|
msg1 = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
|
sender_addr1 = msg1.sender.get_snapshot().address
|
||||||
|
|
||||||
|
alice.stop_io()
|
||||||
|
old_alice_addr = alice.get_config("configured_addr")
|
||||||
|
alice_vcard = alice.self_contact.make_vcard()
|
||||||
|
assert old_alice_addr in alice_vcard
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
alice.add_transport_from_qr(qr)
|
||||||
|
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
# Cannot use the address that is not
|
||||||
|
# configured for any transport.
|
||||||
|
alice.set_config("configured_addr", bob_addr)
|
||||||
|
|
||||||
|
# Load old address so it is cached.
|
||||||
|
assert alice.get_config("configured_addr") == old_alice_addr
|
||||||
|
alice.set_config("configured_addr", new_alice_addr)
|
||||||
|
# Make sure that setting `configured_addr` invalidated the cache.
|
||||||
|
assert alice.get_config("configured_addr") == new_alice_addr
|
||||||
|
|
||||||
|
alice_vcard = alice.self_contact.make_vcard()
|
||||||
|
assert old_alice_addr not in alice_vcard
|
||||||
|
assert new_alice_addr in alice_vcard
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
alice.delete_transport(new_alice_addr)
|
||||||
|
alice.start_io()
|
||||||
|
|
||||||
|
alice_chat_bob.send_text("Hello again!")
|
||||||
|
|
||||||
|
msg2 = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
|
sender_addr2 = msg2.sender.get_snapshot().address
|
||||||
|
|
||||||
|
assert msg1.sender == msg2.sender
|
||||||
|
assert sender_addr1 != sender_addr2
|
||||||
|
assert sender_addr1 == old_alice_addr
|
||||||
|
assert sender_addr2 == new_alice_addr
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_on_demand(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
alice.set_config("download_limit", "1")
|
||||||
|
|
||||||
|
alice.stop_io()
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
alice.add_transport_from_qr(qr)
|
||||||
|
alice.start_io()
|
||||||
|
|
||||||
|
alice.create_chat(bob)
|
||||||
|
chat_bob_alice = bob.create_chat(alice)
|
||||||
|
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
|
||||||
|
msg = alice.wait_for_incoming_msg()
|
||||||
|
snapshot = msg.get_snapshot()
|
||||||
|
assert snapshot.download_state == DownloadState.AVAILABLE
|
||||||
|
chat_id = snapshot.chat_id
|
||||||
|
# Actually the message isn't available yet. Wait somehow for the post-message to arrive.
|
||||||
|
chat_bob_alice.send_message("Now you can download my previous message")
|
||||||
|
alice.wait_for_incoming_msg()
|
||||||
|
alice._rpc.download_full_message(alice.id, msg.id)
|
||||||
|
for dstate in [DownloadState.IN_PROGRESS, DownloadState.DONE]:
|
||||||
|
event = alice.wait_for_event(EventType.MSGS_CHANGED)
|
||||||
|
assert event.chat_id == chat_id
|
||||||
|
assert event.msg_id == msg.id
|
||||||
|
assert msg.get_snapshot().download_state == dstate
|
||||||
|
|
||||||
|
|
||||||
|
def test_reconfigure_transport(acfactory) -> None:
|
||||||
|
"""Test that reconfiguring the transport works."""
|
||||||
|
account = acfactory.get_online_account()
|
||||||
|
|
||||||
|
[transport] = account.list_transports()
|
||||||
|
account.add_or_update_transport(transport)
|
||||||
|
|
||||||
|
|
||||||
|
def test_transport_synchronization(acfactory, log) -> None:
|
||||||
|
"""Test synchronization of transports between devices."""
|
||||||
|
|
||||||
|
def wait_for_io_started(ac):
|
||||||
|
while True:
|
||||||
|
ev = ac.wait_for_event(EventType.INFO)
|
||||||
|
if "scheduler is running" in ev.msg:
|
||||||
|
return
|
||||||
|
|
||||||
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
|
ac1_clone = ac1.clone()
|
||||||
|
ac1_clone.bring_online()
|
||||||
|
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
|
||||||
|
ac1.add_transport_from_qr(qr)
|
||||||
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
wait_for_io_started(ac1_clone)
|
||||||
|
assert len(ac1.list_transports()) == 2
|
||||||
|
assert len(ac1_clone.list_transports()) == 2
|
||||||
|
|
||||||
|
ac1_clone.add_transport_from_qr(qr)
|
||||||
|
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
wait_for_io_started(ac1)
|
||||||
|
assert len(ac1.list_transports()) == 3
|
||||||
|
assert len(ac1_clone.list_transports()) == 3
|
||||||
|
|
||||||
|
log.section("ac1 clone removes second transport")
|
||||||
|
[transport1, transport2, transport3] = ac1_clone.list_transports()
|
||||||
|
addr3 = transport3["addr"]
|
||||||
|
ac1_clone.delete_transport(transport2["addr"])
|
||||||
|
|
||||||
|
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
wait_for_io_started(ac1)
|
||||||
|
[transport1, transport3] = ac1.list_transports()
|
||||||
|
|
||||||
|
log.section("ac1 changes the primary transport")
|
||||||
|
ac1.set_config("configured_addr", transport3["addr"])
|
||||||
|
|
||||||
|
# One event for updated `add_timestamp` of the new primary transport,
|
||||||
|
# one event for the `configured_addr` update.
|
||||||
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
[transport1, transport3] = ac1_clone.list_transports()
|
||||||
|
assert ac1_clone.get_config("configured_addr") == addr3
|
||||||
|
|
||||||
|
log.section("ac1 removes the first transport")
|
||||||
|
ac1.delete_transport(transport1["addr"])
|
||||||
|
|
||||||
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
wait_for_io_started(ac1_clone)
|
||||||
|
[transport3] = ac1_clone.list_transports()
|
||||||
|
assert transport3["addr"] == addr3
|
||||||
|
assert ac1_clone.get_config("configured_addr") == addr3
|
||||||
|
|
||||||
|
ac2_chat = ac2.create_chat(ac1)
|
||||||
|
ac2_chat.send_text("Hello!")
|
||||||
|
|
||||||
|
assert ac1.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||||
|
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_transport_sync_new_as_primary(acfactory, log) -> None:
|
||||||
|
"""Test synchronization of new transport as primary between devices."""
|
||||||
|
ac1, bob = acfactory.get_online_accounts(2)
|
||||||
|
ac1_clone = ac1.clone()
|
||||||
|
ac1_clone.bring_online()
|
||||||
|
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
|
||||||
|
ac1.add_transport_from_qr(qr)
|
||||||
|
ac1_transports = ac1.list_transports()
|
||||||
|
assert len(ac1_transports) == 2
|
||||||
|
[transport1, transport2] = ac1_transports
|
||||||
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
assert len(ac1_clone.list_transports()) == 2
|
||||||
|
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||||
|
|
||||||
|
log.section("ac1 changes the primary transport")
|
||||||
|
ac1.set_config("configured_addr", transport2["addr"])
|
||||||
|
|
||||||
|
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||||
|
assert ac1_clone.get_config("configured_addr") == transport2["addr"]
|
||||||
|
|
||||||
|
log.section("ac1_clone receives a message via the new primary transport")
|
||||||
|
ac1_chat = ac1.create_chat(bob)
|
||||||
|
ac1_chat.send_text("Hello!")
|
||||||
|
bob_chat_id = bob.wait_for_incoming_msg_event().chat_id
|
||||||
|
bob_chat = bob.get_chat_by_id(bob_chat_id)
|
||||||
|
bob_chat.accept()
|
||||||
|
bob_chat.send_text("hello back")
|
||||||
|
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "hello back"
|
||||||
|
|
||||||
|
|
||||||
|
def test_recognize_self_address(acfactory) -> None:
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
bob_chat = bob.create_chat(alice)
|
||||||
|
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
alice.add_transport_from_qr(qr)
|
||||||
|
|
||||||
|
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||||
|
alice.set_config("configured_addr", new_alice_addr)
|
||||||
|
|
||||||
|
bob_chat.send_text("Hello!")
|
||||||
|
msg = alice.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert msg.chat == alice.create_chat(bob)
|
||||||
|
|
||||||
|
|
||||||
|
def test_transport_limit(acfactory) -> None:
|
||||||
|
"""Test transports limit."""
|
||||||
|
account = acfactory.get_online_account()
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
|
||||||
|
limit = 5
|
||||||
|
|
||||||
|
for _ in range(1, limit):
|
||||||
|
account.add_transport_from_qr(qr)
|
||||||
|
|
||||||
|
assert len(account.list_transports()) == limit
|
||||||
|
|
||||||
|
with pytest.raises(JsonRpcError):
|
||||||
|
account.add_transport_from_qr(qr)
|
||||||
|
|
||||||
|
second_addr = account.list_transports()[1]["addr"]
|
||||||
|
account.delete_transport(second_addr)
|
||||||
|
|
||||||
|
# test that adding a transport after deleting one works again
|
||||||
|
account.add_transport_from_qr(qr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_message_info_imap_urls(acfactory) -> None:
|
||||||
|
"""Test that message info contains IMAP URLs of where the message was received."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
for i in range(3):
|
||||||
|
alice.add_transport_from_qr(qr)
|
||||||
|
# Wait for all transports to go IDLE after adding each one.
|
||||||
|
for _ in range(i + 1):
|
||||||
|
alice.bring_online()
|
||||||
|
|
||||||
|
# Enable multi-device mode so messages are not deleted immediately.
|
||||||
|
alice.set_config("bcc_self", "1")
|
||||||
|
|
||||||
|
# Bob creates chat, learning about Alice's currently selected transport.
|
||||||
|
# This is where he will send the message.
|
||||||
|
bob_chat = bob.create_chat(alice)
|
||||||
|
|
||||||
|
# Alice switches to another transport and removes the rest of the transports.
|
||||||
|
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||||
|
alice.set_config("configured_addr", new_alice_addr)
|
||||||
|
removed_addrs = []
|
||||||
|
for transport in alice.list_transports():
|
||||||
|
if transport["addr"] != new_alice_addr:
|
||||||
|
alice.delete_transport(transport["addr"])
|
||||||
|
removed_addrs.append(transport["addr"])
|
||||||
|
alice.stop_io()
|
||||||
|
alice.start_io()
|
||||||
|
|
||||||
|
bob_chat.send_text("Hello!")
|
||||||
|
|
||||||
|
msg = alice.wait_for_incoming_msg()
|
||||||
|
msg_info = msg.get_info()
|
||||||
|
assert new_alice_addr in msg_info
|
||||||
|
for removed_addr in removed_addrs:
|
||||||
|
assert removed_addr not in msg_info
|
||||||
|
assert f"{new_alice_addr}/INBOX" in msg_info
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_primary_transport(acfactory, log) -> None:
|
||||||
|
"""Test that after removing the primary relay, Alice can still receive messages."""
|
||||||
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
qr = acfactory.get_account_qr()
|
||||||
|
|
||||||
|
alice.add_transport_from_qr(qr)
|
||||||
|
alice.bring_online()
|
||||||
|
|
||||||
|
bob_chat = bob.create_chat(alice)
|
||||||
|
alice.create_chat(bob)
|
||||||
|
|
||||||
|
log.section("Alice sets up second transport")
|
||||||
|
[transport1, transport2] = alice.list_transports()
|
||||||
|
alice.set_config("configured_addr", transport2["addr"])
|
||||||
|
|
||||||
|
bob_chat.send_text("Hello!")
|
||||||
|
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert msg1.text == "Hello!"
|
||||||
|
|
||||||
|
log.section("Alice removes the primary relay")
|
||||||
|
alice.delete_transport(transport1["addr"])
|
||||||
|
alice.stop_io()
|
||||||
|
alice.start_io()
|
||||||
|
|
||||||
|
bob_chat.send_text("Hello again!")
|
||||||
|
msg2 = alice.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert msg2.text == "Hello again!"
|
||||||
|
assert msg2.chat.get_basic_snapshot().chat_type == ChatType.SINGLE
|
||||||
|
assert msg2.chat == alice.create_chat(bob)
|
||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||||
|
from deltachat_rpc_client.const import ChatType
|
||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@@ -58,8 +59,7 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
|||||||
assert "Alice" in svg
|
assert "Alice" in svg
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("protect", [True, False])
|
def test_qr_securejoin(acfactory):
|
||||||
def test_qr_securejoin(acfactory, protect):
|
|
||||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
# Setup second device for Alice
|
# Setup second device for Alice
|
||||||
@@ -67,8 +67,7 @@ def test_qr_securejoin(acfactory, protect):
|
|||||||
alice2 = alice.clone()
|
alice2 = alice.clone()
|
||||||
|
|
||||||
logging.info("Alice creates a group")
|
logging.info("Alice creates a group")
|
||||||
alice_chat = alice.create_group("Group", protect=protect)
|
alice_chat = alice.create_group("Group")
|
||||||
assert alice_chat.get_basic_snapshot().is_protected == protect
|
|
||||||
|
|
||||||
logging.info("Bob joins the group")
|
logging.info("Bob joins the group")
|
||||||
qr_code = alice_chat.get_qr_code()
|
qr_code = alice_chat.get_qr_code()
|
||||||
@@ -87,9 +86,8 @@ def test_qr_securejoin(acfactory, protect):
|
|||||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert alice_contact_bob_snapshot.is_verified
|
assert alice_contact_bob_snapshot.is_verified
|
||||||
|
|
||||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||||
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
|
||||||
|
|
||||||
# Test that Bob verified Alice's profile.
|
# Test that Bob verified Alice's profile.
|
||||||
bob_contact_alice = bob.create_contact(alice)
|
bob_contact_alice = bob.create_contact(alice)
|
||||||
@@ -112,6 +110,148 @@ def test_qr_securejoin(acfactory, protect):
|
|||||||
fiona.wait_for_securejoin_joiner_success()
|
fiona.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("all_devices_online", [True, False])
|
||||||
|
def test_qr_securejoin_broadcast(acfactory, all_devices_online):
|
||||||
|
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
alice2 = alice.clone()
|
||||||
|
bob2 = bob.clone()
|
||||||
|
|
||||||
|
if all_devices_online:
|
||||||
|
alice2.start_io()
|
||||||
|
bob2.start_io()
|
||||||
|
|
||||||
|
logging.info("===================== Alice creates a broadcast =====================")
|
||||||
|
alice_chat = alice.create_broadcast("Broadcast channel!")
|
||||||
|
snapshot = alice_chat.get_basic_snapshot()
|
||||||
|
assert not snapshot.is_unpromoted # Broadcast channels are never unpromoted
|
||||||
|
|
||||||
|
logging.info("===================== Bob joins the broadcast =====================")
|
||||||
|
|
||||||
|
qr_code = alice_chat.get_qr_code()
|
||||||
|
bob.secure_join(qr_code)
|
||||||
|
alice.wait_for_securejoin_inviter_success()
|
||||||
|
bob.wait_for_securejoin_joiner_success()
|
||||||
|
alice_chat.send_text("Hello everyone!")
|
||||||
|
|
||||||
|
def get_broadcast(ac):
|
||||||
|
chat = ac.get_chatlist(query="Broadcast channel!")[0]
|
||||||
|
assert chat.get_basic_snapshot().name == "Broadcast channel!"
|
||||||
|
return chat
|
||||||
|
|
||||||
|
def wait_for_broadcast_messages(ac):
|
||||||
|
snapshot1 = ac.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert snapshot1.text == "You joined the channel."
|
||||||
|
|
||||||
|
snapshot2 = ac.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert snapshot2.text == "Hello everyone!"
|
||||||
|
|
||||||
|
chat = get_broadcast(ac)
|
||||||
|
assert snapshot1.chat_id == chat.id
|
||||||
|
assert snapshot2.chat_id == chat.id
|
||||||
|
|
||||||
|
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||||
|
# Check that the chat partner is verified.
|
||||||
|
contact_snapshot = contact.get_snapshot()
|
||||||
|
assert contact_snapshot.is_verified
|
||||||
|
|
||||||
|
chat = get_broadcast(ac)
|
||||||
|
chat_msgs = chat.get_messages()
|
||||||
|
|
||||||
|
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
|
assert encrypted_msg.text == "Messages are end-to-end encrypted."
|
||||||
|
assert encrypted_msg.is_info
|
||||||
|
|
||||||
|
if please_wait_info_msg:
|
||||||
|
first_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
|
assert "invited you to join this channel" in first_msg.text
|
||||||
|
assert first_msg.is_info
|
||||||
|
|
||||||
|
if inviter_side:
|
||||||
|
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
|
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||||
|
assert member_added_msg.info_contact_id == contact_snapshot.id
|
||||||
|
else:
|
||||||
|
if chat_msgs[0].get_snapshot().text == "You joined the channel.":
|
||||||
|
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
|
else:
|
||||||
|
member_added_msg = chat_msgs.pop(1).get_snapshot()
|
||||||
|
assert member_added_msg.text == "You joined the channel."
|
||||||
|
assert member_added_msg.is_info
|
||||||
|
|
||||||
|
hello_msg = chat_msgs.pop(0).get_snapshot()
|
||||||
|
assert hello_msg.text == "Hello everyone!"
|
||||||
|
assert not hello_msg.is_info
|
||||||
|
assert hello_msg.show_padlock
|
||||||
|
assert hello_msg.error is None
|
||||||
|
|
||||||
|
assert len(chat_msgs) == 0
|
||||||
|
|
||||||
|
chat_snapshot = chat.get_full_snapshot()
|
||||||
|
assert chat_snapshot.is_encrypted
|
||||||
|
assert chat_snapshot.name == "Broadcast channel!"
|
||||||
|
if inviter_side:
|
||||||
|
assert chat_snapshot.chat_type == ChatType.OUT_BROADCAST
|
||||||
|
else:
|
||||||
|
assert chat_snapshot.chat_type == ChatType.IN_BROADCAST
|
||||||
|
assert chat_snapshot.can_send == inviter_side
|
||||||
|
|
||||||
|
chat_contacts = chat_snapshot.contact_ids
|
||||||
|
assert contact.id in chat_contacts
|
||||||
|
if inviter_side:
|
||||||
|
assert len(chat_contacts) == 1
|
||||||
|
else:
|
||||||
|
assert len(chat_contacts) == 2
|
||||||
|
assert SpecialContactId.SELF in chat_contacts
|
||||||
|
assert chat_snapshot.self_in_group
|
||||||
|
|
||||||
|
wait_for_broadcast_messages(bob)
|
||||||
|
|
||||||
|
check_account(alice, alice.create_contact(bob), inviter_side=True)
|
||||||
|
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||||
|
|
||||||
|
logging.info("===================== Test Alice's second device =====================")
|
||||||
|
|
||||||
|
# Start second Alice device, if it wasn't started already.
|
||||||
|
alice2.start_io()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
msg_id = alice2.wait_for_msgs_changed_event().msg_id
|
||||||
|
if msg_id:
|
||||||
|
snapshot = alice2.get_message_by_id(msg_id).get_snapshot()
|
||||||
|
if snapshot.text == "Hello everyone!":
|
||||||
|
break
|
||||||
|
|
||||||
|
check_account(alice2, alice2.create_contact(bob), inviter_side=True)
|
||||||
|
|
||||||
|
logging.info("===================== Test Bob's second device =====================")
|
||||||
|
|
||||||
|
# Start second Bob device, if it wasn't started already.
|
||||||
|
bob2.start_io()
|
||||||
|
bob2.wait_for_securejoin_joiner_success()
|
||||||
|
wait_for_broadcast_messages(bob2)
|
||||||
|
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
|
||||||
|
|
||||||
|
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||||
|
logging.info("===================== Fiona joins the group via alice2 =====================")
|
||||||
|
alice.stop_io()
|
||||||
|
fiona.secure_join(qr_code)
|
||||||
|
alice2.wait_for_securejoin_inviter_success()
|
||||||
|
fiona.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
|
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert snapshot.text == "You joined the channel."
|
||||||
|
|
||||||
|
get_broadcast(alice2).get_messages()[2].resend()
|
||||||
|
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||||
|
assert snapshot.text == "Hello everyone!"
|
||||||
|
|
||||||
|
check_account(fiona, fiona.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||||
|
|
||||||
|
# For Bob, the channel must not have changed:
|
||||||
|
check_account(bob, bob.create_contact(alice), inviter_side=False, please_wait_info_msg=True)
|
||||||
|
|
||||||
|
|
||||||
def test_qr_securejoin_contact_request(acfactory) -> None:
|
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
@@ -120,13 +260,13 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
|||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
bob_chat_alice = snapshot.chat
|
bob_chat_alice = snapshot.chat
|
||||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
alice_chat = alice.create_group("Verified group", protect=True)
|
alice_chat = alice.create_group("Group")
|
||||||
logging.info("Bob joins verified group")
|
logging.info("Bob joins the group")
|
||||||
qr_code = alice_chat.get_qr_code()
|
qr_code = alice_chat.get_qr_code()
|
||||||
bob.secure_join(qr_code)
|
bob.secure_join(qr_code)
|
||||||
while True:
|
while True:
|
||||||
@@ -150,8 +290,8 @@ def test_qr_readreceipt(acfactory) -> None:
|
|||||||
for joiner in [bob, charlie]:
|
for joiner in [bob, charlie]:
|
||||||
joiner.wait_for_securejoin_joiner_success()
|
joiner.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
logging.info("Alice creates a verified group")
|
logging.info("Alice creates a group")
|
||||||
group = alice.create_group("Group", protect=True)
|
group = alice.create_group("Group")
|
||||||
|
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
||||||
@@ -164,8 +304,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
|||||||
|
|
||||||
logging.info("Bob and Charlie receive a group")
|
logging.info("Bob and Charlie receive a group")
|
||||||
|
|
||||||
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
|
bob_message = bob.wait_for_incoming_msg()
|
||||||
bob_message = bob.get_message_by_id(bob_msg_id)
|
|
||||||
bob_snapshot = bob_message.get_snapshot()
|
bob_snapshot = bob_message.get_snapshot()
|
||||||
assert bob_snapshot.text == "Hello"
|
assert bob_snapshot.text == "Hello"
|
||||||
|
|
||||||
@@ -176,8 +315,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
|||||||
|
|
||||||
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
|
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.wait_for_incoming_msg()
|
||||||
charlie_message = charlie.get_message_by_id(charlie_msg_id)
|
|
||||||
charlie_snapshot = charlie_message.get_snapshot()
|
charlie_snapshot = charlie_message.get_snapshot()
|
||||||
assert charlie_snapshot.text == "Hi from Bob!"
|
assert charlie_snapshot.text == "Hi from Bob!"
|
||||||
|
|
||||||
@@ -216,11 +354,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
logging.info("ac1 creates verified group")
|
logging.info("ac1 creates a group")
|
||||||
chat = ac1.create_group("Verified group", protect=True)
|
chat = ac1.create_group("Group")
|
||||||
assert chat.get_basic_snapshot().is_protected
|
|
||||||
|
|
||||||
logging.info("ac2 joins verified group")
|
logging.info("ac2 joins the group")
|
||||||
qr_code = chat.get_qr_code()
|
qr_code = chat.get_qr_code()
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
ac2.wait_for_securejoin_joiner_success()
|
ac2.wait_for_securejoin_joiner_success()
|
||||||
@@ -253,7 +390,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
ac3_contact_ac2 = ac3.create_contact(ac2)
|
ac3_contact_ac2 = ac3.create_contact(ac2)
|
||||||
ac3_chat.remove_contact(ac3_contact_ac2_old)
|
ac3_chat.remove_contact(ac3_contact_ac2_old)
|
||||||
|
|
||||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
assert "removed" in snapshot.text
|
assert "removed" in snapshot.text
|
||||||
|
|
||||||
ac3_chat.add_contact(ac3_contact_ac2)
|
ac3_chat.add_contact(ac3_contact_ac2)
|
||||||
@@ -266,25 +403,26 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
|||||||
logging.info("ac2 got event message: %s", snapshot.text)
|
logging.info("ac2 got event message: %s", snapshot.text)
|
||||||
assert "added" in snapshot.text
|
assert "added" in snapshot.text
|
||||||
|
|
||||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
assert "added" in snapshot.text
|
assert "added" in snapshot.text
|
||||||
|
|
||||||
chat = Chat(ac2, chat_id)
|
chat = Chat(ac2, chat_id)
|
||||||
chat.send_text("Works again!")
|
chat.send_text("Works again!")
|
||||||
|
|
||||||
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
message = ac3.wait_for_incoming_msg()
|
||||||
message = ac3.get_message_by_id(msg_id)
|
|
||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.text == "Works again!"
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Works again!"
|
assert snapshot.text == "Works again!"
|
||||||
|
|
||||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||||
ac1_contact_ac3 = ac1.create_contact(ac3)
|
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||||
assert ac1_contact_ac2_snapshot.is_verified
|
# Until we reset verifications and then send the _verified header,
|
||||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
|
# verification is not gossiped here:
|
||||||
|
assert not ac1_contact_ac2_snapshot.is_verified
|
||||||
|
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
|
||||||
|
|
||||||
|
|
||||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||||
@@ -302,8 +440,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
|||||||
# we first create a fully joined verified group, and then start
|
# we first create a fully joined verified group, and then start
|
||||||
# joining a second time but interrupt it, to create pending bob state
|
# joining a second time but interrupt it, to create pending bob state
|
||||||
|
|
||||||
logging.info("ac1: create verified group that ac2 fully joins")
|
logging.info("ac1: create a group that ac2 fully joins")
|
||||||
ch1 = ac1.create_group("Group", protect=True)
|
ch1 = ac1.create_group("Group")
|
||||||
qr_code = ch1.get_qr_code()
|
qr_code = ch1.get_qr_code()
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
ac1.wait_for_securejoin_inviter_success()
|
ac1.wait_for_securejoin_inviter_success()
|
||||||
@@ -311,9 +449,8 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
|||||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||||
ch1.send_text("ac1 says hello")
|
ch1.send_text("ac1 says hello")
|
||||||
while 1:
|
while 1:
|
||||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
if snapshot.text == "ac1 says hello":
|
if snapshot.text == "ac1 says hello":
|
||||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
|
||||||
break
|
break
|
||||||
|
|
||||||
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||||
@@ -327,15 +464,14 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
|||||||
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||||
|
|
||||||
logging.info("ac3: create a verified group VG with ac2")
|
logging.info("ac3: create a verified group VG with ac2")
|
||||||
vg = ac3.create_group("ac3-created", protect=True)
|
vg = ac3.create_group("ac3-created")
|
||||||
vg.add_contact(ac3.create_contact(ac2))
|
vg.add_contact(ac3.create_contact(ac2))
|
||||||
|
|
||||||
# ensure ac2 receives message in VG
|
# ensure ac2 receives message in VG
|
||||||
vg.send_text("hello")
|
vg.send_text("hello")
|
||||||
while 1:
|
while 1:
|
||||||
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
msg = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
if msg.text == "hello":
|
if msg.text == "hello":
|
||||||
assert msg.chat.get_basic_snapshot().is_protected
|
|
||||||
break
|
break
|
||||||
|
|
||||||
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||||
@@ -359,7 +495,7 @@ def test_qr_new_group_unblocked(acfactory):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
ac1_chat = ac1.create_group("Group for joining", protect=True)
|
ac1_chat = ac1.create_group("Group for joining")
|
||||||
qr_code = ac1_chat.get_qr_code()
|
qr_code = ac1_chat.get_qr_code()
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
|
|
||||||
@@ -371,7 +507,7 @@ def test_qr_new_group_unblocked(acfactory):
|
|||||||
ac2.wait_for_incoming_msg_event()
|
ac2.wait_for_incoming_msg_event()
|
||||||
|
|
||||||
ac1_new_chat.send_text("Hello!")
|
ac1_new_chat.send_text("Hello!")
|
||||||
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
ac2_msg = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
assert ac2_msg.text == "Hello!"
|
assert ac2_msg.text == "Hello!"
|
||||||
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||||
|
|
||||||
@@ -384,8 +520,7 @@ def test_aeap_flow_verified(acfactory):
|
|||||||
addr, password = acfactory.get_credentials()
|
addr, password = acfactory.get_credentials()
|
||||||
|
|
||||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||||
chat = ac1.create_group("hello", protect=True)
|
chat = ac1.create_group("hello")
|
||||||
assert chat.get_basic_snapshot().is_protected
|
|
||||||
qr_code = chat.get_qr_code()
|
qr_code = chat.get_qr_code()
|
||||||
logging.info("ac2: start QR-code based join-group protocol")
|
logging.info("ac2: start QR-code based join-group protocol")
|
||||||
ac2.secure_join(qr_code)
|
ac2.secure_join(qr_code)
|
||||||
@@ -397,7 +532,7 @@ def test_aeap_flow_verified(acfactory):
|
|||||||
|
|
||||||
logging.info("receiving first message")
|
logging.info("receiving first message")
|
||||||
ac2.wait_for_incoming_msg_event() # member added 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()
|
msg_in_1 = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
assert msg_in_1.text == msg_out.text
|
assert msg_in_1.text == msg_out.text
|
||||||
|
|
||||||
logging.info("changing email account")
|
logging.info("changing email account")
|
||||||
@@ -411,7 +546,7 @@ def test_aeap_flow_verified(acfactory):
|
|||||||
msg_out = chat.send_text("changed address").get_snapshot()
|
msg_out = chat.send_text("changed address").get_snapshot()
|
||||||
|
|
||||||
logging.info("receiving second message")
|
logging.info("receiving second message")
|
||||||
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
msg_in_2 = ac2.wait_for_incoming_msg()
|
||||||
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||||
assert msg_in_2_snapshot.text == msg_out.text
|
assert msg_in_2_snapshot.text == msg_out.text
|
||||||
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
||||||
@@ -439,33 +574,35 @@ def test_gossip_verification(acfactory) -> None:
|
|||||||
|
|
||||||
logging.info("Bob creates an Autocrypt group")
|
logging.info("Bob creates an Autocrypt group")
|
||||||
bob_group_chat = bob.create_group("Autocrypt Group")
|
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||||
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_alice)
|
||||||
bob_group_chat.add_contact(bob_contact_carol)
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
bob_group_chat.send_message(text="Hello Autocrypt group")
|
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()
|
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Hello Autocrypt group"
|
assert snapshot.text == "Hello Autocrypt group"
|
||||||
assert snapshot.show_padlock
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
# Autocrypt group does not propagate verification.
|
# Group propagates verification using Autocrypt-Gossip header.
|
||||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
|
# Until we reset verifications and then send the _verified header,
|
||||||
|
# verification is not gossiped here:
|
||||||
assert not carol_contact_alice_snapshot.is_verified
|
assert not carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
logging.info("Bob creates a Securejoin group")
|
logging.info("Bob creates a Securejoin group")
|
||||||
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
|
bob_group_chat = bob.create_group("Securejoin Group")
|
||||||
assert bob_group_chat.get_basic_snapshot().is_protected
|
|
||||||
bob_group_chat.add_contact(bob_contact_alice)
|
bob_group_chat.add_contact(bob_contact_alice)
|
||||||
bob_group_chat.add_contact(bob_contact_carol)
|
bob_group_chat.add_contact(bob_contact_carol)
|
||||||
bob_group_chat.send_message(text="Hello Securejoin group")
|
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()
|
snapshot = carol.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Hello Securejoin group"
|
assert snapshot.text == "Hello Securejoin group"
|
||||||
assert snapshot.show_padlock
|
assert snapshot.show_padlock
|
||||||
|
|
||||||
# Securejoin propagates verification.
|
# Securejoin propagates verification.
|
||||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||||
assert carol_contact_alice_snapshot.is_verified
|
# Until we reset verifications and then send the _verified header,
|
||||||
|
# verification is not gossiped here:
|
||||||
|
assert not carol_contact_alice_snapshot.is_verified
|
||||||
|
|
||||||
|
|
||||||
def test_securejoin_after_contact_resetup(acfactory) -> None:
|
def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||||
@@ -477,7 +614,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
# ac3 creates protected group with ac1.
|
# ac3 creates protected group with ac1.
|
||||||
ac3_chat = ac3.create_group("Verified group", protect=True)
|
ac3_chat = ac3.create_group("Group")
|
||||||
|
|
||||||
# ac1 joins ac3 group.
|
# ac1 joins ac3 group.
|
||||||
ac3_qr_code = ac3_chat.get_qr_code()
|
ac3_qr_code = ac3_chat.get_qr_code()
|
||||||
@@ -485,7 +622,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
ac1.wait_for_securejoin_joiner_success()
|
ac1.wait_for_securejoin_joiner_success()
|
||||||
|
|
||||||
# ac1 waits for member added message and creates a QR code.
|
# ac1 waits for member added message and creates a QR code.
|
||||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||||
|
|
||||||
@@ -522,10 +659,9 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
|
|
||||||
# Wait for member added.
|
# Wait for member added.
|
||||||
logging.info("ac2 waits for member added message")
|
logging.info("ac2 waits for member added message")
|
||||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.is_info
|
assert snapshot.is_info
|
||||||
ac2_chat = snapshot.chat
|
ac2_chat = snapshot.chat
|
||||||
assert ac2_chat.get_basic_snapshot().is_protected
|
|
||||||
assert len(ac2_chat.get_contacts()) == 3
|
assert len(ac2_chat.get_contacts()) == 3
|
||||||
|
|
||||||
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||||
@@ -535,9 +671,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
|||||||
def test_withdraw_securejoin_qr(acfactory):
|
def test_withdraw_securejoin_qr(acfactory):
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
logging.info("Alice creates a verified group")
|
logging.info("Alice creates a group")
|
||||||
alice_chat = alice.create_group("Verified group", protect=True)
|
alice_chat = alice.create_group("Group")
|
||||||
assert alice_chat.get_basic_snapshot().is_protected
|
|
||||||
logging.info("Bob joins verified group")
|
logging.info("Bob joins verified group")
|
||||||
|
|
||||||
qr_code = alice_chat.get_qr_code()
|
qr_code = alice_chat.get_qr_code()
|
||||||
@@ -546,9 +681,8 @@ def test_withdraw_securejoin_qr(acfactory):
|
|||||||
|
|
||||||
alice.clear_all_events()
|
alice.clear_all_events()
|
||||||
|
|
||||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
|
||||||
bob_chat.leave()
|
bob_chat.leave()
|
||||||
|
|
||||||
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||||
@@ -567,6 +701,6 @@ def test_withdraw_securejoin_qr(acfactory):
|
|||||||
event = alice.wait_for_event()
|
event = alice.wait_for_event()
|
||||||
if (
|
if (
|
||||||
event.kind == EventType.WARNING
|
event.kind == EventType.WARNING
|
||||||
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
|
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,12 @@
|
|||||||
def test_vcard(acfactory) -> None:
|
def test_vcard(acfactory) -> None:
|
||||||
alice, bob = acfactory.get_online_accounts(2)
|
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||||
|
|
||||||
|
bob.create_chat(alice)
|
||||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||||
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
||||||
|
alice_contact_charlie_snapshot = alice_contact_charlie.get_snapshot()
|
||||||
|
alice_contact_fiona = alice.create_contact(fiona, "Fiona")
|
||||||
|
alice_contact_fiona_snapshot = alice_contact_fiona.get_snapshot()
|
||||||
|
|
||||||
alice_chat_bob = alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
alice_chat_bob.send_contact(alice_contact_charlie)
|
alice_chat_bob.send_contact(alice_contact_charlie)
|
||||||
@@ -12,3 +16,12 @@ def test_vcard(acfactory) -> None:
|
|||||||
snapshot = message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.vcard_contact
|
assert snapshot.vcard_contact
|
||||||
assert snapshot.vcard_contact.addr == "charlie@example.org"
|
assert snapshot.vcard_contact.addr == "charlie@example.org"
|
||||||
|
assert snapshot.vcard_contact.color == alice_contact_charlie_snapshot.color
|
||||||
|
|
||||||
|
alice_chat_bob.send_contact(alice_contact_fiona)
|
||||||
|
event = bob.wait_for_incoming_msg_event()
|
||||||
|
message = bob.get_message_by_id(event.msg_id)
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.vcard_contact
|
||||||
|
assert snapshot.vcard_contact.key
|
||||||
|
assert snapshot.vcard_contact.color == alice_contact_fiona_snapshot.color
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ def test_webxdc(acfactory) -> None:
|
|||||||
"sourceCodeUrl": None,
|
"sourceCodeUrl": None,
|
||||||
"summary": None,
|
"summary": None,
|
||||||
"selfAddr": webxdc_info["selfAddr"],
|
"selfAddr": webxdc_info["selfAddr"],
|
||||||
|
"isAppSender": False,
|
||||||
|
"isBroadcast": False,
|
||||||
"sendUpdateInterval": 1000,
|
"sendUpdateInterval": 1000,
|
||||||
"sendUpdateMaxSize": 18874368,
|
"sendUpdateMaxSize": 18874368,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "2.2.0"
|
version = "2.50.0-dev"
|
||||||
description = "DeltaChat JSON-RPC server"
|
description = "DeltaChat JSON-RPC server"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { startDeltaChat } from "@deltachat/stdio-rpc-server";
|
|||||||
import { C } from "@deltachat/jsonrpc-client";
|
import { C } from "@deltachat/jsonrpc-client";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const dc = await startDeltaChat("deltachat-data");
|
const dc = startDeltaChat("deltachat-data");
|
||||||
console.log(await dc.rpc.getSystemInfo());
|
console.log(await dc.rpc.getSystemInfo());
|
||||||
dc.close()
|
dc.close()
|
||||||
}
|
}
|
||||||
|
|||||||
11
deltachat-rpc-server/npm-package/index.d.ts
vendored
11
deltachat-rpc-server/npm-package/index.d.ts
vendored
@@ -15,7 +15,7 @@ export interface SearchOptions {
|
|||||||
*/
|
*/
|
||||||
export function getRPCServerPath(
|
export function getRPCServerPath(
|
||||||
options?: Partial<SearchOptions>
|
options?: Partial<SearchOptions>
|
||||||
): Promise<string>;
|
): string;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -33,8 +33,15 @@ export interface StartOptions {
|
|||||||
* @param directory directory for accounts folder
|
* @param directory directory for accounts folder
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): Promise<DeltaChatOverJsonRpcServer>
|
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): DeltaChatOverJsonRpcServer
|
||||||
|
|
||||||
|
export class DeltaChatOverJsonRpc extends StdioDeltaChat {
|
||||||
|
constructor(
|
||||||
|
directory: string,
|
||||||
|
options?: Partial<SearchOptions & StartOptions>
|
||||||
|
);
|
||||||
|
readonly pathToServerBinary: string;
|
||||||
|
}
|
||||||
|
|
||||||
export namespace FnTypes {
|
export namespace FnTypes {
|
||||||
export type getRPCServerPath = typeof getRPCServerPath
|
export type getRPCServerPath = typeof getRPCServerPath
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//@ts-check
|
//@ts-check
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { stat } from "node:fs/promises";
|
import { statSync } from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
|
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
|
||||||
@@ -36,7 +36,7 @@ function findRPCServerInNodeModules() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import("./index").FnTypes.getRPCServerPath} */
|
/** @type {import("./index").FnTypes.getRPCServerPath} */
|
||||||
export async function getRPCServerPath(options = {}) {
|
export function getRPCServerPath(options = {}) {
|
||||||
const { takeVersionFromPATH, disableEnvPath } = {
|
const { takeVersionFromPATH, disableEnvPath } = {
|
||||||
takeVersionFromPATH: false,
|
takeVersionFromPATH: false,
|
||||||
disableEnvPath: false,
|
disableEnvPath: false,
|
||||||
@@ -45,7 +45,7 @@ export async function getRPCServerPath(options = {}) {
|
|||||||
// 1. check if it is set as env var
|
// 1. check if it is set as env var
|
||||||
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
|
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
|
||||||
try {
|
try {
|
||||||
if (!(await stat(process.env[ENV_VAR_NAME])).isFile()) {
|
if (!statSync(process.env[ENV_VAR_NAME]).isFile()) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
|
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
|
||||||
);
|
);
|
||||||
@@ -68,41 +68,49 @@ export async function getRPCServerPath(options = {}) {
|
|||||||
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||||
|
|
||||||
/** @type {import("./index").FnTypes.startDeltaChat} */
|
/** @type {import("./index").FnTypes.startDeltaChat} */
|
||||||
export async function startDeltaChat(directory, options = {}) {
|
export function startDeltaChat(directory, options = {}) {
|
||||||
const pathToServerBinary = await getRPCServerPath(options);
|
return new DeltaChatOverJsonRpc(directory, options);
|
||||||
const server = spawn(pathToServerBinary, {
|
}
|
||||||
env: {
|
|
||||||
RUST_LOG: process.env.RUST_LOG,
|
export class DeltaChatOverJsonRpc extends StdioDeltaChat {
|
||||||
DC_ACCOUNTS_PATH: directory,
|
/**
|
||||||
},
|
*
|
||||||
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
|
* @param {string} directory
|
||||||
});
|
* @param {Partial<import("./index").SearchOptions & import("./index").StartOptions>} options
|
||||||
|
*/
|
||||||
server.on("error", (err) => {
|
constructor(directory, options = {}) {
|
||||||
throw new Error(FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err));
|
const pathToServerBinary = getRPCServerPath(options);
|
||||||
});
|
const server = spawn(pathToServerBinary, {
|
||||||
let shouldClose = false;
|
env: {
|
||||||
|
RUST_LOG: process.env.RUST_LOG,
|
||||||
server.on("exit", () => {
|
DC_ACCOUNTS_PATH: directory,
|
||||||
if (shouldClose) {
|
},
|
||||||
return;
|
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
|
||||||
}
|
});
|
||||||
throw new Error("Server quit");
|
|
||||||
});
|
server.on("error", (err) => {
|
||||||
|
throw new Error(
|
||||||
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
|
FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err)
|
||||||
//@ts-expect-error
|
);
|
||||||
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
|
});
|
||||||
|
let shouldClose = false;
|
||||||
dc.close = () => {
|
|
||||||
shouldClose = true;
|
server.on("exit", () => {
|
||||||
if (!server.kill()) {
|
if (shouldClose) {
|
||||||
console.log("server termination failed");
|
return;
|
||||||
}
|
}
|
||||||
};
|
throw new Error("Server quit");
|
||||||
|
});
|
||||||
//@ts-expect-error
|
|
||||||
dc.pathToServerBinary = pathToServerBinary;
|
super(server.stdin, server.stdout, true);
|
||||||
|
|
||||||
return dc;
|
this.close = () => {
|
||||||
|
shouldClose = true;
|
||||||
|
if (!server.kill()) {
|
||||||
|
console.log("server termination failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pathToServerBinary = pathToServerBinary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"version": "2.2.0"
|
"version": "2.50.0-dev"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ use yerpc::{RpcClient, RpcSession};
|
|||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
// Logs from `log` crate and traces from `tracing` crate
|
||||||
|
// are configurable with `RUST_LOG` environment variable
|
||||||
|
// and go to stderr to avoid interfering with JSON-RPC using stdout.
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.init();
|
||||||
|
|
||||||
let r = main_impl().await;
|
let r = main_impl().await;
|
||||||
// From tokio documentation:
|
// From tokio documentation:
|
||||||
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
||||||
@@ -41,22 +49,22 @@ async fn main_impl() -> Result<()> {
|
|||||||
if let Some(first_arg) = args.next() {
|
if let Some(first_arg) = args.next() {
|
||||||
if first_arg.to_str() == Some("--version") {
|
if first_arg.to_str() == Some("--version") {
|
||||||
if let Some(arg) = args.next() {
|
if let Some(arg) = args.next() {
|
||||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||||
}
|
}
|
||||||
eprintln!("{}", &*DC_VERSION_STR);
|
eprintln!("{DC_VERSION_STR}");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if first_arg.to_str() == Some("--openrpc") {
|
} else if first_arg.to_str() == Some("--openrpc") {
|
||||||
if let Some(arg) = args.next() {
|
if let Some(arg) = args.next() {
|
||||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||||
}
|
}
|
||||||
println!("{}", CommandApi::openrpc_specification()?);
|
println!("{}", CommandApi::openrpc_specification()?);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
return Err(anyhow!("Unrecognized option {first_arg:?}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(arg) = args.next() {
|
if let Some(arg) = args.next() {
|
||||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
return Err(anyhow!("Unrecognized argument {arg:?}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install signal handlers early so that the shutdown is graceful starting from here.
|
// Install signal handlers early so that the shutdown is graceful starting from here.
|
||||||
@@ -64,14 +72,6 @@ async fn main_impl() -> Result<()> {
|
|||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
||||||
|
|
||||||
// Logs from `log` crate and traces from `tracing` crate
|
|
||||||
// are configurable with `RUST_LOG` environment variable
|
|
||||||
// and go to stderr to avoid interfering with JSON-RPC using stdout.
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_env_filter(EnvFilter::from_default_env())
|
|
||||||
.with_writer(std::io::stderr)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||||
log::info!("Starting with accounts directory `{path}`.");
|
log::info!("Starting with accounts directory `{path}`.");
|
||||||
let writable = true;
|
let writable = true;
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ impl SystemTimeTools {
|
|||||||
pub fn shift(duration: Duration) {
|
pub fn shift(duration: Duration) {
|
||||||
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
|
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates the system clock being rewound by `duration`.
|
||||||
|
pub fn shift_back(duration: Duration) {
|
||||||
|
*SYSTEM_TIME_SHIFT.write().unwrap() -= duration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
48
deny.toml
48
deny.toml
@@ -12,6 +12,38 @@ ignore = [
|
|||||||
|
|
||||||
# Unmaintained paste
|
# Unmaintained paste
|
||||||
"RUSTSEC-2024-0436",
|
"RUSTSEC-2024-0436",
|
||||||
|
|
||||||
|
# Unmaintained rustls-pemfile
|
||||||
|
# It is a transitive dependency of iroh 0.35.0,
|
||||||
|
# this should be fixed by upgrading to iroh 1.0 once it is released.
|
||||||
|
"RUSTSEC-2025-0134",
|
||||||
|
|
||||||
|
# rustls-webpki v0.102.8
|
||||||
|
# We cannot upgrade to >=0.103.10 because
|
||||||
|
# it is a transitive dependency of iroh 0.35.0
|
||||||
|
# which depends on ^0.102.
|
||||||
|
# <https://rustsec.org/advisories/RUSTSEC-2026-0049>
|
||||||
|
# <https://rustsec.org/advisories/RUSTSEC-2026-0098>
|
||||||
|
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
|
||||||
|
"RUSTSEC-2026-0049",
|
||||||
|
"RUSTSEC-2026-0098",
|
||||||
|
"RUSTSEC-2026-0099",
|
||||||
|
|
||||||
|
# Panic in CRL signature checks.
|
||||||
|
# We do not check CRL and cannot update rustls-webpki 0.102.8
|
||||||
|
# which is a dependency of iroh 0.35.0.
|
||||||
|
# <https://rustsec.org/advisories/RUSTSEC-2026-0104>
|
||||||
|
"RUSTSEC-2026-0104",
|
||||||
|
|
||||||
|
# hickory-proto 0.25.2 unbounded loop in DNSSEC code.
|
||||||
|
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
|
||||||
|
# <https://rustsec.org/advisories/RUSTSEC-2026-0118>
|
||||||
|
"RUSTSEC-2026-0118",
|
||||||
|
|
||||||
|
# hickory-proto 0.25.2 quadratic complexity issue.
|
||||||
|
# Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02.
|
||||||
|
# <https://rustsec.org/advisories/RUSTSEC-2026-0119>
|
||||||
|
"RUSTSEC-2026-0119"
|
||||||
]
|
]
|
||||||
|
|
||||||
[bans]
|
[bans]
|
||||||
@@ -22,34 +54,32 @@ ignore = [
|
|||||||
skip = [
|
skip = [
|
||||||
{ name = "async-channel", version = "1.9.0" },
|
{ name = "async-channel", version = "1.9.0" },
|
||||||
{ name = "bitflags", version = "1.3.2" },
|
{ name = "bitflags", version = "1.3.2" },
|
||||||
|
{ name = "constant_time_eq", version = "0.3.1" },
|
||||||
|
{ name = "cpufeatures", version = "0.2.17" },
|
||||||
{ name = "derive_more-impl", version = "1.0.0" },
|
{ name = "derive_more-impl", version = "1.0.0" },
|
||||||
{ name = "derive_more", version = "1.0.0" },
|
{ name = "derive_more", version = "1.0.0" },
|
||||||
{ name = "event-listener", version = "2.5.3" },
|
{ name = "event-listener", version = "2.5.3" },
|
||||||
{ name = "getrandom", version = "0.2.12" },
|
{ name = "getrandom", version = "0.2.12" },
|
||||||
{ name = "hashbrown", version = "0.14.5" },
|
|
||||||
{ name = "heck", version = "0.4.1" },
|
{ name = "heck", version = "0.4.1" },
|
||||||
{ name = "http", version = "0.2.12" },
|
{ name = "http", version = "0.2.12" },
|
||||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||||
{ name = "lru", version = "0.12.3" },
|
{ name = "lru", version = "0.12.5" },
|
||||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||||
{ name = "nom", version = "7.1.3" },
|
{ name = "nom", version = "7.1.3" },
|
||||||
{ name = "proc-macro-crate", version = "2.0.0" },
|
|
||||||
{ name = "rand_chacha", version = "0.3.1" },
|
{ name = "rand_chacha", version = "0.3.1" },
|
||||||
{ name = "rand_core", version = "0.6.4" },
|
{ name = "rand_core", version = "0.6.4" },
|
||||||
{ name = "rand", version = "0.8.5" },
|
{ name = "rand", version = "0.8.5" },
|
||||||
{ name = "redox_syscall", version = "0.3.5" },
|
|
||||||
{ name = "redox_syscall", version = "0.4.1" },
|
|
||||||
{ name = "regex-automata", version = "0.1.10" },
|
|
||||||
{ name = "regex-syntax", version = "0.6.29" },
|
|
||||||
{ name = "rustix", version = "0.38.44" },
|
{ name = "rustix", version = "0.38.44" },
|
||||||
|
{ name = "rustls-webpki", version = "0.102.8" },
|
||||||
{ name = "serdect", version = "0.2.0" },
|
{ name = "serdect", version = "0.2.0" },
|
||||||
|
{ name = "socket2", version = "0.5.9" },
|
||||||
{ name = "spin", version = "0.9.8" },
|
{ name = "spin", version = "0.9.8" },
|
||||||
{ name = "strum_macros", version = "0.26.2" },
|
{ name = "strum_macros", version = "0.26.2" },
|
||||||
{ name = "strum", version = "0.26.2" },
|
{ name = "strum", version = "0.26.2" },
|
||||||
{ name = "syn", version = "1.0.109" },
|
{ name = "syn", version = "1.0.109" },
|
||||||
{ name = "thiserror-impl", version = "1.0.69" },
|
{ name = "thiserror-impl", version = "1.0.69" },
|
||||||
{ name = "thiserror", version = "1.0.69" },
|
{ name = "thiserror", version = "1.0.69" },
|
||||||
{ name = "toml_edit", version = "0.20.7" },
|
{ name = "toml_datetime", version = "0.6.11" },
|
||||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||||
{ name = "windows" },
|
{ name = "windows" },
|
||||||
{ name = "windows_aarch64_gnullvm" },
|
{ name = "windows_aarch64_gnullvm" },
|
||||||
@@ -67,8 +97,6 @@ skip = [
|
|||||||
{ name = "windows_x86_64_gnu" },
|
{ name = "windows_x86_64_gnu" },
|
||||||
{ name = "windows_x86_64_gnullvm" },
|
{ name = "windows_x86_64_gnullvm" },
|
||||||
{ name = "windows_x86_64_msvc" },
|
{ name = "windows_x86_64_msvc" },
|
||||||
{ name = "winnow", version = "0.5.40" },
|
|
||||||
{ name = "zerocopy", version = "0.7.32" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
AEAP MVP
|
|
||||||
========
|
|
||||||
|
|
||||||
Changes to the UIs
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
|
||||||
|
|
||||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
|
||||||
|
|
||||||
Changes in the core
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
- [x] We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
|
||||||
|
|
||||||
- [x] If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
|
||||||
|
|
||||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
|
||||||
|
|
||||||
- The key stays the same.
|
|
||||||
|
|
||||||
- [x] No changes for 1:1 chats, there simply is a new one. (This works since, contrary to group messages, messages sent to a 1:1 chat are not assigned to the group chat but always to the 1:1 chat with the sender. So it's not a problem that the new messages might be put into the old chat if they are a reply to a message there.)
|
|
||||||
|
|
||||||
- [ ] When sending a message: If any of the secondary self addrs is in the chat's member list, remove it locally (because we just transitioned away from it). We add a log message for this (alternatively, a system message in the chat would be more visible).
|
|
||||||
|
|
||||||
- [x] ([#3385](https://github.com/deltachat/deltachat-core-rust/pull/3385)) When receiving a message: If the key exists, but belongs to another address (we may want to benchmark this)
|
|
||||||
AND there is a `Chat-Version` header\
|
|
||||||
AND the message is signed correctly
|
|
||||||
AND the From address is (also) in the encrypted (and therefore signed) headers <sup>[[1]](#myfootnote1)</sup>\
|
|
||||||
AND the message timestamp is newer than the contact's `lastseen` (to prevent changing the address back when messages arrive out of order) (this condition is not that important since we will have eventual consistency even without it):
|
|
||||||
|
|
||||||
Replace the contact in _all_ groups, possibly deduplicate the members list, and add a system message to all of these chats.
|
|
||||||
|
|
||||||
- Note that we can't simply compare the keys byte-by-byte, since the UID may have changed, or the sender may have rotated the key and signed the new key with the old one.
|
|
||||||
|
|
||||||
<a name="myfootnote1">[1]</a>: Without this check, an attacker could replay a message from Alice to Bob. Then Bob's device would do an AEAP transition from Alice's to the attacker's address, allowing for easier phishing.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>More details about this</summary>
|
|
||||||
Suppose Alice sends a message to Evil (or to a group with both Evil and Bob). Evil then forwards the message to Bob, changing the From and To headers (and if necessary Message-Id) and replacing `addr=alice@example.org;` in the autocrypt header with `addr=evil@example.org;`.
|
|
||||||
|
|
||||||
Then Bob's device sees that there is a message which is signed by Alice's key and comes from Evil's address and would do the AEAP transition, i.e. replace Alice with Evil in all groups and show a message "Alice changed their address from alice@example.org to evil@example.org". Disadvantages for Evil are that Bob's message will be shown on Alice's device, possibly creating confusion/suspicion, and that the usual "Setup changed for..." message will be shown the next time Evil sends a message (because Evil doesn't know Alice's private key).
|
|
||||||
|
|
||||||
Possible mitigations:
|
|
||||||
- if we make the AEAP device message sth. like "Automatically removed alice@example.org and added evil@example.org", then this will create more suspicion, making the phishing harder (we didn't talk about what what the wording should be at all yet).
|
|
||||||
- Add something similar to replay protection to our Autocrypt implementation. This could be done e.g. by adding a second `From` header to the protected headers. If it's present, the receiver then requires it to be the same as the outer `From`, and if it's not present, we don't do AEAP --> **That's what we implemented**
|
|
||||||
|
|
||||||
Note that usually a mail is signed by a key that has a UID matching the from address.
|
|
||||||
|
|
||||||
That's not mandatory for Autocrypt (and in fact, we just keep the old UID when changing the self address, so with AEAP the UID will actually be different than the from address sometimes)
|
|
||||||
|
|
||||||
https://autocrypt.org/level1.html#openpgp-based-key-data says:
|
|
||||||
> The content of the user id packet is only decorative
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Notes:
|
|
||||||
|
|
||||||
- We treat protected and non-protected chats the same
|
|
||||||
- We leave the aeap transition statement away since it seems not to be needed, makes things harder on the sending side, wastes some network traffic, and is worse for privacy (since more people know what old addresses you had).
|
|
||||||
- As soon as we encrypt read receipts, sending a read receipt will be enough to tell a lot of people that you transitioned
|
|
||||||
- AEAP will make the problem of inconsistent group state worse, both because it doesn't work if the message is unencrypted (even if the design allowed it, it would be problematic security-wise) and because some chat partners may have gotten the transition and some not. We should do something against this at some point in the future, like asking the user whether they want to add/remove the members to restore consistent group state.
|
|
||||||
|
|
||||||
#### Downsides of this design:
|
|
||||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
|
||||||
|
|
||||||
#### Upsides:
|
|
||||||
- With this approach, it's easy to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
|
||||||
- Faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.
|
|
||||||
|
|
||||||
### Alternatives and old discussions/plans:
|
|
||||||
|
|
||||||
- Change the contact instead of rewriting the group member lists. This seems to call for more trouble since we will end up with multiple contacts having the same email address.
|
|
||||||
|
|
||||||
- If needed, we could add a header a) indicating that the sender did an address transition or b) listing all the secondary (old) addresses. For now, there is no big enough benefit to warrant introducing another header and its processing on the receiver side (including all the necessary checks and handling of error cases). Instead, we only check for the `Chat-Version` header to prevent accidental transitions when an MUA user sends a message from another email address with the same key.
|
|
||||||
|
|
||||||
- The condition for a transition temporarily was:
|
|
||||||
|
|
||||||
> When receiving a message: If we are going to assign a message to a chat, but the sender is not a member of this chat\
|
|
||||||
> AND the signing key is the same as the direct (non-gossiped) key of one of the chat members\
|
|
||||||
> AND ...
|
|
||||||
|
|
||||||
However, this would mean that in 1:1 messages can't trigger a transition, since we don't assign private messages to the parent chat, but always to the 1:1 chat with the sender.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Some previous state of the discussion, which temporarily lived in an issue description</summary>
|
|
||||||
Summarizing the discussions from https://github.com/deltachat/deltachat-core-rust/pull/2896, mostly quoting @hpk42:
|
|
||||||
|
|
||||||
1. (DONE) At the time of configure we push the current primary to become a secondary.
|
|
||||||
|
|
||||||
2. When a message is sent out to a chat, and the message is encrypted, and we have secondary addresses, then we
|
|
||||||
a) add a protected "AEAP-Replacement" header that contains all secondary addresses
|
|
||||||
b) if any of the secondary addresses is in the chat's member list, we remove it and leave a system message that we did so
|
|
||||||
3. When an encrypted message with a replacement header is received, replace the e-mail address of all secondary contacts (if they exist) with the new primary and drop a sysmessage in all chats the secondary is member off. This might (in edge cases) result in chats that have two or more contacts with the same e-mail address. We might ignore this for a first release and just log a warning. Let's maybe not get hung up on this case before everything else works.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- for now we will send out aeap replacement headers forever, there is no termination condition other than lack of secondary addresses. I think that's fine for now. Later on we might introduce options to remove secondary addresses but i wouldn't do this for a first release/PR.
|
|
||||||
- the design is resilient against changing e-mail providers from A to B to C and then back to A, with partially updated chats and diverging views from recipients/contacts on this transition. In the end, you will have a primary and some secondaries, and when you start sending out messages everybody will eventually synchronize when they receive the current state of primaries/secondaries.
|
|
||||||
- of course on incoming message for need to check for each stated secondary address in the replacement header that it uses the same signature as the signature we verified as valid with the incoming message **--> Also we have to somehow make sure that the signing key was not just gossiped from some random other person in some group.**
|
|
||||||
- there are no extra flags/columns in the database needed (i hope)
|
|
||||||
|
|
||||||
#### Downsides of the chosen approach:
|
|
||||||
- Inconsistent group state: Suppose Alice does an AEAP transition and sends a 1:1 message to Bob, so Bob rewrites Alice's contact. Alice, Bob and Charlie are together in a group. Before Alice writes to this group, Bob and Charlie will have different membership lists, and Bob will send messages to Alice's new address, while Charlie will send them to her old address.
|
|
||||||
- There will be multiple contacts with the same address in the database. We will have to do something against this at some point.
|
|
||||||
|
|
||||||
The most obvious alternative would be to create a new contact with the new address and replace the old contact in the groups.
|
|
||||||
|
|
||||||
#### Upsides:
|
|
||||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
|
||||||
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
|
||||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
|
|
||||||
|
|
||||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
|
||||||
|
|
||||||
_end of the previous state of the discussion_
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
Other
|
|
||||||
-----
|
|
||||||
|
|
||||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
|
||||||
|
|
||||||
Notes during implementing
|
|
||||||
========================
|
|
||||||
|
|
||||||
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.
|
|
||||||
18
flake.lock
generated
18
flake.lock
generated
@@ -47,11 +47,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1747291057,
|
"lastModified": 1763361733,
|
||||||
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
|
"narHash": "sha256-ka7dpwH3HIXCyD2wl5F7cPLeRbqZoY2ullALsvxdPt8=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
|
"rev": "6c8d48e3b0ae371b19ac1485744687b788e80193",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -147,11 +147,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1747179050,
|
"lastModified": 1762977756,
|
||||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -202,11 +202,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1746889290,
|
"lastModified": 1762860488,
|
||||||
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
|
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
|
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
37
flake.nix
37
flake.nix
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
description = "Delta Chat core";
|
description = "Chatmail core";
|
||||||
inputs = {
|
inputs = {
|
||||||
fenix.url = "github:nix-community/fenix";
|
fenix.url = "github:nix-community/fenix";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
@@ -14,7 +14,15 @@
|
|||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
inherit (pkgs.stdenv) isDarwin;
|
inherit (pkgs.stdenv) isDarwin;
|
||||||
fenixPkgs = fenix.packages.${system};
|
fenixPkgs = fenix.packages.${system};
|
||||||
naersk' = pkgs.callPackage naersk { };
|
fenixToolchain = fenixPkgs.combine [
|
||||||
|
fenixPkgs.stable.rustc
|
||||||
|
fenixPkgs.stable.cargo
|
||||||
|
fenixPkgs.stable.rust-std
|
||||||
|
];
|
||||||
|
naersk' = pkgs.callPackage naersk {
|
||||||
|
cargo = fenixToolchain;
|
||||||
|
rustc = fenixToolchain;
|
||||||
|
};
|
||||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||||
builtins.attrValues {
|
builtins.attrValues {
|
||||||
@@ -34,7 +42,6 @@
|
|||||||
./Cargo.lock
|
./Cargo.lock
|
||||||
./Cargo.toml
|
./Cargo.toml
|
||||||
./CMakeLists.txt
|
./CMakeLists.txt
|
||||||
./CONTRIBUTING.md
|
|
||||||
./deltachat_derive
|
./deltachat_derive
|
||||||
./deltachat-contact-tools
|
./deltachat-contact-tools
|
||||||
./deltachat-ffi
|
./deltachat-ffi
|
||||||
@@ -98,9 +105,6 @@
|
|||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.perl # Needed to build vendored OpenSSL.
|
pkgs.perl # Needed to build vendored OpenSSL.
|
||||||
];
|
];
|
||||||
buildInputs = pkgs.lib.optionals isDarwin [
|
|
||||||
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
|
|
||||||
];
|
|
||||||
auditable = false; # Avoid cargo-auditable failures.
|
auditable = false; # Avoid cargo-auditable failures.
|
||||||
doCheck = false; # Disable test as it requires network access.
|
doCheck = false; # Disable test as it requires network access.
|
||||||
};
|
};
|
||||||
@@ -240,6 +244,9 @@
|
|||||||
auditable = false; # Avoid cargo-auditable failures.
|
auditable = false; # Avoid cargo-auditable failures.
|
||||||
doCheck = false; # Disable test as it requires network access.
|
doCheck = false; # Disable test as it requires network access.
|
||||||
|
|
||||||
|
CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||||
|
CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS = "-Clink-args=-L${pkgsCross.libiconv}/lib";
|
||||||
|
|
||||||
CARGO_BUILD_TARGET = rustTarget;
|
CARGO_BUILD_TARGET = rustTarget;
|
||||||
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
|
||||||
CARGO_BUILD_RUSTFLAGS = [
|
CARGO_BUILD_RUSTFLAGS = [
|
||||||
@@ -471,6 +478,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
libdeltachat =
|
libdeltachat =
|
||||||
|
let
|
||||||
|
rustPlatform = (pkgs.makeRustPlatform {
|
||||||
|
cargo = fenixToolchain;
|
||||||
|
rustc = fenixToolchain;
|
||||||
|
});
|
||||||
|
in
|
||||||
pkgs.stdenv.mkDerivation {
|
pkgs.stdenv.mkDerivation {
|
||||||
pname = "libdeltachat";
|
pname = "libdeltachat";
|
||||||
version = manifest.version;
|
version = manifest.version;
|
||||||
@@ -480,14 +493,9 @@
|
|||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.perl # Needed to build vendored OpenSSL.
|
pkgs.perl # Needed to build vendored OpenSSL.
|
||||||
pkgs.cmake
|
pkgs.cmake
|
||||||
pkgs.rustPlatform.cargoSetupHook
|
rustPlatform.cargoSetupHook
|
||||||
pkgs.cargo
|
fenixPkgs.stable.rustc
|
||||||
];
|
fenixPkgs.stable.cargo
|
||||||
buildInputs = pkgs.lib.optionals isDarwin [
|
|
||||||
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
|
|
||||||
pkgs.darwin.apple_sdk.frameworks.Security
|
|
||||||
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
|
|
||||||
pkgs.libiconv
|
|
||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
@@ -587,6 +595,7 @@
|
|||||||
(python3.withPackages (pypkgs: with pypkgs; [
|
(python3.withPackages (pypkgs: with pypkgs; [
|
||||||
tox
|
tox
|
||||||
]))
|
]))
|
||||||
|
nodejs
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bolero = "0.13.3"
|
bolero = "0.13.4"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mailparse = { workspace = true }
|
mailparse = { workspace = true }
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ def datadir():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip("The test is flaky in CI and crashes the interpreter as of 2025-11-12")
|
||||||
def test_echo_quit_plugin(acfactory, lp):
|
def test_echo_quit_plugin(acfactory, lp):
|
||||||
lp.sec("creating one echo_and_quit bot")
|
lp.sec("creating one echo_and_quit bot")
|
||||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
requires = ["setuptools>=77", "wheel", "cffi>=1.0.0", "pkgconfig"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "2.2.0"
|
version = "2.50.0-dev"
|
||||||
|
license = "MPL-2.0"
|
||||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.10"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Topic :: Communications :: Chat",
|
"Topic :: Communications :: Chat",
|
||||||
"Topic :: Communications :: Email",
|
"Topic :: Communications :: Email",
|
||||||
@@ -23,7 +23,6 @@ classifiers = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cffi>=1.0.0",
|
"cffi>=1.0.0",
|
||||||
"imap-tools",
|
"imap-tools",
|
||||||
"importlib_metadata;python_version<'3.8'",
|
|
||||||
"pluggy",
|
"pluggy",
|
||||||
"requests",
|
"requests",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -330,7 +330,21 @@ class Account:
|
|||||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||||
|
|
||||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
||||||
"""get a contact for the email address or None if it's blocked or doesn't exist."""
|
"""Looks up a known and unblocked contact with a given e-mail address.
|
||||||
|
To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||||
|
|
||||||
|
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
||||||
|
(e.g. an address-contact and a key-contact),
|
||||||
|
this looks up the most recently seen contact,
|
||||||
|
i.e. which contact is returned depends on which contact last sent a message.
|
||||||
|
If the user just clicked on a mailto: link, then this is the best thing you can do.
|
||||||
|
But **DO NOT** internally represent contacts by their email address
|
||||||
|
and do not use this function to look them up;
|
||||||
|
otherwise this function will sometimes look up the wrong contact.
|
||||||
|
Instead, you should internally represent contacts by their ids.
|
||||||
|
|
||||||
|
To validate an e-mail address independently of the contact database
|
||||||
|
use check_email_validity()."""
|
||||||
_, addr = parseaddr(email)
|
_, addr = parseaddr(email)
|
||||||
addr = as_dc_charpointer(addr)
|
addr = as_dc_charpointer(addr)
|
||||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
||||||
@@ -390,18 +404,16 @@ class Account:
|
|||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
contacts: Optional[List[Contact]] = None,
|
contacts: Optional[List[Contact]] = None,
|
||||||
verified: bool = False,
|
|
||||||
) -> Chat:
|
) -> Chat:
|
||||||
"""create a new group chat object.
|
"""create a new group chat object.
|
||||||
|
|
||||||
Chats are unpromoted until the first message is sent.
|
Chats are unpromoted until the first message is sent.
|
||||||
|
|
||||||
:param contacts: list of contacts to add
|
:param contacts: list of contacts to add
|
||||||
:param verified: if true only verified contacts can be added.
|
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
:returns: a :class:`deltachat.chat.Chat` object.
|
||||||
"""
|
"""
|
||||||
bytes_name = name.encode("utf8")
|
bytes_name = name.encode("utf8")
|
||||||
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
chat_id = lib.dc_create_group_chat(self._dc_context, 0, bytes_name)
|
||||||
chat = Chat(self, chat_id)
|
chat = Chat(self, chat_id)
|
||||||
if contacts is not None:
|
if contacts is not None:
|
||||||
for contact in contacts:
|
for contact in contacts:
|
||||||
@@ -541,17 +553,6 @@ class Account:
|
|||||||
def imex(self, path: str, imex_cmd: int, passphrase: Optional[str] = None) -> None:
|
def imex(self, path: str, imex_cmd: int, passphrase: Optional[str] = None) -> None:
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), as_dc_charpointer(passphrase))
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), as_dc_charpointer(passphrase))
|
||||||
|
|
||||||
def initiate_key_transfer(self) -> str:
|
|
||||||
"""return setup code after a Autocrypt setup message
|
|
||||||
has been successfully sent to our own e-mail address ("self-sent message").
|
|
||||||
If sending out was unsuccessful, a RuntimeError is raised.
|
|
||||||
"""
|
|
||||||
self.check_is_configured()
|
|
||||||
res = lib.dc_initiate_key_transfer(self._dc_context)
|
|
||||||
if res == ffi.NULL:
|
|
||||||
raise RuntimeError("could not send out autocrypt setup message")
|
|
||||||
return from_dc_charpointer(res)
|
|
||||||
|
|
||||||
def get_setup_contact_qr(self) -> str:
|
def get_setup_contact_qr(self) -> str:
|
||||||
"""get/create Setup-Contact QR Code as ascii-string.
|
"""get/create Setup-Contact QR Code as ascii-string.
|
||||||
|
|
||||||
|
|||||||
@@ -142,13 +142,6 @@ class Chat:
|
|||||||
"""
|
"""
|
||||||
return bool(lib.dc_chat_can_send(self._dc_chat))
|
return bool(lib.dc_chat_can_send(self._dc_chat))
|
||||||
|
|
||||||
def is_protected(self) -> bool:
|
|
||||||
"""return True if this chat is a protected chat.
|
|
||||||
|
|
||||||
:returns: True if chat is protected, False otherwise.
|
|
||||||
"""
|
|
||||||
return bool(lib.dc_chat_is_protected(self._dc_chat))
|
|
||||||
|
|
||||||
def get_name(self) -> Optional[str]:
|
def get_name(self) -> Optional[str]:
|
||||||
"""return name of this chat.
|
"""return name of this chat.
|
||||||
|
|
||||||
@@ -278,15 +271,6 @@ class Chat:
|
|||||||
sent out. This is the same object as was passed in, which
|
sent out. This is the same object as was passed in, which
|
||||||
has been modified with the new state of the core.
|
has been modified with the new state of the core.
|
||||||
"""
|
"""
|
||||||
if msg.is_out_preparing():
|
|
||||||
assert msg.id != 0
|
|
||||||
# get a fresh copy of dc_msg, the core needs it
|
|
||||||
maybe_msg = Message.from_db(self.account, msg.id)
|
|
||||||
if maybe_msg is not None:
|
|
||||||
msg = maybe_msg
|
|
||||||
else:
|
|
||||||
raise ValueError("message does not exist")
|
|
||||||
|
|
||||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
@@ -340,26 +324,6 @@ class Chat:
|
|||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
return Message.from_db(self.account, sent_id)
|
return Message.from_db(self.account, sent_id)
|
||||||
|
|
||||||
def send_prepared(self, message):
|
|
||||||
"""send a previously prepared message.
|
|
||||||
|
|
||||||
:param message: a :class:`Message` instance previously returned by
|
|
||||||
:meth:`prepare_file`.
|
|
||||||
:raises ValueError: if message can not be sent.
|
|
||||||
:returns: a :class:`deltachat.message.Message` instance as sent out.
|
|
||||||
"""
|
|
||||||
assert message.id != 0 and message.is_out_preparing()
|
|
||||||
# get a fresh copy of dc_msg, the core needs it
|
|
||||||
msg = Message.from_db(self.account, message.id)
|
|
||||||
|
|
||||||
# pass 0 as chat-id because core-docs say it's ok when out-preparing
|
|
||||||
sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
|
|
||||||
if sent_id == 0:
|
|
||||||
raise ValueError("message could not be sent")
|
|
||||||
assert sent_id == msg.id
|
|
||||||
# modify message in place to avoid bad state for the caller
|
|
||||||
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
|
|
||||||
|
|
||||||
def set_draft(self, message):
|
def set_draft(self, message):
|
||||||
"""set message as draft.
|
"""set message as draft.
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user