Compare commits

...

947 commits

Author SHA1 Message Date
AsamK
f6d81e3c05 Update gradle
Some checks failed
signal-cli CI / build (21) (push) Has been cancelled
signal-cli CI / build (24) (push) Has been cancelled
signal-cli CI / build-graalvm (push) Has been cancelled
signal-cli CI / build-client (macos) (push) Has been cancelled
signal-cli CI / build-client (ubuntu) (push) Has been cancelled
signal-cli CI / build-client (windows) (push) Has been cancelled
CodeQL / Analyse (push) Has been cancelled
2025-08-17 17:35:59 +02:00
AsamK
42f10670b6 Replace deprecated groovy utils 2025-08-17 17:35:14 +02:00
AsamK
b453d7a0b9 Add new svr2 mrenclave 2025-08-02 12:05:01 +02:00
AsamK
f9a36c6e04 Fix send parameters to be all camel case
Fixes #1814
2025-07-16 20:59:26 +02:00
AsamK
be48afb2b5 Fix container build 2025-07-16 20:56:02 +02:00
AsamK
a0960fcabd Prepare next release 2025-07-16 20:55:50 +02:00
AsamK
dbc454ba9e Bump version to 0.13.18 2025-07-16 19:40:10 +02:00
AsamK
2225e69277 Update sqlite-jdbc 2025-07-16 19:37:03 +02:00
AsamK
783201d12e Fix incorrect error message 2025-07-16 19:17:21 +02:00
AsamK
3e981d66e9 Fix null pointer regression 2025-07-14 18:52:41 +02:00
AsamK
7c7fc76a64 Add support for sending view once messages
Closes #1812
2025-07-14 16:42:06 +02:00
AsamK
c924d5c03a Update libsignal-service-java 2025-07-14 16:21:47 +02:00
AsamK
dc787be17b Build rust json-rpc client in CI 2025-07-12 11:57:23 +02:00
AsamK
3d4070a139 Compile UnixStream support only on unix systems 2025-07-12 11:42:12 +02:00
AsamK
dbdff83132 Update README
Fixes #1803
2025-07-12 11:09:24 +02:00
AsamK
4ce194afe2 Add missing username parameter to getUserStatus command in json-rpc client 2025-07-12 11:03:54 +02:00
AsamK
ca33249170 Handle rate limit exception correctly when querying usernames
Fixes #1797
2025-07-12 11:03:28 +02:00
AsamK
a96626c468 Update to rust 2024 edition 2025-07-12 10:16:57 +02:00
AsamK
d54be747da Remove unused dependency 2025-07-12 10:15:27 +02:00
AsamK
ff846bc678 Fix clippy warnings 2025-07-12 10:05:57 +02:00
AsamK
1b7f755590 Update dependencies 2025-07-12 10:05:14 +02:00
AsamK
887ed3bb44 Show better error message when sending fails due to missing pre keys 2025-07-08 17:35:17 +02:00
AsamK
3180eba836 Exit if account check fails at startup
Fixes #1804
2025-07-08 17:34:04 +02:00
AsamK
cb06cbdcca Shut down when dbus daemon connection goes away unexpectedly
Fixes #1800
2025-06-29 11:22:30 +02:00
AsamK
069325af47 Extend shutdown request with optional error 2025-06-29 11:22:30 +02:00
AsamK
e7ca02f1fb Prepare next release 2025-06-29 11:22:30 +02:00
AsamK
fa9bb3c210 Bump version to 0.13.17 2025-06-28 14:57:20 +02:00
AsamK
e6113d4d96 Update libsignal-service-java 2025-06-28 14:35:56 +02:00
AsamK
6cc3a6f561 Update dependencies 2025-06-25 00:20:42 +02:00
AsamK
70c79eac01 Keep all unhandled fields of remote storage record
Fixes #1792
2025-06-24 23:13:00 +02:00
AsamK
5dc66f839d Close attachment input streams after upload
Fixes #1790
2025-06-10 19:36:52 +02:00
AsamK
a0d5744c49 Improve behavior when pin data doesn't exist on the server 2025-06-08 16:22:03 +02:00
AsamK
6b60a6d5a5 Fix NPR when loading an inactive group
Fixes #1786
2025-06-08 14:48:25 +02:00
AsamK
0257344940 Prepare next release 2025-06-08 14:47:20 +02:00
AsamK
17cd99be59 Bump version to 0.13.16 2025-06-07 16:58:00 +02:00
AsamK
2f8328847c Update dependencies 2025-06-07 16:14:55 +02:00
AsamK
7e9727aa38 Update tests 2025-06-07 16:14:55 +02:00
AsamK
bf87fcc652 Ensure messages are created with a unique timestamp
Fixes #1783
2025-06-03 22:22:51 +02:00
AsamK
6b46314eab Update dependencies 2025-06-03 21:59:38 +02:00
AsamK
e89803464b Update libsignal-service 2025-06-01 21:51:03 +02:00
AsamK
a9bb8d9aae Update gradle 2025-06-01 16:11:21 +02:00
AsamK
74909408c4 Add missing reflect config
Fixes #1768
2025-05-10 10:18:03 +02:00
AsamK
bb124a922d Prepare next release 2025-05-08 22:56:55 +02:00
AsamK
56e11d0857 Update codeql v3 2025-05-08 22:55:26 +02:00
AsamK
d0d0021f57 Bump version to 0.13.15 2025-05-08 21:49:51 +02:00
AsamK
7aafb05995 Update dependencies 2025-05-08 21:31:17 +02:00
AsamK
e594f3b237 Update libsignal-service 2025-05-08 21:31:17 +02:00
AsamK
bb86830a61 Add compatibility flag for graalvm native build 2025-05-08 20:42:13 +02:00
AsamK
bcc1eadc7d Remove unused e164 field from account record 2025-05-08 20:36:27 +02:00
AsamK
4fd9e55c3c Enable native access required by Java 24
Fixes #1765
2025-05-08 20:04:49 +02:00
AsamK
a2900085c9 Use java 24 in CI 2025-05-08 19:37:57 +02:00
AsamK
5e11cf1c50 Update gradle 2025-05-08 19:37:41 +02:00
AsamK
4e455d85d6 Add more logging to register 2025-04-26 09:04:05 +02:00
AsamK
1e685c7cab Extend merge/split logging 2025-04-09 20:44:10 +02:00
AsamK
ce813e4529 Update client dependencies 2025-04-08 16:24:57 +02:00
AsamK
bd7948e246 Prepare next release 2025-04-08 16:24:30 +02:00
AsamK
b998f322f5 Bump version to 0.13.14 2025-04-06 20:10:46 +02:00
AsamK
db2182aa7d Update libsignal-service 2025-04-06 20:02:02 +02:00
AsamK
69a9b30732 Update libsignal-service 2025-03-31 14:56:56 +02:00
AsamK
3dc8844cb4 Update libraries 2025-03-31 09:19:20 +02:00
AsamK
adb6787d5b Refresh prekeys when receiving message with invalid key id 2025-03-31 09:11:28 +02:00
AsamK
14b07be0dc Always renew session when failing to decrypt message 2025-03-31 09:11:05 +02:00
AsamK
6befda7ef1 Update graalvm build tools 2025-03-22 10:55:16 +01:00
AsamK
67302eb9c3 Replace cached envelopes when moving
Fixes #1730
2025-03-18 18:20:44 +01:00
AsamK
f18015ff2e f 2025-03-18 18:13:38 +01:00
AsamK
1295ef69ca Use record patterns 2025-03-16 22:07:29 +01:00
AsamK
f26a0d2891 Update libsignal-service 2025-03-16 22:06:58 +01:00
AsamK
2b150112ff Remove previous prekeys when importing legacy prekeys 2025-03-16 12:22:21 +01:00
AsamK
7aede7c17f Remove previous prekeys when importing legacy prekeys 2025-03-16 12:18:59 +01:00
AsamK
b92cbc6a7c Exclude libsignal-client testing libraries 2025-03-04 10:04:34 +01:00
AsamK
68b7416e57 Update gradle wrapper 2025-03-04 08:32:56 +01:00
AsamK
4feba68afd Replace deprecated gradle api 2025-03-04 08:29:05 +01:00
AsamK
4eb34c7a93 Update apt repository before installing packages 2025-02-28 09:46:45 +01:00
AsamK
26fd3e379a Prepare next release 2025-02-28 09:46:28 +01:00
AsamK
93d281e712 Bump version to 0.13.13 2025-02-28 09:36:27 +01:00
AsamK
985af6e445 Update libsignal-service 2025-02-28 09:32:17 +01:00
AsamK
5693d871f7 Update dependencies 2025-02-28 09:32:17 +01:00
AsamK
dba8cf7a6f Update reflect-config 2025-02-27 18:01:21 +01:00
AsamK
141d3326ab Add in-memory cache to KeyValueStore 2025-02-27 17:21:31 +01:00
AsamK
d3d2caac5a Tweak hikari config 2025-02-27 17:21:31 +01:00
AsamK
e1f4dae5c2 Show better error message when receiving an empty JSON RPC line
Fixes #1715
2025-02-27 11:39:31 +01:00
AsamK
cf5c943127 Remove legacy SVR2 enclave 2025-02-27 11:34:23 +01:00
AsamK
ed79e0b377 Check if required quote-author parameter is missing
Fixes #1716
2025-02-27 11:14:54 +01:00
Enguerran P.
a089a5ef04 Update README > landline procedure
Update landline (`--voice`) registration procedure.

This closes #1666 (🤘)
2025-02-07 23:16:46 +01:00
AsamK
90145655f4 Update CHANGELOG.md 2025-02-07 19:15:55 +01:00
AsamK
3cd07ae9cd Set libsignal network proxy to match java proxy
Fixes #1523
2025-02-07 18:30:10 +01:00
AsamK
8aa71c132f Fix log message 2025-02-07 18:30:10 +01:00
AsamK
b579935846 Update README 2025-02-07 18:30:10 +01:00
AsamK
dfa886fae9 Update dependencies 2025-02-07 18:30:10 +01:00
AsamK
f04f789231 Update gradle action 2025-01-31 16:45:47 +01:00
AsamK
a6ec71dc31 Add --mobilecoin-address as alias to updateProfile
Closes #1638
2025-01-30 20:18:07 +01:00
AsamK
47d65586cd Improve handling of unknown storage records
Fixes #1696
2025-01-30 19:57:44 +01:00
AsamK
b8d8413a22 Fix creating builder from contact
Fixes #1678
2025-01-23 17:12:40 +01:00
AsamK
5e16123632 Extend updateContact command with nick given/family name and note 2025-01-23 17:11:33 +01:00
AsamK
d57442bd2a Prepare next release 2025-01-19 13:26:12 +01:00
AsamK
70313c45a9 Update reflect-config.json
Fixes #1686
2025-01-19 13:25:08 +01:00
AsamK
f14c204764 Bump version to 0.13.12 2025-01-18 20:53:46 +01:00
AsamK
71d3b83a1c Update user agent 2025-01-18 20:24:01 +01:00
AsamK
148bf7dee2 Add man page to build tar file
Fixes #1660
2025-01-18 20:07:41 +01:00
AsamK
2d1ba6b4ca Extend man Makefile 2025-01-18 19:56:23 +01:00
AsamK
055a8ee8b9 Add general description for global/subcommand arguments
Fixes #1649
2025-01-18 16:59:25 +01:00
AsamK
407a20d4bb Update client dependencies 2025-01-18 16:53:09 +01:00
AsamK
05cd6aee6a Add version and group to all modules 2025-01-18 16:30:28 +01:00
AsamK
a1378507b2 Rename lib to libsignal-cli 2025-01-18 16:30:07 +01:00
AsamK
78cd0b13de Update dependencies 2025-01-18 16:08:43 +01:00
AsamK
c25468a71e Update reflect-config.json 2025-01-17 16:09:39 +01:00
AsamK
a5d2e1ea23 Use getRawQuery to prevent double decoding the query
Fixes #1682
2025-01-17 16:03:00 +01:00
AsamK
6acf16ef4e Improve tests 2025-01-14 23:12:45 +01:00
AsamK
e11e093020 Enable sqlite WAL journal_mode
Related #1670
2025-01-14 22:35:45 +01:00
AsamK
74c2604dc8 Set sqlite PRAGMA via Url 2025-01-14 22:31:36 +01:00
AsamK
e4af0be0ad Use existing connection to read configuration during storage sync 2025-01-14 21:33:12 +01:00
AsamK
5ac5938c8b Reduce log level of invalid sync contact address
Fixes #1663
2025-01-14 20:41:04 +01:00
AsamK
94269744ad Improve final address when merging recipients 2025-01-14 20:30:06 +01:00
AsamK
7a25ae5b9c Fix reading nickname from storage record
Fixes #1664
2025-01-14 20:30:06 +01:00
AsamK
cbd92654cf Store pni correctly in storage record 2025-01-14 20:30:06 +01:00
AsamK
bd95373a70 Parse unregisteredAtTimestamp correctly
Fixes #1651
Fixes #1646
2025-01-14 20:30:06 +01:00
AsamK
d982633215 Update dependencies 2025-01-14 20:30:06 +01:00
AsamK
f91ca82902 Prepare next release 2025-01-14 20:30:06 +01:00
Slayer
c55ee85c5c Fixing RW connections deadlock on SQLite
Without this change we're getting a connection in the same thread we hold one already.
2025-01-14 12:21:12 +01:00
AsamK
a3776c88bd Bump version to 0.13.11 2024-12-26 20:31:28 +01:00
AsamK
4a781656b4 Update dependencies 2024-12-26 20:23:08 +01:00
AsamK
11d38f29ef Improve splitting of long message bodies 2024-12-26 20:15:19 +01:00
AsamK
22a0ff976a Update libsignal-service 2024-12-26 20:14:53 +01:00
AsamK
c05b47e4d0 Delete storage id of unregistered recipients after remote update 2024-12-25 17:19:33 +01:00
AsamK
ac145e6a27 Ignore destination if it's an empty uuid
Fixes #1643
2024-12-25 16:23:36 +01:00
AsamK
f00b8523d9 Update dependencies 2024-12-15 21:18:02 +01:00
AsamK
c3f8d68ceb Create account entropy pool instead of master key 2024-12-15 21:14:40 +01:00
AsamK
9d92a3e06b Update libsignal-service 2024-12-15 21:13:59 +01:00
AsamK
f2df600d38 Update CHANGELOG.md
Fixes #1641
2024-12-01 10:12:46 +01:00
AsamK
24d344fda4 Prepare next release 2024-11-30 16:32:31 +01:00
AsamK
0a296e77a0 Bump version to 0.13.10 2024-11-30 16:14:53 +01:00
AsamK
ba147a48f8 Update reflect config 2024-11-30 15:45:26 +01:00
AsamK
77a5c454b7 Improve documentation for recipient arguments
Fixes #1639
2024-11-29 21:10:46 +01:00
AsamK
2c68b5a9e1 Add support for using PNI as recipient 2024-11-29 21:10:46 +01:00
AsamK
68c9d84d19 Update libsignal-service
Fixes #1633
2024-11-24 13:04:04 +01:00
AsamK
fe752e0c79 Add simplification for single recipient reactions 2024-11-24 11:51:54 +01:00
AsamK
26b5a4c582 Small code improvement 2024-11-23 23:57:23 +01:00
AsamK
10ee295ea3 Fix receiving shared contacts in graalvm mode
Fixes #1629
2024-11-23 23:57:23 +01:00
AsamK
6a5ea5fc01 Workaround issue with invalid address in recipient store 2024-11-23 23:57:23 +01:00
AsamK
ff6cb5262a Update libsignal-service
Support for storage encryption v2 and account entropy pool

Fixes #1632
2024-11-23 23:57:23 +01:00
AsamK
f2005593ec Reformat files 2024-11-23 22:35:06 +01:00
AsamK
3533500b73 Update dependencies 2024-11-23 22:35:06 +01:00
AsamK
e5251ae158 Switch to using toml version catalogs 2024-11-21 21:17:33 +01:00
AsamK
a5e272be3f Prepare next release 2024-10-28 23:45:03 +01:00
AsamK
277652e3f2 Bump version to 0.13.9 2024-10-28 23:37:26 +01:00
AsamK
181086aefe Update graalvm config
Fixes #1612
2024-10-28 10:58:14 +01:00
AsamK
a1d552698a Fix verifyAccount
Fixes #1614
2024-10-28 10:42:26 +01:00
AsamK
1b959608c3 Simplify verification code request 2024-10-28 10:35:18 +01:00
AsamK
acddef6e35 Prepare next release 2024-10-26 15:02:28 +02:00
AsamK
0e77870b59 Bump version to 0.13.7 2024-10-26 15:01:05 +02:00
AsamK
0a287b0b3e Reformat files 2024-10-26 13:10:33 +02:00
AsamK
5a4f4ba6db Implement message expiration timer version
Fixes #1605
2024-10-26 13:08:21 +02:00
AsamK
5171107a29 Run CI with Java 23 2024-10-25 17:22:47 +02:00
AsamK
47e6fc1769 Update dependencies 2024-10-25 17:20:07 +02:00
AsamK
9afd4e4328 Update libsignal-service 2024-10-25 17:20:07 +02:00
Jailson Dias
eac2a47163 add group info on json message 2024-10-25 17:19:17 +02:00
Jailson Dias
5646f65195 add received and delivered timestamps on json message 2024-10-25 17:19:17 +02:00
AsamK
fab1b96c21 Upload text attachment also if there no other attachments
Fixes #1598
2024-09-29 09:43:51 +02:00
AsamK
91eacc18c2 Refactor attachment upload 2024-09-29 09:43:26 +02:00
AsamK
7f1fc932ad Add constant for MAX_MESSAGE_BODY_SIZE 2024-09-29 09:43:05 +02:00
AsamK
304c44064d Prepare next release 2024-09-28 22:14:26 +02:00
AsamK
946c483f35 Bump version to 0.13.7 2024-09-28 22:13:24 +02:00
AsamK
69d691f416 Update graalvm reflect-config
Fixes #1590
2024-09-28 20:41:58 +02:00
AsamK
a6ab8f7e80 Remove v2 suffix from secure value recovery 2024-09-28 20:41:58 +02:00
AsamK
19dc2d446b Update libsignal-service 2024-09-28 20:41:58 +02:00
AsamK
8524037900 Update gradle 2024-09-28 12:13:49 +02:00
AsamK
dbbc3fbd71 Remove unnecessary proto conversion 2024-09-24 17:28:24 +02:00
AsamK
c6e93126fa Fix truncating cdsi table
Fixes #1587
2024-09-12 23:09:20 +02:00
AsamK
352699c4b6 Update artifact CI actions 2024-09-11 17:50:01 +02:00
AsamK
bea2772491 Update graalvm buildtools 2024-09-10 19:16:42 +02:00
AsamK
bab8ddf35a Update slf4j 2024-09-09 18:31:47 +02:00
AsamK
1d5d16f57e Adapt signal_jni file names in graalvm config 2024-09-09 18:31:34 +02:00
AsamK
6f9e9e9302 Prepare next release 2024-09-08 19:24:18 +02:00
AsamK
20add0f27b Downgrade dbus-java 2024-09-08 19:24:18 +02:00
AsamK
eca3c6fa30 Replace deprecated DBusMap 2024-09-08 19:24:18 +02:00
AsamK
a0d1b081ff Bump version to 0.13.6 2024-09-08 18:59:22 +02:00
AsamK
d852c60c37 Update dependencies 2024-09-08 18:55:23 +02:00
AsamK
2b5451f74a Update client dependencies 2024-09-08 18:51:56 +02:00
AsamK
b322716215 Update CHANGELOG.md 2024-09-08 10:18:26 +02:00
AsamK
41726d339f Update libsignal-service-java 2024-09-08 10:18:17 +02:00
AsamK
52bb92dce4 Call getter instead of using cached instance variable 2024-09-08 10:15:02 +02:00
AsamK
65adfdd6f5 Add signal CDN 3 2024-09-08 10:15:02 +02:00
AsamK
d8b1a2fffe Fix stripping the correct identifiers when merging recipients 2024-09-08 09:24:51 +02:00
AsamK
ea436ecb64 Fix possible db dead lock
Fixes #1483
2024-09-08 09:22:52 +02:00
AsamK
82fbde4f19 Adapt code style 2024-09-08 09:00:39 +02:00
AsamK
19b15e68e4 Improve addDevice error message
Fixes #1573
2024-09-08 09:00:34 +02:00
AsamK
b51f849fe6 Send sync message for read/viewed receipt messages
Fixes #1570
2024-09-08 08:51:33 +02:00
AsamK
7cc0ef1c70 Improve error message and log output for failed jsonrpc commands 2024-09-08 08:30:31 +02:00
AsamK
485c4fd467 Improve daemon deprecation message 2024-09-08 08:30:08 +02:00
AsamK
bda395191b Don't set previousE164s param if cdsi token is empty
Fixes #1576
2024-09-08 08:29:54 +02:00
AsamK
cb129db95e Update libsignal-service 2024-08-20 17:39:39 +02:00
AsamK
130cee9906 Fix sending to groups with non sender key capable members
Regression in 0.13.5

