mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-09-01 06:28:11 +00:00
Compare commits
758 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c17e01c37 | ||
|
|
9605c94f09 | ||
|
|
d2f5696df1 | ||
|
|
81e513d6b7 | ||
|
|
dbf11fd006 | ||
|
|
7e802166a3 | ||
|
|
0f88410465 | ||
|
|
13819c8f09 | ||
|
|
85580d1ff6 | ||
|
|
f8ea8ed314 | ||
|
|
472784e614 | ||
|
|
db97731f63 | ||
|
|
ff9f2028ae | ||
|
|
9ee5262876 | ||
|
|
b01b6b370d | ||
|
|
890f798a03 | ||
|
|
3c319f8b4a | ||
|
|
fd3f572499 | ||
|
|
854cb35d15 | ||
|
|
e78521bb5c | ||
|
|
86ad7bf53f | ||
|
|
de5daf0ad4 | ||
|
|
ecc76ec52b | ||
|
|
6fa1d1bf90 | ||
|
|
d29f6c1641 | ||
|
|
b3e3eda4f7 | ||
|
|
1e59c6f6cc | ||
|
|
457658aca0 | ||
|
|
2cf5e25e9e | ||
|
|
151b42c6e9 | ||
|
|
91138452bb | ||
|
|
304ffb0e33 | ||
|
|
987832099d | ||
|
|
64a629002d | ||
|
|
c26e0632c5 | ||
|
|
21dd00d96a | ||
|
|
95c6dae927 | ||
|
|
bb2409a7f2 | ||
|
|
89756bb7a7 | ||
|
|
6808b66897 | ||
|
|
14f98602d2 | ||
|
|
fd98cce8b3 | ||
|
|
4f4a4b9fb4 | ||
|
|
e15c29e4e8 | ||
|
|
2e367644d9 | ||
|
|
78dba20a59 | ||
|
|
b48ccf2605 | ||
|
|
54d713ea7d | ||
|
|
4dda7582a3 | ||
|
|
248c0c0dab | ||
|
|
e70bddd790 | ||
|
|
ac5ed431d3 | ||
|
|
fb10e1a501 | ||
|
|
2a744edd43 | ||
|
|
c95a67862e | ||
|
|
5a525f51f6 | ||
|
|
eab6ffbd8c | ||
|
|
8809b90b72 | ||
|
|
98d9960e66 | ||
|
|
254efba643 | ||
|
|
bb433ebe59 | ||
|
|
5ac06a5ccd | ||
|
|
debb8a20e6 | ||
|
|
6bef205b3f | ||
|
|
443b4da8f8 | ||
|
|
afe8c24665 | ||
|
|
9f0676d563 | ||
|
|
b3c1b6a4f6 | ||
|
|
bf1376d74d | ||
|
|
de678596c6 | ||
|
|
29e65a5c8e | ||
|
|
60779c91c6 | ||
|
|
bda4e7fc0f | ||
|
|
12ffc34967 | ||
|
|
9e3585dcce | ||
|
|
46c61c5aac | ||
|
|
8d6264e02e | ||
|
|
f057c5031c | ||
|
|
40b1928844 | ||
|
|
2a827f1285 | ||
|
|
ced9560040 | ||
|
|
44d54b3215 | ||
|
|
393e1efcd1 | ||
|
|
f34b552054 | ||
|
|
46ce552589 | ||
|
|
6da5c37504 | ||
|
|
4601e60118 | ||
|
|
fcf82b9318 | ||
|
|
9c8137fafa | ||
|
|
0a1531dcce | ||
|
|
c10f618a3e | ||
|
|
4a3d9d90a6 | ||
|
|
b4275414e1 | ||
|
|
5f94b7b6d1 | ||
|
|
dc43e44020 | ||
|
|
251bd2d87a | ||
|
|
a3fcda7598 | ||
|
|
c9e2504349 | ||
|
|
9b09df5f17 | ||
|
|
5fe94ff44a | ||
|
|
6286a054eb | ||
|
|
e6635d1bb0 | ||
|
|
056878fad7 | ||
|
|
da214817be | ||
|
|
d6edaf3be2 | ||
|
|
47e50988b5 | ||
|
|
aa446619f2 | ||
|
|
6405655127 | ||
|
|
b8d990b0f9 | ||
|
|
417d2ce971 | ||
|
|
33b2b563b3 | ||
|
|
740cd6f89b | ||
|
|
7887ed408d | ||
|
|
ddfad2c4ce | ||
|
|
7e95ea7403 | ||
|
|
2991cdafe7 | ||
|
|
561dfc373f | ||
|
|
5bfb044245 | ||
|
|
e1b17bf863 | ||
|
|
aafb40fd94 | ||
|
|
7dc55eba81 | ||
|
|
a03d17a9e4 | ||
|
|
364f89f1d0 | ||
|
|
d0ee90dbbc | ||
|
|
398faa50b0 | ||
|
|
e9eabbeeb5 | ||
|
|
132dfb95dc | ||
|
|
2651823d4d | ||
|
|
4709cfacc7 | ||
|
|
9bc4c0ecd8 | ||
|
|
763ddf85e6 | ||
|
|
b2bab0d0dc | ||
|
|
62fc96c4c9 | ||
|
|
2667688139 | ||
|
|
990d1eab58 | ||
|
|
e6b33b8da7 | ||
|
|
d40f62ec21 | ||
|
|
265369e353 | ||
|
|
d1106299fe | ||
|
|
7919a0f4aa | ||
|
|
7a8a34f45e | ||
|
|
0a777ea7df | ||
|
|
103a0807ca | ||
|
|
135d3a1677 | ||
|
|
59a0bd87cd | ||
|
|
c94da00212 | ||
|
|
36649a8526 | ||
|
|
3297acd3f4 | ||
|
|
3e5caf9284 | ||
|
|
53ae9e4266 | ||
|
|
313f5392ef | ||
|
|
7d89375d3a | ||
|
|
db0f660d08 | ||
|
|
b498d2050a | ||
|
|
27a722dc75 | ||
|
|
7014f629fe | ||
|
|
30b57bdb3d | ||
|
|
b94162afbc | ||
|
|
2885ffeee8 | ||
|
|
6071291f16 | ||
|
|
37b8a4a996 | ||
|
|
af56a28b94 | ||
|
|
dfc7e3b495 | ||
|
|
e9114ae8fc | ||
|
|
f77a74d93f | ||
|
|
5cda87ee0e | ||
|
|
7384407823 | ||
|
|
7fa56a37fd | ||
|
|
8fcd953ece | ||
|
|
775236efc3 | ||
|
|
4b8dec26a9 | ||
|
|
d94e05c38c | ||
|
|
1bbf98fac0 | ||
|
|
c70515035f | ||
|
|
a9d235b7f1 | ||
|
|
92ded3fdf2 | ||
|
|
aa1ed9e233 | ||
|
|
3b6c199b1d | ||
|
|
6d22ceef24 | ||
|
|
54ff59737e | ||
|
|
516a37ba69 | ||
|
|
6a6bebd503 | ||
|
|
f9cbfa6d6c | ||
|
|
d4b3816c5d | ||
|
|
4a35d47515 | ||
|
|
52d4d61e2b | ||
|
|
5bff902394 | ||
|
|
f33eb86335 | ||
|
|
2ea26b9d1b | ||
|
|
10fa3e1619 | ||
|
|
956e17c81c | ||
|
|
6f749352d8 | ||
|
|
5f3f6c071b | ||
|
|
5795d43d0d | ||
|
|
de42f55e37 | ||
|
|
7e93a15204 | ||
|
|
715f819c3e | ||
|
|
345de8fb5d | ||
|
|
667a47c03a | ||
|
|
5dd5e304bd | ||
|
|
60a1589616 | ||
|
|
4d1d28672d | ||
|
|
fefca7d837 | ||
|
|
d9f5a573cd | ||
|
|
4c77cde9da | ||
|
|
658b098c3a | ||
|
|
70644ba31b | ||
|
|
06a706d070 | ||
|
|
14297986f2 | ||
|
|
0bd4d554d8 | ||
|
|
32c8d4f801 | ||
|
|
82abc20871 | ||
|
|
e8ab01f665 | ||
|
|
dee557a9ad | ||
|
|
f1fa2eba1d | ||
|
|
ccd58bbf23 | ||
|
|
54700d9cd0 | ||
|
|
984ea47f9d | ||
|
|
9458972d15 | ||
|
|
8eb9662694 | ||
|
|
a9be1aa608 | ||
|
|
3af9dff0ed | ||
|
|
c5e4b250b8 | ||
|
|
5fafa24974 | ||
|
|
b26c521930 | ||
|
|
eb52380ecf | ||
|
|
f1de69d7ff | ||
|
|
ba2214d8c7 | ||
|
|
fca4d7459c | ||
|
|
ad2338b898 | ||
|
|
c237f98044 | ||
|
|
87945ac506 | ||
|
|
11d96c894d | ||
|
|
8dcc16d640 | ||
|
|
9a1ddd0d41 | ||
|
|
b8bb58b083 | ||
|
|
7feba3c8c7 | ||
|
|
dae44c81d2 | ||
|
|
c2ac74a6be | ||
|
|
62811d92c5 | ||
|
|
feaee2bfe1 | ||
|
|
91701f609a | ||
|
|
d27aea2220 | ||
|
|
e832a57f35 | ||
|
|
13e6445498 | ||
|
|
552ce6b06d | ||
|
|
c3bce6730d | ||
|
|
5d2daedba3 | ||
|
|
6ee043449b | ||
|
|
5510d1e20d | ||
|
|
cad61dae37 | ||
|
|
1fcedca0f1 | ||
|
|
7b08225c57 | ||
|
|
06fd350e9c | ||
|
|
9ee747271a | ||
|
|
ef3c1ea0e7 | ||
|
|
3b784aa32b | ||
|
|
05a9635dc4 | ||
|
|
6abe3c7a06 | ||
|
|
98b46353ea | ||
|
|
af0f4a6b01 | ||
|
|
96ec6624c5 | ||
|
|
9f1ae5f4db | ||
|
|
1d06f042e1 | ||
|
|
761bcdd064 | ||
|
|
c108d59cfc | ||
|
|
429c4dca07 | ||
|
|
52d818be45 | ||
|
|
21a546d2bc | ||
|
|
e65fee0efb | ||
|
|
29dc873535 | ||
|
|
69d892e5b1 | ||
|
|
3be0aefa20 | ||
|
|
a399666fba | ||
|
|
1af03e3e16 | ||
|
|
7a82283762 | ||
|
|
fb92bc3894 | ||
|
|
a22af8303a | ||
|
|
371dc06842 | ||
|
|
e60b80effc | ||
|
|
b9008f0b66 | ||
|
|
2239231601 | ||
|
|
9a45138d8b | ||
|
|
edd52a97b4 | ||
|
|
f469a4d166 | ||
|
|
b2076cfc12 | ||
|
|
991ff04196 | ||
|
|
597f0368cf | ||
|
|
c37d8c6ce1 | ||
|
|
61bc30eb43 | ||
|
|
f6d81e3c05 | ||
|
|
42f10670b6 | ||
|
|
b453d7a0b9 | ||
|
|
f9a36c6e04 | ||
|
|
be48afb2b5 | ||
|
|
a0960fcabd | ||
|
|
dbc454ba9e | ||
|
|
2225e69277 | ||
|
|
783201d12e | ||
|
|
3e981d66e9 | ||
|
|
7c7fc76a64 | ||
|
|
c924d5c03a | ||
|
|
dc787be17b | ||
|
|
3d4070a139 | ||
|
|
dbdff83132 | ||
|
|
4ce194afe2 | ||
|
|
ca33249170 | ||
|
|
a96626c468 | ||
|
|
d54be747da | ||
|
|
ff846bc678 | ||
|
|
1b7f755590 | ||
|
|
887ed3bb44 | ||
|
|
3180eba836 | ||
|
|
cb06cbdcca | ||
|
|
069325af47 | ||
|
|
e7ca02f1fb | ||
|
|
fa9bb3c210 | ||
|
|
e6113d4d96 | ||
|
|
6cc3a6f561 | ||
|
|
70c79eac01 | ||
|
|
5dc66f839d | ||
|
|
a0d5744c49 | ||
|
|
6b60a6d5a5 | ||
|
|
0257344940 | ||
|
|
17cd99be59 | ||
|
|
2f8328847c | ||
|
|
7e9727aa38 | ||
|
|
bf87fcc652 | ||
|
|
6b46314eab | ||
|
|
e89803464b | ||
|
|
a9bb8d9aae | ||
|
|
74909408c4 | ||
|
|
bb124a922d | ||
|
|
56e11d0857 | ||
|
|
d0d0021f57 | ||
|
|
7aafb05995 | ||
|
|
e594f3b237 | ||
|
|
bb86830a61 | ||
|
|
bcc1eadc7d | ||
|
|
4fd9e55c3c | ||
|
|
a2900085c9 | ||
|
|
5e11cf1c50 | ||
|
|
4e455d85d6 | ||
|
|
1e685c7cab | ||
|
|
ce813e4529 | ||
|
|
bd7948e246 | ||
|
|
b998f322f5 | ||
|
|
db2182aa7d | ||
|
|
69a9b30732 | ||
|
|
3dc8844cb4 | ||
|
|
adb6787d5b | ||
|
|
14b07be0dc | ||
|
|
6befda7ef1 | ||
|
|
67302eb9c3 | ||
|
|
f18015ff2e | ||
|
|
1295ef69ca | ||
|
|
f26a0d2891 | ||
|
|
2b150112ff | ||
|
|
7aede7c17f | ||
|
|
b92cbc6a7c | ||
|
|
68b7416e57 | ||
|
|
4feba68afd | ||
|
|
4eb34c7a93 | ||
|
|
26fd3e379a | ||
|
|
93d281e712 | ||
|
|
985af6e445 | ||
|
|
5693d871f7 | ||
|
|
dba8cf7a6f | ||
|
|
141d3326ab | ||
|
|
d3d2caac5a | ||
|
|
e1f4dae5c2 | ||
|
|
cf5c943127 | ||
|
|
ed79e0b377 | ||
|
|
a089a5ef04 | ||
|
|
90145655f4 | ||
|
|
3cd07ae9cd | ||
|
|
8aa71c132f | ||
|
|
b579935846 | ||
|
|
dfa886fae9 | ||
|
|
f04f789231 | ||
|
|
a6ec71dc31 | ||
|
|
47d65586cd | ||
|
|
b8d8413a22 | ||
|
|
5e16123632 | ||
|
|
d57442bd2a | ||
|
|
70313c45a9 | ||
|
|
f14c204764 | ||
|
|
71d3b83a1c | ||
|
|
148bf7dee2 | ||
|
|
2d1ba6b4ca | ||
|
|
055a8ee8b9 | ||
|
|
407a20d4bb | ||
|
|
05cd6aee6a | ||
|
|
a1378507b2 | ||
|
|
78cd0b13de | ||
|
|
c25468a71e | ||
|
|
a5d2e1ea23 | ||
|
|
6acf16ef4e | ||
|
|
e11e093020 | ||
|
|
74c2604dc8 | ||
|
|
e4af0be0ad | ||
|
|
5ac5938c8b | ||
|
|
94269744ad | ||
|
|
7a25ae5b9c | ||
|
|
cbd92654cf | ||
|
|
bd95373a70 | ||
|
|
d982633215 | ||
|
|
f91ca82902 | ||
|
|
c55ee85c5c | ||
|
|
a3776c88bd | ||
|
|
4a781656b4 | ||
|
|
11d38f29ef | ||
|
|
22a0ff976a | ||
|
|
c05b47e4d0 | ||
|
|
ac145e6a27 | ||
|
|
f00b8523d9 | ||
|
|
c3f8d68ceb | ||
|
|
9d92a3e06b | ||
|
|
f2df600d38 | ||
|
|
24d344fda4 | ||
|
|
0a296e77a0 | ||
|
|
ba147a48f8 | ||
|
|
77a5c454b7 | ||
|
|
2c68b5a9e1 | ||
|
|
68c9d84d19 | ||
|
|
fe752e0c79 | ||
|
|
26b5a4c582 | ||
|
|
10ee295ea3 | ||
|
|
6a5ea5fc01 | ||
|
|
ff6cb5262a | ||
|
|
f2005593ec | ||
|
|
3533500b73 | ||
|
|
e5251ae158 | ||
|
|
a5e272be3f | ||
|
|
277652e3f2 | ||
|
|
181086aefe | ||
|
|
a1d552698a | ||
|
|
1b959608c3 | ||
|
|
acddef6e35 | ||
|
|
0e77870b59 | ||
|
|
0a287b0b3e | ||
|
|
5a4f4ba6db | ||
|
|
5171107a29 | ||
|
|
47e6fc1769 | ||
|
|
9afd4e4328 | ||
|
|
eac2a47163 | ||
|
|
5646f65195 | ||
|
|
fab1b96c21 | ||
|
|
91eacc18c2 | ||
|
|
7f1fc932ad | ||
|
|
304c44064d | ||
|
|
946c483f35 | ||
|
|
69d691f416 | ||
|
|
a6ab8f7e80 | ||
|
|
19dc2d446b | ||
|
|
8524037900 | ||
|
|
dbbc3fbd71 | ||
|
|
c6e93126fa | ||
|
|
352699c4b6 | ||
|
|
bea2772491 | ||
|
|
bab8ddf35a | ||
|
|
1d5d16f57e | ||
|
|
6f9e9e9302 | ||
|
|
20add0f27b | ||
|
|
eca3c6fa30 | ||
|
|
a0d1b081ff | ||
|
|
d852c60c37 | ||
|
|
2b5451f74a | ||
|
|
b322716215 | ||
|
|
41726d339f | ||
|
|
52bb92dce4 | ||
|
|
65adfdd6f5 | ||
|
|
d8b1a2fffe | ||
|
|
ea436ecb64 | ||
|
|
82fbde4f19 | ||
|
|
19b15e68e4 | ||
|
|
b51f849fe6 | ||
|
|
7cc0ef1c70 | ||
|
|
485c4fd467 | ||
|
|
bda395191b | ||
|
|
cb129db95e | ||
|
|
130cee9906 | ||
|
|
6bdc9a4b16 | ||
|
|
a69a9b7b0e | ||
|
|
6ea373fbd5 | ||
|
|
62da514850 | ||
|
|
fe3934171d | ||
|
|
3d11221732 | ||
|
|
e961488b1a | ||
|
|
2db3d3259e | ||
|
|
e51b1ee23a | ||
|
|
5ff66728e3 | ||
|
|
baf7b74a61 | ||
|
|
c716f94649 | ||
|
|
8b355918e8 | ||
|
|
67012b40b1 | ||
|
|
fd402b52c8 | ||
|
|
5a97b9e134 | ||
|
|
17596795c2 | ||
|
|
10b9c264fd | ||
|
|
a2b002ac5e | ||
|
|
6d764db114 | ||
|
|
777cfbb69f | ||
|
|
9781c56571 | ||
|
|
04cf54263e | ||
|
|
6baf0eac13 | ||
|
|
fb21a42cce | ||
|
|
53d7e0f08b | ||
|
|
8f756cd90c | ||
|
|
fb81bf1d05 | ||
|
|
04726f005c | ||
|
|
09e3e7f335 | ||
|
|
90b1e4bc02 | ||
|
|
3f31f1a8a6 | ||
|
|
3cd8e323c9 | ||
|
|
7060faf5d3 | ||
|
|
c9f2cca024 | ||
|
|
f0054372b8 | ||
|
|
0a82c51b79 | ||
|
|
cef83d962c | ||
|
|
c1775913b9 | ||
|
|
e4c5144fbf | ||
|
|
8aeaf927e6 | ||
|
|
7e0d4c9b89 | ||
|
|
71de8e63cc | ||
|
|
e456d06cb0 | ||
|
|
e0cd5b987e | ||
|
|
e5ebb732cb | ||
|
|
419beee29a | ||
|
|
d4e1f9b7f1 | ||
|
|
edce33ae15 | ||
|
|
95f9e18de2 | ||
|
|
95d4b7d012 | ||
|
|
17c24b3ff2 | ||
|
|
be0e8ddd8a | ||
|
|
49cc9cd9f8 | ||
|
|
c85c995fef | ||
|
|
dda23e76ac | ||
|
|
95e70b9d15 | ||
|
|
abddf24752 | ||
|
|
d356d92b5e | ||
|
|
8b4f377cf1 | ||
|
|
323a801600 | ||
|
|
3372992dc2 | ||
|
|
ee39733978 | ||
|
|
5e17fe8414 | ||
|
|
aebe64571d | ||
|
|
0cc2da690a | ||
|
|
2424fc1f53 | ||
|
|
2e4cd0eddc | ||
|
|
189b21dbde | ||
|
|
e77d9e3d60 | ||
|
|
df76aa9919 | ||
|
|
378ac23c6c | ||
|
|
fc2ae856d2 | ||
|
|
57164ad7fb | ||
|
|
6c44662496 | ||
|
|
22ac3cb50f | ||
|
|
b76964f219 | ||
|
|
2f3c064462 | ||
|
|
c9002d9481 | ||
|
|
83d471818d | ||
|
|
0bb2a64781 | ||
|
|
08ba774b71 | ||
|
|
59c1f4eed2 | ||
|
|
f1e3b5c9cc | ||
|
|
7bc7242f08 | ||
|
|
6f407ab509 | ||
|
|
db9acaf4ff | ||
|
|
7821815429 | ||
|
|
cd88e896fa | ||
|
|
67fd10b978 | ||
|
|
9051f68ba6 | ||
|
|
3b3377e6e9 | ||
|
|
fb99ff5284 | ||
|
|
ed40d116b7 | ||
|
|
d84362eb0f | ||
|
|
2c0ad7feb7 | ||
|
|
7206b4da25 | ||
|
|
fd671576a4 | ||
|
|
6cd57312a1 | ||
|
|
25258db55d | ||
|
|
91ed49e019 | ||
|
|
bd792f5b7f | ||
|
|
ed4b1e8f02 | ||
|
|
e78146eb67 | ||
|
|
9f6b6cb657 | ||
|
|
fb85e1a068 | ||
|
|
c78299e7bc | ||
|
|
9a5e29e015 | ||
|
|
7cf3a989bf | ||
|
|
d486563099 | ||
|
|
be699cbd85 | ||
|
|
4e61f2b2e5 | ||
|
|
1bf703b012 | ||
|
|
a48fdaf8b7 | ||
|
|
a0f33ac942 | ||
|
|
bea659f948 | ||
|
|
7fb263fdf0 | ||
|
|
3be1c24b1b | ||
|
|
0b33cb55b8 | ||
|
|
00e71ed0fc | ||
|
|
a40810e33e | ||
|
|
15c9d04703 | ||
|
|
e13dcdc85a | ||
|
|
2ab5b2817e | ||
|
|
080c14d111 | ||
|
|
d1e32f2b11 | ||
|
|
0840e0dedb | ||
|
|
888d6bf091 | ||
|
|
e2f308a57a | ||
|
|
76fe6ad799 | ||
|
|
90df256e85 | ||
|
|
f92466f6be | ||
|
|
6a5dcd00b2 | ||
|
|
6c65de8ddf | ||
|
|
f696097301 | ||
|
|
7d3db03d4a | ||
|
|
78c87e501e | ||
|
|
2974b466aa | ||
|
|
ca1a852b44 | ||
|
|
ac2343b142 | ||
|
|
be9efb9a25 | ||
|
|
30e8e36635 | ||
|
|
3290a5bf4d | ||
|
|
c07ba14fc6 | ||
|
|
c6e9e19858 | ||
|
|
375bdb7948 | ||
|
|
5e7899675e | ||
|
|
328ba0202f | ||
|
|
59cbbde835 | ||
|
|
caa4fa0180 | ||
|
|
fc6a4b78eb | ||
|
|
2729772adb | ||
|
|
70e0f7027e | ||
|
|
4dcdffda0a | ||
|
|
2535cd9c2c | ||
|
|
d3eef6c820 | ||
|
|
35fb1d60af | ||
|
|
089ec22666 | ||
|
|
66077f317b | ||
|
|
5904bfa92b | ||
|
|
2c1d7a803f | ||
|
|
6d5b8b6a7b | ||
|
|
b88f8c205e | ||
|
|
ce4afbe4c2 | ||
|
|
fcaecef961 | ||
|
|
81d2e315e3 | ||
|
|
fddf982832 | ||
|
|
3602ef9be9 | ||
|
|
37c65ca6b4 | ||
|
|
77f284661b | ||
|
|
0d60c4d464 | ||
|
|
f06eeb01b9 | ||
|
|
fbcc1cfb50 | ||
|
|
26cef99cdf | ||
|
|
5cac7feabe | ||
|
|
d844e0f396 | ||
|
|
ed11bf6368 | ||
|
|
eaa6b7cf57 | ||
|
|
f8ea631b03 | ||
|
|
7b0744ec75 | ||
|
|
9f4a2b3e26 | ||
|
|
9741c93ce9 | ||
|
|
7e9940be4a | ||
|
|
c0aa338d7c | ||
|
|
b20978e08e | ||
|
|
5d33f71d4d | ||
|
|
1058e33f12 | ||
|
|
b7fedff511 | ||
|
|
f252597002 | ||
|
|
cbbfc4ea6e | ||
|
|
6b04197eaa | ||
|
|
3027ba2cf1 | ||
|
|
19e9f31afd | ||
|
|
5a1d2580cb | ||
|
|
cb5cace8da | ||
|
|
e734c125ad | ||
|
|
d351f64fb1 | ||
|
|
e61f587bfc | ||
|
|
4e8f0a41c7 | ||
|
|
4fbc97c92a | ||
|
|
1e35ac380e | ||
|
|
85b0647a3e | ||
|
|
7b899d1853 | ||
|
|
699b21f066 | ||
|
|
8d423adb4d | ||
|
|
506dcfa6c0 | ||
|
|
91e0d5164b | ||
|
|
67d8ffcde5 | ||
|
|
44c9aded65 | ||
|
|
f1ccfc0361 | ||
|
|
cdef9c435c | ||
|
|
ed8ac5b84c | ||
|
|
8d55dfb66b | ||
|
|
80c1a6d2af | ||
|
|
895740755d | ||
|
|
90601b7b6b | ||
|
|
cc2ffbb4da | ||
|
|
0c24064939 | ||
|
|
7859084be8 | ||
|
|
101c217ef8 | ||
|
|
7887a5a613 | ||
|
|
800ff09a37 | ||
|
|
20f8fa2ebd | ||
|
|
314159c273 | ||
|
|
bf16885278 | ||
|
|
3ae6f7ab7c | ||
|
|
e829dd8b5c | ||
|
|
62c71eaafa | ||
|
|
24ec25ac62 | ||
|
|
19b5c76154 | ||
|
|
2852b2ea6d | ||
|
|
fc2e9bbfae | ||
|
|
d51dd7ae57 | ||
|
|
9ba70c1808 | ||
|
|
9ec942ea1d | ||
|
|
b9e644269b | ||
|
|
5b56445741 | ||
|
|
1ed5148624 | ||
|
|
fb7c63c507 | ||
|
|
2c5edbc981 | ||
|
|
1addffe622 | ||
|
|
733c14bbc8 | ||
|
|
ca2e6adedb | ||
|
|
5cc20ace1f | ||
|
|
400dcf2899 | ||
|
|
dd3326f038 | ||
|
|
505de39d2a | ||
|
|
24069c8277 | ||
|
|
bb78e9aaeb | ||
|
|
5c39344cff | ||
|
|
7cd24a74af | ||
|
|
a675631965 | ||
|
|
33c4e17c0d | ||
|
|
56ee173d03 | ||
|
|
af4709255a | ||
|
|
527e1aefc9 | ||
|
|
a66dd0dc79 | ||
|
|
7b5b5776f0 | ||
|
|
91ab0b12b0 | ||
|
|
c2ea7045f5 | ||
|
|
af3cae5f3d | ||
|
|
54c3b19052 | ||
|
|
04de0010b5 | ||
|
|
66161b74a6 | ||
|
|
889ef66784 | ||
|
|
fae83f8d22 | ||
|
|
6f4d538832 | ||
|
|
a0c345185b | ||
|
|
c0f771684d | ||
|
|
90ec01bfbf | ||
|
|
7f83bfefd6 | ||
|
|
310aadbdc0 | ||
|
|
6b2de3a24d | ||
|
|
002a87d3ba | ||
|
|
6f63346905 | ||
|
|
735766669e |
57
.github/workflows/build.yml
vendored
Normal file
57
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
pull_request:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# java="25" is the LTS Java version used in reproducible builds script (default in Containerfile).
|
||||||
|
# More Java versions can be added to test compatibility, eg. "26".
|
||||||
|
java: ["25", "26"]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.java }}" != "25" ]; then
|
||||||
|
export OVERRIDE_JAVA_VERSION="${{ matrix.java }}"
|
||||||
|
fi
|
||||||
|
./reproducible-builds/build.sh
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: signal-cli-archive-${{ matrix.java }}
|
||||||
|
path: dist/*
|
||||||
|
|
||||||
|
build-client:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu
|
||||||
|
- macos
|
||||||
|
- windows
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./client
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Install rust
|
||||||
|
run: rustup default stable
|
||||||
|
- name: Build client
|
||||||
|
run: cargo build --release --verbose
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: signal-cli-client-${{ matrix.os }}
|
||||||
|
path: |
|
||||||
|
client/target/release/signal-cli-client
|
||||||
|
client/target/release/signal-cli-client.exe
|
||||||
58
.github/workflows/ci.yml
vendored
58
.github/workflows/ci.yml
vendored
@ -1,58 +0,0 @@
|
|||||||
name: signal-cli CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- '**'
|
|
||||||
pull_request:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read # to fetch code (actions/checkout)
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
java: [ '17', '21' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: ${{ matrix.java }}
|
|
||||||
cache: 'gradle'
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: ./gradlew --no-daemon build
|
|
||||||
- name: Compress archive
|
|
||||||
run: gzip -n -9 build/distributions/signal-cli-*.tar
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: signal-cli-archive-${{ matrix.java }}
|
|
||||||
path: build/distributions/signal-cli-*.tar.gz
|
|
||||||
|
|
||||||
build-graalvm:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: graalvm/setup-graalvm@v1
|
|
||||||
with:
|
|
||||||
version: 'latest'
|
|
||||||
java-version: '17'
|
|
||||||
cache: 'gradle'
|
|
||||||
components: 'native-image'
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: ./gradlew --no-daemon nativeCompile
|
|
||||||
- name: Archive production artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: signal-cli-native
|
|
||||||
path: build/native/nativeCompile/signal-cli
|
|
||||||
12
.github/workflows/codeql-analysis.yml
vendored
12
.github/workflows/codeql-analysis.yml
vendored
@ -21,13 +21,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Setup Java JDK
|
- name: Setup Java JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: 17
|
java-version: 25
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v4
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
# with:
|
# with:
|
||||||
# languages: go, javascript, csharp, python, cpp, java
|
# languages: go, javascript, csharp, python, cpp, java
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@ -57,4 +57,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v4
|
||||||
|
|||||||
27
.github/workflows/dependency-graph.yml
vendored
Normal file
27
.github/workflows/dependency-graph.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: dependency-graph
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
submit-dependency-graph:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '25'
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v5
|
||||||
|
with:
|
||||||
|
dependency-graph: generate-and-submit
|
||||||
|
- name: Generate dependency graph
|
||||||
|
run: ./gradlew --no-daemon dependencies
|
||||||
239
.github/workflows/release.yml
vendored
239
.github/workflows/release.yml
vendored
@ -5,185 +5,78 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: read # to fetch code (actions/checkout)
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: signal-cli
|
IMAGE_NAME: signal-cli
|
||||||
IMAGE_REGISTRY: ghcr.io/asamk
|
IMAGE_REGISTRY: ghcr.io/asamk
|
||||||
REGISTRY_USER: ${{ github.actor }}
|
REGISTRY_USER: ${{ github.actor }}
|
||||||
REGISTRY_PASSWORD: ${{ github.token }}
|
REGISTRY_PASSWORD: ${{ github.token }}
|
||||||
|
ARCHIVE_JAVA_VERSION: 25
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: ./.github/workflows/build.yml
|
||||||
|
|
||||||
ci_wf:
|
release:
|
||||||
uses: AsamK/signal-cli/.github/workflows/ci.yml@master
|
needs: build
|
||||||
# ${{ github.repository }} not accepted here
|
|
||||||
|
|
||||||
lib_to_jar:
|
|
||||||
needs: ci_wf
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
signal_cli_version: ${{ steps.cli_ver.outputs.version }}
|
version: ${{ steps.version.outputs.version }}
|
||||||
release_id: ${{ steps.create_release.outputs.id }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
- name: Get signal-cli version
|
- name: Get signal-cli version
|
||||||
id: cli_ver
|
id: version
|
||||||
run: |
|
run: |
|
||||||
ver="${GITHUB_REF_NAME#v}"
|
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
||||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Extract archive
|
- name: Create draft release and upload assets
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
tree .
|
gh release create "v${{ steps.version.outputs.version }}" \
|
||||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
--repo "${{ github.repository }}" \
|
||||||
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
|
--title "v${{ steps.version.outputs.version }}" \
|
||||||
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
|
--draft \
|
||||||
rm -rf signal-cli-archive-*/
|
--verify-tag \
|
||||||
|
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
|
||||||
- name: Get signal-client jar version
|
"signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \
|
||||||
id: lib_ver
|
"signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \
|
||||||
run: |
|
"signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz"
|
||||||
JAR_PREFIX=libsignal-client-
|
|
||||||
jar_file=$(find ./signal-cli-*/lib/ -name "$JAR_PREFIX*.jar")
|
|
||||||
jar_version=$(echo "$jar_file" | xargs basename | sed "s/$JAR_PREFIX//; s/.jar//")
|
|
||||||
echo "$jar_version"
|
|
||||||
echo "signal_client_version=${jar_version}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Download signal-client builds
|
|
||||||
env:
|
|
||||||
RELEASES_URL: https://github.com/signalapp/libsignal/releases/download/
|
|
||||||
FILE_NAMES: signal_jni.dll libsignal_jni.dylib
|
|
||||||
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
|
|
||||||
run: |
|
|
||||||
for file_name in $FILE_NAMES; do
|
|
||||||
curl -sOL "${RELEASES_URL}/v${SIGNAL_CLIENT_VER}/${file_name}" # note: added v
|
|
||||||
done
|
|
||||||
tree .
|
|
||||||
|
|
||||||
- name: Compress native app
|
|
||||||
env:
|
|
||||||
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
|
||||||
run: |
|
|
||||||
chmod +x signal-cli-native/signal-cli
|
|
||||||
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-native.tar.gz -C signal-cli-native signal-cli
|
|
||||||
rm -rf signal-cli-native/
|
|
||||||
|
|
||||||
- name: Replace Windows lib
|
|
||||||
env:
|
|
||||||
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
|
||||||
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
|
|
||||||
run: |
|
|
||||||
mv signal_jni.dll libsignal_jni.so
|
|
||||||
zip -u ./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar ./libsignal_jni.so
|
|
||||||
tar -czf signal-cli-${SIGNAL_CLI_VER}-Windows.tar.gz signal-cli-*/
|
|
||||||
|
|
||||||
- name: Replace macOS lib
|
|
||||||
env:
|
|
||||||
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
|
|
||||||
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
|
|
||||||
run: |
|
|
||||||
jar_file=./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar
|
|
||||||
zip -d $jar_file libsignal_jni.so
|
|
||||||
zip $jar_file libsignal_jni.dylib
|
|
||||||
tar -czf signal-cli-${SIGNAL_CLI_VER}-macOS.tar.gz signal-cli-*/
|
|
||||||
|
|
||||||
- name: Create release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
|
|
||||||
release_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
|
|
||||||
draft: true
|
|
||||||
|
|
||||||
- name: Upload Linux archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload Linux native archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload windows archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload macos archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
build-container:
|
build-container:
|
||||||
needs: ci_wf
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
- name: Get signal-cli version
|
|
||||||
id: cli_ver
|
|
||||||
run: |
|
|
||||||
ver="${GITHUB_REF_NAME#v}"
|
|
||||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Move archive file
|
- name: Move archive file
|
||||||
run: |
|
run: |
|
||||||
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
|
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}.tar.gz
|
||||||
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
|
||||||
rm -r signal-cli-archive-* signal-cli-native
|
|
||||||
mkdir -p build/install/
|
mkdir -p build/install/
|
||||||
mv ./signal-cli-*/ build/install/signal-cli
|
mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v3
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
|
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
||||||
containerfiles:
|
containerfiles: ./Containerfile
|
||||||
./Containerfile
|
|
||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v2
|
uses: redhat-actions/push-to-registry@v3
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
@ -197,41 +90,75 @@ jobs:
|
|||||||
echo "${{ toJSON(steps.push.outputs) }}"
|
echo "${{ toJSON(steps.push.outputs) }}"
|
||||||
|
|
||||||
build-container-native:
|
build-container-native:
|
||||||
needs: ci_wf
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v6
|
||||||
- name: Download signal-cli build from CI workflow
|
- name: Download signal-cli build from CI workflow
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
- name: Get signal-cli version
|
|
||||||
id: cli_ver
|
|
||||||
run: |
|
|
||||||
ver="${GITHUB_REF_NAME#v}"
|
|
||||||
echo "version=${ver}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Move archive file
|
- name: Move archive file
|
||||||
run: |
|
run: |
|
||||||
|
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
|
||||||
mkdir -p build/native/nativeCompile/
|
mkdir -p build/native/nativeCompile/
|
||||||
chmod +x ./signal-cli-native/signal-cli
|
mv signal-cli build/native/nativeCompile/
|
||||||
mv ./signal-cli-native/signal-cli build/native/nativeCompile/
|
chmod +x build/native/nativeCompile/signal-cli
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v3
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
|
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
||||||
containerfiles:
|
containerfiles: ./native.Containerfile
|
||||||
./native.Containerfile
|
|
||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v2
|
uses: redhat-actions/push-to-registry@v3
|
||||||
|
id: push
|
||||||
|
with:
|
||||||
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
|
tags: ${{ steps.build_image.outputs.tags }}
|
||||||
|
registry: ${{ env.IMAGE_REGISTRY }}
|
||||||
|
username: ${{ env.REGISTRY_USER }}
|
||||||
|
password: ${{ env.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Echo outputs
|
||||||
|
run: |
|
||||||
|
echo "${{ toJSON(steps.push.outputs) }}"
|
||||||
|
|
||||||
|
build-container-client:
|
||||||
|
needs: release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Download signal-cli build from CI workflow
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
|
||||||
|
- name: Move archive file
|
||||||
|
run: |
|
||||||
|
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
|
||||||
|
mkdir -p client/target/release/
|
||||||
|
mv signal-cli-client client/target/release/
|
||||||
|
chmod +x client/target/release/signal-cli-client
|
||||||
|
|
||||||
|
- name: Build Image
|
||||||
|
id: build_image
|
||||||
|
uses: redhat-actions/buildah-build@v3
|
||||||
|
with:
|
||||||
|
image: ${{ env.IMAGE_NAME }}
|
||||||
|
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
||||||
|
containerfiles: ./client.Containerfile
|
||||||
|
oci: true
|
||||||
|
|
||||||
|
- name: Push To GHCR
|
||||||
|
uses: redhat-actions/push-to-registry@v3
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
.gradle/
|
.gradle/
|
||||||
|
.kotlin/
|
||||||
.idea/*
|
.idea/*
|
||||||
!.idea/codeStyles/
|
!.idea/codeStyles/
|
||||||
build/
|
build/
|
||||||
@ -13,3 +14,10 @@ out/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/bin/
|
/bin/
|
||||||
/test-config/
|
/test-config/
|
||||||
|
/dist/
|
||||||
|
/github/
|
||||||
|
man/*.1
|
||||||
|
man/*.5
|
||||||
|
man/man1
|
||||||
|
man/man5
|
||||||
|
.superpowers/
|
||||||
|
|||||||
4
.idea/codeStyles/Project.xml
generated
4
.idea/codeStyles/Project.xml
generated
@ -5,8 +5,8 @@
|
|||||||
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
<option name="GENERATE_FINAL_LOCALS" value="true" />
|
||||||
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
|
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
|
||||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
|
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
|
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||||
<option name="IMPORT_LAYOUT_TABLE">
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
<value>
|
<value>
|
||||||
<package name="com" withSubpackages="true" static="false" />
|
<package name="com" withSubpackages="true" static="false" />
|
||||||
|
|||||||
733
CHANGELOG.md
733
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,7 @@ If you have a question you can ask it in the [GitHub discussions page](https://g
|
|||||||
- Be sure to include a **title and clear description**, as much relevant information as possible.
|
- Be sure to include a **title and clear description**, as much relevant information as possible.
|
||||||
- Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using
|
- Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using
|
||||||
- Specify if it's the normal java or the graalvm native version.
|
- Specify if it's the normal java or the graalvm native version.
|
||||||
- Run the failing command with `--verbose` flag to get a more detailed log output and include that in the bug report
|
- Run the failing command with `-vv --scrub-log` flags to get a more detailed log output and include that in the bug report
|
||||||
|
|
||||||
# Pull request
|
# Pull request
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM docker.io/eclipse-temurin:17-jre
|
FROM docker.io/azul/zulu-openjdk:25-jre-headless
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
|
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
|
||||||
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
|
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
|
||||||
|
|||||||
69
README.md
69
README.md
@ -3,7 +3,7 @@
|
|||||||
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
|
||||||
It supports registering, verifying, sending and receiving messages.
|
It supports registering, verifying, sending and receiving messages.
|
||||||
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
|
||||||
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal/service).
|
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/lib/libsignal-service).
|
||||||
For registering you need a phone number where you can receive SMS or incoming calls.
|
For registering you need a phone number where you can receive SMS or incoming calls.
|
||||||
|
|
||||||
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
signal-cli is primarily intended to be used on servers to notify admins of important events.
|
||||||
@ -11,6 +11,10 @@ For this use-case, it has a daemon mode with JSON-RPC interface ([man page](http
|
|||||||
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)).
|
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)).
|
||||||
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
|
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
|
||||||
|
|
||||||
|
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
|
||||||
|
The official Signal clients expire after three months and then the Signal-Server can make incompatible changes.
|
||||||
|
So signal-cli releases older than three months may not work correctly.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
You can [build signal-cli](#building) yourself or use
|
You can [build signal-cli](#building) yourself or use
|
||||||
@ -19,24 +23,33 @@ Windows. There's also a [docker image and some Linux packages](https://github.co
|
|||||||
|
|
||||||
System requirements:
|
System requirements:
|
||||||
|
|
||||||
- at least Java Runtime Environment (JRE) 17
|
- at least Java Runtime Environment (JRE) 25
|
||||||
- native library: libsignal-client
|
- native library: libsignal-client
|
||||||
|
|
||||||
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
|
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
|
||||||
systems/architectures
|
systems/architectures
|
||||||
see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
|
see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
|
||||||
|
|
||||||
### Install system-wide on Linux
|
### Install system-wide on Linux [ JVM build ]
|
||||||
|
|
||||||
See [latest version](https://github.com/AsamK/signal-cli/releases).
|
See [latest version](https://github.com/AsamK/signal-cli/releases).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export VERSION=<latest version, format "x.y.z">
|
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
|
||||||
wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux.tar.gz
|
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz
|
||||||
sudo tar xf signal-cli-"${VERSION}"-Linux.tar.gz -C /opt
|
sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt
|
||||||
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
|
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Install system-wide on Linux [ GraalVM native build ]
|
||||||
|
|
||||||
|
```sh
|
||||||
|
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
|
||||||
|
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux-native.tar.gz
|
||||||
|
sudo tar xf signal-cli-"${VERSION}"-Linux-native.tar.gz -C /opt
|
||||||
|
sudo ln -sf /opt/signal-cli /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
You can find further instructions on the Wiki:
|
You can find further instructions on the Wiki:
|
||||||
|
|
||||||
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
|
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
|
||||||
@ -51,12 +64,31 @@ Important: The ACCOUNT is your phone number in international format and must inc
|
|||||||
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
|
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
|
||||||
of all country codes.)
|
of all country codes.)
|
||||||
|
|
||||||
|
* Link to an existing account
|
||||||
|
|
||||||
|
If you have an existing Signal account associated
|
||||||
|
with a number, you can link signal-cli to it with:
|
||||||
|
|
||||||
|
signal-cli link
|
||||||
|
|
||||||
* Register a number (with SMS verification)
|
* Register a number (with SMS verification)
|
||||||
|
|
||||||
|
Alternatively, if you don't have an existing Signal
|
||||||
|
account, you can register one from signal-cli. Note
|
||||||
|
that this will unregister any existing client
|
||||||
|
associated with the same number.
|
||||||
|
|
||||||
signal-cli -a ACCOUNT register
|
signal-cli -a ACCOUNT register
|
||||||
|
|
||||||
You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly
|
You can register Signal using a landline number. In this case, you need to follow the procedure below:
|
||||||
to the voice call verification by adding the `--voice` switch at the end of above register command.
|
* Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`)
|
||||||
|
* You will get an error `400 (InvalidTransportModeException)`, this is normal
|
||||||
|
* Wait 60 seconds
|
||||||
|
* Attempt a voice call verification by adding the `--voice` switch and wait for the call:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
signal-cli -a ACCOUNT register --voice
|
||||||
|
```
|
||||||
|
|
||||||
Registering may require solving a CAPTCHA
|
Registering may require solving a CAPTCHA
|
||||||
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
|
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
|
||||||
@ -72,6 +104,12 @@ of all country codes.)
|
|||||||
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
|
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* Send a message to a username, usernames need to be prefixed with `u:`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
|
||||||
|
```
|
||||||
|
|
||||||
* Pipe the message content from another process.
|
* Pipe the message content from another process.
|
||||||
|
|
||||||
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
|
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
|
||||||
@ -122,14 +160,23 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
|
|||||||
./gradlew run --args="--help"
|
./gradlew run --args="--help"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### JSON Schemas for the JSON-RPC mode
|
||||||
|
|
||||||
|
1. Generate [JSON Schema](https://json-schema.org/) files for all the JSON-RPC data classes (`src/main/java/org/asamk/signal/json`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew jsonSchemas
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The generated files can be found in the `build/generated/META-INF/schemas` folder.
|
||||||
|
|
||||||
### Building a native binary with GraalVM (EXPERIMENTAL)
|
### Building a native binary with GraalVM (EXPERIMENTAL)
|
||||||
|
|
||||||
It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not
|
It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not
|
||||||
work in all situations.
|
work in all situations.
|
||||||
|
|
||||||
1. [Install GraalVM and setup the enviroment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
|
1. [Install GraalVM and setup the environment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
|
||||||
2. [Install prerequisites](https://www.graalvm.org/reference-manual/native-image/#prerequisites)
|
2. Execute Gradle:
|
||||||
3. Execute Gradle:
|
|
||||||
|
|
||||||
./gradlew nativeCompile
|
./gradlew nativeCompile
|
||||||
|
|
||||||
|
|||||||
134
build.gradle.kts
134
build.gradle.kts
@ -1,31 +1,46 @@
|
|||||||
|
import groovy.json.JsonOutput
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
application
|
application
|
||||||
eclipse
|
eclipse
|
||||||
`check-lib-versions`
|
`check-lib-versions`
|
||||||
id("org.graalvm.buildtools.native") version "0.9.27"
|
id("org.graalvm.buildtools.native") version "1.1.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
version = "0.12.2"
|
allprojects {
|
||||||
|
group = "org.asamk"
|
||||||
|
version = "0.14.8"
|
||||||
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_25
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_25
|
||||||
|
|
||||||
|
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass.set("org.asamk.signal.Main")
|
mainClass.set("org.asamk.signal.Main")
|
||||||
|
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
|
|
||||||
graalvmNative {
|
graalvmNative {
|
||||||
binaries {
|
binaries {
|
||||||
this["main"].run {
|
this["main"].run {
|
||||||
|
buildArgs.add("-Dfile.encoding=UTF-8")
|
||||||
|
buildArgs.add("-J-Dfile.encoding=UTF-8")
|
||||||
|
buildArgs.add("-march=compatibility")
|
||||||
|
buildArgs.add("--enable-native-access=ALL-UNNAMED")
|
||||||
resources.autodetect()
|
resources.autodetect()
|
||||||
configurationFileDirectories.from(file("graalvm-config-dir"))
|
|
||||||
if (System.getenv("GRAALVM_HOME") == null) {
|
if (System.getenv("GRAALVM_HOME") == null) {
|
||||||
toolchainDetection.set(true)
|
toolchainDetection.set(true)
|
||||||
javaLauncher.set(javaToolchains.launcherFor {
|
javaLauncher.set(javaToolchains.launcherFor {
|
||||||
languageVersion.set(JavaLanguageVersion.of(17))
|
languageVersion.set(JavaLanguageVersion.of(25))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
toolchainDetection.set(false)
|
toolchainDetection.set(false)
|
||||||
@ -34,7 +49,48 @@ graalvmNative {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val artifactType = Attribute.of("artifactType", String::class.java)
|
||||||
|
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
|
||||||
dependencies {
|
dependencies {
|
||||||
|
attributesSchema {
|
||||||
|
attribute(minified)
|
||||||
|
}
|
||||||
|
artifactTypes.getByName("jar") {
|
||||||
|
attributes.attribute(minified, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations.runtimeClasspath.configure {
|
||||||
|
attributes {
|
||||||
|
attribute(minified, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val excludePatterns = mapOf(
|
||||||
|
"libsignal-client" to setOf(
|
||||||
|
"libsignal_jni_testing_amd64.so",
|
||||||
|
"signal_jni_testing_amd64.dll",
|
||||||
|
"libsignal_jni_testing_amd64.dylib",
|
||||||
|
"libsignal_jni_testing_aarch64.dylib",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val schemaAnnotationProcessor = configurations.create("schemaAnnotationProcessor") {
|
||||||
|
isCanBeConsumed = false
|
||||||
|
isCanBeResolved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
registerTransform(JarFileExcluder::class) {
|
||||||
|
from.attribute(minified, false).attribute(artifactType, "jar")
|
||||||
|
to.attribute(minified, true).attribute(artifactType, "jar")
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
excludeFilesByArtifact = excludePatterns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaAnnotationProcessor(libs.micronaut.json.schema.processor)
|
||||||
|
schemaAnnotationProcessor(libs.micronaut.inject.java)
|
||||||
implementation(libs.bouncycastle)
|
implementation(libs.bouncycastle)
|
||||||
implementation(libs.jackson.databind)
|
implementation(libs.jackson.databind)
|
||||||
implementation(libs.argparse4j)
|
implementation(libs.argparse4j)
|
||||||
@ -42,7 +98,20 @@ dependencies {
|
|||||||
implementation(libs.slf4j.api)
|
implementation(libs.slf4j.api)
|
||||||
implementation(libs.slf4j.jul)
|
implementation(libs.slf4j.jul)
|
||||||
implementation(libs.logback)
|
implementation(libs.logback)
|
||||||
implementation(project(":lib"))
|
implementation(libs.zxing)
|
||||||
|
implementation(libs.micronaut.json.schema.annotations)
|
||||||
|
if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) {
|
||||||
|
implementation(libs.micronaut.json.schema.generator)
|
||||||
|
}
|
||||||
|
implementation(project(":libsignal-cli"))
|
||||||
|
|
||||||
|
testImplementation(libs.junit.jupiter)
|
||||||
|
testImplementation(platform(libs.junit.jupiter.bom))
|
||||||
|
testRuntimeOnly(libs.junit.launcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<Test>("test") {
|
||||||
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
@ -66,23 +135,68 @@ tasks.withType<Jar> {
|
|||||||
attributes(
|
attributes(
|
||||||
"Implementation-Title" to project.name,
|
"Implementation-Title" to project.name,
|
||||||
"Implementation-Version" to project.version,
|
"Implementation-Version" to project.version,
|
||||||
"Main-Class" to application.mainClass.get()
|
"Main-Class" to application.mainClass.get(),
|
||||||
|
"Enable-Native-Access" to "ALL-UNNAMED",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task("fatJar", type = Jar::class) {
|
tasks.register("fatJar", type = Jar::class) {
|
||||||
archiveBaseName.set("${project.name}-fat")
|
archiveBaseName.set("${project.name}-fat")
|
||||||
exclude(
|
exclude(
|
||||||
"META-INF/*.SF",
|
"META-INF/*.SF",
|
||||||
|
"META-INF/**/*.MF",
|
||||||
"META-INF/*.DSA",
|
"META-INF/*.DSA",
|
||||||
"META-INF/*.RSA",
|
"META-INF/*.RSA",
|
||||||
"META-INF/NOTICE*",
|
"META-INF/NOTICE*",
|
||||||
"META-INF/LICENSE*",
|
"META-INF/LICENSE*",
|
||||||
"META-INF/INDEX.LIST",
|
"META-INF/INDEX.LIST",
|
||||||
"**/module-info.class"
|
"**/module-info.class",
|
||||||
)
|
)
|
||||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
duplicatesStrategy = DuplicatesStrategy.WARN
|
||||||
|
doFirst {
|
||||||
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
|
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
|
||||||
|
}
|
||||||
with(tasks.jar.get())
|
with(tasks.jar.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register("writeLibsignalVersion") {
|
||||||
|
doLast {
|
||||||
|
val resolutionResult = configurations.runtimeClasspath.get().incoming.resolutionResult
|
||||||
|
val libsignalDep =
|
||||||
|
resolutionResult.allDependencies.find { dep -> dep.requested is ModuleComponentSelector && (dep.requested as ModuleComponentSelector).group == "org.signal" && (dep.requested as ModuleComponentSelector).moduleIdentifier.name == "libsignal-client" }
|
||||||
|
if (libsignalDep != null) {
|
||||||
|
val version = (libsignalDep.requested as ModuleComponentSelector).version
|
||||||
|
file("libsignal-version").writeText(version + "\n")
|
||||||
|
} else {
|
||||||
|
throw GradleException("Could not find libsignal-client dependency")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<JavaCompile>("jsonSchemas") {
|
||||||
|
dependsOn(tasks.compileJava)
|
||||||
|
val schemaBaseUri = "http://localhost:8080/schemas/"
|
||||||
|
source = sourceSets.main.get().java
|
||||||
|
include("org/asamk/signal/json/**/*.java")
|
||||||
|
classpath = sourceSets.main.get().compileClasspath + files(sourceSets.main.get().java.destinationDirectory)
|
||||||
|
destinationDirectory.set(layout.buildDirectory.dir("generated"))
|
||||||
|
options.annotationProcessorPath = schemaAnnotationProcessor
|
||||||
|
options.compilerArgs.addAll(
|
||||||
|
listOf(
|
||||||
|
"-Amicronaut.processing.group=org.asamk",
|
||||||
|
"-Amicronaut.processing.module=signal-cli",
|
||||||
|
"-Amicronaut.processing.annotations=org.asamk.signal.json.*",
|
||||||
|
"-Amicronaut.jsonschema.baseUri=$schemaBaseUri",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
doLast {
|
||||||
|
fileTree(destinationDirectory.get().dir("META-INF/schemas").asFile) {
|
||||||
|
include("*.schema.json")
|
||||||
|
}.forEach { schemaFile ->
|
||||||
|
val normalized = schemaFile.readText().replace("\"$schemaBaseUri/", "\"")
|
||||||
|
val prettyJson = JsonOutput.prettyPrint(normalized)
|
||||||
|
schemaFile.writeText("$prettyJson\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,11 +7,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
|
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
|
||||||
compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
|
compilerOptions.jvmTarget.set(JvmTarget.JVM_25)
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_25
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
import groovy.util.XmlSlurper
|
|
||||||
import groovy.util.slurpersupport.GPathResult
|
|
||||||
import org.codehaus.groovy.runtime.ResourceGroovyMethods
|
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.api.Task
|
import org.gradle.api.Task
|
||||||
import org.gradle.api.artifacts.Dependency
|
import org.gradle.api.artifacts.Dependency
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
class CheckLibVersionsPlugin : Plugin<Project> {
|
class CheckLibVersionsPlugin : Plugin<Project> {
|
||||||
override fun apply(project: Project) {
|
override fun apply(project: Project) {
|
||||||
project.task("checkLibVersions") {
|
project.tasks.register("checkLibVersions") {
|
||||||
description =
|
description =
|
||||||
"Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded."
|
"Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded."
|
||||||
doLast {
|
doLast {
|
||||||
@ -28,15 +24,15 @@ class CheckLibVersionsPlugin : Plugin<Project> {
|
|||||||
val name = dependency.name
|
val name = dependency.name
|
||||||
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
|
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
|
||||||
try {
|
try {
|
||||||
val url = ResourceGroovyMethods.toURL(metaDataUrl)
|
val dbf = DocumentBuilderFactory.newInstance()
|
||||||
val metaDataText = ResourceGroovyMethods.getText(url)
|
val db = dbf.newDocumentBuilder()
|
||||||
val metadata = XmlSlurper().parseText(metaDataText)
|
val doc = db.parse(metaDataUrl)
|
||||||
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest")
|
val newest = doc.getElementsByTagName("latest").item(0).textContent
|
||||||
if (version != newest.toString()) {
|
if (version != newest.toString()) {
|
||||||
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logger.debug("Unable to download or parse $metaDataUrl: $e.message")
|
logger.debug("Unable to download or parse {}: {}", metaDataUrl, e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
buildSrc/src/main/kotlin/ExcludeFileFromJar.kt
Normal file
53
buildSrc/src/main/kotlin/ExcludeFileFromJar.kt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import org.gradle.api.artifacts.transform.*
|
||||||
|
import org.gradle.api.file.FileSystemLocation
|
||||||
|
import org.gradle.api.provider.Provider
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.PathSensitive
|
||||||
|
import org.gradle.api.tasks.PathSensitivity
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
@CacheableTransform
|
||||||
|
abstract class JarFileExcluder : TransformAction<JarFileExcluder.Parameters> {
|
||||||
|
interface Parameters : TransformParameters {
|
||||||
|
@get:Input
|
||||||
|
var excludeFilesByArtifact: Map<String, Set<String>>
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:PathSensitive(PathSensitivity.NAME_ONLY)
|
||||||
|
@get:InputArtifact
|
||||||
|
abstract val inputArtifact: Provider<FileSystemLocation>
|
||||||
|
|
||||||
|
override
|
||||||
|
fun transform(outputs: TransformOutputs) {
|
||||||
|
val fileName = inputArtifact.get().asFile.name
|
||||||
|
for (entry in parameters.excludeFilesByArtifact) {
|
||||||
|
if (fileName.startsWith(entry.key)) {
|
||||||
|
val nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf("."))
|
||||||
|
excludeFiles(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}.jar"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputs.file(inputArtifact)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun excludeFiles(artifact: File, excludeFiles: Set<String>, jarFile: File) {
|
||||||
|
ZipInputStream(FileInputStream(artifact)).use { input ->
|
||||||
|
ZipOutputStream(FileOutputStream(jarFile)).use { output ->
|
||||||
|
var entry = input.nextEntry
|
||||||
|
while (entry != null) {
|
||||||
|
if (!excludeFiles.contains(entry.name)) {
|
||||||
|
output.putNextEntry(entry)
|
||||||
|
input.copyTo(output)
|
||||||
|
output.closeEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = input.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
client.Containerfile
Normal file
11
client.Containerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM docker.io/debian:testing-slim
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
|
||||||
|
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
|
||||||
|
LABEL org.opencontainers.image.licenses=GPL-3.0-only
|
||||||
|
|
||||||
|
RUN useradd signal-cli --system
|
||||||
|
ADD client/target/release/signal-cli-client /usr/bin/signal-cli-client
|
||||||
|
|
||||||
|
USER signal-cli
|
||||||
|
ENTRYPOINT ["/usr/bin/signal-cli-client"]
|
||||||
1424
client/Cargo.lock
generated
1424
client/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "signal-cli-client"
|
name = "signal-cli-client"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
|
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
|
||||||
log = "0.4"
|
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
|
||||||
jsonrpsee = { version = "0.20.0", features = [
|
jsonrpsee = { version = "0.26", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"async-client",
|
"async-client",
|
||||||
"http-client",
|
"http-client",
|
||||||
@ -20,4 +19,4 @@ jsonrpsee = { version = "0.20.0", features = [
|
|||||||
bytes = "1"
|
bytes = "1"
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use clap::{crate_version, Parser, Subcommand, ValueEnum};
|
|
||||||
use std::{ffi::OsString, net::SocketAddr};
|
use std::{ffi::OsString, net::SocketAddr};
|
||||||
|
|
||||||
|
use clap::{crate_version, Parser, Subcommand, ValueEnum};
|
||||||
|
|
||||||
/// JSON-RPC client for signal-cli
|
/// JSON-RPC client for signal-cli
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(rename_all = "kebab-case", version = crate_version!())]
|
#[command(rename_all = "kebab-case", version = crate_version!())]
|
||||||
@ -9,11 +10,15 @@ pub struct Cli {
|
|||||||
#[arg(short = 'a', long)]
|
#[arg(short = 'a', long)]
|
||||||
pub account: Option<String>,
|
pub account: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub output: Option<String>,
|
||||||
|
|
||||||
/// TCP host and port of signal-cli daemon
|
/// TCP host and port of signal-cli daemon
|
||||||
#[arg(long, conflicts_with = "json_rpc_http")]
|
#[arg(long, conflicts_with = "json_rpc_http")]
|
||||||
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
||||||
|
|
||||||
/// UNIX socket address and port of signal-cli daemon
|
/// UNIX socket address and port of signal-cli daemon
|
||||||
|
#[cfg(unix)]
|
||||||
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
||||||
pub json_rpc_socket: Option<Option<OsString>>,
|
pub json_rpc_socket: Option<Option<OsString>>,
|
||||||
|
|
||||||
@ -21,9 +26,6 @@ pub struct Cli {
|
|||||||
#[arg(long, conflicts_with = "json_rpc_socket")]
|
#[arg(long, conflicts_with = "json_rpc_socket")]
|
||||||
pub json_rpc_http: Option<Option<String>>,
|
pub json_rpc_http: Option<Option<String>>,
|
||||||
|
|
||||||
#[arg(value_enum, long, default_value_t = OutputTypes::Json)]
|
|
||||||
pub output: OutputTypes,
|
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
|
||||||
@ -31,13 +33,6 @@ pub struct Cli {
|
|||||||
pub command: CliCommands,
|
pub command: CliCommands,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ValueEnum, Clone, Debug)]
|
|
||||||
#[value(rename_all = "kebab-case")]
|
|
||||||
pub enum OutputTypes {
|
|
||||||
PlainText,
|
|
||||||
Json,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
#[command(rename_all = "camelCase", version = crate_version!())]
|
#[command(rename_all = "camelCase", version = crate_version!())]
|
||||||
@ -46,6 +41,10 @@ pub enum CliCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
uri: String,
|
uri: String,
|
||||||
},
|
},
|
||||||
|
AddStickerPack {
|
||||||
|
#[arg(long)]
|
||||||
|
uri: String,
|
||||||
|
},
|
||||||
#[command(rename_all = "kebab-case")]
|
#[command(rename_all = "kebab-case")]
|
||||||
Block {
|
Block {
|
||||||
recipient: Vec<String>,
|
recipient: Vec<String>,
|
||||||
@ -57,8 +56,40 @@ pub enum CliCommands {
|
|||||||
#[arg(long = "ignore-registered")]
|
#[arg(long = "ignore-registered")]
|
||||||
ignore_registered: Option<bool>,
|
ignore_registered: Option<bool>,
|
||||||
},
|
},
|
||||||
|
FinishChangeNumber {
|
||||||
|
number: String,
|
||||||
|
#[arg(short = 'v', long = "verification-code")]
|
||||||
|
verification_code: String,
|
||||||
|
|
||||||
|
#[arg(short = 'p', long)]
|
||||||
|
pin: Option<String>,
|
||||||
|
},
|
||||||
|
GetAttachment {
|
||||||
|
#[arg(long)]
|
||||||
|
id: String,
|
||||||
|
#[arg(long)]
|
||||||
|
recipient: Option<String>,
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Option<String>,
|
||||||
|
},
|
||||||
|
GetAvatar {
|
||||||
|
#[arg(long)]
|
||||||
|
contact: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
profile: Option<String>,
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Option<String>,
|
||||||
|
},
|
||||||
|
GetSticker {
|
||||||
|
#[arg(long = "pack-id")]
|
||||||
|
pack_id: String,
|
||||||
|
#[arg(long = "sticker-id")]
|
||||||
|
sticker_id: u32,
|
||||||
|
},
|
||||||
GetUserStatus {
|
GetUserStatus {
|
||||||
recipient: Vec<String>,
|
recipient: Vec<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
username: Vec<String>,
|
||||||
},
|
},
|
||||||
JoinGroup {
|
JoinGroup {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@ -66,7 +97,7 @@ pub enum CliCommands {
|
|||||||
},
|
},
|
||||||
Link {
|
Link {
|
||||||
#[arg(short = 'n', long)]
|
#[arg(short = 'n', long)]
|
||||||
name: String,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
ListAccounts,
|
ListAccounts,
|
||||||
ListContacts {
|
ListContacts {
|
||||||
@ -77,6 +108,10 @@ pub enum CliCommands {
|
|||||||
blocked: Option<bool>,
|
blocked: Option<bool>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
detailed: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
internal: bool,
|
||||||
},
|
},
|
||||||
ListDevices,
|
ListDevices,
|
||||||
ListGroups {
|
ListGroups {
|
||||||
@ -107,11 +142,15 @@ pub enum CliCommands {
|
|||||||
voice: bool,
|
voice: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
captcha: Option<String>,
|
captcha: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
reregister: bool,
|
||||||
},
|
},
|
||||||
RemoveContact {
|
RemoveContact {
|
||||||
recipient: String,
|
recipient: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
forget: bool,
|
forget: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
hide: bool,
|
||||||
},
|
},
|
||||||
RemoveDevice {
|
RemoveDevice {
|
||||||
#[arg(short = 'd', long = "device-id")]
|
#[arg(short = 'd', long = "device-id")]
|
||||||
@ -137,21 +176,36 @@ pub enum CliCommands {
|
|||||||
#[arg(short = 'g', long)]
|
#[arg(short = 'g', long)]
|
||||||
group_id: Vec<String>,
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
note_to_self: bool,
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
|
||||||
#[arg(short = 'e', long)]
|
#[arg(short = 'e', long)]
|
||||||
end_session: bool,
|
end_session: bool,
|
||||||
|
|
||||||
#[arg(short = 'm', long)]
|
#[arg(short = 'm', long)]
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
message_from_stdin: bool,
|
||||||
|
|
||||||
#[arg(short = 'a', long)]
|
#[arg(short = 'a', long)]
|
||||||
attachment: Vec<String>,
|
attachment: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
view_once: bool,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
mention: Vec<String>,
|
mention: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
text_style: Vec<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
quote_timestamp: Option<u64>,
|
quote_timestamp: Option<u64>,
|
||||||
|
|
||||||
@ -164,6 +218,24 @@ pub enum CliCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
quote_mention: Vec<String>,
|
quote_mention: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
quote_text_style: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
quote_attachment: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
preview_url: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
preview_title: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
preview_description: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
preview_image: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
sticker: Option<String>,
|
sticker: Option<String>,
|
||||||
|
|
||||||
@ -172,6 +244,28 @@ pub enum CliCommands {
|
|||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
story_author: Option<String>,
|
story_author: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
edit_timestamp: Option<u64>,
|
||||||
|
|
||||||
|
#[arg(long = "no-urgent")]
|
||||||
|
no_urgent: bool,
|
||||||
|
},
|
||||||
|
SendAdminDelete {
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'a', long = "target-author")]
|
||||||
|
target_author: String,
|
||||||
|
|
||||||
|
#[arg(short = 't', long = "target-timestamp")]
|
||||||
|
target_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
story: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
},
|
},
|
||||||
SendContacts,
|
SendContacts,
|
||||||
SendPaymentNotification {
|
SendPaymentNotification {
|
||||||
@ -183,15 +277,117 @@ pub enum CliCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
note: String,
|
note: String,
|
||||||
},
|
},
|
||||||
|
SendPinMessage {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'a', long = "target-author")]
|
||||||
|
target_author: String,
|
||||||
|
|
||||||
|
#[arg(short = 't', long = "target-timestamp")]
|
||||||
|
target_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(short = 'd', long = "pin-duration")]
|
||||||
|
pin_duration: Option<i32>,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
story: bool,
|
||||||
|
},
|
||||||
|
SendPollCreate {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'q', long = "question")]
|
||||||
|
question: String,
|
||||||
|
|
||||||
|
#[arg(short = 'o', long = "option")]
|
||||||
|
option: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long = "no-multi")]
|
||||||
|
no_multi: bool,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
},
|
||||||
|
SendPollTerminate {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long = "poll-timestamp")]
|
||||||
|
poll_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
},
|
||||||
|
SendPollVote {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long = "poll-author")]
|
||||||
|
poll_author: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "poll-timestamp")]
|
||||||
|
poll_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(short = 'o', long = "option")]
|
||||||
|
option: Vec<i32>,
|
||||||
|
|
||||||
|
#[arg(long = "vote-count")]
|
||||||
|
vote_count: i32,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
},
|
||||||
SendReaction {
|
SendReaction {
|
||||||
recipient: Vec<String>,
|
recipient: Vec<String>,
|
||||||
|
|
||||||
#[arg(short = 'g', long = "group-id")]
|
#[arg(short = 'g', long = "group-id")]
|
||||||
group_id: Vec<String>,
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
#[arg(long = "note-to-self")]
|
#[arg(long = "note-to-self")]
|
||||||
note_to_self: bool,
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
|
||||||
#[arg(short = 'e', long)]
|
#[arg(short = 'e', long)]
|
||||||
emoji: String,
|
emoji: String,
|
||||||
|
|
||||||
@ -210,6 +406,9 @@ pub enum CliCommands {
|
|||||||
SendReceipt {
|
SendReceipt {
|
||||||
recipient: String,
|
recipient: String,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
#[arg(short = 't', long = "target-timestamp")]
|
#[arg(short = 't', long = "target-timestamp")]
|
||||||
target_timestamp: Vec<u64>,
|
target_timestamp: Vec<u64>,
|
||||||
|
|
||||||
@ -226,9 +425,49 @@ pub enum CliCommands {
|
|||||||
#[arg(short = 's', long)]
|
#[arg(short = 's', long)]
|
||||||
stop: bool,
|
stop: bool,
|
||||||
},
|
},
|
||||||
|
SendUnpinMessage {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'a', long = "target-author")]
|
||||||
|
target_author: String,
|
||||||
|
|
||||||
|
#[arg(short = 't', long = "target-timestamp")]
|
||||||
|
target_timestamp: u64,
|
||||||
|
|
||||||
|
#[arg(long = "note-to-self")]
|
||||||
|
note_to_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
notify_self: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
story: bool,
|
||||||
|
},
|
||||||
|
SendMessageRequestResponse {
|
||||||
|
recipient: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(short = 'g', long = "group-id")]
|
||||||
|
group_id: Vec<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
r#type: MessageRequestResponseType,
|
||||||
|
},
|
||||||
SetPin {
|
SetPin {
|
||||||
pin: String,
|
pin: String,
|
||||||
},
|
},
|
||||||
|
StartChangeNumber {
|
||||||
|
number: String,
|
||||||
|
#[arg(short = 'v', long)]
|
||||||
|
voice: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
captcha: Option<String>,
|
||||||
|
},
|
||||||
SubmitRateLimitChallenge {
|
SubmitRateLimitChallenge {
|
||||||
challenge: String,
|
challenge: String,
|
||||||
captcha: String,
|
captcha: String,
|
||||||
@ -256,6 +495,16 @@ pub enum CliCommands {
|
|||||||
UpdateAccount {
|
UpdateAccount {
|
||||||
#[arg(short = 'n', long = "device-name")]
|
#[arg(short = 'n', long = "device-name")]
|
||||||
device_name: Option<String>,
|
device_name: Option<String>,
|
||||||
|
#[arg(long = "unrestricted-unidentified-sender")]
|
||||||
|
unrestricted_unidentified_sender: Option<bool>,
|
||||||
|
#[arg(long = "discoverable-by-number")]
|
||||||
|
discoverable_by_number: Option<bool>,
|
||||||
|
#[arg(long = "number-sharing")]
|
||||||
|
number_sharing: Option<bool>,
|
||||||
|
#[arg(short = 'u', long = "username")]
|
||||||
|
username: Option<String>,
|
||||||
|
#[arg(long = "delete-username")]
|
||||||
|
delete_username: bool,
|
||||||
},
|
},
|
||||||
UpdateConfiguration {
|
UpdateConfiguration {
|
||||||
#[arg(long = "read-receipts")]
|
#[arg(long = "read-receipts")]
|
||||||
@ -278,6 +527,28 @@ pub enum CliCommands {
|
|||||||
|
|
||||||
#[arg(short = 'n', long)]
|
#[arg(short = 'n', long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "given-name")]
|
||||||
|
given_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "family-name")]
|
||||||
|
family_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "nick-given-name")]
|
||||||
|
nick_given_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "nick-family-name")]
|
||||||
|
nick_family_name: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
note: Option<String>,
|
||||||
|
},
|
||||||
|
UpdateDevice {
|
||||||
|
#[arg(short = 'd', long = "device-id")]
|
||||||
|
device_id: u32,
|
||||||
|
|
||||||
|
#[arg(short = 'n', long = "device-name")]
|
||||||
|
device_name: String,
|
||||||
},
|
},
|
||||||
UpdateGroup {
|
UpdateGroup {
|
||||||
#[arg(short = 'g', long = "group-id")]
|
#[arg(short = 'g', long = "group-id")]
|
||||||
@ -327,6 +598,12 @@ pub enum CliCommands {
|
|||||||
|
|
||||||
#[arg(short = 'e', long)]
|
#[arg(short = 'e', long)]
|
||||||
expiration: Option<u32>,
|
expiration: Option<u32>,
|
||||||
|
|
||||||
|
#[arg(long = "member-label-emoji")]
|
||||||
|
member_label_emoji: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "member-label")]
|
||||||
|
member_label: Option<String>,
|
||||||
},
|
},
|
||||||
UpdateProfile {
|
UpdateProfile {
|
||||||
#[arg(long = "given-name")]
|
#[arg(long = "given-name")]
|
||||||
@ -341,7 +618,7 @@ pub enum CliCommands {
|
|||||||
#[arg(long = "about-emoji")]
|
#[arg(long = "about-emoji")]
|
||||||
about_emoji: Option<String>,
|
about_emoji: Option<String>,
|
||||||
|
|
||||||
#[arg(long = "mobile-coin-address")]
|
#[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")]
|
||||||
mobile_coin_address: Option<String>,
|
mobile_coin_address: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@ -383,3 +660,10 @@ pub enum GroupPermission {
|
|||||||
EveryMember,
|
EveryMember,
|
||||||
OnlyAdmins,
|
OnlyAdmins,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(ValueEnum, Clone, Debug)]
|
||||||
|
#[value(rename_all = "kebab-case")]
|
||||||
|
pub enum MessageRequestResponseType {
|
||||||
|
Accept,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use jsonrpsee::async_client::ClientBuilder;
|
use jsonrpsee::async_client::ClientBuilder;
|
||||||
use jsonrpsee::core::client::SubscriptionClientT;
|
use jsonrpsee::core::client::{Error, SubscriptionClientT};
|
||||||
use jsonrpsee::core::Error;
|
|
||||||
use jsonrpsee::http_client::HttpClientBuilder;
|
use jsonrpsee::http_client::HttpClientBuilder;
|
||||||
use jsonrpsee::proc_macros::rpc;
|
use jsonrpsee::proc_macros::rpc;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -18,6 +17,13 @@ pub trait Rpc {
|
|||||||
uri: String,
|
uri: String,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "addStickerPack", param_kind = map)]
|
||||||
|
async fn add_sticker_pack(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
uri: String,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "block", param_kind = map)]
|
#[method(name = "block", param_kind = map)]
|
||||||
fn block(
|
fn block(
|
||||||
&self,
|
&self,
|
||||||
@ -33,21 +39,58 @@ pub trait Rpc {
|
|||||||
#[allow(non_snake_case)] ignoreRegistered: Option<bool>,
|
#[allow(non_snake_case)] ignoreRegistered: Option<bool>,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "getAttachment", param_kind = map)]
|
||||||
|
fn get_attachment(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
id: String,
|
||||||
|
recipient: Option<String>,
|
||||||
|
#[allow(non_snake_case)] groupId: Option<String>,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "getAvatar", param_kind = map)]
|
||||||
|
fn get_avatar(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
contact: Option<String>,
|
||||||
|
profile: Option<String>,
|
||||||
|
#[allow(non_snake_case)] groupId: Option<String>,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "getSticker", param_kind = map)]
|
||||||
|
fn get_sticker(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
#[allow(non_snake_case)] packId: String,
|
||||||
|
#[allow(non_snake_case)] stickerId: u32,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "getUserStatus", param_kind = map)]
|
#[method(name = "getUserStatus", param_kind = map)]
|
||||||
fn get_user_status(
|
fn get_user_status(
|
||||||
&self,
|
&self,
|
||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "joinGroup", param_kind = map)]
|
#[method(name = "joinGroup", param_kind = map)]
|
||||||
fn join_group(&self, account: Option<String>, uri: String) -> Result<Value, ErrorObjectOwned>;
|
fn join_group(&self, account: Option<String>, uri: String) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[method(name = "finishChangeNumber", param_kind = map)]
|
||||||
|
fn finish_change_number(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
number: String,
|
||||||
|
verificationCode: String,
|
||||||
|
pin: Option<String>,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "finishLink", param_kind = map)]
|
#[method(name = "finishLink", param_kind = map)]
|
||||||
fn finish_link(
|
fn finish_link(
|
||||||
&self,
|
&self,
|
||||||
#[allow(non_snake_case)] deviceLinkUri: String,
|
#[allow(non_snake_case)] deviceLinkUri: String,
|
||||||
#[allow(non_snake_case)] deviceName: String,
|
#[allow(non_snake_case)] deviceName: Option<String>,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "listAccounts", param_kind = map)]
|
#[method(name = "listAccounts", param_kind = map)]
|
||||||
@ -61,6 +104,8 @@ pub trait Rpc {
|
|||||||
#[allow(non_snake_case)] allRecipients: bool,
|
#[allow(non_snake_case)] allRecipients: bool,
|
||||||
blocked: Option<bool>,
|
blocked: Option<bool>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
detailed: bool,
|
||||||
|
internal: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "listDevices", param_kind = map)]
|
#[method(name = "listDevices", param_kind = map)]
|
||||||
@ -98,6 +143,7 @@ pub trait Rpc {
|
|||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
voice: bool,
|
voice: bool,
|
||||||
captcha: Option<String>,
|
captcha: Option<String>,
|
||||||
|
reregister: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "removeContact", param_kind = map)]
|
#[method(name = "removeContact", param_kind = map)]
|
||||||
@ -106,6 +152,7 @@ pub trait Rpc {
|
|||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipient: String,
|
recipient: String,
|
||||||
forget: bool,
|
forget: bool,
|
||||||
|
hide: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "removeDevice", param_kind = map)]
|
#[method(name = "removeDevice", param_kind = map)]
|
||||||
@ -128,29 +175,123 @@ pub trait Rpc {
|
|||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
#[method(name = "send", param_kind = map)]
|
#[method(name = "send", param_kind = map)]
|
||||||
fn send(
|
fn send(
|
||||||
&self,
|
&self,
|
||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
#[allow(non_snake_case)] endSession: bool,
|
#[allow(non_snake_case)] endSession: bool,
|
||||||
message: String,
|
message: String,
|
||||||
attachments: Vec<String>,
|
attachments: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] viewOnce: bool,
|
||||||
mentions: Vec<String>,
|
mentions: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] textStyle: Vec<String>,
|
||||||
#[allow(non_snake_case)] quoteTimestamp: Option<u64>,
|
#[allow(non_snake_case)] quoteTimestamp: Option<u64>,
|
||||||
#[allow(non_snake_case)] quoteAuthor: Option<String>,
|
#[allow(non_snake_case)] quoteAuthor: Option<String>,
|
||||||
#[allow(non_snake_case)] quoteMessage: Option<String>,
|
#[allow(non_snake_case)] quoteMessage: Option<String>,
|
||||||
#[allow(non_snake_case)] quoteMention: Vec<String>,
|
#[allow(non_snake_case)] quoteMention: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] quoteTextStyle: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] quoteAttachment: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] previewUrl: Option<String>,
|
||||||
|
#[allow(non_snake_case)] previewTitle: Option<String>,
|
||||||
|
#[allow(non_snake_case)] previewDescription: Option<String>,
|
||||||
|
#[allow(non_snake_case)] previewImage: Option<String>,
|
||||||
sticker: Option<String>,
|
sticker: Option<String>,
|
||||||
#[allow(non_snake_case)] storyTimestamp: Option<u64>,
|
#[allow(non_snake_case)] storyTimestamp: Option<u64>,
|
||||||
#[allow(non_snake_case)] storyAuthor: Option<String>,
|
#[allow(non_snake_case)] storyAuthor: Option<String>,
|
||||||
|
#[allow(non_snake_case)] editTimestamp: Option<u64>,
|
||||||
|
#[allow(non_snake_case)] noUrgent: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "sendContacts", param_kind = map)]
|
#[method(name = "sendContacts", param_kind = map)]
|
||||||
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendAdminDelete", param_kind = map)]
|
||||||
|
fn send_admin_delete(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] targetAuthor: String,
|
||||||
|
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||||
|
story: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendPinMessage", param_kind = map)]
|
||||||
|
fn send_pin_message(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] targetAuthor: String,
|
||||||
|
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||||
|
#[allow(non_snake_case)] pinDuration: Option<i32>,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
story: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendPollCreate", param_kind = map)]
|
||||||
|
fn send_poll_create(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
question: String,
|
||||||
|
option: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] noMulti: bool,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendPollVote", param_kind = map)]
|
||||||
|
fn send_poll_vote(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] pollAuthor: Option<String>,
|
||||||
|
#[allow(non_snake_case)] pollTimestamp: u64,
|
||||||
|
option: Vec<i32>,
|
||||||
|
#[allow(non_snake_case)] voteCount: i32,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendPollTerminate", param_kind = map)]
|
||||||
|
fn send_poll_terminate(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] pollTimestamp: u64,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendUnpinMessage", param_kind = map)]
|
||||||
|
fn send_unpin_message(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] targetAuthor: String,
|
||||||
|
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||||
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
|
story: bool,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "sendPaymentNotification", param_kind = map)]
|
#[method(name = "sendPaymentNotification", param_kind = map)]
|
||||||
fn send_payment_notification(
|
fn send_payment_notification(
|
||||||
&self,
|
&self,
|
||||||
@ -166,7 +307,9 @@ pub trait Rpc {
|
|||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
#[allow(non_snake_case)] groupIds: Vec<String>,
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
usernames: Vec<String>,
|
||||||
#[allow(non_snake_case)] noteToSelf: bool,
|
#[allow(non_snake_case)] noteToSelf: bool,
|
||||||
|
#[allow(non_snake_case)] notifySelf: bool,
|
||||||
emoji: String,
|
emoji: String,
|
||||||
#[allow(non_snake_case)] targetAuthor: String,
|
#[allow(non_snake_case)] targetAuthor: String,
|
||||||
#[allow(non_snake_case)] targetTimestamp: u64,
|
#[allow(non_snake_case)] targetTimestamp: u64,
|
||||||
@ -179,6 +322,7 @@ pub trait Rpc {
|
|||||||
&self,
|
&self,
|
||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
recipient: String,
|
recipient: String,
|
||||||
|
usernames: Vec<String>,
|
||||||
#[allow(non_snake_case)] targetTimestamps: Vec<u64>,
|
#[allow(non_snake_case)] targetTimestamps: Vec<u64>,
|
||||||
r#type: String,
|
r#type: String,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
@ -195,6 +339,15 @@ pub trait Rpc {
|
|||||||
stop: bool,
|
stop: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "sendMessageRequestResponse", param_kind = map)]
|
||||||
|
fn send_message_request_response(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
recipients: Vec<String>,
|
||||||
|
#[allow(non_snake_case)] groupIds: Vec<String>,
|
||||||
|
r#type: String,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "setPin", param_kind = map)]
|
#[method(name = "setPin", param_kind = map)]
|
||||||
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>;
|
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
@ -206,6 +359,15 @@ pub trait Rpc {
|
|||||||
captcha: String,
|
captcha: String,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "startChangeNumber", param_kind = map)]
|
||||||
|
fn start_change_number(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
number: String,
|
||||||
|
voice: bool,
|
||||||
|
captcha: Option<String>,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "startLink", param_kind = map)]
|
#[method(name = "startLink", param_kind = map)]
|
||||||
fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>;
|
fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>;
|
||||||
|
|
||||||
@ -233,18 +395,24 @@ pub trait Rpc {
|
|||||||
#[allow(non_snake_case)] deleteAccount: bool,
|
#[allow(non_snake_case)] deleteAccount: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
#[method(name = "updateAccount", param_kind = map)]
|
#[method(name = "updateAccount", param_kind = map)]
|
||||||
fn update_account(
|
fn update_account(
|
||||||
&self,
|
&self,
|
||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
#[allow(non_snake_case)] deviceName: Option<String>,
|
deviceName: Option<String>,
|
||||||
|
unrestrictedUnidentifiedSender: Option<bool>,
|
||||||
|
discoverableByNumber: Option<bool>,
|
||||||
|
numberSharing: Option<bool>,
|
||||||
|
username: Option<String>,
|
||||||
|
deleteUsername: bool,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "updateConfiguration", param_kind = map)]
|
#[method(name = "updateConfiguration", param_kind = map)]
|
||||||
fn update_configuration(
|
fn update_configuration(
|
||||||
&self,
|
&self,
|
||||||
account: Option<String>,
|
account: Option<String>,
|
||||||
#[allow(non_snake_case)] readReceiptes: Option<bool>,
|
#[allow(non_snake_case)] readReceipts: Option<bool>,
|
||||||
#[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>,
|
#[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>,
|
||||||
#[allow(non_snake_case)] typingIndicators: Option<bool>,
|
#[allow(non_snake_case)] typingIndicators: Option<bool>,
|
||||||
#[allow(non_snake_case)] linkPreviews: Option<bool>,
|
#[allow(non_snake_case)] linkPreviews: Option<bool>,
|
||||||
@ -257,6 +425,19 @@ pub trait Rpc {
|
|||||||
recipient: String,
|
recipient: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
expiration: Option<u32>,
|
expiration: Option<u32>,
|
||||||
|
#[allow(non_snake_case)] givenName: Option<String>,
|
||||||
|
#[allow(non_snake_case)] familyName: Option<String>,
|
||||||
|
#[allow(non_snake_case)] nickGivenName: Option<String>,
|
||||||
|
#[allow(non_snake_case)] nickFamilyName: Option<String>,
|
||||||
|
note: Option<String>,
|
||||||
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
|
#[method(name = "updateDevice", param_kind = map)]
|
||||||
|
fn update_device(
|
||||||
|
&self,
|
||||||
|
account: Option<String>,
|
||||||
|
#[allow(non_snake_case)] deviceId: u32,
|
||||||
|
#[allow(non_snake_case)] deviceName: String,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "updateGroup", param_kind = map)]
|
#[method(name = "updateGroup", param_kind = map)]
|
||||||
@ -279,6 +460,8 @@ pub trait Rpc {
|
|||||||
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
|
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
|
||||||
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
|
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
|
||||||
expiration: Option<u32>,
|
expiration: Option<u32>,
|
||||||
|
#[allow(non_snake_case)] memberLabelEmoji: Option<String>,
|
||||||
|
#[allow(non_snake_case)] memberLabel: Option<String>,
|
||||||
) -> Result<Value, ErrorObjectOwned>;
|
) -> Result<Value, ErrorObjectOwned>;
|
||||||
|
|
||||||
#[method(name = "updateProfile", param_kind = map)]
|
#[method(name = "updateProfile", param_kind = map)]
|
||||||
@ -327,20 +510,23 @@ pub struct JsonLink {
|
|||||||
pub device_link_uri: String,
|
pub device_link_uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect_tcp(tcp: impl ToSocketAddrs) -> Result<impl SubscriptionClientT, Error> {
|
pub async fn connect_tcp(
|
||||||
|
tcp: impl ToSocketAddrs,
|
||||||
|
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
||||||
let (sender, receiver) = super::transports::tcp::connect(tcp).await?;
|
let (sender, receiver) = super::transports::tcp::connect(tcp).await?;
|
||||||
|
|
||||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
pub async fn connect_unix(
|
pub async fn connect_unix(
|
||||||
socket_path: impl AsRef<Path>,
|
socket_path: impl AsRef<Path>,
|
||||||
) -> Result<impl SubscriptionClientT, Error> {
|
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
||||||
let (sender, receiver) = super::transports::ipc::connect(socket_path).await?;
|
let (sender, receiver) = super::transports::ipc::connect(socket_path).await?;
|
||||||
|
|
||||||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT, Error> {
|
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> {
|
||||||
HttpClientBuilder::default().build(uri)
|
HttpClientBuilder::default().build(uri)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
use std::{path::PathBuf, time::Duration};
|
use std::{path::PathBuf, time::Duration};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::Cli;
|
use jsonrpsee::core::client::{Error as RpcError, Subscription, SubscriptionClientT};
|
||||||
use jsonrpsee::core::client::{Subscription, SubscriptionClientT};
|
use serde_json::{Error, Value};
|
||||||
use jsonrpsee::core::Error as RpcError;
|
|
||||||
use serde_json::Value;
|
|
||||||
use tokio::{select, time::sleep};
|
use tokio::{select, time::sleep};
|
||||||
|
|
||||||
use crate::cli::{GroupPermission, LinkState};
|
use cli::Cli;
|
||||||
|
|
||||||
|
use crate::cli::{CliCommands, GroupPermission, LinkState};
|
||||||
use crate::jsonrpc::RpcClient;
|
use crate::jsonrpc::RpcClient;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
@ -38,7 +38,7 @@ async fn handle_command(
|
|||||||
client: impl SubscriptionClientT + Sync,
|
client: impl SubscriptionClientT + Sync,
|
||||||
) -> Result<Value, RpcError> {
|
) -> Result<Value, RpcError> {
|
||||||
match cli.command {
|
match cli.command {
|
||||||
cli::CliCommands::Receive { timeout } => {
|
CliCommands::Receive { timeout } => {
|
||||||
let mut stream = client.subscribe_receive(cli.account).await?;
|
let mut stream = client.subscribe_receive(cli.account).await?;
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -50,50 +50,63 @@ async fn handle_command(
|
|||||||
stream.unsubscribe().await?;
|
stream.unsubscribe().await?;
|
||||||
Ok(Value::Null)
|
Ok(Value::Null)
|
||||||
}
|
}
|
||||||
cli::CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
|
CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
|
||||||
cli::CliCommands::Block {
|
CliCommands::Block {
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
} => client.block(cli.account, recipient, group_id).await,
|
} => client.block(cli.account, recipient, group_id).await,
|
||||||
cli::CliCommands::DeleteLocalAccountData { ignore_registered } => {
|
CliCommands::DeleteLocalAccountData { ignore_registered } => {
|
||||||
client
|
client
|
||||||
.delete_local_account_data(cli.account, ignore_registered)
|
.delete_local_account_data(cli.account, ignore_registered)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::GetUserStatus { recipient } => {
|
CliCommands::GetUserStatus {
|
||||||
client.get_user_status(cli.account, recipient).await
|
recipient,
|
||||||
|
username,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.get_user_status(cli.account, recipient, username)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
||||||
cli::CliCommands::Link { name } => {
|
CliCommands::Link { name } => {
|
||||||
let url = client
|
let url = client
|
||||||
.start_link(cli.account)
|
.start_link(cli.account)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
||||||
.device_link_uri;
|
.device_link_uri;
|
||||||
println!("{}", url);
|
println!("{url}");
|
||||||
client.finish_link(url, name).await
|
client.finish_link(url, name).await
|
||||||
}
|
}
|
||||||
cli::CliCommands::ListAccounts => client.list_accounts().await,
|
CliCommands::ListAccounts => client.list_accounts().await,
|
||||||
cli::CliCommands::ListContacts {
|
CliCommands::ListContacts {
|
||||||
recipient,
|
recipient,
|
||||||
all_recipients,
|
all_recipients,
|
||||||
blocked,
|
blocked,
|
||||||
name,
|
name,
|
||||||
|
detailed,
|
||||||
|
internal,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.list_contacts(cli.account, recipient, all_recipients, blocked, name)
|
.list_contacts(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
all_recipients,
|
||||||
|
blocked,
|
||||||
|
name,
|
||||||
|
detailed,
|
||||||
|
internal,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::ListDevices => client.list_devices(cli.account).await,
|
CliCommands::ListDevices => client.list_devices(cli.account).await,
|
||||||
cli::CliCommands::ListGroups {
|
CliCommands::ListGroups {
|
||||||
detailed: _,
|
detailed: _,
|
||||||
group_id,
|
group_id,
|
||||||
} => client.list_groups(cli.account, group_id).await,
|
} => client.list_groups(cli.account, group_id).await,
|
||||||
cli::CliCommands::ListIdentities { number } => {
|
CliCommands::ListIdentities { number } => client.list_identities(cli.account, number).await,
|
||||||
client.list_identities(cli.account, number).await
|
CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
|
||||||
}
|
CliCommands::QuitGroup {
|
||||||
cli::CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
|
|
||||||
cli::CliCommands::QuitGroup {
|
|
||||||
group_id,
|
group_id,
|
||||||
delete,
|
delete,
|
||||||
admin,
|
admin,
|
||||||
@ -102,17 +115,29 @@ async fn handle_command(
|
|||||||
.quit_group(cli.account, group_id, delete, admin)
|
.quit_group(cli.account, group_id, delete, admin)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::Register { voice, captcha } => {
|
CliCommands::Register {
|
||||||
client.register(cli.account, voice, captcha).await
|
voice,
|
||||||
|
captcha,
|
||||||
|
reregister,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.register(cli.account, voice, captcha, reregister)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::RemoveContact { recipient, forget } => {
|
CliCommands::RemoveContact {
|
||||||
client.remove_contact(cli.account, recipient, forget).await
|
recipient,
|
||||||
|
forget,
|
||||||
|
hide,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.remove_contact(cli.account, recipient, forget, hide)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::RemoveDevice { device_id } => {
|
CliCommands::RemoveDevice { device_id } => {
|
||||||
client.remove_device(cli.account, device_id).await
|
client.remove_device(cli.account, device_id).await
|
||||||
}
|
}
|
||||||
cli::CliCommands::RemovePin => client.remove_pin(cli.account).await,
|
CliCommands::RemovePin => client.remove_pin(cli.account).await,
|
||||||
cli::CliCommands::RemoteDelete {
|
CliCommands::RemoteDelete {
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
@ -128,44 +153,91 @@ async fn handle_command(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::Send {
|
CliCommands::Send {
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
|
username,
|
||||||
|
notify_self,
|
||||||
note_to_self,
|
note_to_self,
|
||||||
end_session,
|
end_session,
|
||||||
message,
|
message,
|
||||||
|
message_from_stdin,
|
||||||
attachment,
|
attachment,
|
||||||
|
view_once,
|
||||||
mention,
|
mention,
|
||||||
|
text_style,
|
||||||
quote_timestamp,
|
quote_timestamp,
|
||||||
quote_author,
|
quote_author,
|
||||||
quote_message,
|
quote_message,
|
||||||
quote_mention,
|
quote_mention,
|
||||||
|
quote_text_style,
|
||||||
|
quote_attachment,
|
||||||
|
preview_url,
|
||||||
|
preview_title,
|
||||||
|
preview_description,
|
||||||
|
preview_image,
|
||||||
sticker,
|
sticker,
|
||||||
story_timestamp,
|
story_timestamp,
|
||||||
story_author,
|
story_author,
|
||||||
|
edit_timestamp,
|
||||||
|
no_urgent,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.send(
|
.send(
|
||||||
cli.account,
|
cli.account,
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
|
username,
|
||||||
|
notify_self,
|
||||||
note_to_self,
|
note_to_self,
|
||||||
end_session,
|
end_session,
|
||||||
message.unwrap_or_default(),
|
if message_from_stdin {
|
||||||
|
std::io::read_to_string(std::io::stdin()).unwrap()
|
||||||
|
} else {
|
||||||
|
message.unwrap_or_default()
|
||||||
|
},
|
||||||
attachment,
|
attachment,
|
||||||
|
view_once,
|
||||||
mention,
|
mention,
|
||||||
|
text_style,
|
||||||
quote_timestamp,
|
quote_timestamp,
|
||||||
quote_author,
|
quote_author,
|
||||||
quote_message,
|
quote_message,
|
||||||
quote_mention,
|
quote_mention,
|
||||||
|
quote_text_style,
|
||||||
|
quote_attachment,
|
||||||
|
preview_url,
|
||||||
|
preview_title,
|
||||||
|
preview_description,
|
||||||
|
preview_image,
|
||||||
sticker,
|
sticker,
|
||||||
story_timestamp,
|
story_timestamp,
|
||||||
story_author,
|
story_author,
|
||||||
|
edit_timestamp,
|
||||||
|
no_urgent,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::SendContacts => client.send_contacts(cli.account).await,
|
CliCommands::SendContacts => client.send_contacts(cli.account).await,
|
||||||
cli::CliCommands::SendPaymentNotification {
|
CliCommands::SendAdminDelete {
|
||||||
|
group_id,
|
||||||
|
target_author,
|
||||||
|
target_timestamp,
|
||||||
|
story,
|
||||||
|
notify_self,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.send_admin_delete(
|
||||||
|
cli.account,
|
||||||
|
group_id,
|
||||||
|
target_author,
|
||||||
|
target_timestamp,
|
||||||
|
story,
|
||||||
|
notify_self,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::SendPaymentNotification {
|
||||||
recipient,
|
recipient,
|
||||||
receipt,
|
receipt,
|
||||||
note,
|
note,
|
||||||
@ -174,10 +246,108 @@ async fn handle_command(
|
|||||||
.send_payment_notification(cli.account, recipient, receipt, note)
|
.send_payment_notification(cli.account, recipient, receipt, note)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::SendReaction {
|
CliCommands::SendPinMessage {
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
|
username,
|
||||||
|
target_author,
|
||||||
|
target_timestamp,
|
||||||
|
pin_duration,
|
||||||
note_to_self,
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
story,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.send_pin_message(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
target_author,
|
||||||
|
target_timestamp,
|
||||||
|
pin_duration,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
story,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::SendPollCreate {
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
question,
|
||||||
|
option,
|
||||||
|
no_multi,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.send_poll_create(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
question,
|
||||||
|
option,
|
||||||
|
no_multi,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::SendPollTerminate {
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
poll_timestamp,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.send_poll_terminate(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
poll_timestamp,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::SendPollVote {
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
poll_author,
|
||||||
|
poll_timestamp,
|
||||||
|
option,
|
||||||
|
vote_count,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.send_poll_vote(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
poll_author,
|
||||||
|
poll_timestamp,
|
||||||
|
option,
|
||||||
|
vote_count,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::SendReaction {
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
emoji,
|
emoji,
|
||||||
target_author,
|
target_author,
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
@ -189,7 +359,9 @@ async fn handle_command(
|
|||||||
cli.account,
|
cli.account,
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
|
username,
|
||||||
note_to_self,
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
emoji,
|
emoji,
|
||||||
target_author,
|
target_author,
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
@ -198,8 +370,9 @@ async fn handle_command(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::SendReceipt {
|
CliCommands::SendReceipt {
|
||||||
recipient,
|
recipient,
|
||||||
|
username,
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
r#type,
|
r#type,
|
||||||
} => {
|
} => {
|
||||||
@ -207,6 +380,7 @@ async fn handle_command(
|
|||||||
.send_receipt(
|
.send_receipt(
|
||||||
cli.account,
|
cli.account,
|
||||||
recipient,
|
recipient,
|
||||||
|
username,
|
||||||
target_timestamp,
|
target_timestamp,
|
||||||
match r#type {
|
match r#type {
|
||||||
cli::ReceiptType::Read => "read".to_owned(),
|
cli::ReceiptType::Read => "read".to_owned(),
|
||||||
@ -215,8 +389,8 @@ async fn handle_command(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
|
CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
|
||||||
cli::CliCommands::SendTyping {
|
CliCommands::SendTyping {
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
stop,
|
stop,
|
||||||
@ -225,13 +399,37 @@ async fn handle_command(
|
|||||||
.send_typing(cli.account, recipient, group_id, stop)
|
.send_typing(cli.account, recipient, group_id, stop)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
|
CliCommands::SendUnpinMessage {
|
||||||
cli::CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
target_author,
|
||||||
|
target_timestamp,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
story,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.send_unpin_message(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
username,
|
||||||
|
target_author,
|
||||||
|
target_timestamp,
|
||||||
|
note_to_self,
|
||||||
|
notify_self,
|
||||||
|
story,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
|
||||||
|
CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
|
||||||
client
|
client
|
||||||
.submit_rate_limit_challenge(cli.account, challenge, captcha)
|
.submit_rate_limit_challenge(cli.account, challenge, captcha)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::Trust {
|
CliCommands::Trust {
|
||||||
recipient,
|
recipient,
|
||||||
trust_all_known_keys,
|
trust_all_known_keys,
|
||||||
verified_safety_number,
|
verified_safety_number,
|
||||||
@ -245,17 +443,34 @@ async fn handle_command(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::Unblock {
|
CliCommands::Unblock {
|
||||||
recipient,
|
recipient,
|
||||||
group_id,
|
group_id,
|
||||||
} => client.unblock(cli.account, recipient, group_id).await,
|
} => client.unblock(cli.account, recipient, group_id).await,
|
||||||
cli::CliCommands::Unregister { delete_account } => {
|
CliCommands::Unregister { delete_account } => {
|
||||||
client.unregister(cli.account, delete_account).await
|
client.unregister(cli.account, delete_account).await
|
||||||
}
|
}
|
||||||
cli::CliCommands::UpdateAccount { device_name } => {
|
CliCommands::UpdateAccount {
|
||||||
client.update_account(cli.account, device_name).await
|
device_name,
|
||||||
|
unrestricted_unidentified_sender,
|
||||||
|
discoverable_by_number,
|
||||||
|
number_sharing,
|
||||||
|
username,
|
||||||
|
delete_username,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.update_account(
|
||||||
|
cli.account,
|
||||||
|
device_name,
|
||||||
|
unrestricted_unidentified_sender,
|
||||||
|
discoverable_by_number,
|
||||||
|
number_sharing,
|
||||||
|
username,
|
||||||
|
delete_username,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::UpdateConfiguration {
|
CliCommands::UpdateConfiguration {
|
||||||
read_receipts,
|
read_receipts,
|
||||||
unidentified_delivery_indicators,
|
unidentified_delivery_indicators,
|
||||||
typing_indicators,
|
typing_indicators,
|
||||||
@ -271,16 +486,39 @@ async fn handle_command(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::UpdateContact {
|
CliCommands::UpdateContact {
|
||||||
recipient,
|
recipient,
|
||||||
expiration,
|
expiration,
|
||||||
name,
|
name,
|
||||||
|
given_name,
|
||||||
|
family_name,
|
||||||
|
nick_given_name,
|
||||||
|
nick_family_name,
|
||||||
|
note,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.update_contact(cli.account, recipient, name, expiration)
|
.update_contact(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
name,
|
||||||
|
expiration,
|
||||||
|
given_name,
|
||||||
|
family_name,
|
||||||
|
nick_given_name,
|
||||||
|
nick_family_name,
|
||||||
|
note,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::UpdateGroup {
|
CliCommands::UpdateDevice {
|
||||||
|
device_id,
|
||||||
|
device_name,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.update_device(cli.account, device_id, device_name)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::UpdateGroup {
|
||||||
group_id,
|
group_id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@ -297,6 +535,8 @@ async fn handle_command(
|
|||||||
set_permission_edit_details,
|
set_permission_edit_details,
|
||||||
set_permission_send_messages,
|
set_permission_send_messages,
|
||||||
expiration,
|
expiration,
|
||||||
|
member_label_emoji,
|
||||||
|
member_label,
|
||||||
} => {
|
} => {
|
||||||
client
|
client
|
||||||
.update_group(
|
.update_group(
|
||||||
@ -330,10 +570,12 @@ async fn handle_command(
|
|||||||
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
|
||||||
}),
|
}),
|
||||||
expiration,
|
expiration,
|
||||||
|
member_label_emoji,
|
||||||
|
member_label,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::UpdateProfile {
|
CliCommands::UpdateProfile {
|
||||||
given_name,
|
given_name,
|
||||||
family_name,
|
family_name,
|
||||||
about,
|
about,
|
||||||
@ -355,14 +597,72 @@ async fn handle_command(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
cli::CliCommands::UploadStickerPack { path } => {
|
CliCommands::UploadStickerPack { path } => {
|
||||||
client.upload_sticker_pack(cli.account, path).await
|
client.upload_sticker_pack(cli.account, path).await
|
||||||
}
|
}
|
||||||
cli::CliCommands::Verify {
|
CliCommands::Verify {
|
||||||
verification_code,
|
verification_code,
|
||||||
pin,
|
pin,
|
||||||
} => client.verify(cli.account, verification_code, pin).await,
|
} => client.verify(cli.account, verification_code, pin).await,
|
||||||
cli::CliCommands::Version => client.version().await,
|
CliCommands::Version => client.version().await,
|
||||||
|
CliCommands::AddStickerPack { uri } => client.add_sticker_pack(cli.account, uri).await,
|
||||||
|
CliCommands::FinishChangeNumber {
|
||||||
|
number,
|
||||||
|
verification_code,
|
||||||
|
pin,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.finish_change_number(cli.account, number, verification_code, pin)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::GetAttachment {
|
||||||
|
id,
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.get_attachment(cli.account, id, recipient, group_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::GetAvatar {
|
||||||
|
contact,
|
||||||
|
profile,
|
||||||
|
group_id,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.get_avatar(cli.account, contact, profile, group_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::GetSticker {
|
||||||
|
pack_id,
|
||||||
|
sticker_id,
|
||||||
|
} => client.get_sticker(cli.account, pack_id, sticker_id).await,
|
||||||
|
CliCommands::StartChangeNumber {
|
||||||
|
number,
|
||||||
|
voice,
|
||||||
|
captcha,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.start_change_number(cli.account, number, voice, captcha)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
CliCommands::SendMessageRequestResponse {
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
r#type,
|
||||||
|
} => {
|
||||||
|
client
|
||||||
|
.send_message_request_response(
|
||||||
|
cli.account,
|
||||||
|
recipient,
|
||||||
|
group_id,
|
||||||
|
match r#type {
|
||||||
|
cli::MessageRequestResponseType::Accept => "accept".to_owned(),
|
||||||
|
cli::MessageRequestResponseType::Delete => "delete".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,6 +686,12 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
|
|||||||
|
|
||||||
handle_command(cli, client).await
|
handle_command(cli, client).await
|
||||||
} else {
|
} else {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
Err(RpcError::Custom("Invalid socket".into()))
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
let socket_path = cli
|
let socket_path = cli
|
||||||
.json_rpc_socket
|
.json_rpc_socket
|
||||||
.clone()
|
.clone()
|
||||||
@ -405,11 +711,12 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
|
|||||||
handle_command(cli, client).await
|
handle_command(cli, client).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn stream_next(
|
async fn stream_next(
|
||||||
timeout: f64,
|
timeout: f64,
|
||||||
stream: &mut Subscription<Value>,
|
stream: &mut Subscription<Value>,
|
||||||
) -> Option<Result<Value, RpcError>> {
|
) -> Option<Result<Value, Error>> {
|
||||||
if timeout < 0.0 {
|
if timeout < 0.0 {
|
||||||
stream.next().await
|
stream.next().await
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
|
use std::io::Error;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use futures_util::stream::StreamExt;
|
use futures_util::stream::StreamExt;
|
||||||
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
||||||
use jsonrpsee::core::Error;
|
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
use tokio_util::codec::Decoder;
|
use tokio_util::codec::Decoder;
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
||||||
use jsonrpsee::core::{
|
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
|
||||||
async_trait,
|
|
||||||
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
|
|
||||||
};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
pub mod ipc;
|
pub mod ipc;
|
||||||
mod stream_codec;
|
mod stream_codec;
|
||||||
pub mod tcp;
|
pub mod tcp;
|
||||||
@ -21,7 +19,6 @@ struct Sender<T: Send + Sink<String>> {
|
|||||||
inner: T,
|
inner: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
||||||
for Sender<T>
|
for Sender<T>
|
||||||
{
|
{
|
||||||
@ -31,7 +28,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
|
|||||||
self.inner
|
self.inner
|
||||||
.send(body)
|
.send(body)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +36,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
|
|||||||
self.inner
|
self.inner
|
||||||
.close()
|
.close()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,7 +45,6 @@ struct Receiver<T: Send + Stream> {
|
|||||||
inner: T,
|
inner: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
||||||
for Receiver<T>
|
for Receiver<T>
|
||||||
{
|
{
|
||||||
@ -58,7 +54,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
|
|||||||
match self.inner.next().await {
|
match self.inner.next().await {
|
||||||
None => Err(Errors::Closed),
|
None => Err(Errors::Closed),
|
||||||
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
||||||
Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))),
|
Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ impl Decoder for StreamCodec {
|
|||||||
|
|
||||||
match str::from_utf8(line.as_ref()) {
|
match str::from_utf8(line.as_ref()) {
|
||||||
Ok(s) => Ok(Some(s.to_string())),
|
Ok(s) => Ok(Some(s.to_string())),
|
||||||
Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid UTF-8")),
|
Err(_) => Err(io::Error::other("invalid UTF-8")),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
use std::io::Error;
|
||||||
|
|
||||||
use futures_util::stream::StreamExt;
|
use futures_util::stream::StreamExt;
|
||||||
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
|
||||||
use jsonrpsee::core::Error;
|
|
||||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||||
use tokio_util::codec::Decoder;
|
use tokio_util::codec::Decoder;
|
||||||
|
|
||||||
|
|||||||
154
data/org.asamk.SignalCli.metainfo.xml
Normal file
154
data/org.asamk.SignalCli.metainfo.xml
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="console-application">
|
||||||
|
<id>org.asamk.SignalCli</id>
|
||||||
|
|
||||||
|
<name>signal-cli</name>
|
||||||
|
<summary>Use Signal messenger in terminal</summary>
|
||||||
|
<developer id="org.asamk">
|
||||||
|
<name>AsamK</name>
|
||||||
|
</developer>
|
||||||
|
<icon type="stock">org.asamk.SignalCli</icon>
|
||||||
|
<keywords>
|
||||||
|
<keyword>signal</keyword>
|
||||||
|
<keyword>signal-cli</keyword>
|
||||||
|
<keyword>messenger</keyword>
|
||||||
|
<keyword>messaging</keyword>
|
||||||
|
</keywords>
|
||||||
|
|
||||||
|
<url type="bugtracker">https://github.com/AsamK/signal-cli/issues</url>
|
||||||
|
<url type="homepage">https://github.com/AsamK/signal-cli</url>
|
||||||
|
<url type="donation">https://github.com/sponsors/AsamK</url>
|
||||||
|
<url type="faq">https://github.com/AsamK/signal-cli/discussions</url>
|
||||||
|
<url type="vcs-browser">https://github.com/AsamK/signal-cli</url>
|
||||||
|
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>GPL-3.0-only</project_license>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
signal-cli is an unofficial commandline interface for the Signal Messenger.
|
||||||
|
It supports many Signal functions, including registering, verifying, sending and receiving messages.
|
||||||
|
For registering you need a phone number where you can receive SMS or incoming calls.
|
||||||
|
Alternatively signal-cli can be linked to an existing App account.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<categories>
|
||||||
|
<category>Utility</category>
|
||||||
|
<category>Java</category>
|
||||||
|
</categories>
|
||||||
|
|
||||||
|
<provides>
|
||||||
|
<binary>signal-cli</binary>
|
||||||
|
</provides>
|
||||||
|
<content_rating type="oars-1.1">
|
||||||
|
<content_attribute id="social-chat">intense</content_attribute>
|
||||||
|
</content_rating>
|
||||||
|
<releases>
|
||||||
|
<release version="0.14.7" date="2026-08-01">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.7</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.14.6" date="2026-07-12">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.6</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.14.5" date="2026-06-11">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.5</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.14.4" date="2026-05-23">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.14.3" date="2026-04-22">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.3</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.14.2" date="2026-04-04">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.14.1" date="2026-03-08">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.1</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.14.0" date="2026-03-01">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.0</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.24" date="2026-02-05">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.24</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.23" date="2026-01-24">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.23</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.22" date="2025-11-14">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.22</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.21" date="2025-10-25">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.21</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.20" date="2025-09-23">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.20</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.19" date="2025-09-15">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.19</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.18" date="2025-07-16">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.18</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.17" date="2025-06-28">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.17</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.16" date="2025-06-07">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.16</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.15" date="2025-05-08">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.15</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.14" date="2025-04-06">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.14</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.13" date="2025-02-28">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.13</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.12" date="2025-01-18">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.12</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.11" date="2024-12-26">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.11</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.10" date="2024-11-30">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.10</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.9" date="2024-10-28">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.9</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.8" date="2024-10-26">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.8</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.7" date="2024-09-28">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.7</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.6" date="2024-09-08">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.6</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.5" date="2024-07-25">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.5</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.4" date="2024-06-06">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.4</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.3" date="2024-04-19">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.3</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.2" date="2024-03-23">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.2</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.1" date="2024-02-27">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.1</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.13.0" date="2024-02-18">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.0</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.12.8" date="2024-02-08">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.8</url>
|
||||||
|
</release>
|
||||||
|
<release version="0.12.7" date="2023-12-15">
|
||||||
|
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.7</url>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
</component>
|
||||||
10
data/org.asamk.SignalCli.svg
Normal file
10
data/org.asamk.SignalCli.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="128" height="128" version="1.1" viewBox="0 0 33.867 33.867" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(-32.279 -138.64)">
|
||||||
|
<g transform="matrix(.45526 0 0 .45526 33.984 140.17)">
|
||||||
|
<path d="m33.468 66.938c-18.454 0-33.468-15.014-33.468-33.469s15.014-33.469 33.468-33.469c18.455 0 33.469 15.014 33.469 33.469 0 5.621-1.421 11.161-4.116 16.076l4.608 17.2-16.849-4.516c-5.172 3.084-11.069 4.709-17.112 4.709z" fill="#fff"/>
|
||||||
|
<path d="m33.468 67.184c-18.454 0-33.468-15.014-33.468-33.469s15.014-33.469 33.468-33.469c18.455 0 33.469 15.014 33.469 33.469 0 5.621-1.421 11.161-4.116 16.076l4.608 17.2-16.849-4.516c-5.172 3.084-11.069 4.709-17.112 4.709zm0-62.938c-16.249 0-29.468 13.22-29.468 29.469s13.219 29.469 29.468 29.469c5.582 0 11.021-1.574 15.729-4.554l0.74-0.468 11.835 3.171-3.243-12.1 0.419-0.72c2.609-4.484 3.988-9.602 3.988-14.799 0-16.248-13.219-29.468-29.468-29.468z"/>
|
||||||
|
<path d="m25.515 45.296q-2.3937 0-4.2817-0.97772-1.8543-0.97772-2.9332-3.0343-1.0451-2.0566-1.0451-5.2595 0-3.3377 1.1126-5.428 1.1126-2.0903 3.0006-3.068 1.9217-0.97772 4.3492-0.97772 1.3823 0 2.6634 0.30343 1.2812 0.26972 2.0903 0.67429l-0.91029 2.4612q-0.80915-0.30343-1.888-0.57315t-2.0229-0.26972q-5.3269 0-5.3269 6.844 0 3.2703 1.2812 5.0235 1.3149 1.7194 3.8772 1.7194 1.4834 0 2.596-0.30343 1.1463-0.30343 2.0903-0.74172v2.6297q-0.91029 0.472-2.0229 0.708-1.0789 0.26972-2.6297 0.26972zm11.901-0.33714h-2.9669v-25.623h2.9669zm7.2486-24.848q0.67429 0 1.18 0.472 0.53943 0.43829 0.53943 1.416 0 0.94401-0.53943 1.416-0.50572 0.472-1.18 0.472-0.74172 0-1.2474-0.472-0.50572-0.472-0.50572-1.416 0-0.97772 0.50572-1.416 0.50572-0.472 1.2474-0.472zm1.4497 6.7766v18.071h-2.9669v-18.071z" aria-label="cli"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -32,8 +32,6 @@ RestrictAddressFamilies=AF_INET AF_INET6
|
|||||||
RestrictNamespaces=true
|
RestrictNamespaces=true
|
||||||
RestrictRealtime=true
|
RestrictRealtime=true
|
||||||
RestrictSUIDSGID=true
|
RestrictSUIDSGID=true
|
||||||
# JVM always exits with 143 in reaction to SIGTERM signal
|
|
||||||
SuccessExitStatus=143
|
|
||||||
StandardInput=socket
|
StandardInput=socket
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
|
|||||||
@ -8,11 +8,9 @@ After=network-online.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=dbus
|
Type=dbus
|
||||||
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
||||||
ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon --system
|
ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon --dbus-system
|
||||||
User=signal-cli
|
User=signal-cli
|
||||||
BusName=org.asamk.Signal
|
BusName=org.asamk.Signal
|
||||||
# JVM always exits with 143 in reaction to SIGTERM signal
|
|
||||||
SuccessExitStatus=143
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
Alias=dbus-org.asamk.Signal.service
|
Alias=dbus-org.asamk.Signal.service
|
||||||
|
|||||||
@ -8,11 +8,9 @@ After=network-online.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=dbus
|
Type=dbus
|
||||||
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
Environment="SIGNAL_CLI_OPTS=-Xms2m"
|
||||||
ExecStart=%dir%/bin/signal-cli -a %I --config /var/lib/signal-cli daemon --system
|
ExecStart=%dir%/bin/signal-cli -a %I --config /var/lib/signal-cli daemon --dbus-system
|
||||||
User=signal-cli
|
User=signal-cli
|
||||||
BusName=org.asamk.Signal
|
BusName=org.asamk.Signal
|
||||||
# JVM always exits with 143 in reaction to SIGTERM signal
|
|
||||||
SuccessExitStatus=143
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
Alias=dbus-org.asamk.Signal.service
|
Alias=dbus-org.asamk.Signal.service
|
||||||
|
|||||||
359
docs/CALL_TUNNEL.md
Normal file
359
docs/CALL_TUNNEL.md
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
# Voice Call Support
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
signal-cli supports voice calls by spawning a subprocess called
|
||||||
|
`signal-call-tunnel` for each call. The tunnel handles WebRTC negotiation and
|
||||||
|
audio transport. signal-cli communicates with the tunnel over its stdin/stdout
|
||||||
|
using newline-delimited JSON messages, relaying signaling between the tunnel
|
||||||
|
and the Signal protocol.
|
||||||
|
|
||||||
|
```
|
||||||
|
signal-cli signal-call-tunnel
|
||||||
|
| |
|
||||||
|
|-- spawn --------------------------->|
|
||||||
|
|-- config JSON on stdin ------------>|
|
||||||
|
| |
|
||||||
|
|-- commands on stdin --------------->|
|
||||||
|
|<-- events on stdout ----------------|
|
||||||
|
| | WebRTC
|
||||||
|
| signaling relay | audio I/O
|
||||||
|
| |
|
||||||
|
| (stderr: tunnel logging) -------->| (captured by signal-cli)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each call gets its own tunnel process. When the call ends, signal-cli closes
|
||||||
|
stdin and destroys the process.
|
||||||
|
|
||||||
|
Audio device names (`inputDeviceName`, `outputDeviceName`) are opaque strings
|
||||||
|
returned by the tunnel in its `ready` message. signal-cli passes them through
|
||||||
|
to JSON-RPC clients, which use them to connect audio via platform APIs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spawning the Tunnel
|
||||||
|
|
||||||
|
For each call, signal-cli:
|
||||||
|
|
||||||
|
1. Spawns `signal-call-tunnel`
|
||||||
|
2. Writes config JSON followed by a newline to stdin
|
||||||
|
3. Keeps stdin open for subsequent control messages
|
||||||
|
4. Reads control events from stdout
|
||||||
|
5. Captures stderr for logging
|
||||||
|
|
||||||
|
The `signal-call-tunnel` binary is located by searching (in order):
|
||||||
|
|
||||||
|
1. `SIGNAL_CALL_TUNNEL_BIN` environment variable
|
||||||
|
2. `<signal-cli install dir>/bin/signal-call-tunnel` (detected from jar location)
|
||||||
|
3. `signal-call-tunnel` on `PATH`
|
||||||
|
|
||||||
|
### Config JSON
|
||||||
|
|
||||||
|
The first line written to the tunnel's stdin:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"call_id": 12345,
|
||||||
|
"is_outgoing": true,
|
||||||
|
"local_device_id": 1,
|
||||||
|
"input_device_name": "signal_input",
|
||||||
|
"output_device_name": "signal_output"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|----------------------|-------------------------|-----------------------------------------------|
|
||||||
|
| `call_id` | unsigned 64-bit integer | Call identifier (use unsigned representation) |
|
||||||
|
| `is_outgoing` | boolean | Whether this is an outgoing call |
|
||||||
|
| `local_device_id` | integer | Signal device ID |
|
||||||
|
| `input_device_name` | string (optional) | Requested input audio device name |
|
||||||
|
| `output_device_name` | string (optional) | Requested output audio device name |
|
||||||
|
|
||||||
|
If `input_device_name` or `output_device_name` are omitted, the tunnel
|
||||||
|
chooses default names. On Linux, these are per-call unique names (e.g.,
|
||||||
|
`signal_input_<call_id>`). On macOS, these are the fixed names `signal_input`
|
||||||
|
and `signal_output`, which must match the pre-installed BlackHole drivers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Control Protocol
|
||||||
|
|
||||||
|
Newline-delimited JSON messages over stdin (signal-cli to tunnel) and stdout
|
||||||
|
(tunnel to signal-cli). The first line on stdin is the config JSON. Subsequent
|
||||||
|
lines are control messages.
|
||||||
|
|
||||||
|
### signal-cli -> Tunnel (stdin)
|
||||||
|
|
||||||
|
| Type | When | Fields |
|
||||||
|
|----------------------|----------------------------|---------------------------------------------------------------------------------------------------|
|
||||||
|
| `createOutgoingCall` | Outgoing call setup | `callId`, `peerId` |
|
||||||
|
| `proceed` | After offer/receivedOffer | `callId`, `hideIp`, `iceServers` |
|
||||||
|
| `receivedOffer` | Incoming call | `callId`, `peerId`, `opaque`, `age`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
|
||||||
|
| `receivedAnswer` | Outgoing call answered | `opaque`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
|
||||||
|
| `receivedIce` | ICE candidates arrive | `candidates` (array of base64 opaque blobs) |
|
||||||
|
| `accept` | User accepts incoming call | *(none)* |
|
||||||
|
| `hangup` | End the call | *(none)* |
|
||||||
|
|
||||||
|
### Tunnel -> signal-cli (stdout)
|
||||||
|
|
||||||
|
| Type | When | Fields |
|
||||||
|
|---------------|---------------------------------------------|------------------------------------------------------|
|
||||||
|
| `ready` | Control socket bound, audio devices created | `inputDeviceName`, `outputDeviceName` |
|
||||||
|
| `sendOffer` | Tunnel generated an offer | `callId`, `opaque`, `callMediaType` |
|
||||||
|
| `sendAnswer` | Tunnel generated an answer | `callId`, `opaque` |
|
||||||
|
| `sendIce` | ICE candidates gathered | `callId`, `candidates` (array of `{"opaque":"..."}`) |
|
||||||
|
| `sendHangup` | Tunnel wants to hang up | `callId`, `hangupType` |
|
||||||
|
| `sendBusy` | Line is busy | `callId` |
|
||||||
|
| `stateChange` | Call state transition | `state`, `reason` (optional) |
|
||||||
|
| `error` | Something went wrong | `message` |
|
||||||
|
|
||||||
|
Opaque blobs and identity keys are base64-encoded. ICE servers use the format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"urls": [
|
||||||
|
"turn:example.com"
|
||||||
|
],
|
||||||
|
"username": "u",
|
||||||
|
"password": "p"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Startup Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
signal-cli signal-call-tunnel
|
||||||
|
| |
|
||||||
|
|-- spawn process ------------------> |
|
||||||
|
|-- config JSON + newline on stdin ---->|
|
||||||
|
| | parse config
|
||||||
|
| | initialize audio
|
||||||
|
| |
|
||||||
|
|<-------- ready (on stdout) -----------|
|
||||||
|
| {"type":"ready", |
|
||||||
|
| "inputDeviceName":"...", |
|
||||||
|
| "outputDeviceName":"..."} |
|
||||||
|
| |
|
||||||
|
|-- control messages on stdin --------->|
|
||||||
|
|<-- control events on stdout ----------|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Call Flows
|
||||||
|
|
||||||
|
### Outgoing call
|
||||||
|
|
||||||
|
```
|
||||||
|
signal-cli signal-call-tunnel Remote Phone
|
||||||
|
| | |
|
||||||
|
|-- spawn + config ------->| |
|
||||||
|
|<-- ready ----------------| |
|
||||||
|
|-- createOutgoingCall --->| |
|
||||||
|
|-- proceed (TURN) ------->| |
|
||||||
|
| | create offer |
|
||||||
|
|<-- sendOffer ------------| |
|
||||||
|
|-- offer via Signal -------------------------------->|
|
||||||
|
|<-- answer via Signal -------------------------------|
|
||||||
|
|-- receivedAnswer ------->| (+ identity keys) |
|
||||||
|
|<-- sendIce --------------| |
|
||||||
|
|-- ICE via Signal -------------------------------> |
|
||||||
|
|<-- ICE via Signal -------------------------------- |
|
||||||
|
|-- receivedIce ---------->| |
|
||||||
|
| | ICE connects |
|
||||||
|
|<-- stateChange:Connected | |
|
||||||
|
```
|
||||||
|
|
||||||
|
### Incoming call
|
||||||
|
|
||||||
|
```
|
||||||
|
signal-cli signal-call-tunnel Remote Phone
|
||||||
|
| | |
|
||||||
|
|<-- offer via Signal --------------------------------|
|
||||||
|
|-- spawn + config ------->| |
|
||||||
|
|<-- ready ----------------| |
|
||||||
|
|-- receivedOffer -------->| (+ identity keys) |
|
||||||
|
|-- proceed (TURN) ------->| |
|
||||||
|
| | process offer |
|
||||||
|
|<-- sendAnswer -----------| |
|
||||||
|
|-- answer via Signal -------------------------------->|
|
||||||
|
|<-- sendIce --------------| |
|
||||||
|
|-- ICE via Signal ------------------------------> |
|
||||||
|
|<-- ICE via Signal -------------------------------- |
|
||||||
|
|-- receivedIce ---------->| |
|
||||||
|
| | ICE connecting... |
|
||||||
|
| | |
|
||||||
|
| (user accepts call) | |
|
||||||
|
| Java defers accept | |
|
||||||
|
| | |
|
||||||
|
|<-- stateChange:Ringing --| (tunnel ready to accept)|
|
||||||
|
|-- accept --------------->| (deferred accept sent) |
|
||||||
|
| | accept |
|
||||||
|
|<-- stateChange:Connected | |
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON-RPC client perspective
|
||||||
|
|
||||||
|
An external application (bot, UI, test script) interacts via JSON-RPC only.
|
||||||
|
|
||||||
|
**Important:** Call event notifications are not sent by default. Clients must
|
||||||
|
call `subscribeCallEvents` before initiating or receiving calls. Without this,
|
||||||
|
incoming calls are silently ignored (no tunnel is spawned).
|
||||||
|
|
||||||
|
```
|
||||||
|
JSON-RPC Client signal-cli daemon
|
||||||
|
| |
|
||||||
|
|-- subscribeCallEvents() ------------>| (required: enables call support)
|
||||||
|
| |
|
||||||
|
|-- startCall(recipient) ------------->|
|
||||||
|
|<-- {callId, state, -|
|
||||||
|
| inputDeviceName, |
|
||||||
|
| outputDeviceName} |
|
||||||
|
| |
|
||||||
|
|<-- callEvent: RINGING_OUTGOING ------|
|
||||||
|
| ... remote answers ... |
|
||||||
|
|<-- callEvent: CONNECTED -------------|
|
||||||
|
| |
|
||||||
|
| connect to audio devices |
|
||||||
|
| (via platform audio APIs) |
|
||||||
|
| |
|
||||||
|
|-- hangupCall(callId) --------------->| (or: receive callEvent ENDED)
|
||||||
|
|<-- callEvent: ENDED -----------------|
|
||||||
|
| disconnect from audio devices |
|
||||||
|
```
|
||||||
|
|
||||||
|
For incoming calls:
|
||||||
|
|
||||||
|
```
|
||||||
|
JSON-RPC Client signal-cli daemon
|
||||||
|
| |
|
||||||
|
|-- subscribeCallEvents() ------------>| (if not already subscribed)
|
||||||
|
| |
|
||||||
|
|<-- callEvent: RINGING_INCOMING ------| (includes callId, device names)
|
||||||
|
| |
|
||||||
|
|-- acceptCall(callId) --------------->|
|
||||||
|
|<-- {callId, state, -|
|
||||||
|
| inputDeviceName, |
|
||||||
|
| outputDeviceName} |
|
||||||
|
| |
|
||||||
|
|<-- callEvent: CONNECTING ------------|
|
||||||
|
|<-- callEvent: CONNECTED -------------|
|
||||||
|
| |
|
||||||
|
| connect to audio devices |
|
||||||
|
| (via platform audio APIs) |
|
||||||
|
```
|
||||||
|
|
||||||
|
To stop receiving call events, call `unsubscribeCallEvents`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Machine
|
||||||
|
|
||||||
|
Call states as seen by JSON-RPC clients:
|
||||||
|
|
||||||
|
```
|
||||||
|
startCall()
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+----- RINGING_OUTGOING ----+ RINGING_INCOMING -----+
|
||||||
|
| | | | |
|
||||||
|
| (timeout | (answered) | (rejected) | acceptCall() | (timeout
|
||||||
|
| ~60s) | | | | ~60s)
|
||||||
|
v v v v v
|
||||||
|
ENDED CONNECTED ENDED CONNECTING ENDED
|
||||||
|
| |
|
||||||
|
| v
|
||||||
|
| CONNECTED
|
||||||
|
| |
|
||||||
|
| (hangup/error) | (hangup/error)
|
||||||
|
v v
|
||||||
|
ENDED ENDED
|
||||||
|
```
|
||||||
|
|
||||||
|
For outgoing calls, `CONNECTED` fires directly when the tunnel reports
|
||||||
|
`Connected` state -- there is no intermediate `CONNECTING` event.
|
||||||
|
|
||||||
|
For incoming calls, `CONNECTING` is set by Java when the user calls
|
||||||
|
`acceptCall()`, before the tunnel completes ICE negotiation.
|
||||||
|
|
||||||
|
Both directions have a 60-second ring timeout.
|
||||||
|
|
||||||
|
Reconnection (ICE restart):
|
||||||
|
|
||||||
|
```
|
||||||
|
CONNECTED --> RECONNECTING --> CONNECTED (ICE restart succeeded)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
ENDED (ICE restart failed)
|
||||||
|
```
|
||||||
|
|
||||||
|
`RECONNECTING` maps from the tunnel's `Connecting` state, which is emitted
|
||||||
|
during ICE restarts (not during initial connection).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CallManager.java
|
||||||
|
|
||||||
|
`lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java`
|
||||||
|
|
||||||
|
Manages the call lifecycle from the Java side:
|
||||||
|
|
||||||
|
1. Spawns `signal-call-tunnel` and writes config JSON to stdin
|
||||||
|
2. Keeps stdin open as the control write channel; reads stdout for control events
|
||||||
|
3. Captures stderr for tunnel logging
|
||||||
|
4. Parses `inputDeviceName` and `outputDeviceName` from the tunnel's `ready`
|
||||||
|
message and includes them in `CallInfo`
|
||||||
|
5. Translates tunnel state changes into `CallInfo.State` values and fires
|
||||||
|
`callEvent` JSON-RPC notifications to connected clients
|
||||||
|
6. Defers the `accept` message for incoming calls until the tunnel reports
|
||||||
|
`Ringing` state (sending earlier causes the tunnel to drop it)
|
||||||
|
7. Schedules a 60-second ring timeout for both incoming and outgoing calls
|
||||||
|
8. On hangup: sends hangup message, closes stdin, and destroys the process
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Peer ID consistency
|
||||||
|
|
||||||
|
The `peerId` field in `createOutgoingCall` and `receivedOffer` must be the actual
|
||||||
|
remote peer UUID (e.g., `senderAddress.toString()`). The tunnel rejects ICE
|
||||||
|
candidates if the peer ID doesn't match across calls, causing "Ignoring
|
||||||
|
peer-reflexive ICE candidate because the ufrag is unknown."
|
||||||
|
|
||||||
|
### sendHangup semantics
|
||||||
|
|
||||||
|
`sendHangup` from the tunnel is a request to send a hangup message via Signal
|
||||||
|
protocol. It is **not** a local state change -- local state transitions come
|
||||||
|
exclusively from `stateChange` events. For single-device clients, ignore
|
||||||
|
`AcceptedOnAnotherDevice`, `DeclinedOnAnotherDevice`, and
|
||||||
|
`BusyOnAnotherDevice` hangup types in the `hangupType` field -- sending these to
|
||||||
|
the remote peer causes it to terminate the call prematurely.
|
||||||
|
|
||||||
|
### Call ID serialization
|
||||||
|
|
||||||
|
Call IDs can exceed `Long.MAX_VALUE` in Java. Use `Long.toUnsignedString()` when
|
||||||
|
serializing to JSON for the tunnel (which expects unsigned 64-bit integers). In
|
||||||
|
the config JSON, `call_id` should also use unsigned representation.
|
||||||
|
|
||||||
|
### Incoming hangup filtering
|
||||||
|
|
||||||
|
When receiving hangup messages via Signal protocol, only honor `NORMAL` type
|
||||||
|
hangups. `ACCEPTED`, `DECLINED`, and `BUSY` types are multi-device coordination
|
||||||
|
messages and should be ignored by single-device clients.
|
||||||
|
|
||||||
|
### JSON-RPC call ID types
|
||||||
|
|
||||||
|
JSON-RPC clients may send call IDs as various numeric types (Long, BigInteger,
|
||||||
|
Integer). Use `Number.longValue()` rather than direct casting when extracting
|
||||||
|
call IDs from JSON-RPC parameters.
|
||||||
|
|
||||||
|
### Identity key format
|
||||||
|
|
||||||
|
Identity keys in `senderIdentityKey` and `receiverIdentityKey` must be **raw
|
||||||
|
32-byte Curve25519 public keys** (without the 0x05 DJB type prefix). If the
|
||||||
|
33-byte serialized form is used instead, SRTP key derivation produces different
|
||||||
|
keys on each side, causing authentication failures.
|
||||||
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name":"[Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"com.sun.security.auth.module.UnixSystem",
|
|
||||||
"fields":[{"name":"gid"}, {"name":"groups"}, {"name":"uid"}, {"name":"username"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.Boolean",
|
|
||||||
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.Class",
|
|
||||||
"methods":[{"name":"getCanonicalName","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.ClassLoader",
|
|
||||||
"methods":[{"name":"getPlatformClassLoader","parameterTypes":[] }, {"name":"loadClass","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.IllegalArgumentException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.IllegalStateException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.NoSuchMethodError"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.String"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.Throwable",
|
|
||||||
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.lang.UnsatisfiedLinkError",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"java.util.UUID",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long","long"] }, {"name":"getLeastSignificantBits","parameterTypes":[] }, {"name":"getMostSignificantBits","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.asamk.signal.manager.storage.protocol.SignalProtocolStore",
|
|
||||||
"methods":[{"name":"getIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress"] }, {"name":"getIdentityKeyPair","parameterTypes":[] }, {"name":"getLocalRegistrationId","parameterTypes":[] }, {"name":"isTrustedIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.IdentityKey","org.signal.libsignal.protocol.state.IdentityKeyStore$Direction"] }, {"name":"loadKyberPreKey","parameterTypes":["int"] }, {"name":"loadPreKey","parameterTypes":["int"] }, {"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] }, {"name":"loadSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress"] }, {"name":"loadSignedPreKey","parameterTypes":["int"] }, {"name":"markKyberPreKeyUsed","parameterTypes":["int"] }, {"name":"removePreKey","parameterTypes":["int"] }, {"name":"saveIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.IdentityKey"] }, {"name":"storeSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID","org.signal.libsignal.protocol.groups.state.SenderKeyRecord"] }, {"name":"storeSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.state.SessionRecord"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.asamk.signal.manager.storage.senderKeys.SenderKeyStore",
|
|
||||||
"methods":[{"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] }, {"name":"storeSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID","org.signal.libsignal.protocol.groups.state.SenderKeyRecord"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
|
|
||||||
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.DuplicateMessageException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.IdentityKey",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["byte[]"] }, {"name":"serialize","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.IdentityKeyPair",
|
|
||||||
"methods":[{"name":"serialize","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.InvalidKeyException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.InvalidKeyIdException"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.InvalidMessageException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.NoSessionException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.SignalProtocolAddress",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["java.lang.String","int"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.UntrustedIdentityException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.fingerprint.FingerprintParsingException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.logging.Log",
|
|
||||||
"methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.message.PlaintextContent",
|
|
||||||
"fields":[{"name":"unsafeHandle"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.message.PreKeySignalMessage",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.message.SenderKeyMessage",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.message.SignalMessage",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
|
|
||||||
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.KyberPreKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.PreKeyRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.PreKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.SessionRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}],
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["byte[]"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.SessionStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.SignedPreKeyRecord",
|
|
||||||
"fields":[{"name":"unsafeHandle"}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.protocol.state.SignedPreKeyStore"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.usernames.BadNicknameCharacterException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.usernames.CannotBeEmptyException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.usernames.NicknameTooLongException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.usernames.NicknameTooShortException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.signal.libsignal.zkgroup.InvalidInputException",
|
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.BusyHandler",
|
|
||||||
"methods":[{"name":"callback","parameterTypes":["int"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.Collation",
|
|
||||||
"methods":[{"name":"xCompare","parameterTypes":["java.lang.String","java.lang.String"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.Function",
|
|
||||||
"fields":[{"name":"args"}, {"name":"context"}, {"name":"value"}],
|
|
||||||
"methods":[{"name":"xFunc","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.Function$Aggregate",
|
|
||||||
"methods":[{"name":"clone","parameterTypes":[] }, {"name":"xFinal","parameterTypes":[] }, {"name":"xStep","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.Function$Window",
|
|
||||||
"methods":[{"name":"xInverse","parameterTypes":[] }, {"name":"xValue","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.ProgressHandler",
|
|
||||||
"methods":[{"name":"progress","parameterTypes":[] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.core.DB",
|
|
||||||
"methods":[{"name":"onCommit","parameterTypes":["boolean"] }, {"name":"onUpdate","parameterTypes":["int","java.lang.String","java.lang.String","long"] }, {"name":"throwex","parameterTypes":[] }, {"name":"throwex","parameterTypes":["int"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.core.DB$ProgressObserver",
|
|
||||||
"methods":[{"name":"progress","parameterTypes":["int","int"] }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"org.sqlite.core.NativeDB",
|
|
||||||
"fields":[{"name":"busyHandler"}, {"name":"commitListener"}, {"name":"pointer"}, {"name":"progressHandler"}, {"name":"updateListener"}],
|
|
||||||
"methods":[{"name":"stringToUtf8ByteArray","parameterTypes":["java.lang.String"] }, {"name":"throwex","parameterTypes":["java.lang.String"] }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"type":"agent-extracted",
|
|
||||||
"classes":[
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"interfaces":["java.sql.Connection"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"interfaces":["org.asamk.Signal"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"interfaces":["org.asamk.Signal$Configuration"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"interfaces":["org.asamk.Signal$Device"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"interfaces":["org.asamk.Signal$Group"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"interfaces":["org.asamk.SignalControl"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"interfaces":["org.freedesktop.dbus.interfaces.DBus"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,194 +0,0 @@
|
|||||||
{
|
|
||||||
"resources":{
|
|
||||||
"includes":[{
|
|
||||||
"pattern":"\\QMETA-INF/maven/org.xerial/sqlite-jdbc/pom.properties\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/java.sql.Driver\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.transport.ITransportProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AG\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AI\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AS\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AT\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AU\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AZ\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BB\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BM\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BO\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BS\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CA\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CH\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CI\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CL\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CN\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CO\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DK\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EC\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ES\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FI\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GB\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HK\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HU\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ID\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IN\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IT\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_JP\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_LV\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MM\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MO\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MX\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MY\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NG\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NL\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PA\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PH\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PL\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RO\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RU\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SA\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SI\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SK\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TH\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TR\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UA\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UG\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_VE\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_XK\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/annotation/annotation.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/collections/collections.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/coroutines/coroutines.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/internal/internal.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/kotlin.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/ranges/ranges.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qkotlin/reflect/reflect.kotlin_builtins\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qlibsignal_jni.dylib\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qlibsignal_jni.so\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qorg/asamk/signal/manager/config/whisper.store\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qorg/sqlite/native/Linux/x86_64/libsqlitejdbc.so\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qsignal_jni.dll\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"\\Qsqlite-jdbc.properties\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"com/google/i18n/phonenumbers/data/.*"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/nfc.nrm\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/uprops.icu\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qsun/net/idn/uidna.spp\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qsun/net/www/content-types.properties\\E"
|
|
||||||
}, {
|
|
||||||
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
|
|
||||||
}]},
|
|
||||||
"bundles":[{
|
|
||||||
"name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl",
|
|
||||||
"locales":["", "en", "und"]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"types":[
|
|
||||||
],
|
|
||||||
"lambdaCapturingTypes":[
|
|
||||||
],
|
|
||||||
"proxies":[
|
|
||||||
]
|
|
||||||
}
|
|
||||||
29
gradle/libs.versions.toml
Normal file
29
gradle/libs.versions.toml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[versions]
|
||||||
|
slf4j = "2.0.18"
|
||||||
|
coroutines = "1.10.2"
|
||||||
|
junit = "6.1.2"
|
||||||
|
micronaut-json-schema = "2.1.0"
|
||||||
|
micronaut-core = "5.1.10"
|
||||||
|
signal-service = "2.15.3_unofficial_152"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85"
|
||||||
|
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.1"
|
||||||
|
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
|
||||||
|
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
|
||||||
|
zxing = "com.google.zxing:core:3.5.4"
|
||||||
|
micronaut-json-schema-annotations = { module = "io.micronaut.jsonschema:micronaut-json-schema-annotations", version.ref = "micronaut-json-schema" }
|
||||||
|
micronaut-json-schema-processor = { module = "io.micronaut.jsonschema:micronaut-json-schema-processor", version.ref = "micronaut-json-schema" }
|
||||||
|
micronaut-json-schema-generator = { module = "io.micronaut.jsonschema:micronaut-json-schema-generator", version.ref = "micronaut-json-schema" }
|
||||||
|
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-core" }
|
||||||
|
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||||
|
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
||||||
|
logback = "ch.qos.logback:logback-classic:1.6.1"
|
||||||
|
|
||||||
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
|
||||||
|
sqlite = "org.xerial:sqlite-jdbc:3.53.2.1"
|
||||||
|
hikari = "com.zaxxer:HikariCP:7.1.0"
|
||||||
|
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
|
||||||
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||||
|
junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,7 +1,9 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
31
gradlew
vendored
31
gradlew
vendored
@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -15,10 +15,12 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
# gradlew start up script for POSIX generated by Gradle.
|
||||||
#
|
#
|
||||||
# Important for running:
|
# Important for running:
|
||||||
#
|
#
|
||||||
@ -27,7 +29,7 @@
|
|||||||
# bash, then to run this script, type that shell name before the whole
|
# bash, then to run this script, type that shell name before the whole
|
||||||
# command line, like:
|
# command line, like:
|
||||||
#
|
#
|
||||||
# ksh Gradle
|
# ksh gradlew
|
||||||
#
|
#
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
@ -55,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@ -84,7 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@ -112,7 +114,6 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@ -145,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
@ -153,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@ -170,7 +171,6 @@ fi
|
|||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@ -202,16 +202,15 @@ fi
|
|||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command:
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# and any embedded shellness will be escaped.
|
||||||
# double quotes to make sure that they get re-expanded; and
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
58
gradlew.bat
vendored
58
gradlew.bat
vendored
@ -13,16 +13,18 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem gradlew startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@ -43,13 +45,13 @@ set JAVA_EXE=java.exe
|
|||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute gradlew
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||||
|
@rem which allows us to clear the local environment before executing the java command
|
||||||
|
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||||
|
|
||||||
:end
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
|
|||||||
@ -4,17 +4,23 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_25
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_25
|
||||||
|
|
||||||
|
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString()
|
val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
if (libsignalClientPath == null) {
|
if (libsignalClientPath == null) {
|
||||||
implementation(libs.signalservice)
|
implementation(libs.signalnetwork)
|
||||||
} else {
|
} else {
|
||||||
implementation(libs.signalservice) {
|
implementation(libs.signalnetwork) {
|
||||||
exclude(group = "org.signal", module = "libsignal-client")
|
exclude(group = "org.signal", module = "libsignal-client")
|
||||||
}
|
}
|
||||||
implementation(files(libsignalClientPath))
|
implementation(files(libsignalClientPath))
|
||||||
@ -24,8 +30,11 @@ dependencies {
|
|||||||
implementation(libs.slf4j.api)
|
implementation(libs.slf4j.api)
|
||||||
implementation(libs.sqlite)
|
implementation(libs.sqlite)
|
||||||
implementation(libs.hikari)
|
implementation(libs.hikari)
|
||||||
|
compileOnly(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit.jupiter)
|
||||||
|
testImplementation(platform(libs.junit.jupiter.bom))
|
||||||
|
testRuntimeOnly(libs.junit.launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<Test>("test") {
|
tasks.named<Test>("test") {
|
||||||
|
|||||||
@ -1,9 +1,16 @@
|
|||||||
package org.asamk.signal.manager;
|
package org.asamk.signal.manager;
|
||||||
|
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||||
|
import org.asamk.signal.manager.api.CallInfo;
|
||||||
|
import org.asamk.signal.manager.api.CallOffer;
|
||||||
|
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
||||||
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.Configuration;
|
import org.asamk.signal.manager.api.Configuration;
|
||||||
import org.asamk.signal.manager.api.Device;
|
import org.asamk.signal.manager.api.Device;
|
||||||
|
import org.asamk.signal.manager.api.DeviceLimitExceededException;
|
||||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||||
import org.asamk.signal.manager.api.Group;
|
import org.asamk.signal.manager.api.Group;
|
||||||
import org.asamk.signal.manager.api.GroupId;
|
import org.asamk.signal.manager.api.GroupId;
|
||||||
@ -13,32 +20,43 @@ import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
|||||||
import org.asamk.signal.manager.api.Identity;
|
import org.asamk.signal.manager.api.Identity;
|
||||||
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
||||||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||||
|
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||||
import org.asamk.signal.manager.api.InvalidStickerException;
|
import org.asamk.signal.manager.api.InvalidStickerException;
|
||||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||||
import org.asamk.signal.manager.api.LastGroupAdminException;
|
import org.asamk.signal.manager.api.LastGroupAdminException;
|
||||||
import org.asamk.signal.manager.api.Message;
|
import org.asamk.signal.manager.api.Message;
|
||||||
import org.asamk.signal.manager.api.MessageEnvelope;
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||||
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
||||||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||||
import org.asamk.signal.manager.api.Pair;
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
||||||
|
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||||
|
import org.asamk.signal.manager.api.PinLockedException;
|
||||||
|
import org.asamk.signal.manager.api.RateLimitException;
|
||||||
import org.asamk.signal.manager.api.ReceiveConfig;
|
import org.asamk.signal.manager.api.ReceiveConfig;
|
||||||
import org.asamk.signal.manager.api.Recipient;
|
import org.asamk.signal.manager.api.Recipient;
|
||||||
import org.asamk.signal.manager.api.RecipientIdentifier;
|
import org.asamk.signal.manager.api.RecipientIdentifier;
|
||||||
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
||||||
|
import org.asamk.signal.manager.api.SendMessageResult;
|
||||||
import org.asamk.signal.manager.api.SendMessageResults;
|
import org.asamk.signal.manager.api.SendMessageResults;
|
||||||
import org.asamk.signal.manager.api.StickerPack;
|
import org.asamk.signal.manager.api.StickerPack;
|
||||||
|
import org.asamk.signal.manager.api.StickerPackId;
|
||||||
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
import org.asamk.signal.manager.api.StickerPackInvalidException;
|
||||||
import org.asamk.signal.manager.api.StickerPackUrl;
|
import org.asamk.signal.manager.api.StickerPackUrl;
|
||||||
|
import org.asamk.signal.manager.api.TurnServer;
|
||||||
import org.asamk.signal.manager.api.TypingAction;
|
import org.asamk.signal.manager.api.TypingAction;
|
||||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||||
import org.asamk.signal.manager.api.UpdateGroup;
|
import org.asamk.signal.manager.api.UpdateGroup;
|
||||||
import org.asamk.signal.manager.api.UpdateProfile;
|
import org.asamk.signal.manager.api.UpdateProfile;
|
||||||
import org.asamk.signal.manager.api.UserStatus;
|
import org.asamk.signal.manager.api.UserStatus;
|
||||||
|
import org.asamk.signal.manager.api.UsernameLinkUrl;
|
||||||
|
import org.asamk.signal.manager.api.UsernameStatus;
|
||||||
|
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -54,7 +72,11 @@ import java.util.Set;
|
|||||||
public interface Manager extends Closeable {
|
public interface Manager extends Closeable {
|
||||||
|
|
||||||
static boolean isValidNumber(final String e164Number, final String countryCode) {
|
static boolean isValidNumber(final String e164Number, final String countryCode) {
|
||||||
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
|
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isValidAci(final String aci) {
|
||||||
|
return UuidUtil.INSTANCE.isUuid(aci);
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isSignalClientAvailable() {
|
static boolean isSignalClientAvailable() {
|
||||||
@ -74,6 +96,8 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
String getSelfNumber();
|
String getSelfNumber();
|
||||||
|
|
||||||
|
String getSelfACI();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is used for checking a set of phone numbers for registration on Signal
|
* This is used for checking a set of phone numbers for registration on Signal
|
||||||
*
|
*
|
||||||
@ -81,13 +105,20 @@ public interface Manager extends Closeable {
|
|||||||
* @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
|
* @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
|
||||||
* @throws IOException if it's unable to get the contacts to check if they're registered
|
* @throws IOException if it's unable to get the contacts to check if they're registered
|
||||||
*/
|
*/
|
||||||
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException;
|
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
|
||||||
|
|
||||||
void updateAccountAttributes(String deviceName) throws IOException;
|
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
|
||||||
|
|
||||||
|
void updateAccountAttributes(
|
||||||
|
String deviceName,
|
||||||
|
Boolean unrestrictedUnidentifiedSender,
|
||||||
|
final Boolean discoverableByNumber,
|
||||||
|
final Boolean numberSharing
|
||||||
|
) throws IOException;
|
||||||
|
|
||||||
Configuration getConfiguration();
|
Configuration getConfiguration();
|
||||||
|
|
||||||
void updateConfiguration(Configuration configuration) throws IOException, NotPrimaryDeviceException;
|
void updateConfiguration(Configuration configuration) throws NotPrimaryDeviceException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the user's profile.
|
* Update the user's profile.
|
||||||
@ -95,11 +126,15 @@ public interface Manager extends Closeable {
|
|||||||
*/
|
*/
|
||||||
void updateProfile(UpdateProfile updateProfile) throws IOException;
|
void updateProfile(UpdateProfile updateProfile) throws IOException;
|
||||||
|
|
||||||
|
String getUsername();
|
||||||
|
|
||||||
|
UsernameLinkUrl getUsernameLink();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a username for the account.
|
* Set a username for the account.
|
||||||
* If the username is null, it will be deleted.
|
* If the username is null, it will be deleted.
|
||||||
*/
|
*/
|
||||||
String setUsername(String username) throws IOException, InvalidUsernameException;
|
void setUsername(String username) throws IOException, InvalidUsernameException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a username for the account.
|
* Set a username for the account.
|
||||||
@ -107,34 +142,61 @@ public interface Manager extends Closeable {
|
|||||||
*/
|
*/
|
||||||
void deleteUsername() throws IOException;
|
void deleteUsername() throws IOException;
|
||||||
|
|
||||||
|
void startChangeNumber(
|
||||||
|
String newNumber,
|
||||||
|
boolean voiceVerification,
|
||||||
|
String captcha
|
||||||
|
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException;
|
||||||
|
|
||||||
|
void finishChangeNumber(
|
||||||
|
String newNumber,
|
||||||
|
String verificationCode,
|
||||||
|
String pin
|
||||||
|
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
|
||||||
|
|
||||||
void unregister() throws IOException;
|
void unregister() throws IOException;
|
||||||
|
|
||||||
void deleteAccount() throws IOException;
|
void deleteAccount() throws IOException;
|
||||||
|
|
||||||
void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException;
|
void submitRateLimitRecaptchaChallenge(
|
||||||
|
String challenge,
|
||||||
|
String captcha
|
||||||
|
) throws IOException, CaptchaRejectedException;
|
||||||
|
|
||||||
List<Device> getLinkedDevices() throws IOException;
|
List<Device> getLinkedDevices() throws IOException;
|
||||||
|
|
||||||
void removeLinkedDevices(int deviceId) throws IOException;
|
void updateLinkedDevice(int deviceId, String name) throws IOException, NotPrimaryDeviceException;
|
||||||
|
|
||||||
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException;
|
void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException;
|
||||||
|
|
||||||
|
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException;
|
||||||
|
|
||||||
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException;
|
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException;
|
||||||
|
|
||||||
List<Group> getGroups();
|
List<Group> getGroups();
|
||||||
|
|
||||||
|
List<Group> getGroups(Collection<GroupId> groupIds);
|
||||||
|
|
||||||
SendGroupMessageResults quitGroup(
|
SendGroupMessageResults quitGroup(
|
||||||
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
|
GroupId groupId,
|
||||||
|
Set<RecipientIdentifier.Single> groupAdmins
|
||||||
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
|
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
|
||||||
|
|
||||||
void deleteGroup(GroupId groupId) throws IOException;
|
void deleteGroup(GroupId groupId) throws IOException;
|
||||||
|
|
||||||
|
SendGroupMessageResults terminateGroup(
|
||||||
|
GroupId groupId
|
||||||
|
) throws IOException, GroupNotFoundException, NotAGroupMemberException;
|
||||||
|
|
||||||
Pair<GroupId, SendGroupMessageResults> createGroup(
|
Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
String name, Set<RecipientIdentifier.Single> members, String avatarFile
|
String name,
|
||||||
|
Set<RecipientIdentifier.Single> members,
|
||||||
|
String avatarFile
|
||||||
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
|
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
|
||||||
|
|
||||||
SendGroupMessageResults updateGroup(
|
SendGroupMessageResults updateGroup(
|
||||||
final GroupId groupId, final UpdateGroup updateGroup
|
final GroupId groupId,
|
||||||
|
final UpdateGroup updateGroup
|
||||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
Pair<GroupId, SendGroupMessageResults> joinGroup(
|
Pair<GroupId, SendGroupMessageResults> joinGroup(
|
||||||
@ -142,27 +204,42 @@ public interface Manager extends Closeable {
|
|||||||
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
|
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
|
||||||
|
|
||||||
SendMessageResults sendTypingMessage(
|
SendMessageResults sendTypingMessage(
|
||||||
TypingAction action, Set<RecipientIdentifier> recipients
|
TypingAction action,
|
||||||
|
Set<RecipientIdentifier> recipients
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
||||||
|
|
||||||
SendMessageResults sendReadReceipt(
|
SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
|
||||||
RecipientIdentifier.Single sender, List<Long> messageIds
|
|
||||||
) throws IOException;
|
|
||||||
|
|
||||||
SendMessageResults sendViewedReceipt(
|
SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
|
||||||
RecipientIdentifier.Single sender, List<Long> messageIds
|
|
||||||
) throws IOException;
|
|
||||||
|
|
||||||
SendMessageResults sendMessage(
|
SendMessageResults sendMessage(
|
||||||
Message message, Set<RecipientIdentifier> recipients
|
Message message,
|
||||||
|
Set<RecipientIdentifier> recipients,
|
||||||
|
boolean notifySelf
|
||||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||||
|
|
||||||
SendMessageResults sendEditMessage(
|
SendMessageResults sendEditMessage(
|
||||||
Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
|
Message message,
|
||||||
|
Set<RecipientIdentifier> recipients,
|
||||||
|
long editTargetTimestamp
|
||||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post a file attachment story to "My Story" or to a group.
|
||||||
|
*
|
||||||
|
* @param attachment path to the file to upload and post as a story
|
||||||
|
* @param allowsReplies whether other users are allowed to reply to this story
|
||||||
|
* @param groupId if present, post the story to this group instead of "My Story"
|
||||||
|
*/
|
||||||
|
SendMessageResults sendStory(
|
||||||
|
String attachment,
|
||||||
|
boolean allowsReplies,
|
||||||
|
Optional<GroupId> groupId
|
||||||
|
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException;
|
||||||
|
|
||||||
SendMessageResults sendRemoteDeleteMessage(
|
SendMessageResults sendRemoteDeleteMessage(
|
||||||
long targetSentTimestamp, Set<RecipientIdentifier> recipients
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier> recipients
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
||||||
|
|
||||||
SendMessageResults sendMessageReaction(
|
SendMessageResults sendMessageReaction(
|
||||||
@ -171,36 +248,102 @@ public interface Manager extends Closeable {
|
|||||||
RecipientIdentifier.Single targetAuthor,
|
RecipientIdentifier.Single targetAuthor,
|
||||||
long targetSentTimestamp,
|
long targetSentTimestamp,
|
||||||
Set<RecipientIdentifier> recipients,
|
Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf,
|
||||||
final boolean isStory
|
final boolean isStory
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendAdminDelete(
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier.Group> recipients,
|
||||||
|
boolean notifySelf,
|
||||||
|
boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendPinMessage(
|
||||||
|
int pinDuration,
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier> recipients,
|
||||||
|
boolean notifySelf,
|
||||||
|
boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendUnpinMessage(
|
||||||
|
RecipientIdentifier.Single targetAuthor,
|
||||||
|
long targetSentTimestamp,
|
||||||
|
Set<RecipientIdentifier> recipients,
|
||||||
|
boolean notifySelf,
|
||||||
|
boolean isStory
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
SendMessageResults sendPaymentNotificationMessage(
|
SendMessageResults sendPaymentNotificationMessage(
|
||||||
byte[] receipt, String note, RecipientIdentifier.Single recipient
|
byte[] receipt,
|
||||||
|
String note,
|
||||||
|
RecipientIdentifier.Single recipient
|
||||||
) throws IOException;
|
) throws IOException;
|
||||||
|
|
||||||
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
||||||
|
|
||||||
|
SendMessageResults sendMessageRequestResponse(
|
||||||
|
MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
||||||
|
Set<RecipientIdentifier> recipientIdentifiers
|
||||||
|
);
|
||||||
|
|
||||||
|
SendMessageResults sendPollCreateMessage(
|
||||||
|
final String question,
|
||||||
|
final boolean allowMultiple,
|
||||||
|
final List<String> options,
|
||||||
|
final Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendPollVoteMessage(
|
||||||
|
final RecipientIdentifier.Single targetAuthor,
|
||||||
|
final long targetSentTimestamp,
|
||||||
|
final List<Integer> optionIndexes,
|
||||||
|
final int voteCount,
|
||||||
|
final Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
SendMessageResults sendPollTerminateMessage(
|
||||||
|
final long targetSentTimestamp,
|
||||||
|
final Set<RecipientIdentifier> recipients,
|
||||||
|
final boolean notifySelf
|
||||||
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void hideRecipient(RecipientIdentifier.Single recipient);
|
||||||
|
|
||||||
void deleteRecipient(RecipientIdentifier.Single recipient);
|
void deleteRecipient(RecipientIdentifier.Single recipient);
|
||||||
|
|
||||||
void deleteContact(RecipientIdentifier.Single recipient);
|
void deleteContact(RecipientIdentifier.Single recipient);
|
||||||
|
|
||||||
void setContactName(
|
void setContactName(
|
||||||
RecipientIdentifier.Single recipient, String givenName, final String familyName
|
final RecipientIdentifier.Single recipient,
|
||||||
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
|
final String givenName,
|
||||||
|
final String familyName,
|
||||||
|
final String nickGivenName,
|
||||||
|
final String nickFamilyName,
|
||||||
|
final String note
|
||||||
|
) throws UnregisteredRecipientException;
|
||||||
|
|
||||||
void setContactsBlocked(
|
void setContactsBlocked(
|
||||||
Collection<RecipientIdentifier.Single> recipient, boolean blocked
|
Collection<RecipientIdentifier.Single> recipient,
|
||||||
|
boolean blocked
|
||||||
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
|
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
void setGroupsBlocked(
|
void setGroupsBlocked(
|
||||||
Collection<GroupId> groupId, boolean blocked
|
Collection<GroupId> groupId,
|
||||||
|
boolean blocked
|
||||||
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
|
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the expiration timer for a contact
|
* Change the expiration timer for a contact
|
||||||
*/
|
*/
|
||||||
void setExpirationTimer(
|
void setExpirationTimer(
|
||||||
RecipientIdentifier.Single recipient, int messageExpirationTimer
|
RecipientIdentifier.Single recipient,
|
||||||
|
int messageExpirationTimer
|
||||||
) throws IOException, UnregisteredRecipientException;
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -239,9 +382,13 @@ public interface Manager extends Closeable {
|
|||||||
* Receive new messages from server, returns if no new message arrive in a timespan of timeout.
|
* Receive new messages from server, returns if no new message arrive in a timespan of timeout.
|
||||||
*/
|
*/
|
||||||
void receiveMessages(
|
void receiveMessages(
|
||||||
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
|
Optional<Duration> timeout,
|
||||||
|
Optional<Integer> maxMessages,
|
||||||
|
ReceiveMessageHandler handler
|
||||||
) throws IOException, AlreadyReceivingException;
|
) throws IOException, AlreadyReceivingException;
|
||||||
|
|
||||||
|
void stopReceiveMessages();
|
||||||
|
|
||||||
void setReceiveConfig(ReceiveConfig receiveConfig);
|
void setReceiveConfig(ReceiveConfig receiveConfig);
|
||||||
|
|
||||||
boolean isContactBlocked(RecipientIdentifier.Single recipient);
|
boolean isContactBlocked(RecipientIdentifier.Single recipient);
|
||||||
@ -269,7 +416,8 @@ public interface Manager extends Closeable {
|
|||||||
* @param recipient account of the identity
|
* @param recipient account of the identity
|
||||||
*/
|
*/
|
||||||
boolean trustIdentityVerified(
|
boolean trustIdentityVerified(
|
||||||
RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode
|
RecipientIdentifier.Single recipient,
|
||||||
|
IdentityVerificationCode verificationCode
|
||||||
) throws UnregisteredRecipientException;
|
) throws UnregisteredRecipientException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -285,8 +433,59 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
InputStream retrieveAttachment(final String id) throws IOException;
|
InputStream retrieveAttachment(final String id) throws IOException;
|
||||||
|
|
||||||
|
InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException;
|
||||||
|
|
||||||
|
InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException;
|
||||||
|
|
||||||
|
// --- Voice call methods ---
|
||||||
|
|
||||||
|
CallInfo startCall(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
CallInfo acceptCall(long callId) throws IOException;
|
||||||
|
|
||||||
|
void hangupCall(long callId) throws IOException;
|
||||||
|
|
||||||
|
SendMessageResult rejectCall(long callId) throws IOException;
|
||||||
|
|
||||||
|
List<CallInfo> listActiveCalls();
|
||||||
|
|
||||||
|
void sendCallOffer(
|
||||||
|
RecipientIdentifier.Single recipient,
|
||||||
|
CallOffer offer
|
||||||
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void sendCallAnswer(
|
||||||
|
RecipientIdentifier.Single recipient,
|
||||||
|
long callId,
|
||||||
|
byte[] answerOpaque
|
||||||
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void sendIceUpdate(
|
||||||
|
RecipientIdentifier.Single recipient,
|
||||||
|
long callId,
|
||||||
|
List<byte[]> iceCandidates
|
||||||
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void sendHangup(
|
||||||
|
RecipientIdentifier.Single recipient,
|
||||||
|
long callId,
|
||||||
|
MessageEnvelope.Call.Hangup.Type type
|
||||||
|
) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
void sendBusy(RecipientIdentifier.Single recipient, long callId) throws IOException, UnregisteredRecipientException;
|
||||||
|
|
||||||
|
List<TurnServer> getTurnServerInfo() throws IOException;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void close() throws IOException;
|
void close();
|
||||||
|
|
||||||
|
void addCallEventListener(CallEventListener listener);
|
||||||
|
|
||||||
|
void removeCallEventListener(CallEventListener listener);
|
||||||
|
|
||||||
interface ReceiveMessageHandler {
|
interface ReceiveMessageHandler {
|
||||||
|
|
||||||
@ -295,4 +494,9 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
void handleMessage(MessageEnvelope envelope, Throwable e);
|
void handleMessage(MessageEnvelope envelope, Throwable e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CallEventListener {
|
||||||
|
|
||||||
|
void handleCallEvent(CallInfo callInfo, String reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
package org.asamk.signal.manager;
|
package org.asamk.signal.manager;
|
||||||
|
|
||||||
import org.asamk.signal.manager.internal.LibSignalLogger;
|
import org.asamk.signal.manager.internal.LibSignalLogger;
|
||||||
|
import org.asamk.signal.manager.internal.SignalLogger;
|
||||||
|
|
||||||
public class ManagerLogger {
|
public class ManagerLogger {
|
||||||
|
|
||||||
public static void initLogger() {
|
public static void initLogger() {
|
||||||
LibSignalLogger.initLogger();
|
LibSignalLogger.initLogger();
|
||||||
|
SignalLogger.initLogger();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,10 @@ package org.asamk.signal.manager;
|
|||||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||||
|
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||||
import org.asamk.signal.manager.api.PinLockedException;
|
import org.asamk.signal.manager.api.PinLockedException;
|
||||||
import org.asamk.signal.manager.api.RateLimitException;
|
import org.asamk.signal.manager.api.RateLimitException;
|
||||||
|
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -12,12 +14,15 @@ import java.io.IOException;
|
|||||||
public interface RegistrationManager extends Closeable {
|
public interface RegistrationManager extends Closeable {
|
||||||
|
|
||||||
void register(
|
void register(
|
||||||
boolean voiceVerification, String captcha
|
boolean voiceVerification,
|
||||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException;
|
String captcha,
|
||||||
|
final boolean forceRegister
|
||||||
|
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
|
||||||
|
|
||||||
void verifyAccount(
|
void verifyAccount(
|
||||||
String verificationCode, String pin
|
String verificationCode,
|
||||||
) throws IOException, PinLockedException, IncorrectPinException;
|
String pin
|
||||||
|
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
|
||||||
|
|
||||||
void deleteLocalAccountData() throws IOException;
|
void deleteLocalAccountData() throws IOException;
|
||||||
|
|
||||||
|
|||||||
@ -4,5 +4,5 @@ import org.asamk.signal.manager.api.TrustNewIdentity;
|
|||||||
|
|
||||||
public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) {
|
public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) {
|
||||||
|
|
||||||
public static Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false);
|
public static final Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package org.asamk.signal.manager;
|
|||||||
|
|
||||||
import org.asamk.signal.manager.api.AccountCheckException;
|
import org.asamk.signal.manager.api.AccountCheckException;
|
||||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
import org.asamk.signal.manager.api.NotRegisteredException;
|
||||||
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||||
import org.asamk.signal.manager.config.ServiceConfig;
|
import org.asamk.signal.manager.config.ServiceConfig;
|
||||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||||
@ -14,7 +15,7 @@ import org.asamk.signal.manager.internal.RegistrationManagerImpl;
|
|||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
import org.asamk.signal.manager.storage.accounts.AccountsStore;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.signal.libsignal.protocol.util.KeyHelper;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
@ -65,29 +66,56 @@ public class SignalAccountFiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||||
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||||
try {
|
try {
|
||||||
return initManager(a.number(), a.path());
|
return new Pair<Manager, Throwable>(initManagerByNumber(a.number(), a.path()), null);
|
||||||
} catch (NotRegisteredException | IOException | AccountCheckException e) {
|
} catch (NotRegisteredException e) {
|
||||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||||
return null;
|
return null;
|
||||||
} catch (Throwable e) {
|
} catch (AccountCheckException | IOException e) {
|
||||||
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||||
throw e;
|
return new Pair<Manager, Throwable>(null, e);
|
||||||
}
|
}
|
||||||
}).filter(Objects::nonNull).toList();
|
}).filter(Objects::nonNull).toList();
|
||||||
|
|
||||||
|
for (final var pair : managerPairs) {
|
||||||
|
if (pair.second() instanceof IOException e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var managers = managerPairs.stream()
|
||||||
|
.filter(p -> p != null && p.first() != null)
|
||||||
|
.map(Pair::first)
|
||||||
|
.toList();
|
||||||
return new MultiAccountManagerImpl(managers, this);
|
return new MultiAccountManagerImpl(managers, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
final var accountPath = accountsStore.getPathByNumber(number);
|
final var accountPath = accountsStore.getPathByNumber(number);
|
||||||
return this.initManager(number, accountPath);
|
return this.initManagerByNumber(number, accountPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Manager initManager(
|
public Manager initManagerByAci(String aciStr) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
String number, String accountPath
|
final var aci = ACI.parseOrThrow(aciStr);
|
||||||
|
final var accountPath = accountsStore.getPathByAci(aci);
|
||||||
|
return this.initManagerByAci(aci, accountPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manager initManagerByNumber(
|
||||||
|
String number,
|
||||||
|
String accountPath
|
||||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
|
final var account = loadAccount(accountPath);
|
||||||
|
if (!number.equals(account.getNumber())) {
|
||||||
|
account.close();
|
||||||
|
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
return initManagerFromAccount(number, accountPath, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalAccount loadAccount(final String accountPath) throws NotRegisteredException, IOException {
|
||||||
if (accountPath == null) {
|
if (accountPath == null) {
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
}
|
}
|
||||||
@ -95,12 +123,27 @@ public class SignalAccountFiles {
|
|||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
|
||||||
if (!number.equals(account.getNumber())) {
|
|
||||||
account.close();
|
|
||||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Manager initManagerByAci(
|
||||||
|
ACI aci,
|
||||||
|
String accountPath
|
||||||
|
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||||
|
final var account = loadAccount(accountPath);
|
||||||
|
if (!aci.equals(account.getAci())) {
|
||||||
|
account.close();
|
||||||
|
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci());
|
||||||
|
}
|
||||||
|
|
||||||
|
return initManagerFromAccount(aci.toString(), accountPath, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ManagerImpl initManagerFromAccount(
|
||||||
|
final String identifier,
|
||||||
|
final String accountPath,
|
||||||
|
final SignalAccount account
|
||||||
|
) throws NotRegisteredException, IOException, AccountCheckException {
|
||||||
if (!account.isRegistered()) {
|
if (!account.isRegistered()) {
|
||||||
account.close();
|
account.close();
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
@ -122,10 +165,10 @@ public class SignalAccountFiles {
|
|||||||
manager.checkAccountState();
|
manager.checkAccountState();
|
||||||
} catch (DeprecatedVersionException e) {
|
} catch (DeprecatedVersionException e) {
|
||||||
manager.close();
|
manager.close();
|
||||||
throw new AccountCheckException("signal-cli version is too old for the Signal-Server, please update.");
|
throw new IOException("signal-cli version is too old for the Signal-Server, please update.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
manager.close();
|
manager.close();
|
||||||
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
|
throw new AccountCheckException("Error while checking account " + identifier + ": " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.getServiceEnvironment() == null) {
|
if (account.getServiceEnvironment() == null) {
|
||||||
@ -153,15 +196,14 @@ public class SignalAccountFiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RegistrationManager initRegistrationManager(
|
public RegistrationManager initRegistrationManager(
|
||||||
String number, Consumer<Manager> newManagerListener
|
String number,
|
||||||
|
Consumer<Manager> newManagerListener
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var accountPath = accountsStore.getPathByNumber(number);
|
final var accountPath = accountsStore.getPathByNumber(number);
|
||||||
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
||||||
final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
|
final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
|
||||||
var aciIdentityKey = KeyUtils.generateIdentityKeyPair();
|
var aciIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||||
var pniIdentityKey = KeyUtils.generateIdentityKeyPair();
|
var pniIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||||
var registrationId = KeyHelper.generateRegistrationId(false);
|
|
||||||
var pniRegistrationId = KeyHelper.generateRegistrationId(false);
|
|
||||||
|
|
||||||
var profileKey = KeyUtils.createProfileKey();
|
var profileKey = KeyUtils.createProfileKey();
|
||||||
var account = SignalAccount.create(pathConfig.dataPath(),
|
var account = SignalAccount.create(pathConfig.dataPath(),
|
||||||
@ -170,10 +212,9 @@ public class SignalAccountFiles {
|
|||||||
serviceEnvironment,
|
serviceEnvironment,
|
||||||
aciIdentityKey,
|
aciIdentityKey,
|
||||||
pniIdentityKey,
|
pniIdentityKey,
|
||||||
registrationId,
|
|
||||||
pniRegistrationId,
|
|
||||||
profileKey,
|
profileKey,
|
||||||
settings);
|
settings);
|
||||||
|
account.initDatabase();
|
||||||
|
|
||||||
return new RegistrationManagerImpl(account,
|
return new RegistrationManagerImpl(account,
|
||||||
pathConfig,
|
pathConfig,
|
||||||
@ -188,6 +229,7 @@ public class SignalAccountFiles {
|
|||||||
account.close();
|
account.close();
|
||||||
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
|
||||||
}
|
}
|
||||||
|
account.initDatabase();
|
||||||
|
|
||||||
return new RegistrationManagerImpl(account,
|
return new RegistrationManagerImpl(account,
|
||||||
pathConfig,
|
pathConfig,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package org.asamk.signal.manager.actions;
|
|||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
import org.asamk.signal.manager.helper.Context;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.signal.core.models.ServiceId;
|
||||||
|
|
||||||
public class RenewSessionAction implements HandleAction {
|
public class RenewSessionAction implements HandleAction {
|
||||||
|
|
||||||
@ -19,10 +19,8 @@ public class RenewSessionAction implements HandleAction {
|
|||||||
@Override
|
@Override
|
||||||
public void execute(Context context) throws Throwable {
|
public void execute(Context context) throws Throwable {
|
||||||
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
|
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
|
||||||
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) {
|
|
||||||
context.getSendHelper().sendNullMessage(recipientId);
|
context.getSendHelper().sendNullMessage(recipientId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object o) {
|
public boolean equals(final Object o) {
|
||||||
|
|||||||
@ -13,7 +13,9 @@ public class ResendMessageAction implements HandleAction {
|
|||||||
private final MessageSendLogEntry messageSendLogEntry;
|
private final MessageSendLogEntry messageSendLogEntry;
|
||||||
|
|
||||||
public ResendMessageAction(
|
public ResendMessageAction(
|
||||||
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
|
final RecipientId recipientId,
|
||||||
|
final long timestamp,
|
||||||
|
final MessageSendLogEntry messageSendLogEntry
|
||||||
) {
|
) {
|
||||||
this.recipientId = recipientId;
|
this.recipientId = recipientId;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package org.asamk.signal.manager.actions;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.helper.Context;
|
||||||
|
|
||||||
|
public class RetrieveDeviceNameAction implements HandleAction {
|
||||||
|
|
||||||
|
private static final RetrieveDeviceNameAction INSTANCE = new RetrieveDeviceNameAction();
|
||||||
|
|
||||||
|
public static RetrieveDeviceNameAction create() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RetrieveDeviceNameAction() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Context context) throws Throwable {
|
||||||
|
context.getAccountHelper().refreshDeviceName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package org.asamk.signal.manager.actions;
|
|
||||||
|
|
||||||
import org.asamk.signal.manager.helper.Context;
|
|
||||||
|
|
||||||
public class RetrieveStorageDataAction implements HandleAction {
|
|
||||||
|
|
||||||
private static final RetrieveStorageDataAction INSTANCE = new RetrieveStorageDataAction();
|
|
||||||
|
|
||||||
private RetrieveStorageDataAction() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RetrieveStorageDataAction create() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Context context) throws Throwable {
|
|
||||||
context.getStorageHelper().readDataFromStorage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,7 +15,9 @@ public class SendReceiptAction implements HandleAction {
|
|||||||
private final List<Long> timestamps = new ArrayList<>();
|
private final List<Long> timestamps = new ArrayList<>();
|
||||||
|
|
||||||
public SendReceiptAction(
|
public SendReceiptAction(
|
||||||
final RecipientId recipientId, final SignalServiceReceiptMessage.Type type, final long timestamp
|
final RecipientId recipientId,
|
||||||
|
final SignalServiceReceiptMessage.Type type,
|
||||||
|
final long timestamp
|
||||||
) {
|
) {
|
||||||
this.recipientId = recipientId;
|
this.recipientId = recipientId;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import org.signal.libsignal.metadata.ProtocolException;
|
|||||||
import org.signal.libsignal.protocol.message.CiphertextMessage;
|
import org.signal.libsignal.protocol.message.CiphertextMessage;
|
||||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
|
||||||
import org.whispersystems.signalservice.internal.push.Envelope;
|
import org.whispersystems.signalservice.internal.push.Envelope;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -15,29 +14,21 @@ import java.util.Optional;
|
|||||||
public class SendRetryMessageRequestAction implements HandleAction {
|
public class SendRetryMessageRequestAction implements HandleAction {
|
||||||
|
|
||||||
private final RecipientId recipientId;
|
private final RecipientId recipientId;
|
||||||
private final ServiceId serviceId;
|
|
||||||
private final ProtocolException protocolException;
|
private final ProtocolException protocolException;
|
||||||
private final SignalServiceEnvelope envelope;
|
private final SignalServiceEnvelope envelope;
|
||||||
private final ServiceId accountId;
|
|
||||||
|
|
||||||
public SendRetryMessageRequestAction(
|
public SendRetryMessageRequestAction(
|
||||||
final RecipientId recipientId,
|
final RecipientId recipientId,
|
||||||
final ServiceId serviceId,
|
|
||||||
final ProtocolException protocolException,
|
final ProtocolException protocolException,
|
||||||
final SignalServiceEnvelope envelope,
|
final SignalServiceEnvelope envelope
|
||||||
final ServiceId accountId
|
|
||||||
) {
|
) {
|
||||||
this.recipientId = recipientId;
|
this.recipientId = recipientId;
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.protocolException = protocolException;
|
this.protocolException = protocolException;
|
||||||
this.envelope = envelope;
|
this.envelope = envelope;
|
||||||
this.accountId = accountId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(Context context) throws Throwable {
|
public void execute(Context context) throws Throwable {
|
||||||
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
|
|
||||||
|
|
||||||
int senderDevice = protocolException.getSenderDevice();
|
int senderDevice = protocolException.getSenderDevice();
|
||||||
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
|
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
|
||||||
protocolException.getGroupId().get())) : Optional.empty();
|
protocolException.getGroupId().get())) : Optional.empty();
|
||||||
@ -50,7 +41,9 @@ public class SendRetryMessageRequestAction implements HandleAction {
|
|||||||
envelopeType = messageContent.getType();
|
envelopeType = messageContent.getType();
|
||||||
} else {
|
} else {
|
||||||
originalContent = envelope.getContent();
|
originalContent = envelope.getContent();
|
||||||
envelopeType = envelopeTypeToCiphertextMessageType(envelope.getType());
|
envelopeType = envelope.getType() == null
|
||||||
|
? CiphertextMessage.WHISPER_TYPE
|
||||||
|
: envelopeTypeToCiphertextMessageType(envelope.getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent,
|
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent,
|
||||||
@ -67,7 +60,7 @@ public class SendRetryMessageRequestAction implements HandleAction {
|
|||||||
return CiphertextMessage.WHISPER_TYPE;
|
return CiphertextMessage.WHISPER_TYPE;
|
||||||
}
|
}
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
case PREKEY_BUNDLE -> CiphertextMessage.PREKEY_TYPE;
|
case PREKEY_MESSAGE -> CiphertextMessage.PREKEY_TYPE;
|
||||||
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
|
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
|
||||||
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
|
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
|
||||||
default -> CiphertextMessage.WHISPER_TYPE;
|
default -> CiphertextMessage.WHISPER_TYPE;
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.asamk.signal.manager.actions;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.helper.Context;
|
||||||
|
import org.asamk.signal.manager.jobs.SyncStorageJob;
|
||||||
|
|
||||||
|
public class SyncStorageDataAction implements HandleAction {
|
||||||
|
|
||||||
|
private static final SyncStorageDataAction INSTANCE = new SyncStorageDataAction();
|
||||||
|
|
||||||
|
private SyncStorageDataAction() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SyncStorageDataAction create() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Context context) throws Throwable {
|
||||||
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/src/main/java/org/asamk/signal/manager/api/CallInfo.java
Normal file
21
lib/src/main/java/org/asamk/signal/manager/api/CallInfo.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
public record CallInfo(
|
||||||
|
long callId,
|
||||||
|
State state,
|
||||||
|
RecipientAddress recipient,
|
||||||
|
String inputDeviceName,
|
||||||
|
String outputDeviceName,
|
||||||
|
boolean isOutgoing
|
||||||
|
) {
|
||||||
|
|
||||||
|
public enum State {
|
||||||
|
IDLE,
|
||||||
|
RINGING_INCOMING,
|
||||||
|
RINGING_OUTGOING,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED,
|
||||||
|
RECONNECTING,
|
||||||
|
ENDED
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
public record CallOffer(
|
||||||
|
long callId, Type type, byte[] opaque
|
||||||
|
) {
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
AUDIO,
|
||||||
|
VIDEO
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
public class CaptchaRejectedException extends Exception {
|
||||||
|
|
||||||
|
public CaptchaRejectedException() {
|
||||||
|
super("Captcha rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
public CaptchaRejectedException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CaptchaRejectedException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,11 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
public class CaptchaRequiredException extends Exception {
|
public class CaptchaRequiredException extends Exception {
|
||||||
|
|
||||||
private long nextAttemptTimestamp;
|
private long nextVerificationAttemptMilliseconds;
|
||||||
|
|
||||||
public CaptchaRequiredException(final long nextAttemptTimestamp) {
|
public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) {
|
||||||
super("Captcha required");
|
super("Captcha required");
|
||||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CaptchaRequiredException(final String message) {
|
public CaptchaRequiredException(final String message) {
|
||||||
@ -17,7 +17,7 @@ public class CaptchaRequiredException extends Exception {
|
|||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getNextAttemptTimestamp() {
|
public long getNextVerificationAttemptMilliseconds() {
|
||||||
return nextAttemptTimestamp;
|
return nextVerificationAttemptMilliseconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,50 +2,44 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
import org.whispersystems.signalservice.internal.util.Util;
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
|
||||||
import java.util.Objects;
|
public record Contact(
|
||||||
|
String givenName,
|
||||||
public class Contact {
|
String familyName,
|
||||||
|
String nickName,
|
||||||
private final String givenName;
|
String nickNameGivenName,
|
||||||
|
String nickNameFamilyName,
|
||||||
private final String familyName;
|
String note,
|
||||||
|
String color,
|
||||||
private final String color;
|
int messageExpirationTime,
|
||||||
|
int messageExpirationTimeVersion,
|
||||||
private final int messageExpirationTime;
|
long muteUntil,
|
||||||
|
boolean hideStory,
|
||||||
private final boolean blocked;
|
boolean isBlocked,
|
||||||
|
long blockedAt,
|
||||||
private final boolean archived;
|
boolean isArchived,
|
||||||
|
boolean isProfileSharingEnabled,
|
||||||
private final boolean profileSharingEnabled;
|
boolean isHidden,
|
||||||
|
Long unregisteredTimestamp
|
||||||
public Contact(
|
|
||||||
final String givenName,
|
|
||||||
final String familyName,
|
|
||||||
final String color,
|
|
||||||
final int messageExpirationTime,
|
|
||||||
final boolean blocked,
|
|
||||||
final boolean archived,
|
|
||||||
final boolean profileSharingEnabled
|
|
||||||
) {
|
) {
|
||||||
this.givenName = givenName;
|
|
||||||
this.familyName = familyName;
|
|
||||||
this.color = color;
|
|
||||||
this.messageExpirationTime = messageExpirationTime;
|
|
||||||
this.blocked = blocked;
|
|
||||||
this.archived = archived;
|
|
||||||
this.profileSharingEnabled = profileSharingEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Contact(final Builder builder) {
|
private Contact(final Builder builder) {
|
||||||
givenName = builder.givenName;
|
this(builder.givenName,
|
||||||
familyName = builder.familyName;
|
builder.familyName,
|
||||||
color = builder.color;
|
builder.nickName,
|
||||||
messageExpirationTime = builder.messageExpirationTime;
|
builder.nickNameGivenName,
|
||||||
blocked = builder.blocked;
|
builder.nickNameFamilyName,
|
||||||
archived = builder.archived;
|
builder.note,
|
||||||
profileSharingEnabled = builder.profileSharingEnabled;
|
builder.color,
|
||||||
|
builder.messageExpirationTime,
|
||||||
|
builder.messageExpirationTimeVersion,
|
||||||
|
builder.muteUntil,
|
||||||
|
builder.hideStory,
|
||||||
|
builder.isBlocked,
|
||||||
|
builder.blockedAt,
|
||||||
|
builder.isArchived,
|
||||||
|
builder.isProfileSharingEnabled,
|
||||||
|
builder.isHidden,
|
||||||
|
builder.unregisteredTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder newBuilder() {
|
public static Builder newBuilder() {
|
||||||
@ -54,13 +48,23 @@ public class Contact {
|
|||||||
|
|
||||||
public static Builder newBuilder(final Contact copy) {
|
public static Builder newBuilder(final Contact copy) {
|
||||||
Builder builder = new Builder();
|
Builder builder = new Builder();
|
||||||
builder.givenName = copy.getGivenName();
|
builder.givenName = copy.givenName();
|
||||||
builder.familyName = copy.getFamilyName();
|
builder.familyName = copy.familyName();
|
||||||
builder.color = copy.getColor();
|
builder.nickName = copy.nickName();
|
||||||
builder.messageExpirationTime = copy.getMessageExpirationTime();
|
builder.nickNameGivenName = copy.nickNameGivenName();
|
||||||
builder.blocked = copy.isBlocked();
|
builder.nickNameFamilyName = copy.nickNameFamilyName();
|
||||||
builder.archived = copy.isArchived();
|
builder.note = copy.note();
|
||||||
builder.profileSharingEnabled = copy.isProfileSharingEnabled();
|
builder.color = copy.color();
|
||||||
|
builder.messageExpirationTime = copy.messageExpirationTime();
|
||||||
|
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
|
||||||
|
builder.muteUntil = copy.muteUntil();
|
||||||
|
builder.hideStory = copy.hideStory();
|
||||||
|
builder.isBlocked = copy.isBlocked();
|
||||||
|
builder.blockedAt = copy.blockedAt();
|
||||||
|
builder.isArchived = copy.isArchived();
|
||||||
|
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
||||||
|
builder.isHidden = copy.isHidden();
|
||||||
|
builder.unregisteredTimestamp = copy.unregisteredTimestamp();
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,72 +83,48 @@ public class Contact {
|
|||||||
return givenName + " " + familyName;
|
return givenName + " " + familyName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getGivenName() {
|
public String getDisplayNickname() {
|
||||||
return givenName;
|
final var noNickGivenName = Util.isEmpty(nickNameGivenName);
|
||||||
|
final var noNickFamilyName = Util.isEmpty(nickNameFamilyName);
|
||||||
|
|
||||||
|
if (noNickGivenName && noNickFamilyName) {
|
||||||
|
return null;
|
||||||
|
} else if (noNickGivenName) {
|
||||||
|
return nickNameFamilyName;
|
||||||
|
} else if (noNickFamilyName) {
|
||||||
|
return nickNameGivenName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFamilyName() {
|
return nickNameGivenName + " " + nickNameFamilyName;
|
||||||
return familyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getColor() {
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMessageExpirationTime() {
|
|
||||||
return messageExpirationTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isBlocked() {
|
|
||||||
return blocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isArchived() {
|
|
||||||
return archived;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isProfileSharingEnabled() {
|
|
||||||
return profileSharingEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(final Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
final Contact contact = (Contact) o;
|
|
||||||
return messageExpirationTime == contact.messageExpirationTime
|
|
||||||
&& blocked == contact.blocked
|
|
||||||
&& archived == contact.archived
|
|
||||||
&& profileSharingEnabled == contact.profileSharingEnabled
|
|
||||||
&& Objects.equals(givenName, contact.givenName)
|
|
||||||
&& Objects.equals(familyName, contact.familyName)
|
|
||||||
&& Objects.equals(color, contact.color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(givenName,
|
|
||||||
familyName,
|
|
||||||
color,
|
|
||||||
messageExpirationTime,
|
|
||||||
blocked,
|
|
||||||
archived,
|
|
||||||
profileSharingEnabled);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
|
|
||||||
private String givenName;
|
private String givenName;
|
||||||
private String familyName;
|
private String familyName;
|
||||||
|
private String nickName;
|
||||||
|
private String nickNameGivenName;
|
||||||
|
private String nickNameFamilyName;
|
||||||
|
private String note;
|
||||||
private String color;
|
private String color;
|
||||||
private int messageExpirationTime;
|
private int messageExpirationTime;
|
||||||
private boolean blocked;
|
private int messageExpirationTimeVersion = 1;
|
||||||
private boolean archived;
|
private long muteUntil;
|
||||||
private boolean profileSharingEnabled;
|
private boolean hideStory;
|
||||||
|
private boolean isBlocked;
|
||||||
|
private long blockedAt;
|
||||||
|
private boolean isArchived;
|
||||||
|
private boolean isProfileSharingEnabled;
|
||||||
|
private boolean isHidden;
|
||||||
|
private Long unregisteredTimestamp;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Builder newBuilder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
public Builder withGivenName(final String val) {
|
public Builder withGivenName(final String val) {
|
||||||
givenName = val;
|
givenName = val;
|
||||||
return this;
|
return this;
|
||||||
@ -155,6 +135,26 @@ public class Contact {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withNickName(final String val) {
|
||||||
|
nickName = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withNickNameGivenName(final String val) {
|
||||||
|
nickNameGivenName = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withNickNameFamilyName(final String val) {
|
||||||
|
nickNameFamilyName = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withNote(final String val) {
|
||||||
|
note = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder withColor(final String val) {
|
public Builder withColor(final String val) {
|
||||||
color = val;
|
color = val;
|
||||||
return this;
|
return this;
|
||||||
@ -165,18 +165,53 @@ public class Contact {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withBlocked(final boolean val) {
|
public Builder withMessageExpirationTimeVersion(final int val) {
|
||||||
blocked = val;
|
messageExpirationTimeVersion = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withArchived(final boolean val) {
|
public Builder withMuteUntil(final long val) {
|
||||||
archived = val;
|
muteUntil = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withProfileSharingEnabled(final boolean val) {
|
public Builder withHideStory(final boolean val) {
|
||||||
profileSharingEnabled = val;
|
hideStory = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withIsBlocked(final boolean val) {
|
||||||
|
if (val && !isBlocked) {
|
||||||
|
blockedAt = System.currentTimeMillis();
|
||||||
|
} else if (!val) {
|
||||||
|
blockedAt = 0;
|
||||||
|
}
|
||||||
|
isBlocked = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withBlockedAt(final long val) {
|
||||||
|
blockedAt = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withIsArchived(final boolean val) {
|
||||||
|
isArchived = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withIsProfileSharingEnabled(final boolean val) {
|
||||||
|
isProfileSharingEnabled = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withIsHidden(final boolean val) {
|
||||||
|
isHidden = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withUnregisteredTimestamp(final Long val) {
|
||||||
|
unregisteredTimestamp = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
public class DeviceLimitExceededException extends Exception {
|
||||||
|
|
||||||
|
public DeviceLimitExceededException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceLimitExceededException(final String message, final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
|
|||||||
}
|
}
|
||||||
ECPublicKey deviceKey;
|
ECPublicKey deviceKey;
|
||||||
try {
|
try {
|
||||||
deviceKey = Curve.decodePoint(publicKeyBytes, 0);
|
deviceKey = new ECPublicKey(publicKeyBytes);
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
throw new InvalidDeviceLinkException("Invalid device link", e);
|
throw new InvalidDeviceLinkException("Invalid device link", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,9 @@ public record Group(
|
|||||||
String title,
|
String title,
|
||||||
String description,
|
String description,
|
||||||
GroupInviteLinkUrl groupInviteLinkUrl,
|
GroupInviteLinkUrl groupInviteLinkUrl,
|
||||||
Set<RecipientAddress> members,
|
Set<GroupMember> members,
|
||||||
Set<RecipientAddress> pendingMembers,
|
Set<RecipientAddress> pendingMembers,
|
||||||
Set<RecipientAddress> requestingMembers,
|
Set<RecipientAddress> requestingMembers,
|
||||||
Set<RecipientAddress> adminMembers,
|
|
||||||
Set<RecipientAddress> bannedMembers,
|
Set<RecipientAddress> bannedMembers,
|
||||||
boolean isBlocked,
|
boolean isBlocked,
|
||||||
int messageExpirationTimer,
|
int messageExpirationTimer,
|
||||||
@ -23,11 +22,14 @@ public record Group(
|
|||||||
GroupPermission permissionEditDetails,
|
GroupPermission permissionEditDetails,
|
||||||
GroupPermission permissionSendMessage,
|
GroupPermission permissionSendMessage,
|
||||||
boolean isMember,
|
boolean isMember,
|
||||||
boolean isAdmin
|
boolean isAdmin,
|
||||||
|
boolean isTerminated
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static Group from(
|
public static Group from(
|
||||||
final GroupInfo groupInfo, final RecipientAddressResolver recipientStore, final RecipientId selfRecipientId
|
final GroupInfo groupInfo,
|
||||||
|
final RecipientAddressResolver recipientStore,
|
||||||
|
final RecipientId selfRecipientId
|
||||||
) {
|
) {
|
||||||
return new Group(groupInfo.getGroupId(),
|
return new Group(groupInfo.getGroupId(),
|
||||||
groupInfo.getTitle(),
|
groupInfo.getTitle(),
|
||||||
@ -35,8 +37,7 @@ public record Group(
|
|||||||
groupInfo.getGroupInviteLink(),
|
groupInfo.getGroupInviteLink(),
|
||||||
groupInfo.getMembers()
|
groupInfo.getMembers()
|
||||||
.stream()
|
.stream()
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
.map(m -> org.asamk.signal.manager.api.GroupMember.from(m, recipientStore))
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
|
||||||
.collect(Collectors.toSet()),
|
.collect(Collectors.toSet()),
|
||||||
groupInfo.getPendingMembers()
|
groupInfo.getPendingMembers()
|
||||||
.stream()
|
.stream()
|
||||||
@ -48,11 +49,6 @@ public record Group(
|
|||||||
.map(recipientStore::resolveRecipientAddress)
|
.map(recipientStore::resolveRecipientAddress)
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
||||||
.collect(Collectors.toSet()),
|
.collect(Collectors.toSet()),
|
||||||
groupInfo.getAdminMembers()
|
|
||||||
.stream()
|
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
|
||||||
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
|
|
||||||
.collect(Collectors.toSet()),
|
|
||||||
groupInfo.getBannedMembers()
|
groupInfo.getBannedMembers()
|
||||||
.stream()
|
.stream()
|
||||||
.map(recipientStore::resolveRecipientAddress)
|
.map(recipientStore::resolveRecipientAddress)
|
||||||
@ -64,6 +60,7 @@ public record Group(
|
|||||||
groupInfo.getPermissionEditDetails(),
|
groupInfo.getPermissionEditDetails(),
|
||||||
groupInfo.getPermissionSendMessage(),
|
groupInfo.getPermissionSendMessage(),
|
||||||
groupInfo.isMember(selfRecipientId),
|
groupInfo.isMember(selfRecipientId),
|
||||||
groupInfo.isAdmin(selfRecipientId));
|
groupInfo.isAdmin(selfRecipientId),
|
||||||
|
groupInfo.isTerminated());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.asamk.signal.manager.groups.GroupLinkPassword;
|
import org.asamk.signal.manager.groups.GroupLinkPassword;
|
||||||
|
import org.signal.core.util.Base64;
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.signal.storageservice.protos.groups.GroupInviteLink;
|
import org.signal.storageservice.storage.protos.groups.GroupInviteLink;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
||||||
import org.whispersystems.util.Base64UrlSafe;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@ -39,21 +39,21 @@ public final class GroupInviteLinkUrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) {
|
if (!"/".equals(uri.getPath()) && !uri.getPath().isEmpty()) {
|
||||||
throw new InvalidGroupLinkException("No path was expected in uri");
|
throw new InvalidGroupLinkException("No path was expected in uri");
|
||||||
}
|
}
|
||||||
|
|
||||||
var encoding = uri.getFragment();
|
var encoding = uri.getFragment();
|
||||||
|
|
||||||
if (encoding == null || encoding.length() == 0) {
|
if (encoding == null || encoding.isEmpty()) {
|
||||||
throw new InvalidGroupLinkException("No reference was in the uri");
|
throw new InvalidGroupLinkException("No reference was in the uri");
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes = Base64UrlSafe.decodePaddingAgnostic(encoding);
|
var bytes = Base64.decode(encoding);
|
||||||
GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes);
|
GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes);
|
||||||
|
|
||||||
if (groupInviteLink.v1Contents != null) {
|
if (groupInviteLink.contentsV1 != null) {
|
||||||
var groupInviteLinkContentsV1 = groupInviteLink.v1Contents;
|
var groupInviteLinkContentsV1 = groupInviteLink.contentsV1;
|
||||||
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray());
|
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray());
|
||||||
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray());
|
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray());
|
||||||
|
|
||||||
@ -90,12 +90,12 @@ public final class GroupInviteLinkUrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
|
||||||
var groupInviteLink = new GroupInviteLink.Builder().v1Contents(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey(
|
var groupInviteLink = new GroupInviteLink.Builder().contentsV1(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey(
|
||||||
ByteString.of(groupMasterKey.serialize()))
|
ByteString.of(groupMasterKey.serialize()))
|
||||||
.inviteLinkPassword(ByteString.of(password.serialize()))
|
.inviteLinkPassword(ByteString.of(password.serialize()))
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
var encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.encode());
|
var encoding = Base64.encodeUrlSafeWithoutPadding(groupInviteLink.encode());
|
||||||
|
|
||||||
return GROUP_URL_PREFIX + encoding;
|
return GROUP_URL_PREFIX + encoding;
|
||||||
}
|
}
|
||||||
@ -112,7 +112,7 @@ public final class GroupInviteLinkUrl {
|
|||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final static class InvalidGroupLinkException extends Exception {
|
public static final class InvalidGroupLinkException extends Exception {
|
||||||
|
|
||||||
public InvalidGroupLinkException(String message) {
|
public InvalidGroupLinkException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
@ -123,7 +123,7 @@ public final class GroupInviteLinkUrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final static class UnknownGroupLinkVersionException extends Exception {
|
public static final class UnknownGroupLinkVersionException extends Exception {
|
||||||
|
|
||||||
public UnknownGroupLinkVersionException(String message) {
|
public UnknownGroupLinkVersionException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||||
|
import org.asamk.signal.manager.storage.groups.GroupMemberInfo;
|
||||||
|
|
||||||
|
public record GroupMember(
|
||||||
|
RecipientAddress recipientAddress, boolean isAdmin, String labelEmoji, String label
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static GroupMember from(final GroupMemberInfo memberInfo, final RecipientAddressResolver recipientStore) {
|
||||||
|
return new GroupMember(recipientStore.resolveRecipientAddress(memberInfo.getRecipientId())
|
||||||
|
.toApiRecipientAddress(), memberInfo.isAdmin(), memberInfo.labelEmoji(), memberInfo.labelString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,10 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.signal.libsignal.protocol.IdentityKey;
|
|
||||||
|
|
||||||
public record Identity(
|
public record Identity(
|
||||||
RecipientAddress recipient,
|
RecipientAddress recipient,
|
||||||
IdentityKey identityKey,
|
byte[] fingerprint,
|
||||||
String safetyNumber,
|
String safetyNumber,
|
||||||
byte[] scannableSafetyNumber,
|
byte[] scannableSafetyNumber,
|
||||||
TrustLevel trustLevel,
|
TrustLevel trustLevel,
|
||||||
long dateAddedTimestamp
|
long dateAddedTimestamp
|
||||||
) {
|
) {}
|
||||||
|
|
||||||
public byte[] getFingerprint() {
|
|
||||||
return identityKey.getPublicKey().serialize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,6 +2,10 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
public class InvalidNumberException extends Exception {
|
public class InvalidNumberException extends Exception {
|
||||||
|
|
||||||
|
public InvalidNumberException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
InvalidNumberException(String message, Throwable e) {
|
InvalidNumberException(String message, Throwable e) {
|
||||||
super(message, e);
|
super(message, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,15 @@ import java.util.Optional;
|
|||||||
public record Message(
|
public record Message(
|
||||||
String messageText,
|
String messageText,
|
||||||
List<String> attachments,
|
List<String> attachments,
|
||||||
|
boolean viewOnce,
|
||||||
|
boolean voiceNote,
|
||||||
List<Mention> mentions,
|
List<Mention> mentions,
|
||||||
Optional<Quote> quote,
|
Optional<Quote> quote,
|
||||||
Optional<Sticker> sticker,
|
Optional<Sticker> sticker,
|
||||||
List<Preview> previews,
|
List<Preview> previews,
|
||||||
Optional<StoryReply> storyReply,
|
Optional<StoryReply> storyReply,
|
||||||
List<TextStyle> textStyles
|
List<TextStyle> textStyles,
|
||||||
|
boolean urgent
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
|
||||||
@ -21,8 +24,12 @@ public record Message(
|
|||||||
RecipientIdentifier.Single author,
|
RecipientIdentifier.Single author,
|
||||||
String message,
|
String message,
|
||||||
List<Mention> mentions,
|
List<Mention> mentions,
|
||||||
List<TextStyle> textStyles
|
List<TextStyle> textStyles,
|
||||||
) {}
|
List<Attachment> attachments
|
||||||
|
) {
|
||||||
|
|
||||||
|
public record Attachment(String contentType, String filename, String preview) {}
|
||||||
|
}
|
||||||
|
|
||||||
public record Sticker(byte[] packId, int stickerId) {}
|
public record Sticker(byte[] packId, int stickerId) {}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package org.asamk.signal.manager.api;
|
|||||||
import org.asamk.signal.manager.groups.GroupUtils;
|
import org.asamk.signal.manager.groups.GroupUtils;
|
||||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||||
|
import org.asamk.signal.manager.util.MimeUtils;
|
||||||
import org.signal.libsignal.metadata.ProtocolException;
|
import org.signal.libsignal.metadata.ProtocolException;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
@ -34,7 +35,9 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes
|
|||||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -113,17 +116,35 @@ public record MessageEnvelope(
|
|||||||
Optional<Long> remoteDeleteId,
|
Optional<Long> remoteDeleteId,
|
||||||
Optional<Sticker> sticker,
|
Optional<Sticker> sticker,
|
||||||
List<SharedContact> sharedContacts,
|
List<SharedContact> sharedContacts,
|
||||||
|
Optional<PollCreate> pollCreate,
|
||||||
|
Optional<PollVote> pollVote,
|
||||||
|
Optional<PollTerminate> pollTerminate,
|
||||||
List<Mention> mentions,
|
List<Mention> mentions,
|
||||||
List<Preview> previews,
|
List<Preview> previews,
|
||||||
List<TextStyle> textStyles
|
List<TextStyle> textStyles,
|
||||||
|
Optional<PinMessage> pinMessage,
|
||||||
|
Optional<UnpinMessage> unpinMessage,
|
||||||
|
Optional<AdminDelete> adminDelete
|
||||||
) {
|
) {
|
||||||
|
|
||||||
static Data from(
|
static Data from(
|
||||||
final SignalServiceDataMessage dataMessage,
|
final SignalServiceDataMessage dataMessage,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider
|
final AttachmentFileProvider fileProvider
|
||||||
) {
|
) {
|
||||||
|
var body = dataMessage.getBody();
|
||||||
|
if (dataMessage.getAttachments().isPresent()) {
|
||||||
|
for (final var attachment : dataMessage.getAttachments().get()) {
|
||||||
|
if (MimeUtils.LONG_TEXT.equals(attachment.getContentType()) && attachment.isPointer()) {
|
||||||
|
final var longBody = longTexts.get(attachment.asPointer().getRemoteId().toString());
|
||||||
|
if (longBody != null) {
|
||||||
|
body = Optional.of(longBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return new Data(dataMessage.getTimestamp(),
|
return new Data(dataMessage.getTimestamp(),
|
||||||
dataMessage.getGroupContext().map(GroupContext::from),
|
dataMessage.getGroupContext().map(GroupContext::from),
|
||||||
dataMessage.getStoryContext()
|
dataMessage.getStoryContext()
|
||||||
@ -131,15 +152,17 @@ public record MessageEnvelope(
|
|||||||
recipientResolver,
|
recipientResolver,
|
||||||
addressResolver)),
|
addressResolver)),
|
||||||
dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
|
dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
|
||||||
dataMessage.getBody(),
|
body,
|
||||||
dataMessage.getExpiresInSeconds(),
|
dataMessage.getExpiresInSeconds(),
|
||||||
dataMessage.isExpirationUpdate(),
|
dataMessage.isExpirationUpdate(),
|
||||||
dataMessage.isViewOnce(),
|
dataMessage.isViewOnce(),
|
||||||
dataMessage.isEndSession(),
|
false,
|
||||||
dataMessage.isProfileKeyUpdate(),
|
dataMessage.isProfileKeyUpdate(),
|
||||||
dataMessage.getProfileKey().isPresent(),
|
dataMessage.getProfileKey().isPresent(),
|
||||||
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
|
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
|
||||||
dataMessage.getQuote().map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
|
dataMessage.getQuote()
|
||||||
|
.filter(q -> q.getAuthor() != null && q.getAuthor().isValid())
|
||||||
|
.map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
|
||||||
dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null),
|
dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null),
|
||||||
dataMessage.getAttachments()
|
dataMessage.getAttachments()
|
||||||
.map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
|
.map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
|
||||||
@ -151,6 +174,9 @@ public record MessageEnvelope(
|
|||||||
.map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
|
.map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
|
||||||
.toList())
|
.toList())
|
||||||
.orElse(List.of()),
|
.orElse(List.of()),
|
||||||
|
dataMessage.getPollCreate().map(PollCreate::from),
|
||||||
|
dataMessage.getPollVote().map(p -> PollVote.from(p, recipientResolver, addressResolver)),
|
||||||
|
dataMessage.getPollTerminate().map(PollTerminate::from),
|
||||||
dataMessage.getMentions()
|
dataMessage.getMentions()
|
||||||
.map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
|
.map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
|
||||||
.orElse(List.of()),
|
.orElse(List.of()),
|
||||||
@ -159,7 +185,10 @@ public record MessageEnvelope(
|
|||||||
.orElse(List.of()),
|
.orElse(List.of()),
|
||||||
dataMessage.getBodyRanges()
|
dataMessage.getBodyRanges()
|
||||||
.map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList())
|
.map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList())
|
||||||
.orElse(List.of()));
|
.orElse(List.of()),
|
||||||
|
dataMessage.getPinnedMessage().map(p -> PinMessage.from(p, recipientResolver, addressResolver)),
|
||||||
|
dataMessage.getUnpinnedMessage().map(p -> UnpinMessage.from(p, recipientResolver, addressResolver)),
|
||||||
|
dataMessage.getAdminDelete().map(p -> AdminDelete.from(p, recipientResolver, addressResolver)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
|
public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
|
||||||
@ -234,7 +263,7 @@ public record MessageEnvelope(
|
|||||||
return new Quote(quote.getId(),
|
return new Quote(quote.getId(),
|
||||||
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor()))
|
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor()))
|
||||||
.toApiRecipientAddress(),
|
.toApiRecipientAddress(),
|
||||||
Optional.ofNullable(quote.getText()),
|
Optional.of(quote.getText()),
|
||||||
quote.getMentions() == null
|
quote.getMentions() == null
|
||||||
? List.of()
|
? List.of()
|
||||||
: quote.getMentions()
|
: quote.getMentions()
|
||||||
@ -291,9 +320,9 @@ public record MessageEnvelope(
|
|||||||
boolean isBorderless
|
boolean isBorderless
|
||||||
) {
|
) {
|
||||||
|
|
||||||
static Attachment from(SignalServiceAttachment attachment, AttachmentFileProvider fileProvider) {
|
static Attachment from(SignalServiceAttachment signalAttachment, AttachmentFileProvider fileProvider) {
|
||||||
if (attachment.isPointer()) {
|
if (signalAttachment.isPointer()) {
|
||||||
final var a = attachment.asPointer();
|
final var a = signalAttachment.asPointer();
|
||||||
final var attachmentFile = fileProvider.getFile(a);
|
final var attachmentFile = fileProvider.getFile(a);
|
||||||
return new Attachment(Optional.of(attachmentFile.getName()),
|
return new Attachment(Optional.of(attachmentFile.getName()),
|
||||||
Optional.of(attachmentFile),
|
Optional.of(attachmentFile),
|
||||||
@ -310,8 +339,9 @@ public record MessageEnvelope(
|
|||||||
a.isGif(),
|
a.isGif(),
|
||||||
a.isBorderless());
|
a.isBorderless());
|
||||||
} else {
|
} else {
|
||||||
final var a = attachment.asStream();
|
Attachment attachment = null;
|
||||||
return new Attachment(Optional.empty(),
|
try (final var a = signalAttachment.asStream()) {
|
||||||
|
attachment = new Attachment(Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
a.getFileName(),
|
a.getFileName(),
|
||||||
a.getContentType(),
|
a.getContentType(),
|
||||||
@ -325,11 +355,16 @@ public record MessageEnvelope(
|
|||||||
a.getVoiceNote(),
|
a.getVoiceNote(),
|
||||||
a.isGif(),
|
a.isGif(),
|
||||||
a.isBorderless());
|
a.isBorderless());
|
||||||
|
return attachment;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Attachment from(
|
static Attachment from(
|
||||||
SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider
|
SignalServiceDataMessage.Quote.QuotedAttachment a,
|
||||||
|
final AttachmentFileProvider fileProvider
|
||||||
) {
|
) {
|
||||||
return new Attachment(Optional.empty(),
|
return new Attachment(Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
@ -381,7 +416,7 @@ public record MessageEnvelope(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record Name(
|
public record Name(
|
||||||
Optional<String> display,
|
Optional<String> nickname,
|
||||||
Optional<String> given,
|
Optional<String> given,
|
||||||
Optional<String> family,
|
Optional<String> family,
|
||||||
Optional<String> prefix,
|
Optional<String> prefix,
|
||||||
@ -390,7 +425,7 @@ public record MessageEnvelope(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
static Name from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Name name) {
|
static Name from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Name name) {
|
||||||
return new Name(name.getDisplay(),
|
return new Name(name.getNickname(),
|
||||||
name.getGiven(),
|
name.getGiven(),
|
||||||
name.getFamily(),
|
name.getFamily(),
|
||||||
name.getPrefix(),
|
name.getPrefix(),
|
||||||
@ -472,15 +507,15 @@ public record MessageEnvelope(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) {
|
static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) {
|
||||||
return new Address(Address.Type.from(address.getType()),
|
return new Address(Type.from(address.getType()),
|
||||||
address.getLabel(),
|
address.getLabel(),
|
||||||
address.getLabel(),
|
address.getStreet(),
|
||||||
address.getLabel(),
|
address.getPobox(),
|
||||||
address.getLabel(),
|
address.getNeighborhood(),
|
||||||
address.getLabel(),
|
address.getCity(),
|
||||||
address.getLabel(),
|
address.getRegion(),
|
||||||
address.getLabel(),
|
address.getPostcode(),
|
||||||
address.getLabel());
|
address.getCountry());
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
@ -499,11 +534,47 @@ public record MessageEnvelope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record PollCreate(
|
||||||
|
String question, boolean allowMultiple, List<String> options
|
||||||
|
) {
|
||||||
|
|
||||||
|
static PollCreate from(
|
||||||
|
SignalServiceDataMessage.PollCreate pollCreate
|
||||||
|
) {
|
||||||
|
return new PollCreate(pollCreate.getQuestion(), pollCreate.getAllowMultiple(), pollCreate.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PollVote(
|
||||||
|
RecipientAddress targetAuthor, long targetSentTimestamp, List<Integer> optionIndexes, int voteCount
|
||||||
|
) {
|
||||||
|
|
||||||
|
static PollVote from(
|
||||||
|
SignalServiceDataMessage.PollVote pollVote,
|
||||||
|
RecipientResolver recipientResolver,
|
||||||
|
RecipientAddressResolver addressResolver
|
||||||
|
) {
|
||||||
|
return new PollVote(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(pollVote.getTargetAuthor()))
|
||||||
|
.toApiRecipientAddress(),
|
||||||
|
pollVote.getTargetSentTimestamp(),
|
||||||
|
pollVote.getOptionIndexes(),
|
||||||
|
pollVote.getVoteCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PollTerminate(long targetSentTimestamp) {
|
||||||
|
|
||||||
|
static PollTerminate from(SignalServiceDataMessage.PollTerminate pollTerminate) {
|
||||||
|
return new PollTerminate(pollTerminate.getTargetSentTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
|
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
|
||||||
|
|
||||||
static Preview from(
|
static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) {
|
||||||
SignalServicePreview preview, final AttachmentFileProvider fileProvider
|
|
||||||
) {
|
|
||||||
return new Preview(preview.getTitle(),
|
return new Preview(preview.getTitle(),
|
||||||
preview.getDescription(),
|
preview.getDescription(),
|
||||||
preview.getDate(),
|
preview.getDate(),
|
||||||
@ -512,18 +583,68 @@ public record MessageEnvelope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record PinMessage(
|
||||||
|
RecipientAddress targetAuthor, long targetSentTimestamp, long pinDurationSeconds
|
||||||
|
) {
|
||||||
|
|
||||||
|
static PinMessage from(
|
||||||
|
SignalServiceDataMessage.PinnedMessage pinnedMessage,
|
||||||
|
RecipientResolver recipientResolver,
|
||||||
|
RecipientAddressResolver addressResolver
|
||||||
|
) {
|
||||||
|
return new PinMessage(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
|
pinnedMessage.getTargetAuthor())).toApiRecipientAddress(),
|
||||||
|
pinnedMessage.getTargetSentTimestamp(),
|
||||||
|
Boolean.TRUE.equals(pinnedMessage.getForever())
|
||||||
|
? -1
|
||||||
|
: pinnedMessage.getPinDurationInSeconds() == null
|
||||||
|
? 0
|
||||||
|
: pinnedMessage.getPinDurationInSeconds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UnpinMessage(RecipientAddress targetAuthor, long targetSentTimestamp) {
|
||||||
|
|
||||||
|
static UnpinMessage from(
|
||||||
|
SignalServiceDataMessage.UnpinnedMessage unpinnedMessage,
|
||||||
|
RecipientResolver recipientResolver,
|
||||||
|
RecipientAddressResolver addressResolver
|
||||||
|
) {
|
||||||
|
return new UnpinMessage(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
|
unpinnedMessage.getTargetAuthor())).toApiRecipientAddress(),
|
||||||
|
unpinnedMessage.getTargetSentTimestamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AdminDelete(RecipientAddress targetAuthor, long targetSentTimestamp) {
|
||||||
|
|
||||||
|
static AdminDelete from(
|
||||||
|
SignalServiceDataMessage.AdminDelete adminDelete,
|
||||||
|
RecipientResolver recipientResolver,
|
||||||
|
RecipientAddressResolver addressResolver
|
||||||
|
) {
|
||||||
|
return new AdminDelete(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
|
adminDelete.getTargetAuthor())).toApiRecipientAddress(), adminDelete.getTargetSentTimestamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Edit(long targetSentTimestamp, Data dataMessage) {
|
public record Edit(long targetSentTimestamp, Data dataMessage) {
|
||||||
|
|
||||||
public static Edit from(
|
public static Edit from(
|
||||||
final SignalServiceEditMessage editMessage,
|
final SignalServiceEditMessage editMessage,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider
|
final AttachmentFileProvider fileProvider
|
||||||
) {
|
) {
|
||||||
return new Edit(editMessage.getTargetSentTimestamp(),
|
return new Edit(editMessage.getTargetSentTimestamp(),
|
||||||
Data.from(editMessage.getDataMessage(), recipientResolver, addressResolver, fileProvider));
|
Data.from(editMessage.getDataMessage(),
|
||||||
|
longTexts,
|
||||||
|
recipientResolver,
|
||||||
|
addressResolver,
|
||||||
|
fileProvider));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,12 +661,13 @@ public record MessageEnvelope(
|
|||||||
|
|
||||||
public static Sync from(
|
public static Sync from(
|
||||||
final SignalServiceSyncMessage syncMessage,
|
final SignalServiceSyncMessage syncMessage,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider
|
final AttachmentFileProvider fileProvider
|
||||||
) {
|
) {
|
||||||
return new Sync(syncMessage.getSent()
|
return new Sync(syncMessage.getSent()
|
||||||
.map(s -> Sent.from(s, recipientResolver, addressResolver, fileProvider)),
|
.map(s -> Sent.from(s, longTexts, recipientResolver, addressResolver, fileProvider)),
|
||||||
syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)),
|
syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)),
|
||||||
syncMessage.getRead()
|
syncMessage.getRead()
|
||||||
.map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
|
.map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
|
||||||
@ -574,6 +696,7 @@ public record MessageEnvelope(
|
|||||||
|
|
||||||
static Sent from(
|
static Sent from(
|
||||||
SentTranscriptMessage sentMessage,
|
SentTranscriptMessage sentMessage,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider
|
final AttachmentFileProvider fileProvider
|
||||||
@ -589,9 +712,17 @@ public record MessageEnvelope(
|
|||||||
.toApiRecipientAddress())
|
.toApiRecipientAddress())
|
||||||
.collect(Collectors.toSet()),
|
.collect(Collectors.toSet()),
|
||||||
sentMessage.getDataMessage()
|
sentMessage.getDataMessage()
|
||||||
.map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)),
|
.map(message -> Data.from(message,
|
||||||
|
longTexts,
|
||||||
|
recipientResolver,
|
||||||
|
addressResolver,
|
||||||
|
fileProvider)),
|
||||||
sentMessage.getEditMessage()
|
sentMessage.getEditMessage()
|
||||||
.map(message -> Edit.from(message, recipientResolver, addressResolver, fileProvider)),
|
.map(message -> Edit.from(message,
|
||||||
|
longTexts,
|
||||||
|
recipientResolver,
|
||||||
|
addressResolver,
|
||||||
|
fileProvider)),
|
||||||
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
|
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -603,11 +734,15 @@ public record MessageEnvelope(
|
|||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver
|
RecipientAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
return new Blocked(blockedListMessage.getAddresses()
|
return new Blocked(blockedListMessage.individuals.stream()
|
||||||
.stream()
|
.map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(),
|
||||||
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
|
null,
|
||||||
.toApiRecipientAddress())
|
d.getE164(),
|
||||||
.toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
|
null))
|
||||||
|
.toList(),
|
||||||
|
blockedListMessage.groups.stream()
|
||||||
|
.map(group -> GroupId.unknownVersion(group.getGroupId()))
|
||||||
|
.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,7 +753,7 @@ public record MessageEnvelope(
|
|||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver
|
RecipientAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
|
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSenderAci()))
|
||||||
.toApiRecipientAddress(), readMessage.getTimestamp());
|
.toApiRecipientAddress(), readMessage.getTimestamp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -681,7 +816,9 @@ public record MessageEnvelope(
|
|||||||
DELETE,
|
DELETE,
|
||||||
BLOCK,
|
BLOCK,
|
||||||
BLOCK_AND_DELETE,
|
BLOCK_AND_DELETE,
|
||||||
UNBLOCK_AND_ACCEPT;
|
UNBLOCK_AND_ACCEPT,
|
||||||
|
SPAM,
|
||||||
|
BLOCK_AND_SPAM;
|
||||||
|
|
||||||
static Type from(MessageRequestResponseMessage.Type type) {
|
static Type from(MessageRequestResponseMessage.Type type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
@ -691,6 +828,8 @@ public record MessageEnvelope(
|
|||||||
case BLOCK -> BLOCK;
|
case BLOCK -> BLOCK;
|
||||||
case BLOCK_AND_DELETE -> BLOCK_AND_DELETE;
|
case BLOCK_AND_DELETE -> BLOCK_AND_DELETE;
|
||||||
case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT;
|
case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT;
|
||||||
|
case SPAM -> SPAM;
|
||||||
|
case BLOCK_AND_SPAM -> BLOCK_AND_SPAM;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -707,7 +846,6 @@ public record MessageEnvelope(
|
|||||||
Optional<Busy> busy,
|
Optional<Busy> busy,
|
||||||
List<IceUpdate> iceUpdate,
|
List<IceUpdate> iceUpdate,
|
||||||
Optional<Opaque> opaque,
|
Optional<Opaque> opaque,
|
||||||
boolean isMultiRing,
|
|
||||||
boolean isUrgent
|
boolean isUrgent
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -723,17 +861,13 @@ public record MessageEnvelope(
|
|||||||
.map(m -> m.stream().map(IceUpdate::from).toList())
|
.map(m -> m.stream().map(IceUpdate::from).toList())
|
||||||
.orElse(List.of()),
|
.orElse(List.of()),
|
||||||
callMessage.getOpaqueMessage().map(Opaque::from),
|
callMessage.getOpaqueMessage().map(Opaque::from),
|
||||||
callMessage.isMultiRing(),
|
|
||||||
callMessage.isUrgent());
|
callMessage.isUrgent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Offer(long id, String sdp, Type type, byte[] opaque) {
|
public record Offer(long id, Type type, byte[] opaque) {
|
||||||
|
|
||||||
static Offer from(OfferMessage offerMessage) {
|
static Offer from(OfferMessage offerMessage) {
|
||||||
return new Offer(offerMessage.getId(),
|
return new Offer(offerMessage.getId(), Type.from(offerMessage.getType()), offerMessage.getOpaque());
|
||||||
offerMessage.getSdp(),
|
|
||||||
Type.from(offerMessage.getType()),
|
|
||||||
offerMessage.getOpaque());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
@ -749,10 +883,10 @@ public record MessageEnvelope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Answer(long id, String sdp, byte[] opaque) {
|
public record Answer(long id, byte[] opaque) {
|
||||||
|
|
||||||
static Answer from(AnswerMessage answerMessage) {
|
static Answer from(AnswerMessage answerMessage) {
|
||||||
return new Answer(answerMessage.getId(), answerMessage.getSdp(), answerMessage.getOpaque());
|
return new Answer(answerMessage.getId(), answerMessage.getOpaque());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -790,10 +924,10 @@ public record MessageEnvelope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record IceUpdate(long id, String sdp, byte[] opaque) {
|
public record IceUpdate(long id, byte[] opaque) {
|
||||||
|
|
||||||
static IceUpdate from(IceUpdateMessage iceUpdateMessage) {
|
static IceUpdate from(IceUpdateMessage iceUpdateMessage) {
|
||||||
return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getSdp(), iceUpdateMessage.getOpaque());
|
return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getOpaque());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -824,9 +958,7 @@ public record MessageEnvelope(
|
|||||||
Optional<TextAttachment> textAttachment
|
Optional<TextAttachment> textAttachment
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static Story from(
|
public static Story from(SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider) {
|
||||||
SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider
|
|
||||||
) {
|
|
||||||
return new Story(storyMessage.getAllowsReplies().orElse(false),
|
return new Story(storyMessage.getAllowsReplies().orElse(false),
|
||||||
storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
|
storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
|
||||||
storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
|
storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
|
||||||
@ -844,7 +976,8 @@ public record MessageEnvelope(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
static TextAttachment from(
|
static TextAttachment from(
|
||||||
SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider
|
SignalServiceTextAttachment textAttachment,
|
||||||
|
final AttachmentFileProvider fileProvider
|
||||||
) {
|
) {
|
||||||
return new TextAttachment(textAttachment.getText(),
|
return new TextAttachment(textAttachment.getText(),
|
||||||
textAttachment.getStyle().map(Style::from),
|
textAttachment.getStyle().map(Style::from),
|
||||||
@ -891,13 +1024,15 @@ public record MessageEnvelope(
|
|||||||
public static MessageEnvelope from(
|
public static MessageEnvelope from(
|
||||||
SignalServiceEnvelope envelope,
|
SignalServiceEnvelope envelope,
|
||||||
SignalServiceContent content,
|
SignalServiceContent content,
|
||||||
|
Map<String, String> longTexts,
|
||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver,
|
RecipientAddressResolver addressResolver,
|
||||||
final AttachmentFileProvider fileProvider,
|
final AttachmentFileProvider fileProvider,
|
||||||
Exception exception
|
Exception exception
|
||||||
) {
|
) {
|
||||||
final var source = !envelope.isUnidentifiedSender() && envelope.hasSourceServiceId()
|
final var serviceId = envelope.getSourceServiceId();
|
||||||
? recipientResolver.resolveRecipient(envelope.getSourceAddress())
|
final var source = !envelope.isUnidentifiedSender() && serviceId != null
|
||||||
|
? recipientResolver.resolveRecipient(serviceId)
|
||||||
: envelope.isUnidentifiedSender() && content != null
|
: envelope.isUnidentifiedSender() && content != null
|
||||||
? recipientResolver.resolveRecipient(content.getSender())
|
? recipientResolver.resolveRecipient(content.getSender())
|
||||||
: exception instanceof ProtocolException e
|
: exception instanceof ProtocolException e
|
||||||
@ -920,9 +1055,15 @@ public record MessageEnvelope(
|
|||||||
receipt = content.getReceiptMessage().map(Receipt::from);
|
receipt = content.getReceiptMessage().map(Receipt::from);
|
||||||
typing = content.getTypingMessage().map(Typing::from);
|
typing = content.getTypingMessage().map(Typing::from);
|
||||||
data = content.getDataMessage()
|
data = content.getDataMessage()
|
||||||
.map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider));
|
.map(dataMessage -> Data.from(dataMessage,
|
||||||
edit = content.getEditMessage().map(s -> Edit.from(s, recipientResolver, addressResolver, fileProvider));
|
longTexts,
|
||||||
sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider));
|
recipientResolver,
|
||||||
|
addressResolver,
|
||||||
|
fileProvider));
|
||||||
|
edit = content.getEditMessage()
|
||||||
|
.map(s -> Edit.from(s, longTexts, recipientResolver, addressResolver, fileProvider));
|
||||||
|
sync = content.getSyncMessage()
|
||||||
|
.map(s -> Sync.from(s, longTexts, recipientResolver, addressResolver, fileProvider));
|
||||||
call = content.getCallMessage().map(Call::from);
|
call = content.getCallMessage().map(Call::from);
|
||||||
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
|
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -3,5 +3,16 @@ package org.asamk.signal.manager.api;
|
|||||||
public enum PhoneNumberSharingMode {
|
public enum PhoneNumberSharingMode {
|
||||||
EVERYBODY,
|
EVERYBODY,
|
||||||
CONTACTS,
|
CONTACTS,
|
||||||
NOBODY,
|
NOBODY;
|
||||||
|
|
||||||
|
public static PhoneNumberSharingMode valueOfOrNull(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return valueOf(value);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
public class PinLockMissingException extends Exception {}
|
||||||
@ -26,6 +26,8 @@ public class Profile {
|
|||||||
|
|
||||||
private final Set<Capability> capabilities;
|
private final Set<Capability> capabilities;
|
||||||
|
|
||||||
|
private final PhoneNumberSharingMode phoneNumberSharingMode;
|
||||||
|
|
||||||
public Profile(
|
public Profile(
|
||||||
final long lastUpdateTimestamp,
|
final long lastUpdateTimestamp,
|
||||||
final String givenName,
|
final String givenName,
|
||||||
@ -35,7 +37,8 @@ public class Profile {
|
|||||||
final String avatarUrlPath,
|
final String avatarUrlPath,
|
||||||
final byte[] mobileCoinAddress,
|
final byte[] mobileCoinAddress,
|
||||||
final UnidentifiedAccessMode unidentifiedAccessMode,
|
final UnidentifiedAccessMode unidentifiedAccessMode,
|
||||||
final Set<Capability> capabilities
|
final Set<Capability> capabilities,
|
||||||
|
final PhoneNumberSharingMode phoneNumberSharingMode
|
||||||
) {
|
) {
|
||||||
this.lastUpdateTimestamp = lastUpdateTimestamp;
|
this.lastUpdateTimestamp = lastUpdateTimestamp;
|
||||||
this.givenName = givenName;
|
this.givenName = givenName;
|
||||||
@ -46,6 +49,7 @@ public class Profile {
|
|||||||
this.mobileCoinAddress = mobileCoinAddress;
|
this.mobileCoinAddress = mobileCoinAddress;
|
||||||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||||
this.capabilities = capabilities;
|
this.capabilities = capabilities;
|
||||||
|
this.phoneNumberSharingMode = phoneNumberSharingMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Profile(final Builder builder) {
|
private Profile(final Builder builder) {
|
||||||
@ -58,6 +62,7 @@ public class Profile {
|
|||||||
mobileCoinAddress = builder.mobileCoinAddress;
|
mobileCoinAddress = builder.mobileCoinAddress;
|
||||||
unidentifiedAccessMode = builder.unidentifiedAccessMode;
|
unidentifiedAccessMode = builder.unidentifiedAccessMode;
|
||||||
capabilities = builder.capabilities;
|
capabilities = builder.capabilities;
|
||||||
|
phoneNumberSharingMode = builder.phoneNumberSharingMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder newBuilder() {
|
public static Builder newBuilder() {
|
||||||
@ -136,6 +141,10 @@ public class Profile {
|
|||||||
return capabilities;
|
return capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PhoneNumberSharingMode getPhoneNumberSharingMode() {
|
||||||
|
return phoneNumberSharingMode;
|
||||||
|
}
|
||||||
|
|
||||||
public enum UnidentifiedAccessMode {
|
public enum UnidentifiedAccessMode {
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
DISABLED,
|
DISABLED,
|
||||||
@ -153,9 +162,7 @@ public class Profile {
|
|||||||
|
|
||||||
public enum Capability {
|
public enum Capability {
|
||||||
storage,
|
storage,
|
||||||
gv1Migration,
|
storageServiceEncryptionV2Capability;
|
||||||
senderKey,
|
|
||||||
announcementGroup;
|
|
||||||
|
|
||||||
public static Capability valueOfOrNull(String value) {
|
public static Capability valueOfOrNull(String value) {
|
||||||
try {
|
try {
|
||||||
@ -203,6 +210,7 @@ public class Profile {
|
|||||||
private byte[] mobileCoinAddress;
|
private byte[] mobileCoinAddress;
|
||||||
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
|
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
|
||||||
private Set<Capability> capabilities = Collections.emptySet();
|
private Set<Capability> capabilities = Collections.emptySet();
|
||||||
|
private PhoneNumberSharingMode phoneNumberSharingMode;
|
||||||
private long lastUpdateTimestamp = 0;
|
private long lastUpdateTimestamp = 0;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
@ -243,6 +251,11 @@ public class Profile {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withPhoneNumberSharingMode(final PhoneNumberSharingMode val) {
|
||||||
|
phoneNumberSharingMode = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Profile build() {
|
public Profile build() {
|
||||||
return new Profile(this);
|
return new Profile(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,19 @@ public class ProofRequiredException extends Exception {
|
|||||||
|
|
||||||
private final String token;
|
private final String token;
|
||||||
private final Set<Option> options;
|
private final Set<Option> options;
|
||||||
private final long retryAfterSeconds;
|
private final long retryAfterMilliseconds;
|
||||||
|
|
||||||
public ProofRequiredException(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
|
public ProofRequiredException(final String token, final Set<Option> options, final long retryAfterMilliseconds) {
|
||||||
this.token = e.getToken();
|
super("Rate limit");
|
||||||
this.options = e.getOptions().stream().map(Option::from).collect(Collectors.toSet());
|
this.token = token;
|
||||||
this.retryAfterSeconds = e.getRetryAfterSeconds();
|
this.options = options;
|
||||||
|
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProofRequiredException from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
|
||||||
|
return new ProofRequiredException(e.getToken(),
|
||||||
|
e.getOptions().stream().map(Option::from).collect(Collectors.toSet()),
|
||||||
|
e.getRetryAfterSeconds() * 1000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getToken() {
|
public String getToken() {
|
||||||
@ -26,17 +33,17 @@ public class ProofRequiredException extends Exception {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getRetryAfterSeconds() {
|
public long getRetryAfterMilliseconds() {
|
||||||
return retryAfterSeconds;
|
return retryAfterMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Option {
|
public enum Option {
|
||||||
RECAPTCHA,
|
CAPTCHA,
|
||||||
PUSH_CHALLENGE;
|
PUSH_CHALLENGE;
|
||||||
|
|
||||||
static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) {
|
static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) {
|
||||||
return switch (option) {
|
return switch (option) {
|
||||||
case RECAPTCHA -> RECAPTCHA;
|
case CAPTCHA -> CAPTCHA;
|
||||||
case PUSH_CHALLENGE -> PUSH_CHALLENGE;
|
case PUSH_CHALLENGE -> PUSH_CHALLENGE;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,18 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
public class RateLimitException extends Exception {
|
public class RateLimitException extends Exception {
|
||||||
|
|
||||||
private final long nextAttemptTimestamp;
|
private final Long retryAfterMilliseconds;
|
||||||
|
|
||||||
public RateLimitException(final long nextAttemptTimestamp) {
|
public RateLimitException(final Long retryAfterMilliseconds) {
|
||||||
super("Rate limit");
|
super("Rate limit");
|
||||||
this.nextAttemptTimestamp = nextAttemptTimestamp;
|
this.retryAfterMilliseconds = retryAfterMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getNextAttemptTimestamp() {
|
public static RateLimitException from(org.whispersystems.signalservice.api.push.exceptions.RateLimitException e) {
|
||||||
return nextAttemptTimestamp;
|
return new RateLimitException(e.getRetryAfterMilliseconds().orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getRetryAfterMilliseconds() {
|
||||||
|
return retryAfterMilliseconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
public record ReceiveConfig(boolean ignoreAttachments, boolean ignoreStories, boolean sendReadReceipts) {}
|
public record ReceiveConfig(
|
||||||
|
boolean ignoreAttachments,
|
||||||
|
boolean ignoreStories,
|
||||||
|
boolean ignoreAvatars,
|
||||||
|
boolean ignoreStickers,
|
||||||
|
boolean sendReadReceipts
|
||||||
|
) {}
|
||||||
|
|||||||
@ -20,13 +20,16 @@ public class Recipient {
|
|||||||
|
|
||||||
private final Profile profile;
|
private final Profile profile;
|
||||||
|
|
||||||
|
private final Boolean discoverable;
|
||||||
|
|
||||||
public Recipient(
|
public Recipient(
|
||||||
final RecipientId recipientId,
|
final RecipientId recipientId,
|
||||||
final RecipientAddress address,
|
final RecipientAddress address,
|
||||||
final Contact contact,
|
final Contact contact,
|
||||||
final ProfileKey profileKey,
|
final ProfileKey profileKey,
|
||||||
final ExpiringProfileKeyCredential expiringProfileKeyCredential,
|
final ExpiringProfileKeyCredential expiringProfileKeyCredential,
|
||||||
final Profile profile
|
final Profile profile,
|
||||||
|
final Boolean discoverable
|
||||||
) {
|
) {
|
||||||
this.recipientId = recipientId;
|
this.recipientId = recipientId;
|
||||||
this.address = address;
|
this.address = address;
|
||||||
@ -34,6 +37,7 @@ public class Recipient {
|
|||||||
this.profileKey = profileKey;
|
this.profileKey = profileKey;
|
||||||
this.expiringProfileKeyCredential = expiringProfileKeyCredential;
|
this.expiringProfileKeyCredential = expiringProfileKeyCredential;
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
|
this.discoverable = discoverable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Recipient(final Builder builder) {
|
private Recipient(final Builder builder) {
|
||||||
@ -41,8 +45,9 @@ public class Recipient {
|
|||||||
address = builder.address;
|
address = builder.address;
|
||||||
contact = builder.contact;
|
contact = builder.contact;
|
||||||
profileKey = builder.profileKey;
|
profileKey = builder.profileKey;
|
||||||
expiringProfileKeyCredential = builder.expiringProfileKeyCredential1;
|
expiringProfileKeyCredential = builder.expiringProfileKeyCredential;
|
||||||
profile = builder.profile;
|
profile = builder.profile;
|
||||||
|
discoverable = builder.discoverable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder newBuilder() {
|
public static Builder newBuilder() {
|
||||||
@ -55,7 +60,7 @@ public class Recipient {
|
|||||||
builder.address = copy.getAddress();
|
builder.address = copy.getAddress();
|
||||||
builder.contact = copy.getContact();
|
builder.contact = copy.getContact();
|
||||||
builder.profileKey = copy.getProfileKey();
|
builder.profileKey = copy.getProfileKey();
|
||||||
builder.expiringProfileKeyCredential1 = copy.getExpiringProfileKeyCredential();
|
builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
|
||||||
builder.profile = copy.getProfile();
|
builder.profile = copy.getProfile();
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
@ -84,6 +89,10 @@ public class Recipient {
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getDiscoverable() {
|
||||||
|
return discoverable;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object o) {
|
public boolean equals(final Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
@ -108,8 +117,9 @@ public class Recipient {
|
|||||||
private RecipientAddress address;
|
private RecipientAddress address;
|
||||||
private Contact contact;
|
private Contact contact;
|
||||||
private ProfileKey profileKey;
|
private ProfileKey profileKey;
|
||||||
private ExpiringProfileKeyCredential expiringProfileKeyCredential1;
|
private ExpiringProfileKeyCredential expiringProfileKeyCredential;
|
||||||
private Profile profile;
|
private Profile profile;
|
||||||
|
private Boolean discoverable;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
}
|
}
|
||||||
@ -135,7 +145,7 @@ public class Recipient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) {
|
public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) {
|
||||||
expiringProfileKeyCredential1 = val;
|
expiringProfileKeyCredential = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,6 +154,11 @@ public class Recipient {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withDiscoverable(final Boolean val) {
|
||||||
|
discoverable = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Recipient build() {
|
public Recipient build() {
|
||||||
return new Recipient(this);
|
return new Recipient(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,49 +1,55 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Optional<String> username) {
|
public record RecipientAddress(
|
||||||
|
Optional<String> aci, Optional<String> pni, Optional<String> number, Optional<String> username
|
||||||
|
) {
|
||||||
|
|
||||||
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
|
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a RecipientAddress.
|
* Construct a RecipientAddress.
|
||||||
*
|
*
|
||||||
* @param uuid The UUID of the user, if available.
|
* @param aci The ACI of the user, if available.
|
||||||
|
* @param pni The PNI of the user, if available.
|
||||||
* @param number The phone number of the user, if available.
|
* @param number The phone number of the user, if available.
|
||||||
*/
|
*/
|
||||||
public RecipientAddress {
|
public RecipientAddress {
|
||||||
uuid = uuid.isPresent() && uuid.get().equals(UNKNOWN_UUID) ? Optional.empty() : uuid;
|
if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) {
|
||||||
if (uuid.isEmpty() && number.isEmpty() && username.isEmpty()) {
|
throw new AssertionError("Must have either a ACI, PNI, username or E164 number!");
|
||||||
throw new AssertionError("Must have either a UUID or E164 number!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientAddress(UUID uuid, String e164) {
|
public RecipientAddress(String e164) {
|
||||||
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.empty());
|
this(null, null, e164, null);
|
||||||
}
|
|
||||||
|
|
||||||
public RecipientAddress(UUID uuid, String e164, String username) {
|
|
||||||
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.ofNullable(username));
|
|
||||||
}
|
|
||||||
|
|
||||||
public RecipientAddress(SignalServiceAddress address) {
|
|
||||||
this(Optional.of(address.getServiceId().getRawUuid()), address.getNumber(), Optional.empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientAddress(UUID uuid) {
|
public RecipientAddress(UUID uuid) {
|
||||||
this(Optional.of(uuid), Optional.empty(), Optional.empty());
|
this(uuid.toString(), null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecipientAddress(String aci, String pni, String e164, String username) {
|
||||||
|
this(Optional.ofNullable(aci),
|
||||||
|
Optional.ofNullable(pni),
|
||||||
|
Optional.ofNullable(e164),
|
||||||
|
Optional.ofNullable(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<UUID> uuid() {
|
||||||
|
return aci.map(UUID::fromString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getIdentifier() {
|
public String getIdentifier() {
|
||||||
if (uuid.isPresent()) {
|
if (aci.isPresent()) {
|
||||||
return uuid.get().toString();
|
return aci.get();
|
||||||
} else if (number.isPresent()) {
|
} else if (number.isPresent()) {
|
||||||
return number.get();
|
return number.get();
|
||||||
|
} else if (pni.isPresent()) {
|
||||||
|
return pni.get();
|
||||||
} else if (username.isPresent()) {
|
} else if (username.isPresent()) {
|
||||||
return username.get();
|
return username.get();
|
||||||
} else {
|
} else {
|
||||||
@ -54,17 +60,16 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Opt
|
|||||||
public String getLegacyIdentifier() {
|
public String getLegacyIdentifier() {
|
||||||
if (number.isPresent()) {
|
if (number.isPresent()) {
|
||||||
return number.get();
|
return number.get();
|
||||||
} else if (uuid.isPresent()) {
|
|
||||||
return uuid.get().toString();
|
|
||||||
} else if (username.isPresent()) {
|
|
||||||
return username.get();
|
|
||||||
} else {
|
} else {
|
||||||
throw new AssertionError("Given the checks in the constructor, this should not be possible.");
|
return getIdentifier();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(RecipientAddress other) {
|
public boolean matches(RecipientAddress other) {
|
||||||
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get()))
|
return (aci.isPresent() && other.aci.isPresent() && aci.get().equals(other.aci.get()))
|
||||||
|
|| (
|
||||||
|
pni.isPresent() && other.pni.isPresent() && pni.get().equals(other.pni.get())
|
||||||
|
)
|
||||||
|| (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get()))
|
|| (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get()))
|
||||||
|| (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get()));
|
|| (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get()));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@ -24,11 +24,18 @@ public sealed interface RecipientIdentifier {
|
|||||||
sealed interface Single extends RecipientIdentifier {
|
sealed interface Single extends RecipientIdentifier {
|
||||||
|
|
||||||
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
|
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
|
||||||
try {
|
if (UuidUtil.INSTANCE.isUuid(identifier)) {
|
||||||
if (UuidUtil.isUuid(identifier)) {
|
|
||||||
return new Uuid(UUID.fromString(identifier));
|
return new Uuid(UUID.fromString(identifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (identifier.startsWith("PNI:")) {
|
||||||
|
final var pni = identifier.substring(4);
|
||||||
|
if (!UuidUtil.INSTANCE.isUuid(pni)) {
|
||||||
|
throw new InvalidNumberException("Invalid PNI");
|
||||||
|
}
|
||||||
|
return new Pni(UUID.fromString(pni));
|
||||||
|
}
|
||||||
|
|
||||||
if (identifier.startsWith("u:")) {
|
if (identifier.startsWith("u:")) {
|
||||||
return new Username(identifier.substring(2));
|
return new Username(identifier.substring(2));
|
||||||
}
|
}
|
||||||
@ -39,16 +46,15 @@ public sealed interface RecipientIdentifier {
|
|||||||
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
|
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
|
||||||
}
|
}
|
||||||
return new Number(normalizedNumber);
|
return new Number(normalizedNumber);
|
||||||
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
|
|
||||||
throw new InvalidNumberException(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Single fromAddress(RecipientAddress address) {
|
static Single fromAddress(RecipientAddress address) {
|
||||||
if (address.number().isPresent()) {
|
if (address.number().isPresent()) {
|
||||||
return new Number(address.number().get());
|
return new Number(address.number().get());
|
||||||
} else if (address.uuid().isPresent()) {
|
} else if (address.aci().isPresent()) {
|
||||||
return new Uuid(address.uuid().get());
|
return new Uuid(UUID.fromString(address.aci().get()));
|
||||||
|
} else if (address.pni().isPresent()) {
|
||||||
|
return new Pni(UUID.fromString(address.pni().get().substring(4)));
|
||||||
} else if (address.username().isPresent()) {
|
} else if (address.username().isPresent()) {
|
||||||
return new Username(address.username().get());
|
return new Username(address.username().get());
|
||||||
}
|
}
|
||||||
@ -71,6 +77,19 @@ public sealed interface RecipientIdentifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record Pni(UUID pni) implements Single {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return "PNI:" + pni.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecipientAddress toPartialRecipientAddress() {
|
||||||
|
return new RecipientAddress(null, getIdentifier(), null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
record Number(String number) implements Single {
|
record Number(String number) implements Single {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -80,7 +99,7 @@ public sealed interface RecipientIdentifier {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecipientAddress toPartialRecipientAddress() {
|
public RecipientAddress toPartialRecipientAddress() {
|
||||||
return new RecipientAddress(null, number);
|
return new RecipientAddress(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +112,7 @@ public sealed interface RecipientIdentifier {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecipientAddress toPartialRecipientAddress() {
|
public RecipientAddress toPartialRecipientAddress() {
|
||||||
return new RecipientAddress(null, null, username);
|
return new RecipientAddress(null, null, null, username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,13 +9,13 @@ public record SendMessageResult(
|
|||||||
boolean isNetworkFailure,
|
boolean isNetworkFailure,
|
||||||
boolean isUnregisteredFailure,
|
boolean isUnregisteredFailure,
|
||||||
boolean isIdentityFailure,
|
boolean isIdentityFailure,
|
||||||
boolean isRateLimitFailure,
|
RateLimitException rateLimitException,
|
||||||
ProofRequiredException proofRequiredFailure,
|
ProofRequiredException proofRequiredFailure,
|
||||||
boolean isInvalidPreKeyFailure
|
boolean isInvalidPreKeyFailure
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static SendMessageResult unregisteredFailure(RecipientAddress address) {
|
public static SendMessageResult unregisteredFailure(RecipientAddress address) {
|
||||||
return new SendMessageResult(address, false, false, true, false, false, null, false);
|
return new SendMessageResult(address, false, false, true, false, null, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SendMessageResult from(
|
public static SendMessageResult from(
|
||||||
@ -23,16 +23,30 @@ public record SendMessageResult(
|
|||||||
RecipientResolver recipientResolver,
|
RecipientResolver recipientResolver,
|
||||||
RecipientAddressResolver addressResolver
|
RecipientAddressResolver addressResolver
|
||||||
) {
|
) {
|
||||||
|
final var rateLimitFailure = sendMessageResult.getRateLimitFailure();
|
||||||
|
final var proofRequiredFailure = sendMessageResult.getProofRequiredFailure();
|
||||||
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
|
||||||
sendMessageResult.getAddress())).toApiRecipientAddress(),
|
sendMessageResult.getAddress())).toApiRecipientAddress(),
|
||||||
sendMessageResult.isSuccess(),
|
sendMessageResult.isSuccess(),
|
||||||
sendMessageResult.isNetworkFailure(),
|
sendMessageResult.isNetworkFailure(),
|
||||||
sendMessageResult.isUnregisteredFailure(),
|
sendMessageResult.isUnregisteredFailure(),
|
||||||
sendMessageResult.getIdentityFailure() != null,
|
sendMessageResult.getIdentityFailure() != null,
|
||||||
sendMessageResult.getRateLimitFailure() != null || sendMessageResult.getProofRequiredFailure() != null,
|
rateLimitFailure == null ? null : RateLimitException.from(rateLimitFailure),
|
||||||
sendMessageResult.getProofRequiredFailure() == null
|
proofRequiredFailure == null ? null : ProofRequiredException.from(proofRequiredFailure),
|
||||||
? null
|
|
||||||
: new ProofRequiredException(sendMessageResult.getProofRequiredFailure()),
|
|
||||||
sendMessageResult.isInvalidPreKeyFailure());
|
sendMessageResult.isInvalidPreKeyFailure());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRateLimitFailure() {
|
||||||
|
return this.rateLimitException != null || this.proofRequiredFailure != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long rateLimitRetryAfterMilliseconds() {
|
||||||
|
if (proofRequiredFailure != null) {
|
||||||
|
return proofRequiredFailure.getRetryAfterMilliseconds();
|
||||||
|
} else if (rateLimitException != null) {
|
||||||
|
return rateLimitException.getRetryAfterMilliseconds();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
|
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
|
||||||
|
|
||||||
@ -19,4 +20,25 @@ public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<S
|
|||||||
.allMatch(identityFailure -> identityFailure)
|
.allMatch(identityFailure -> identityFailure)
|
||||||
&& results.values().stream().mapToInt(List::size).sum() > 0;
|
&& results.values().stream().mapToInt(List::size).sum() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasOnlyRateLimitFailure() {
|
||||||
|
return results.values()
|
||||||
|
.stream()
|
||||||
|
.flatMap(res -> res.stream().map(SendMessageResult::isRateLimitFailure))
|
||||||
|
.allMatch(r -> r) && results.values().stream().mapToInt(List::size).sum() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Longest rate-limit retry-after window across all rate-limited recipients, in milliseconds.
|
||||||
|
* Null when no recipient reported one (server omitted Retry-After, or no rate-limit failures).
|
||||||
|
*/
|
||||||
|
public Long maxRateLimitRetryAfterMilliseconds() {
|
||||||
|
return results.values()
|
||||||
|
.stream()
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.map(SendMessageResult::rateLimitRetryAfterMilliseconds)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.max(Long::compareTo)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
import org.signal.core.util.Hex;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class StickerPackId {
|
public class StickerPackId {
|
||||||
@ -32,4 +34,9 @@ public class StickerPackId {
|
|||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Arrays.hashCode(id);
|
return Arrays.hashCode(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "StickerPackId{" + Hex.toStringCondensed(id) + '}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
import org.signal.core.util.Hex;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@ -11,10 +11,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
|
|
||||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||||
|
|
||||||
public final class StickerPackUrl {
|
public record StickerPackUrl(StickerPackId packId, byte[] packKey) {
|
||||||
|
|
||||||
private final StickerPackId packId;
|
|
||||||
private final byte[] packKey;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws InvalidStickerPackLinkException If url cannot be parsed.
|
* @throws InvalidStickerPackLinkException If url cannot be parsed.
|
||||||
@ -48,11 +45,6 @@ public final class StickerPackUrl {
|
|||||||
return new StickerPackUrl(packId, packKey);
|
return new StickerPackUrl(packId, packKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StickerPackUrl(final StickerPackId packId, final byte[] packKey) {
|
|
||||||
this.packId = packId;
|
|
||||||
this.packKey = packKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public URI getUrl() {
|
public URI getUrl() {
|
||||||
try {
|
try {
|
||||||
return new URI("https",
|
return new URI("https",
|
||||||
@ -67,15 +59,7 @@ public final class StickerPackUrl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public StickerPackId getPackId() {
|
public static final class InvalidStickerPackLinkException extends Exception {
|
||||||
return packId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getPackKey() {
|
|
||||||
return packKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final static class InvalidStickerPackLinkException extends Exception {
|
|
||||||
|
|
||||||
public InvalidStickerPackLinkException(String message) {
|
public InvalidStickerPackLinkException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
|
||||||
|
|
||||||
public enum TrustLevel {
|
public enum TrustLevel {
|
||||||
UNTRUSTED,
|
UNTRUSTED,
|
||||||
@ -17,14 +16,6 @@ public enum TrustLevel {
|
|||||||
return TrustLevel.cachedValues[i];
|
return TrustLevel.cachedValues[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) {
|
|
||||||
return switch (identityState) {
|
|
||||||
case DEFAULT -> TRUSTED_UNVERIFIED;
|
|
||||||
case UNVERIFIED -> UNTRUSTED;
|
|
||||||
case VERIFIED -> TRUSTED_VERIFIED;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) {
|
public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) {
|
||||||
return switch (verifiedState) {
|
return switch (verifiedState) {
|
||||||
case DEFAULT -> TRUSTED_UNVERIFIED;
|
case DEFAULT -> TRUSTED_UNVERIFIED;
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record TurnServer(
|
||||||
|
String username, String password, List<String> urls
|
||||||
|
) {}
|
||||||
@ -19,6 +19,8 @@ public class UpdateGroup {
|
|||||||
private final String avatarFile;
|
private final String avatarFile;
|
||||||
private final Integer expirationTimer;
|
private final Integer expirationTimer;
|
||||||
private final Boolean isAnnouncementGroup;
|
private final Boolean isAnnouncementGroup;
|
||||||
|
private final String labelEmoji;
|
||||||
|
private final String labelString;
|
||||||
|
|
||||||
private UpdateGroup(final Builder builder) {
|
private UpdateGroup(final Builder builder) {
|
||||||
name = builder.name;
|
name = builder.name;
|
||||||
@ -36,6 +38,8 @@ public class UpdateGroup {
|
|||||||
avatarFile = builder.avatarFile;
|
avatarFile = builder.avatarFile;
|
||||||
expirationTimer = builder.expirationTimer;
|
expirationTimer = builder.expirationTimer;
|
||||||
isAnnouncementGroup = builder.isAnnouncementGroup;
|
isAnnouncementGroup = builder.isAnnouncementGroup;
|
||||||
|
labelEmoji = builder.labelEmoji;
|
||||||
|
labelString = builder.labelString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder newBuilder() {
|
public static Builder newBuilder() {
|
||||||
@ -57,7 +61,9 @@ public class UpdateGroup {
|
|||||||
copy.editDetailsPermission,
|
copy.editDetailsPermission,
|
||||||
copy.avatarFile,
|
copy.avatarFile,
|
||||||
copy.expirationTimer,
|
copy.expirationTimer,
|
||||||
copy.isAnnouncementGroup);
|
copy.isAnnouncementGroup,
|
||||||
|
copy.labelEmoji,
|
||||||
|
copy.labelString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder newBuilder(
|
public static Builder newBuilder(
|
||||||
@ -75,7 +81,9 @@ public class UpdateGroup {
|
|||||||
final GroupPermission editDetailsPermission,
|
final GroupPermission editDetailsPermission,
|
||||||
final String avatarFile,
|
final String avatarFile,
|
||||||
final Integer expirationTimer,
|
final Integer expirationTimer,
|
||||||
final Boolean isAnnouncementGroup
|
final Boolean isAnnouncementGroup,
|
||||||
|
final String labelEmoji,
|
||||||
|
final String labelString
|
||||||
) {
|
) {
|
||||||
return new Builder(name,
|
return new Builder(name,
|
||||||
description,
|
description,
|
||||||
@ -91,7 +99,9 @@ public class UpdateGroup {
|
|||||||
editDetailsPermission,
|
editDetailsPermission,
|
||||||
avatarFile,
|
avatarFile,
|
||||||
expirationTimer,
|
expirationTimer,
|
||||||
isAnnouncementGroup);
|
isAnnouncementGroup,
|
||||||
|
labelEmoji,
|
||||||
|
labelString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@ -154,6 +164,14 @@ public class UpdateGroup {
|
|||||||
return isAnnouncementGroup;
|
return isAnnouncementGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getLabelEmoji() {
|
||||||
|
return labelEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabelString() {
|
||||||
|
return labelString;
|
||||||
|
}
|
||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
@ -171,6 +189,8 @@ public class UpdateGroup {
|
|||||||
private String avatarFile;
|
private String avatarFile;
|
||||||
private Integer expirationTimer;
|
private Integer expirationTimer;
|
||||||
private Boolean isAnnouncementGroup;
|
private Boolean isAnnouncementGroup;
|
||||||
|
private String labelEmoji;
|
||||||
|
private String labelString;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
}
|
}
|
||||||
@ -190,7 +210,9 @@ public class UpdateGroup {
|
|||||||
final GroupPermission editDetailsPermission,
|
final GroupPermission editDetailsPermission,
|
||||||
final String avatarFile,
|
final String avatarFile,
|
||||||
final Integer expirationTimer,
|
final Integer expirationTimer,
|
||||||
final Boolean isAnnouncementGroup
|
final Boolean isAnnouncementGroup,
|
||||||
|
final String labelEmoji,
|
||||||
|
final String labelString
|
||||||
) {
|
) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
@ -207,6 +229,8 @@ public class UpdateGroup {
|
|||||||
this.avatarFile = avatarFile;
|
this.avatarFile = avatarFile;
|
||||||
this.expirationTimer = expirationTimer;
|
this.expirationTimer = expirationTimer;
|
||||||
this.isAnnouncementGroup = isAnnouncementGroup;
|
this.isAnnouncementGroup = isAnnouncementGroup;
|
||||||
|
this.labelEmoji = labelEmoji;
|
||||||
|
this.labelString = labelString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withName(final String val) {
|
public Builder withName(final String val) {
|
||||||
@ -284,6 +308,16 @@ public class UpdateGroup {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withLabelEmoji(final String val) {
|
||||||
|
labelEmoji = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withLabelString(final String val) {
|
||||||
|
labelString = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public UpdateGroup build() {
|
public UpdateGroup build() {
|
||||||
return new UpdateGroup(this);
|
return new UpdateGroup(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
import org.signal.core.util.Base64;
|
||||||
|
import org.signal.core.util.UuidUtil;
|
||||||
|
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class UsernameLinkUrl {
|
||||||
|
|
||||||
|
private static final Pattern URL_REGEX = Pattern.compile("(https://)?signal.me/?#eu/([a-zA-Z0-9+\\-_/]+)");
|
||||||
|
|
||||||
|
private static final String BASE_URL = "https://signal.me/#eu/";
|
||||||
|
|
||||||
|
private final String url;
|
||||||
|
private final UsernameLinkComponents usernameLinkComponents;
|
||||||
|
|
||||||
|
public static UsernameLinkUrl fromUri(String url) throws InvalidUsernameLinkException {
|
||||||
|
final var matcher = URL_REGEX.matcher(url);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
throw new InvalidUsernameLinkException("Invalid username link");
|
||||||
|
}
|
||||||
|
final var path = matcher.group(2);
|
||||||
|
final byte[] allBytes;
|
||||||
|
try {
|
||||||
|
allBytes = Base64.decode(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new InvalidUsernameLinkException("Invalid base64 encoding");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allBytes.length != 48) {
|
||||||
|
throw new InvalidUsernameLinkException("Invalid username link");
|
||||||
|
}
|
||||||
|
|
||||||
|
final var entropy = Arrays.copyOfRange(allBytes, 0, 32);
|
||||||
|
final var serverId = Arrays.copyOfRange(allBytes, 32, allBytes.length);
|
||||||
|
final var serverIdUuid = UuidUtil.INSTANCE.parseOrNull(serverId);
|
||||||
|
if (serverIdUuid == null) {
|
||||||
|
throw new InvalidUsernameLinkException("Invalid serverId");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UsernameLinkUrl(new UsernameLinkComponents(entropy, serverIdUuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UsernameLinkUrl(UsernameLinkComponents usernameLinkComponents) {
|
||||||
|
this.usernameLinkComponents = usernameLinkComponents;
|
||||||
|
this.url = createUrl(usernameLinkComponents);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createUrl(UsernameLinkComponents usernameLinkComponents) {
|
||||||
|
final var entropy = usernameLinkComponents.getEntropy();
|
||||||
|
final var serverId = UuidUtil.toByteArray(usernameLinkComponents.getServerId());
|
||||||
|
|
||||||
|
final var combined = new byte[entropy.length + serverId.length];
|
||||||
|
System.arraycopy(entropy, 0, combined, 0, entropy.length);
|
||||||
|
System.arraycopy(serverId, 0, combined, entropy.length, serverId.length);
|
||||||
|
|
||||||
|
final var base64 = Base64.encodeUrlSafeWithoutPadding(combined);
|
||||||
|
return BASE_URL + base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UsernameLinkComponents getComponents() {
|
||||||
|
return usernameLinkComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class InvalidUsernameLinkException extends Exception {
|
||||||
|
|
||||||
|
public InvalidUsernameLinkException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidUsernameLinkException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record UsernameStatus(String username, UUID uuid, boolean unrestrictedUnidentifiedAccess) {}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
public class VerificationMethodNotAvailableException extends Exception {
|
||||||
|
|
||||||
|
public VerificationMethodNotAvailableException() {
|
||||||
|
super("Invalid verification method");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package org.asamk.signal.manager.config;
|
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
class IasTrustStore implements TrustStore {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputStream getKeyStoreInputStream() {
|
|
||||||
return IasTrustStore.class.getResourceAsStream("ias.store");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getKeyStorePassword() {
|
|
||||||
return "whisper";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +1,3 @@
|
|||||||
package org.asamk.signal.manager.config;
|
package org.asamk.signal.manager.config;
|
||||||
|
|
||||||
public class KeyBackupConfig {
|
public record KeyBackupConfig(String enclaveName, byte[] serviceId, String mrenclave) {}
|
||||||
|
|
||||||
private final String enclaveName;
|
|
||||||
private final byte[] serviceId;
|
|
||||||
private final String mrenclave;
|
|
||||||
|
|
||||||
public KeyBackupConfig(final String enclaveName, final byte[] serviceId, final String mrenclave) {
|
|
||||||
this.enclaveName = enclaveName;
|
|
||||||
this.serviceId = serviceId;
|
|
||||||
this.mrenclave = mrenclave;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEnclaveName() {
|
|
||||||
return enclaveName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getServiceId() {
|
|
||||||
return serviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMrenclave() {
|
|
||||||
return mrenclave;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,21 +1,19 @@
|
|||||||
package org.asamk.signal.manager.config;
|
package org.asamk.signal.manager.config;
|
||||||
|
|
||||||
import org.bouncycastle.util.encoders.Hex;
|
import org.signal.libsignal.net.Network.Environment;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
import org.signal.network.config.HttpProxy;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
import org.signal.network.config.SignalCdnUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
import org.signal.network.config.SignalCdsiUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
|
import org.signal.network.config.SignalProxy;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
import org.signal.network.config.SignalServiceConfiguration;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
import org.signal.network.config.SignalServiceUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
import org.signal.network.config.SignalStorageUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
import org.signal.network.config.SignalSvr2Url;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
import org.signal.network.config.TrustStore;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -23,39 +21,41 @@ import java.util.Optional;
|
|||||||
import okhttp3.Dns;
|
import okhttp3.Dns;
|
||||||
import okhttp3.Interceptor;
|
import okhttp3.Interceptor;
|
||||||
|
|
||||||
|
import static org.asamk.signal.manager.api.ServiceEnvironment.LIVE;
|
||||||
|
|
||||||
class LiveConfig {
|
class LiveConfig {
|
||||||
|
|
||||||
private final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||||
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
|
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
|
||||||
private final static String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
||||||
private final static String SVR2_MRENCLAVE = "6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094";
|
.decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6");
|
||||||
|
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||||
|
private static final String SVR2_MRENCLAVE = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4";
|
||||||
|
private static final String SVR2_MRENCLAVE_LEGACY = "1240acbd4aa26974184844c8a46b1022d3957ac8a76c1fd8f5b1a15141ee0708";
|
||||||
|
|
||||||
private final static String KEY_BACKUP_ENCLAVE_NAME = "e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5";
|
private static final String URL = "https://chat.signal.org";
|
||||||
private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode(
|
private static final String CDN_URL = "https://cdn.signal.org";
|
||||||
"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89");
|
private static final String CDN2_URL = "https://cdn2.signal.org";
|
||||||
private final static String KEY_BACKUP_MRENCLAVE = "45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c";
|
private static final String CDN3_URL = "https://cdn3.signal.org";
|
||||||
private final static String FALLBACK_KEY_BACKUP_ENCLAVE_NAME = "0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f";
|
private static final String STORAGE_URL = "https://storage.signal.org";
|
||||||
private final static byte[] FALLBACK_KEY_BACKUP_SERVICE_ID = Hex.decode(
|
private static final String SIGNAL_CDSI_URL = "https://cdsi.signal.org";
|
||||||
"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39");
|
private static final String SIGNAL_SVR2_URL = "https://svr2.signal.org";
|
||||||
private final static String FALLBACK_KEY_BACKUP_MRENCLAVE = "ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba";
|
private static final TrustStore TRUST_STORE = new WhisperTrustStore();
|
||||||
|
|
||||||
private final static String URL = "https://chat.signal.org";
|
private static final Optional<Dns> dns = Optional.empty();
|
||||||
private final static String CDN_URL = "https://cdn.signal.org";
|
private static final Optional<SignalProxy> proxy = Optional.empty();
|
||||||
private final static String CDN2_URL = "https://cdn2.signal.org";
|
private static final Optional<HttpProxy> systemProxy = Optional.empty();
|
||||||
private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org";
|
|
||||||
private final static String STORAGE_URL = "https://storage.signal.org";
|
|
||||||
private final static String SIGNAL_CDSI_URL = "https://cdsi.signal.org";
|
|
||||||
private final static String SIGNAL_SVR2_URL = "https://svr2.signal.org";
|
|
||||||
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
|
|
||||||
|
|
||||||
private final static Optional<Dns> dns = Optional.empty();
|
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||||
private final static Optional<SignalProxy> proxy = Optional.empty();
|
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==");
|
||||||
|
private static final byte[] genericServerPublicParams = Base64.getDecoder()
|
||||||
private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
|
||||||
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P");
|
|
||||||
private final static byte[] genericServerPublicParams = Base64.getDecoder()
|
|
||||||
.decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN");
|
.decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN");
|
||||||
|
|
||||||
|
private static final byte[] backupServerPublicParams = Base64.getDecoder()
|
||||||
|
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
|
||||||
|
|
||||||
|
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
|
||||||
|
|
||||||
static SignalServiceConfiguration createDefaultServiceConfiguration(
|
static SignalServiceConfiguration createDefaultServiceConfiguration(
|
||||||
final List<Interceptor> interceptors
|
final List<Interceptor> interceptors
|
||||||
) {
|
) {
|
||||||
@ -63,42 +63,38 @@ class LiveConfig {
|
|||||||
Map.of(0,
|
Map.of(0,
|
||||||
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
|
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
|
||||||
2,
|
2,
|
||||||
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
|
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)},
|
||||||
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
|
3,
|
||||||
|
new SignalCdnUrl[]{new SignalCdnUrl(CDN3_URL, TRUST_STORE)}),
|
||||||
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
|
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
|
||||||
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
|
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
|
||||||
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
|
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
|
||||||
interceptors,
|
interceptors,
|
||||||
dns,
|
dns,
|
||||||
proxy,
|
proxy,
|
||||||
|
systemProxy,
|
||||||
zkGroupServerPublicParams,
|
zkGroupServerPublicParams,
|
||||||
genericServerPublicParams);
|
genericServerPublicParams,
|
||||||
|
backupServerPublicParams,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
static List<ECPublicKey> getUnidentifiedSenderTrustRoots() {
|
||||||
try {
|
try {
|
||||||
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
|
return List.of(new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT),
|
||||||
|
new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT2));
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static KeyBackupConfig createKeyBackupConfig() {
|
static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) {
|
||||||
return new KeyBackupConfig(KEY_BACKUP_ENCLAVE_NAME, KEY_BACKUP_SERVICE_ID, KEY_BACKUP_MRENCLAVE);
|
return new ServiceEnvironmentConfig(LIVE,
|
||||||
}
|
LIBSIGNAL_NET_ENV,
|
||||||
|
createDefaultServiceConfiguration(interceptors),
|
||||||
static Collection<KeyBackupConfig> createFallbackKeyBackupConfigs() {
|
getUnidentifiedSenderTrustRoots(),
|
||||||
return List.of(new KeyBackupConfig(FALLBACK_KEY_BACKUP_ENCLAVE_NAME,
|
CDSI_MRENCLAVE,
|
||||||
FALLBACK_KEY_BACKUP_SERVICE_ID,
|
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
||||||
FALLBACK_KEY_BACKUP_MRENCLAVE));
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getCdsiMrenclave() {
|
|
||||||
return CDSI_MRENCLAVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getSvr2Mrenclave() {
|
|
||||||
return SVR2_MRENCLAVE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LiveConfig() {
|
private LiveConfig() {
|
||||||
|
|||||||
@ -3,55 +3,47 @@ package org.asamk.signal.manager.config;
|
|||||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||||
import org.signal.libsignal.protocol.util.Medium;
|
import org.signal.libsignal.protocol.util.Medium;
|
||||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import okhttp3.Interceptor;
|
import okhttp3.Interceptor;
|
||||||
|
|
||||||
public class ServiceConfig {
|
public class ServiceConfig {
|
||||||
|
|
||||||
public final static int PREKEY_MINIMUM_COUNT = 10;
|
public static final int PREKEY_MINIMUM_COUNT = 10;
|
||||||
public final static int PREKEY_BATCH_SIZE = 100;
|
public static final int PREKEY_BATCH_SIZE = 100;
|
||||||
public final static int PREKEY_MAXIMUM_ID = Medium.MAX_VALUE;
|
public static final int PREKEY_MAXIMUM_ID = Medium.MAX_VALUE;
|
||||||
public final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
|
public static final long PREKEY_ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30);
|
||||||
public final static long MAX_ENVELOPE_SIZE = 0;
|
public static final long PREKEY_STALE_AGE = TimeUnit.DAYS.toMillis(90);
|
||||||
public final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
|
public static final long SIGNED_PREKEY_ROTATE_AGE = TimeUnit.DAYS.toMillis(2);
|
||||||
public final static boolean AUTOMATIC_NETWORK_RETRY = true;
|
|
||||||
public final static int GROUP_MAX_SIZE = 1001;
|
|
||||||
|
|
||||||
private final static KeyStore iasKeyStore;
|
public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
|
||||||
|
public static final long MAX_ENVELOPE_SIZE = 0;
|
||||||
static {
|
public static final int MAX_INCREMENTAL_MACS_PER_ENVELOPE = 10;
|
||||||
try {
|
public static final int MAX_MESSAGE_SIZE_BYTES = 2000;
|
||||||
TrustStore contactTrustStore = new IasTrustStore();
|
public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
|
||||||
|
public static final boolean AUTOMATIC_NETWORK_RETRY = true;
|
||||||
var keyStore = KeyStore.getInstance("BKS");
|
public static final int GROUP_MAX_SIZE = 1001;
|
||||||
keyStore.load(contactTrustStore.getKeyStoreInputStream(),
|
public static final int MAXIMUM_ONE_OFF_REQUEST_SIZE = 3;
|
||||||
contactTrustStore.getKeyStorePassword().toCharArray());
|
public static final long UNREGISTERED_LIFESPAN = TimeUnit.DAYS.toMillis(30);
|
||||||
|
|
||||||
iasKeyStore = keyStore;
|
|
||||||
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
||||||
final var giftBadges = !isPrimaryDevice;
|
final var attachmentBackfill = !isPrimaryDevice;
|
||||||
return new AccountAttributes.Capabilities(false, true, true, true, true, giftBadges, false, false);
|
final var spqr = true;
|
||||||
}
|
final var usernameSyncChangeMessage = !isPrimaryDevice;
|
||||||
|
final var optionalPhoneNumber = !isPrimaryDevice;
|
||||||
public static KeyStore getIasKeyStore() {
|
return new AccountAttributes.Capabilities(true,
|
||||||
return iasKeyStore;
|
true,
|
||||||
|
attachmentBackfill,
|
||||||
|
spqr,
|
||||||
|
usernameSyncChangeMessage,
|
||||||
|
optionalPhoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
||||||
ServiceEnvironment serviceEnvironment, String userAgent
|
ServiceEnvironment serviceEnvironment,
|
||||||
|
String userAgent
|
||||||
) {
|
) {
|
||||||
final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
|
final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
@ -61,20 +53,8 @@ public class ServiceConfig {
|
|||||||
final var interceptors = List.of(userAgentInterceptor);
|
final var interceptors = List.of(userAgentInterceptor);
|
||||||
|
|
||||||
return switch (serviceEnvironment) {
|
return switch (serviceEnvironment) {
|
||||||
case LIVE -> new ServiceEnvironmentConfig(serviceEnvironment,
|
case LIVE -> LiveConfig.getServiceEnvironmentConfig(interceptors);
|
||||||
LiveConfig.createDefaultServiceConfiguration(interceptors),
|
case STAGING -> StagingConfig.getServiceEnvironmentConfig(interceptors);
|
||||||
LiveConfig.getUnidentifiedSenderTrustRoot(),
|
|
||||||
LiveConfig.createKeyBackupConfig(),
|
|
||||||
LiveConfig.createFallbackKeyBackupConfigs(),
|
|
||||||
LiveConfig.getCdsiMrenclave(),
|
|
||||||
LiveConfig.getSvr2Mrenclave());
|
|
||||||
case STAGING -> new ServiceEnvironmentConfig(serviceEnvironment,
|
|
||||||
StagingConfig.createDefaultServiceConfiguration(interceptors),
|
|
||||||
StagingConfig.getUnidentifiedSenderTrustRoot(),
|
|
||||||
StagingConfig.createKeyBackupConfig(),
|
|
||||||
StagingConfig.createFallbackKeyBackupConfigs(),
|
|
||||||
StagingConfig.getCdsiMrenclave(),
|
|
||||||
StagingConfig.getSvr2Mrenclave());
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,67 +1,17 @@
|
|||||||
package org.asamk.signal.manager.config;
|
package org.asamk.signal.manager.config;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||||
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
import org.signal.network.config.SignalServiceConfiguration;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.List;
|
||||||
|
|
||||||
public class ServiceEnvironmentConfig {
|
public record ServiceEnvironmentConfig(
|
||||||
|
ServiceEnvironment type,
|
||||||
private final ServiceEnvironment type;
|
Network.Environment netEnvironment,
|
||||||
private final SignalServiceConfiguration signalServiceConfiguration;
|
SignalServiceConfiguration signalServiceConfiguration,
|
||||||
|
List<ECPublicKey> unidentifiedSenderTrustRoots,
|
||||||
private final ECPublicKey unidentifiedSenderTrustRoot;
|
String cdsiMrenclave,
|
||||||
|
List<String> svr2Mrenclaves
|
||||||
private final KeyBackupConfig keyBackupConfig;
|
) {}
|
||||||
private final Collection<KeyBackupConfig> fallbackKeyBackupConfigs;
|
|
||||||
|
|
||||||
private final String cdsiMrenclave;
|
|
||||||
private final String svr2Mrenclave;
|
|
||||||
|
|
||||||
public ServiceEnvironmentConfig(
|
|
||||||
final ServiceEnvironment type,
|
|
||||||
final SignalServiceConfiguration signalServiceConfiguration,
|
|
||||||
final ECPublicKey unidentifiedSenderTrustRoot,
|
|
||||||
final KeyBackupConfig keyBackupConfig,
|
|
||||||
final Collection<KeyBackupConfig> fallbackKeyBackupConfigs,
|
|
||||||
final String cdsiMrenclave,
|
|
||||||
final String svr2Mrenclave
|
|
||||||
) {
|
|
||||||
this.type = type;
|
|
||||||
this.signalServiceConfiguration = signalServiceConfiguration;
|
|
||||||
this.unidentifiedSenderTrustRoot = unidentifiedSenderTrustRoot;
|
|
||||||
this.keyBackupConfig = keyBackupConfig;
|
|
||||||
this.fallbackKeyBackupConfigs = fallbackKeyBackupConfigs;
|
|
||||||
this.cdsiMrenclave = cdsiMrenclave;
|
|
||||||
this.svr2Mrenclave = svr2Mrenclave;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServiceEnvironment getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SignalServiceConfiguration getSignalServiceConfiguration() {
|
|
||||||
return signalServiceConfiguration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ECPublicKey getUnidentifiedSenderTrustRoot() {
|
|
||||||
return unidentifiedSenderTrustRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyBackupConfig getKeyBackupConfig() {
|
|
||||||
return keyBackupConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<KeyBackupConfig> getFallbackKeyBackupConfigs() {
|
|
||||||
return fallbackKeyBackupConfigs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCdsiMrenclave() {
|
|
||||||
return cdsiMrenclave;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSvr2Mrenclave() {
|
|
||||||
return svr2Mrenclave;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,21 +1,19 @@
|
|||||||
package org.asamk.signal.manager.config;
|
package org.asamk.signal.manager.config;
|
||||||
|
|
||||||
import org.bouncycastle.util.encoders.Hex;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
|
||||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
import org.signal.network.config.HttpProxy;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
import org.signal.network.config.SignalCdnUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
import org.signal.network.config.SignalCdsiUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
|
import org.signal.network.config.SignalProxy;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
import org.signal.network.config.SignalServiceConfiguration;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
import org.signal.network.config.SignalServiceUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
import org.signal.network.config.SignalStorageUrl;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
import org.signal.network.config.SignalSvr2Url;
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url;
|
import org.signal.network.config.TrustStore;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -23,39 +21,41 @@ import java.util.Optional;
|
|||||||
import okhttp3.Dns;
|
import okhttp3.Dns;
|
||||||
import okhttp3.Interceptor;
|
import okhttp3.Interceptor;
|
||||||
|
|
||||||
|
import static org.asamk.signal.manager.api.ServiceEnvironment.STAGING;
|
||||||
|
|
||||||
class StagingConfig {
|
class StagingConfig {
|
||||||
|
|
||||||
private final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||||
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
|
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
|
||||||
private final static String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
|
||||||
private final static String SVR2_MRENCLAVE = "a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95";
|
.decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm");
|
||||||
|
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||||
|
private static final String SVR2_MRENCLAVE = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302";
|
||||||
|
private static final String SVR2_MRENCLAVE_LEGACY = "97f151f6ed078edbbfd72fa9cae694dcc08353f1f5e8d9ccd79a971b10ffc535";
|
||||||
|
|
||||||
private final static String KEY_BACKUP_ENCLAVE_NAME = "39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b";
|
private static final String URL = "https://chat.staging.signal.org";
|
||||||
private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode(
|
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
||||||
"ee1d0d972b7ea903615670de43ab1b6e7a825e811c70a29bb5fe0f819e0975fa");
|
private static final String CDN2_URL = "https://cdn2-staging.signal.org";
|
||||||
private final static String KEY_BACKUP_MRENCLAVE = "45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c";
|
private static final String CDN3_URL = "https://cdn3-staging.signal.org";
|
||||||
private final static String FALLBACK_KEY_BACKUP_ENCLAVE_NAME = "dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99";
|
private static final String STORAGE_URL = "https://storage-staging.signal.org";
|
||||||
private final static byte[] FALLBACK_KEY_BACKUP_SERVICE_ID = Hex.decode(
|
private static final String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org";
|
||||||
"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a");
|
private static final String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org";
|
||||||
private final static String FALLBACK_KEY_BACKUP_MRENCLAVE = "ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba";
|
private static final TrustStore TRUST_STORE = new WhisperTrustStore();
|
||||||
|
|
||||||
private final static String URL = "https://chat.staging.signal.org";
|
private static final Optional<Dns> dns = Optional.empty();
|
||||||
private final static String CDN_URL = "https://cdn-staging.signal.org";
|
private static final Optional<SignalProxy> proxy = Optional.empty();
|
||||||
private final static String CDN2_URL = "https://cdn2-staging.signal.org";
|
private static final Optional<HttpProxy> systemProxy = Optional.empty();
|
||||||
private final static String SIGNAL_KEY_BACKUP_URL = "https://api-staging.backup.signal.org";
|
|
||||||
private final static String STORAGE_URL = "https://storage-staging.signal.org";
|
|
||||||
private final static String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org";
|
|
||||||
private final static String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org";
|
|
||||||
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
|
|
||||||
|
|
||||||
private final static Optional<Dns> dns = Optional.empty();
|
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||||
private final static Optional<SignalProxy> proxy = Optional.empty();
|
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==");
|
||||||
|
private static final byte[] genericServerPublicParams = Base64.getDecoder()
|
||||||
private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
|
||||||
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj");
|
|
||||||
private final static byte[] genericServerPublicParams = Base64.getDecoder()
|
|
||||||
.decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N");
|
.decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N");
|
||||||
|
|
||||||
|
private static final byte[] backupServerPublicParams = Base64.getDecoder()
|
||||||
|
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
|
||||||
|
|
||||||
|
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
|
||||||
|
|
||||||
static SignalServiceConfiguration createDefaultServiceConfiguration(
|
static SignalServiceConfiguration createDefaultServiceConfiguration(
|
||||||
final List<Interceptor> interceptors
|
final List<Interceptor> interceptors
|
||||||
) {
|
) {
|
||||||
@ -63,42 +63,38 @@ class StagingConfig {
|
|||||||
Map.of(0,
|
Map.of(0,
|
||||||
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
|
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
|
||||||
2,
|
2,
|
||||||
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}),
|
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)},
|
||||||
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
|
3,
|
||||||
|
new SignalCdnUrl[]{new SignalCdnUrl(CDN3_URL, TRUST_STORE)}),
|
||||||
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
|
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
|
||||||
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
|
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
|
||||||
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
|
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
|
||||||
interceptors,
|
interceptors,
|
||||||
dns,
|
dns,
|
||||||
proxy,
|
proxy,
|
||||||
|
systemProxy,
|
||||||
zkGroupServerPublicParams,
|
zkGroupServerPublicParams,
|
||||||
genericServerPublicParams);
|
genericServerPublicParams,
|
||||||
|
backupServerPublicParams,
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
static List<ECPublicKey> getUnidentifiedSenderTrustRoots() {
|
||||||
try {
|
try {
|
||||||
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
|
return List.of(new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT),
|
||||||
|
new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT2));
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static KeyBackupConfig createKeyBackupConfig() {
|
static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) {
|
||||||
return new KeyBackupConfig(KEY_BACKUP_ENCLAVE_NAME, KEY_BACKUP_SERVICE_ID, KEY_BACKUP_MRENCLAVE);
|
return new ServiceEnvironmentConfig(STAGING,
|
||||||
}
|
LIBSIGNAL_NET_ENV,
|
||||||
|
createDefaultServiceConfiguration(interceptors),
|
||||||
static Collection<KeyBackupConfig> createFallbackKeyBackupConfigs() {
|
getUnidentifiedSenderTrustRoots(),
|
||||||
return List.of(new KeyBackupConfig(FALLBACK_KEY_BACKUP_ENCLAVE_NAME,
|
CDSI_MRENCLAVE,
|
||||||
FALLBACK_KEY_BACKUP_SERVICE_ID,
|
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
|
||||||
FALLBACK_KEY_BACKUP_MRENCLAVE));
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getCdsiMrenclave() {
|
|
||||||
return CDSI_MRENCLAVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getSvr2Mrenclave() {
|
|
||||||
return SVR2_MRENCLAVE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private StagingConfig() {
|
private StagingConfig() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.asamk.signal.manager.config;
|
package org.asamk.signal.manager.config;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
import org.signal.network.config.TrustStore;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
|||||||
public class GroupUtils {
|
public class GroupUtils {
|
||||||
|
|
||||||
public static void setGroupContext(
|
public static void setGroupContext(
|
||||||
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo
|
final SignalServiceDataMessage.Builder messageBuilder,
|
||||||
|
final GroupInfo groupInfo
|
||||||
) {
|
) {
|
||||||
if (groupInfo instanceof GroupInfoV1) {
|
if (groupInfo instanceof GroupInfoV1) {
|
||||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
|
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
|
||||||
public interface AccountFileUpdater {
|
public interface AccountFileUpdater {
|
||||||
|
|
||||||
|
|||||||
@ -3,49 +3,70 @@ package org.asamk.signal.manager.helper;
|
|||||||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
|
||||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||||
|
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||||
import org.asamk.signal.manager.api.PinLockedException;
|
import org.asamk.signal.manager.api.PinLockedException;
|
||||||
import org.asamk.signal.manager.api.RateLimitException;
|
import org.asamk.signal.manager.api.RateLimitException;
|
||||||
|
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||||
|
import org.asamk.signal.manager.jobs.SyncStorageJob;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
import org.asamk.signal.manager.util.Utils;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
|
import org.signal.core.util.Base64;
|
||||||
|
import org.signal.core.util.crypto.DeviceNameCipher;
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
|
import org.signal.libsignal.protocol.NoSessionException;
|
||||||
|
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
|
||||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||||
|
import org.signal.libsignal.protocol.util.KeyHelper;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.signal.libsignal.usernames.Username;
|
import org.signal.libsignal.usernames.Username;
|
||||||
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||||
|
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
|
||||||
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
|
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
|
||||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
|
||||||
import org.whispersystems.util.Base64UrlSafe;
|
import org.whispersystems.signalservice.internal.push.SyncMessage;
|
||||||
|
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okio.ByteString;
|
||||||
|
|
||||||
|
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
|
||||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||||
|
|
||||||
public class AccountHelper {
|
public class AccountHelper {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class);
|
private static final Logger logger = LoggerFactory.getLogger(AccountHelper.class);
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final SignalAccount account;
|
private final SignalAccount account;
|
||||||
@ -64,6 +85,42 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void checkAccountState() throws IOException {
|
public void checkAccountState() throws IOException {
|
||||||
|
if (account.getAci() == null) {
|
||||||
|
account.setRegistered(false);
|
||||||
|
throw new IOException("Account without ACI");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
updateAccountAttributes();
|
||||||
|
if (account.getPreviousStorageVersion() < 9) {
|
||||||
|
context.getPreKeyHelper().forceRefreshPreKeys();
|
||||||
|
} else {
|
||||||
|
context.getPreKeyHelper().refreshPreKeysIfNecessary();
|
||||||
|
}
|
||||||
|
if (account.getPni() == null) {
|
||||||
|
checkWhoAmiI();
|
||||||
|
}
|
||||||
|
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
||||||
|
throw new IOException("Missing PNI identity key, relinking required");
|
||||||
|
}
|
||||||
|
if (account.getPreviousStorageVersion() < 10
|
||||||
|
&& account.isPrimaryDevice()
|
||||||
|
&& account.getRegistrationLockPin() != null) {
|
||||||
|
migrateRegistrationPin();
|
||||||
|
}
|
||||||
|
if (account.getUsername() != null && account.getUsernameLink() == null) {
|
||||||
|
try {
|
||||||
|
tryToSetUsernameLink(new Username(account.getUsername()));
|
||||||
|
} catch (BaseUsernameException e) {
|
||||||
|
logger.debug("Invalid local username");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (DeprecatedVersionException e) {
|
||||||
|
logger.debug("Signal-Server returned deprecated version exception", e);
|
||||||
|
throw e;
|
||||||
|
} catch (AuthorizationFailedException e) {
|
||||||
|
account.setRegistered(false);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
if (account.getLastReceiveTimestamp() == 0) {
|
if (account.getLastReceiveTimestamp() == 0) {
|
||||||
logger.info("The Signal protocol expects that incoming messages are regularly received.");
|
logger.info("The Signal protocol expects that incoming messages are regularly received.");
|
||||||
} else {
|
} else {
|
||||||
@ -75,27 +132,6 @@ public class AccountHelper {
|
|||||||
days);
|
days);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
updateAccountAttributes();
|
|
||||||
context.getPreKeyHelper().refreshPreKeysIfNecessary();
|
|
||||||
if (account.getAci() == null || account.getPni() == null) {
|
|
||||||
checkWhoAmiI();
|
|
||||||
}
|
|
||||||
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
|
||||||
context.getSyncHelper().requestSyncPniIdentity();
|
|
||||||
}
|
|
||||||
if (account.getPreviousStorageVersion() < 4
|
|
||||||
&& account.isPrimaryDevice()
|
|
||||||
&& account.getRegistrationLockPin() != null) {
|
|
||||||
migrateRegistrationPin();
|
|
||||||
}
|
|
||||||
} catch (DeprecatedVersionException e) {
|
|
||||||
logger.debug("Signal-Server returned deprecated version exception", e);
|
|
||||||
throw e;
|
|
||||||
} catch (AuthorizationFailedException e) {
|
|
||||||
account.setRegistered(false);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkWhoAmiI() throws IOException {
|
public void checkWhoAmiI() throws IOException {
|
||||||
@ -118,11 +154,11 @@ public class AccountHelper {
|
|||||||
account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
|
account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
|
||||||
}
|
}
|
||||||
account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
||||||
// TODO check and update remote storage
|
|
||||||
context.getUnidentifiedAccessHelper().rotateSenderCertificates();
|
context.getUnidentifiedAccessHelper().rotateSenderCertificates();
|
||||||
dependencies.resetAfterAddressChange();
|
dependencies.resetAfterAddressChange();
|
||||||
context.getGroupV2Helper().clearAuthCredentialCache();
|
context.getGroupV2Helper().clearAuthCredentialCache();
|
||||||
context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
|
context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
|
||||||
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPni(
|
public void setPni(
|
||||||
@ -139,87 +175,290 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void startChangeNumber(
|
public void startChangeNumber(
|
||||||
String newNumber, String captcha, boolean voiceVerification
|
String newNumber,
|
||||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
|
boolean voiceVerification,
|
||||||
|
String captcha
|
||||||
|
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
|
||||||
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
|
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
|
||||||
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
|
final var registrationApi = accountManager.getRegistrationApi();
|
||||||
|
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
|
||||||
account.getSessionId(newNumber),
|
account.getSessionId(newNumber),
|
||||||
id -> account.setSessionId(newNumber, id),
|
id -> account.setSessionId(newNumber, id),
|
||||||
voiceVerification,
|
voiceVerification,
|
||||||
captcha);
|
captcha);
|
||||||
NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
|
NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void finishChangeNumber(
|
public void finishChangeNumber(
|
||||||
String newNumber, String verificationCode, String pin
|
String newNumber,
|
||||||
) throws IncorrectPinException, PinLockedException, IOException {
|
String verificationCode,
|
||||||
// TODO create new PNI identity key
|
String pin
|
||||||
final List<OutgoingPushMessage> deviceMessages = null;
|
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
|
||||||
final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
|
for (var attempts = 0; attempts < 5; attempts++) {
|
||||||
final Map<String, KyberPreKeyEntity> devicePniLastResortKyberPrekeys = null;
|
try {
|
||||||
final Map<String, Integer> pniRegistrationIds = null;
|
finishChangeNumberInternal(newNumber, verificationCode, pin);
|
||||||
var sessionId = account.getSessionId(account.getNumber());
|
break;
|
||||||
|
} catch (MismatchedDevicesException e) {
|
||||||
|
logger.debug("Change number failed with mismatched devices, retrying.");
|
||||||
|
try {
|
||||||
|
dependencies.getMessageSender().handleChangeNumberMismatchDevices(e.getMismatchedDevices());
|
||||||
|
} catch (UntrustedIdentityException ex) {
|
||||||
|
throw new AssertionError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishChangeNumberInternal(
|
||||||
|
String newNumber,
|
||||||
|
String verificationCode,
|
||||||
|
String pin
|
||||||
|
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
|
||||||
|
final var pniIdentity = KeyUtils.generateIdentityKeyPair();
|
||||||
|
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
|
||||||
|
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
|
||||||
|
final var devicePniLastResortKyberPreKeys = new HashMap<Integer, KyberPreKeyEntity>();
|
||||||
|
final var pniRegistrationIds = new HashMap<Integer, Integer>();
|
||||||
|
|
||||||
|
final var selfDeviceId = account.getDeviceId();
|
||||||
|
SyncMessage.PniChangeNumber selfChangeNumber = null;
|
||||||
|
|
||||||
|
final var deviceIds = new ArrayList<Integer>();
|
||||||
|
deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||||
|
final var aci = account.getAci();
|
||||||
|
final var accountDataStore = account.getSignalServiceDataStore().aci();
|
||||||
|
final var subDeviceSessions = accountDataStore.getSubDeviceSessions(aci.toString())
|
||||||
|
.stream()
|
||||||
|
.filter(deviceId -> accountDataStore.containsSession(new SignalProtocolAddress(aci.toString(),
|
||||||
|
deviceId)))
|
||||||
|
.toList();
|
||||||
|
deviceIds.addAll(subDeviceSessions);
|
||||||
|
|
||||||
|
final var messageSender = dependencies.getMessageSender();
|
||||||
|
for (final var deviceId : deviceIds) {
|
||||||
|
// Signed Prekey
|
||||||
|
final SignedPreKeyRecord signedPreKeyRecord;
|
||||||
|
try {
|
||||||
|
signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
|
||||||
|
pniIdentity.getPrivateKey());
|
||||||
|
final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
|
||||||
|
signedPreKeyRecord.getKeyPair().getPublicKey(),
|
||||||
|
signedPreKeyRecord.getSignature());
|
||||||
|
devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new AssertionError("unexpected invalid key", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last-resort kyber prekey
|
||||||
|
final KyberPreKeyRecord lastResortKyberPreKeyRecord;
|
||||||
|
try {
|
||||||
|
lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
|
||||||
|
pniIdentity.getPrivateKey());
|
||||||
|
final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
|
||||||
|
lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
|
||||||
|
lastResortKyberPreKeyRecord.getSignature());
|
||||||
|
devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new AssertionError("unexpected invalid key", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration Id
|
||||||
|
var pniRegistrationId = -1;
|
||||||
|
while (pniRegistrationId < 0 || pniRegistrationIds.containsValue(pniRegistrationId)) {
|
||||||
|
pniRegistrationId = KeyHelper.generateRegistrationId(false);
|
||||||
|
}
|
||||||
|
pniRegistrationIds.put(deviceId, pniRegistrationId);
|
||||||
|
|
||||||
|
// Device Message
|
||||||
|
final var pniChangeNumber = new SyncMessage.PniChangeNumber.Builder().identityKeyPair(ByteString.of(
|
||||||
|
pniIdentity.serialize()))
|
||||||
|
.signedPreKey(ByteString.of(signedPreKeyRecord.serialize()))
|
||||||
|
.lastResortKyberPreKey(ByteString.of(lastResortKyberPreKeyRecord.serialize()))
|
||||||
|
.registrationId(pniRegistrationId)
|
||||||
|
.newE164(newNumber)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if (deviceId == selfDeviceId) {
|
||||||
|
selfChangeNumber = pniChangeNumber;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
|
||||||
|
pniChangeNumber);
|
||||||
|
encryptedDeviceMessages.add(message);
|
||||||
|
} catch (UntrustedIdentityException | IOException | InvalidKeyException | NoSessionException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var sessionId = account.getSessionId(newNumber);
|
||||||
|
if (sessionId == null) {
|
||||||
|
throw new IOException("No change number verification session active");
|
||||||
|
}
|
||||||
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
final var result = NumberVerificationUtils.verifyNumber(sessionId,
|
||||||
verificationCode,
|
verificationCode,
|
||||||
pin,
|
pin,
|
||||||
context.getPinHelper(),
|
context.getPinHelper(),
|
||||||
(sessionId1, verificationCode1, registrationLock) -> {
|
(sessionId1, verificationCode1, registrationLock) -> {
|
||||||
final var accountManager = dependencies.getAccountManager();
|
final var registrationApi = dependencies.getRegistrationApi();
|
||||||
|
final var accountApi = dependencies.getAccountApi();
|
||||||
try {
|
try {
|
||||||
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1));
|
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
|
||||||
} catch (AlreadyVerifiedException e) {
|
} catch (AlreadyVerifiedException e) {
|
||||||
// Already verified so can continue changing number
|
// Already verified so can continue changing number
|
||||||
}
|
}
|
||||||
return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
|
return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
|
||||||
sessionId1,
|
|
||||||
null,
|
null,
|
||||||
newNumber,
|
newNumber,
|
||||||
registrationLock,
|
registrationLock,
|
||||||
account.getPniIdentityKeyPair().getPublicKey(),
|
pniIdentity.getPublicKey(),
|
||||||
deviceMessages,
|
encryptedDeviceMessages,
|
||||||
devicePniSignedPreKeys,
|
Utils.mapKeys(devicePniSignedPreKeys, Object::toString),
|
||||||
devicePniLastResortKyberPrekeys,
|
Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString),
|
||||||
pniRegistrationIds)));
|
Utils.mapKeys(pniRegistrationIds, Object::toString))));
|
||||||
});
|
});
|
||||||
// TODO handle response
|
|
||||||
updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni()));
|
account.clearSessionId();
|
||||||
|
final var updatePni = PNI.parseOrThrow(result.first().getPni());
|
||||||
|
if (updatePni.equals(account.getPni())) {
|
||||||
|
logger.debug("PNI is unchanged after change number");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePniChangeNumberMessage(selfChangeNumber, updatePni, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handlePniChangeNumberMessage(
|
||||||
|
final SyncMessage.PniChangeNumber pniChangeNumber,
|
||||||
|
final PNI updatedPni,
|
||||||
|
final boolean forcePniPreKeyRotation
|
||||||
|
) {
|
||||||
|
if (pniChangeNumber.identityKeyPair != null
|
||||||
|
&& pniChangeNumber.registrationId != null
|
||||||
|
&& pniChangeNumber.signedPreKey != null) {
|
||||||
|
logger.debug("New PNI: {}", updatedPni);
|
||||||
|
try {
|
||||||
|
setPni(updatedPni,
|
||||||
|
new IdentityKeyPair(pniChangeNumber.identityKeyPair.toByteArray()),
|
||||||
|
pniChangeNumber.newE164,
|
||||||
|
pniChangeNumber.registrationId,
|
||||||
|
new SignedPreKeyRecord(pniChangeNumber.signedPreKey.toByteArray()),
|
||||||
|
pniChangeNumber.lastResortKyberPreKey != null
|
||||||
|
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
|
||||||
|
: null);
|
||||||
|
if (forcePniPreKeyRotation) {
|
||||||
|
try {
|
||||||
|
context.getPreKeyHelper().forceRefreshPreKeys(ServiceIdType.PNI);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to force refresh PNI pre keys after PNI change sync", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to handle change number message", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final int USERNAME_MIN_LENGTH = 3;
|
public static final int USERNAME_MIN_LENGTH = 3;
|
||||||
public static final int USERNAME_MAX_LENGTH = 32;
|
public static final int USERNAME_MAX_LENGTH = 32;
|
||||||
|
|
||||||
public String reserveUsername(String nickname) throws IOException, BaseUsernameException {
|
public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException {
|
||||||
final var currentUsername = account.getUsername();
|
final var currentUsername = account.getUsername();
|
||||||
if (currentUsername != null) {
|
if (currentUsername != null) {
|
||||||
final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
|
final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
|
||||||
if (currentNickname.equals(nickname)) {
|
if (currentNickname.equals(nickname)) {
|
||||||
|
try {
|
||||||
refreshCurrentUsername();
|
refreshCurrentUsername();
|
||||||
return currentUsername;
|
return;
|
||||||
|
} catch (IOException | BaseUsernameException e) {
|
||||||
|
logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
|
final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
|
||||||
final var candidateHashes = new ArrayList<String>();
|
reserveUsername(candidates);
|
||||||
for (final var candidate : candidates) {
|
|
||||||
candidateHashes.add(Base64UrlSafe.encodeBytesWithoutPadding(candidate.getHash()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
|
public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
|
||||||
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
final var currentUsername = account.getUsername();
|
||||||
|
if (currentUsername != null) {
|
||||||
|
if (currentUsername.equals(username)) {
|
||||||
|
try {
|
||||||
|
refreshCurrentUsername();
|
||||||
|
return;
|
||||||
|
} catch (IOException | BaseUsernameException e) {
|
||||||
|
logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var candidates = List.of(new Username(username));
|
||||||
|
reserveUsername(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reserveUsername(final List<Username> candidates) throws IOException {
|
||||||
|
final var candidateHashes = new ArrayList<byte[]>();
|
||||||
|
for (final var candidate : candidates) {
|
||||||
|
candidateHashes.add(candidate.getHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
final var usernameHash = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||||
|
final var hashIndex = candidateHashes.stream()
|
||||||
|
.filter(candidateHash -> Arrays.equals(candidateHash, usernameHash))
|
||||||
|
.findFirst()
|
||||||
|
.map(candidateHashes::indexOf)
|
||||||
|
.orElse(-1);
|
||||||
if (hashIndex == -1) {
|
if (hashIndex == -1) {
|
||||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||||
throw new IOException("Unexpected username response");
|
throw new IOException("Unexpected username response");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("[reserveUsername] Successfully reserved username.");
|
logger.debug("[reserveUsername] Successfully reserved username.");
|
||||||
final var username = candidates.get(hashIndex).getUsername();
|
final var username = candidates.get(hashIndex);
|
||||||
|
|
||||||
dependencies.getAccountManager().confirmUsername(username, response);
|
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||||
account.setUsername(username);
|
account.setUsername(username.getUsername());
|
||||||
|
account.setUsernameLink(linkComponents);
|
||||||
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
||||||
|
account.getRecipientStore().rotateSelfStorageId();
|
||||||
logger.debug("[confirmUsername] Successfully confirmed username.");
|
logger.debug("[confirmUsername] Successfully confirmed username.");
|
||||||
|
}
|
||||||
|
|
||||||
return username;
|
public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
|
||||||
|
try {
|
||||||
|
Username.UsernameLink link = username.generateLink();
|
||||||
|
return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
|
||||||
|
} catch (BaseUsernameException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
|
||||||
|
try {
|
||||||
|
Username.UsernameLink link = username.generateLink();
|
||||||
|
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
|
||||||
|
|
||||||
|
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||||
|
} catch (BaseUsernameException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UsernameLinkComponents reclaimUsernameAndLink(
|
||||||
|
Username username,
|
||||||
|
UsernameLinkComponents linkComponents
|
||||||
|
) throws IOException {
|
||||||
|
try {
|
||||||
|
Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
|
||||||
|
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
|
||||||
|
|
||||||
|
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||||
|
} catch (BaseUsernameException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshCurrentUsername() throws IOException, BaseUsernameException {
|
public void refreshCurrentUsername() throws IOException, BaseUsernameException {
|
||||||
@ -231,7 +470,8 @@ public class AccountHelper {
|
|||||||
final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
|
final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
|
||||||
final var serverUsernameHash = whoAmIResponse.getUsernameHash();
|
final var serverUsernameHash = whoAmIResponse.getUsernameHash();
|
||||||
final var hasServerUsername = !isEmpty(serverUsernameHash);
|
final var hasServerUsername = !isEmpty(serverUsernameHash);
|
||||||
final var localUsernameHash = Base64UrlSafe.encodeBytesWithoutPadding(new Username(localUsername).getHash());
|
final var username = new Username(localUsername);
|
||||||
|
final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash());
|
||||||
|
|
||||||
if (!hasServerUsername) {
|
if (!hasServerUsername) {
|
||||||
logger.debug("No remote username is set.");
|
logger.debug("No remote username is set.");
|
||||||
@ -243,55 +483,122 @@ public class AccountHelper {
|
|||||||
|
|
||||||
if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
|
if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
|
||||||
logger.debug("Attempting to resynchronize username.");
|
logger.debug("Attempting to resynchronize username.");
|
||||||
tryReserveConfirmUsername(localUsername, localUsernameHash);
|
try {
|
||||||
|
tryReserveConfirmUsername(username);
|
||||||
|
} catch (NonSuccessfulResponseCodeException e) {
|
||||||
|
logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
|
||||||
|
e.getMessage(),
|
||||||
|
e.getClass().getSimpleName());
|
||||||
|
account.setUsername(null);
|
||||||
|
account.setUsernameLink(null);
|
||||||
|
account.getRecipientStore().rotateSelfStorageId();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Username already set, not refreshing.");
|
logger.debug("Username already set, not refreshing.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tryReserveConfirmUsername(final String username, String localUsernameHash) throws IOException {
|
private void tryReserveConfirmUsername(final Username username) throws IOException {
|
||||||
final var response = dependencies.getAccountManager().reserveUsername(List.of(localUsernameHash));
|
final var usernameLink = account.getUsernameLink();
|
||||||
|
|
||||||
|
if (usernameLink == null) {
|
||||||
|
handleResponseException(dependencies.getAccountApi().reserveUsername(List.of(username.getHash())));
|
||||||
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
||||||
dependencies.getAccountManager().confirmUsername(username, response);
|
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||||
|
account.setUsernameLink(linkComponents);
|
||||||
logger.debug("[confirmUsername] Successfully confirmed existing username.");
|
logger.debug("[confirmUsername] Successfully confirmed existing username.");
|
||||||
|
} else {
|
||||||
|
final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
|
||||||
|
account.setUsernameLink(linkComponents);
|
||||||
|
logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
|
||||||
|
}
|
||||||
|
account.getRecipientStore().rotateSelfStorageId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void tryToSetUsernameLink(Username username) {
|
||||||
|
for (var i = 1; i < 4; i++) {
|
||||||
|
try {
|
||||||
|
final var linkComponents = createUsernameLink(username);
|
||||||
|
account.setUsernameLink(linkComponents);
|
||||||
|
break;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public void deleteUsername() throws IOException {
|
public void deleteUsername() throws IOException {
|
||||||
dependencies.getAccountManager().deleteUsername();
|
handleResponseException(dependencies.getAccountApi().deleteUsernameHash());
|
||||||
|
account.setUsernameLink(null);
|
||||||
account.setUsername(null);
|
account.setUsername(null);
|
||||||
logger.debug("[deleteUsername] Successfully deleted the username.");
|
logger.debug("[deleteUsername] Successfully deleted the username.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeviceName(String deviceName) {
|
public void setDeviceName(String deviceName) {
|
||||||
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
|
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
||||||
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
|
account.setEncryptedDeviceName(Base64.encodeWithoutPadding(encryptedDeviceName));
|
||||||
account.setEncryptedDeviceName(encryptedDeviceName);
|
}
|
||||||
|
|
||||||
|
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
||||||
|
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
||||||
|
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||||
|
.setDeviceName(encryptedDeviceName, deviceId, cont));
|
||||||
|
context.getSyncHelper().sendDeviceNameChange(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getEncryptedDeviceName(final String deviceName) {
|
||||||
|
final var identityKey = account.getAciIdentityKeyPair();
|
||||||
|
return DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), identityKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshDeviceName() throws IOException {
|
||||||
|
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||||
|
.getDevices(cont));
|
||||||
|
final var deviceId = account.getDeviceId();
|
||||||
|
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
|
||||||
|
if (device.isPresent()) {
|
||||||
|
account.setEncryptedDeviceName(device.get().name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateAccountAttributes() throws IOException {
|
public void updateAccountAttributes() throws IOException {
|
||||||
dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
|
handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
|
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
|
||||||
var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
|
final var linkDeviceApi = dependencies.getLinkDeviceApi();
|
||||||
|
final LinkedDeviceVerificationCodeResponse verificationCode;
|
||||||
try {
|
try {
|
||||||
dependencies.getAccountManager()
|
verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
|
||||||
.addDevice(deviceLinkInfo.deviceIdentifier(),
|
} catch (DeviceLimitExceededException e) {
|
||||||
|
throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResponseException(dependencies.getLinkDeviceApi()
|
||||||
|
.linkDevice(account.getNumber(),
|
||||||
|
account.getAci(),
|
||||||
|
account.getPni(),
|
||||||
|
deviceLinkInfo.deviceIdentifier(),
|
||||||
deviceLinkInfo.deviceKey(),
|
deviceLinkInfo.deviceKey(),
|
||||||
account.getAciIdentityKeyPair(),
|
account.getAciIdentityKeyPair(),
|
||||||
account.getPniIdentityKeyPair(),
|
account.getPniIdentityKeyPair(),
|
||||||
account.getProfileKey(),
|
account.getProfileKey(),
|
||||||
verificationCode);
|
account.getOrCreateAccountEntropyPool(),
|
||||||
} catch (InvalidKeyException e) {
|
account.getOrCreatePinMasterKey(),
|
||||||
throw new InvalidDeviceLinkException("Invalid device link", e);
|
account.getOrCreateMediaRootBackupKey(),
|
||||||
}
|
verificationCode.getVerificationCode(),
|
||||||
|
null,
|
||||||
|
account.getAuthCredentialSalt()));
|
||||||
account.setMultiDevice(true);
|
account.setMultiDevice(true);
|
||||||
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeLinkedDevices(int deviceId) throws IOException {
|
public void removeLinkedDevices(int deviceId) throws IOException {
|
||||||
dependencies.getAccountManager().removeDevice(deviceId);
|
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi().removeDevice(deviceId, cont));
|
||||||
var devices = dependencies.getAccountManager().getDevices();
|
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||||
|
.getDevices(cont));
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,19 +606,23 @@ public class AccountHelper {
|
|||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
||||||
|
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegistrationPin(String pin) throws IOException {
|
public void setRegistrationPin(String pin) throws IOException {
|
||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
||||||
|
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
||||||
|
|
||||||
account.setRegistrationLockPin(pin);
|
account.setRegistrationLockPin(pin);
|
||||||
|
updateAccountAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeRegistrationPin() throws IOException {
|
public void removeRegistrationPin() throws IOException {
|
||||||
// Remove KBS Pin
|
// Remove KBS Pin
|
||||||
context.getPinHelper().removeRegistrationLockPin();
|
context.getPinHelper().removeRegistrationLockPin();
|
||||||
|
handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
|
||||||
|
|
||||||
account.setRegistrationLockPin(null);
|
account.setRegistrationLockPin(null);
|
||||||
}
|
}
|
||||||
@ -320,7 +631,7 @@ public class AccountHelper {
|
|||||||
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
||||||
// If this is the primary device, other users can't send messages to this number anymore.
|
// If this is the primary device, other users can't send messages to this number anymore.
|
||||||
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
||||||
dependencies.getAccountManager().setGcmId(Optional.empty());
|
handleResponseExceptionSuspend(cont -> dependencies.getAccountApi().clearFcmToken(cont));
|
||||||
|
|
||||||
account.setRegistered(false);
|
account.setRegistered(false);
|
||||||
unregisteredListener.call();
|
unregisteredListener.call();
|
||||||
@ -334,7 +645,7 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
account.setRegistrationLockPin(null);
|
account.setRegistrationLockPin(null);
|
||||||
|
|
||||||
dependencies.getAccountManager().deleteAccount();
|
handleResponseException(dependencies.getAccountApi().deleteAccount());
|
||||||
|
|
||||||
account.setRegistered(false);
|
account.setRegistered(false);
|
||||||
unregisteredListener.call();
|
unregisteredListener.call();
|
||||||
|
|||||||
@ -6,14 +6,19 @@ import org.asamk.signal.manager.internal.SignalDependencies;
|
|||||||
import org.asamk.signal.manager.storage.AttachmentStore;
|
import org.asamk.signal.manager.storage.AttachmentStore;
|
||||||
import org.asamk.signal.manager.util.AttachmentUtils;
|
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||||
import org.asamk.signal.manager.util.IOUtils;
|
import org.asamk.signal.manager.util.IOUtils;
|
||||||
|
import org.asamk.signal.manager.util.Utils;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
|
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||||
|
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -25,12 +30,14 @@ import java.util.List;
|
|||||||
|
|
||||||
public class AttachmentHelper {
|
public class AttachmentHelper {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(AttachmentHelper.class);
|
private static final Logger logger = LoggerFactory.getLogger(AttachmentHelper.class);
|
||||||
|
|
||||||
private final SignalDependencies dependencies;
|
private final SignalDependencies dependencies;
|
||||||
private final AttachmentStore attachmentStore;
|
private final AttachmentStore attachmentStore;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
public AttachmentHelper(final Context context) {
|
public AttachmentHelper(final Context context) {
|
||||||
|
this.context = context;
|
||||||
this.dependencies = context.getDependencies();
|
this.dependencies = context.getDependencies();
|
||||||
this.attachmentStore = context.getAttachmentStore();
|
this.attachmentStore = context.getAttachmentStore();
|
||||||
}
|
}
|
||||||
@ -43,19 +50,87 @@ public class AttachmentHelper {
|
|||||||
return attachmentStore.retrieveAttachment(id);
|
return attachmentStore.retrieveAttachment(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
|
public List<SignalServiceAttachment> uploadAttachments(
|
||||||
var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments);
|
final List<String> attachments,
|
||||||
|
boolean voiceNote
|
||||||
|
) throws AttachmentInvalidException, IOException {
|
||||||
|
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
|
||||||
|
|
||||||
|
try {
|
||||||
// Upload attachments here, so we only upload once even for multiple recipients
|
// Upload attachments here, so we only upload once even for multiple recipients
|
||||||
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
|
final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
|
||||||
for (var attachmentStream : attachmentStreams) {
|
for (final var attachmentStream : attachmentStreams) {
|
||||||
attachmentPointers.add(uploadAttachment(attachmentStream));
|
attachmentPointers.add(uploadAttachment(attachmentStream));
|
||||||
}
|
}
|
||||||
return attachmentPointers;
|
return attachmentPointers;
|
||||||
|
} finally {
|
||||||
|
for (final var attachmentStream : attachmentStreams) {
|
||||||
|
attachmentStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
|
||||||
|
return uploadAttachments(attachments, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SignalServiceAttachmentStream> createAttachmentStreams(
|
||||||
|
List<String> attachments,
|
||||||
|
boolean voiceNote
|
||||||
|
) throws AttachmentInvalidException, IOException {
|
||||||
|
if (attachments == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
|
||||||
|
for (var attachment : attachments) {
|
||||||
|
final var attachmentStream = getAttachmentStream(attachment, voiceNote);
|
||||||
|
signalServiceAttachments.add(attachmentStream);
|
||||||
|
}
|
||||||
|
return signalServiceAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignalServiceAttachmentStream getAttachmentStream(
|
||||||
|
final String attachment,
|
||||||
|
final boolean voiceNote
|
||||||
|
) throws AttachmentInvalidException {
|
||||||
|
try {
|
||||||
|
// Reject local files that point into the signal-cli data directory
|
||||||
|
if (attachment != null && !attachment.startsWith("data:")) {
|
||||||
|
try {
|
||||||
|
final var file = new File(attachment);
|
||||||
|
final var canonical = file.getCanonicalFile();
|
||||||
|
final var dataPath = context.getAccount().getDataPath().getCanonicalFile();
|
||||||
|
if (canonical.toPath().startsWith(dataPath.toPath())) {
|
||||||
|
throw new AttachmentInvalidException(attachment,
|
||||||
|
new IOException("Attaching files from the signal-cli data directory is not allowed"));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AttachmentInvalidException(attachment, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var streamDetailsAndFileName = Utils.createStreamDetails(attachment);
|
||||||
|
final var streamDetails = streamDetailsAndFileName.first();
|
||||||
|
final var uploadSpec = getResumableUploadSpec(streamDetails);
|
||||||
|
|
||||||
|
return AttachmentUtils.createAttachmentStream(streamDetails,
|
||||||
|
streamDetailsAndFileName.second(),
|
||||||
|
voiceNote,
|
||||||
|
uploadSpec);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AttachmentInvalidException(attachment, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResumableUploadSpec getResumableUploadSpec(final StreamDetails streamDetails) throws IOException {
|
||||||
|
final var streamLength = streamDetails.getLength();
|
||||||
|
final var ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(
|
||||||
|
streamLength));
|
||||||
|
return dependencies.getCdnService().getResumableUploadSpecBlocking(ciphertextLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
|
||||||
var attachmentStream = AttachmentUtils.createAttachmentStream(attachment);
|
final var attachmentStream = getAttachmentStream(attachment, false);
|
||||||
return uploadAttachment(attachmentStream);
|
return uploadAttachment(attachmentStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,9 +166,7 @@ public class AttachmentHelper {
|
|||||||
retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
|
retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void retrieveAttachment(
|
public void retrieveAttachment(SignalServiceAttachment attachment, AttachmentHandler consumer) throws IOException {
|
||||||
SignalServiceAttachment attachment, AttachmentHandler consumer
|
|
||||||
) throws IOException {
|
|
||||||
if (attachment.isStream()) {
|
if (attachment.isStream()) {
|
||||||
var input = attachment.asStream().getInputStream();
|
var input = attachment.asStream().getInputStream();
|
||||||
// don't close input stream here, it might be reused later (e.g. with contact sync messages ...)
|
// don't close input stream here, it might be reused later (e.g. with contact sync messages ...)
|
||||||
@ -118,11 +191,18 @@ public class AttachmentHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private InputStream retrieveAttachmentAsStream(
|
private InputStream retrieveAttachmentAsStream(
|
||||||
SignalServiceAttachmentPointer pointer, File tmpFile
|
SignalServiceAttachmentPointer pointer,
|
||||||
|
File tmpFile
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
|
if (pointer.getDigest().isEmpty()) {
|
||||||
|
throw new IOException("Attachment pointer has no digest.");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return dependencies.getMessageReceiver()
|
return dependencies.getMessageReceiver()
|
||||||
.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
|
.retrieveAttachment(pointer,
|
||||||
|
tmpFile,
|
||||||
|
ServiceConfig.MAX_ATTACHMENT_SIZE,
|
||||||
|
AttachmentCipherInputStream.IntegrityCheck.forEncryptedDigest(pointer.getDigest().get()));
|
||||||
} catch (MissingConfigurationException | InvalidMessageException e) {
|
} catch (MissingConfigurationException | InvalidMessageException e) {
|
||||||
throw new IOException(e);
|
throw new IOException(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,809 @@
|
|||||||
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.Manager;
|
||||||
|
import org.asamk.signal.manager.api.CallInfo;
|
||||||
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
import org.asamk.signal.manager.api.TurnServer;
|
||||||
|
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||||
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.asamk.signal.manager.util.Utils;
|
||||||
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.asamk.signal.manager.util.Utils.callIdUnsigned;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages active voice calls: tracks state, spawns/monitors the signal-call-tunnel
|
||||||
|
* subprocess, routes incoming call messages, and handles timeouts.
|
||||||
|
*/
|
||||||
|
public class CallManager implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CallManager.class);
|
||||||
|
private static final long RING_TIMEOUT_MS = 60_000;
|
||||||
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final SignalAccount account;
|
||||||
|
private final SignalDependencies dependencies;
|
||||||
|
private final Map<Long, CallState> activeCalls = new ConcurrentHashMap<>();
|
||||||
|
private final List<Manager.CallEventListener> callEventListeners = new CopyOnWriteArrayList<>();
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
|
var t = new Thread(r, "call-timeout-scheduler");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
public CallManager(final Context context) {
|
||||||
|
this.context = context;
|
||||||
|
this.account = context.getAccount();
|
||||||
|
this.dependencies = context.getDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCallEventListener(Manager.CallEventListener listener) {
|
||||||
|
callEventListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCallEventListener(Manager.CallEventListener listener) {
|
||||||
|
callEventListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireCallEvent(CallState state, String reason) {
|
||||||
|
var callInfo = state.toCallInfo(account.getRecipientAddressResolver());
|
||||||
|
for (var listener : callEventListeners) {
|
||||||
|
try {
|
||||||
|
listener.handleCallEvent(callInfo, reason);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.warn("Call event listener failed, ignoring", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallInfo startOutgoingCall(
|
||||||
|
final RecipientId recipientId
|
||||||
|
) throws IOException {
|
||||||
|
var callId = generateCallId();
|
||||||
|
var recipientAddress = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
|
||||||
|
|
||||||
|
var state = new CallState(callId, CallInfo.State.RINGING_OUTGOING, recipientId, null, true);
|
||||||
|
logger.debug("Starting outgoing call {} to {} (recipientId: {})",
|
||||||
|
callIdUnsigned(callId),
|
||||||
|
recipientAddress,
|
||||||
|
recipientId);
|
||||||
|
activeCalls.put(callId, state);
|
||||||
|
dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
|
||||||
|
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
|
||||||
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
|
// Spawn call tunnel binary and connect control channel
|
||||||
|
spawnMediaTunnel(state);
|
||||||
|
|
||||||
|
// Fetch TURN servers
|
||||||
|
var turnServers = getTurnServers();
|
||||||
|
|
||||||
|
// Send createOutgoingCall + proceed via control channel
|
||||||
|
var createMsg = mapper.createObjectNode();
|
||||||
|
createMsg.put("type", "createOutgoingCall");
|
||||||
|
createMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||||
|
createMsg.put("peerId", recipientAddress.toString());
|
||||||
|
sendControlMessage(state, writeJson(createMsg));
|
||||||
|
sendProceed(state, callId, turnServers);
|
||||||
|
|
||||||
|
// Schedule ring timeout
|
||||||
|
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
logger.debug("Started outgoing call {} to {}", callIdUnsigned(callId), recipientAddress);
|
||||||
|
return state.toCallInfo(account.getRecipientAddressResolver());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallInfo acceptIncomingCall(final long callId) throws IOException {
|
||||||
|
final var state = getActiveCall(callId);
|
||||||
|
if (state.state != CallInfo.State.RINGING_INCOMING) {
|
||||||
|
throw new IOException("Call "
|
||||||
|
+ callId
|
||||||
|
+ " is not in RINGING_INCOMING state (current: "
|
||||||
|
+ state.state
|
||||||
|
+ ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer the accept until the tunnel reports Ringing state.
|
||||||
|
// Sending accept too early (while RingRTC is in ConnectingBeforeAccepted)
|
||||||
|
// causes it to be silently dropped.
|
||||||
|
state.acceptPending = true;
|
||||||
|
// If the tunnel is already in Ringing state, send immediately
|
||||||
|
sendAcceptIfReady(state);
|
||||||
|
|
||||||
|
state.state = CallInfo.State.CONNECTING;
|
||||||
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
|
logger.debug("Accepted incoming call {}", callIdUnsigned(callId));
|
||||||
|
return state.toCallInfo(account.getRecipientAddressResolver());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hangupCall(final long callId) throws IOException {
|
||||||
|
getActiveCall(callId);
|
||||||
|
endCall(callId, "local_hangup");
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendMessageResult rejectCall(final long callId) throws IOException {
|
||||||
|
final var callState = getActiveCall(callId);
|
||||||
|
|
||||||
|
final var result = sendBusyMessage(callState.callId, callState.recipientId, callState.deviceId);
|
||||||
|
|
||||||
|
endCall(callId, "rejected");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CallInfo> listActiveCalls() {
|
||||||
|
return activeCalls.values()
|
||||||
|
.stream()
|
||||||
|
.map((CallState callState) -> callState.toCallInfo(account.getRecipientAddressResolver()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TurnServer> getTurnServers() throws IOException {
|
||||||
|
try {
|
||||||
|
var turnServerList = handleResponseException(dependencies.getCallingApi().getTurnServerInfo());
|
||||||
|
return turnServerList.stream()
|
||||||
|
.map(info -> new TurnServer(info.getUsername(), info.getPassword(), info.getUrls()))
|
||||||
|
.toList();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.warn("Failed to get TURN server info, returning empty list", e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Incoming call message handling ---
|
||||||
|
|
||||||
|
public void handleIncomingOffer(
|
||||||
|
final RecipientId recipientId,
|
||||||
|
final int deviceId,
|
||||||
|
final long callId,
|
||||||
|
final MessageEnvelope.Call.Offer.Type type,
|
||||||
|
final byte[] opaque
|
||||||
|
) {
|
||||||
|
if (callEventListeners.isEmpty()) {
|
||||||
|
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
|
||||||
|
callIdUnsigned(callId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var senderAddress = account.getRecipientAddressResolver()
|
||||||
|
.resolveRecipientAddress(recipientId)
|
||||||
|
.toApiRecipientAddress();
|
||||||
|
|
||||||
|
logger.debug("Incoming offer opaque ({} bytes)", opaque == null ? 0 : opaque.length);
|
||||||
|
|
||||||
|
var state = new CallState(callId, CallInfo.State.RINGING_INCOMING, recipientId, deviceId, false);
|
||||||
|
logger.debug("Starting incoming call {} from {} (recipientId: {})",
|
||||||
|
callIdUnsigned(callId),
|
||||||
|
senderAddress,
|
||||||
|
recipientId);
|
||||||
|
activeCalls.put(callId, state);
|
||||||
|
|
||||||
|
// Spawn call tunnel binary immediately
|
||||||
|
spawnMediaTunnel(state);
|
||||||
|
|
||||||
|
// Get identity keys for the receivedOffer message
|
||||||
|
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
|
||||||
|
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||||
|
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
|
||||||
|
|
||||||
|
// Fetch TURN servers
|
||||||
|
List<TurnServer> turnServers;
|
||||||
|
try {
|
||||||
|
turnServers = getTurnServers();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to get TURN servers for incoming call {}", callIdUnsigned(callId), e);
|
||||||
|
turnServers = List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send receivedOffer to subprocess
|
||||||
|
var offerMsg = mapper.createObjectNode();
|
||||||
|
offerMsg.put("type", "receivedOffer");
|
||||||
|
offerMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||||
|
offerMsg.put("peerId", senderAddress.toString());
|
||||||
|
offerMsg.put("senderDeviceId", deviceId);
|
||||||
|
offerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
|
||||||
|
offerMsg.put("age", 0);
|
||||||
|
offerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
|
||||||
|
offerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
|
||||||
|
sendControlMessage(state, writeJson(offerMsg));
|
||||||
|
|
||||||
|
// Send proceed with TURN servers
|
||||||
|
sendProceed(state, callId, turnServers);
|
||||||
|
|
||||||
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
|
// Schedule ring timeout
|
||||||
|
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
logger.debug("Incoming call {} from {}", callIdUnsigned(callId), senderAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingAnswer(final long callId, final int deviceId, final byte[] opaque) {
|
||||||
|
var state = activeCalls.get(callId);
|
||||||
|
if (state == null) {
|
||||||
|
logger.warn("Received answer for unknown call {}", callIdUnsigned(callId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get identity keys
|
||||||
|
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
|
||||||
|
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||||
|
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
|
||||||
|
|
||||||
|
// Forward raw opaque to subprocess
|
||||||
|
var answerMsg = mapper.createObjectNode();
|
||||||
|
answerMsg.put("type", "receivedAnswer");
|
||||||
|
answerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
|
||||||
|
answerMsg.put("senderDeviceId", deviceId);
|
||||||
|
answerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
|
||||||
|
answerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
|
||||||
|
sendControlMessage(state, writeJson(answerMsg));
|
||||||
|
|
||||||
|
state.deviceId = deviceId;
|
||||||
|
state.state = CallInfo.State.CONNECTING;
|
||||||
|
fireCallEvent(state, null);
|
||||||
|
|
||||||
|
logger.debug("Received answer for call {}", callIdUnsigned(callId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingIceCandidate(final long callId, final byte[] opaque, final int deviceId) {
|
||||||
|
var state = activeCalls.get(callId);
|
||||||
|
if (state == null) {
|
||||||
|
logger.debug("Received ICE candidate for unknown call {}", callIdUnsigned(callId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to subprocess as receivedIce
|
||||||
|
var iceMsg = mapper.createObjectNode();
|
||||||
|
iceMsg.put("type", "receivedIce");
|
||||||
|
iceMsg.put("senderDeviceId", deviceId);
|
||||||
|
var candidates = iceMsg.putArray("candidates");
|
||||||
|
candidates.add(java.util.Base64.getEncoder().encodeToString(opaque));
|
||||||
|
sendControlMessage(state, writeJson(iceMsg));
|
||||||
|
logger.debug("Forwarded ICE candidate to tunnel for call {}", callIdUnsigned(callId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingHangup(final long callId) {
|
||||||
|
if (callEventListeners.isEmpty() && !activeCalls.containsKey(callId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
endCall(callId, "remote_hangup");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleIncomingBusy(final long callId) {
|
||||||
|
if (callEventListeners.isEmpty() && !activeCalls.containsKey(callId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
endCall(callId, "remote_busy");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal helpers ---
|
||||||
|
|
||||||
|
private CallState getActiveCall(final long callId) throws IOException {
|
||||||
|
var state = activeCalls.get(callId);
|
||||||
|
if (state == null) {
|
||||||
|
throw new IOException("No active call with id " + callIdUnsigned(callId));
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendBusyMessage(final long callId, final RecipientId recipientId, final int deviceId) {
|
||||||
|
var busyMessage = new BusyMessage(callId);
|
||||||
|
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, deviceId);
|
||||||
|
return context.getSendHelper().sendCallMessage(callMessage, recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendControlMessage(CallState state, String json) {
|
||||||
|
if (state.controlWriter == null) {
|
||||||
|
logger.debug("Queueing control message for call {} (not yet connected): {}",
|
||||||
|
callIdUnsigned(state.callId),
|
||||||
|
json);
|
||||||
|
state.pendingControlMessages.add(json);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.controlWriter.println(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendProceed(CallState state, long callId, List<TurnServer> turnServers) {
|
||||||
|
var proceedMsg = mapper.createObjectNode();
|
||||||
|
proceedMsg.put("type", "proceed");
|
||||||
|
proceedMsg.put("callId", Utils.callIdUnsigned(callId));
|
||||||
|
proceedMsg.put("hideIp", false);
|
||||||
|
var iceServers = proceedMsg.putArray("iceServers");
|
||||||
|
for (var ts : turnServers) {
|
||||||
|
var server = iceServers.addObject();
|
||||||
|
server.put("username", ts.username());
|
||||||
|
server.put("password", ts.password());
|
||||||
|
var urls = server.putArray("urls");
|
||||||
|
for (var url : ts.urls()) {
|
||||||
|
urls.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendControlMessage(state, writeJson(proceedMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void spawnMediaTunnel(CallState state) {
|
||||||
|
try {
|
||||||
|
var command = new ArrayList<>(List.of(findTunnelBinary()));
|
||||||
|
|
||||||
|
var processBuilder = new ProcessBuilder(command);
|
||||||
|
// Keep stdout and stderr separate: stdout = control protocol, stderr = logging
|
||||||
|
processBuilder.redirectErrorStream(false);
|
||||||
|
var process = processBuilder.start();
|
||||||
|
|
||||||
|
state.tunnelProcess = process;
|
||||||
|
|
||||||
|
// Write config JSON to stdin, then keep stdin open for control messages
|
||||||
|
var config = buildConfig(state);
|
||||||
|
var stdinStream = process.getOutputStream();
|
||||||
|
stdinStream.write(config.getBytes(StandardCharsets.UTF_8));
|
||||||
|
stdinStream.write('\n');
|
||||||
|
stdinStream.flush();
|
||||||
|
|
||||||
|
// stdin is the control write channel
|
||||||
|
state.controlWriter = new PrintWriter(new OutputStreamWriter(stdinStream, StandardCharsets.UTF_8), true);
|
||||||
|
|
||||||
|
// Flush any pending control messages
|
||||||
|
for (var msg : state.pendingControlMessages) {
|
||||||
|
state.controlWriter.println(msg);
|
||||||
|
}
|
||||||
|
state.pendingControlMessages.clear();
|
||||||
|
|
||||||
|
// If accept was deferred, send it now
|
||||||
|
sendAcceptIfReady(state);
|
||||||
|
|
||||||
|
// Read control events from subprocess stdout
|
||||||
|
Thread.ofVirtual()
|
||||||
|
.name("control-read-" + callIdUnsigned(state.callId))
|
||||||
|
.start(() -> readControlEvents(state, process.getInputStream()));
|
||||||
|
|
||||||
|
// Drain subprocess stderr to prevent pipe buffer deadlock
|
||||||
|
Thread.ofVirtual().name("tunnel-stderr-" + callIdUnsigned(state.callId)).start(() -> {
|
||||||
|
try (var reader = new BufferedReader(new InputStreamReader(process.getErrorStream(),
|
||||||
|
StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
logger.debug("[tunnel-{}] {}", callIdUnsigned(state.callId), line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor process exit
|
||||||
|
process.onExit().thenAcceptAsync(p -> {
|
||||||
|
logger.debug("Tunnel for call {} exited with code {}", callIdUnsigned(state.callId), p.exitValue());
|
||||||
|
if (activeCalls.containsKey(state.callId)) {
|
||||||
|
endCall(state.callId, "tunnel_exit");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug("Spawned signal-call-tunnel for call {}", callIdUnsigned(state.callId));
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to spawn tunnel for call {}", callIdUnsigned(state.callId), e);
|
||||||
|
endCall(state.callId, "tunnel_spawn_error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findTunnelBinary() {
|
||||||
|
// Check environment variable first
|
||||||
|
var envPath = System.getenv("SIGNAL_CALL_TUNNEL_BIN");
|
||||||
|
if (envPath != null && !envPath.isEmpty()) {
|
||||||
|
return envPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check relative to the signal-cli installation directory
|
||||||
|
try {
|
||||||
|
var codeSource = CallManager.class.getProtectionDomain().getCodeSource();
|
||||||
|
if (codeSource != null) {
|
||||||
|
var jarPath = Path.of(codeSource.getLocation().toURI());
|
||||||
|
var binPath = tunnelBinaryFromCodeSourcePath(jarPath);
|
||||||
|
if (Files.isExecutable(binPath)) {
|
||||||
|
return binPath.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.debug("Failed to determine install dir from code source", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to PATH
|
||||||
|
return "signal-call-tunnel";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the expected tunnel binary path from a code source path.
|
||||||
|
* The code source (jar or class dir) is expected to be in {@code <install>/lib/},
|
||||||
|
* so we go up two levels to reach the install root, then look for
|
||||||
|
* {@code bin/signal-call-tunnel}.
|
||||||
|
*/
|
||||||
|
static Path tunnelBinaryFromCodeSourcePath(Path codeSourcePath) {
|
||||||
|
var installDir = codeSourcePath.getParent().getParent();
|
||||||
|
return installDir.resolve("bin").resolve("signal-call-tunnel");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildConfig(CallState state) {
|
||||||
|
var config = mapper.createObjectNode();
|
||||||
|
config.put("call_id", Utils.callIdUnsigned(state.callId));
|
||||||
|
config.put("is_outgoing", state.isOutgoing);
|
||||||
|
config.put("local_device_id", 1);
|
||||||
|
return writeJson(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readControlEvents(CallState state, java.io.InputStream inputStream) {
|
||||||
|
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
logger.debug("Control event for call {}: {}", callIdUnsigned(state.callId), line);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var json = mapper.readTree(line);
|
||||||
|
var type = json.has("type") ? json.get("type").asText() : "";
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "ready" -> {
|
||||||
|
if (json.has("inputDeviceName")) {
|
||||||
|
state.inputDeviceName = json.get("inputDeviceName").asText();
|
||||||
|
}
|
||||||
|
if (json.has("outputDeviceName")) {
|
||||||
|
state.outputDeviceName = json.get("outputDeviceName").asText();
|
||||||
|
}
|
||||||
|
logger.debug("Tunnel ready for call {}: input={}, output={}",
|
||||||
|
callIdUnsigned(state.callId),
|
||||||
|
state.inputDeviceName,
|
||||||
|
state.outputDeviceName);
|
||||||
|
}
|
||||||
|
case "sendOffer" -> {
|
||||||
|
var opaqueB64 = json.get("opaque").asText();
|
||||||
|
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
|
||||||
|
logSendMessageResult(sendOfferViaSignal(state, opaque));
|
||||||
|
}
|
||||||
|
case "sendAnswer" -> {
|
||||||
|
var opaqueB64 = json.get("opaque").asText();
|
||||||
|
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
|
||||||
|
logSendMessageResult(sendAnswerViaSignal(state, opaque));
|
||||||
|
}
|
||||||
|
case "sendIce" -> {
|
||||||
|
var candidatesArr = json.get("candidates");
|
||||||
|
var opaqueList = new ArrayList<byte[]>();
|
||||||
|
for (var c : candidatesArr) {
|
||||||
|
opaqueList.add(java.util.Base64.getDecoder().decode(c.get("opaque").asText()));
|
||||||
|
}
|
||||||
|
logSendMessageResult(sendIceViaSignal(state, opaqueList));
|
||||||
|
}
|
||||||
|
case "sendHangup" -> {
|
||||||
|
// RingRTC wants us to send a hangup message via Signal protocol.
|
||||||
|
// This is NOT a local state change — local state is handled by stateChange events.
|
||||||
|
var hangupType = json.has("hangupType")
|
||||||
|
? json.get("hangupType").asText("normal")
|
||||||
|
: "normal";
|
||||||
|
// Skip multi-device hangup types — signal-cli is single-device,
|
||||||
|
// and sending these to the remote peer causes it to terminate the call.
|
||||||
|
if (hangupType.contains("onanotherdevice")) {
|
||||||
|
logger.debug("Ignoring multi-device hangup type: {}", hangupType);
|
||||||
|
} else {
|
||||||
|
logSendMessageResult(sendHangupViaSignal(state, hangupType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "sendBusy" -> {
|
||||||
|
logSendMessageResult(sendBusyViaSignal(state));
|
||||||
|
}
|
||||||
|
case "stateChange" -> {
|
||||||
|
var ringrtcState = json.get("state").asText();
|
||||||
|
var reason = json.has("reason") ? json.get("reason").asText(null) : null;
|
||||||
|
handleStateChange(state, ringrtcState, reason);
|
||||||
|
}
|
||||||
|
case "error" -> {
|
||||||
|
var message = json.has("message") ? json.get("message").asText("unknown") : "unknown";
|
||||||
|
logger.error("Tunnel error for call {}: {}", callIdUnsigned(state.callId), message);
|
||||||
|
endCall(state.callId, "tunnel_error");
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
logger.debug("Unknown control event type '{}' for call {}",
|
||||||
|
type,
|
||||||
|
callIdUnsigned(state.callId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to parse control event JSON for call {}: {}",
|
||||||
|
callIdUnsigned(state.callId),
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.debug("Control read ended for call {}: {}", callIdUnsigned(state.callId), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStateChange(CallState state, String ringrtcState, String reason) {
|
||||||
|
if (ringrtcState.startsWith("Incoming")) {
|
||||||
|
// Don't downgrade if we've already accepted
|
||||||
|
if (state.state == CallInfo.State.CONNECTING) return;
|
||||||
|
state.state = CallInfo.State.RINGING_INCOMING;
|
||||||
|
} else if (ringrtcState.startsWith("Outgoing")) {
|
||||||
|
state.state = CallInfo.State.RINGING_OUTGOING;
|
||||||
|
} else if ("Ringing".equals(ringrtcState)) {
|
||||||
|
// Tunnel is now ready to accept — flush deferred accept if pending
|
||||||
|
state.tunnelRinging = true;
|
||||||
|
sendAcceptIfReady(state);
|
||||||
|
return;
|
||||||
|
} else if ("Connected".equals(ringrtcState)) {
|
||||||
|
state.state = CallInfo.State.CONNECTED;
|
||||||
|
} else if ("Connecting".equals(ringrtcState)) {
|
||||||
|
state.state = CallInfo.State.RECONNECTING;
|
||||||
|
} else if ("Ended".equals(ringrtcState) || "Rejected".equals(ringrtcState)) {
|
||||||
|
endCall(state.callId, reason != null ? reason : ringrtcState.toLowerCase());
|
||||||
|
return;
|
||||||
|
} else if ("Concluded".equals(ringrtcState)) {
|
||||||
|
// Cleanup, no-op
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fireCallEvent(state, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void logSendMessageResult(SendMessageResult result) {
|
||||||
|
var identifier = result.getAddress().getIdentifier();
|
||||||
|
if (result.getProofRequiredFailure() != null) {
|
||||||
|
final var failure = result.getProofRequiredFailure();
|
||||||
|
logger.warn(
|
||||||
|
"CAPTCHA proof required for sending to \"{}\", available options \"{}\" with challenge token \"{}\", or wait \"{}\" seconds.\n",
|
||||||
|
identifier,
|
||||||
|
failure.getOptions()
|
||||||
|
.stream()
|
||||||
|
.map(ProofRequiredException.Option::toString)
|
||||||
|
.collect(Collectors.joining(", ")),
|
||||||
|
failure.getToken(),
|
||||||
|
failure.getRetryAfterSeconds());
|
||||||
|
} else if (result.isNetworkFailure()) {
|
||||||
|
logger.warn("Network failure for \"{}\"", identifier);
|
||||||
|
} else if (result.getRateLimitFailure() != null) {
|
||||||
|
logger.warn("Rate limit failure for \"{}\"", identifier);
|
||||||
|
} else if (result.isUnregisteredFailure()) {
|
||||||
|
logger.warn("Unregistered user \"{}\"", identifier);
|
||||||
|
} else if (result.getIdentityFailure() != null) {
|
||||||
|
logger.warn("Untrusted Identity for \"{}\"", identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAcceptIfReady(CallState state) {
|
||||||
|
if (state.acceptPending && state.tunnelRinging && state.controlWriter != null) {
|
||||||
|
state.acceptPending = false;
|
||||||
|
logger.debug("Sending deferred accept for call {}", callIdUnsigned(state.callId));
|
||||||
|
var acceptMsg = mapper.createObjectNode();
|
||||||
|
acceptMsg.put("type", "accept");
|
||||||
|
state.controlWriter.println(writeJson(acceptMsg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendOfferViaSignal(CallState state, byte[] opaque) {
|
||||||
|
var offerMessage = new OfferMessage(state.callId, OfferMessage.Type.AUDIO_CALL, opaque);
|
||||||
|
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, state.deviceId);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
logger.debug("Sent offer via Signal for call {}", callIdUnsigned(state.callId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendAnswerViaSignal(CallState state, byte[] opaque) {
|
||||||
|
var answerMessage = new AnswerMessage(state.callId, opaque);
|
||||||
|
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, state.deviceId);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
logger.debug("Sent answer via Signal for call {}", callIdUnsigned(state.callId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendIceViaSignal(CallState state, List<byte[]> opaqueList) {
|
||||||
|
var iceUpdates = opaqueList.stream().map(opaque -> new IceUpdateMessage(state.callId, opaque)).toList();
|
||||||
|
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, state.deviceId);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
logger.debug("Sent {} ICE candidates via Signal for call {}", opaqueList.size(), callIdUnsigned(state.callId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendBusyViaSignal(CallState state) {
|
||||||
|
var busyMessage = new BusyMessage(state.callId);
|
||||||
|
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, state.deviceId);
|
||||||
|
return context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendMessageResult sendHangupViaSignal(CallState state, String hangupType) {
|
||||||
|
var type = switch (hangupType) {
|
||||||
|
case "accepted", "acceptedonanotherdevice" -> HangupMessage.Type.ACCEPTED;
|
||||||
|
case "declined", "declinedonanotherdevice" -> HangupMessage.Type.DECLINED;
|
||||||
|
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
|
||||||
|
default -> HangupMessage.Type.NORMAL;
|
||||||
|
};
|
||||||
|
var hangupMessage = new HangupMessage(state.callId, type, 0);
|
||||||
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getRemoteIdentityKey(CallState state) {
|
||||||
|
try {
|
||||||
|
var address = context.getRecipientHelper().resolveSignalServiceAddress(state.recipientId);
|
||||||
|
var serviceId = address.getServiceId();
|
||||||
|
var identityInfo = account.getIdentityKeyStore().getIdentityInfo(serviceId);
|
||||||
|
if (identityInfo != null) {
|
||||||
|
return getRawIdentityKeyBytes(identityInfo.getIdentityKey());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to get remote identity key for call {}", callIdUnsigned(state.callId), e);
|
||||||
|
}
|
||||||
|
logger.warn("Using local identity key as fallback for remote identity key");
|
||||||
|
return getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip the 0x05 DJB type prefix from a serialized identity key to get the
|
||||||
|
* raw 32-byte Curve25519 public key. Signal Android does this via
|
||||||
|
* WebRtcUtil.getPublicKeyBytes() before passing keys to RingRTC.
|
||||||
|
*/
|
||||||
|
private static byte[] getRawIdentityKeyBytes(IdentityKey identityKey) {
|
||||||
|
var serializedKey = identityKey.serialize();
|
||||||
|
return getRawIdentityKeyBytes(serializedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getRawIdentityKeyBytes(final byte[] serializedKey) {
|
||||||
|
if (serializedKey.length == 33 && serializedKey[0] == 0x05) {
|
||||||
|
return java.util.Arrays.copyOfRange(serializedKey, 1, serializedKey.length);
|
||||||
|
}
|
||||||
|
return serializedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String writeJson(ObjectNode node) {
|
||||||
|
try {
|
||||||
|
return mapper.writeValueAsString(node);
|
||||||
|
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
|
||||||
|
throw new RuntimeException("Failed to serialize JSON", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endCall(final long callId, final String reason) {
|
||||||
|
var state = activeCalls.remove(callId);
|
||||||
|
dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
|
||||||
|
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
|
||||||
|
if (state == null) return;
|
||||||
|
|
||||||
|
state.state = CallInfo.State.ENDED;
|
||||||
|
fireCallEvent(state, reason);
|
||||||
|
logger.debug("Call {} ended: {}", callIdUnsigned(callId), reason);
|
||||||
|
|
||||||
|
// Send Signal protocol hangup to remote peer (unless they initiated the end)
|
||||||
|
if (!"remote_hangup".equals(reason)
|
||||||
|
&& !"rejected".equals(reason)
|
||||||
|
&& !"remote_busy".equals(reason)
|
||||||
|
&& !"ringrtc_hangup".equals(reason)) {
|
||||||
|
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, 0);
|
||||||
|
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
|
||||||
|
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
logger.warn("Failed to send hangup to remote for call {}", callIdUnsigned(callId));
|
||||||
|
logSendMessageResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send hangup via control channel (stdin) before killing process
|
||||||
|
if (state.controlWriter != null) {
|
||||||
|
try {
|
||||||
|
var hangupMsg = mapper.createObjectNode();
|
||||||
|
hangupMsg.put("type", "hangup");
|
||||||
|
state.controlWriter.println(writeJson(hangupMsg));
|
||||||
|
state.controlWriter.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.debug("Failed to send hangup via control channel", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill tunnel process
|
||||||
|
if (state.tunnelProcess != null && state.tunnelProcess.isAlive()) {
|
||||||
|
state.tunnelProcess.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRingTimeout(final long callId) {
|
||||||
|
var state = activeCalls.get(callId);
|
||||||
|
if (state == null) return;
|
||||||
|
|
||||||
|
if (state.state == CallInfo.State.RINGING_INCOMING || state.state == CallInfo.State.RINGING_OUTGOING) {
|
||||||
|
logger.debug("Call {} ring timeout", callIdUnsigned(callId));
|
||||||
|
endCall(callId, "ring_timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long generateCallId() {
|
||||||
|
return new BigInteger(64, new SecureRandom()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
for (var callId : new ArrayList<>(activeCalls.keySet())) {
|
||||||
|
endCall(callId, "shutdown");
|
||||||
|
}
|
||||||
|
synchronized (callEventListeners) {
|
||||||
|
callEventListeners.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal call state tracking ---
|
||||||
|
|
||||||
|
static class CallState {
|
||||||
|
|
||||||
|
final long callId;
|
||||||
|
volatile CallInfo.State state;
|
||||||
|
final RecipientId recipientId;
|
||||||
|
volatile Integer deviceId;
|
||||||
|
final boolean isOutgoing;
|
||||||
|
volatile String inputDeviceName;
|
||||||
|
volatile String outputDeviceName;
|
||||||
|
volatile Process tunnelProcess;
|
||||||
|
volatile PrintWriter controlWriter;
|
||||||
|
// Control messages queued before the tunnel process starts
|
||||||
|
final List<String> pendingControlMessages = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
// Accept deferred until tunnel reports Ringing state
|
||||||
|
volatile boolean acceptPending = false;
|
||||||
|
// True once the tunnel has reported "Ringing" (ready to accept)
|
||||||
|
volatile boolean tunnelRinging = false;
|
||||||
|
|
||||||
|
CallState(
|
||||||
|
long callId,
|
||||||
|
CallInfo.State state,
|
||||||
|
RecipientId recipientId,
|
||||||
|
final Integer deviceId,
|
||||||
|
boolean isOutgoing
|
||||||
|
) {
|
||||||
|
this.callId = callId;
|
||||||
|
this.state = state;
|
||||||
|
this.recipientId = recipientId;
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.isOutgoing = isOutgoing;
|
||||||
|
}
|
||||||
|
|
||||||
|
CallInfo toCallInfo(RecipientAddressResolver addressResolver) {
|
||||||
|
return new CallInfo(callId,
|
||||||
|
state,
|
||||||
|
addressResolver.resolveRecipientAddress(recipientId).toApiRecipientAddress(),
|
||||||
|
inputDeviceName,
|
||||||
|
outputDeviceName,
|
||||||
|
isOutgoing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user