Fixes #1568
2024-08-20 17:13:57 +02:00
AsamK
6bdc9a4b16 Update gradle 2024-08-20 17:10:03 +02:00
AsamK
a69a9b7b0e Add method to update group endorsements 2024-08-20 17:10:03 +02:00
AsamK
6ea373fbd5 Prepare next release 2024-08-20 17:10:03 +02:00
Ingmar Lippert
62da514850
Add more specificity to addDevice (#1561) 2024-08-20 16:59:52 +02:00
AsamK
fe3934171d Bump version to 0.13.5 2024-07-25 22:47:16 +02:00
AsamK
3d11221732 Update jackson 2024-07-25 22:43:55 +02:00
AsamK
e961488b1a Update man page 2024-07-25 22:40:46 +02:00
AsamK
2db3d3259e Use UploadSpec for attachment uploads 2024-07-25 22:31:35 +02:00
AsamK
e51b1ee23a Update libsignal-service 2024-07-25 16:27:24 +02:00
AsamK
5ff66728e3 Update libsignal-service 2024-06-26 15:38:55 +02:00
AsamK
baf7b74a61 Prepare next release 2024-06-06 11:15:02 +02:00
AsamK
c716f94649 Bump version to 0.13.4 2024-06-06 10:47:42 +02:00
AsamK
8b355918e8 Use jvm running gradle if it's compatible with targetCompatibility 2024-06-06 10:40:21 +02:00
AsamK
67012b40b1 Update user agent 2024-06-06 10:16:53 +02:00
AsamK
fd402b52c8 Update doc 2024-06-06 10:16:40 +02:00
AsamK
5a97b9e134 Update groups when using listGroups command
Fixes #1517
2024-06-06 10:07:20 +02:00
AsamK
17596795c2 Only store profile keys for group history if none is known yet 2024-06-06 10:07:20 +02:00
AsamK
10b9c264fd Update libsignal-service
Fixes #1530
2024-06-06 10:07:20 +02:00
AsamK
a2b002ac5e Add java 22 to CI 2024-06-06 09:12:40 +02:00
AsamK
6d764db114 Update dependencies 2024-06-02 13:26:17 +02:00
AsamK
777cfbb69f Update gradle wrapper 2024-06-02 13:25:09 +02:00
AsamK
9781c56571 Improve username update error message
Fixes #1535
2024-05-24 16:09:07 +02:00
AsamK
04cf54263e Fix getUserStatus command with only username parameter
Related #1535
2024-05-23 12:46:15 +02:00
AsamK
6baf0eac13 Fix type parsing in JSON RPC mode
Fixes #1533
2024-05-21 20:28:16 +02:00
AsamK
fb21a42cce Update graalvm build tools 2024-05-18 22:13:29 +02:00
AsamK
53d7e0f08b Handle all possible identifiers of a RecipientAddress
Fixes #1516
2024-05-17 18:02:05 +02:00
AsamK
8f756cd90c Update libsignal-service 2024-05-09 21:32:34 +02:00
Sebastian Scheibner
fb81bf1d05
Update README.md 2024-05-09 21:20:38 +02:00
AsamK
04726f005c Save account file after setting username link
Fixes #1515
2024-05-01 09:08:00 +02:00
AsamK
09e3e7f335 Rotate storageId after setting username 2024-05-01 09:07:39 +02:00
AsamK
90b1e4bc02 Delete username link when deleting username 2024-05-01 09:04:03 +02:00
AsamK
3f31f1a8a6 Update metainfo 2024-04-21 09:34:06 +02:00
dependabot[bot]
3cd8e323c9
Bump rustls from 0.21.10 to 0.21.11 in /client (#1511)
Bumps [rustls](https://github.com/rustls/rustls) from 0.21.10 to 0.21.11.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.21.10...v/0.21.11)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-20 13:53:36 +02:00
AsamK
7060faf5d3 Prepare next release 2024-04-19 19:25:27 +02:00
AsamK
c9f2cca024 Bump version to 0.13.3 2024-04-19 19:21:07 +02:00
AsamK
f0054372b8 Add timestamp to account file
Closes #1498
2024-04-19 17:25:43 +02:00
AsamK
0a82c51b79 Update dependencies 2024-04-19 17:17:32 +02:00
AsamK
cef83d962c Fix missing null check 2024-04-19 17:07:29 +02:00
AsamK
c1775913b9 Implement dbus support for listIdentities
Fixes #195
2024-04-19 17:07:18 +02:00
AsamK
e4c5144fbf Add more details to listContacts command
Fixes #1502
2024-04-17 21:26:16 +02:00
AsamK
8aeaf927e6 Add missing parts for new nick name and note columns 2024-04-17 21:21:28 +02:00
AsamK
7e0d4c9b89 Store profile phone number sharing mode and discoverable state 2024-04-17 21:08:09 +02:00
AsamK
71de8e63cc Cache newly created session record
Fixes #1481
2024-04-15 19:23:50 +02:00
AsamK
e456d06cb0 Add aci,pni to API RecipientAddress 2024-04-15 19:23:50 +02:00
AsamK
e0cd5b987e Add handling for new nickname and note fields 2024-04-15 19:23:50 +02:00
Stephen Brennan
e5ebb732cb
Document the unit of "start" and "length" for mentions and text styles (#1505)
The unit of UTF-16 code units is not necessarily obvious for users of
languages that index strings by Unicode code points. Provide a pointer
to an FAQ entry as well:

https://github.com/AsamK/signal-cli/wiki/FAQ#string-indexing-units

Closes #1504

Signed-off-by: Stephen Brennan <stephen@brennan.io>
2024-04-13 20:26:15 +02:00
AsamK
419beee29a Update libsignal-service-java 2024-04-06 14:04:56 +02:00
AsamK
d4e1f9b7f1 Remove unnecessary config field 2024-04-06 14:04:56 +02:00
AsamK
edce33ae15 Disable java 22 until gradle supports it 2024-04-06 14:04:56 +02:00
dependabot[bot]
95f9e18de2
Bump h2 from 0.3.24 to 0.3.26 in /client (#1501)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-06 14:04:48 +02:00
AsamK
95d4b7d012 Update gradle wrapper 2024-04-06 12:34:55 +02:00
AsamK
17c24b3ff2 Update libsignal-service-java 2024-03-27 22:58:13 +01:00
AsamK
be0e8ddd8a Add reregister to tests 2024-03-27 22:58:13 +01:00
AsamK
49cc9cd9f8 Update CHANGELOG.md 2024-03-23 15:00:02 +01:00
AsamK
c85c995fef Prepare next release 2024-03-23 10:34:45 +01:00
AsamK
dda23e76ac Bump version to 0.13.2 2024-03-23 09:57:15 +01:00
AsamK
95e70b9d15 Add Java 22 to CI 2024-03-23 09:50:59 +01:00
AsamK
abddf24752 Update user agent 2024-03-23 09:50:59 +01:00
AsamK
d356d92b5e Extend getUserStatus command for usernames 2024-03-22 10:54:42 +01:00
AsamK
8b4f377cf1 Update dependencies 2024-03-22 10:04:57 +01:00
dependabot[bot]
323a801600
Bump mio from 0.8.10 to 0.8.11 in /client (#1488)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-09 13:08:46 +01:00
AsamK
3372992dc2 Add appstream metainfo file 2024-03-09 11:35:36 +01:00
AsamK
ee39733978 Update libsignal-service 2024-03-07 17:44:19 +01:00
AsamK
5e17fe8414 Fix typo 2024-02-27 19:35:38 +01:00
AsamK
aebe64571d Prepare next release 2024-02-27 18:20:21 +01:00
AsamK
0cc2da690a Bump version to 0.13.1 2024-02-27 18:18:30 +01:00
AsamK
2424fc1f53 Add register parameter to force reregistration 2024-02-27 18:12:43 +01:00
AsamK
2e4cd0eddc Only retry messages after identity was trusted
Fixes #1477
2024-02-27 17:52:21 +01:00
AsamK
189b21dbde Improve error message if captcha is rejected by server
Fixes #1328
2024-02-26 22:13:57 +01:00
AsamK
e77d9e3d60 Update libsignal-service 2024-02-26 22:07:36 +01:00
AsamK
df76aa9919 Default number sharing to NOBODY
Matches the official apps behavior.

Closes #1472
2024-02-26 18:27:09 +01:00
AsamK
378ac23c6c Update account attributes after setting a pin
Ensures that the recovery password gets set immediately.

Related to #1447
2024-02-26 18:23:37 +01:00
AsamK
fc2ae856d2 Adapt account record processor for linked devices 2024-02-25 19:41:34 +01:00
AsamK
57164ad7fb Prevent crash when receiving already migrated group v1 from storage
Fixes #1471
2024-02-25 19:41:10 +01:00
AsamK
6c44662496 Allow overriding user agent string
Not recommended, as it could lead to issues with newer Signal protocol changes.

Fixes #1476
2024-02-25 18:27:20 +01:00
AsamK
22ac3cb50f Removing linked devices only works on the primary device 2024-02-25 18:12:36 +01:00
AsamK
b76964f219 Improve warning message 2024-02-25 17:54:18 +01:00
AsamK
2f3c064462 Update documentation 2024-02-25 17:46:27 +01:00
AsamK
c9002d9481 Ignore failure when uploading PNI prekeys
Can happen if PNI identity key hasn't been sent to the server yet.
2024-02-25 17:46:09 +01:00
AsamK
83d471818d Allow setting a username with explicit descriminator
Fixes #1469
2024-02-22 20:00:00 +01:00
AsamK
0bb2a64781 Add missing field handling in account record processor 2024-02-22 20:00:00 +01:00
AsamK
08ba774b71 Update graalvm buildtools 2024-02-20 17:37:59 +01:00
AsamK
59c1f4eed2 Show information when requesting voice verification without SMS verification
Fixes #1373
2024-02-20 17:37:31 +01:00
AsamK
f1e3b5c9cc Add uniqueness check to db migration 2024-02-20 17:05:56 +01:00
AsamK
7bc7242f08 Add uniqueness check to db migration 2024-02-20 11:03:32 +01:00
Viktor Szépe
6f407ab509
Fix typos (#1459) 2024-02-19 08:20:24 +01:00
AsamK
db9acaf4ff Set snapshot version 2024-02-18 22:14:09 +01:00
AsamK
7821815429 Bump version to 0.13.0 2024-02-18 22:01:41 +01:00
AsamK
cd88e896fa Fix missing unique constraint on storage_id table 2024-02-18 22:01:41 +01:00
AsamK
67fd10b978 Update dependencies 2024-02-18 21:43:02 +01:00
AsamK
9051f68ba6 Enable profile sharing when accepting message request 2024-02-18 21:26:36 +01:00
AsamK
3b3377e6e9 Update CHANGELOG 2024-02-18 21:06:17 +01:00
AsamK
fb99ff5284 Document creating and sending to usernames 2024-02-18 21:04:50 +01:00
AsamK
ed40d116b7 Fix sending to username 2024-02-18 20:58:55 +01:00
AsamK
d84362eb0f Add sendMessageRequestResponse command 2024-02-18 20:48:16 +01:00
AsamK
2c0ad7feb7 Fix issue with prekey update 2024-02-18 20:48:16 +01:00
AsamK
7206b4da25 Fix use of aci instead of serviceId
Fixes #1455
2024-02-18 20:48:01 +01:00
AsamK
fd671576a4 Fix issues with receiving message to PNI address 2024-02-18 20:46:07 +01:00
AsamK
6cd57312a1 Add parameter to configure phone number privacy 2024-02-18 20:42:41 +01:00
AsamK
25258db55d Restructure pre key refresh to be more robust 2024-02-18 16:32:50 +01:00
AsamK
91ed49e019 Includ pni signatures if necessary 2024-02-18 16:31:34 +01:00
AsamK
bd792f5b7f Update libsignal-service 2024-02-18 15:16:41 +01:00
AsamK
ed4b1e8f02 Improve sql query 2024-02-10 12:24:13 +01:00
AsamK
e78146eb67 Fix saving username link components 2024-02-10 11:54:32 +01:00
AsamK
9f6b6cb657 Implement account reregistration with recovery password 2024-02-10 11:54:06 +01:00
AsamK
fb85e1a068 Update dependencies 2024-02-09 22:51:03 +01:00
AsamK
c78299e7bc Update gradle wrapper 2024-02-09 22:11:50 +01:00
AsamK
9a5e29e015 Update CHANGELOG.md 2024-02-09 22:11:01 +01:00
AsamK
7cf3a989bf Add command to retrieve avatars and stickers
Fixes #1125
2024-02-09 22:10:46 +01:00
AsamK
d486563099 Ignore invalid ACI/PNI in recipient table 2024-02-09 18:34:32 +01:00
AsamK
be699cbd85 Fix potential dead lock in recipient store 2024-02-09 18:07:30 +01:00
AsamK
4e61f2b2e5 Use app id as dbus name for flatpak version 2024-02-09 18:07:30 +01:00
AsamK
1bf703b012 Add --bus-name option to use different D-Bus bus name 2024-02-09 18:07:30 +01:00
AsamK
a48fdaf8b7 Bump version to 0.12.8 2024-02-09 18:07:30 +01:00
AsamK
a0f33ac942 Update reflect config 2024-02-09 18:07:30 +01:00
AsamK
bea659f948 Add icon 2024-02-05 16:58:24 +01:00
AsamK
7fb263fdf0 Update dependencies 2024-02-01 17:54:26 +01:00
AsamK
3be1c24b1b Update client dependencies 2024-01-31 21:17:23 +01:00
AsamK
0b33cb55b8 Adapt listContacts to only show registered users
Fixes #1395
2024-01-31 20:49:12 +01:00
AsamK
00e71ed0fc Improve scrubbing of sensitive identifiers
Fixes #1189
2024-01-31 20:33:01 +01:00
AsamK
a40810e33e Update CHANGELOG.md 2024-01-30 19:00:33 +01:00
AsamK
15c9d04703 Add missing command to json-rpc client 2024-01-30 18:58:27 +01:00
AsamK
e13dcdc85a Prevent empty username 2024-01-30 17:59:59 +01:00
AsamK
2ab5b2817e Deprecate daemon mode without explicit mode flag 2024-01-30 17:44:51 +01:00
AsamK
080c14d111 Clean old prekeys only after server message queue is empty 2024-01-30 17:21:29 +01:00
AsamK
d1e32f2b11 Improve some log output 2024-01-29 18:21:44 +01:00
AsamK
0840e0dedb Add CDSI recipients refresh job 2024-01-28 23:04:34 +01:00
AsamK
888d6bf091 Split unregistered recipients 2024-01-28 23:04:34 +01:00
AsamK
e2f308a57a Store profile sharing for group v2 2024-01-28 22:38:41 +01:00
AsamK
76fe6ad799 Recreate recipient database with aci column 2024-01-28 22:38:41 +01:00
AsamK
90df256e85 Fix storage sync issues 2024-01-28 22:38:41 +01:00
AsamK
f92466f6be Store recipient unregistered state 2024-01-28 22:38:41 +01:00
AsamK
6a5dcd00b2 Implement remote storage sync
Closes #604
2024-01-28 22:38:41 +01:00
AsamK
6c65de8ddf Update libsignal-service 2024-01-26 23:32:29 +01:00
AsamK
f696097301 Improve JobExecutor 2024-01-25 21:47:40 +01:00
AsamK
7d3db03d4a Update dbusjava 2024-01-25 21:03:25 +01:00
AsamK
78c87e501e Improve log level 2024-01-25 21:00:16 +01:00
AsamK
2974b466aa Update sqlite 2024-01-22 18:40:46 +01:00
AsamK
ca1a852b44 Handle mentions when receiving from dbus 2024-01-22 18:40:39 +01:00
dependabot[bot]
ac2343b142
Bump h2 from 0.3.21 to 0.3.24 in /client (#1418)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.21 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.21...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-20 09:18:20 +01:00
AsamK
be9efb9a25 Update libsignal-service-java 2024-01-14 11:42:48 +01:00
AsamK
30e8e36635 Add --unrestricted-unidentified-sender to updateAccount command 2024-01-13 16:00:55 +01:00
AsamK
3290a5bf4d Add --notify-self parmeter
Fixes #1087
2024-01-12 18:09:42 +01:00
AsamK
c07ba14fc6 Update dependencies 2024-01-04 18:09:36 +01:00
AsamK
c6e9e19858 Update libsignal-service-java 2024-01-04 18:00:19 +01:00
morph027
375bdb7948
add file.encoding=UTF-8 to native builds, refs #1105 (#1404)
Signed-off-by: morph027 <stefan.heitmueller@gmx.com>
2023-12-19 18:48:54 +01:00
AsamK
5e7899675e Update CHANGELOG.md 2023-12-15 19:53:35 +01:00
AsamK
328ba0202f Bump version to 0.12.7 2023-12-15 19:10:56 +01:00
AsamK
59cbbde835 Update CHANGELOG.md 2023-12-15 19:05:13 +01:00
AsamK
caa4fa0180 Fix inspection issues 2023-12-15 17:32:30 +01:00
AsamK
fc6a4b78eb Update libsignal-service-java
Uses new device link endpoint

Fixes #1399
2023-12-14 18:07:13 +01:00
AsamK
2729772adb Fix release pipeline 2023-12-11 19:11:26 +01:00
AsamK
70e0f7027e Bump version to 0.12.6 2023-12-11 18:32:25 +01:00
AsamK
4dcdffda0a Use a new SVR2 enclave. 2023-12-10 19:30:36 +01:00
AsamK
2535cd9c2c Update libsignal-service-java 2023-12-10 18:57:05 +01:00
AsamK
d3eef6c820 Update dependencies 2023-12-06 18:12:17 +01:00
AsamK
35fb1d60af Update gradle wrapper 2023-12-06 18:11:27 +01:00
AsamK
089ec22666 Fix overlapping sqlite sessions in migration
Fixes #1394
2023-12-06 18:09:02 +01:00
AsamK
66077f317b Add missing capabilities for linking account
Fixes #1386
2023-11-28 19:12:53 +01:00
AsamK
5904bfa92b Update CHANGELOG 2023-11-21 21:14:16 +01:00
AsamK
2c1d7a803f Set snapshot version 2023-11-21 21:10:44 +01:00
AsamK
6d5b8b6a7b Bump version to 0.12.5 2023-11-21 21:07:54 +01:00
AsamK
b88f8c205e Update dependencies 2023-11-21 21:02:47 +01:00
AsamK
ce4afbe4c2 Update libsignal-service-java
Fixes #1384
2023-11-21 21:02:47 +01:00
AsamK
fcaecef961 Improve source serviceId handling 2023-11-21 19:59:06 +01:00
AsamK
81d2e315e3 Prevent groupV2Operations from being null 2023-11-21 17:17:26 +01:00
AsamK
fddf982832 Fix receiving contact address
Fixes #1385
2023-11-21 17:09:21 +01:00
AsamK
3602ef9be9 Add -u flag to send to username 2023-11-21 17:09:21 +01:00
AsamK
37c65ca6b4 Implement username links 2023-11-21 17:09:21 +01:00
AsamK
77f284661b Fix storing of username 2023-11-12 19:33:40 +01:00
AsamK
0d60c4d464 Refactor group v2 migration 2023-11-12 19:33:40 +01:00
AsamK
f06eeb01b9 Refactor getGroup method 2023-11-12 15:21:29 +01:00
AsamK
fbcc1cfb50 Refactor KeyValueStore 2023-11-12 14:24:26 +01:00
AsamK
26cef99cdf Don't store self profile key in recipient store 2023-11-12 13:13:58 +01:00
AsamK
5cac7feabe Add who am I job 2023-11-12 12:12:02 +01:00
AsamK
d844e0f396 Add download profile avatar job 2023-11-12 11:59:48 +01:00
AsamK
ed11bf6368 Make JobExecutor execute jobs asynchronously 2023-11-12 11:51:38 +01:00
AsamK
eaa6b7cf57 Check whoAmI after regular CDSI sync 2023-11-11 18:12:22 +01:00
AsamK
f8ea631b03 Refactor trusted recipient resolver 2023-11-11 17:24:45 +01:00
AsamK
7b0744ec75 Add --hide parameter to removeContact command 2023-11-11 11:39:33 +01:00
AsamK
9f4a2b3e26 Convert Contact to record 2023-11-11 11:09:24 +01:00
AsamK
9741c93ce9 Move all dbus interaction to dbus package 2023-11-10 15:44:42 +01:00
AsamK
7e9940be4a Refactor DaemonCommand 2023-11-10 15:32:45 +01:00
AsamK
c0aa338d7c Reorder static final modifier 2023-11-10 14:03:13 +01:00
AsamK
b20978e08e Remove now unnecessary SuccessExitStatus from systemd units 2023-11-09 23:26:38 +01:00
AsamK
5d33f71d4d Use improved shutdown for receive command 2023-11-09 19:23:11 +01:00
AsamK
1058e33f12 Use improved shutdown for daemon command 2023-11-09 19:23:11 +01:00
AsamK
b7fedff511 Use improved shutdown for jsonRpc command 2023-11-09 19:23:11 +01:00
AsamK
f252597002 Add infrastructure for better shutdown handling 2023-11-09 19:23:11 +01:00
AsamK
cbbfc4ea6e Rename exportManager method 2023-11-09 18:54:17 +01:00
AsamK
6b04197eaa Refactor getManagerFromQuery method 2023-11-09 18:54:17 +01:00
AsamK
3027ba2cf1 Refactor log config 2023-11-09 15:55:43 +01:00
AsamK
19e9f31afd Remove now unnecessary try/catch 2023-11-09 09:22:46 +01:00
AsamK
5a1d2580cb Use a single PushServiceSocket 2023-11-09 09:22:46 +01:00
AsamK
cb5cace8da Update libsignal-service 2023-11-09 09:22:46 +01:00
AsamK
e734c125ad Ignore quotes without author
Fixes #1369
2023-11-08 12:40:34 +01:00
AsamK
d351f64fb1 Configure log LevelChangePropagator 2023-11-08 12:05:55 +01:00
AsamK
e61f587bfc Reduce use of printStackTrace 2023-11-07 09:28:29 +01:00
AsamK
4e8f0a41c7 Update dependencies 2023-11-07 09:11:36 +01:00
AsamK
4fbc97c92a Register java exit handlers in graalvm native build 2023-11-07 08:54:05 +01:00
AsamK
1e35ac380e Refactor DaemonCommand 2023-11-07 08:54:05 +01:00
AsamK
85b0647a3e Extract getReceiveConfig to utils 2023-11-07 08:54:05 +01:00
spezialist1
7b899d1853
Add example syntax for a mixed text style (#1367) 2023-11-06 11:58:11 +01:00
AsamK
699b21f066 Handle rate limit error in JSON-RPC mode 2023-11-05 16:20:33 +01:00
AsamK
8d423adb4d Use safety numbers with ACI by default 2023-11-05 11:59:09 +01:00
AsamK
506dcfa6c0 Update README.md 2023-11-04 11:32:42 +01:00
AsamK
91e0d5164b Ensure profile key is stored in profileKeyStore
Fixes #1362
2023-11-03 20:31:35 +01:00
AsamK
67d8ffcde5 Initialize database before registering 2023-11-03 20:19:41 +01:00
AsamK
44c9aded65 Don't check self number in recipients refresh 2023-11-03 20:01:05 +01:00
AsamK
f1ccfc0361 Initialize pre key offsets when creating new account 2023-11-03 20:00:26 +01:00
AsamK
cdef9c435c Check if account is already registered before attempting verification 2023-11-03 19:59:39 +01:00
AsamK
ed8ac5b84c Use new threads API 2023-10-24 17:36:32 +02:00
AsamK
8d55dfb66b Use pattern matching switch cases 2023-10-24 17:36:32 +02:00
AsamK
80c1a6d2af Switch to Java 21 2023-10-24 17:36:32 +02:00
AsamK
895740755d Remove included by default native-image component 2023-10-23 18:01:49 +02:00
AsamK
90601b7b6b Fix man page generation 2023-10-22 21:33:15 +02:00
AsamK
cc2ffbb4da Bump version to 0.12.4 2023-10-22 20:49:22 +02:00
AsamK
0c24064939 Bump version to 0.12.4 2023-10-22 20:42:25 +02:00
AsamK
7859084be8 Fix tests 2023-10-22 20:42:25 +02:00
AsamK
101c217ef8 Update captcha help text 2023-10-22 20:42:25 +02:00
AsamK
7887a5a613 Prevent unnecessary compile warnings 2023-10-22 20:42:25 +02:00
AsamK
800ff09a37 Update dependencies 2023-10-22 20:42:25 +02:00
AsamK
20f8fa2ebd Prevent ConcurrentModificationException
Fixes #1351
2023-10-22 20:42:25 +02:00
AsamK
314159c273 Update libsignal-service-java 2023-10-22 20:42:25 +02:00
Vladimir Glinskikh
bf16885278
Bump upgrade gradle from 8.3 to 8.4 (#1353) 2023-10-21 12:49:28 +02:00
AsamK
3ae6f7ab7c Update graalvm reflect-config 2023-10-19 23:31:41 +02:00
AsamK
e829dd8b5c Update graalvm reflect-config
Fixes #1352
2023-10-19 23:18:07 +02:00
dependabot[bot]
62c71eaafa
Bump rustix from 0.37.23 to 0.37.25 in /client (#1350)
Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.37.23 to 0.37.25.
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.37.23...v0.37.25)

---
updated-dependencies:
- dependency-name: rustix
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 08:01:22 +02:00
AsamK
24ec25ac62 Fix typo 2023-10-18 22:57:51 +02:00
Pēteris Caune
19b5c76154
Fix typo (#1348) 2023-10-18 07:47:10 +02:00
AsamK
2852b2ea6d Extend log scrubber for dbus serviceIds 2023-10-17 23:05:45 +02:00
AsamK
fc2e9bbfae Fix inspections 2023-10-17 20:34:21 +02:00
AsamK
d51dd7ae57 Use .isEmpty() for checking lists and strings 2023-10-17 20:34:21 +02:00
AsamK
9ba70c1808 Update bouncycastle dependency 2023-10-17 19:48:22 +02:00
AsamK
9ec942ea1d Update workflow permission 2023-10-17 19:34:42 +02:00
AsamK
b9e644269b Use gradle-build-action for caching and dependency submission 2023-10-17 19:22:09 +02:00
AsamK
5b56445741 Update github checkout action 2023-10-17 19:21:41 +02:00
AsamK
1ed5148624 Bump version to 0.12.3 2023-10-17 15:24:56 +02:00
AsamK
fb7c63c507 Reduce sqlite logging 2023-10-17 15:21:18 +02:00
AsamK
2c5edbc981 Add cache for serviceId to recipient id/address mapping 2023-10-17 15:20:14 +02:00
AsamK
1addffe622 Store username aci link in recipient store 2023-10-17 14:58:15 +02:00
AsamK
733c14bbc8 Ignore invalid recipient numbers 2023-10-17 14:19:32 +02:00
AsamK
ca2e6adedb Update dependencies 2023-10-17 14:18:35 +02:00
AsamK
5cc20ace1f Ignore failures from SVR v1 pin 2023-10-17 13:34:09 +02:00
AsamK
400dcf2899 Refactor creating linked account files 2023-10-16 19:01:31 +02:00
AsamK
dd3326f038 Do a full recipients refresh every day 2023-10-16 19:01:31 +02:00
AsamK
505de39d2a Use partial cdsi request only for a maximum of 3 numbers
Fixes #1239
2023-10-16 19:01:08 +02:00
AsamK
24069c8277 Update documentation 2023-10-15 22:37:46 +02:00
AsamK
bb78e9aaeb Update libsignal-service-java 2023-10-15 22:37:01 +02:00
AsamK
5c39344cff Implement full CDSI refresh 2023-10-15 22:36:45 +02:00
AsamK
7cd24a74af Improve handling of CDSI resource exhaustion 2023-10-15 19:02:59 +02:00
AsamK
a675631965 Resolve self address after setting new PNI 2023-10-15 18:53:24 +02:00
AsamK
33c4e17c0d Implement change phone number
Closes #1240
2023-10-14 23:37:23 +02:00
AsamK
56ee173d03 Implement exit code for rate limiting error
Closes #1240
2023-10-13 19:36:58 +02:00
AsamK
af4709255a Fix creation of key_value table for new accounts 2023-10-13 12:54:30 +02:00
AsamK
527e1aefc9 Refactor generateSignedPreKeyRecord 2023-10-12 21:15:19 +02:00
AsamK
a66dd0dc79 Implement EditMessageReceived signal for dbus 2023-10-10 20:21:40 +02:00
AsamK
7b5b5776f0 Add --quote-attachment paramter to send command
Fixes #1318
2023-10-10 19:54:45 +02:00
AsamK
91ab0b12b0 Rename pre key id offset field 2023-10-09 19:13:45 +02:00
AsamK
c2ea7045f5 Prevent deleting self recipient 2023-10-09 19:13:45 +02:00
AsamK
af3cae5f3d Restructure account save file 2023-10-09 19:13:45 +02:00
AsamK
54c3b19052 Support serviceId based safety number for scannable safety numbers 2023-10-06 21:42:28 +02:00
AsamK
04de0010b5 Update libsignal-service-java 2023-10-06 19:58:09 +02:00
AsamK
66161b74a6 Move aci/pni to account data object 2023-10-06 19:53:32 +02:00
AsamK
889ef66784 Update release pipeline
The default libsignal-client jar now contains Linux/macOS/Window native library
2023-10-06 18:55:13 +02:00
AsamK
fae83f8d22 Extend json-rpc man page 2023-10-05 22:18:36 +02:00
AsamK
6f4d538832 Execute JSON-RPC requests in parallel 2023-10-05 22:18:13 +02:00
AsamK
a0c345185b Move metadata to db 2023-10-04 20:19:07 +02:00
AsamK
c0f771684d Move configuration store to db 2023-10-04 20:19:07 +02:00
AsamK
90ec01bfbf Only output username if set 2023-10-04 17:10:16 +02:00
AsamK
7f83bfefd6 Reorganize SignalAccount 2023-10-02 12:09:41 +02:00
AsamK
310aadbdc0 Fix handling edit message 2023-10-01 17:06:07 +02:00
AsamK
6b2de3a24d Reduce hikari logging noise 2023-10-01 16:30:14 +02:00
AsamK
002a87d3ba Add pre key cleanup and improve refresh 2023-10-01 16:29:41 +02:00
AsamK
6f63346905 Fix log statement 2023-09-30 22:32:45 +02:00
AsamK
735766669e Convert classes to records 2023-09-30 22:26:08 +02:00
AsamK
0dda8b405e Bump version to 0.12.2 2023-09-30 21:36:12 +02:00
AsamK
47626b2af4 Update libsignal-service 2023-09-30 21:28:02 +02:00
AsamK
fed2c94431 Update libsignal-service 2023-09-24 18:03:38 +02:00
AsamK
da7428d6bd Switch to zulu java distribution 2023-09-24 17:49:00 +02:00
AsamK
dced7a14c8 Remove jetbrains annotation 2023-09-23 17:48:03 +02:00
AsamK
f1d735f93d Update libsignal-service 2023-09-23 16:21:19 +02:00
AsamK
73b4239744 Add libsignal_client_path property to override libsignal-client jar fiel 2023-09-23 12:37:23 +02:00
AsamK
67747253e8 Improve message cache behavior in case of io error 2023-09-23 12:25:30 +02:00
AsamK
0bdc400a30 Update java version in ci workflow 2023-09-23 11:54:30 +02:00
AsamK
b12a016f3a Update graalvm build tools 2023-09-23 11:52:49 +02:00
AsamK
fd851ba6cb Fix batch response in case of empty responses list
This adapts the implementation to the JSON-RPC specification.
2023-09-23 11:52:37 +02:00
AsamK
b2a32666e9 Store serviceId in protocol stores as TEXT
ServiceIds are no longer just UUIDs, they can now have prefixes like "PNI:"
2023-09-23 11:51:50 +02:00
AsamK
a7744e837c Improve robustness in receiving messages 2023-09-16 12:00:55 +02:00
AsamK
e5aa10a730 Update README 2023-09-16 11:57:43 +02:00
AsamK
e626cc0390 Update dependencies 2023-09-08 19:19:15 +02:00
AsamK
4d93415485 Update graalvm buildtools 2023-09-08 17:54:07 +02:00
AsamK
ed746c389c Update libsignal-service 2023-09-08 17:53:49 +02:00
AsamK
a7a5947a1b Implement multi-account mode for jsonRpc command
Fixes #1319
2023-09-01 12:11:38 +02:00
AsamK
c8daef5113 Add --receive-mode parameter to jsonRpc command 2023-09-01 12:08:56 +02:00
AsamK
ee195c966e Update libsignal-service 2023-09-01 11:31:41 +02:00
AsamK
6ecda07577 Bump version to 0.12.1 2023-08-26 18:05:51 +02:00
AsamK
26fea2d6a0 Update dependencies 2023-08-26 18:05:26 +02:00
AsamK
87e79bceaa Fix output format of jsonrpc receive notification
Fixes #1313
2023-08-26 17:17:25 +02:00
AsamK
bc3bdbbf21 Improve identites listing 2023-08-26 16:33:39 +02:00
AsamK
884fa2748e Ensure uniqueness of dbus identity names 2023-08-22 11:56:27 +02:00
AsamK
4a37227b95 Improve logging 2023-08-22 11:52:22 +02:00
AsamK
a9f1944636 Show better error message when using addDevice on a linked device 2023-08-21 17:23:16 +02:00
AsamK
6d23eb3bf6 Show better error message when using addDevice on a linked device 2023-08-21 17:14:04 +02:00
AsamK
3d13c69e41 Extend logging 2023-08-21 17:02:12 +02:00
AsamK
fb8624f630 Fix incorrect sql query 2023-08-21 17:00:15 +02:00
AsamK
795c5f6a04 Fix npr when upgrading old accounts 2023-08-20 22:37:25 +02:00
AsamK
98a8c991e2 Update gradle wrapper 2023-08-20 22:36:29 +02:00
AsamK
e867c57af8 Add addStickerPack command 2023-08-20 22:36:18 +02:00
AsamK
133e2cc222 Document listStickerPacks command 2023-08-20 19:41:00 +02:00
AsamK
e0a326f15b Fix prekey log output 2023-08-20 19:40:42 +02:00
AsamK
b3f550ee68 Unsubscribe in client after timeout 2023-08-17 21:06:55 +02:00
AsamK
29923cf930 Bump version to 0.12.0 2023-08-11 00:43:19 +02:00
AsamK
9a63f97a19 Update dependencies 2023-08-11 00:43:19 +02:00
AsamK
2d73ba6735 Update graalvm reflect config 2023-08-11 00:36:22 +02:00
AsamK
8037fb2d66 Switch to jsonrpsee
Fixes #1275
2023-08-11 00:36:22 +02:00
AsamK
edbf803a98 Update dependencies 2023-08-11 00:36:22 +02:00
AsamK
b51c791629 Adapt receive subscription notification to have payload in result field 2023-08-11 00:36:22 +02:00
AsamK
6cbd583746 Update CHANGELOG.md 2023-08-09 21:23:10 +02:00
AsamK
b55d75ef99 Update dependencies 2023-08-09 21:14:17 +02:00
AsamK
5e692df08c Ignore groovy deprecations 2023-08-09 21:13:58 +02:00
AsamK
e19ad40023 Update graalvm build tools 2023-08-09 21:01:46 +02:00
AsamK
934697af28 Update libsignal-service-java 2023-08-09 20:48:52 +02:00
AsamK
ca088bcc33 Extend tests 2023-08-09 20:48:52 +02:00
AsamK
1559d28ed8 Fix linking when preKeyCount is not available 2023-08-09 20:48:52 +02:00
AsamK
376a1704df Add missing handling for SecureValueRecovery.RestoreResponse.Missing response 2023-08-09 19:38:29 +02:00
AsamK
68dbf27b2f Update staging key backup service id 2023-08-09 19:37:15 +02:00
AsamK
6bde5960aa Fix issues with pni parsing 2023-08-07 21:03:39 +02:00
AsamK
86e1079195 Update libsignal-service 2023-08-06 19:09:11 +02:00
AsamK
409a707baa Update libsignal-service 2023-07-20 20:58:49 +02:00
AsamK
86f50e0355 Add support for SVR2 2023-07-14 20:28:34 +02:00
AsamK
02d4cb4a14 Update libsignal-service-java 2023-07-14 19:25:32 +02:00
Benjamin Loison
2487fff44a
Remove unnecessary spaces in README.md and add sh syntax highlighting (#1270) 2023-07-05 10:13:21 +02:00
AsamK
ffeae1a95a Store last resort kyber pre key from PniChangeNumber message 2023-07-01 14:06:35 +02:00
AsamK
e57e5b090e Update libsignal-service-java 2023-06-24 01:18:47 +02:00
AsamK
c8e35991b9 Update dependencies 2023-06-23 21:48:02 +02:00
AsamK
0ebfd989d1 Refactor ACI/PNI store handling 2023-06-18 17:54:52 +02:00
AsamK
306e38c9ee Implement support for kyber pre keys 2023-06-17 22:32:05 +02:00
AsamK
4e5c859aab Update logback 2023-06-17 17:22:26 +02:00
AsamK
ac815f7598 Fix json deserialization for request params
Fixes #1261
2023-06-17 12:59:55 +02:00
AsamK
a3f7de89f1 Update graalvm buildtools 2023-06-17 12:21:15 +02:00
AsamK
b70a43aeb5 Update graalvm config 2023-06-17 10:44:02 +02:00
Adimarantis
8726c4ede0
Update identities after trust (#1264) 2023-06-17 09:21:28 +02:00
AsamK
0c5993c0ad Add support for invalid pre key failure when sending message 2023-06-11 17:47:48 +02:00
AsamK
da25b2a763 Add missing return to app command handling 2023-06-11 17:17:04 +02:00
AsamK
aad4b53524 Update dependencies 2023-06-11 17:16:38 +02:00
AsamK
3d5c440aa2 Improve uuid/number handling 2023-06-11 16:39:54 +02:00
AsamK
4f8da7819e Reformat code 2023-06-05 19:30:18 +02:00
Adimarantis
a96c4938b1
Dbus identities (#1259)
* Dbus Identities and Trust

* Update src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java

* PR feedback updates

* Documentation and consistent case sensitivity

* doc for listIdentities and getIdentity
2023-06-05 19:26:00 +02:00
AsamK
c62a1e829f Remove duplicate address resolving 2023-06-03 16:11:21 +02:00
AsamK
106af6a801 Refactor trust command implementation 2023-06-03 16:10:47 +02:00
AsamK
2ae5297f7d Refactor App init method 2023-06-02 22:52:45 +02:00
AsamK
a7db3a5610 Extract dbus client command handling 2023-06-02 22:43:18 +02:00
AsamK
d0d3e20713 Add helper method to create valid dbus object path 2023-06-02 21:29:58 +02:00
AsamK
c852bd8a85 Remove libsignal logs on console, when log-file is given 2023-05-30 13:03:00 +02:00
AsamK
7499b41d6b Remove old methods/classes from graalvm config 2023-05-30 13:02:09 +02:00
AsamK
ee12805d05 Bump version to 0.11.11 2023-05-24 23:05:58 +02:00
AsamK
dacb48b4f4 Update sqlite-jdbc 2023-05-24 23:05:58 +02:00
AsamK
90f82b6d4c Update libsignal-service 2023-05-24 22:40:04 +02:00
AsamK
227f87e1ce Fallback to legacy send if group send fails due to invalid prekey 2023-05-23 19:34:13 +02:00
AsamK
83c75acd0a Log signal-cli version on startup 2023-05-23 19:34:13 +02:00
AsamK
ff162cb44d Refactor addDeviceLink method 2023-05-23 19:34:13 +02:00
AsamK
e5a67d6ce1 Refactor manager lib package structure 2023-05-23 19:34:13 +02:00
exquo
5197212d3f
Fix TextStyle doc example typo (#1251) 2023-05-22 17:35:34 +02:00
AsamK
8c1b5d54f7 Reduce log output of third party libs
Fixes #1064
2023-05-21 11:49:26 +02:00
AsamK
8a0005d900 Remove unused code 2023-05-21 11:18:18 +02:00
AsamK
a754eb6faf Better logging for registration failure 2023-05-21 11:15:49 +02:00
AsamK
c9082a63f0 Update user agent version 2023-05-21 11:05:59 +02:00
AsamK
314b3cafbb Refactor selfNumber in send command 2023-05-20 13:01:54 +02:00
AsamK
91700ce995 Implement textStyles for sending and receiving
Fixes #1250
2023-05-20 12:49:57 +02:00
AsamK
145a2f1179 Fix json deserialization in legacy stores
Fixes #1248
2023-05-18 11:08:57 +02:00
AsamK
6e61bc2000 Update dependencies 2023-05-18 11:00:22 +02:00
AsamK
9da6f3a702 Include all libsignal-client libs in native build
Fixes #1237
2023-05-17 19:51:57 +02:00
AsamK
760934d5a5 Fix deleting old unregistered recipient
Fixes #1242
2023-05-16 23:09:07 +02:00
AsamK
a3bc754e80 Fix migration of legacy recipient storage 2023-05-16 22:50:27 +02:00
AsamK
ae652b1294 Update graalvm buildtools 2023-05-16 22:49:59 +02:00
AsamK
ab01867235 Switch CI to temurin distribution 2023-05-11 20:20:51 +02:00
AsamK
6b0b36a555 Bump version to 0.11.10 2023-05-11 19:36:26 +02:00
AsamK
ee63a27fe9 Use Java 20 in CI build 2023-05-11 19:19:09 +02:00
AsamK
5f0c49d653 Update dependencies 2023-05-11 19:18:48 +02:00
AsamK
8a31b7f2c1 Implement editing of previous messages 2023-05-11 19:10:29 +02:00
AsamK
72390e595d Update libsignal-service-java 2023-05-11 17:45:23 +02:00
AsamK
e0e8e11443 Validate incoming messages before handling them 2023-04-23 19:57:06 +02:00
AsamK
f7f882e834 Use modern switch syntax 2023-04-23 19:55:23 +02:00
AsamK
60ad582012 Store envelopes using envelope proto 2023-04-23 19:39:36 +02:00
AsamK
f6aebb5917 Bump version to 0.11.9.1 2023-04-23 10:19:25 +02:00
AsamK
3a4ad96a00 Fix build with java 20
- previous fix required a jdk 17 to be installed as well
2023-04-22 19:19:00 +02:00
AsamK
e0ed651e53 Bump version to 0.11.9 2023-04-22 17:54:49 +02:00
AsamK
c703644dbe Fix build with java 20 2023-04-22 12:44:42 +02:00
AsamK
da4cc7dc6b Announce support for gift badges if linked device
Fixes #1226
2023-04-16 21:19:17 +02:00
AsamK
842f13b2fc Update libsignal-service-java 2023-04-16 21:19:17 +02:00
AsamK
d2251ccd14 Remove old libzkgroup fallback code 2023-04-16 21:09:24 +02:00
AsamK
6a3ebfd4c8 Update graalvm buildtools native 2023-04-13 18:08:52 +02:00
AsamK
ff803eaf45 Update gradle wrapper 2023-04-13 18:08:01 +02:00
AsamK
c93480692c Bump version to 0.11.8 2023-04-05 11:40:13 +02:00
AsamK
542a3f36a4 Fix issue with unknown identity serviceId
Fixes #1215
2023-04-05 11:25:53 +02:00
AsamK
24a8c528d0 Filter null from db result 2023-04-05 11:22:43 +02:00
AsamK
780c69d804 Reduce use of unknown serviceIds 2023-04-05 11:10:46 +02:00
AsamK
22c948166a Add labels to Containerfile 2023-04-04 12:02:24 +02:00
AsamK
64436bc9ab Refresh username after registering 2023-04-04 11:43:50 +02:00
AsamK
6106e1878b Update README.md 2023-04-03 19:00:56 +02:00
AsamK
c788c5a40e Add capabilities and read receipt handling to provisioning 2023-04-03 19:00:27 +02:00
AsamK
9f60ed534a Implement support for usernames 2023-04-03 19:00:05 +02:00
AsamK
03f193b34c Fix deleting old group in dbus mode
Fixes #1192
2023-04-02 19:25:49 +02:00
signals-from-outer-space
db42f61cbb Added missing file attachment attributes in JsonAttachment output
Closes #1217
Fixes #1216
2023-04-02 18:49:42 +02:00
exquo
49591aedb4
Document accepting group invitations (#1218) 2023-04-02 17:58:54 +02:00
AsamK
21aa2b2a7f Update dependencies 2023-04-01 12:35:46 +02:00
AsamK
276ecef300 Update libsignal-service-java
- Use session based number verification and registration
2023-04-01 12:19:53 +02:00
AsamK
20b3563f21 Update gradle wrapper 2023-03-18 21:06:59 +01:00
cedb
15630356e1 Change content-type check to check contains
So far it was doing an equals check, but a string like "application/json; charset=utf-8"
is similarly valid. And some clients like OkHttp actually automatically add the
charset.

Closes #1152
2023-03-03 18:11:56 +01:00
AsamK
f1fd528483 Fix sending large text messages to multiple recipients
Fixes #1177
2023-03-03 18:06:31 +01:00
AsamK
75d7270d5a Add verbose exception log for send error 2023-03-03 17:58:02 +01:00
AsamK
3206639778 Scrub E164 in dbus path
Related #1189
2023-02-19 20:18:05 +01:00
AsamK
2e63c3b4f7 Update CHANGELOG.md 2023-02-19 16:05:14 +01:00
AsamK
d63405d170 Bump version 2023-02-19 14:59:33 +01:00
AsamK
91e0db185c Fix remove recipient method
Fixes #1183
2023-02-19 14:21:16 +01:00
AsamK
f97543eecd Update gradle 2023-02-19 14:21:16 +01:00
AsamK
3606fb67bb Update libsignal-service-java 2023-02-19 14:21:16 +01:00
AsamK
426b59d13e Add dependencyResolutionManagement 2023-02-19 13:58:33 +01:00
AsamK
d94ec8e144 Update dependencies 2023-02-19 13:28:44 +01:00
Marvin A. Ruder
4186349bba
Bump sqlite-jdbc from 3.40.0.0 to 3.40.1.0 (#1178)
* Should fix #1129
2023-02-06 18:20:32 +01:00
AsamK
7816325e63 Fix issue with missing pni identity key
Fixes #1176
2023-02-05 12:49:12 +01:00
AsamK
15da210de7 Print text styles in plain text output 2023-01-27 21:46:39 +01:00
AsamK
a4f7632981 Update dependencies 2023-01-27 21:44:41 +01:00
AsamK
a1b16a9118 Update CONTRIBUTING.md 2023-01-27 21:25:40 +01:00
AsamK
c100b504dc Document submitRateLimitChallenge 2023-01-21 19:42:49 +01:00
AsamK
161ec9f828 Update dependencies 2023-01-07 12:14:47 +01:00
AsamK
210466e7d9 Allow JSON-RPC commands without account param if only one account exists 2022-12-30 13:50:40 +01:00
AsamK
0702159596 Bump version 2022-12-18 20:09:15 +01:00
AsamK
8d7e533196 Update dependencies 2022-12-18 20:05:53 +01:00
AsamK
47feda6ae4 Restrict workflow permissions 2022-12-04 23:16:53 +01:00
AsamK
44c945f45d Update dependencies 2022-11-24 17:31:11 +01:00
ced-b
35def4445d
Fix handling of attachments in JSON RPC (#1109)
* Fix handling of attachments in JSON RPC

It turns out that using a custom serializer on an
input stream did not work well. For one the stream seems
to be getting closed before the JSON gets written. But
also the method for writing it was throwing an
UnsupportedOperationException further down in Jackson.

The above simplifies the matter by simply outputting the
Base64 string first and then setting it on the model.

* Add missing files to attachment fix

Co-authored-by: cedb <cedb@keylimebox.org>
2022-11-24 17:29:45 +01:00
ced-b
3e60303b90
Add alive check (#1107)
Adds a simple HTTP endpoint that can be used by the container
environment to see if the app is started and available.

Co-authored-by: cedb <cedb@keylimebox.org>
2022-11-22 07:58:34 +01:00
AsamK
b6e9dfa97d Add fallback locale for voice verification
Fixes #1101
2022-11-20 11:27:33 +01:00
AsamK
5771bb858f Allow using data URIs for updateGroup/updateProfile avatars
Fixes #1082
2022-11-14 19:31:40 +01:00
AsamK
dcaf1cc189 Bump version 2022-11-09 19:00:59 +01:00
AsamK
5e1fc79c33 Fix SignalAccount initialization
Fixes #1092
2022-11-08 17:18:24 +01:00
AsamK
8997d7f91f Update README.md 2022-11-08 17:18:22 +01:00
AsamK
54a08f560e Bump version 2022-11-07 20:12:41 +01:00
AsamK
ccb37c00f6 Update dependencies 2022-11-07 19:43:15 +01:00
AsamK
6281cbfd5f Catch all exceptions when reading session record
Fixes #1083
2022-11-03 15:55:12 +01:00
AsamK
6502f3f487 Publish docker image to ghcr 2022-11-03 15:33:28 +01:00
AsamK
00535c9a42 Package native file as executable 2022-11-03 15:18:59 +01:00
AsamK
e6cf11cb3d Add missing check to httpAddres 2022-11-03 15:10:17 +01:00
AsamK
c5eb0fd351 Rework release workflow 2022-11-03 13:31:37 +01:00
AsamK
a780be70dd Add http endpoint events with SSE 2022-11-03 00:03:37 +01:00
AsamK
1d98e5307a Handle missing separator in query string parser 2022-11-02 23:16:38 +01:00
AsamK
36abb8ae8f Add check for exact path match
HttpExchange just checks startsWith, so would also match /api/v1/rpcfoobar
2022-11-02 21:17:28 +01:00
AsamK
0c4642aa20 Fix length for empty response to prevent chunked stream
The java HttpExchange expects length -1 to send Content-length: 0 ...
2022-11-02 21:15:08 +01:00
AsamK
1b029b765f Extract http endpoint handler function 2022-11-02 21:13:52 +01:00
AsamK
9563496efb Update CHANGELOG.md 2022-11-02 17:47:52 +01:00
AsamK
c628e27d2e Update man page 2022-11-02 17:46:20 +01:00
ced-b
1ad0e94b64
Exposing Signal CLI as HTTP Server (#1078)
* Add initial proof of concept for http server

* Add support for registration commands

* Add support  for MultiLocalCommands

* Improve handling of HTTP responses

Makes it so that responses area all uniformly JSON and wrapped
into the proper response envelope.

* Add caching for workflows

* Run http server with daemon command

This fits the existing command line API better

* Wrap the existing JSON RPC handler in HTTP Service

This is a redesign of earlier attempts to make an HTTP service. Fixing
that service turned out that it would have to be a copy of the
SignalJsonRpcDispatcherHandler. So instead of copy pasting all the
code the existing service is simply being wrapped.

* Switch http server to use command handler

* Clean up and simplification

* Pass full InetSocketAddress

* Minor fixes and improvements

Based on code review.

Co-authored-by: cedb <cedb@keylimebox.org>
2022-11-02 17:44:12 +01:00
AsamK
43face8ead Small improvements 2022-11-01 22:48:47 +01:00
ced-b
2e4d346bc8
Add command to get an attachment (#1080)
* Add command to get an attachment

* Refactor retrieving of attachments to use StreamDetails

* Refactor AttachmentCommand to GetAttachmentCommand

* Minor improvements to GetAttachmentCommand

* Use JSON serializer to serialize binary data

Serializing the stream is better for memory handling than
loading the whole thing into the file.

* Clean up unneeded class

* Added command to doc

Co-authored-by: cedb <cedb@keylimebox.org>
2022-11-01 22:47:43 +01:00
AsamK
bf76c04664 Refactor JsonRpcReader to for handling a single message 2022-11-01 18:06:40 +01:00
AsamK
ae678871ec Extract JSON-RPC command handler 2022-11-01 17:10:15 +01:00
AsamK
9620eb06ac Update libsignal-service 2022-11-01 13:58:09 +01:00
AsamK
9096229637 Improve behavior with synchronous and asynchronous receivers 2022-11-01 13:58:09 +01:00
AsamK
eec3d782d3 Add caching for workflows 2022-10-31 16:40:39 +01:00
AsamK
43a7478791 Update reflect-config.json 2022-10-31 15:29:18 +01:00
AsamK
175057e781 Implement receive command for JSON-RPC mode
The command returns a list of messages, as soon as the timeout is reached
after the last message has been received or the maximum number of messages
has been received.
2022-10-31 11:56:25 +01:00
AsamK
de2bfc7f79 Add optional message limit for receive command 2022-10-31 11:17:52 +01:00
AsamK
5ed9db4f08 Implement replying to stories 2022-10-30 18:18:21 +01:00
AsamK
fea19c9e20 Implement reacting to stories 2022-10-30 14:41:02 +01:00
AsamK
207764e0be Add option to disable adding message to send log 2022-10-30 11:00:25 +01:00
AsamK
49aaff2bbe Increase sqlite busy timeout 2022-10-29 13:54:06 +02:00
AsamK
25e84f2f5d Add account to plain text output
Fixes #1075
2022-10-29 13:49:14 +02:00
AsamK
e63f2fafb9 Add color to listContacts output
Fixes #1072
2022-10-28 20:12:11 +02:00
AsamK
0b5a063b62 Use complete address instead of only identifier for retry envelope
Fixes #1074
2022-10-28 18:13:25 +02:00
AsamK
e0c2f58e8d Store attachments with a file extension
Taken from the filename if present, otherwise guessed from the contentType
2022-10-22 22:00:31 +02:00
AsamK
0084a2e722 Update reflect-config.json 2022-10-22 17:43:15 +02:00
AsamK
7ff1500122 Improve logging in prekey store 2022-10-22 17:43:15 +02:00
AsamK
7805622f07 Send long text messages as attachment instead
This matches the behavior of the official clients.
2022-10-22 17:43:15 +02:00
AsamK
a8e68dce3a Extract MimeUtils 2022-10-22 17:43:15 +02:00
AsamK
b9eee539bd Add PNI to recipients 2022-10-21 22:02:33 +02:00
AsamK
e450f36e81 Don't output caption if it's empty 2022-10-21 16:22:09 +02:00
AsamK
ae221e0447 Disable graalvm toolchain detection if GRAALVM_HOME is set 2022-10-21 10:34:08 +02:00
AsamK
2a1be0bd85 Improve graalvm native build wih resource autodetect 2022-10-21 10:34:08 +02:00
AsamK
af324eeca5 Bump version 2022-10-19 20:35:53 +02:00
AsamK
eb71fd1a5a Add java 19 to CI 2022-10-19 20:34:42 +02:00
AsamK
e4a4788d5e Add native graalvm build to release 2022-10-19 20:34:16 +02:00
AsamK
47b6fe7dbe Enable story capability
Only receiving is supported
2022-10-19 19:11:37 +02:00
AsamK
9ffacfe90e Add --ignore-stories flag to prevent receiving story messages 2022-10-19 19:11:37 +02:00
AsamK
3f7d8c60b9 Add workaround for #1045 2022-10-19 17:51:12 +02:00
AsamK
2e5d8fe561 Add build graalvm native step for CI 2022-10-19 15:46:58 +02:00
AsamK
7188c75351 Update reflect-config 2022-10-19 15:36:17 +02:00
AsamK
316c35b258 Add additional logging for reading message cache 2022-10-19 11:02:10 +02:00
AsamK
9da42e27f1 Update workflow actions 2022-10-18 18:11:35 +02:00
AsamK
0aee7ff552 Update graalvm buildtools 2022-10-18 18:11:35 +02:00
AsamK
228713ebb5 Reset pre key offset if it somehow gets corrupted
Fixes #1055
2022-10-18 17:55:51 +02:00
AsamK
266129c61b Update reflect-config 2022-10-18 17:38:46 +02:00
AsamK
3522e43617 Update dependencies 2022-10-16 20:47:51 +02:00
AsamK
45a5795c9c Handle PniChangeNumber 2022-10-16 20:07:33 +02:00
AsamK
94d79692df Update to clap 4 2022-10-09 12:30:25 +02:00
AsamK
58bb4b5358 Update client dependencies 2022-10-09 12:18:43 +02:00
AsamK
51fef48016 Refactor resolve recipient 2022-10-08 17:42:03 +02:00
AsamK
7ab013cee9 Do recipient merge in one transaction 2022-10-08 17:42:03 +02:00
AsamK
f2b334b57a Refactor check for registered users 2022-10-08 17:42:03 +02:00
AsamK
7eb7ee44f2 Refactor RecipientAddress 2022-10-08 17:42:03 +02:00
AsamK
34cc64f8ce Ensure self profile key is always stored in profile store
Fixes #1040
2022-10-07 21:51:01 +02:00
AsamK
ca5951861a Fix issue when receiving invalid message from invalid sender 2022-10-07 21:50:07 +02:00
AsamK
605f31d1ad Improve handling of group join messages 2022-10-07 21:17:45 +02:00
AsamK
30167d81e6 Add missing check for story group context 2022-10-07 21:17:45 +02:00
AsamK
a247b444e5 Fix typo 2022-10-07 21:17:45 +02:00
AsamK
1424a2980f Approve join requests instead of just adding the member 2022-10-07 21:17:45 +02:00
AsamK
01e1115806 Refresh group before updating 2022-10-07 21:17:45 +02:00
AsamK
c9c8af42c2 Implement refuse group join requests 2022-10-07 19:49:43 +02:00
AsamK
489fb2ac22 Improve error message when joining a group with already pending admin approval 2022-10-07 19:31:27 +02:00
AsamK
a708025a16 GraalVM agent formatting changes 2022-10-07 19:30:34 +02:00
AsamK
31429b7faf Bump version 2022-10-07 17:24:29 +02:00
Benjamin Schmid
9c5235c273
fix(GraalVM): explictly declare symbols causing GraalVM compiler failure (#1037)
Fixes #1016
2022-10-07 17:16:32 +02:00
AsamK
8e717e00b1 Add missing urgent column to projection
Fixes #1031
2022-10-07 12:09:43 +02:00
AsamK
9a4693136d Fix update from old versions without PNI
Fixes #1032
2022-10-07 12:04:22 +02:00
AsamK
cf0110ab95 Update libsignal-service
Fixes #1030
2022-10-06 21:56:40 +02:00
AsamK
abc5d5e863 Bump version 2022-10-06 17:59:59 +02:00
technillogue
0c74338c9c Bump user agent version
Closes #1024
2022-10-06 17:57:48 +02:00
AsamK
1dd22132ff Update libsignal-service 2022-10-06 17:55:16 +02:00
AsamK
76c400d2c3 Show better error message when signal-cli version is outdated 2022-10-06 17:55:16 +02:00
ETL
d9472ec19c
Update user agent (#1023)
Recent changes from Signal caused DeprecatedVersionException to occur.

Changing the user agent debugged the receiving (sending still erroring)
2022-10-06 08:03:25 +02:00
AsamK
9d821fb892 Bump version 2022-10-05 20:08:24 +02:00
AsamK
8c6553ce73 Update dependencies 2022-10-05 20:08:21 +02:00
AsamK
1d4ed23394 Fix sending messages to groups
Fixes #1019
2022-10-03 21:31:30 +02:00
AsamK
c1fb44a145 Ignore md files in fat jar build 2022-10-03 21:30:01 +02:00
AsamK
333aacf4ab Delete migrated stores after closing the input stream
Fixes #1018
2022-10-03 13:38:41 +02:00
AsamK
5b5827f885 Update gradle 2022-10-03 13:13:41 +02:00
AsamK
6fbbf38a84 Fix building fatJar 2022-10-03 13:13:17 +02:00
AsamK
c1004fb4f8 Bump version 2022-10-02 18:16:12 +02:00
AsamK
de8ddb2a2b Update dependencies 2022-10-02 18:02:25 +02:00
AsamK
6feff1e42b Add sendPaymentNotification dbus method
Closes #1010
2022-09-18 16:56:30 +02:00
AsamK
ed3992d993 Use CDSI for contact discovery in compat mode 2022-09-17 12:21:28 +02:00
AsamK
60ed2c292f Update libsignal-service-java 2022-09-17 11:26:44 +02:00
AsamK
1d77153a2b Add --scrub-log flag to remove possibly sensitive information from the log 2022-09-04 11:15:23 +02:00
AsamK
2e8e81a926 Update dependencies 2022-09-02 22:33:48 +02:00
AsamK
c7aa9834a9 Fix default value for phone number sharing mode
Fixes #1002
2022-08-29 19:31:30 +02:00
AsamK
ac8925b2bd Always use content sender if available 2022-08-28 17:15:24 +02:00
AsamK
25fb1b08bc Log error for fatal load errors 2022-08-28 16:32:35 +02:00
AsamK
adcc88823f Create database tables with strict mode 2022-08-28 16:06:13 +02:00
AsamK
280d8d7f10 Key tables on serviceId instead of recipientId 2022-08-28 16:04:05 +02:00
AsamK
04fa046815 Move test config 2022-08-28 16:04:05 +02:00
AsamK
11a06d6b33 Extract method to read result set 2022-08-28 16:04:05 +02:00
AsamK
38c9fe9cb5 Store urgent field in database 2022-08-28 16:04:05 +02:00
AsamK
2c586266ff Disallow sending an empty message
It would be ignored by the offical apps anyway
2022-08-28 16:04:05 +02:00
AsamK
4120630309 Remove fallback to reading stdin if no message body is given
To read a message from stdin, use the `--message-from-stdin` flag
2022-08-28 16:04:05 +02:00
AsamK
1ea4309a2a Change meaning of -v flag from --version to --verbose 2022-08-28 16:04:05 +02:00
AsamK
08dc65350f Move sender key store to database 2022-08-28 16:04:05 +02:00
AsamK
0c4a037dde Move identity store to database 2022-08-28 16:04:05 +02:00
AsamK
dc8b83a110 Implement SignalServiceAccountDataStore for PNI 2022-08-28 16:04:05 +02:00
AsamK
484daa4c69 Move session store to database 2022-08-28 16:04:05 +02:00
AsamK
65c9a2e185 Move group store to database 2022-08-28 16:04:05 +02:00
AsamK
46adc1af98 Add RecipientIdCreator for creating recipientIds from the database 2022-08-28 16:04:05 +02:00
AsamK
7da2e1b262 Move pre key stores to database 2022-08-28 16:04:05 +02:00
AsamK
9a698929f4 Move sticker store to database 2022-08-28 16:04:05 +02:00
AsamK
862c2fec87 Move recipient store to database 2022-08-28 15:41:27 +02:00
AsamK
9d534dc7bb Store account file version after migration 2022-08-28 15:40:23 +02:00
AsamK
a25043e5d4 Add fallback KBS and migrate to current version 2022-08-28 15:35:15 +02:00
AsamK
cd77d3d410 Update KBS enclave 2022-08-28 15:35:11 +02:00
AsamK
0d72c03fa5 Update CDSI enclave 2022-08-19 17:01:24 +02:00
AsamK
173a2416fa Bump version to 0.10.11 2022-08-17 18:24:54 +02:00
AsamK
71fff4b14f Update sqlite-jdbc 2022-08-17 18:21:06 +02:00
AsamK
007acf8c6d Adapt log level when using system locale 2022-08-17 18:13:14 +02:00
AsamK
6843643aad Update libsignal-service-java 2022-08-11 20:17:21 +02:00
AsamK
64f6a1f072 Update CDS config 2022-08-11 18:59:40 +02:00
AsamK
b09811c1d4 Update libsignal-service-java 2022-08-09 23:20:19 +02:00
AsamK
a593051512 Implement receive handling for story messages 2022-08-09 23:20:12 +02:00
AsamK
81e36d4f9b Update libsignal-service-java 2022-08-03 18:02:56 +02:00
Sebastian Scheibner
597ac9b504 Update FUNDING.yml 2022-07-30 16:59:08 +02:00
AsamK
525f04d446 Update CHANGELOG.md 2022-07-30 16:52:20 +02:00
AsamK
d8a5244a88 Bump version to 0.10.10 2022-07-30 14:56:17 +02:00
AsamK
d69b9ea394 Update KBS enclave
Fixes #985
2022-07-28 17:00:09 +02:00
AsamK
7a42737287 Workaround possible GraalVM issue 2022-07-27 20:58:58 +02:00
AsamK
1cc21834e2 Workaround possible GraalVM issue
Fixes #987
2022-07-27 18:20:07 +02:00
AsamK
956cb97331 Update libsignal-service-java 2022-07-24 10:46:20 +02:00
AsamK
72293d8d87 Bump version to 0.10.9 2022-07-16 15:25:58 +02:00
AsamK
de583475f9 Update libsignal-service-java 2022-07-16 15:17:48 +02:00
AsamK
ed7d023581 Update gradle wrapper 2022-07-16 11:08:39 +02:00
AsamK
457b093dce Improve import of contact from storage 2022-07-16 11:08:39 +02:00
AsamK
dcde9fbe8e Use toList() Stream method 2022-07-16 11:08:39 +02:00
AsamK
f4346e3f0a Update account attributes before checking whoAmI 2022-07-16 11:08:39 +02:00
AsamK
9da2a00403 Fix issue with loading legacy profile store
Fixes #981
2022-07-15 20:13:36 +02:00
AsamK
1c7d1861d6 Update graalvm buildtools 2022-07-15 20:07:28 +02:00
AsamK
9f7979314f Prevent duplicate family name when handling contact sync message 2022-06-27 16:57:46 +02:00
AsamK
dca1d479e8 Update graalvm buildtools 2022-06-27 15:39:53 +02:00
AsamK
c586f58286 Reset cached storage manifest after provisioning/registering 2022-06-21 20:38:10 +02:00
AsamK
9553b1ef00 Check account identifiers on the server before updating the account attributes 2022-06-21 20:38:10 +02:00
AsamK
a4db5d616a Create master key before storage sync if it doesn't exist yet 2022-06-21 20:38:10 +02:00
AsamK
c1dc44d4fd Separate registrationLock attribute from master key 2022-06-21 20:38:10 +02:00
John Rush
6bbbccb9ac
Update README.md (#972)
Fix typo from "JSON-PRC" to "JSON-RPC".
2022-06-21 10:11:30 +02:00
AsamK
280bdbefdc Only send profile key update message from the primary device 2022-06-18 12:28:02 +02:00
AsamK
abebffb2cd Improve output for profile key update messages 2022-06-18 12:28:02 +02:00
AsamK
adce64bc21 Bump version 2022-06-13 15:46:03 +02:00
AsamK
4ea3d94d07 Improve number filtering of listContacts command
If the given number is not registered, don't output all recipients
2022-06-13 15:35:38 +02:00
AsamK
aaa6412469 Allow registering new accounts on both live and staging environments
in the same config directory
2022-06-12 17:29:25 +02:00
AsamK
c487929bcd Make version command work on the command line 2022-06-12 17:29:20 +02:00
AsamK
0b63e78d2b Add logging to test script 2022-06-12 17:29:03 +02:00
AsamK
e9e66e1005 Handle groups sync message again 2022-06-11 12:34:55 +02:00
AsamK
7bb690e58e Update libsignal-service-java 2022-06-11 12:34:55 +02:00
AsamK
26620f3137 Refactor identity key store 2022-06-09 21:05:36 +02:00
AsamK
c8cd36bde8 Unsubscribe receive if jsonRpcSender channel is closed 2022-06-08 17:51:18 +02:00
AsamK
7bf06aef5e Store self profile key in profile store after linking 2022-06-08 17:51:16 +02:00
AsamK
0e65e67077 Log more detailed provisioning errors 2022-06-08 17:51:11 +02:00
AsamK
936a68433d Add additional logging for payment address parsing 2022-06-08 15:21:24 +02:00
AsamK
8236696492 Fix typo 2022-06-08 15:00:57 +02:00
AsamK
1757e939e1 Reformat sql statements in MessageSendLogStore 2022-06-07 15:26:42 +02:00
AsamK
5afe9c5337 Rename ThreadInfo to LegacyThreadInfo 2022-06-07 15:15:44 +02:00
AsamK
5b5a1718e9 Update register error message 2022-06-07 11:53:53 +02:00
AsamK
51c2352d67 Extend logging when determining default locale 2022-06-06 17:14:17 +02:00
AsamK
3ad87e1362 Reformat 2022-06-06 17:13:41 +02:00
Kevin
cb5e3c6bf7
Added base64 encoded attachment support (#966)
* Added base64 encoded attachment support

* Added final

* Added full RFC 2397 support

* Added feedback

* Update doc

* Update signal-cli.1.adoc

Co-authored-by: Sebastian Scheibner <asamk@gmx.de>
2022-06-04 11:11:35 +02:00
AsamK
63e94a9fb4 Bump version 2022-05-29 22:25:53 +02:00
AsamK
badbb55ef2 Split given/family name in updateContact command 2022-05-29 22:21:24 +02:00
AsamK
27dbc671e0 Add option to send read receipts for all received data messages
Fixes #850
2022-05-26 18:00:23 +02:00
AsamK
fd92a96e1a Wrap ignoreAttachments option in a ReceiveConfig record 2022-05-26 17:53:14 +02:00
AsamK
8828b60288 Refactor SendReceiptAction to take type argument 2022-05-26 17:37:54 +02:00
AsamK
e03c48e0ae Use console charset for reading/writing to stdin/out 2022-05-26 15:01:36 +02:00
AsamK
425e451237 Update dbus-java 2022-05-26 15:01:36 +02:00
AsamK
d8d859ab02 Fix inspection issues 2022-05-26 12:23:21 +02:00
AsamK
b178c7c67a Implement sending link previews
Fixes #276
2022-05-25 23:23:33 +02:00
AsamK
0f701df91f Refactor attachment upload helpers 2022-05-25 22:31:15 +02:00
AsamK
b292de8ae6 Store retrieved storage manifest locally 2022-05-24 21:09:06 +02:00
AsamK
87016f6ecf Handle account record the same as other records when reading storage 2022-05-24 21:09:06 +02:00
AsamK
145962597c Store family name in recipient store 2022-05-24 21:09:06 +02:00
AsamK
9ad24614cb Rename main/master device to primary device 2022-05-24 14:36:33 +02:00
AsamK
ad65eb7ea2 Refresh pre keys after receiving a sync message with PNI identity 2022-05-23 17:43:55 +02:00
AsamK
ce7aeb02bc Only generate prekeys if the corresponding identity key exists 2022-05-23 17:39:18 +02:00
AsamK
5b1c6c0d64 Split contact name field in given name and family name 2022-05-23 16:42:38 +02:00
AsamK
69e952738b Update code style for text blocks 2022-05-23 16:26:07 +02:00
AsamK
621d822a6c Refresh self profile before rotating profile key 2022-05-23 16:24:42 +02:00
AsamK
3abb641c7c Reduce direct use of recipient store 2022-05-23 13:21:28 +02:00
AsamK
995eaa6e7c Print more detailed error message when registering with non-normalized number
Fixes #958
2022-05-22 22:01:11 +02:00
AsamK
cf07512d24 Update json-rpc client 2022-05-21 12:11:03 +02:00
Sebastian Scheibner
5a63b5419f
Update README.md 2022-05-21 11:47:46 +02:00
AsamK
34c0968f5e Add mobile-coin-address to updateProfile command 2022-05-21 10:44:28 +02:00
AsamK
bf75d9b4e0 Decrypt and verify the profile payment address 2022-05-21 10:44:28 +02:00
AsamK
3666531f8b Refactor manager update profile method 2022-05-21 09:29:58 +02:00
AsamK
7587a60387 Implement sendPayment notification command 2022-05-21 09:04:23 +02:00
AsamK
b18991b9fb Update documentation 2022-05-21 09:00:42 +02:00
AsamK
2ecddba375 Update json-rpc client 2022-05-20 12:11:28 +02:00
AsamK
5f941004f5 Extend listContacts command with profiles and filtering 2022-05-20 11:53:33 +02:00
AsamK
496cd5e621 Fix repackage if building with multiple java versions 2022-05-19 13:48:34 +02:00
AsamK
470aeadbd9 Bump version 2022-05-19 13:06:57 +02:00
AsamK
06e2811012 Only update profile keys from authoritative group changes 2022-05-19 12:55:37 +02:00
AsamK
be28d13d0d Update libsignal-service-java 2022-05-19 10:35:48 +02:00
AsamK
cf1626ea31 Rotate profile key after blocking a contact/group 2022-05-18 19:19:16 +02:00
AsamK
b1e56faab2 Store profile sharing enabled for contacts
Automatically enable it when sending direct messages
2022-05-18 19:12:55 +02:00
AsamK
06a9884e99 Don't reset avatar url path when updating profile with same avatar 2022-05-18 19:12:54 +02:00
AsamK
7ac6c9a170 Cleanup fileChannel if file locking fails 2022-05-18 16:04:35 +02:00
AsamK
53f47d42fc Refactor set blocked methods to accept multiple recipientIds/groupIds 2022-05-18 11:09:05 +02:00
AsamK
2d60f98e93 Keep unrestricted unidentified access if it was set before 2022-05-18 10:45:53 +02:00
AsamK
bb9d44811c Include unrestricted unidentified access state in getUserStatus response 2022-05-17 20:39:06 +02:00
AsamK
376fcba9ec Include reason for inactive group link in error message 2022-05-17 20:22:56 +02:00
AsamK
e844abcad1 Store payment address of profiles 2022-05-17 20:20:03 +02:00
AsamK
c3425979dd Added new parameter to filter the output of listGroups by groupId
Fixes #953
2022-05-16 15:22:03 +02:00
AsamK
68eb6b6bb4 Update graalvm config
Fixes #942
2022-05-16 14:35:34 +02:00
AsamK
f0fe1fac66 Update native-image handling in tests 2022-05-16 14:19:15 +02:00
AsamK
477e6516ed Prevent printing stack trace for unregistered users 2022-05-16 12:27:43 +02:00
AsamK
22add1cbee Add command to delete local account data
Fixes #912
2022-05-16 12:27:43 +02:00
AsamK
55dde93811 Store information if account is registerd on LIVE or STAGING env in account file
Fixes #896
2022-05-16 12:27:43 +02:00
AsamK
656282459c Add warning if user has no profile name set 2022-05-14 15:51:23 +02:00
AsamK
35679216f7 Extract getSelfProfile method 2022-05-14 15:51:23 +02:00
AsamK
a80e18169a Update libsignal-service-java 2022-05-14 15:51:23 +02:00
AsamK
5837a6982b Fix deleting a recipient which has no uuid
Fixes #946
2022-05-02 17:13:30 +02:00
AsamK
275be59c25 Fix graalvm native build
Fixes #944
2022-04-26 22:53:47 +02:00
AsamK
a0c304d3ca Update dependencies 2022-04-18 11:02:29 +02:00
AsamK
f968d9bf3b Use Java 17 for codeql 2022-04-12 15:45:42 +02:00
AsamK
0309c85c6a Update gradle wrapper 2022-04-12 15:44:23 +02:00
AsamK
59b2d2f5a1 Add Java 18 to CI workflows 2022-04-12 15:40:28 +02:00
AsamK
a48601b028 Update repackage-native-libs.yml 2022-04-11 23:02:41 +02:00
AsamK
406faa1bbb Bump version 2022-04-11 22:30:32 +02:00
AsamK
8f85d164ff Update reflect-config.json 2022-04-11 22:30:32 +02:00
AsamK
6893e91190 Update dependencies 2022-04-11 22:24:48 +02:00
AsamK
945ff44de3 Refresh pre keys for PNI identity
Fixes #930
2022-04-11 20:05:02 +02:00
AsamK
2a20e70aab Add support for banning/unbanning group members 2022-04-09 19:03:05 +02:00
AsamK
69f1d0c213 Workaround adoc issue 2022-04-09 19:03:05 +02:00
AsamK
2c15995289 Output RATE_LIMIT_FAILURE type more reliably in json output
Fixes #923
2022-04-06 22:35:51 +02:00
AsamK
af71115767 Update reflect-config.json 2022-04-06 22:34:34 +02:00
AsamK
02a018462b Prevent deleting a group locally if the user is still a member
Fixes #924
2022-04-06 22:28:07 +02:00
AsamK
8dc82a30af Fix check if there was a send error when creating a group without members
Fixes #925
2022-04-06 22:17:06 +02:00
AsamK
c8f819cb94 Fix output of rate limit exception 2022-04-06 22:09:22 +02:00
AsamK
d27a12a6cf Update libsignal-service-java 2022-03-29 21:37:06 +02:00
AsamK
86e5ef7382 Fix plain text output of blocked group ids 2022-03-27 18:57:21 +02:00
AsamK
b5d578e483 Update graalvm build tools plugin 2022-03-27 18:56:36 +02:00
AsamK
9eb97746c1 Update libsignal-service-java 2022-03-17 23:33:57 +01:00
AsamK
fd25ef539f Bump version 2022-03-17 22:33:43 +01:00
AsamK
4f5d5afc4f Use correct captcha url in test script 2022-03-17 21:49:29 +01:00
AsamK
0686fd8e68 Fix multi account commands for newly created accounts
Fixes #913
2022-03-17 21:45:30 +01:00
AsamK
942999b7b4 Update libsignal-service-java 2022-03-16 21:08:41 +01:00
AsamK
3c2fa65e05 Fix NoSuchElementException in json serialization for messages from an untrusted identity
Fixes #910
2022-03-16 19:37:58 +01:00
AsamK
90df362c35 Bump version 2022-03-02 22:58:22 +01:00
AsamK
bf2a83755e Mark --target-timestamp flag as required for sendReceipt command
Fixes #901
2022-03-02 22:45:26 +01:00
AsamK
6f8784a1c9 Update libsignal-service-java
Fixes #897
2022-02-24 17:31:33 +01:00
AsamK
8b75504822 Bump version 2022-02-20 16:57:49 +01:00
AsamK
ec945cd227 Add --message-from-stdin flag for send command 2022-02-20 14:22:53 +01:00
AsamK
f3b2df62da Update libsignal-service-java 2022-02-20 14:22:53 +01:00
AsamK
2c44b65e9f Prevent preKeyIds from starting at zero 2022-02-20 14:22:53 +01:00
AsamK
1ffb6d6907 Handle UnsupportedOperationException in daemon command 2022-02-20 14:22:53 +01:00
AsamK
69fe3986cf Handle OverlappingFileLockException when linking 2022-02-20 14:22:53 +01:00
AsamK
abde122a35 Implement JSON-RPC client PoC 2022-02-20 14:22:49 +01:00
AsamK
7261129609 Rename error field in json receive response to exception 2022-02-19 18:47:11 +01:00
AsamK
3f582e9c2e Prevent a stale jsonrpc connection from interfering with message receiving
Fixes #893
2022-02-19 18:47:11 +01:00
AsamK
832604e763 Update graalvm build tools 2022-02-19 18:47:11 +01:00
AsamK
ccce539843 Improve error messages when daemon is already running 2022-02-13 20:08:30 +01:00
AsamK
90c787f8e2 Load multiple accounts in parallel 2022-02-13 20:08:30 +01:00
AsamK
cf0cc50e32 Improve stop receive handling
Only interrupt the receive thread if it is currently waiting for new
messages from the server, otherwise just set a stop flag.
2022-02-12 14:09:30 +01:00
AsamK
bb3b9692e3 Implement basic listAccounts for dbus client mode
Fixes #891
2022-02-12 11:20:05 +01:00
AsamK
0e0ef402fc Improve decryption error logging 2022-02-12 11:17:44 +01:00
AsamK
b0bb602eb5 Update libsignal-service-java 2022-02-11 22:52:06 +01:00
AsamK
d690b35ed9 Implement support for change number as linked device 2022-02-11 21:03:54 +01:00
AsamK
f207c2abc3 Reexport dbus objects when self number changes 2022-02-11 21:03:54 +01:00
AsamK
796f4d0d96 Prevent stripping number from self recipient in edge cases 2022-02-11 21:03:54 +01:00
AsamK
8985cc2656 Update self identifiers after whoAmI request 2022-02-11 21:03:54 +01:00
AsamK
7a06d3959e Extract number verification code logic 2022-02-11 21:03:54 +01:00
AsamK
292ef0f2da Move credentials provider to SignalAccount 2022-02-11 21:03:54 +01:00
AsamK
0476895c3d Store account list in accounts.json file 2022-02-11 21:03:54 +01:00
AsamK
ff6b733cd0 Add SignalAccountFiles as a central entry point 2022-02-11 21:03:54 +01:00
AsamK
1db7f8d76e Add additional logging to message send log store 2022-02-11 21:03:54 +01:00
AsamK
6dabf4550b Fix type of device id to Integer
Fixes #889
2022-02-11 21:03:54 +01:00
AsamK
fd8ba66509 Fix behavior when reading a v1 group from storage that has already been migrated locally
Fixes #888
2022-02-10 15:42:43 +01:00
AsamK
8966535efe Update reflect-config.json 2022-02-10 15:42:43 +01:00
Simon F
76f27449df
Add reference to alpine linux aport to README (#887) 2022-02-10 07:43:55 +01:00
AsamK
38ade4e985 Close database connection before sleeping 2022-02-09 19:09:03 +01:00
AsamK
8e773d92c1 Align receive timeout behavior for dbus client with cli and JSON-RPC
Timeout is reset by every incoming message
2022-02-06 19:33:50 +01:00
AsamK
5cccf52103 Cleanup manager package 2022-02-06 17:46:34 +01:00
AsamK
3040da99c4 Extra Database base class from AccountDatabase 2022-02-06 13:01:35 +01:00
AsamK
b9f66248ac Use RecipientAddress in AvatarStore 2022-02-06 12:29:47 +01:00
AsamK
285bfafdc1 Log exception when saving file fails 2022-02-04 15:43:20 +01:00
AsamK
aadd908464 Add new CA certificate to trust store for chat server.
Existing one expires in 2023.
2022-02-03 19:31:09 +01:00
AsamK
73d44ba3fe Switch to a less cpu intensive function to check if libsignal-client is available 2022-02-03 19:05:30 +01:00
Sebastian Scheibner
8c6b909365
Update README.md 2022-02-02 15:02:21 +01:00
AsamK
641856ebb4 Bump version 2022-02-01 21:52:34 +01:00
AsamK
be0993c5d8 Improve JSON-RPC subscribeReceive method with subscription id 2022-02-01 20:59:49 +01:00
exquo
e5a8cdb056
Repackage signal-client native builds (#879)
* Repackage signal-client native builds

* Change repo to upstream

* Use `listAccounts` to test run signal-cli

* Use "macOS" in filename
2022-02-01 20:57:32 +01:00
AsamK
e284b99076 Refactor JsonMessageEnvelope to remove unnecessary number canonicalization 2022-01-29 15:05:14 +01:00
AsamK
380c892e24 Add more informative thread names 2022-01-29 15:05:14 +01:00
AsamK
2e74acaabe Add --log-file parameter to write logs to separate file
Use logback for more control over the log output

Fixes #845
2022-01-29 15:05:08 +01:00
AsamK
95cc0ae7fd Implement MessageSendLog for resending after encryption error 2022-01-28 22:55:51 +01:00
AsamK
3491782912 Fix sender check for requesting message resend 2022-01-27 23:10:26 +01:00
AsamK
ffaa9d2ed3 Output sender also for sealed sender messages that fail to decrypt 2022-01-27 20:08:27 +01:00
morph027
d812c249ba
add GraalVM reflections for GroupJoinInfo (#872)
Signed-off-by: morph027 <stefan.heitmueller@gmx.com>

Co-authored-by: morph027 <stefan.heitmueller@gmx.com>
2022-01-26 22:04:36 +01:00
AsamK
238455ad6c Mark profile for refresh when receiving a profile key message 2022-01-26 22:01:09 +01:00
AsamK
ede0dfeef4 Fix output for envelope receipts 2022-01-26 22:01:09 +01:00
AsamK
67146f9cc7 Create stores in SignalAccount lazily 2022-01-26 21:03:04 +01:00
AsamK
e5537dc4db Update graalvm config 2022-01-26 19:22:46 +01:00
AsamK
5d23b1ed9d Improve error handling of getUserStatus command for invalid phonen numbers 2022-01-23 20:51:10 +01:00
morph027
166bec0f8d
add org.whispersystems.signalservice.internal.push.SignalServiceProtos.storyContext_ to graalvm reflect config (#868)
Signed-off-by: morph027 <stefan.heitmueller@gmx.com>

Co-authored-by: morph027 <stefan.heitmueller@gmx.com>
2022-01-23 20:48:55 +01:00
AsamK
d51b957ada Bump version 2022-01-22 22:58:46 +01:00
AsamK
4ffb93129d Update libsignal-service-java 2022-01-22 16:55:51 +01:00
AsamK
9439de1c15 Add recipient id to logging 2022-01-22 16:55:35 +01:00
AsamK
80befec589 Make deviceId an int 2022-01-22 16:41:00 +01:00
AsamK
a3c5cfd2f4 Archive old sessions when an identity key has changed 2022-01-22 13:21:56 +01:00
AsamK
7d935749aa Fix profile fetch with an invalid LANG variable 2022-01-17 18:26:08 +01:00
AsamK
4310059e6a Update gradle 2022-01-16 10:45:56 +01:00
AsamK
7b2b258b1d Update year 2022-01-16 10:45:36 +01:00
AsamK
c7e9e19a8a Fix CHANGELOG 2022-01-16 10:44:08 +01:00
395 changed files with 35742 additions and 12052 deletions

View file

@ -1,6 +1,14 @@
name: signal-cli CI
on: [ push, pull_request ]
on:
push:
branches:
- '**'
pull_request:
workflow_call:
permissions:
contents: write # to fetch code (actions/checkout) and submit dependency graph (gradle/gradle-build-action)
jobs:
build:
@ -8,20 +16,81 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17' ]
java: [ '21', '24' ]
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
dependency-graph: generate-and-submit
- name: Install asciidoc
run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base
- name: Build with Gradle
run: ./gradlew build
run: ./gradlew --no-daemon build
- name: Build man page
run: |
cd man
make install
- name: Add man page to archive
run: |
version=$(tar tf build/distributions/signal-cli-*.tar | head -n1 | sed 's|signal-cli-\([^/]*\)/.*|\1|')
echo $version
tar --transform="flags=r;s|man|signal-cli-${version}/man|" -rf build/distributions/signal-cli-${version}.tar man/man{1,5}
- name: Compress archive
run: gzip -n -9 build/distributions/signal-cli-*.tar
- name: Archive production artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: signal-cli-archive-${{ matrix.java }}
path: build/distributions/signal-cli-*.tar.gz
build-graalvm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: graalvm/setup-graalvm@v1
with:
version: 'latest'
java-version: '21'
cache: 'gradle'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build with Gradle
run: ./gradlew --no-daemon nativeCompile
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: signal-cli-native
path: build/native/nativeCompile/signal-cli
build-client:
strategy:
matrix:
os:
- ubuntu
- macos
- windows
runs-on: ${{ matrix.os }}-latest
defaults:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v4
- name: Install rust
run: rustup default stable
- name: Build client
run: cargo build --release --verbose
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: signal-cli-client-${{ matrix.os }}
path: |
client/target/release/signal-cli-client
client/target/release/signal-cli-client.exe

View file

@ -9,6 +9,10 @@ on:
schedule:
- cron: '0 7 * * 4'
permissions:
contents: read # to fetch code (actions/checkout)
security-events: write
jobs:
analyse:
name: Analyse
@ -17,12 +21,13 @@ jobs:
steps:
- name: Setup Java JDK
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'zulu'
java-version: 21
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@ -30,7 +35,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v3
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
@ -38,7 +43,7 @@ jobs:
# 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)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -52,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v3

257
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,257 @@
name: release
on:
push:
tags:
- v*
permissions:
contents: write # to fetch code (actions/checkout) and create release
env:
IMAGE_NAME: signal-cli
IMAGE_REGISTRY: ghcr.io/asamk
REGISTRY_USER: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ github.token }}
jobs:
ci_wf:
permissions:
contents: write
uses: AsamK/signal-cli/.github/workflows/ci.yml@master
# ${{ github.repository }} not accepted here
lib_to_jar:
needs: ci_wf
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
signal_cli_version: ${{ steps.cli_ver.outputs.version }}
release_id: ${{ steps.create_release.outputs.id }}
steps:
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v4
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Extract archive
run: |
tree .
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
rm -rf signal-cli-archive-*/
# - name: Get signal-client jar version
# id: lib_ver
# run: |
# 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 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 }}.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
# - 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:
needs: ci_wf
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v4
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file
run: |
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1)
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
rm -r signal-cli-archive-* signal-cli-native
mkdir -p build/install/
mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
- name: Build Image
id: build_image
uses: redhat-actions/buildah-build@v2
with:
image: ${{ env.IMAGE_NAME }}
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }}
containerfiles:
./Containerfile
oci: true
- name: Push To GHCR
uses: redhat-actions/push-to-registry@v2
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-native:
needs: ci_wf
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v4
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file
run: |
mkdir -p build/native/nativeCompile/
chmod +x ./signal-cli-native/signal-cli
mv ./signal-cli-native/signal-cli build/native/nativeCompile/
- name: Build Image
id: build_image
uses: redhat-actions/buildah-build@v2
with:
image: ${{ env.IMAGE_NAME }}
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native
containerfiles:
./native.Containerfile
oci: true
- name: Push To GHCR
uses: redhat-actions/push-to-registry@v2
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) }}"

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ local.properties
out/
.DS_Store
/bin/
/test-config/

View file

@ -4,8 +4,9 @@
<JavaCodeStyleSettings>
<option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" />
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="com" withSubpackages="true" static="false" />
@ -53,6 +54,9 @@
<option name="TERNARY_OPERATION_WRAP" value="5" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<option name="ARRAY_INITIALIZER_WRAP" value="5" />
<option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
<option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" />
<option name="ENUM_CONSTANTS_WRAP" value="2" />
</codeStyleSettings>
<codeStyleSettings language="XML">

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,18 @@
# Question
If you have a question you can ask it in the [GitHub discussions page](https://github.com/AsamK/signal-cli/discussions)
# Report a bug
- Search [existing issues](https://github.com/AsamK/signal-cli/issues?q=is%3Aissue) if it has been reported already
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/AsamK/signal-cli/issues/new).
- Be sure to include a **title and clear description**, as much relevant information as possible.
- Specify the signal-cli, JDK and OS version you're using. (and libsignal-client version, if self-compiled)
- Run the failing command with `--verbose` flag to get a more detailed log output and include that in the bug report
- If you're unable to find an open issue addressing the
problem, [open a new one](https://github.com/AsamK/signal-cli/issues/new).
- 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 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
# Pull request
- Code style should match the existing code, IntelliJ users can use the auto formatter
- Separate PRs should be opened for each implemented feature or bug fix

11
Containerfile Normal file
View file

@ -0,0 +1,11 @@
FROM docker.io/azul/zulu-openjdk:21-jre-headless
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 --create-home --home-dir /var/lib/signal-cli
ADD build/install/signal-cli /opt/signal-cli
USER signal-cli
ENTRYPOINT ["/opt/signal-cli/bin/signal-cli", "--config=/var/lib/signal-cli"]

View file

@ -1,3 +1,4 @@
github: AsamK
liberapay: asamk
ko_fi: asamk
#bitcoin: bc1qykae53fry8a8ycgdzgv0rlxfc959hmmllvz698

View file

@ -1,30 +1,33 @@
# signal-cli
signal-cli is a commandline interface
for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering,
verifying, sending and receiving messages. To be able to link to an existing Signal-Android/signal-cli instance,
signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because
libsignal-service-java does not yet
support [provisioning as a linked device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). 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. For this use-case, it has a dbus
interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)), that can be used to
send messages from any programming language that has dbus bindings. It also has a JSON-RPC based interface, see
the [documentation](https://github.com/AsamK/signal-cli/wiki/JSON-RPC-service) for more information.
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
It supports registering, verifying, sending and receiving messages.
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).
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.
For this use-case, it has a daemon mode with JSON-RPC interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.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.
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
You can [build signal-cli](#building) yourself, or use
You can [build signal-cli](#building) yourself or use
the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and
Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/) and there is
a [FreeBSD port](https://www.freshports.org/net-im/signal-cli) available as well.
Windows. There's also a [docker image and some Linux packages](https://github.com/AsamK/signal-cli/wiki/Binary-distributions) provided by the community.
System requirements:
- at least Java Runtime Environment (JRE) 17
- at least Java Runtime Environment (JRE) 21
- native library: libsignal-client
The native lib is bundled for x86_64 Linux (with recent enough glibc, see #643), for other systems/architectures
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
systems/architectures
see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
### Install system-wide on Linux
@ -41,7 +44,6 @@ sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
You can find further instructions on the Wiki:
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
- [DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service)
## Usage
@ -55,10 +57,17 @@ of all country codes.)
* Register a number (with SMS verification)
signal-cli -a ACCOUNT register
signal-cli -a ACCOUNT register
You can register Signal using a land line number. In this case you can skip SMS verification process and jump directly
to the voice call verification by adding the `--voice` switch at the end of above register command.
You can register Signal using a landline number. In this case, you need to follow the procedure below:
* 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
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
@ -66,19 +75,27 @@ of all country codes.)
* Verify the number using the code received via SMS or voice, optionally add `--pin PIN_CODE` if you've added a pin code
to your account
signal-cli -a ACCOUNT verify CODE
signal-cli -a ACCOUNT verify CODE
* Send a message
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
```sh
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.
uname -a | signal-cli -a ACCOUNT send RECIPIENT
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
* Receive messages
signal-cli -a ACCOUNT receive
signal-cli -a ACCOUNT receive
**Hint**: The Signal protocol expects that incoming messages are regularly received (using `daemon` or `receive`
command). This is required for the encryption to work efficiently and for getting updates to groups, expiration timer
@ -88,8 +105,8 @@ and other features.
The password and cryptographic keys are created when registering and stored in the current users home directory:
$XDG_DATA_HOME/signal-cli/data/
$HOME/.local/share/signal-cli/data/
$XDG_DATA_HOME/signal-cli/data/
$HOME/.local/share/signal-cli/data/
## Building
@ -98,44 +115,45 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
1. Checkout the source somewhere on your filesystem with
git clone https://github.com/AsamK/signal-cli.git
git clone https://github.com/AsamK/signal-cli.git
2. Execute Gradle:
./gradlew build
./gradlew build
2a. Create shell wrapper in *build/install/signal-cli/bin*:
./gradlew installDist
./gradlew installDist
2b. Create tar file in *build/distributions*:
./gradlew distTar
./gradlew distTar
2c. Create a fat tar file in *build/libs/signal-cli-fat*:
./gradlew fatJar
./gradlew fatJar
2d. Compile and run signal-cli:
./gradlew run --args="--help"
```sh
./gradlew run --args="--help"
```
### 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
work in all situations.
1. [Install GraalVM and setup the enviroment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
2. [Install prerequisites](https://www.graalvm.org/reference-manual/native-image/#prerequisites)
3. Execute Gradle:
1. [Install GraalVM and setup the environment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
2. Execute Gradle:
./gradlew nativeCompile
./gradlew nativeCompile
The binary is available at *build/native/nativeCompile/signal-cli*
## FAQ and Troubleshooting
For frequently asked questions and issues have a look at the [wiki](https://github.com/AsamK/signal-cli/wiki/FAQ)
For frequently asked questions and issues have a look at the [wiki](https://github.com/AsamK/signal-cli/wiki/FAQ).
## License

View file

@ -3,43 +3,94 @@ plugins {
application
eclipse
`check-lib-versions`
id("org.graalvm.buildtools.native") version "0.9.9"
id("org.graalvm.buildtools.native") version "0.10.6"
}
version = "0.10.1"
allprojects {
group = "org.asamk"
version = "0.13.19-SNAPSHOT"
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
toolchain {
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
}
}
}
application {
mainClass.set("org.asamk.signal.Main")
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
}
graalvmNative {
binaries {
this["main"].run {
buildArgs.add("--install-exit-handlers")
buildArgs.add("-Dfile.encoding=UTF-8")
buildArgs.add("-J-Dfile.encoding=UTF-8")
buildArgs.add("-march=compatibility")
resources.autodetect()
configurationFileDirectories.from(file("graalvm-config-dir"))
buildArgs.add("--allow-incomplete-classpath")
buildArgs.add("--report-unsupported-elements-at-runtime")
if (System.getenv("GRAALVM_HOME") == null) {
toolchainDetection.set(true)
javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(21))
})
} else {
toolchainDetection.set(false)
}
}
}
}
repositories {
mavenLocal()
mavenCentral()
val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
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",
)
)
dependencies {
implementation("org.bouncycastle", "bcprov-jdk15on", "1.70")
implementation("com.fasterxml.jackson.core", "jackson-databind", "2.13.1")
implementation("net.sourceforge.argparse4j", "argparse4j", "0.9.0")
implementation("com.github.hypfvieh", "dbus-java-transport-native-unixsocket", "4.0.0")
implementation("org.slf4j", "slf4j-simple", "1.7.32")
implementation("org.slf4j", "jul-to-slf4j", "1.7.32")
implementation(project(":lib"))
registerTransform(JarFileExcluder::class) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
parameters {
excludeFilesByArtifact = excludePatterns
}
}
implementation(libs.bouncycastle)
implementation(libs.jackson.databind)
implementation(libs.argparse4j)
implementation(libs.dbusjava)
implementation(libs.slf4j.api)
implementation(libs.slf4j.jul)
implementation(libs.logback)
implementation(project(":libsignal-cli"))
}
configurations {
@ -63,21 +114,26 @@ tasks.withType<Jar> {
attributes(
"Implementation-Title" to project.name,
"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")
exclude(
"META-INF/*.SF",
"META-INF/*.DSA",
"META-INF/*.RSA",
"META-INF/NOTICE",
"META-INF/LICENSE",
"**/module-info.class"
"META-INF/NOTICE*",
"META-INF/LICENSE*",
"META-INF/INDEX.LIST",
"**/module-info.class",
)
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
with(tasks.jar.get() as CopySpec)
duplicatesStrategy = DuplicatesStrategy.WARN
doFirst {
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}
with(tasks.jar.get())
}

View file

@ -1,7 +1,19 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
plugins {
`kotlin-dsl`
}
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
compilerOptions.jvmTarget.set(JvmTarget.JVM_17)
}
java {
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}

View file

@ -1,10 +1,10 @@
import groovy.util.XmlSlurper
import groovy.util.slurpersupport.GPathResult
import org.codehaus.groovy.runtime.ResourceGroovyMethods
@file:Suppress("DEPRECATION")
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import javax.xml.parsers.DocumentBuilderFactory
class CheckLibVersionsPlugin : Plugin<Project> {
override fun apply(project: Project) {
@ -26,15 +26,15 @@ class CheckLibVersionsPlugin : Plugin<Project> {
val name = dependency.name
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
try {
val url = ResourceGroovyMethods.toURL(metaDataUrl)
val metaDataText = ResourceGroovyMethods.getText(url)
val metadata = XmlSlurper().parseText(metaDataText)
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest")
val dbf = DocumentBuilderFactory.newInstance()
val db = dbf.newDocumentBuilder()
val doc = db.parse(metaDataUrl);
val newest = doc.getElementsByTagName("latest").item(0).textContent
if (version != newest.toString()) {
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
}
} catch (e: Throwable) {
logger.debug("Unable to download or parse $metaDataUrl: $e.message")
logger.debug("Unable to download or parse {}: {}", metaDataUrl, e.message)
}
}
}

View 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
}
}
}
}
}

1
client/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target/

1756
client/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

22
client/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "signal-cli-client"
version = "0.0.1"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
serde = "1"
serde_json = "1"
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
jsonrpsee = { version = "0.25", features = [
"macros",
"async-client",
"http-client",
] }
bytes = "1"
tokio-util = "0.7"
futures-util = "0.3"
thiserror = "2"

470
client/src/cli.rs Normal file
View file

@ -0,0 +1,470 @@
use std::{ffi::OsString, net::SocketAddr};
use clap::{crate_version, Parser, Subcommand, ValueEnum};
/// JSON-RPC client for signal-cli
#[derive(Parser, Debug)]
#[command(rename_all = "kebab-case", version = crate_version!())]
pub struct Cli {
/// Account to use (for daemon in multi-account mode)
#[arg(short = 'a', long)]
pub account: Option<String>,
/// TCP host and port of signal-cli daemon
#[arg(long, conflicts_with = "json_rpc_http")]
pub json_rpc_tcp: Option<Option<SocketAddr>>,
/// UNIX socket address and port of signal-cli daemon
#[cfg(unix)]
#[arg(long, conflicts_with = "json_rpc_tcp")]
pub json_rpc_socket: Option<Option<OsString>>,
/// HTTP URL of signal-cli daemon
#[arg(long, conflicts_with = "json_rpc_socket")]
pub json_rpc_http: Option<Option<String>>,
#[arg(long)]
pub verbose: bool,
#[command(subcommand)]
pub command: CliCommands,
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand, Debug)]
#[command(rename_all = "camelCase", version = crate_version!())]
pub enum CliCommands {
AddDevice {
#[arg(long)]
uri: String,
},
AddStickerPack {
#[arg(long)]
uri: String,
},
#[command(rename_all = "kebab-case")]
Block {
recipient: Vec<String>,
#[arg(short = 'g', long)]
group_id: Vec<String>,
},
DeleteLocalAccountData {
#[arg(long = "ignore-registered")]
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 {
recipient: Vec<String>,
#[arg(long)]
username: Vec<String>,
},
JoinGroup {
#[arg(long)]
uri: String,
},
Link {
#[arg(short = 'n', long)]
name: String,
},
ListAccounts,
ListContacts {
recipient: Vec<String>,
#[arg(short = 'a', long = "all-recipients")]
all_recipients: bool,
#[arg(long)]
blocked: Option<bool>,
#[arg(long)]
name: Option<String>,
},
ListDevices,
ListGroups {
#[arg(short = 'd', long)]
detailed: bool,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
},
ListIdentities {
#[arg(short = 'n', long)]
number: Option<String>,
},
ListStickerPacks,
QuitGroup {
#[arg(short = 'g', long = "group-id")]
group_id: String,
#[arg(long)]
delete: bool,
#[arg(long)]
admin: Vec<String>,
},
Receive {
#[arg(short = 't', long, default_value_t = 3.0)]
timeout: f64,
},
Register {
#[arg(short = 'v', long)]
voice: bool,
#[arg(long)]
captcha: Option<String>,
},
RemoveContact {
recipient: String,
#[arg(long)]
forget: bool,
#[arg(long)]
hide: bool,
},
RemoveDevice {
#[arg(short = 'd', long = "device-id")]
device_id: u32,
},
RemovePin,
RemoteDelete {
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: u64,
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(long = "note-to-self")]
note_to_self: bool,
},
#[command(rename_all = "kebab-case")]
Send {
recipient: Vec<String>,
#[arg(short = 'g', long)]
group_id: Vec<String>,
#[arg(long)]
note_to_self: bool,
#[arg(short = 'e', long)]
end_session: bool,
#[arg(short = 'm', long)]
message: Option<String>,
#[arg(short = 'a', long)]
attachment: Vec<String>,
#[arg(long)]
view_once: bool,
#[arg(long)]
mention: Vec<String>,
#[arg(long)]
text_style: Vec<String>,
#[arg(long)]
quote_timestamp: Option<u64>,
#[arg(long)]
quote_author: Option<String>,
#[arg(long)]
quote_message: Option<String>,
#[arg(long)]
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)]
sticker: Option<String>,
#[arg(long)]
story_timestamp: Option<u64>,
#[arg(long)]
story_author: Option<String>,
#[arg(long)]
edit_timestamp: Option<u64>,
},
SendContacts,
SendPaymentNotification {
recipient: String,
#[arg(long)]
receipt: String,
#[arg(long)]
note: String,
},
SendReaction {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(short = 'e', long)]
emoji: String,
#[arg(short = 'a', long = "target-author")]
target_author: String,
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: u64,
#[arg(short = 'r', long)]
remove: bool,
#[arg(long)]
story: bool,
},
SendReceipt {
recipient: String,
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: Vec<u64>,
#[arg(value_enum, long)]
r#type: ReceiptType,
},
SendSyncRequest,
SendTyping {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 's', long)]
stop: bool,
},
SendMessageRequestResponse {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
r#type: MessageRequestResponseType,
},
SetPin {
pin: String,
},
StartChangeNumber {
number: String,
#[arg(short = 'v', long)]
voice: bool,
#[arg(long)]
captcha: Option<String>,
},
SubmitRateLimitChallenge {
challenge: String,
captcha: String,
},
Trust {
recipient: String,
#[arg(short = 'a', long = "trust-all-known-keys")]
trust_all_known_keys: bool,
#[arg(short = 'v', long = "verified-safety-number")]
verified_safety_number: Option<String>,
},
#[command(rename_all = "kebab-case")]
Unblock {
recipient: Vec<String>,
#[arg(short = 'g', long)]
group_id: Vec<String>,
},
Unregister {
#[arg(long = "delete-account")]
delete_account: bool,
},
UpdateAccount {
#[arg(short = 'n', long = "device-name")]
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>,
},
UpdateConfiguration {
#[arg(long = "read-receipts")]
read_receipts: Option<bool>,
#[arg(long = "unidentified-delivery-indicators")]
unidentified_delivery_indicators: Option<bool>,
#[arg(long = "typing-indicators")]
typing_indicators: Option<bool>,
#[arg(long = "link-previews")]
link_previews: Option<bool>,
},
UpdateContact {
recipient: String,
#[arg(short = 'e', long)]
expiration: Option<u32>,
#[arg(short = 'n', long)]
name: Option<String>,
},
UpdateGroup {
#[arg(short = 'g', long = "group-id")]
group_id: Option<String>,
#[arg(short = 'n', long)]
name: Option<String>,
#[arg(short = 'd', long)]
description: Option<String>,
#[arg(short = 'a', long)]
avatar: Option<String>,
#[arg(short = 'm', long)]
member: Vec<String>,
#[arg(short = 'r', long = "remove-member")]
remove_member: Vec<String>,
#[arg(long)]
admin: Vec<String>,
#[arg(long = "remove-admin")]
remove_admin: Vec<String>,
#[arg(long)]
ban: Vec<String>,
#[arg(long)]
unban: Vec<String>,
#[arg(long = "reset-link")]
reset_link: bool,
#[arg(value_enum, long)]
link: Option<LinkState>,
#[arg(value_enum, long = "set-permission-add-member")]
set_permission_add_member: Option<GroupPermission>,
#[arg(value_enum, long = "set-permission-edit-details")]
set_permission_edit_details: Option<GroupPermission>,
#[arg(value_enum, long = "set-permission-send-messages")]
set_permission_send_messages: Option<GroupPermission>,
#[arg(short = 'e', long)]
expiration: Option<u32>,
},
UpdateProfile {
#[arg(long = "given-name")]
given_name: Option<String>,
#[arg(long = "family-name")]
family_name: Option<String>,
#[arg(long)]
about: Option<String>,
#[arg(long = "about-emoji")]
about_emoji: Option<String>,
#[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")]
mobile_coin_address: Option<String>,
#[arg(long)]
avatar: Option<String>,
#[arg(long = "remove-avatar")]
remove_avatar: bool,
},
UploadStickerPack {
path: String,
},
Verify {
verification_code: String,
#[arg(short = 'p', long)]
pin: Option<String>,
},
Version,
}
#[derive(ValueEnum, Clone, Debug)]
#[value(rename_all = "kebab-case")]
pub enum ReceiptType {
Read,
Viewed,
}
#[derive(ValueEnum, Clone, Debug)]
#[value(rename_all = "kebab-case")]
pub enum LinkState {
Enabled,
EnabledWithApproval,
Disabled,
}
#[derive(ValueEnum, Clone, Debug)]
#[value(rename_all = "kebab-case")]
pub enum GroupPermission {
EveryMember,
OnlyAdmins,
}
#[derive(ValueEnum, Clone, Debug)]
#[value(rename_all = "kebab-case")]
pub enum MessageRequestResponseType {
Accept,
Delete,
}

425
client/src/jsonrpc.rs Normal file
View file

@ -0,0 +1,425 @@
use std::path::Path;
use jsonrpsee::async_client::ClientBuilder;
use jsonrpsee::core::client::{Error, SubscriptionClientT};
use jsonrpsee::http_client::HttpClientBuilder;
use jsonrpsee::proc_macros::rpc;
use serde::Deserialize;
use serde_json::Value;
use tokio::net::ToSocketAddrs;
#[rpc(client)]
pub trait Rpc {
#[method(name = "addDevice", param_kind = map)]
async fn add_device(
&self,
account: Option<String>,
uri: String,
) -> 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)]
fn block(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "deleteLocalAccountData", param_kind = map)]
fn delete_local_account_data(
&self,
account: Option<String>,
#[allow(non_snake_case)] ignoreRegistered: Option<bool>,
) -> 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)]
fn get_user_status(
&self,
account: Option<String>,
recipients: Vec<String>,
usernames: Vec<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "joinGroup", param_kind = map)]
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)]
fn finish_link(
&self,
#[allow(non_snake_case)] deviceLinkUri: String,
#[allow(non_snake_case)] deviceName: String,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listAccounts", param_kind = map)]
fn list_accounts(&self) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listContacts", param_kind = map)]
fn list_contacts(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] allRecipients: bool,
blocked: Option<bool>,
name: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listDevices", param_kind = map)]
fn list_devices(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listGroups", param_kind = map)]
fn list_groups(
&self,
account: Option<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listIdentities", param_kind = map)]
fn list_identities(
&self,
account: Option<String>,
number: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listStickerPacks", param_kind = map)]
fn list_sticker_packs(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
#[method(name = "quitGroup", param_kind = map)]
fn quit_group(
&self,
account: Option<String>,
#[allow(non_snake_case)] groupId: String,
delete: bool,
admins: Vec<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "register", param_kind = map)]
fn register(
&self,
account: Option<String>,
voice: bool,
captcha: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "removeContact", param_kind = map)]
fn remove_contact(
&self,
account: Option<String>,
recipient: String,
forget: bool,
hide: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "removeDevice", param_kind = map)]
fn remove_device(
&self,
account: Option<String>,
#[allow(non_snake_case)] deviceId: u32,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "removePin", param_kind = map)]
fn remove_pin(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
#[method(name = "remoteDelete", param_kind = map)]
fn remote_delete(
&self,
account: Option<String>,
#[allow(non_snake_case)] targetTimestamp: u64,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
#[allow(non_snake_case)] noteToSelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[allow(non_snake_case)]
#[method(name = "send", param_kind = map)]
fn send(
&self,
account: Option<String>,
recipients: Vec<String>,
groupIds: Vec<String>,
noteToSelf: bool,
endSession: bool,
message: String,
attachments: Vec<String>,
viewOnce: bool,
mentions: Vec<String>,
textStyle: Vec<String>,
quoteTimestamp: Option<u64>,
quoteAuthor: Option<String>,
quoteMessage: Option<String>,
quoteMention: Vec<String>,
quoteTextStyle: Vec<String>,
quoteAttachment: Vec<String>,
previewUrl: Option<String>,
previewTitle: Option<String>,
previewDescription: Option<String>,
previewImage: Option<String>,
sticker: Option<String>,
storyTimestamp: Option<u64>,
storyAuthor: Option<String>,
editTimestamp: Option<u64>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendContacts", param_kind = map)]
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPaymentNotification", param_kind = map)]
fn send_payment_notification(
&self,
account: Option<String>,
recipient: String,
receipt: String,
note: String,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendReaction", param_kind = map)]
fn send_reaction(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
#[allow(non_snake_case)] noteToSelf: bool,
emoji: String,
#[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64,
remove: bool,
story: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendReceipt", param_kind = map)]
fn send_receipt(
&self,
account: Option<String>,
recipient: String,
#[allow(non_snake_case)] targetTimestamps: Vec<u64>,
r#type: String,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendSyncRequest", param_kind = map)]
fn send_sync_request(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendTyping", param_kind = map)]
fn send_typing(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
stop: bool,
) -> 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)]
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>;
#[method(name = "submitRateLimitChallenge", param_kind = map)]
fn submit_rate_limit_challenge(
&self,
account: Option<String>,
challenge: String,
captcha: String,
) -> 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)]
fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>;
#[method(name = "trust", param_kind = map)]
fn trust(
&self,
account: Option<String>,
recipient: String,
#[allow(non_snake_case)] trustAllKnownKeys: bool,
#[allow(non_snake_case)] verifiedSafetyNumber: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "unblock", param_kind = map)]
fn unblock(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "unregister", param_kind = map)]
fn unregister(
&self,
account: Option<String>,
#[allow(non_snake_case)] deleteAccount: bool,
) -> Result<Value, ErrorObjectOwned>;
#[allow(non_snake_case)]
#[method(name = "updateAccount", param_kind = map)]
fn update_account(
&self,
account: Option<String>,
deviceName: Option<String>,
unrestrictedUnidentifiedSender: Option<bool>,
discoverableByNumber: Option<bool>,
numberSharing: Option<bool>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateConfiguration", param_kind = map)]
fn update_configuration(
&self,
account: Option<String>,
#[allow(non_snake_case)] readReceipts: Option<bool>,
#[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>,
#[allow(non_snake_case)] typingIndicators: Option<bool>,
#[allow(non_snake_case)] linkPreviews: Option<bool>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateContact", param_kind = map)]
fn update_contact(
&self,
account: Option<String>,
recipient: String,
name: Option<String>,
expiration: Option<u32>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateGroup", param_kind = map)]
fn update_group(
&self,
account: Option<String>,
#[allow(non_snake_case)] groupId: Option<String>,
name: Option<String>,
description: Option<String>,
avatar: Option<String>,
member: Vec<String>,
#[allow(non_snake_case)] removeMember: Vec<String>,
admin: Vec<String>,
#[allow(non_snake_case)] removeAdmin: Vec<String>,
ban: Vec<String>,
unban: Vec<String>,
#[allow(non_snake_case)] resetLink: bool,
#[allow(non_snake_case)] link: Option<String>,
#[allow(non_snake_case)] setPermissionAddMember: Option<String>,
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
expiration: Option<u32>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateProfile", param_kind = map)]
fn update_profile(
&self,
account: Option<String>,
#[allow(non_snake_case)] givenName: Option<String>,
#[allow(non_snake_case)] familyName: Option<String>,
about: Option<String>,
#[allow(non_snake_case)] aboutEmoji: Option<String>,
#[allow(non_snake_case)] mobileCoinAddress: Option<String>,
avatar: Option<String>,
#[allow(non_snake_case)] removeAvatar: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "uploadStickerPack", param_kind = map)]
fn upload_sticker_pack(
&self,
account: Option<String>,
path: String,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "verify", param_kind = map)]
fn verify(
&self,
account: Option<String>,
#[allow(non_snake_case)] verificationCode: String,
pin: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[subscription(
name = "subscribeReceive" => "receive",
unsubscribe = "unsubscribeReceive",
item = Value,
param_kind = map
)]
async fn subscribe_receive(&self, account: Option<String>) -> SubscriptionResult;
#[method(name = "version")]
fn version(&self) -> Result<Value, ErrorObjectOwned>;
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonLink {
pub device_link_uri: String,
}
pub async fn connect_tcp(
tcp: impl ToSocketAddrs,
) -> Result<impl SubscriptionClientT, std::io::Error> {
let (sender, receiver) = super::transports::tcp::connect(tcp).await?;
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
}
#[cfg(unix)]
pub async fn connect_unix(
socket_path: impl AsRef<Path>,
) -> Result<impl SubscriptionClientT, std::io::Error> {
let (sender, receiver) = super::transports::ipc::connect(socket_path).await?;
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
}
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> {
HttpClientBuilder::default().build(uri)
}

526
client/src/main.rs Normal file
View file

@ -0,0 +1,526 @@
use std::{path::PathBuf, time::Duration};
use clap::Parser;
use jsonrpsee::core::client::{Error as RpcError, Subscription, SubscriptionClientT};
use serde_json::{Error, Value};
use tokio::{select, time::sleep};
use cli::Cli;
use crate::cli::{CliCommands, GroupPermission, LinkState};
use crate::jsonrpc::RpcClient;
mod cli;
#[allow(non_snake_case, clippy::too_many_arguments)]
mod jsonrpc;
mod transports;
const DEFAULT_TCP: &str = "127.0.0.1:7583";
const DEFAULT_SOCKET_SUFFIX: &str = "signal-cli/socket";
const DEFAULT_HTTP: &str = "http://localhost:8080/api/v1/rpc";
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let cli = cli::Cli::parse();
let result = connect(cli).await;
match result {
Ok(Value::Null) => {}
Ok(v) => println!("{v}"),
Err(e) => return Err(anyhow::anyhow!("JSON-RPC command failed: {e:?}")),
}
Ok(())
}
async fn handle_command(
cli: Cli,
client: impl SubscriptionClientT + Sync,
) -> Result<Value, RpcError> {
match cli.command {
CliCommands::Receive { timeout } => {
let mut stream = client.subscribe_receive(cli.account).await?;
{
while let Some(v) = stream_next(timeout, &mut stream).await {
let v = v?;
println!("{v}");
}
}
stream.unsubscribe().await?;
Ok(Value::Null)
}
CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
CliCommands::Block {
recipient,
group_id,
} => client.block(cli.account, recipient, group_id).await,
CliCommands::DeleteLocalAccountData { ignore_registered } => {
client
.delete_local_account_data(cli.account, ignore_registered)
.await
}
CliCommands::GetUserStatus {
recipient,
username,
} => {
client
.get_user_status(cli.account, recipient, username)
.await
}
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
CliCommands::Link { name } => {
let url = client
.start_link(cli.account)
.await
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
.device_link_uri;
println!("{url}");
client.finish_link(url, name).await
}
CliCommands::ListAccounts => client.list_accounts().await,
CliCommands::ListContacts {
recipient,
all_recipients,
blocked,
name,
} => {
client
.list_contacts(cli.account, recipient, all_recipients, blocked, name)
.await
}
CliCommands::ListDevices => client.list_devices(cli.account).await,
CliCommands::ListGroups {
detailed: _,
group_id,
} => client.list_groups(cli.account, group_id).await,
CliCommands::ListIdentities { number } => client.list_identities(cli.account, number).await,
CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
CliCommands::QuitGroup {
group_id,
delete,
admin,
} => {
client
.quit_group(cli.account, group_id, delete, admin)
.await
}
CliCommands::Register { voice, captcha } => {
client.register(cli.account, voice, captcha).await
}
CliCommands::RemoveContact {
recipient,
forget,
hide,
} => {
client
.remove_contact(cli.account, recipient, forget, hide)
.await
}
CliCommands::RemoveDevice { device_id } => {
client.remove_device(cli.account, device_id).await
}
CliCommands::RemovePin => client.remove_pin(cli.account).await,
CliCommands::RemoteDelete {
target_timestamp,
recipient,
group_id,
note_to_self,
} => {
client
.remote_delete(
cli.account,
target_timestamp,
recipient,
group_id,
note_to_self,
)
.await
}
CliCommands::Send {
recipient,
group_id,
note_to_self,
end_session,
message,
attachment,
view_once,
mention,
text_style,
quote_timestamp,
quote_author,
quote_message,
quote_mention,
quote_text_style,
quote_attachment,
preview_url,
preview_title,
preview_description,
preview_image,
sticker,
story_timestamp,
story_author,
edit_timestamp,
} => {
client
.send(
cli.account,
recipient,
group_id,
note_to_self,
end_session,
message.unwrap_or_default(),
attachment,
view_once,
mention,
text_style,
quote_timestamp,
quote_author,
quote_message,
quote_mention,
quote_text_style,
quote_attachment,
preview_url,
preview_title,
preview_description,
preview_image,
sticker,
story_timestamp,
story_author,
edit_timestamp,
)
.await
}
CliCommands::SendContacts => client.send_contacts(cli.account).await,
CliCommands::SendPaymentNotification {
recipient,
receipt,
note,
} => {
client
.send_payment_notification(cli.account, recipient, receipt, note)
.await
}
CliCommands::SendReaction {
recipient,
group_id,
note_to_self,
emoji,
target_author,
target_timestamp,
remove,
story,
} => {
client
.send_reaction(
cli.account,
recipient,
group_id,
note_to_self,
emoji,
target_author,
target_timestamp,
remove,
story,
)
.await
}
CliCommands::SendReceipt {
recipient,
target_timestamp,
r#type,
} => {
client
.send_receipt(
cli.account,
recipient,
target_timestamp,
match r#type {
cli::ReceiptType::Read => "read".to_owned(),
cli::ReceiptType::Viewed => "viewed".to_owned(),
},
)
.await
}
CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
CliCommands::SendTyping {
recipient,
group_id,
stop,
} => {
client
.send_typing(cli.account, recipient, group_id, stop)
.await
}
CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
client
.submit_rate_limit_challenge(cli.account, challenge, captcha)
.await
}
CliCommands::Trust {
recipient,
trust_all_known_keys,
verified_safety_number,
} => {
client
.trust(
cli.account,
recipient,
trust_all_known_keys,
verified_safety_number,
)
.await
}
CliCommands::Unblock {
recipient,
group_id,
} => client.unblock(cli.account, recipient, group_id).await,
CliCommands::Unregister { delete_account } => {
client.unregister(cli.account, delete_account).await
}
CliCommands::UpdateAccount {
device_name,
unrestricted_unidentified_sender,
discoverable_by_number,
number_sharing,
} => {
client
.update_account(
cli.account,
device_name,
unrestricted_unidentified_sender,
discoverable_by_number,
number_sharing,
)
.await
}
CliCommands::UpdateConfiguration {
read_receipts,
unidentified_delivery_indicators,
typing_indicators,
link_previews,
} => {
client
.update_configuration(
cli.account,
read_receipts,
unidentified_delivery_indicators,
typing_indicators,
link_previews,
)
.await
}
CliCommands::UpdateContact {
recipient,
expiration,
name,
} => {
client
.update_contact(cli.account, recipient, name, expiration)
.await
}
CliCommands::UpdateGroup {
group_id,
name,
description,
avatar,
member,
remove_member,
admin,
remove_admin,
ban,
unban,
reset_link,
link,
set_permission_add_member,
set_permission_edit_details,
set_permission_send_messages,
expiration,
} => {
client
.update_group(
cli.account,
group_id,
name,
description,
avatar,
member,
remove_member,
admin,
remove_admin,
ban,
unban,
reset_link,
link.map(|link| match link {
LinkState::Enabled => "enabled".to_owned(),
LinkState::EnabledWithApproval => "enabledWithApproval".to_owned(),
LinkState::Disabled => "disabled".to_owned(),
}),
set_permission_add_member.map(|p| match p {
GroupPermission::EveryMember => "everyMember".to_owned(),
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
}),
set_permission_edit_details.map(|p| match p {
GroupPermission::EveryMember => "everyMember".to_owned(),
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
}),
set_permission_send_messages.map(|p| match p {
GroupPermission::EveryMember => "everyMember".to_owned(),
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
}),
expiration,
)
.await
}
CliCommands::UpdateProfile {
given_name,
family_name,
about,
about_emoji,
mobile_coin_address,
avatar,
remove_avatar,
} => {
client
.update_profile(
cli.account,
given_name,
family_name,
about,
about_emoji,
mobile_coin_address,
avatar,
remove_avatar,
)
.await
}
CliCommands::UploadStickerPack { path } => {
client.upload_sticker_pack(cli.account, path).await
}
CliCommands::Verify {
verification_code,
pin,
} => client.verify(cli.account, verification_code, pin).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
}
}
}
async fn connect(cli: Cli) -> Result<Value, RpcError> {
if let Some(http) = &cli.json_rpc_http {
let uri = if let Some(uri) = http {
uri
} else {
DEFAULT_HTTP
};
let client = jsonrpc::connect_http(uri)
.await
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
handle_command(cli, client).await
} else if let Some(tcp) = cli.json_rpc_tcp {
let socket_addr = tcp.unwrap_or_else(|| DEFAULT_TCP.parse().unwrap());
let client = jsonrpc::connect_tcp(socket_addr)
.await
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
handle_command(cli, client).await
} else {
#[cfg(windows)]
{
Err(RpcError::Custom("Invalid socket".into()))
}
#[cfg(unix)]
{
let socket_path = cli
.json_rpc_socket
.clone()
.unwrap_or(None)
.or_else(|| {
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
PathBuf::from(runtime_dir)
.join(DEFAULT_SOCKET_SUFFIX)
.into()
})
})
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
let client = jsonrpc::connect_unix(socket_path)
.await
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
handle_command(cli, client).await
}
}
}
async fn stream_next(
timeout: f64,
stream: &mut Subscription<Value>,
) -> Option<Result<Value, Error>> {
if timeout < 0.0 {
stream.next().await
} else {
select! {
v = stream.next() => v,
_= sleep(Duration::from_millis((timeout * 1000.0) as u64)) => None,
}
}
}

View file

@ -0,0 +1,23 @@
use std::io::Error;
use std::path::Path;
use futures_util::stream::StreamExt;
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
use tokio::net::UnixStream;
use tokio_util::codec::Decoder;
use super::stream_codec::StreamCodec;
use super::{Receiver, Sender};
/// Connect to a JSON-RPC Unix Socket server.
pub async fn connect(
socket: impl AsRef<Path>,
) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
let connection = UnixStream::connect(socket).await?;
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
let sender = Sender { inner: sink };
let receiver = Receiver { inner: stream };
Ok((sender, receiver))
}

View file

@ -0,0 +1,60 @@
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
use thiserror::Error;
#[cfg(unix)]
pub mod ipc;
mod stream_codec;
pub mod tcp;
#[derive(Debug, Error)]
enum Errors {
#[error("Other: {0}")]
Other(String),
#[error("Closed")]
Closed,
}
struct Sender<T: Send + Sink<String>> {
inner: T,
}
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
for Sender<T>
{
type Error = Errors;
async fn send(&mut self, body: String) -> Result<(), Self::Error> {
self.inner
.send(body)
.await
.map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(())
}
async fn close(&mut self) -> Result<(), Self::Error> {
self.inner
.close()
.await
.map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(())
}
}
struct Receiver<T: Send + Stream> {
inner: T,
}
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
for Receiver<T>
{
type Error = Errors;
async fn receive(&mut self) -> Result<ReceivedMessage, Self::Error> {
match self.inner.next().await {
None => Err(Errors::Closed),
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
}
}
}

View file

@ -0,0 +1,61 @@
use bytes::BytesMut;
use std::{io, str};
use tokio_util::codec::{Decoder, Encoder};
type Separator = u8;
/// Stream codec for streaming protocols (ipc, tcp)
#[derive(Debug, Default)]
pub struct StreamCodec {
incoming_separator: Separator,
outgoing_separator: Separator,
}
impl StreamCodec {
/// Default codec with streaming input data. Input can be both enveloped and not.
pub fn stream_incoming() -> Self {
StreamCodec::new(b'\n', b'\n')
}
/// New custom stream codec
pub fn new(incoming_separator: Separator, outgoing_separator: Separator) -> Self {
StreamCodec {
incoming_separator,
outgoing_separator,
}
}
}
impl Decoder for StreamCodec {
type Item = String;
type Error = io::Error;
fn decode(&mut self, buf: &mut BytesMut) -> io::Result<Option<Self::Item>> {
if let Some(i) = buf
.as_ref()
.iter()
.position(|&b| b == self.incoming_separator)
{
let line = buf.split_to(i);
let _ = buf.split_to(1);
match str::from_utf8(line.as_ref()) {
Ok(s) => Ok(Some(s.to_string())),
Err(_) => Err(io::Error::other("invalid UTF-8")),
}
} else {
Ok(None)
}
}
}
impl Encoder<String> for StreamCodec {
type Error = io::Error;
fn encode(&mut self, msg: String, buf: &mut BytesMut) -> io::Result<()> {
let mut payload = msg.into_bytes();
payload.push(self.outgoing_separator);
buf.extend_from_slice(&payload);
Ok(())
}
}

View file

@ -0,0 +1,22 @@
use std::io::Error;
use futures_util::stream::StreamExt;
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
use tokio::net::{TcpStream, ToSocketAddrs};
use tokio_util::codec::Decoder;
use super::stream_codec::StreamCodec;
use super::{Receiver, Sender};
/// Connect to a JSON-RPC TCP server.
pub async fn connect(
socket: impl ToSocketAddrs,
) -> Result<(impl TransportSenderT + Send, impl TransportReceiverT + Send), Error> {
let connection = TcpStream::connect(socket).await?;
let (sink, stream) = StreamCodec::stream_incoming().framed(connection).split();
let sender = Sender { inner: sink };
let receiver = Receiver { inner: stream };
Ok((sender, receiver))
}

View file

@ -0,0 +1,112 @@
<?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.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>

View 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

View file

@ -32,8 +32,6 @@ RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
StandardInput=socket
StandardOutput=journal
StandardError=journal

View file

@ -8,11 +8,9 @@ After=network-online.target
[Service]
Type=dbus
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
BusName=org.asamk.Signal
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
[Install]
Alias=dbus-org.asamk.Signal.service

View file

@ -8,11 +8,9 @@ After=network-online.target
[Service]
Type=dbus
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
BusName=org.asamk.Signal
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
[Install]
Alias=dbus-org.asamk.Signal.service

View file

@ -1,170 +1,312 @@
[
{
"name":"[B"
},
{
"name":"[Z"
},
{
"name":"[[B"
},
{
"name":"com.sun.security.auth.module.UnixSystem",
"fields":[
{"name":"gid"},
{"name":"groups"},
{"name":"uid"},
{"name":"username"}
]}
,
"fields":[{"name":"gid"}, {"name":"groups"}, {"name":"uid"}, {"name":"username"}]
},
{
"name":"java.lang.Boolean",
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]}
,
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.Class",
"methods":[{"name":"getCanonicalName","parameterTypes":[] }, {"name":"getClassLoader","parameterTypes":[] }]
},
{
"name":"java.lang.ClassLoader",
"methods":[
{"name":"getPlatformClassLoader","parameterTypes":[] },
{"name":"loadClass","parameterTypes":["java.lang.String"] }
]}
,
"methods":[{"name":"getPlatformClassLoader","parameterTypes":[] }, {"name":"loadClass","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.ClassNotFoundException"
},
{
"name":"java.lang.Enum",
"methods":[{"name":"ordinal","parameterTypes":[] }]
},
{
"name":"java.lang.IllegalArgumentException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.IllegalStateException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.NoSuchMethodError"}
,
"name":"java.lang.Long",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"java.lang.NoClassDefFoundError"
},
{
"name":"java.lang.NoSuchMethodError"
},
{
"name":"java.lang.String"
},
{
"name":"java.lang.Thread",
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
},
{
"name":"java.lang.Throwable",
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }]
},
{
"name":"java.lang.UnsatisfiedLinkError",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
,
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.util.HashMap",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"java.util.Map",
"methods":[{"name":"get","parameterTypes":["java.lang.Object"] }, {"name":"put","parameterTypes":["java.lang.Object","java.lang.Object"] }, {"name":"remove","parameterTypes":["java.lang.Object"] }]
},
{
"name":"java.util.UUID",
"methods":[
{"name":"<init>","parameterTypes":["long","long"] },
{"name":"getLeastSignificantBits","parameterTypes":[] },
{"name":"getMostSignificantBits","parameterTypes":[] }
]}
,
"methods":[{"name":"<init>","parameterTypes":["long","long"] }, {"name":"getLeastSignificantBits","parameterTypes":[] }, {"name":"getMostSignificantBits","parameterTypes":[] }]
},
{
"name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"}
,
"name":"jdk.internal.loader.ClassLoaders$AppClassLoader"
},
{
"name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"
},
{
"name":"org.asamk.signal.manager.storage.protocol.SignalProtocolStore",
"methods":[
{"name":"getIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress"] },
{"name":"getIdentityKeyPair","parameterTypes":[] },
{"name":"getLocalRegistrationId","parameterTypes":[] },
{"name":"isTrustedIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.IdentityKey","org.whispersystems.libsignal.state.IdentityKeyStore$Direction"] },
{"name":"loadPreKey","parameterTypes":["int"] },
{"name":"loadSenderKey","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","java.util.UUID"] },
{"name":"loadSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress"] },
{"name":"loadSignedPreKey","parameterTypes":["int"] },
{"name":"removePreKey","parameterTypes":["int"] },
{"name":"saveIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.IdentityKey"] },
{"name":"storeSenderKey","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","java.util.UUID","org.whispersystems.libsignal.groups.state.SenderKeyRecord"] },
{"name":"storeSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.state.SessionRecord"] }
]}
,
"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.graalvm.nativebridge.jni.JNIExceptionWrapperEntryPoints",
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]}
,
"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.whispersystems.libsignal.DuplicateMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
,
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]
},
{
"name":"org.whispersystems.libsignal.IdentityKey",
"methods":[
{"name":"<init>","parameterTypes":["byte[]"] },
{"name":"serialize","parameterTypes":[] }
]}
,
"name":"org.signal.libsignal.internal.CompletableFuture",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }, {"name":"completeExceptionally","parameterTypes":["java.lang.Throwable"] }, {"name":"setCancellationId","parameterTypes":["long"] }]
},
{
"name":"org.whispersystems.libsignal.IdentityKeyPair",
"methods":[{"name":"serialize","parameterTypes":[] }]}
,
"name":"org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner",
"methods":[{"name":"unsafeNativeHandleWithoutGuard","parameterTypes":[] }]
},
{
"name":"org.whispersystems.libsignal.InvalidMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
,
"name":"org.signal.libsignal.net.CdsiLookupResponse",
"methods":[{"name":"<init>","parameterTypes":["java.util.Map","int"] }]
},
{
"name":"org.whispersystems.libsignal.SignalProtocolAddress",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","int"] }]}
,
"name":"org.signal.libsignal.net.CdsiLookupResponse$Entry",
"methods":[{"name":"<init>","parameterTypes":["byte[]","byte[]"] }]
},
{
"name":"org.whispersystems.libsignal.UntrustedIdentityException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]}
,
"name":"org.signal.libsignal.net.ChatService"
},
{
"name":"org.whispersystems.libsignal.groups.state.SenderKeyRecord",
"name":"org.signal.libsignal.net.ChatService$DebugInfo"
},
{
"name":"org.signal.libsignal.net.ChatService$Response"
},
{
"name":"org.signal.libsignal.net.ChatService$ResponseAndDebugInfo"
},
{
"name":"org.signal.libsignal.net.NetworkException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.net.RetryLaterException",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"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":"nativeHandle","parameterTypes":[] }
]}
,
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.whispersystems.libsignal.groups.state.SenderKeyStore"}
,
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyStore"
},
{
"name":"org.whispersystems.libsignal.logging.Log",
"methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]}
,
"name":"org.signal.libsignal.protocol.logging.Log",
"methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]
},
{
"name":"org.whispersystems.libsignal.protocol.PlaintextContent",
"name":"org.signal.libsignal.protocol.message.PlaintextContent",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.message.PreKeySignalMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"nativeHandle","parameterTypes":[] }]}
,
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.whispersystems.libsignal.protocol.PreKeySignalMessage",
"name":"org.signal.libsignal.protocol.message.SenderKeyMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[
{"name":"<init>","parameterTypes":["long"] },
{"name":"nativeHandle","parameterTypes":[] }
]}
,
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.whispersystems.libsignal.protocol.SenderKeyMessage",
"name":"org.signal.libsignal.protocol.message.SignalMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]}
,
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.whispersystems.libsignal.protocol.SignalMessage",
"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.IdentityKeyStore$IdentityChange"
},
{
"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":"nativeHandle","parameterTypes":[] }
]}
,
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["byte[]"] }]
},
{
"name":"org.whispersystems.libsignal.state.IdentityKeyStore"}
,
"name":"org.signal.libsignal.protocol.state.SessionStore"
},
{
"name":"org.whispersystems.libsignal.state.IdentityKeyStore$Direction",
"fields":[
{"name":"RECEIVING"},
{"name":"SENDING"}
]}
,
"name":"org.signal.libsignal.protocol.state.SignedPreKeyRecord",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.whispersystems.libsignal.state.PreKeyRecord",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"nativeHandle","parameterTypes":[] }]}
,
"name":"org.signal.libsignal.protocol.state.SignedPreKeyStore"
},
{
"name":"org.whispersystems.libsignal.state.PreKeyStore"}
,
"name":"org.signal.libsignal.usernames.BadDiscriminatorCharacterException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.whispersystems.libsignal.state.SessionRecord",
"fields":[{"name":"unsafeHandle"}],
"methods":[
{"name":"<init>","parameterTypes":["byte[]"] },
{"name":"nativeHandle","parameterTypes":[] }
]}
,
"name":"org.signal.libsignal.usernames.BadNicknameCharacterException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.whispersystems.libsignal.state.SessionStore"}
,
"name":"org.signal.libsignal.usernames.CannotBeEmptyException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.whispersystems.libsignal.state.SignedPreKeyRecord",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"nativeHandle","parameterTypes":[] }]}
,
"name":"org.signal.libsignal.usernames.DiscriminatorCannotBeZeroException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.whispersystems.libsignal.state.SignedPreKeyStore"}
"name":"org.signal.libsignal.usernames.MissingSeparatorException",
"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"] }]
}
]

View file

@ -1,20 +1,26 @@
[
{
"interfaces":["org.asamk.Signal"]}
,
"interfaces":["java.sql.Connection"]
},
{
"interfaces":["org.asamk.Signal$Configuration"]}
,
"interfaces":["org.asamk.Signal"]
},
{
"interfaces":["org.asamk.Signal$Device"]}
,
"interfaces":["org.asamk.Signal$Configuration"]
},
{
"interfaces":["org.asamk.Signal$Group"]}
,
"interfaces":["org.asamk.Signal$Device"]
},
{
"interfaces":["org.asamk.SignalControl"]}
,
"interfaces":["org.asamk.Signal$Group"]
},
{
"interfaces":["org.freedesktop.dbus.interfaces.DBus"]}
"interfaces":["org.asamk.Signal$Identity"]
},
{
"interfaces":["org.asamk.SignalControl"]
},
{
"interfaces":["org.freedesktop.dbus.interfaces.DBus"]
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,197 +1,226 @@
{
"resources":{
"includes":[
{
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.transport.ITransportProvider\\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_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_CN\\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_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_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_XK\\E"
},
{
"pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\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":"com/google/i18n/phonenumbers/data/.*"
}
]},
"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/com.sun.net.httpserver.spi.HttpServerProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E"
}, {
"pattern":"\\QMETA-INF/services/java.net.spi.InetAddressResolverProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.nio.file.spi.FileTypeDetector\\E"
}, {
"pattern":"\\QMETA-INF/services/java.sql.Driver\\E"
}, {
"pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.util.spi.ResourceBundleControlProvider\\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/kotlin.reflect.jvm.internal.impl.util.ModuleVisibilityHelper\\E"
}, {
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.message.ISocketProvider\\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/jvm/jvm.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.so\\E"
}, {
"pattern":"\\Qlibsignal_jni_aarch64.dylib\\E"
}, {
"pattern":"\\Qlibsignal_jni_amd64.dylib\\E"
}, {
"pattern":"\\Qlibsignal_jni_amd64.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":"\\Qsignal_jni_amd64.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:\\Qjdk/internal/icu/impl/data/icudt72b/nfc.nrm\\E"
}, {
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt72b/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"
}]
"name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl",
"locales":["", "de", "en", "und"]
}]
}

View file

@ -1,2 +1,8 @@
[
]
{
"types":[
],
"lambdaCapturingTypes":[
],
"proxies":[
]
}

17
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,17 @@
[versions]
slf4j = "2.0.17"
[libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.81"
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.19.1"
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
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.5.18"
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_127"
sqlite = "org.xerial:sqlite-jdbc:3.50.2.0"
hikari = "com.zaxxer:HikariCP:6.3.0"
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2"
junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.2"

Binary file not shown.

View file

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

49
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
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.
MAX_FD=maximum
@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@ -133,22 +133,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -193,18 +200,28 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# 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"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

41
gradlew.bat vendored
View file

@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
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

View file

@ -4,21 +4,37 @@ plugins {
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
repositories {
mavenLocal()
mavenCentral()
}
val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString()
dependencies {
implementation("com.github.turasa", "signal-service-java", "2.15.3_unofficial_38")
implementation("com.fasterxml.jackson.core", "jackson-databind", "2.13.1")
implementation("com.google.protobuf", "protobuf-javalite", "3.11.4")
implementation("org.bouncycastle", "bcprov-jdk15on", "1.70")
implementation("org.slf4j", "slf4j-api", "1.7.32")
if (libsignalClientPath == null) {
implementation(libs.signalservice)
} else {
implementation(libs.signalservice) {
exclude(group = "org.signal", module = "libsignal-client")
}
implementation(files(libsignalClientPath))
}
implementation(libs.jackson.databind)
implementation(libs.bouncycastle)
implementation(libs.slf4j.api)
implementation(libs.sqlite)
implementation(libs.hikari)
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.launcher)
}
tasks.named<Test>("test") {
useJUnitPlatform()
}
configurations {

View file

@ -1,55 +0,0 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.util.IOUtils;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class AttachmentStore {
private final File attachmentsPath;
public AttachmentStore(final File attachmentsPath) {
this.attachmentsPath = attachmentsPath;
}
public void storeAttachmentPreview(
final SignalServiceAttachmentRemoteId attachmentId, final AttachmentStorer storer
) throws IOException {
storeAttachment(getAttachmentPreviewFile(attachmentId), storer);
}
public void storeAttachment(
final SignalServiceAttachmentRemoteId attachmentId, final AttachmentStorer storer
) throws IOException {
storeAttachment(getAttachmentFile(attachmentId), storer);
}
private void storeAttachment(final File attachmentFile, final AttachmentStorer storer) throws IOException {
createAttachmentsDir();
try (OutputStream output = new FileOutputStream(attachmentFile)) {
storer.store(output);
}
}
private File getAttachmentPreviewFile(SignalServiceAttachmentRemoteId attachmentId) {
return new File(attachmentsPath, attachmentId.toString() + ".preview");
}
public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
return new File(attachmentsPath, attachmentId.toString());
}
private void createAttachmentsDir() throws IOException {
IOUtils.createPrivateDirectories(attachmentsPath);
}
@FunctionalInterface
public interface AttachmentStorer {
void store(OutputStream outputStream) throws IOException;
}
}

View file

@ -1,17 +0,0 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.jobs.Job;
public class JobExecutor {
private final Context context;
public JobExecutor(final Context context) {
this.context = context;
}
public void enqueueJob(Job job) {
job.run(context);
}
}

View file

@ -1,105 +1,92 @@
package org.asamk.signal.manager;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException;
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.Device;
import org.asamk.signal.manager.api.DeviceLimitExceededException;
import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.GroupId;
import org.asamk.signal.manager.api.GroupInviteLinkUrl;
import org.asamk.signal.manager.api.GroupNotFoundException;
import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.IdentityVerificationCode;
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.InvalidStickerException;
import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.LastGroupAdminException;
import org.asamk.signal.manager.api.Message;
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.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair;
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.Recipient;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults;
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.StickerPackUrl;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.asamk.signal.manager.api.UpdateProfile;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.io.InputStream;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public interface Manager extends Closeable {
static Manager init(
String number,
File settingsPath,
ServiceEnvironment serviceEnvironment,
String userAgent,
TrustNewIdentity trustNewIdentity
) throws IOException, NotRegisteredException {
var pathConfig = PathConfig.createDefault(settingsPath);
if (!SignalAccount.userExists(pathConfig.dataPath(), number)) {
throw new NotRegisteredException();
}
var account = SignalAccount.load(pathConfig.dataPath(), number, true, trustNewIdentity);
if (!account.isRegistered()) {
account.close();
throw new NotRegisteredException();
}
final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
}
static void initLogger() {
LibSignalLogger.initLogger();
}
static boolean isValidNumber(final String e164Number, final String countryCode) {
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
}
static List<String> getAllLocalAccountNumbers(File settingsPath) {
var pathConfig = PathConfig.createDefault(settingsPath);
final var dataPath = pathConfig.dataPath();
final var files = dataPath.listFiles();
if (files == null) {
return List.of();
static boolean isSignalClientAvailable() {
final Logger logger = LoggerFactory.getLogger(Manager.class);
try {
try {
org.signal.libsignal.internal.Native.UuidCiphertext_CheckValidContents(new byte[0]);
} catch (Exception e) {
logger.trace("Expected exception when checking libsignal-client: {}", e.getMessage());
}
return true;
} catch (UnsatisfiedLinkError e) {
logger.warn("Failed to call libsignal-client: {}", e.getMessage());
return false;
}
return Arrays.stream(files)
.filter(File::isFile)
.map(File::getName)
.filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
.toList();
}
String getSelfNumber();
void checkAccountState() throws IOException;
/**
* This is used for checking a set of phone numbers for registration on Signal
*
@ -107,79 +94,120 @@ 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.
* @throws IOException if it's unable to get the contacts to check if they're registered
*/
Map<String, Pair<String, UUID>> areUsersRegistered(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();
void updateConfiguration(Configuration configuration) throws IOException, NotMasterDeviceException;
void updateConfiguration(Configuration configuration) throws NotPrimaryDeviceException;
/**
* @param givenName if null, the previous givenName will be kept
* @param familyName if null, the previous familyName will be kept
* @param about if null, the previous about text will be kept
* @param aboutEmoji if null, the previous about emoji will be kept
* @param avatar if avatar is null the image from the local avatar store is used (if present),
* Update the user's profile.
* If a field is null, the previous value will be kept.
*/
void setProfile(
String givenName, String familyName, String about, String aboutEmoji, Optional<File> avatar
) throws IOException;
void updateProfile(UpdateProfile updateProfile) throws IOException;
String getUsername();
UsernameLinkUrl getUsernameLink();
/**
* Set a username for the account.
* If the username is null, it will be deleted.
*/
void setUsername(String username) throws IOException, InvalidUsernameException;
/**
* Set a username for the account.
* If the username is null, it will be deleted.
*/
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 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;
void removeLinkedDevices(long deviceId) throws IOException;
void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException;
void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException;
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException;
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotMasterDeviceException;
Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException;
List<Group> getGroups();
SendGroupMessageResults quitGroup(
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
GroupId groupId,
Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
void deleteGroup(GroupId groupId) throws IOException;
Pair<GroupId, SendGroupMessageResults> createGroup(
String name, Set<RecipientIdentifier.Single> members, File avatarFile
String name,
Set<RecipientIdentifier.Single> members,
String avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
SendGroupMessageResults updateGroup(
final GroupId groupId, final UpdateGroup updateGroup
final GroupId groupId,
final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
Pair<GroupId, SendGroupMessageResults> joinGroup(
GroupInviteLinkUrl inviteLinkUrl
) throws IOException, InactiveGroupLinkException;
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
SendMessageResults sendTypingMessage(
TypingAction action, Set<RecipientIdentifier> recipients
TypingAction action,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendReadReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
) throws IOException;
SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
SendMessageResults sendViewedReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
) throws IOException;
SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
SendMessageResults sendMessage(
Message message, Set<RecipientIdentifier> recipients
Message message,
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
SendMessageResults sendEditMessage(
Message message,
Set<RecipientIdentifier> recipients,
long editTargetTimestamp
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set<RecipientIdentifier> recipients
long targetSentTimestamp,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendMessageReaction(
@ -187,32 +215,54 @@ public interface Manager extends Closeable {
boolean remove,
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set<RecipientIdentifier> recipients
Set<RecipientIdentifier> recipients,
final boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPaymentNotificationMessage(
byte[] receipt,
String note,
RecipientIdentifier.Single recipient
) throws IOException;
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
SendMessageResults sendMessageRequestResponse(
MessageEnvelope.Sync.MessageRequestResponse.Type type,
Set<RecipientIdentifier> recipientIdentifiers
);
void hideRecipient(RecipientIdentifier.Single recipient);
void deleteRecipient(RecipientIdentifier.Single recipient);
void deleteContact(RecipientIdentifier.Single recipient);
void setContactName(
RecipientIdentifier.Single recipient, String name
) throws NotMasterDeviceException, IOException, UnregisteredRecipientException;
final RecipientIdentifier.Single recipient,
final String givenName,
final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) throws NotPrimaryDeviceException, UnregisteredRecipientException;
void setContactBlocked(
RecipientIdentifier.Single recipient, boolean blocked
) throws NotMasterDeviceException, IOException, UnregisteredRecipientException;
void setContactsBlocked(
Collection<RecipientIdentifier.Single> recipient,
boolean blocked
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
void setGroupBlocked(
GroupId groupId, boolean blocked
) throws GroupNotFoundException, IOException, NotMasterDeviceException;
void setGroupsBlocked(
Collection<GroupId> groupId,
boolean blocked
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
/**
* Change the expiration timer for a contact
*/
void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer
RecipientIdentifier.Single recipient,
int messageExpirationTimer
) throws IOException, UnregisteredRecipientException;
/**
@ -223,6 +273,8 @@ public interface Manager extends Closeable {
*/
StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException;
void installStickerPack(StickerPackUrl url) throws IOException;
List<StickerPack> getStickerPacks();
void requestAllSyncData() throws IOException;
@ -248,22 +300,26 @@ public interface Manager extends Closeable {
/**
* Receive new messages from server, returns if no new message arrive in a timespan of timeout.
*/
void receiveMessages(Duration timeout, ReceiveMessageHandler handler) throws IOException;
void receiveMessages(
Optional<Duration> timeout,
Optional<Integer> maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException;
/**
* Receive new messages from server, returns only if the thread is interrupted.
*/
void receiveMessages(ReceiveMessageHandler handler) throws IOException;
void stopReceiveMessages();
void setIgnoreAttachments(boolean ignoreAttachments);
boolean hasCaughtUpWithOldMessages();
void setReceiveConfig(ReceiveConfig receiveConfig);
boolean isContactBlocked(RecipientIdentifier.Single recipient);
void sendContacts() throws IOException;
List<Pair<RecipientAddress, Contact>> getContacts();
List<Recipient> getRecipients(
boolean onlyContacts,
Optional<Boolean> blocked,
Collection<RecipientIdentifier.Single> address,
Optional<String> name
);
String getContactOrProfileName(RecipientIdentifier.Single recipient);
@ -274,33 +330,13 @@ public interface Manager extends Closeable {
List<Identity> getIdentities(RecipientIdentifier.Single recipient);
/**
* Trust this the identity with this fingerprint
* Trust this the identity with this fingerprint/safetyNumber
*
* @param recipient account of the identity
* @param fingerprint Fingerprint
* @param recipient account of the identity
*/
boolean trustIdentityVerified(
RecipientIdentifier.Single recipient, byte[] fingerprint
) throws UnregisteredRecipientException;
/**
* Trust this the identity with this safety number
*
* @param recipient account of the identity
* @param safetyNumber Safety number
*/
boolean trustIdentityVerifiedSafetyNumber(
RecipientIdentifier.Single recipient, String safetyNumber
) throws UnregisteredRecipientException;
/**
* Trust this the identity with this scannable safety number
*
* @param recipient account of the identity
* @param safetyNumber Scannable safety number
*/
boolean trustIdentityVerifiedSafetyNumber(
RecipientIdentifier.Single recipient, byte[] safetyNumber
RecipientIdentifier.Single recipient,
IdentityVerificationCode verificationCode
) throws UnregisteredRecipientException;
/**
@ -310,10 +346,22 @@ public interface Manager extends Closeable {
*/
boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException;
void addAddressChangedListener(Runnable listener);
void addClosedListener(Runnable listener);
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;
@Override
void close() throws IOException;
void close();
interface ReceiveMessageHandler {

View file

@ -1,996 +0,0 @@
/*
Copyright (C) 2015-2021 AsamK and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.asamk.signal.manager;
import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.RecipientIdentifier;
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.StickerPack;
import org.asamk.signal.manager.api.StickerPackId;
import org.asamk.signal.manager.api.StickerPackUrl;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.Sticker;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.StickerUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ManagerImpl implements Manager {
private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
private SignalAccount account;
private final SignalDependencies dependencies;
private final Context context;
private final ExecutorService executor = Executors.newCachedThreadPool();
private Thread receiveThread;
private boolean isReceivingSynchronous;
private final Set<ReceiveMessageHandler> weakHandlers = new HashSet<>();
private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
private final List<Runnable> closedListeners = new ArrayList<>();
ManagerImpl(
SignalAccount account,
PathConfig pathConfig,
ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent
) {
this.account = account;
final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(),
account.getAccount(),
account.getPassword(),
account.getDeviceId());
final var sessionLock = new SignalSessionLock() {
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
@Override
public Lock acquire() {
LEGACY_LOCK.lock();
return LEGACY_LOCK::unlock;
}
};
this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
userAgent,
credentialsProvider,
account.getSignalProtocolStore(),
executor,
sessionLock);
final var avatarStore = new AvatarStore(pathConfig.avatarsPath());
final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath());
final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath());
this.context = new Context(account, dependencies, avatarStore, attachmentStore, stickerPackStore);
this.context.getAccountHelper().setUnregisteredListener(this::close);
this.context.getReceiveHelper().setAuthenticationFailureListener(this::close);
this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> {
synchronized (this) {
this.notifyAll();
}
});
}
@Override
public String getSelfNumber() {
return account.getAccount();
}
@Override
public void checkAccountState() throws IOException {
context.getAccountHelper().checkAccountState();
}
@Override
public Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) throws IOException {
final var canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
try {
final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getAccount());
if (!canonicalizedNumber.equals(n)) {
logger.debug("Normalized number {} to {}.", n, canonicalizedNumber);
}
return canonicalizedNumber;
} catch (InvalidNumberException e) {
return "";
}
}));
// Note "registeredUsers" has no optionals. It only gives us info on users who are registered
final var canonicalizedNumbersSet = canonicalizedNumbers.values()
.stream()
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
final var registeredUsers = context.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet);
return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
final var number = canonicalizedNumbers.get(n);
final var aci = registeredUsers.get(number);
return new Pair<>(number.isEmpty() ? null : number, aci == null ? null : aci.uuid());
}));
}
@Override
public void updateAccountAttributes(String deviceName) throws IOException {
if (deviceName != null) {
context.getAccountHelper().setDeviceName(deviceName);
}
context.getAccountHelper().updateAccountAttributes();
}
@Override
public Configuration getConfiguration() {
final var configurationStore = account.getConfigurationStore();
return Configuration.from(configurationStore);
}
@Override
public void updateConfiguration(
Configuration configuration
) throws NotMasterDeviceException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
final var configurationStore = account.getConfigurationStore();
if (configuration.readReceipts().isPresent()) {
configurationStore.setReadReceipts(configuration.readReceipts().get());
}
if (configuration.unidentifiedDeliveryIndicators().isPresent()) {
configurationStore.setUnidentifiedDeliveryIndicators(configuration.unidentifiedDeliveryIndicators().get());
}
if (configuration.typingIndicators().isPresent()) {
configurationStore.setTypingIndicators(configuration.typingIndicators().get());
}
if (configuration.linkPreviews().isPresent()) {
configurationStore.setLinkPreviews(configuration.linkPreviews().get());
}
context.getSyncHelper().sendConfigurationMessage();
}
@Override
public void setProfile(
String givenName, final String familyName, String about, String aboutEmoji, java.util.Optional<File> avatar
) throws IOException {
context.getProfileHelper()
.setProfile(givenName,
familyName,
about,
aboutEmoji,
avatar == null ? null : Optional.fromNullable(avatar.orElse(null)));
context.getSyncHelper().sendSyncFetchProfileMessage();
}
@Override
public void unregister() throws IOException {
context.getAccountHelper().unregister();
}
@Override
public void deleteAccount() throws IOException {
context.getAccountHelper().deleteAccount();
}
@Override
public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException {
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
}
@Override
public List<Device> getLinkedDevices() throws IOException {
var devices = dependencies.getAccountManager().getDevices();
account.setMultiDevice(devices.size() > 1);
var identityKey = account.getIdentityKeyPair().getPrivateKey();
return devices.stream().map(d -> {
String deviceName = d.getName();
if (deviceName != null) {
try {
deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey);
} catch (IOException e) {
logger.debug("Failed to decrypt device name, maybe plain text?", e);
}
}
return new Device(d.getId(),
deviceName,
d.getCreated(),
d.getLastSeen(),
d.getId() == account.getDeviceId());
}).toList();
}
@Override
public void removeLinkedDevices(long deviceId) throws IOException {
context.getAccountHelper().removeLinkedDevices(deviceId);
}
@Override
public void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException {
var deviceLinkInfo = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
context.getAccountHelper().addDevice(deviceLinkInfo);
}
@Override
public void setRegistrationLockPin(java.util.Optional<String> pin) throws IOException, NotMasterDeviceException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
if (pin.isPresent()) {
context.getAccountHelper().setRegistrationPin(pin.get());
} else {
context.getAccountHelper().removeRegistrationPin();
}
}
void refreshPreKeys() throws IOException {
context.getPreKeyHelper().refreshPreKeys();
}
@Override
public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
return context.getProfileHelper().getRecipientProfile(context.getRecipientHelper().resolveRecipient(recipient));
}
@Override
public List<Group> getGroups() {
return account.getGroupStore().getGroups().stream().map(this::toGroup).toList();
}
private Group toGroup(final GroupInfo groupInfo) {
if (groupInfo == null) {
return null;
}
return Group.from(groupInfo,
account.getRecipientStore()::resolveRecipientAddress,
account.getSelfRecipientId());
}
@Override
public SendGroupMessageResults quitGroup(
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException {
final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins);
return context.getGroupHelper().quitGroup(groupId, newAdmins);
}
@Override
public void deleteGroup(GroupId groupId) throws IOException {
context.getGroupHelper().deleteGroup(groupId);
}
@Override
public Pair<GroupId, SendGroupMessageResults> createGroup(
String name, Set<RecipientIdentifier.Single> members, File avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException {
return context.getGroupHelper()
.createGroup(name,
members == null ? null : context.getRecipientHelper().resolveRecipients(members),
avatarFile);
}
@Override
public SendGroupMessageResults updateGroup(
final GroupId groupId, final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException {
return context.getGroupHelper()
.updateGroup(groupId,
updateGroup.getName(),
updateGroup.getDescription(),
updateGroup.getMembers() == null
? null
: context.getRecipientHelper().resolveRecipients(updateGroup.getMembers()),
updateGroup.getRemoveMembers() == null
? null
: context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveMembers()),
updateGroup.getAdmins() == null
? null
: context.getRecipientHelper().resolveRecipients(updateGroup.getAdmins()),
updateGroup.getRemoveAdmins() == null
? null
: context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveAdmins()),
updateGroup.isResetGroupLink(),
updateGroup.getGroupLinkState(),
updateGroup.getAddMemberPermission(),
updateGroup.getEditDetailsPermission(),
updateGroup.getAvatarFile(),
updateGroup.getExpirationTimer(),
updateGroup.getIsAnnouncementGroup());
}
@Override
public Pair<GroupId, SendGroupMessageResults> joinGroup(
GroupInviteLinkUrl inviteLinkUrl
) throws IOException, InactiveGroupLinkException {
return context.getGroupHelper().joinGroup(inviteLinkUrl);
}
private SendMessageResults sendMessage(
SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
long timestamp = System.currentTimeMillis();
messageBuilder.withTimestamp(timestamp);
for (final var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single single) {
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(single);
final var result = context.getSendHelper().sendMessage(messageBuilder, recipientId);
results.put(recipient, List.of(toSendMessageResult(result)));
} catch (UnregisteredRecipientException e) {
results.put(recipient,
List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
}
} else if (recipient instanceof RecipientIdentifier.NoteToSelf) {
final var result = context.getSendHelper().sendSelfMessage(messageBuilder);
results.put(recipient, List.of(toSendMessageResult(result)));
} else if (recipient instanceof RecipientIdentifier.Group group) {
final var result = context.getSendHelper().sendAsGroupMessage(messageBuilder, group.groupId());
results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
}
}
return new SendMessageResults(timestamp, results);
}
private SendMessageResult toSendMessageResult(final org.whispersystems.signalservice.api.messages.SendMessageResult result) {
return SendMessageResult.from(result,
account.getRecipientStore(),
account.getRecipientStore()::resolveRecipientAddress);
}
private SendMessageResults sendTypingMessage(
SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
final var timestamp = System.currentTimeMillis();
for (var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single single) {
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent());
try {
final var recipientId = context.getRecipientHelper().resolveRecipient(single);
final var result = context.getSendHelper().sendTypingMessage(message, recipientId);
results.put(recipient, List.of(toSendMessageResult(result)));
} catch (UnregisteredRecipientException e) {
results.put(recipient,
List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress())));
}
} else if (recipient instanceof RecipientIdentifier.Group) {
final var groupId = ((RecipientIdentifier.Group) recipient).groupId();
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize()));
final var result = context.getSendHelper().sendGroupTypingMessage(message, groupId);
results.put(recipient, result.stream().map(this::toSendMessageResult).toList());
}
}
return new SendMessageResults(timestamp, results);
}
@Override
public SendMessageResults sendTypingMessage(
TypingAction action, Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return sendTypingMessage(action.toSignalService(), recipients);
}
@Override
public SendMessageResults sendReadReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
) throws IOException {
final var timestamp = System.currentTimeMillis();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
messageIds,
timestamp);
return sendReceiptMessage(sender, timestamp, receiptMessage);
}
@Override
public SendMessageResults sendViewedReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
) throws IOException {
final var timestamp = System.currentTimeMillis();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds,
timestamp);
return sendReceiptMessage(sender, timestamp, receiptMessage);
}
private SendMessageResults sendReceiptMessage(
final RecipientIdentifier.Single sender,
final long timestamp,
final SignalServiceReceiptMessage receiptMessage
) throws IOException {
try {
final var result = context.getSendHelper()
.sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender));
return new SendMessageResults(timestamp, Map.of(sender, List.of(toSendMessageResult(result))));
} catch (UnregisteredRecipientException e) {
return new SendMessageResults(timestamp,
Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress()))));
}
}
@Override
public SendMessageResults sendMessage(
Message message, Set<RecipientIdentifier> recipients
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
final var messageBuilder = SignalServiceDataMessage.newBuilder();
applyMessage(messageBuilder, message);
return sendMessage(messageBuilder, recipients);
}
private void applyMessage(
final SignalServiceDataMessage.Builder messageBuilder, final Message message
) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
messageBuilder.withBody(message.messageText());
final var attachments = message.attachments();
if (attachments != null) {
messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(attachments));
}
if (message.mentions().size() > 0) {
messageBuilder.withMentions(resolveMentions(message.mentions()));
}
if (message.quote().isPresent()) {
final var quote = message.quote().get();
messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(),
context.getRecipientHelper()
.resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(quote.author())),
quote.message(),
List.of(),
resolveMentions(quote.mentions())));
}
if (message.sticker().isPresent()) {
final var sticker = message.sticker().get();
final var packId = StickerPackId.deserialize(sticker.packId());
final var stickerId = sticker.stickerId();
final var stickerPack = context.getAccount().getStickerStore().getStickerPack(packId);
if (stickerPack == null) {
throw new InvalidStickerException("Sticker pack not found");
}
final var manifest = context.getStickerHelper().getOrRetrieveStickerPack(packId, stickerPack.getPackKey());
if (manifest.stickers().size() <= stickerId) {
throw new InvalidStickerException("Sticker id not part of this pack");
}
final var manifestSticker = manifest.stickers().get(stickerId);
final var streamDetails = context.getStickerPackStore().retrieveSticker(packId, stickerId);
if (streamDetails == null) {
throw new InvalidStickerException("Missing local sticker file");
}
messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(),
stickerPack.getPackKey(),
stickerId,
manifestSticker.emoji(),
AttachmentUtils.createAttachment(streamDetails, Optional.absent())));
}
}
private ArrayList<SignalServiceDataMessage.Mention> resolveMentions(final List<Message.Mention> mentionList) throws IOException, UnregisteredRecipientException {
final var mentions = new ArrayList<SignalServiceDataMessage.Mention>();
for (final var m : mentionList) {
final var recipientId = context.getRecipientHelper().resolveRecipient(m.recipient());
mentions.add(new SignalServiceDataMessage.Mention(context.getRecipientHelper()
.resolveSignalServiceAddress(recipientId)
.getAci(), m.start(), m.length()));
}
return mentions;
}
@Override
public SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
return sendMessage(messageBuilder, recipients);
}
@Override
public SendMessageResults sendMessageReaction(
String emoji,
boolean remove,
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor);
var reaction = new SignalServiceDataMessage.Reaction(emoji,
remove,
context.getRecipientHelper().resolveSignalServiceAddress(targetAuthorRecipientId),
targetSentTimestamp);
final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction);
return sendMessage(messageBuilder, recipients);
}
@Override
public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
try {
return sendMessage(messageBuilder,
recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()));
} catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
throw new AssertionError(e);
} finally {
for (var recipient : recipients) {
final RecipientId recipientId;
try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
} catch (UnregisteredRecipientException e) {
continue;
}
account.getSessionStore().deleteAllSessions(recipientId);
}
}
}
@Override
public void deleteRecipient(final RecipientIdentifier.Single recipient) {
account.removeRecipient(account.getRecipientStore().resolveRecipient(recipient.toPartialRecipientAddress()));
}
@Override
public void deleteContact(final RecipientIdentifier.Single recipient) {
account.getContactStore()
.deleteContact(account.getRecipientStore().resolveRecipient(recipient.toPartialRecipientAddress()));
}
@Override
public void setContactName(
RecipientIdentifier.Single recipient, String name
) throws NotMasterDeviceException, IOException, UnregisteredRecipientException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
context.getContactHelper().setContactName(context.getRecipientHelper().resolveRecipient(recipient), name);
}
@Override
public void setContactBlocked(
RecipientIdentifier.Single recipient, boolean blocked
) throws NotMasterDeviceException, IOException, UnregisteredRecipientException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
context.getContactHelper().setContactBlocked(context.getRecipientHelper().resolveRecipient(recipient), blocked);
// TODO cycle our profile key, if we're not together in a group with recipient
context.getSyncHelper().sendBlockedList();
}
@Override
public void setGroupBlocked(
final GroupId groupId, final boolean blocked
) throws GroupNotFoundException, NotMasterDeviceException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
context.getGroupHelper().setGroupBlocked(groupId, blocked);
// TODO cycle our profile key
context.getSyncHelper().sendBlockedList();
}
@Override
public void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer
) throws IOException, UnregisteredRecipientException {
var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer);
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
try {
sendMessage(messageBuilder, Set.of(recipient));
} catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) {
throw new AssertionError(e);
}
}
@Override
public StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
var messageSender = dependencies.getMessageSender();
var packKey = KeyUtils.createStickerUploadKey();
var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
var sticker = new Sticker(packId, packKey);
account.getStickerStore().updateSticker(sticker);
return new StickerPackUrl(packId, packKey);
}
@Override
public List<StickerPack> getStickerPacks() {
final var stickerPackStore = context.getStickerPackStore();
return account.getStickerStore().getStickerPacks().stream().map(pack -> {
if (stickerPackStore.existsStickerPack(pack.getPackId())) {
try {
final var manifest = stickerPackStore.retrieveManifest(pack.getPackId());
return new StickerPack(pack.getPackId(),
new StickerPackUrl(pack.getPackId(), pack.getPackKey()),
pack.isInstalled(),
manifest.title(),
manifest.author(),
java.util.Optional.ofNullable(manifest.cover() == null ? null : manifest.cover().toApi()),
manifest.stickers().stream().map(JsonStickerPack.JsonSticker::toApi).toList());
} catch (Exception e) {
logger.warn("Failed to read local sticker pack manifest: {}", e.getMessage(), e);
}
}
return new StickerPack(pack.getPackId(), pack.getPackKey(), pack.isInstalled());
}).toList();
}
@Override
public void requestAllSyncData() throws IOException {
context.getSyncHelper().requestAllSyncData();
retrieveRemoteStorage();
}
void retrieveRemoteStorage() throws IOException {
if (account.getStorageKey() != null) {
context.getStorageHelper().readDataFromStorage();
}
}
@Override
public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
if (isReceivingSynchronous) {
throw new IllegalStateException("Already receiving message synchronously.");
}
synchronized (messageHandlers) {
if (isWeakListener) {
weakHandlers.add(handler);
} else {
messageHandlers.add(handler);
startReceiveThreadIfRequired();
}
}
}
private void startReceiveThreadIfRequired() {
if (receiveThread != null) {
return;
}
receiveThread = new Thread(() -> {
logger.debug("Starting receiving messages");
while (!Thread.interrupted()) {
try {
context.getReceiveHelper().receiveMessages(Duration.ofMinutes(1), false, (envelope, e) -> {
synchronized (messageHandlers) {
Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> {
try {
h.handleMessage(envelope, e);
} catch (Exception ex) {
logger.warn("Message handler failed, ignoring", ex);
}
});
}
});
break;
} catch (IOException e) {
logger.warn("Receiving messages failed, retrying", e);
}
}
logger.debug("Finished receiving messages");
synchronized (messageHandlers) {
receiveThread = null;
// Check if in the meantime another handler has been registered
if (!messageHandlers.isEmpty()) {
logger.debug("Another handler has been registered, starting receive thread again");
startReceiveThreadIfRequired();
}
}
});
receiveThread.start();
}
@Override
public void removeReceiveHandler(final ReceiveMessageHandler handler) {
final Thread thread;
synchronized (messageHandlers) {
weakHandlers.remove(handler);
messageHandlers.remove(handler);
if (!messageHandlers.isEmpty() || receiveThread == null || isReceivingSynchronous) {
return;
}
thread = receiveThread;
receiveThread = null;
}
stopReceiveThread(thread);
}
private void stopReceiveThread(final Thread thread) {
thread.interrupt();
try {
thread.join();
} catch (InterruptedException ignored) {
}
}
@Override
public boolean isReceiving() {
if (isReceivingSynchronous) {
return true;
}
synchronized (messageHandlers) {
return messageHandlers.size() > 0;
}
}
@Override
public void receiveMessages(Duration timeout, ReceiveMessageHandler handler) throws IOException {
receiveMessages(timeout, true, handler);
}
@Override
public void receiveMessages(ReceiveMessageHandler handler) throws IOException {
receiveMessages(Duration.ofMinutes(1), false, handler);
}
private void receiveMessages(
Duration timeout, boolean returnOnTimeout, ReceiveMessageHandler handler
) throws IOException {
if (isReceiving()) {
throw new IllegalStateException("Already receiving message.");
}
isReceivingSynchronous = true;
receiveThread = Thread.currentThread();
try {
context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, handler);
} finally {
receiveThread = null;
isReceivingSynchronous = false;
}
}
@Override
public void setIgnoreAttachments(final boolean ignoreAttachments) {
context.getReceiveHelper().setIgnoreAttachments(ignoreAttachments);
}
@Override
public boolean hasCaughtUpWithOldMessages() {
return context.getReceiveHelper().hasCaughtUpWithOldMessages();
}
@Override
public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
final RecipientId recipientId;
try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
} catch (IOException | UnregisteredRecipientException e) {
return false;
}
return context.getContactHelper().isContactBlocked(recipientId);
}
@Override
public void sendContacts() throws IOException {
context.getSyncHelper().sendContacts();
}
@Override
public List<Pair<RecipientAddress, Contact>> getContacts() {
return account.getContactStore()
.getContacts()
.stream()
.map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second()))
.toList();
}
@Override
public String getContactOrProfileName(RecipientIdentifier.Single recipient) {
final RecipientId recipientId;
try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
} catch (IOException | UnregisteredRecipientException e) {
return null;
}
final var contact = account.getContactStore().getContact(recipientId);
if (contact != null && !Util.isEmpty(contact.getName())) {
return contact.getName();
}
final var profile = context.getProfileHelper().getRecipientProfile(recipientId);
if (profile != null) {
return profile.getDisplayName();
}
return null;
}
@Override
public Group getGroup(GroupId groupId) {
return toGroup(context.getGroupHelper().getGroup(groupId));
}
@Override
public List<Identity> getIdentities() {
return account.getIdentityKeyStore().getIdentities().stream().map(this::toIdentity).toList();
}
private Identity toIdentity(final IdentityInfo identityInfo) {
if (identityInfo == null) {
return null;
}
final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId());
final var scannableFingerprint = context.getIdentityHelper()
.computeSafetyNumberForScanning(identityInfo.getRecipientId(), identityInfo.getIdentityKey());
return new Identity(address,
identityInfo.getIdentityKey(),
context.getIdentityHelper()
.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()),
scannableFingerprint == null ? null : scannableFingerprint.getSerialized(),
identityInfo.getTrustLevel(),
identityInfo.getDateAdded());
}
@Override
public List<Identity> getIdentities(RecipientIdentifier.Single recipient) {
IdentityInfo identity;
try {
identity = account.getIdentityKeyStore()
.getIdentity(context.getRecipientHelper().resolveRecipient(recipient));
} catch (IOException | UnregisteredRecipientException e) {
identity = null;
}
return identity == null ? List.of() : List.of(toIdentity(identity));
}
@Override
public boolean trustIdentityVerified(
RecipientIdentifier.Single recipient, byte[] fingerprint
) throws UnregisteredRecipientException {
return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityVerified(r, fingerprint));
}
@Override
public boolean trustIdentityVerifiedSafetyNumber(
RecipientIdentifier.Single recipient, String safetyNumber
) throws UnregisteredRecipientException {
return trustIdentity(recipient,
r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber));
}
@Override
public boolean trustIdentityVerifiedSafetyNumber(
RecipientIdentifier.Single recipient, byte[] safetyNumber
) throws UnregisteredRecipientException {
return trustIdentity(recipient,
r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber));
}
@Override
public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityAllKeys(r));
}
private boolean trustIdentity(
RecipientIdentifier.Single recipient, Function<RecipientId, Boolean> trustMethod
) throws UnregisteredRecipientException {
RecipientId recipientId;
try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
} catch (IOException e) {
return false;
}
final var updated = trustMethod.apply(recipientId);
if (updated && this.isReceiving()) {
context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
}
return updated;
}
@Override
public void addClosedListener(final Runnable listener) {
synchronized (closedListeners) {
closedListeners.add(listener);
}
}
@Override
public void close() {
Thread thread;
synchronized (messageHandlers) {
weakHandlers.clear();
messageHandlers.clear();
thread = receiveThread;
receiveThread = null;
}
if (thread != null) {
stopReceiveThread(thread);
}
executor.shutdown();
dependencies.getSignalWebSocket().disconnect();
synchronized (closedListeners) {
closedListeners.forEach(Runnable::run);
closedListeners.clear();
}
if (account != null) {
account.close();
}
account = null;
}
}

View file

@ -0,0 +1,10 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.internal.LibSignalLogger;
public class ManagerLogger {
public static void initLogger() {
LibSignalLogger.initLogger();
}
}

View file

@ -10,6 +10,8 @@ public interface MultiAccountManager extends AutoCloseable {
List<String> getAccountNumbers();
List<Manager> getManagers();
void addOnManagerAddedHandler(Consumer<Manager> handler);
void addOnManagerRemovedHandler(Consumer<Manager> handler);

View file

@ -1,8 +0,0 @@
package org.asamk.signal.manager;
public class NotMasterDeviceException extends Exception {
public NotMasterDeviceException() {
super("This function is not supported for linked devices.");
}
}

View file

@ -1,36 +1,14 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.api.UserAlreadyExistsException;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
public interface ProvisioningManager {
static ProvisioningManager init(
File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
) {
return init(settingsPath, serviceEnvironment, userAgent, null);
}
static ProvisioningManager init(
File settingsPath,
ServiceEnvironment serviceEnvironment,
String userAgent,
Consumer<Manager> newManagerListener
) {
var pathConfig = PathConfig.createDefault(settingsPath);
final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
return new ProvisioningManagerImpl(pathConfig, serviceConfiguration, userAgent, newManagerListener);
}
URI getDeviceLinkUri() throws TimeoutException, IOException;
String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExists;
String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException;
}

View file

@ -1,190 +0,0 @@
/*
Copyright (C) 2015-2021 AsamK and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.asamk.signal.manager;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
public class ProvisioningManagerImpl implements ProvisioningManager {
private final static Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
private final PathConfig pathConfig;
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent;
private final Consumer<Manager> newManagerListener;
private final SignalServiceAccountManager accountManager;
private final IdentityKeyPair tempIdentityKey;
private final int registrationId;
private final String password;
ProvisioningManagerImpl(
PathConfig pathConfig,
ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent,
final Consumer<Manager> newManagerListener
) {
this.pathConfig = pathConfig;
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent;
this.newManagerListener = newManagerListener;
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
registrationId = KeyHelper.generateRegistrationId(false);
password = KeyUtils.createPassword();
GroupsV2Operations groupsV2Operations;
try {
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
} catch (Throwable ignored) {
groupsV2Operations = null;
}
accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
new DynamicCredentialsProvider(null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID),
userAgent,
groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
}
@Override
public URI getDeviceLinkUri() throws TimeoutException, IOException {
var deviceUuid = accountManager.getNewDeviceUuid();
return new DeviceLinkInfo(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
}
@Override
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExists {
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber();
logger.info("Received link information from {}, linking in progress ...", number);
if (SignalAccount.userExists(pathConfig.dataPath(), number) && !canRelinkExistingAccount(number)) {
throw new UserAlreadyExists(number, SignalAccount.getFileName(pathConfig.dataPath(), number));
}
var encryptedDeviceName = deviceName == null
? null
: DeviceNameUtil.encryptDeviceName(deviceName, ret.getIdentity().getPrivateKey());
logger.debug("Finishing new device registration");
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
false,
true,
registrationId,
encryptedDeviceName);
// Create new account with the synced identity
var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
SignalAccount account = null;
try {
account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.dataPath(),
number,
ret.getAci(),
password,
encryptedDeviceName,
deviceId,
ret.getIdentity(),
registrationId,
profileKey,
TrustNewIdentity.ON_FIRST_USE);
ManagerImpl m = null;
try {
m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
account = null;
logger.debug("Refreshing pre keys");
try {
m.refreshPreKeys();
} catch (Exception e) {
logger.error("Failed to refresh pre keys.");
}
logger.debug("Requesting sync data");
try {
m.requestAllSyncData();
} catch (Exception e) {
logger.error(
"Failed to request sync messages from linked device, data can be requested again with `sendSyncRequest`.");
}
if (newManagerListener != null) {
newManagerListener.accept(m);
m = null;
}
return number;
} finally {
if (m != null) {
m.close();
}
}
} finally {
if (account != null) {
account.close();
}
}
}
private boolean canRelinkExistingAccount(final String number) throws IOException {
final SignalAccount signalAccount;
try {
signalAccount = SignalAccount.load(pathConfig.dataPath(), number, false, TrustNewIdentity.ON_FIRST_USE);
} catch (IOException e) {
logger.debug("Account in use or failed to load.", e);
return false;
}
try (signalAccount) {
if (signalAccount.isMasterDevice()) {
logger.debug("Account is a master device.");
return false;
}
final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
try (m) {
m.checkAccountState();
} catch (AuthorizationFailedException ignored) {
return true;
}
logger.debug("Account is still successfully linked.");
return false;
}
}
}

View file

@ -2,64 +2,29 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException;
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.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.util.KeyUtils;
import org.whispersystems.libsignal.util.KeyHelper;
import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;
public interface RegistrationManager extends Closeable {
static RegistrationManager init(
String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
) throws IOException {
return init(number, settingsPath, serviceEnvironment, userAgent, null);
}
static RegistrationManager init(
String number,
File settingsPath,
ServiceEnvironment serviceEnvironment,
String userAgent,
Consumer<Manager> newManagerListener
) throws IOException {
var pathConfig = PathConfig.createDefault(settingsPath);
final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
if (!SignalAccount.userExists(pathConfig.dataPath(), number)) {
var identityKey = KeyUtils.generateIdentityKeyPair();
var registrationId = KeyHelper.generateRegistrationId(false);
var profileKey = KeyUtils.createProfileKey();
var account = SignalAccount.create(pathConfig.dataPath(),
number,
identityKey,
registrationId,
profileKey,
TrustNewIdentity.ON_FIRST_USE);
return new RegistrationManagerImpl(account,
pathConfig,
serviceConfiguration,
userAgent,
newManagerListener);
}
var account = SignalAccount.load(pathConfig.dataPath(), number, true, TrustNewIdentity.ON_FIRST_USE);
return new RegistrationManagerImpl(account, pathConfig, serviceConfiguration, userAgent, newManagerListener);
}
void register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException;
void register(
boolean voiceVerification,
String captcha,
final boolean forceRegister
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
void verifyAccount(
String verificationCode, String pin
) throws IOException, PinLockedException, IncorrectPinException;
String verificationCode,
String pin
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
void deleteLocalAccountData() throws IOException;
boolean isRegistered();
}

View file

@ -1,263 +0,0 @@
/*
Copyright (C) 2015-2021 AsamK and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.asamk.signal.manager;
import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.IOException;
import java.util.function.Consumer;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
public class RegistrationManagerImpl implements RegistrationManager {
private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class);
private SignalAccount account;
private final PathConfig pathConfig;
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent;
private final Consumer<Manager> newManagerListener;
private final SignalServiceAccountManager accountManager;
private final PinHelper pinHelper;
RegistrationManagerImpl(
SignalAccount account,
PathConfig pathConfig,
ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent,
Consumer<Manager> newManagerListener
) {
this.account = account;
this.pathConfig = pathConfig;
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent;
this.newManagerListener = newManagerListener;
GroupsV2Operations groupsV2Operations;
try {
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
} catch (Throwable ignored) {
groupsV2Operations = null;
}
this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
new DynamicCredentialsProvider(
// Using empty UUID, because registering doesn't work otherwise
null, account.getAccount(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID),
userAgent,
groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
final var keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
10);
this.pinHelper = new PinHelper(keyBackupService);
}
@Override
public void register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException {
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
if (account.getAci() != null) {
try {
final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
new DynamicCredentialsProvider(account.getAci(),
account.getAccount(),
account.getPassword(),
account.getDeviceId()),
userAgent,
null,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
accountManager.setAccountAttributes(account.getEncryptedDeviceName(),
null,
account.getLocalRegistrationId(),
true,
null,
account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
account.getSelfUnidentifiedAccessKey(),
account.isUnrestrictedUnidentifiedAccess(),
capabilities,
account.isDiscoverableByPhoneNumber());
account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) {
final var m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
account = null;
newManagerListener.accept(m);
}
return;
} catch (IOException e) {
logger.debug("Failed to reactivate account");
}
}
final ServiceResponse<RequestVerificationCodeResponse> response;
if (voiceVerification) {
response = accountManager.requestVoiceVerificationCode(Utils.getDefaultLocale(),
Optional.fromNullable(captcha),
Optional.absent(),
Optional.absent());
} else {
response = accountManager.requestSmsVerificationCode(false,
Optional.fromNullable(captcha),
Optional.absent(),
Optional.absent());
}
try {
handleResponseException(response);
} catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
throw new CaptchaRequiredException(e.getMessage(), e);
}
}
@Override
public void verifyAccount(
String verificationCode, String pin
) throws IOException, PinLockedException, IncorrectPinException {
verificationCode = verificationCode.replace("-", "");
VerifyAccountResponse response;
MasterKey masterKey;
try {
response = verifyAccountWithCode(verificationCode, null);
masterKey = null;
pin = null;
} catch (LockedException e) {
if (pin == null) {
throw new PinLockedException(e.getTimeRemaining());
}
KbsPinData registrationLockData;
try {
registrationLockData = pinHelper.getRegistrationLockData(pin, e);
} catch (KeyBackupSystemNoDataException ex) {
throw new IOException(e);
} catch (KeyBackupServicePinException ex) {
throw new IncorrectPinException(ex.getTriesRemaining());
}
if (registrationLockData == null) {
throw e;
}
var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
try {
response = verifyAccountWithCode(verificationCode, registrationLock);
} catch (LockedException _e) {
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
}
masterKey = registrationLockData.getMasterKey();
}
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
account.finishRegistration(ACI.parseOrNull(response.getUuid()), masterKey, pin);
ManagerImpl m = null;
try {
m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
account = null;
m.refreshPreKeys();
if (response.isStorageCapable()) {
m.retrieveRemoteStorage();
}
// Set an initial empty profile so user can be added to groups
try {
m.setProfile(null, null, null, null, null);
} catch (NoClassDefFoundError e) {
logger.warn("Failed to set default profile: {}", e.getMessage());
}
if (newManagerListener != null) {
newManagerListener.accept(m);
m = null;
}
} finally {
if (m != null) {
m.close();
}
}
}
private VerifyAccountResponse verifyAccountWithCode(
final String verificationCode, final String registrationLock
) throws IOException {
final ServiceResponse<VerifyAccountResponse> response;
if (registrationLock == null) {
response = accountManager.verifyAccount(verificationCode,
account.getLocalRegistrationId(),
true,
account.getSelfUnidentifiedAccessKey(),
account.isUnrestrictedUnidentifiedAccess(),
ServiceConfig.capabilities,
account.isDiscoverableByPhoneNumber());
} else {
response = accountManager.verifyAccountWithRegistrationLockPin(verificationCode,
account.getLocalRegistrationId(),
true,
registrationLock,
account.getSelfUnidentifiedAccessKey(),
account.isUnrestrictedUnidentifiedAccess(),
ServiceConfig.capabilities,
account.isDiscoverableByPhoneNumber());
}
handleResponseException(response);
return response.getResult().get();
}
@Override
public void close() {
if (account != null) {
account.close();
account = null;
}
}
private void handleResponseException(final ServiceResponse<?> response) throws IOException {
final var throwableOptional = response.getExecutionError().or(response.getApplicationError());
if (throwableOptional.isPresent()) {
if (throwableOptional.get() instanceof IOException) {
throw (IOException) throwableOptional.get();
} else {
throw new IOException(throwableOptional.get());
}
}
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.api.TrustNewIdentity;
public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) {
public static final Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false);
}

View file

@ -0,0 +1,208 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.api.AccountCheckException;
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.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.internal.AccountFileUpdaterImpl;
import org.asamk.signal.manager.internal.ManagerImpl;
import org.asamk.signal.manager.internal.MultiAccountManagerImpl;
import org.asamk.signal.manager.internal.PathConfig;
import org.asamk.signal.manager.internal.ProvisioningManagerImpl;
import org.asamk.signal.manager.internal.RegistrationManagerImpl;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.accounts.AccountsStore;
import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
public class SignalAccountFiles {
private static final Logger logger = LoggerFactory.getLogger(MultiAccountManager.class);
private final PathConfig pathConfig;
private final ServiceEnvironment serviceEnvironment;
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent;
private final Settings settings;
private final AccountsStore accountsStore;
public SignalAccountFiles(
final File settingsPath,
final ServiceEnvironment serviceEnvironment,
final String userAgent,
final Settings settings
) throws IOException {
this.pathConfig = PathConfig.createDefault(settingsPath);
this.serviceEnvironment = serviceEnvironment;
this.serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(this.serviceEnvironment, userAgent);
this.userAgent = userAgent;
this.settings = settings;
this.accountsStore = new AccountsStore(pathConfig.dataPath(), serviceEnvironment, accountPath -> {
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
return null;
}
try {
return SignalAccount.load(pathConfig.dataPath(), accountPath, false, settings);
} catch (Exception e) {
return null;
}
});
}
public Set<String> getAllLocalAccountNumbers() throws IOException {
return accountsStore.getAllNumbers();
}
public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException {
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
try {
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
} catch (NotRegisteredException e) {
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
return null;
} catch (AccountCheckException | IOException e) {
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
return new Pair<Manager, Throwable>(null, e);
}
}).filter(Objects::nonNull).toList();
for (final var pair : managerPairs) {
if (pair.second() instanceof IOException e) {
throw e;
} else if (pair.second() instanceof AccountCheckException e) {
throw e;
}
}
final var managers = managerPairs.stream().map(Pair::first).toList();
return new MultiAccountManagerImpl(managers, this);
}
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
final var accountPath = accountsStore.getPathByNumber(number);
return this.initManager(number, accountPath);
}
private Manager initManager(
String number,
String accountPath
) throws IOException, NotRegisteredException, AccountCheckException {
if (accountPath == null) {
throw new NotRegisteredException();
}
if (!SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
throw new NotRegisteredException();
}
var account = 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());
}
if (!account.isRegistered()) {
account.close();
throw new NotRegisteredException();
}
if (account.getServiceEnvironment() != null && account.getServiceEnvironment() != serviceEnvironment) {
throw new IOException("Account is registered in another environment: " + account.getServiceEnvironment());
}
account.initDatabase();
final var manager = new ManagerImpl(account,
pathConfig,
new AccountFileUpdaterImpl(accountsStore, accountPath),
serviceEnvironmentConfig,
userAgent);
try {
manager.checkAccountState();
} catch (DeprecatedVersionException e) {
manager.close();
throw new AccountCheckException("signal-cli version is too old for the Signal-Server, please update.");
} catch (IOException e) {
manager.close();
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
}
if (account.getServiceEnvironment() == null) {
account.setServiceEnvironment(serviceEnvironment);
accountsStore.updateAccount(accountPath, account.getNumber(), account.getAci());
}
return manager;
}
public ProvisioningManager initProvisioningManager() {
return initProvisioningManager(null);
}
public ProvisioningManager initProvisioningManager(Consumer<Manager> newManagerListener) {
return new ProvisioningManagerImpl(pathConfig,
serviceEnvironmentConfig,
userAgent,
newManagerListener,
accountsStore);
}
public RegistrationManager initRegistrationManager(String number) throws IOException {
return initRegistrationManager(number, null);
}
public RegistrationManager initRegistrationManager(
String number,
Consumer<Manager> newManagerListener
) throws IOException {
final var accountPath = accountsStore.getPathByNumber(number);
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
var aciIdentityKey = KeyUtils.generateIdentityKeyPair();
var pniIdentityKey = KeyUtils.generateIdentityKeyPair();
var profileKey = KeyUtils.createProfileKey();
var account = SignalAccount.create(pathConfig.dataPath(),
newAccountPath,
number,
serviceEnvironment,
aciIdentityKey,
pniIdentityKey,
profileKey,
settings);
account.initDatabase();
return new RegistrationManagerImpl(account,
pathConfig,
serviceEnvironmentConfig,
userAgent,
newManagerListener,
new AccountFileUpdaterImpl(accountsStore, newAccountPath));
}
var account = 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());
}
account.initDatabase();
return new RegistrationManagerImpl(account,
pathConfig,
serviceEnvironmentConfig,
userAgent,
newManagerListener,
new AccountFileUpdaterImpl(accountsStore, accountPath));
}
}

View file

@ -1,203 +0,0 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.util.concurrent.ExecutorService;
import java.util.function.Supplier;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
public class SignalDependencies {
private final Object LOCK = new Object();
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent;
private final DynamicCredentialsProvider credentialsProvider;
private final SignalServiceDataStore dataStore;
private final ExecutorService executor;
private final SignalSessionLock sessionLock;
private SignalServiceAccountManager accountManager;
private GroupsV2Api groupsV2Api;
private GroupsV2Operations groupsV2Operations;
private ClientZkOperations clientZkOperations;
private SignalWebSocket signalWebSocket;
private SignalServiceMessageReceiver messageReceiver;
private SignalServiceMessageSender messageSender;
private KeyBackupService keyBackupService;
private ProfileService profileService;
private SignalServiceCipher cipher;
public SignalDependencies(
final ServiceEnvironmentConfig serviceEnvironmentConfig,
final String userAgent,
final DynamicCredentialsProvider credentialsProvider,
final SignalServiceDataStore dataStore,
final ExecutorService executor,
final SignalSessionLock sessionLock
) {
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent;
this.credentialsProvider = credentialsProvider;
this.dataStore = dataStore;
this.executor = executor;
this.sessionLock = sessionLock;
}
public ServiceEnvironmentConfig getServiceEnvironmentConfig() {
return serviceEnvironmentConfig;
}
public SignalServiceAccountManager getAccountManager() {
return getOrCreate(() -> accountManager,
() -> accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
credentialsProvider,
userAgent,
getGroupsV2Operations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
}
public GroupsV2Api getGroupsV2Api() {
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
}
public GroupsV2Operations getGroupsV2Operations() {
return getOrCreate(() -> groupsV2Operations,
() -> groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
serviceEnvironmentConfig.getSignalServiceConfiguration())) : null);
}
private ClientZkOperations getClientZkOperations() {
return getOrCreate(() -> clientZkOperations,
() -> clientZkOperations = capabilities.isGv2()
? ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration())
: null);
}
private ClientZkProfileOperations getClientZkProfileOperations() {
final var clientZkOperations = getClientZkOperations();
return clientZkOperations == null ? null : clientZkOperations.getProfileOperations();
}
public SignalWebSocket getSignalWebSocket() {
return getOrCreate(() -> signalWebSocket, () -> {
final var timer = new UptimeSleepTimer();
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
final var webSocketFactory = new WebSocketFactory() {
@Override
public WebSocketConnection createWebSocket() {
return new WebSocketConnection("normal",
serviceEnvironmentConfig.getSignalServiceConfiguration(),
Optional.of(credentialsProvider),
userAgent,
healthMonitor);
}
@Override
public WebSocketConnection createUnidentifiedWebSocket() {
return new WebSocketConnection("unidentified",
serviceEnvironmentConfig.getSignalServiceConfiguration(),
Optional.absent(),
userAgent,
healthMonitor);
}
};
signalWebSocket = new SignalWebSocket(webSocketFactory);
healthMonitor.monitor(signalWebSocket);
});
}
public SignalServiceMessageReceiver getMessageReceiver() {
return getOrCreate(() -> messageReceiver,
() -> messageReceiver = new SignalServiceMessageReceiver(serviceEnvironmentConfig.getSignalServiceConfiguration(),
credentialsProvider,
userAgent,
getClientZkProfileOperations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
}
public SignalServiceMessageSender getMessageSender() {
return getOrCreate(() -> messageSender,
() -> messageSender = new SignalServiceMessageSender(serviceEnvironmentConfig.getSignalServiceConfiguration(),
credentialsProvider,
dataStore,
sessionLock,
userAgent,
getSignalWebSocket(),
Optional.absent(),
getClientZkProfileOperations(),
executor,
ServiceConfig.MAX_ENVELOPE_SIZE,
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
}
public KeyBackupService getKeyBackupService() {
return getOrCreate(() -> keyBackupService,
() -> keyBackupService = getAccountManager().getKeyBackupService(ServiceConfig.getIasKeyStore(),
serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
10));
}
public ProfileService getProfileService() {
return getOrCreate(() -> profileService,
() -> profileService = new ProfileService(getClientZkProfileOperations(),
getMessageReceiver(),
getSignalWebSocket()));
}
public SignalServiceCipher getCipher() {
return getOrCreate(() -> cipher, () -> {
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.getUnidentifiedSenderTrustRoot());
final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164());
final var deviceId = credentialsProvider.getDeviceId();
cipher = new SignalServiceCipher(address, deviceId, dataStore, sessionLock, certificateValidator);
});
}
private <T> T getOrCreate(Supplier<T> supplier, Callable creator) {
var value = supplier.get();
if (value != null) {
return value;
}
synchronized (LOCK) {
value = supplier.get();
if (value != null) {
return value;
}
creator.call();
return supplier.get();
}
}
private interface Callable {
void call();
}
}

View file

@ -1,196 +0,0 @@
package org.asamk.signal.manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Monitors the health of the identified and unidentified WebSockets. If either one appears to be
* unhealthy, will trigger restarting both.
* <p>
* The monitor is also responsible for sending heartbeats/keep-alive messages to prevent
* timeouts.
*/
public final class SignalWebSocketHealthMonitor implements HealthMonitor {
private final static Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class);
private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(WebSocketConnection.KEEPALIVE_TIMEOUT_SECONDS);
private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3;
private SignalWebSocket signalWebSocket;
private final SleepTimer sleepTimer;
private volatile KeepAliveSender keepAliveSender;
private final HealthState identified = new HealthState();
private final HealthState unidentified = new HealthState();
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
this.sleepTimer = sleepTimer;
}
public void monitor(SignalWebSocket signalWebSocket) {
Preconditions.checkNotNull(signalWebSocket);
Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once");
this.signalWebSocket = signalWebSocket;
//noinspection ResultOfMethodCallIgnored
signalWebSocket.getWebSocketState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(s -> onStateChange(s, identified));
//noinspection ResultOfMethodCallIgnored
signalWebSocket.getUnidentifiedWebSocketState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(s -> onStateChange(s, unidentified));
}
private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) {
switch (connectionState) {
case CONNECTED -> logger.debug("WebSocket is now connected");
case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed");
case FAILED -> logger.debug("WebSocket connection failed");
}
healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
if (keepAliveSender == null && isKeepAliveNecessary()) {
keepAliveSender = new KeepAliveSender();
keepAliveSender.start();
} else if (keepAliveSender != null && !isKeepAliveNecessary()) {
keepAliveSender.shutdown();
keepAliveSender = null;
}
}
@Override
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
if (isIdentifiedWebSocket) {
identified.lastKeepAliveReceived = System.currentTimeMillis();
} else {
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
}
}
@Override
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
if (status == 409) {
HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified);
if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
logger.warn("Received too many mismatch device errors, forcing new websockets.");
signalWebSocket.forceNewWebSockets();
signalWebSocket.connect();
}
}
}
private boolean isKeepAliveNecessary() {
return identified.needsKeepAlive || unidentified.needsKeepAlive;
}
private static class HealthState {
private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1));
private volatile boolean needsKeepAlive;
private volatile long lastKeepAliveReceived;
}
/**
* Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If
* either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated.
*/
private class KeepAliveSender extends Thread {
private volatile boolean shouldKeepRunning = true;
public void run() {
identified.lastKeepAliveReceived = System.currentTimeMillis();
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
while (shouldKeepRunning && isKeepAliveNecessary()) {
try {
sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE);
if (shouldKeepRunning && isKeepAliveNecessary()) {
long keepAliveRequiredSinceTime = System.currentTimeMillis()
- MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE;
if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime
|| unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) {
logger.warn("Missed keep alives, identified last: "
+ identified.lastKeepAliveReceived
+ " unidentified last: "
+ unidentified.lastKeepAliveReceived
+ " needed by: "
+ keepAliveRequiredSinceTime);
signalWebSocket.forceNewWebSockets();
signalWebSocket.connect();
} else {
signalWebSocket.sendKeepAlive();
}
}
} catch (Throwable e) {
logger.warn("Error occured in KeepAliveSender, ignoring ...", e);
}
}
}
public void shutdown() {
shouldKeepRunning = false;
}
}
private final static class HttpErrorTracker {
private final long[] timestamps;
private final long errorTimeRange;
public HttpErrorTracker(int samples, long errorTimeRange) {
this.timestamps = new long[samples];
this.errorTimeRange = errorTimeRange;
}
public synchronized boolean addSample(long now) {
long errorsMustBeAfter = now - errorTimeRange;
int count = 1;
int minIndex = 0;
for (int i = 0; i < timestamps.length; i++) {
if (timestamps[i] < errorsMustBeAfter) {
timestamps[i] = 0;
} else if (timestamps[i] != 0) {
count++;
}
if (timestamps[i] < timestamps[minIndex]) {
minIndex = i;
}
}
timestamps[minIndex] = now;
if (count >= timestamps.length) {
Arrays.fill(timestamps, 0);
return true;
}
return false;
}
}
}

View file

@ -2,21 +2,24 @@ package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.ServiceId;
public class RenewSessionAction implements HandleAction {
private final RecipientId recipientId;
private final ServiceId serviceId;
private final ServiceId accountId;
public RenewSessionAction(final RecipientId recipientId) {
public RenewSessionAction(final RecipientId recipientId, final ServiceId serviceId, final ServiceId accountId) {
this.recipientId = recipientId;
this.serviceId = serviceId;
this.accountId = accountId;
}
@Override
public void execute(Context context) throws Throwable {
context.getAccount().getSessionStore().archiveSessions(recipientId);
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) {
context.getSendHelper().sendNullMessage(recipientId);
}
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
context.getSendHelper().sendNullMessage(recipientId);
}
@Override

View file

@ -0,0 +1,44 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
import java.util.Objects;
public class ResendMessageAction implements HandleAction {
private final RecipientId recipientId;
private final long timestamp;
private final MessageSendLogEntry messageSendLogEntry;
public ResendMessageAction(
final RecipientId recipientId,
final long timestamp,
final MessageSendLogEntry messageSendLogEntry
) {
this.recipientId = recipientId;
this.timestamp = timestamp;
this.messageSendLogEntry = messageSendLogEntry;
}
@Override
public void execute(Context context) throws Throwable {
context.getSendHelper().resendMessage(recipientId, timestamp, messageSendLogEntry);
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ResendMessageAction that = (ResendMessageAction) o;
return timestamp == that.timestamp
&& recipientId.equals(that.recipientId)
&& messageSendLogEntry.equals(that.messageSendLogEntry);
}
@Override
public int hashCode() {
return Objects.hash(recipientId, timestamp, messageSendLogEntry);
}
}

View file

@ -1,26 +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 {
if (context.getAccount().getStorageKey() != null) {
context.getStorageHelper().readDataFromStorage();
} else {
if (!context.getAccount().isMasterDevice()) {
context.getSyncHelper().requestAllSyncData();
}
}
}
}

View file

@ -1,6 +1,6 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.api.GroupIdV1;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId;

View file

@ -1,6 +1,6 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.api.GroupIdV1;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId;

View file

@ -0,0 +1,33 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import java.util.Objects;
public class SendProfileKeyAction implements HandleAction {
private final RecipientId recipientId;
public SendProfileKeyAction(final RecipientId recipientId) {
this.recipientId = recipientId;
}
@Override
public void execute(Context context) throws Throwable {
context.getSendHelper().sendProfileKey(recipientId);
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendProfileKeyAction that = (SendProfileKeyAction) o;
return recipientId.equals(that.recipientId);
}
@Override
public int hashCode() {
return Objects.hash(recipientId);
}
}

View file

@ -2,6 +2,7 @@ package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import java.util.ArrayList;
import java.util.List;
@ -10,16 +11,24 @@ import java.util.Objects;
public class SendReceiptAction implements HandleAction {
private final RecipientId recipientId;
private final SignalServiceReceiptMessage.Type type;
private final List<Long> timestamps = new ArrayList<>();
public SendReceiptAction(final RecipientId recipientId, final long timestamp) {
public SendReceiptAction(
final RecipientId recipientId,
final SignalServiceReceiptMessage.Type type,
final long timestamp
) {
this.recipientId = recipientId;
this.type = type;
this.timestamps.add(timestamp);
}
@Override
public void execute(Context context) throws Throwable {
context.getSendHelper().sendDeliveryReceipt(recipientId, timestamps);
final var receiptMessage = new SignalServiceReceiptMessage(type, timestamps, System.currentTimeMillis());
context.getSendHelper().sendReceiptMessage(receiptMessage, recipientId);
}
@Override
@ -34,13 +43,13 @@ public class SendReceiptAction implements HandleAction {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SendReceiptAction that = (SendReceiptAction) o;
// Using only recipientId here on purpose
return recipientId.equals(that.recipientId);
// Using only recipientId and type here on purpose
return recipientId.equals(that.recipientId) && type == that.type;
}
@Override
public int hashCode() {
// Using only recipientId here on purpose
return Objects.hash(recipientId);
// Using only recipientId and type here on purpose
return Objects.hash(recipientId, type);
}
}

View file

@ -1,14 +1,15 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.api.GroupId;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.libsignal.metadata.ProtocolException;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
import org.whispersystems.libsignal.util.guava.Optional;
import org.signal.libsignal.protocol.message.CiphertextMessage;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.Envelope;
import java.util.Optional;
public class SendRetryMessageRequestAction implements HandleAction {
@ -28,11 +29,9 @@ public class SendRetryMessageRequestAction implements HandleAction {
@Override
public void execute(Context context) throws Throwable {
context.getAccount().getSessionStore().archiveSessions(recipientId);
int senderDevice = protocolException.getSenderDevice();
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
protocolException.getGroupId().get())) : Optional.absent();
protocolException.getGroupId().get())) : Optional.empty();
byte[] originalContent;
int envelopeType;
@ -42,7 +41,9 @@ public class SendRetryMessageRequestAction implements HandleAction {
envelopeType = messageContent.getType();
} else {
originalContent = envelope.getContent();
envelopeType = envelopeTypeToCiphertextMessageType(envelope.getType());
envelopeType = envelope.getType() == null
? CiphertextMessage.WHISPER_TYPE
: envelopeTypeToCiphertextMessageType(envelope.getType());
}
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent,
@ -54,10 +55,14 @@ public class SendRetryMessageRequestAction implements HandleAction {
}
private static int envelopeTypeToCiphertextMessageType(int envelopeType) {
return switch (envelopeType) {
case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE -> CiphertextMessage.PREKEY_TYPE;
case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE -> CiphertextMessage.SENDERKEY_TYPE;
case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
final var type = Envelope.Type.fromValue(envelopeType);
if (type == null) {
return CiphertextMessage.WHISPER_TYPE;
}
return switch (type) {
case PREKEY_BUNDLE -> CiphertextMessage.PREKEY_TYPE;
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
default -> CiphertextMessage.WHISPER_TYPE;
};
}

View file

@ -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());
}
}

View file

@ -0,0 +1,20 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
public class UpdateAccountAttributesAction implements HandleAction {
private static final UpdateAccountAttributesAction INSTANCE = new UpdateAccountAttributesAction();
private UpdateAccountAttributesAction() {
}
public static UpdateAccountAttributesAction create() {
return INSTANCE;
}
@Override
public void execute(Context context) throws Throwable {
context.getAccountHelper().updateAccountAttributes();
}
}

View file

@ -0,0 +1,12 @@
package org.asamk.signal.manager.api;
public class AccountCheckException extends Exception {
public AccountCheckException(String message) {
super(message);
}
public AccountCheckException(String message, Exception e) {
super(message, e);
}
}

View file

@ -0,0 +1,12 @@
package org.asamk.signal.manager.api;
public class AlreadyReceivingException extends Exception {
public AlreadyReceivingException(String message) {
super(message);
}
public AlreadyReceivingException(String message, Exception e) {
super(message, e);
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager;
package org.asamk.signal.manager.api;
public class AttachmentInvalidException extends Exception {

View file

@ -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);
}
}

View file

@ -2,6 +2,13 @@ package org.asamk.signal.manager.api;
public class CaptchaRequiredException extends Exception {
private long nextAttemptTimestamp;
public CaptchaRequiredException(final long nextAttemptTimestamp) {
super("Captcha required");
this.nextAttemptTimestamp = nextAttemptTimestamp;
}
public CaptchaRequiredException(final String message) {
super(message);
}
@ -9,4 +16,8 @@ public class CaptchaRequiredException extends Exception {
public CaptchaRequiredException(final String message, final Throwable cause) {
super(message, cause);
}
public long getNextAttemptTimestamp() {
return nextAttemptTimestamp;
}
}

View file

@ -0,0 +1,24 @@
package org.asamk.signal.manager.api;
public record Color(int color) {
public int alpha() {
return color >>> 24;
}
public int red() {
return (color >> 16) & 0xFF;
}
public int green() {
return (color >> 8) & 0xFF;
}
public int blue() {
return color & 0xFF;
}
public String toHexColor() {
return String.format("#%08x", color);
}
}

View file

@ -0,0 +1,193 @@
package org.asamk.signal.manager.api;
import org.whispersystems.signalservice.internal.util.Util;
public record Contact(
String givenName,
String familyName,
String nickName,
String nickNameGivenName,
String nickNameFamilyName,
String note,
String color,
int messageExpirationTime,
int messageExpirationTimeVersion,
long muteUntil,
boolean hideStory,
boolean isBlocked,
boolean isArchived,
boolean isProfileSharingEnabled,
boolean isHidden,
Long unregisteredTimestamp
) {
private Contact(final Builder builder) {
this(builder.givenName,
builder.familyName,
builder.nickName,
builder.nickNameGivenName,
builder.nickNameFamilyName,
builder.note,
builder.color,
builder.messageExpirationTime,
builder.messageExpirationTimeVersion,
builder.muteUntil,
builder.hideStory,
builder.isBlocked,
builder.isArchived,
builder.isProfileSharingEnabled,
builder.isHidden,
builder.unregisteredTimestamp);
}
public static Builder newBuilder() {
return new Builder();
}
public static Builder newBuilder(final Contact copy) {
Builder builder = new Builder();
builder.givenName = copy.givenName();
builder.familyName = copy.familyName();
builder.nickName = copy.nickName();
builder.nickNameGivenName = copy.nickNameGivenName();
builder.nickNameFamilyName = copy.nickNameFamilyName();
builder.note = copy.note();
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.isArchived = copy.isArchived();
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
builder.isHidden = copy.isHidden();
builder.unregisteredTimestamp = copy.unregisteredTimestamp();
return builder;
}
public String getName() {
final var noGivenName = Util.isEmpty(givenName);
final var noFamilyName = Util.isEmpty(familyName);
if (noGivenName && noFamilyName) {
return "";
} else if (noGivenName) {
return familyName;
} else if (noFamilyName) {
return givenName;
}
return givenName + " " + familyName;
}
public static final class Builder {
private String givenName;
private String familyName;
private String nickName;
private String nickNameGivenName;
private String nickNameFamilyName;
private String note;
private String color;
private int messageExpirationTime;
private int messageExpirationTimeVersion = 1;
private long muteUntil;
private boolean hideStory;
private boolean isBlocked;
private boolean isArchived;
private boolean isProfileSharingEnabled;
private boolean isHidden;
private Long unregisteredTimestamp;
private Builder() {
}
public static Builder newBuilder() {
return new Builder();
}
public Builder withGivenName(final String val) {
givenName = val;
return this;
}
public Builder withFamilyName(final String val) {
familyName = val;
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) {
color = val;
return this;
}
public Builder withMessageExpirationTime(final int val) {
messageExpirationTime = val;
return this;
}
public Builder withMessageExpirationTimeVersion(final int val) {
messageExpirationTimeVersion = val;
return this;
}
public Builder withMuteUntil(final long val) {
muteUntil = val;
return this;
}
public Builder withHideStory(final boolean val) {
hideStory = val;
return this;
}
public Builder withIsBlocked(final boolean val) {
isBlocked = 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;
}
public Contact build() {
return new Contact(this);
}
}
}

View file

@ -1,3 +1,3 @@
package org.asamk.signal.manager.api;
public record Device(long id, String name, long created, long lastSeen, boolean isThisDevice) {}
public record Device(int id, String name, long created, long lastSeen, boolean isThisDevice) {}

View file

@ -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);
}
}

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager;
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.util.Utils;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import java.net.URI;
import java.net.URISyntaxException;
@ -14,9 +12,9 @@ import java.util.Base64;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public record DeviceLinkInfo(String deviceIdentifier, ECPublicKey deviceKey) {
public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
public static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws InvalidDeviceLinkException {
public static DeviceLinkUrl parseDeviceLinkUri(URI linkUri) throws InvalidDeviceLinkException {
final var rawQuery = linkUri.getRawQuery();
if (isEmpty(rawQuery)) {
throw new RuntimeException("Invalid device link uri");
@ -38,12 +36,12 @@ public record DeviceLinkInfo(String deviceIdentifier, ECPublicKey deviceKey) {
}
ECPublicKey deviceKey;
try {
deviceKey = Curve.decodePoint(publicKeyBytes, 0);
deviceKey = new ECPublicKey(publicKeyBytes);
} catch (InvalidKeyException e) {
throw new InvalidDeviceLinkException("Invalid device link", e);
}
return new DeviceLinkInfo(deviceIdentifier, deviceKey);
return new DeviceLinkUrl(deviceIdentifier, deviceKey);
}
public URI createDeviceLinkUri() {

View file

@ -1,11 +1,7 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import java.util.Set;
@ -20,6 +16,7 @@ public record Group(
Set<RecipientAddress> pendingMembers,
Set<RecipientAddress> requestingMembers,
Set<RecipientAddress> adminMembers,
Set<RecipientAddress> bannedMembers,
boolean isBlocked,
int messageExpirationTimer,
GroupPermission permissionAddMember,
@ -30,7 +27,9 @@ public record Group(
) {
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(),
groupInfo.getTitle(),
@ -39,18 +38,27 @@ public record Group(
groupInfo.getMembers()
.stream()
.map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()),
groupInfo.getPendingMembers()
.stream()
.map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()),
groupInfo.getRequestingMembers()
.stream()
.map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()),
groupInfo.getAdminMembers()
.stream()
.map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()),
groupInfo.getBannedMembers()
.stream()
.map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()),
groupInfo.isBlocked(),
groupInfo.getMessageExpirationTimer(),

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
import java.util.Arrays;
import java.util.Base64;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
public class GroupIdFormatException extends Exception {

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
import java.util.Base64;

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
import java.util.Base64;

View file

@ -1,17 +1,18 @@
package org.asamk.signal.manager.groups;
import com.google.protobuf.ByteString;
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupLinkPassword;
import org.signal.core.util.Base64;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.storageservice.protos.groups.GroupInviteLink;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.util.Base64UrlSafe;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import okio.ByteString;
public final class GroupInviteLinkUrl {
private static final String GROUP_URL_HOST = "signal.group";
@ -23,7 +24,7 @@ public final class GroupInviteLinkUrl {
public static GroupInviteLinkUrl forGroup(GroupMasterKey groupMasterKey, DecryptedGroup group) {
return new GroupInviteLinkUrl(groupMasterKey,
GroupLinkPassword.fromBytes(group.getInviteLinkPassword().toByteArray()));
GroupLinkPassword.fromBytes(group.inviteLinkPassword.toByteArray()));
}
/**
@ -38,30 +39,27 @@ public final class GroupInviteLinkUrl {
}
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");
}
var encoding = uri.getFragment();
if (encoding == null || encoding.length() == 0) {
if (encoding == null || encoding.isEmpty()) {
throw new InvalidGroupLinkException("No reference was in the uri");
}
var bytes = Base64UrlSafe.decodePaddingAgnostic(encoding);
var groupInviteLink = GroupInviteLink.parseFrom(bytes);
var bytes = Base64.decode(encoding);
GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes);
switch (groupInviteLink.getContentsCase()) {
case V1CONTENTS -> {
var groupInviteLinkContentsV1 = groupInviteLink.getV1Contents();
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey()
.toByteArray());
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.getInviteLinkPassword()
.toByteArray());
if (groupInviteLink.v1Contents != null) {
var groupInviteLinkContentsV1 = groupInviteLink.v1Contents;
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray());
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray());
return new GroupInviteLinkUrl(groupMasterKey, password);
}
default -> throw new UnknownGroupLinkVersionException("Url contains no known group link content");
return new GroupInviteLinkUrl(groupMasterKey, password);
} else {
throw new UnknownGroupLinkVersionException("Url contains no known group link content");
}
} catch (InvalidInputException | IOException e) {
throw new InvalidGroupLinkException(e);
@ -92,13 +90,12 @@ public final class GroupInviteLinkUrl {
}
private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
var groupInviteLink = GroupInviteLink.newBuilder()
.setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder()
.setGroupMasterKey(ByteString.copyFrom(groupMasterKey.serialize()))
.setInviteLinkPassword(ByteString.copyFrom(password.serialize())))
.build();
var groupInviteLink = new GroupInviteLink.Builder().v1Contents(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey(
ByteString.of(groupMasterKey.serialize()))
.inviteLinkPassword(ByteString.of(password.serialize()))
.build()).build();
var encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.toByteArray());
var encoding = Base64.encodeUrlSafeWithoutPadding(groupInviteLink.encode());
return GROUP_URL_PREFIX + encoding;
}
@ -115,7 +112,7 @@ public final class GroupInviteLinkUrl {
return password;
}
public final static class InvalidGroupLinkException extends Exception {
public static final class InvalidGroupLinkException extends Exception {
public InvalidGroupLinkException(String message) {
super(message);
@ -126,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) {
super(message);

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
public enum GroupLinkState {
ENABLED,

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
public class GroupNotFoundException extends Exception {

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
public enum GroupPermission {
EVERY_MEMBER,

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
public class GroupSendingNotAllowedException extends Exception {

View file

@ -1,21 +1,10 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.whispersystems.libsignal.IdentityKey;
import java.util.Date;
public record Identity(
RecipientAddress recipient,
IdentityKey identityKey,
byte[] fingerprint,
String safetyNumber,
byte[] scannableSafetyNumber,
TrustLevel trustLevel,
Date dateAdded
) {
public byte[] getFingerprint() {
return identityKey.getPublicKey().serialize();
}
}
long dateAddedTimestamp
) {}

View file

@ -0,0 +1,28 @@
package org.asamk.signal.manager.api;
import org.signal.libsignal.protocol.util.Hex;
import java.util.Base64;
import java.util.Locale;
public sealed interface IdentityVerificationCode {
record Fingerprint(byte[] fingerprint) implements IdentityVerificationCode {}
record SafetyNumber(String safetyNumber) implements IdentityVerificationCode {}
record ScannableSafetyNumber(byte[] safetyNumber) implements IdentityVerificationCode {}
static IdentityVerificationCode parse(String code) throws Exception {
code = code.replaceAll(" ", "");
if (code.length() == 66) {
final var fingerprintBytes = Hex.fromStringCondensed(code.toLowerCase(Locale.ROOT));
return new Fingerprint(fingerprintBytes);
} else if (code.length() == 60) {
return new SafetyNumber(code);
} else {
final var scannableSafetyNumber = Base64.getDecoder().decode(code);
return new ScannableSafetyNumber(scannableSafetyNumber);
}
}
}

View file

@ -2,6 +2,10 @@ package org.asamk.signal.manager.api;
public class InvalidNumberException extends Exception {
public InvalidNumberException(String message) {
super(message);
}
InvalidNumberException(String message, Throwable e) {
super(message, e);
}

View file

@ -0,0 +1,12 @@
package org.asamk.signal.manager.api;
public class InvalidUsernameException extends Exception {
public InvalidUsernameException(final String message) {
super(message);
}
public InvalidUsernameException(final String message, final Throwable cause) {
super(message, cause);
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
public class LastGroupAdminException extends Exception {

View file

@ -6,14 +6,32 @@ import java.util.Optional;
public record Message(
String messageText,
List<String> attachments,
boolean viewOnce,
List<Mention> mentions,
Optional<Quote> quote,
Optional<Sticker> sticker
Optional<Sticker> sticker,
List<Preview> previews,
Optional<StoryReply> storyReply,
List<TextStyle> textStyles
) {
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
public record Quote(long timestamp, RecipientIdentifier.Single author, String message, List<Mention> mentions) {}
public record Quote(
long timestamp,
RecipientIdentifier.Single author,
String message,
List<Mention> mentions,
List<TextStyle> textStyles,
List<Attachment> attachments
) {
public record Attachment(String contentType, String filename, String preview) {}
}
public record Sticker(byte[] packId, int stickerId) {}
public record Preview(String url, String title, String description, Optional<String> image) {}
public record StoryReply(long timestamp, RecipientIdentifier.Single author) {}
}

View file

@ -1,18 +1,21 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.signal.libsignal.metadata.ProtocolException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
@ -29,8 +32,10 @@ import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptM
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@ -46,8 +51,10 @@ public record MessageEnvelope(
Optional<Receipt> receipt,
Optional<Typing> typing,
Optional<Data> data,
Optional<Edit> edit,
Optional<Sync> sync,
Optional<Call> call
Optional<Call> call,
Optional<Story> story
) {
public record Receipt(long when, Type type, List<Long> timestamps) {
@ -80,7 +87,7 @@ public record MessageEnvelope(
public static Typing from(final SignalServiceTypingMessage typingMessage) {
return new Typing(typingMessage.getTimestamp(),
typingMessage.isTypingStarted() ? Type.STARTED : Type.STOPPED,
Optional.ofNullable(typingMessage.getGroupId().transform(GroupId::unknownVersion).orNull()));
typingMessage.getGroupId().map(GroupId::unknownVersion));
}
public enum Type {
@ -92,12 +99,14 @@ public record MessageEnvelope(
public record Data(
long timestamp,
Optional<GroupContext> groupContext,
Optional<StoryContext> storyContext,
Optional<GroupCallUpdate> groupCallUpdate,
Optional<String> body,
int expiresInSeconds,
boolean isExpirationUpdate,
boolean isViewOnce,
boolean isEndSession,
boolean isProfileKeyUpdate,
boolean hasProfileKey,
Optional<Reaction> reaction,
Optional<Quote> quote,
@ -107,7 +116,8 @@ public record MessageEnvelope(
Optional<Sticker> sticker,
List<SharedContact> sharedContacts,
List<Mention> mentions,
List<Preview> previews
List<Preview> previews,
List<TextStyle> textStyles
) {
static Data from(
@ -117,43 +127,43 @@ public record MessageEnvelope(
final AttachmentFileProvider fileProvider
) {
return new Data(dataMessage.getTimestamp(),
Optional.ofNullable(dataMessage.getGroupContext().transform(GroupContext::from).orNull()),
Optional.ofNullable(dataMessage.getGroupCallUpdate().transform(GroupCallUpdate::from).orNull()),
Optional.ofNullable(dataMessage.getBody().orNull()),
dataMessage.getGroupContext().map(GroupContext::from),
dataMessage.getStoryContext()
.map((SignalServiceDataMessage.StoryContext storyContext) -> StoryContext.from(storyContext,
recipientResolver,
addressResolver)),
dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
dataMessage.getBody(),
dataMessage.getExpiresInSeconds(),
dataMessage.isExpirationUpdate(),
dataMessage.isViewOnce(),
dataMessage.isEndSession(),
dataMessage.isProfileKeyUpdate(),
dataMessage.getProfileKey().isPresent(),
Optional.ofNullable(dataMessage.getReaction()
.transform(r -> Reaction.from(r, recipientResolver, addressResolver))
.orNull()),
Optional.ofNullable(dataMessage.getQuote()
.transform(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider))
.orNull()),
Optional.ofNullable(dataMessage.getPayment()
.transform(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null)
.orNull()),
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
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.getAttachments()
.transform(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
.or(List.of()),
Optional.ofNullable(dataMessage.getRemoteDelete()
.transform(SignalServiceDataMessage.RemoteDelete::getTargetSentTimestamp)
.orNull()),
Optional.ofNullable(dataMessage.getSticker().transform(Sticker::from).orNull()),
.map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
.orElse(List.of()),
dataMessage.getRemoteDelete().map(SignalServiceDataMessage.RemoteDelete::getTargetSentTimestamp),
dataMessage.getSticker().map(Sticker::from),
dataMessage.getSharedContacts()
.transform(a -> a.stream()
.map(a -> a.stream()
.map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
.toList())
.or(List.of()),
.orElse(List.of()),
dataMessage.getMentions()
.transform(a -> a.stream()
.map(m -> Mention.from(m, recipientResolver, addressResolver))
.toList())
.or(List.of()),
.map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
.orElse(List.of()),
dataMessage.getPreviews()
.transform(a -> a.stream().map(preview -> Preview.from(preview, fileProvider)).toList())
.or(List.of()));
.map(a -> a.stream().map(preview -> Preview.from(preview, fileProvider)).toList())
.orElse(List.of()),
dataMessage.getBodyRanges()
.map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList())
.orElse(List.of()));
}
public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
@ -174,6 +184,18 @@ public record MessageEnvelope(
}
}
public record StoryContext(RecipientAddress author, long sentTimestamp) {
static StoryContext from(
SignalServiceDataMessage.StoryContext storyContext,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new StoryContext(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
storyContext.getAuthorServiceId())).toApiRecipientAddress(), storyContext.getSentTimestamp());
}
}
public record GroupCallUpdate(String eraId) {
static GroupCallUpdate from(SignalServiceDataMessage.GroupCallUpdate groupCallUpdate) {
@ -191,7 +213,8 @@ public record MessageEnvelope(
RecipientAddressResolver addressResolver
) {
return new Reaction(reaction.getTargetSentTimestamp(),
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(reaction.getTargetAuthor())),
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(reaction.getTargetAuthor()))
.toApiRecipientAddress(),
reaction.getEmoji(),
reaction.isRemove());
}
@ -202,7 +225,8 @@ public record MessageEnvelope(
RecipientAddress author,
Optional<String> text,
List<Mention> mentions,
List<Attachment> attachments
List<Attachment> attachments,
List<TextStyle> textStyles
) {
static Quote from(
@ -212,8 +236,9 @@ public record MessageEnvelope(
final AttachmentFileProvider fileProvider
) {
return new Quote(quote.getId(),
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor())),
Optional.ofNullable(quote.getText()),
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor()))
.toApiRecipientAddress(),
Optional.of(quote.getText()),
quote.getMentions() == null
? List.of()
: quote.getMentions()
@ -222,7 +247,14 @@ public record MessageEnvelope(
.toList(),
quote.getAttachments() == null
? List.of()
: quote.getAttachments().stream().map(a -> Attachment.from(a, fileProvider)).toList());
: quote.getAttachments().stream().map(a -> Attachment.from(a, fileProvider)).toList(),
quote.getBodyRanges() == null
? List.of()
: quote.getBodyRanges()
.stream()
.filter(r -> r.style != null)
.map(TextStyle::from)
.toList());
}
}
@ -241,9 +273,8 @@ public record MessageEnvelope(
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new Mention(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(mention.getAci())),
mention.getStart(),
mention.getLength());
return new Mention(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(mention.getServiceId()))
.toApiRecipientAddress(), mention.getStart(), mention.getLength());
}
}
@ -264,44 +295,51 @@ public record MessageEnvelope(
boolean isBorderless
) {
static Attachment from(SignalServiceAttachment attachment, AttachmentFileProvider fileProvider) {
if (attachment.isPointer()) {
final var a = attachment.asPointer();
return new Attachment(Optional.of(a.getRemoteId().toString()),
Optional.of(fileProvider.getFile(a.getRemoteId())),
Optional.ofNullable(a.getFileName().orNull()),
static Attachment from(SignalServiceAttachment signalAttachment, AttachmentFileProvider fileProvider) {
if (signalAttachment.isPointer()) {
final var a = signalAttachment.asPointer();
final var attachmentFile = fileProvider.getFile(a);
return new Attachment(Optional.of(attachmentFile.getName()),
Optional.of(attachmentFile),
a.getFileName(),
a.getContentType(),
a.getUploadTimestamp() == 0 ? Optional.empty() : Optional.of(a.getUploadTimestamp()),
Optional.ofNullable(a.getSize().transform(Integer::longValue).orNull()),
Optional.ofNullable(a.getPreview().orNull()),
a.getSize().map(Integer::longValue),
a.getPreview(),
Optional.empty(),
Optional.ofNullable(a.getCaption().orNull()),
a.getCaption().map(c -> c.isEmpty() ? null : c),
a.getWidth() == 0 ? Optional.empty() : Optional.of(a.getWidth()),
a.getHeight() == 0 ? Optional.empty() : Optional.of(a.getHeight()),
a.getVoiceNote(),
a.isGif(),
a.isBorderless());
} else {
final var a = attachment.asStream();
return new Attachment(Optional.empty(),
Optional.empty(),
Optional.ofNullable(a.getFileName().orNull()),
a.getContentType(),
a.getUploadTimestamp() == 0 ? Optional.empty() : Optional.of(a.getUploadTimestamp()),
Optional.of(a.getLength()),
Optional.ofNullable(a.getPreview().orNull()),
Optional.empty(),
Optional.ofNullable(a.getCaption().orNull()),
a.getWidth() == 0 ? Optional.empty() : Optional.of(a.getWidth()),
a.getHeight() == 0 ? Optional.empty() : Optional.of(a.getHeight()),
a.getVoiceNote(),
a.isGif(),
a.isBorderless());
Attachment attachment = null;
try (final var a = signalAttachment.asStream()) {
attachment = new Attachment(Optional.empty(),
Optional.empty(),
a.getFileName(),
a.getContentType(),
a.getUploadTimestamp() == 0 ? Optional.empty() : Optional.of(a.getUploadTimestamp()),
Optional.of(a.getLength()),
a.getPreview(),
Optional.empty(),
a.getCaption(),
a.getWidth() == 0 ? Optional.empty() : Optional.of(a.getWidth()),
a.getHeight() == 0 ? Optional.empty() : Optional.of(a.getHeight()),
a.getVoiceNote(),
a.isGif(),
a.isBorderless());
return attachment;
} catch (IOException e) {
return attachment;
}
}
}
static Attachment from(
SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider
SignalServiceDataMessage.Quote.QuotedAttachment a,
final AttachmentFileProvider fileProvider
) {
return new Attachment(Optional.empty(),
Optional.empty(),
@ -345,17 +383,15 @@ public record MessageEnvelope(
final AttachmentFileProvider fileProvider
) {
return new SharedContact(Name.from(sharedContact.getName()),
Optional.ofNullable(sharedContact.getAvatar()
.transform(avatar1 -> Avatar.from(avatar1, fileProvider))
.orNull()),
sharedContact.getPhone().transform(p -> p.stream().map(Phone::from).toList()).or(List.of()),
sharedContact.getEmail().transform(p -> p.stream().map(Email::from).toList()).or(List.of()),
sharedContact.getAddress().transform(p -> p.stream().map(Address::from).toList()).or(List.of()),
Optional.ofNullable(sharedContact.getOrganization().orNull()));
sharedContact.getAvatar().map(avatar1 -> Avatar.from(avatar1, fileProvider)),
sharedContact.getPhone().map(p -> p.stream().map(Phone::from).toList()).orElse(List.of()),
sharedContact.getEmail().map(p -> p.stream().map(Email::from).toList()).orElse(List.of()),
sharedContact.getAddress().map(p -> p.stream().map(Address::from).toList()).orElse(List.of()),
sharedContact.getOrganization());
}
public record Name(
Optional<String> display,
Optional<String> nickname,
Optional<String> given,
Optional<String> family,
Optional<String> prefix,
@ -364,12 +400,12 @@ public record MessageEnvelope(
) {
static Name from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Name name) {
return new Name(Optional.ofNullable(name.getDisplay().orNull()),
Optional.ofNullable(name.getGiven().orNull()),
Optional.ofNullable(name.getFamily().orNull()),
Optional.ofNullable(name.getPrefix().orNull()),
Optional.ofNullable(name.getSuffix().orNull()),
Optional.ofNullable(name.getMiddle().orNull()));
return new Name(name.getNickname(),
name.getGiven(),
name.getFamily(),
name.getPrefix(),
name.getSuffix(),
name.getMiddle());
}
}
@ -388,9 +424,7 @@ public record MessageEnvelope(
) {
static Phone from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Phone phone) {
return new Phone(phone.getValue(),
Type.from(phone.getType()),
Optional.ofNullable(phone.getLabel().orNull()));
return new Phone(phone.getValue(), Type.from(phone.getType()), phone.getLabel());
}
public enum Type {
@ -415,9 +449,7 @@ public record MessageEnvelope(
) {
static Email from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Email email) {
return new Email(email.getValue(),
Type.from(email.getType()),
Optional.ofNullable(email.getLabel().orNull()));
return new Email(email.getValue(), Type.from(email.getType()), email.getLabel());
}
public enum Type {
@ -450,15 +482,15 @@ public record MessageEnvelope(
) {
static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) {
return new Address(Address.Type.from(address.getType()),
Optional.ofNullable(address.getLabel().orNull()),
Optional.ofNullable(address.getLabel().orNull()),
Optional.ofNullable(address.getLabel().orNull()),
Optional.ofNullable(address.getLabel().orNull()),
Optional.ofNullable(address.getLabel().orNull()),
Optional.ofNullable(address.getLabel().orNull()),
Optional.ofNullable(address.getLabel().orNull()),
Optional.ofNullable(address.getLabel().orNull()));
return new Address(Type.from(address.getType()),
address.getLabel(),
address.getStreet(),
address.getPobox(),
address.getNeighborhood(),
address.getCity(),
address.getRegion(),
address.getPostcode(),
address.getCountry());
}
public enum Type {
@ -479,18 +511,28 @@ public record MessageEnvelope(
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
static Preview from(
SignalServiceDataMessage.Preview preview, final AttachmentFileProvider fileProvider
) {
static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) {
return new Preview(preview.getTitle(),
preview.getDescription(),
preview.getDate(),
preview.getUrl(),
Optional.ofNullable(preview.getImage()
.transform(as -> Attachment.from(as, fileProvider))
.orNull()));
preview.getImage().map(as -> Attachment.from(as, fileProvider)));
}
}
}
public record Edit(long targetSentTimestamp, Data dataMessage) {
public static Edit from(
final SignalServiceEditMessage editMessage,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider
) {
return new Edit(editMessage.getTargetSentTimestamp(),
Data.from(editMessage.getDataMessage(), recipientResolver, addressResolver, fileProvider));
}
}
public record Sync(
@ -510,30 +552,22 @@ public record MessageEnvelope(
RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider
) {
return new Sync(Optional.ofNullable(syncMessage.getSent()
.transform(s -> Sent.from(s, recipientResolver, addressResolver, fileProvider))
.orNull()),
Optional.ofNullable(syncMessage.getBlockedList()
.transform(b -> Blocked.from(b, recipientResolver, addressResolver))
.orNull()),
return new Sync(syncMessage.getSent()
.map(s -> Sent.from(s, recipientResolver, addressResolver, fileProvider)),
syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)),
syncMessage.getRead()
.transform(r -> r.stream()
.map(rm -> Read.from(rm, recipientResolver, addressResolver))
.toList())
.or(List.of()),
.map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
.orElse(List.of()),
syncMessage.getViewed()
.transform(r -> r.stream()
.map(r -> r.stream()
.map(rm -> Viewed.from(rm, recipientResolver, addressResolver))
.toList())
.or(List.of()),
Optional.ofNullable(syncMessage.getViewOnceOpen()
.transform(rm -> ViewOnceOpen.from(rm, recipientResolver, addressResolver))
.orNull()),
Optional.ofNullable(syncMessage.getContacts().transform(Contacts::from).orNull()),
Optional.ofNullable(syncMessage.getGroups().transform(Groups::from).orNull()),
Optional.ofNullable(syncMessage.getMessageRequestResponse()
.transform(m -> MessageRequestResponse.from(m, recipientResolver, addressResolver))
.orNull()));
.orElse(List.of()),
syncMessage.getViewOnceOpen().map(rm -> ViewOnceOpen.from(rm, recipientResolver, addressResolver)),
syncMessage.getContacts().map(Contacts::from),
syncMessage.getGroups().map(Groups::from),
syncMessage.getMessageRequestResponse()
.map(m -> MessageRequestResponse.from(m, recipientResolver, addressResolver)));
}
public record Sent(
@ -541,7 +575,9 @@ public record MessageEnvelope(
long expirationStartTimestamp,
Optional<RecipientAddress> destination,
Set<RecipientAddress> recipients,
Data message
Optional<Data> message,
Optional<Edit> editMessage,
Optional<Story> story
) {
static Sent from(
@ -552,15 +588,19 @@ public record MessageEnvelope(
) {
return new Sent(sentMessage.getTimestamp(),
sentMessage.getExpirationStartTimestamp(),
Optional.ofNullable(sentMessage.getDestination()
.transform(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
d)))
.orNull()),
sentMessage.getDestination()
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
.toApiRecipientAddress()),
sentMessage.getRecipients()
.stream()
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d)))
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
.toApiRecipientAddress())
.collect(Collectors.toSet()),
Data.from(sentMessage.getMessage(), recipientResolver, addressResolver, fileProvider));
sentMessage.getDataMessage()
.map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)),
sentMessage.getEditMessage()
.map(message -> Edit.from(message, recipientResolver, addressResolver, fileProvider)),
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
}
}
@ -571,10 +611,12 @@ public record MessageEnvelope(
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new Blocked(blockedListMessage.getAddresses()
.stream()
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d)))
.toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
return new Blocked(blockedListMessage.individuals.stream()
.map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(),
null,
d.getE164(),
null))
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
}
}
@ -585,8 +627,8 @@ public record MessageEnvelope(
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender())),
readMessage.getTimestamp());
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
.toApiRecipientAddress(), readMessage.getTimestamp());
}
}
@ -597,8 +639,8 @@ public record MessageEnvelope(
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new Viewed(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender())),
readMessage.getTimestamp());
return new Viewed(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender()))
.toApiRecipientAddress(), readMessage.getTimestamp());
}
}
@ -610,7 +652,7 @@ public record MessageEnvelope(
RecipientAddressResolver addressResolver
) {
return new ViewOnceOpen(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
readMessage.getSender())), readMessage.getTimestamp());
readMessage.getSender())).toApiRecipientAddress(), readMessage.getTimestamp());
}
}
@ -636,13 +678,10 @@ public record MessageEnvelope(
RecipientAddressResolver addressResolver
) {
return new MessageRequestResponse(Type.from(messageRequestResponse.getType()),
Optional.ofNullable(messageRequestResponse.getGroupId()
.transform(GroupId::unknownVersion)
.orNull()),
Optional.ofNullable(messageRequestResponse.getPerson()
.transform(p -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
p)))
.orNull()));
messageRequestResponse.getGroupId().map(GroupId::unknownVersion),
messageRequestResponse.getPerson()
.map(p -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(p))
.toApiRecipientAddress()));
}
public enum Type {
@ -651,7 +690,9 @@ public record MessageEnvelope(
DELETE,
BLOCK,
BLOCK_AND_DELETE,
UNBLOCK_AND_ACCEPT;
UNBLOCK_AND_ACCEPT,
SPAM,
BLOCK_AND_SPAM;
static Type from(MessageRequestResponseMessage.Type type) {
return switch (type) {
@ -661,6 +702,8 @@ public record MessageEnvelope(
case BLOCK -> BLOCK;
case BLOCK_AND_DELETE -> BLOCK_AND_DELETE;
case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT;
case SPAM -> SPAM;
case BLOCK_AND_SPAM -> BLOCK_AND_SPAM;
};
}
}
@ -676,30 +719,29 @@ public record MessageEnvelope(
Optional<Hangup> hangup,
Optional<Busy> busy,
List<IceUpdate> iceUpdate,
Optional<Opaque> opaque
Optional<Opaque> opaque,
boolean isUrgent
) {
public static Call from(final SignalServiceCallMessage callMessage) {
return new Call(Optional.ofNullable(callMessage.getDestinationDeviceId().orNull()),
Optional.ofNullable(callMessage.getGroupId().transform(GroupId::unknownVersion).orNull()),
Optional.ofNullable(callMessage.getTimestamp().orNull()),
Optional.ofNullable(callMessage.getOfferMessage().transform(Offer::from).orNull()),
Optional.ofNullable(callMessage.getAnswerMessage().transform(Answer::from).orNull()),
Optional.ofNullable(callMessage.getHangupMessage().transform(Hangup::from).orNull()),
Optional.ofNullable(callMessage.getBusyMessage().transform(Busy::from).orNull()),
return new Call(callMessage.getDestinationDeviceId(),
callMessage.getGroupId().map(GroupId::unknownVersion),
callMessage.getTimestamp(),
callMessage.getOfferMessage().map(Offer::from),
callMessage.getAnswerMessage().map(Answer::from),
callMessage.getHangupMessage().map(Hangup::from),
callMessage.getBusyMessage().map(Busy::from),
callMessage.getIceUpdateMessages()
.transform(m -> m.stream().map(IceUpdate::from).toList())
.or(List.of()),
Optional.ofNullable(callMessage.getOpaqueMessage().transform(Opaque::from).orNull()));
.map(m -> m.stream().map(IceUpdate::from).toList())
.orElse(List.of()),
callMessage.getOpaqueMessage().map(Opaque::from),
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) {
return new Offer(offerMessage.getId(),
offerMessage.getSdp(),
Type.from(offerMessage.getType()),
offerMessage.getOpaque());
return new Offer(offerMessage.getId(), Type.from(offerMessage.getType()), offerMessage.getOpaque());
}
public enum Type {
@ -715,10 +757,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) {
return new Answer(answerMessage.getId(), answerMessage.getSdp(), answerMessage.getOpaque());
return new Answer(answerMessage.getId(), answerMessage.getOpaque());
}
}
@ -729,13 +771,12 @@ public record MessageEnvelope(
}
}
public record Hangup(long id, Type type, int deviceId, boolean isLegacy) {
public record Hangup(long id, Type type, int deviceId) {
static Hangup from(HangupMessage hangupMessage) {
return new Hangup(hangupMessage.getId(),
Type.from(hangupMessage.getType()),
hangupMessage.getDeviceId(),
hangupMessage.isLegacy());
hangupMessage.getDeviceId());
}
public enum Type {
@ -757,10 +798,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) {
return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getSdp(), iceUpdateMessage.getOpaque());
return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getOpaque());
}
}
@ -784,48 +825,129 @@ public record MessageEnvelope(
}
}
public record Story(
boolean allowsReplies,
Optional<GroupId> groupId,
Optional<Data.Attachment> fileAttachment,
Optional<TextAttachment> textAttachment
) {
public static Story from(SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider) {
return new Story(storyMessage.getAllowsReplies().orElse(false),
storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
storyMessage.getTextAttachment().map(t -> TextAttachment.from(t, fileProvider)));
}
public record TextAttachment(
Optional<String> text,
Optional<Style> style,
Optional<Color> textForegroundColor,
Optional<Color> textBackgroundColor,
Optional<Data.Preview> preview,
Optional<Gradient> backgroundGradient,
Optional<Color> backgroundColor
) {
static TextAttachment from(
SignalServiceTextAttachment textAttachment,
final AttachmentFileProvider fileProvider
) {
return new TextAttachment(textAttachment.getText(),
textAttachment.getStyle().map(Style::from),
textAttachment.getTextForegroundColor().map(Color::new),
textAttachment.getTextBackgroundColor().map(Color::new),
textAttachment.getPreview().map(p -> Data.Preview.from(p, fileProvider)),
textAttachment.getBackgroundGradient().map(Gradient::from),
textAttachment.getBackgroundColor().map(Color::new));
}
public enum Style {
DEFAULT,
REGULAR,
BOLD,
SERIF,
SCRIPT,
CONDENSED;
static Style from(SignalServiceTextAttachment.Style style) {
return switch (style) {
case DEFAULT -> DEFAULT;
case REGULAR -> REGULAR;
case BOLD -> BOLD;
case SERIF -> SERIF;
case SCRIPT -> SCRIPT;
case CONDENSED -> CONDENSED;
};
}
}
public record Gradient(
List<Color> colors, List<Float> positions, Optional<Integer> angle
) {
static Gradient from(SignalServiceTextAttachment.Gradient gradient) {
return new Gradient(gradient.getColors().stream().map(Color::new).toList(),
gradient.getPositions(),
gradient.getAngle());
}
}
}
}
public static MessageEnvelope from(
SignalServiceEnvelope envelope,
SignalServiceContent content,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider
final AttachmentFileProvider fileProvider,
Exception exception
) {
final var source = !envelope.isUnidentifiedSender() && envelope.hasSourceUuid()
? recipientResolver.resolveRecipient(envelope.getSourceAddress())
final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
final var source = !envelope.isUnidentifiedSender() && serviceId != null
? recipientResolver.resolveRecipient(serviceId)
: envelope.isUnidentifiedSender() && content != null
? recipientResolver.resolveRecipient(content.getSender())
: null;
: exception instanceof ProtocolException e
? recipientResolver.resolveRecipient(e.getSender())
: null;
final var sourceDevice = envelope.hasSourceDevice()
? envelope.getSourceDevice()
: content != null ? content.getSenderDevice() : 0;
: content != null
? content.getSenderDevice()
: exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
Optional<Receipt> receipt;
Optional<Typing> typing;
Optional<Data> data;
Optional<Edit> edit;
Optional<Sync> sync;
Optional<Call> call;
Optional<Story> story;
if (content != null) {
receipt = Optional.ofNullable(content.getReceiptMessage().transform(Receipt::from).orNull());
typing = Optional.ofNullable(content.getTypingMessage().transform(Typing::from).orNull());
data = Optional.ofNullable(content.getDataMessage()
.transform(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider))
.orNull());
sync = Optional.ofNullable(content.getSyncMessage()
.transform(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider))
.orNull());
call = Optional.ofNullable(content.getCallMessage().transform(Call::from).orNull());
receipt = content.getReceiptMessage().map(Receipt::from);
typing = content.getTypingMessage().map(Typing::from);
data = content.getDataMessage()
.map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider));
edit = content.getEditMessage().map(s -> Edit.from(s, recipientResolver, addressResolver, fileProvider));
sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider));
call = content.getCallMessage().map(Call::from);
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
} else {
receipt = Optional.empty();
receipt = envelope.isReceipt() ? Optional.of(new Receipt(envelope.getServerReceivedTimestamp(),
Receipt.Type.DELIVERY,
List.of(envelope.getTimestamp()))) : Optional.empty();
typing = Optional.empty();
data = Optional.empty();
edit = Optional.empty();
sync = Optional.empty();
call = Optional.empty();
story = Optional.empty();
}
return new MessageEnvelope(source == null
? Optional.empty()
: Optional.of(addressResolver.resolveRecipientAddress(source)),
: Optional.of(addressResolver.resolveRecipientAddress(source).toApiRecipientAddress()),
sourceDevice,
envelope.getTimestamp(),
envelope.getServerReceivedTimestamp(),
@ -834,12 +956,14 @@ public record MessageEnvelope(
receipt,
typing,
data,
edit,
sync,
call);
call,
story);
}
public interface AttachmentFileProvider {
File getFile(SignalServiceAttachmentRemoteId attachmentRemoteId);
File getFile(SignalServiceAttachmentPointer pointer);
}
}

View file

@ -0,0 +1,12 @@
package org.asamk.signal.manager.api;
public class NonNormalizedPhoneNumberException extends Exception {
public NonNormalizedPhoneNumberException(final String message) {
super(message);
}
public NonNormalizedPhoneNumberException(final String message, final Throwable cause) {
super(message, cause);
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.groups;
package org.asamk.signal.manager.api;
public class NotAGroupMemberException extends Exception {

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.api;
public class NotPrimaryDeviceException extends Exception {
public NotPrimaryDeviceException() {
super("This function is not supported for linked devices.");
}
}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager;
package org.asamk.signal.manager.api;
public class NotRegisteredException extends Exception {

View file

@ -0,0 +1,12 @@
package org.asamk.signal.manager.api;
public class PendingAdminApprovalException extends Exception {
public PendingAdminApprovalException(final String message) {
super(message);
}
public PendingAdminApprovalException(final String message, final Throwable cause) {
super(message, cause);
}
}

View file

@ -3,5 +3,16 @@ package org.asamk.signal.manager.api;
public enum PhoneNumberSharingMode {
EVERYBODY,
CONTACTS,
NOBODY,
NOBODY;
public static PhoneNumberSharingMode valueOfOrNull(String value) {
if (value == null) {
return null;
}
try {
return valueOf(value);
} catch (IllegalArgumentException ignored) {
return null;
}
}
}

View file

@ -0,0 +1,3 @@
package org.asamk.signal.manager.api;
public class PinLockMissingException extends Exception {}

View file

@ -1,4 +1,4 @@
package org.asamk.signal.manager.storage.recipients;
package org.asamk.signal.manager.api;
import org.whispersystems.signalservice.internal.util.Util;
@ -20,10 +20,14 @@ public class Profile {
private final String avatarUrlPath;
private final byte[] mobileCoinAddress;
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final Set<Capability> capabilities;
private final PhoneNumberSharingMode phoneNumberSharingMode;
public Profile(
final long lastUpdateTimestamp,
final String givenName,
@ -31,8 +35,10 @@ public class Profile {
final String about,
final String aboutEmoji,
final String avatarUrlPath,
final byte[] mobileCoinAddress,
final UnidentifiedAccessMode unidentifiedAccessMode,
final Set<Capability> capabilities
final Set<Capability> capabilities,
final PhoneNumberSharingMode phoneNumberSharingMode
) {
this.lastUpdateTimestamp = lastUpdateTimestamp;
this.givenName = givenName;
@ -40,8 +46,10 @@ public class Profile {
this.about = about;
this.aboutEmoji = aboutEmoji;
this.avatarUrlPath = avatarUrlPath;
this.mobileCoinAddress = mobileCoinAddress;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities;
this.phoneNumberSharingMode = phoneNumberSharingMode;
}
private Profile(final Builder builder) {
@ -51,8 +59,10 @@ public class Profile {
about = builder.about;
aboutEmoji = builder.aboutEmoji;
avatarUrlPath = builder.avatarUrlPath;
mobileCoinAddress = builder.mobileCoinAddress;
unidentifiedAccessMode = builder.unidentifiedAccessMode;
capabilities = builder.capabilities;
phoneNumberSharingMode = builder.phoneNumberSharingMode;
}
public static Builder newBuilder() {
@ -67,6 +77,7 @@ public class Profile {
builder.about = copy.getAbout();
builder.aboutEmoji = copy.getAboutEmoji();
builder.avatarUrlPath = copy.getAvatarUrlPath();
builder.mobileCoinAddress = copy.getMobileCoinAddress();
builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode();
builder.capabilities = copy.getCapabilities();
return builder;
@ -118,6 +129,10 @@ public class Profile {
return avatarUrlPath;
}
public byte[] getMobileCoinAddress() {
return mobileCoinAddress;
}
public UnidentifiedAccessMode getUnidentifiedAccessMode() {
return unidentifiedAccessMode;
}
@ -126,13 +141,17 @@ public class Profile {
return capabilities;
}
public PhoneNumberSharingMode getPhoneNumberSharingMode() {
return phoneNumberSharingMode;
}
public enum UnidentifiedAccessMode {
UNKNOWN,
DISABLED,
ENABLED,
UNRESTRICTED;
static UnidentifiedAccessMode valueOfOrUnknown(String value) {
public static UnidentifiedAccessMode valueOfOrUnknown(String value) {
try {
return valueOf(value);
} catch (IllegalArgumentException ignored) {
@ -142,13 +161,10 @@ public class Profile {
}
public enum Capability {
gv2,
storage,
gv1Migration,
senderKey,
announcementGroup;
storageServiceEncryptionV2Capability;
static Capability valueOfOrNull(String value) {
public static Capability valueOfOrNull(String value) {
try {
return valueOf(value);
} catch (IllegalArgumentException ignored) {
@ -191,8 +207,10 @@ public class Profile {
private String about;
private String aboutEmoji;
private String avatarUrlPath;
private byte[] mobileCoinAddress;
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
private Set<Capability> capabilities = Collections.emptySet();
private PhoneNumberSharingMode phoneNumberSharingMode;
private long lastUpdateTimestamp = 0;
private Builder() {
@ -233,6 +251,11 @@ public class Profile {
return this;
}
public Builder withPhoneNumberSharingMode(final PhoneNumberSharingMode val) {
phoneNumberSharingMode = val;
return this;
}
public Profile build() {
return new Profile(this);
}
@ -241,5 +264,10 @@ public class Profile {
lastUpdateTimestamp = val;
return this;
}
public Builder withMobileCoinAddress(final byte[] val) {
mobileCoinAddress = val;
return this;
}
}
}

Some files were not shown because too many files have changed in this diff Show more