diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8534f618..b091909f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
## [Unreleased]
+### Added
+
+- `send --attachment-dimensions` and `--attachment-blurhash` to set the placeholder shown before an attachment is downloaded
+
+## [0.14.8] - 2026-09-10
+
+### Added
+
+- Add terminateGroup command to terminate a group for everyone
+- Include isVoiceNote in receive JSON and JSON-RPC attachment payloads
+
+### Improved
+
+- Prevent more storage sync loops
+
+### Fixed
+
+- Fix graalvm issue with image dimension probing
+- Fix issue with sticker storage sync
+
## [0.14.7] - 2026-08-01
### Added
diff --git a/build.gradle.kts b/build.gradle.kts
index b0797538..e4fc5460 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -5,12 +5,12 @@ plugins {
application
eclipse
`check-lib-versions`
- id("org.graalvm.buildtools.native") version "1.1.9"
+ id("org.graalvm.buildtools.native") version "1.1.12"
}
allprojects {
group = "org.asamk"
- version = "0.14.8"
+ version = "0.14.9-SNAPSHOT"
}
java {
diff --git a/client/Cargo.lock b/client/Cargo.lock
index 954b400b..1c1ebce6 100644
--- a/client/Cargo.lock
+++ b/client/Cargo.lock
@@ -54,19 +54,19 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.102"
+version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "async-trait"
-version = "0.1.89"
+version = "0.1.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
@@ -83,21 +83,21 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
-version = "2.11.1"
+version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "bytes"
-version = "1.11.1"
+version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cc"
-version = "1.2.60"
+version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
+checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -117,9 +117,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
-version = "4.6.1"
+version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
dependencies = [
"clap_builder",
"clap_derive",
@@ -127,9 +127,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.6.0"
+version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
dependencies = [
"anstream",
"anstyle",
@@ -140,14 +140,14 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.6.1"
+version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
@@ -164,9 +164,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "combine"
-version = "4.6.7"
+version = "4.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
dependencies = [
"bytes",
"memchr",
@@ -190,13 +190,13 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "displaydoc"
-version = "0.2.5"
+version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
@@ -217,9 +217,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
-version = "0.1.9"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
[[package]]
name = "fnv"
@@ -238,53 +238,53 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-macro"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
name = "futures-sink"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
[[package]]
name = "futures-task"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-timer"
-version = "3.0.3"
+version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
+checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-macro",
@@ -307,9 +307,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.4.13"
+version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
+checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
dependencies = [
"atomic-waker",
"bytes",
@@ -326,9 +326,9 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.17.0"
+version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
@@ -338,9 +338,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
-version = "1.4.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"bytes",
"itoa",
@@ -348,9 +348,9 @@ dependencies = [
[[package]]
name = "http-body"
-version = "1.0.1"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
dependencies = [
"bytes",
"http",
@@ -358,9 +358,9 @@ dependencies = [
[[package]]
name = "http-body-util"
-version = "0.1.3"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [
"bytes",
"futures-core",
@@ -377,9 +377,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hyper"
-version = "1.9.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
dependencies = [
"atomic-waker",
"bytes",
@@ -434,9 +434,9 @@ dependencies = [
[[package]]
name = "icu_collections"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
@@ -448,9 +448,9 @@ dependencies = [
[[package]]
name = "icu_locale_core"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
@@ -461,9 +461,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -475,16 +475,17 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
+ "displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
@@ -495,15 +496,15 @@ dependencies = [
[[package]]
name = "icu_properties_data"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
-version = "2.2.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [
"displaydoc",
"icu_locale_core",
@@ -527,9 +528,9 @@ dependencies = [
[[package]]
name = "idna_adapter"
-version = "1.2.1"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
@@ -537,9 +538,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.14.0"
+version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
@@ -598,7 +599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -632,7 +633,7 @@ dependencies = [
"rustc-hash",
"serde",
"serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
"tokio",
"tokio-stream",
"tower",
@@ -656,7 +657,7 @@ dependencies = [
"rustls-platform-verifier",
"serde",
"serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
"tokio",
"tower",
"url",
@@ -672,7 +673,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -684,14 +685,14 @@ dependencies = [
"http",
"serde",
"serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
]
[[package]]
name = "libc"
-version = "0.2.185"
+version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
@@ -701,27 +702,27 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "log"
-version = "0.4.29"
+version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
-version = "2.8.0"
+version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "mio"
-version = "1.2.0"
+version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
dependencies = [
"libc",
"wasi",
@@ -754,22 +755,22 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project"
-version = "1.1.11"
+version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
-version = "1.1.11"
+version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -780,9 +781,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "potential_utf"
-version = "0.1.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
"zerovec",
]
@@ -798,18 +799,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.106"
+version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.45"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@@ -830,9 +831,9 @@ dependencies = [
[[package]]
name = "rustc-hash"
-version = "2.1.2"
+version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "rustix"
@@ -849,9 +850,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.38"
+version = "0.23.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
+checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"
dependencies = [
"log",
"once_cell",
@@ -864,9 +865,9 @@ dependencies = [
[[package]]
name = "rustls-native-certs"
-version = "0.8.3"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
@@ -876,9 +877,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.14.0"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
@@ -912,9 +913,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
-version = "0.103.13"
+version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
@@ -964,9 +965,9 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
@@ -974,29 +975,29 @@ dependencies = [
[[package]]
name = "serde_core"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.228"
+version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
name = "serde_json"
-version = "1.0.149"
+version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
@@ -1007,13 +1008,13 @@ dependencies = [
[[package]]
name = "shlex"
-version = "1.3.0"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signal-cli-client"
-version = "0.0.1"
+version = "0.14.8"
dependencies = [
"anyhow",
"bytes",
@@ -1022,7 +1023,7 @@ dependencies = [
"jsonrpsee",
"serde",
"serde_json",
- "thiserror 2.0.18",
+ "thiserror 2.0.20",
"tokio",
"tokio-util",
]
@@ -1035,15 +1036,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
-version = "1.15.1"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
[[package]]
name = "socket2"
-version = "0.6.3"
+version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.61.2",
@@ -1069,9 +1070,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
-version = "2.0.117"
+version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
@@ -1092,7 +1104,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -1116,11 +1128,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.18"
+version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
- "thiserror-impl 2.0.18",
+ "thiserror-impl 2.0.20",
]
[[package]]
@@ -1131,25 +1143,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
name = "thiserror-impl"
-version = "2.0.18"
+version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
name = "tinystr"
-version = "0.8.3"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
"zerovec",
@@ -1157,9 +1169,9 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.52.0"
+version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"bytes",
"libc",
@@ -1172,20 +1184,20 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.7.0"
+version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
name = "tokio-rustls"
-version = "0.26.4"
+version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
dependencies = [
"rustls",
"tokio",
@@ -1193,9 +1205,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
-version = "0.1.18"
+version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -1204,13 +1216,14 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.18"
+version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
+ "libc",
"pin-project-lite",
"tokio",
]
@@ -1226,9 +1239,9 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.25.11+spec-1.1.0"
+version = "0.25.13+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
dependencies = [
"indexmap",
"toml_datetime",
@@ -1238,9 +1251,9 @@ dependencies = [
[[package]]
name = "toml_parser"
-version = "1.1.2+spec-1.1.0"
+version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
@@ -1290,7 +1303,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
]
[[package]]
@@ -1375,14 +1388,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
dependencies = [
- "webpki-root-certs 1.0.7",
+ "webpki-root-certs 1.0.9",
]
[[package]]
name = "webpki-root-certs"
-version = "1.0.7"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
dependencies = [
"rustls-pki-types",
]
@@ -1561,24 +1574,24 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
-version = "1.0.1"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
dependencies = [
"memchr",
]
[[package]]
name = "writeable"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "yoke"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [
"stable_deref_trait",
"yoke-derive",
@@ -1593,15 +1606,15 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
"synstructure",
]
[[package]]
name = "zerofrom"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
@@ -1614,21 +1627,21 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 2.0.119",
"synstructure",
]
[[package]]
name = "zeroize"
-version = "1.8.2"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
-version = "0.2.4"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
@@ -1637,9 +1650,9 @@ dependencies = [
[[package]]
name = "zerovec"
-version = "0.11.6"
+version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
dependencies = [
"yoke",
"zerofrom",
@@ -1648,17 +1661,17 @@ dependencies = [
[[package]]
name = "zerovec-derive"
-version = "0.11.3"
+version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.5",
]
[[package]]
name = "zmij"
-version = "1.0.21"
+version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/client/Cargo.toml b/client/Cargo.toml
index f875d1f2..c026935c 100644
--- a/client/Cargo.toml
+++ b/client/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "signal-cli-client"
-version = "0.0.1"
+version = "0.14.8"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml
index 0dce3931..26cf0665 100644
--- a/data/org.asamk.SignalCli.metainfo.xml
+++ b/data/org.asamk.SignalCli.metainfo.xml
@@ -45,6 +45,9 @@
intense
+
+ https://github.com/AsamK/signal-cli/releases/tag/v0.14.8
+
https://github.com/AsamK/signal-cli/releases/tag/v0.14.7
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6a96c4ce..6c9af028 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,14 +1,14 @@
[versions]
-slf4j = "2.0.18"
-coroutines = "1.10.2"
-junit = "6.1.2"
-micronaut-json-schema = "2.1.0"
-micronaut-core = "5.1.10"
-signal-service = "2.15.3_unofficial_152"
+slf4j = "2.0.19"
+coroutines = "1.11.0"
+junit = "6.1.3"
+micronaut-json-schema = "2.2.0"
+micronaut-core = "5.2.0"
+signal-service = "2.15.3_unofficial_153"
[libraries]
-bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85"
-jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.1"
+bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85.2"
+jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.2"
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
zxing = "com.google.zxing:core:3.5.4"
@@ -18,11 +18,11 @@ micronaut-json-schema-generator = { module = "io.micronaut.jsonschema:micronaut-
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-core" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
-logback = "ch.qos.logback:logback-classic:1.6.1"
+logback = "ch.qos.logback:logback-classic:1.6.3"
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
-sqlite = "org.xerial:sqlite-jdbc:3.53.2.1"
+sqlite = "org.xerial:sqlite-jdbc:3.53.4.0"
hikari = "com.zaxxer:HikariCP:7.1.0"
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index b1b8ef56..eddabd2e 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a9db1155..ad7845be 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/BadRequestException.java b/lib/src/main/java/org/asamk/signal/manager/api/BadRequestException.java
new file mode 100644
index 00000000..bffd9f11
--- /dev/null
+++ b/lib/src/main/java/org/asamk/signal/manager/api/BadRequestException.java
@@ -0,0 +1,19 @@
+package org.asamk.signal.manager.api;
+
+import org.signal.libsignal.net.BadRequestError;
+
+import java.io.IOException;
+
+public class BadRequestException extends IOException {
+
+ private final BadRequestError error;
+
+ public BadRequestException(final BadRequestError error) {
+ super(error.toString());
+ this.error = error;
+ }
+
+ public BadRequestError getError() {
+ return error;
+ }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Message.java b/lib/src/main/java/org/asamk/signal/manager/api/Message.java
index b9d60b00..ccb7cba8 100644
--- a/lib/src/main/java/org/asamk/signal/manager/api/Message.java
+++ b/lib/src/main/java/org/asamk/signal/manager/api/Message.java
@@ -6,6 +6,8 @@ import java.util.Optional;
public record Message(
String messageText,
List attachments,
+ List attachmentDimensions,
+ List attachmentBlurHashes,
boolean viewOnce,
boolean voiceNote,
List mentions,
@@ -17,6 +19,8 @@ public record Message(
boolean urgent
) {
+ public record AttachmentDimensions(int width, int height) {}
+
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
public record Quote(
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java
index a868fb36..d7da6217 100644
--- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java
+++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java
@@ -267,19 +267,19 @@ public record MessageEnvelope(
quote.getMentions() == null
? List.of()
: quote.getMentions()
- .stream()
- .map(m -> Mention.from(m, recipientResolver, addressResolver))
- .toList(),
+ .stream()
+ .map(m -> Mention.from(m, recipientResolver, addressResolver))
+ .toList(),
quote.getAttachments() == null
? List.of()
: 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());
+ .stream()
+ .filter(r -> r.style != null)
+ .map(TextStyle::from)
+ .toList());
}
}
@@ -598,7 +598,7 @@ public record MessageEnvelope(
Boolean.TRUE.equals(pinnedMessage.getForever())
? -1
: pinnedMessage.getPinDurationInSeconds() == null
- ? 0
+ ? 0
: pinnedMessage.getPinDurationInSeconds());
}
}
@@ -1034,14 +1034,14 @@ public record MessageEnvelope(
final var source = !envelope.isUnidentifiedSender() && serviceId != null
? recipientResolver.resolveRecipient(serviceId)
: envelope.isUnidentifiedSender() && content != null
- ? recipientResolver.resolveRecipient(content.getSender())
+ ? recipientResolver.resolveRecipient(content.getSender())
: exception instanceof ProtocolException e
- ? recipientResolver.resolveRecipient(e.getSender())
+ ? recipientResolver.resolveRecipient(e.getSender())
: null;
final var sourceDevice = envelope.hasSourceDevice()
? envelope.getSourceDevice()
: content != null
- ? content.getSenderDevice()
+ ? content.getSenderDevice()
: exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
Optional receipt;
diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java
index 0adbc7b4..13f2ece8 100644
--- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java
+++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java
@@ -30,8 +30,8 @@ class LiveConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
.decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
- private static final String SVR2_MRENCLAVE = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4";
- private static final String SVR2_MRENCLAVE_LEGACY = "1240acbd4aa26974184844c8a46b1022d3957ac8a76c1fd8f5b1a15141ee0708";
+ private static final String SVR2_MRENCLAVE = "fdbbacdc0c043d0d53fe1440f62728de0386f45ab0a275bd8f99e03a02af355e";
+ private static final String SVR2_MRENCLAVE_LEGACY = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4";
private static final String URL = "https://chat.signal.org";
private static final String CDN_URL = "https://cdn.signal.org";
@@ -49,10 +49,10 @@ class LiveConfig {
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==");
private static final byte[] genericServerPublicParams = Base64.getDecoder()
- .decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN");
+ .decode("AeCO67P9mIv1yUHkdeZ9JF789GDbox61GvTqq3S4kYc1ADUWxWHQygU390tv1oRWt9WjkdZlU7mKkifF59ftjE+2ZlMmxns6I+ySiLpR8FEmfu+TGpVp3zYTjNV93obJJTyBCCsSHVETCyQRbKdCyb5TMa6LGrvcZaX0Q/VAavhuNA/m4kSiRMgSnYrUjGhVekdDnF+7xioo4wvFnxjIDh7uJQrYOWD6MloNGX7St5gbysTuQQ7i/HI38b9V8x8mKazuDSXxB//BKGZx/XHkK8cHX+QK1MPxYUVM1/CBI5oW");
private static final byte[] backupServerPublicParams = Base64.getDecoder()
- .decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
+ .decode("AZwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1Y4pyiScu0/S10n647ipo+iq97JZQv+UOlwH8ThyNlGT5DfxXCwTqivxHuXvZpuezPgHk5Gxl5aC6xuNxOnwmFlmu4CeSgdhW8+Pp0vAJOQ1MsU2D0+/kzI+tU94nB3tybY/Ao1AcGW2q41uKQbnOJUWwmQaFT6s+xTISgzsg7CPox6oORGX8rnyk/9lic3DbGsUHctIVpMAl/ogJBb4aYC");
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
diff --git a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java
index 40ce0569..e6b1673b 100644
--- a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java
+++ b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java
@@ -30,8 +30,8 @@ class StagingConfig {
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
.decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
- private static final String SVR2_MRENCLAVE = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302";
- private static final String SVR2_MRENCLAVE_LEGACY = "97f151f6ed078edbbfd72fa9cae694dcc08353f1f5e8d9ccd79a971b10ffc535";
+ private static final String SVR2_MRENCLAVE = "0ff2d7d4efbe7cfc24ac069a16fba898928dbe6c40d500c8b6da55733c727d6e";
+ private static final String SVR2_MRENCLAVE_LEGACY = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302";
private static final String URL = "https://chat.staging.signal.org";
private static final String CDN_URL = "https://cdn-staging.signal.org";
@@ -49,10 +49,10 @@ class StagingConfig {
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==");
private static final byte[] genericServerPublicParams = Base64.getDecoder()
- .decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N");
+ .decode("AYhaw+NbxtNLo/RlGFEsHd904hW38LpPJ59jYJlNmT4wwtyOq4xzCs/MyXsfRbIsAYhQjDnpE0rhFtWkMcn/kV740SISwFfpPHunrtZ9h0YWz5QNNbI5I3DRGUjhKXgMU7J7s7qOr0fdms+g0e+L9FMSjJLobDkOngp/m0B5TsxTyqLscJ5VyU69Cj8txImTfHMCKrYphYfRHO78RwPoz2g2tGUAzEbKHm12OgDna2qutkE5TvYqwZczvgZyLVHdHXpvdyOlEdv4afVyWkI7u/S0XYDonIJoHlxqJoTSepZR");
private static final byte[] backupServerPublicParams = Base64.getDecoder()
- .decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
+ .decode("AXYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszS6h3nZ6vRdM/IYGK+cLynw3ucWo7idf3zjOG3b6JnGT/z7XYCr6HuOGkWH4DQWCH98hxVZMGOgmT8DCQoqebQb3oK1yrwEglRWmtI01KhRg9RGUKoQiwuej1JZEY8uaG4Uz9n1cVODJ1iuByhNqGHo+KfI4iWhjtx2AnhYqHViQ3CMd4ASGBJtic9UTFVk/4vegVIy0wfYsAmViftzK6t4");
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
index 98a4d8f0..5f208ede 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
@@ -602,6 +602,7 @@ public class AccountHelper {
account.setMultiDevice(devices.size() > 1);
}
+ @SuppressWarnings("unchecked")
public void migrateRegistrationPin() throws IOException {
var masterKey = account.getOrCreatePinMasterKey();
@@ -609,6 +610,7 @@ public class AccountHelper {
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
}
+ @SuppressWarnings("unchecked")
public void setRegistrationPin(String pin) throws IOException {
var masterKey = account.getOrCreatePinMasterKey();
@@ -619,6 +621,7 @@ public class AccountHelper {
updateAccountAttributes();
}
+ @SuppressWarnings("unchecked")
public void removeRegistrationPin() throws IOException {
// Remove KBS Pin
context.getPinHelper().removeRegistrationLockPin();
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java
index 1fefcb24..85ea14a4 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java
@@ -1,6 +1,7 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.AttachmentInvalidException;
+import org.asamk.signal.manager.api.Message.AttachmentDimensions;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.AttachmentStore;
@@ -52,9 +53,11 @@ public class AttachmentHelper {
public List uploadAttachments(
final List attachments,
+ final List dimensions,
+ final List blurHashes,
boolean voiceNote
) throws AttachmentInvalidException, IOException {
- final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
+ final var attachmentStreams = createAttachmentStreams(attachments, dimensions, blurHashes, voiceNote);
try {
// Upload attachments here, so we only upload once even for multiple recipients
@@ -71,26 +74,31 @@ public class AttachmentHelper {
}
public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException {
- return uploadAttachments(attachments, false);
+ return uploadAttachments(attachments, List.of(), List.of(), false);
}
private List createAttachmentStreams(
List attachments,
+ List dimensions,
+ List blurHashes,
boolean voiceNote
) throws AttachmentInvalidException, IOException {
if (attachments == null) {
return null;
}
final var signalServiceAttachments = new ArrayList(attachments.size());
- for (var attachment : attachments) {
- final var attachmentStream = getAttachmentStream(attachment, voiceNote);
- signalServiceAttachments.add(attachmentStream);
+ for (var i = 0; i < attachments.size(); i++) {
+ final var size = i < dimensions.size() ? dimensions.get(i) : null;
+ final var blurHash = i < blurHashes.size() && !blurHashes.get(i).isEmpty() ? blurHashes.get(i) : null;
+ signalServiceAttachments.add(getAttachmentStream(attachments.get(i), size, blurHash, voiceNote));
}
return signalServiceAttachments;
}
private SignalServiceAttachmentStream getAttachmentStream(
final String attachment,
+ final AttachmentDimensions dimensions,
+ final String blurHash,
final boolean voiceNote
) throws AttachmentInvalidException {
try {
@@ -116,6 +124,8 @@ public class AttachmentHelper {
return AttachmentUtils.createAttachmentStream(streamDetails,
streamDetailsAndFileName.second(),
voiceNote,
+ dimensions,
+ blurHash,
uploadSpec);
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
@@ -130,7 +140,7 @@ public class AttachmentHelper {
}
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
- final var attachmentStream = getAttachmentStream(attachment, false);
+ final var attachmentStream = getAttachmentStream(attachment, null, null, false);
return uploadAttachment(attachmentStream);
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java
index 3f8f2600..97b83e67 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java
@@ -55,7 +55,7 @@ public class ContactHelper {
final var version = contact == null
? 1
: contact.messageExpirationTimeVersion() == Integer.MAX_VALUE
- ? Integer.MAX_VALUE
+ ? Integer.MAX_VALUE
: contact.messageExpirationTimeVersion() + 1;
account.getContactStore()
.storeContact(recipientId,
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
index 83e949b5..6bbe6971 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
@@ -758,8 +758,14 @@ class GroupV2Helper {
var authCredentialResponse = groupApiCredentials.get(todaySeconds);
final var aci = getSelfAci();
final var pni = getSelfPni();
+ final var authCredentialSalt = context.getAccount().getAuthCredentialSalt();
return dependencies.getGroupsV2Api()
- .getGroupsV2AuthorizationString(aci, pni, todaySeconds, groupSecretParams, authCredentialResponse);
+ .getGroupsV2AuthorizationString(aci,
+ pni,
+ authCredentialSalt,
+ todaySeconds,
+ groupSecretParams,
+ authCredentialResponse);
}
private ACI getSelfAci() {
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
index 308102a0..6abe6677 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
@@ -103,8 +103,16 @@ public final class ProfileHelper {
return getRecipientProfiles(recipientIds, false);
}
- public void refreshRecipientProfile(RecipientId recipientId) {
- getRecipientProfile(recipientId, true);
+ public boolean refreshRecipientProfile(RecipientId recipientId) {
+ try {
+ blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE, false));
+ return true;
+ } catch (IOException e) {
+ logger.warn("Failed to retrieve profile for {}, ignoring: {}",
+ context.getRecipientHelper().resolveSignalServiceAddress(recipientId).getIdentifier(),
+ e.getMessage());
+ return false;
+ }
}
public void refreshRecipientProfiles(Collection recipientIds) {
@@ -193,11 +201,9 @@ public final class ProfileHelper {
if (uploadProfile) {
final var streamDetails = avatar != null && avatar.isPresent()
? Utils.createStreamDetails(avatar.get())
- .first()
- : forceUploadAvatar && avatar == null
- ? context.getAvatarStore()
- .retrieveProfileAvatar(account.getSelfRecipientAddress())
- : null;
+ .first()
+ : forceUploadAvatar && avatar == null ? context.getAvatarStore()
+ .retrieveProfileAvatar(account.getSelfRecipientAddress()) : null;
try (streamDetails) {
final var avatarUploadParams = streamDetails != null
? AvatarUploadParams.forAvatar(streamDetails)
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
index 86e73c85..dd057c23 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
@@ -637,11 +637,11 @@ public class SendHelper {
Set senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null
? Set.of()
: targetRecipientIds.stream()
- .filter(s -> this.isSenderKeyCapable(s,
- addressesMap.get(s),
- unidentifiedAccessesMap.get(s),
- groupSendEndorsements))
- .collect(Collectors.toSet());
+ .filter(s -> this.isSenderKeyCapable(s,
+ addressesMap.get(s),
+ unidentifiedAccessesMap.get(s),
+ groupSendEndorsements))
+ .collect(Collectors.toSet());
if (senderKeyTargets.size() < 2) {
logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
senderKeyTargets = Set.of();
@@ -705,11 +705,11 @@ public class SendHelper {
final var expirationMs = Instant.ofEpochMilli(groupSendEndorsementsExpirationMs);
final var groupSendTokens = groupSendEndorsements != null && groupSecretParams != null
? legacyTargets.stream()
- .map(groupSendEndorsements::get)
- .map(endorsement -> Optional.ofNullable(endorsement)
- .map(e -> e.toFullToken(groupSecretParams, expirationMs))
- .orElse(null))
- .toList()
+ .map(groupSendEndorsements::get)
+ .map(endorsement -> Optional.ofNullable(endorsement)
+ .map(e -> e.toFullToken(groupSecretParams, expirationMs))
+ .orElse(null))
+ .toList()
: null;
final var sealedSenderAccesses = SealedSenderAccess.forFanOutGroupSend(groupSendTokens,
senderCertificate,
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
index b47c97c7..8438cdba 100644
--- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
+++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
@@ -46,6 +46,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.stream.Collectors;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
@@ -88,6 +89,7 @@ public class StorageHelper {
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
localManifestVersion);
final var fetchedRemoteManifest = result instanceof ManifestIfDifferentVersionResult.DifferentVersion;
+ final Set identityConflictsPendingRepair = new HashSet<>();
var needsForcePush = false;
final var remoteManifest = switch (result) {
@@ -116,13 +118,16 @@ public class StorageHelper {
if (remoteManifest.version > localManifestVersion) {
logger.trace("Remote version was newer, reading records.");
- needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest);
+ needsForcePush = readDataFromStorage(storageKey,
+ localManifest,
+ remoteManifest,
+ identityConflictsPendingRepair);
} else if (remoteManifest.version < localManifest.version) {
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
}
logger.trace("Done reading data from remote storage");
- readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest);
+ readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest, identityConflictsPendingRepair);
}
logger.trace("Adding missing storageIds to local data");
@@ -153,7 +158,8 @@ public class StorageHelper {
needsMultiDeviceSync = writeToStorage(storageKey,
remoteManifest,
needsForcePush,
- fetchedRemoteManifest);
+ fetchedRemoteManifest,
+ identityConflictsPendingRepair);
} catch (RetryLaterException e) {
// TODO retry later
return;
@@ -198,7 +204,8 @@ public class StorageHelper {
private boolean readDataFromStorage(
final StorageKey storageKey,
final SignalStorageManifest localManifest,
- final SignalStorageManifest remoteManifest
+ final SignalStorageManifest remoteManifest,
+ final Set identityConflictsPendingRepair
) throws IOException {
var needsForcePush = false;
try (final var connection = account.getAccountDatabase().getConnection()) {
@@ -225,7 +232,9 @@ public class StorageHelper {
remoteOnlyRecords.size());
}
- final var listListPair = processKnownRecords(connection, remoteOnlyRecords);
+ final var listListPair = processKnownRecords(connection,
+ remoteOnlyRecords,
+ identityConflictsPendingRepair);
final var unknownInserts = listListPair.first();
final var updatedStorageIds = listListPair.second();
final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds());
@@ -268,7 +277,8 @@ public class StorageHelper {
private void readRecordsWithPreviouslyUnknownTypes(
final StorageKey storageKey,
- final SignalStorageManifest remoteManifest
+ final SignalStorageManifest remoteManifest,
+ final Set identityConflictsPendingRepair
) throws IOException {
try (final var connection = account.getAccountDatabase().getConnection()) {
connection.setAutoCommit(false);
@@ -282,9 +292,8 @@ public class StorageHelper {
logger.debug("Found {} of the known-unknowns remotely.", remote.size());
- processKnownRecords(connection, remote);
- account.getUnknownStorageIdStore()
- .deleteUnknownStorageIds(connection, remote.stream().map(SignalStorageRecord::getId).toList());
+ processKnownRecords(connection, remote, identityConflictsPendingRepair);
+ account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, knownUnknownIds);
}
connection.commit();
} catch (SQLException e) {
@@ -296,7 +305,8 @@ public class StorageHelper {
final StorageKey storageKey,
final SignalStorageManifest remoteManifest,
final boolean needsForcePush,
- final boolean fetchedRemoteManifest
+ final boolean fetchedRemoteManifest,
+ final Set identityConflictsPendingRepair
) throws IOException, RetryLaterException {
final WriteOperationResult remoteWriteOperation;
try (final var connection = account.getAccountDatabase().getConnection()) {
@@ -306,10 +316,8 @@ public class StorageHelper {
var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
logger.debug("ID Difference :: {}", idDifference);
- final var unknownOnlyLocal = idDifference.localOnlyIds()
- .stream()
- .filter(id -> !KNOWN_TYPES.contains(id.getType()))
- .toList();
+ final var unknownStorageIds = account.getUnknownStorageIdStore().getUnknownStorageIds(connection);
+ final var unknownOnlyLocal = findUnknownOnlyLocalStorageIds(idDifference.localOnlyIds(), unknownStorageIds);
if (!unknownOnlyLocal.isEmpty()) {
logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size());
@@ -338,6 +346,15 @@ public class StorageHelper {
return false;
}
+ final var onlyIdentityConflictsPendingRepair = containsOnlyIdentityConflictsPendingRepair(remoteWriteOperation,
+ identityConflictsPendingRepair);
+ if (onlyIdentityConflictsPendingRepair) {
+ logger.warn(
+ "Deferring remote write until the profile fetch says whose identity key is correct. WriteOperationResult :: {}",
+ remoteWriteOperation);
+ return false;
+ }
+
final var loopCheck = storageSyncLoopDetector.onWriteAttempt(remoteWriteOperation,
fetchedRemoteManifest,
false);
@@ -386,6 +403,22 @@ public class StorageHelper {
}
}
+ static List findUnknownOnlyLocalStorageIds(
+ final List localOnlyStorageIds,
+ final Set unknownStorageIds
+ ) {
+ return localOnlyStorageIds.stream().filter(unknownStorageIds::contains).toList();
+ }
+
+ static boolean containsOnlyIdentityConflictsPendingRepair(
+ final WriteOperationResult writeOperation,
+ final Set identityConflictsPendingRepair
+ ) {
+ return writeOperation.deletes().isEmpty() && !writeOperation.inserts().isEmpty() && writeOperation.inserts()
+ .stream()
+ .allMatch(record -> identityConflictsPendingRepair.contains(record.getId()));
+ }
+
private void forcePushToStorage(
final StorageKey storageServiceKey
) throws IOException, RetryLaterException {
@@ -705,7 +738,8 @@ public class StorageHelper {
private Pair, List> processKnownRecords(
final Connection connection,
- List records
+ List records,
+ final Set identityConflictsPendingRepair
) throws SQLException {
final var unknownRecords = new ArrayList();
final var processedRecords = new ArrayList();
@@ -713,7 +747,10 @@ public class StorageHelper {
final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor());
final var groupV1RecordProcessor = new GroupV1RecordProcessor(account, connection);
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
- final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
+ final var contactRecordProcessor = new ContactRecordProcessor(account,
+ connection,
+ context.getJobExecutor(),
+ identityConflictsPendingRepair);
final var stickerPackRecordProcessor = new StickerPackRecordProcessor(account, connection);
final var contactRecords = records.stream()
diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java
index 34ba3586..1b0ed529 100644
--- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java
+++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java
@@ -300,7 +300,7 @@ public class ManagerImpl implements Manager {
final var profile = serviceId == null
? null
: context.getProfileHelper()
- .getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
+ .getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
return new UserStatus(number.isEmpty() ? null : number,
serviceId == null ? null : serviceId.getRawUuid(),
profile != null
@@ -327,7 +327,7 @@ public class ManagerImpl implements Manager {
final var profile = serviceId == null
? null
: context.getProfileHelper()
- .getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
+ .getRecipientProfile(account.getRecipientResolver().resolveRecipient(serviceId));
return new UsernameStatus(username,
serviceId == null ? null : serviceId.getRawUuid(),
profile != null
@@ -715,10 +715,7 @@ public class ManagerImpl implements Manager {
)) {
final var result = notifySelf
? context.getSendHelper()
- .sendMessage(messageBuilder,
- account.getSelfRecipientId(),
- editTargetTimestamp,
- urgent)
+ .sendMessage(messageBuilder, account.getSelfRecipientId(), editTargetTimestamp, urgent)
: context.getSendHelper().sendSelfMessage(messageBuilder, editTargetTimestamp);
results.put(recipient, List.of(toSendMessageResult(result)));
} else if (recipient instanceof RecipientIdentifier.Single single) {
@@ -973,7 +970,10 @@ public class ManagerImpl implements Manager {
}
if (!message.attachments().isEmpty()) {
final var uploadedAttachments = context.getAttachmentHelper()
- .uploadAttachments(message.attachments(), message.voiceNote());
+ .uploadAttachments(message.attachments(),
+ message.attachmentDimensions(),
+ message.attachmentBlurHashes(),
+ message.voiceNote());
if (!additionalAttachments.isEmpty()) {
additionalAttachments.addAll(uploadedAttachments);
messageBuilder.withAttachments(additionalAttachments);
@@ -1041,7 +1041,7 @@ public class ManagerImpl implements Manager {
final var previews = new ArrayList(message.previews().size());
for (final var p : message.previews()) {
final var image = p.image().isPresent() ? context.getAttachmentHelper()
- .uploadAttachment(p.image().get()) : null;
+ .uploadAttachment(p.image().get()) : null;
previews.add(new SignalServicePreview(p.url(),
p.title(),
p.description(),
diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileJob.java b/lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileJob.java
index 22796864..95c4acd7 100644
--- a/lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileJob.java
+++ b/lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileJob.java
@@ -9,9 +9,15 @@ public class DownloadProfileJob implements Job {
private static final Logger logger = LoggerFactory.getLogger(DownloadProfileJob.class);
private final RecipientAddress address;
+ private final boolean resolveIdentityKeyConflict;
public DownloadProfileJob(RecipientAddress address) {
+ this(address, false);
+ }
+
+ public DownloadProfileJob(RecipientAddress address, boolean resolveIdentityKeyConflict) {
this.address = address;
+ this.resolveIdentityKeyConflict = resolveIdentityKeyConflict;
}
@Override
@@ -19,6 +25,9 @@ public class DownloadProfileJob implements Job {
logger.trace("Refreshing profile for {}", address);
final var account = context.getAccount();
final var recipientId = account.getRecipientStore().resolveRecipient(address);
- context.getProfileHelper().refreshRecipientProfile(recipientId);
+ final var refreshed = context.getProfileHelper().refreshRecipientProfile(recipientId);
+ if (refreshed && resolveIdentityKeyConflict) {
+ context.getJobExecutor().enqueueJob(new SyncStorageJob());
+ }
}
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
index 3e07c5e6..77511d6e 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
@@ -959,7 +959,7 @@ public class SignalAccount implements Closeable {
profile.isUnrestrictedUnidentifiedAccess()
? Profile.UnidentifiedAccessMode.UNRESTRICTED
: profile.getUnidentifiedAccess() != null
- ? Profile.UnidentifiedAccessMode.ENABLED
+ ? Profile.UnidentifiedAccessMode.ENABLED
: Profile.UnidentifiedAccessMode.DISABLED,
capabilities,
null);
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
index aadb199d..ceaaef0e 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java
@@ -884,9 +884,9 @@ public class GroupStore {
final var members = membersString == null
? Set.of()
: Arrays.stream(membersString.split(","))
- .map(Integer::valueOf)
- .map(recipientIdCreator::create)
- .collect(Collectors.toSet());
+ .map(Integer::valueOf)
+ .map(recipientIdCreator::create)
+ .collect(Collectors.toSet());
final var expirationTime = resultSet.getInt("expiration_time");
final var blocked = resultSet.getBoolean("blocked");
final var blockedAt = resultSet.getLong("blocked_at");
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java
index 0edf292a..677e5e08 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java
@@ -115,7 +115,7 @@ public class LegacyJsonIdentityKeyStore {
var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get(
"trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED;
var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp")
- .asLong()) : new Date();
+ .asLong()) : new Date();
identities.add(new LegacyIdentityInfo(address, id, trustLevel, added));
} catch (InvalidKeyException e) {
logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage());
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
index 97410474..195b5735 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
@@ -1791,9 +1791,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
profileCapabilities == null
? Set.of()
: Arrays.stream(profileCapabilities.split(","))
- .map(Profile.Capability::valueOfOrNull)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet()),
+ .map(Profile.Capability::valueOfOrNull)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet()),
PhoneNumberSharingMode.valueOfOrNull(resultSet.getString("profile_phone_number_sharing")));
}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java
index 9b3377b5..5876354b 100644
--- a/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java
+++ b/lib/src/main/java/org/asamk/signal/manager/storage/sendLog/MessageSendLogStore.java
@@ -321,10 +321,10 @@ public class MessageSendLogStore implements AutoCloseable {
return content.dataMessage == null
? null
: content.dataMessage.group != null && content.dataMessage.group.id != null
- ? content.dataMessage.group.id.toByteArray()
+ ? content.dataMessage.group.id.toByteArray()
: content.dataMessage.groupV2 != null && content.dataMessage.groupV2.masterKey != null
- ? GroupUtils.getGroupIdV2(new GroupMasterKey(content.dataMessage.groupV2.masterKey.toByteArray()))
- .serialize()
+ ? GroupUtils.getGroupIdV2(new GroupMasterKey(content.dataMessage.groupV2.masterKey.toByteArray()))
+ .serialize()
: null;
} catch (InvalidInputException e) {
logger.warn("Failed to parse groupId id from content");
diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java
index 3829f8d2..8d62cddb 100644
--- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java
+++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java
@@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.regex.Pattern;
import okio.ByteString;
@@ -47,11 +48,18 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor identityConflictsPendingRepair;
- public ContactRecordProcessor(SignalAccount account, Connection connection, final JobExecutor jobExecutor) {
+ public ContactRecordProcessor(
+ SignalAccount account,
+ Connection connection,
+ final JobExecutor jobExecutor,
+ final Set identityConflictsPendingRepair
+ ) {
this.account = account;
this.connection = connection;
this.jobExecutor = jobExecutor;
+ this.identityConflictsPendingRepair = identityConflictsPendingRepair;
this.selfAci = account.getAci();
this.selfPni = account.getPni();
this.selfNumber = account.getNumber();
@@ -80,6 +88,29 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor 0 && aci != null && pni == null && e164.isEmpty();
}
+ static boolean shouldUseRemoteIdentityKey(
+ final boolean isPrimaryDevice,
+ final boolean statesDiffer,
+ final int remoteIdentityKeySize,
+ final int localIdentityKeySize,
+ final long localUnregisteredAtTimestamp,
+ final boolean unrepairableIdentityKeyConflict
+ ) {
+ return remoteIdentityKeySize > 0 && (
+ statesDiffer || localIdentityKeySize == 0 || localUnregisteredAtTimestamp > 0 || (
+ unrepairableIdentityKeyConflict && !isPrimaryDevice
+ )
+ );
+ }
+
+ @Override
+ protected String describeRecord(final SignalContactRecord record) {
+ final var proto = record.getProto();
+ final var aci = ACI.parseOrNull(proto.aci, proto.aciBinary);
+ final var pni = PNI.parseOrNull(proto.pni, proto.pniBinary);
+ return "[" + firstNonNull(aci, pni) + "]";
+ }
+
/**
* Error cases:
* - You can't have a contact record without an ACI or PNI.
@@ -154,32 +185,29 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor 0
+ && localIdentityKeySize > 0
+ && !remote.identityKey.equals(local.identityKey);
+ final var conflictAci = firstNonNull(localAci, remoteAci);
+ final var unrepairableIdentityKeyConflict = identityKeysExistAndConflict && conflictAci == null;
- if (remoteIdentityKeySize > 0 && (!account.isPrimaryDevice() || statesDiffer || localIdentityKeySize == 0)) {
+ if (shouldUseRemoteIdentityKey(account.isPrimaryDevice(),
+ statesDiffer,
+ remoteIdentityKeySize,
+ localIdentityKeySize,
+ local.unregisteredAtTimestamp,
+ unrepairableIdentityKeyConflict)) {
identityState = remote.identityState;
identityKey = remote.identityKey;
} else {
identityState = local.identityState;
- // Only use local's identity key if:
- // 1. Contact has ACI or PNI
- // 2. Remote also has an identity key (if remote size=0, respect that decision)
- if (hasLocalIdentity && localIdentityKeySize > 0 && remoteIdentityKeySize > 0) {
+ if (hasLocalIdentity && localIdentityKeySize > 0) {
identityKey = local.identityKey;
} else {
identityKey = ByteString.EMPTY;
}
}
- if (localAci != null
- && local.identityKey.size() > 0
- && remote.identityKey.size() > 0
- && !local.identityKey.equals(remote.identityKey)) {
- logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
- localAci);
- final var address = getRecipientAddress(local);
- jobExecutor.enqueueJob(new DownloadProfileJob(address));
- }
-
PNI pni;
String e164;
if (account.isPrimaryDevice()) {
@@ -213,6 +241,17 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor 0 && remoteIdentityKeySize == 0) {
+ logger.debug("Remote identity key is missing for {}. Keeping ours.", firstNonNull(localAci, localPni));
+ }
+
final var remoteProfileKey = remote.profileKey.size() == 0
|| KeyUtils.profileKeyOrNull(remote.profileKey.toByteArray()) == null
? ByteString.EMPTY
@@ -237,7 +276,9 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor> implemen
return Collections.unmodifiableSet(updatedStorageIds);
}
+ /**
+ * Optional extra identifying detail about a record, included in every log line for it.
+ */
+ protected String describeRecord(E record) {
+ return "";
+ }
+
private void debug(StorageId i, E record, String message) {
- logger.debug("[{}][{}] {}", i, record.getClass().getSimpleName(), message);
+ logger.debug("[{}][{}]{} {}", i, record.getClass().getSimpleName(), describeRecord(record), message);
}
/**
diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncLoopDetector.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncLoopDetector.java
index 6f3a1e3e..b9b86fcd 100644
--- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncLoopDetector.java
+++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncLoopDetector.java
@@ -20,6 +20,8 @@ public final class StorageSyncLoopDetector {
private final LeakyBucket rateBucket = new LeakyBucket(100,
Duration.ofMinutes(10).toMillis(),
new InMemoryBucketState());
+ private boolean lastAttemptChargedContent;
+ private boolean lastAttemptChargedRate;
public StorageSyncLoopDetector(final BooleanSupplier isMultiDevice) {
this.isMultiDevice = isMultiDevice;
@@ -39,6 +41,8 @@ public final class StorageSyncLoopDetector {
final boolean isRetry,
final long now
) {
+ lastAttemptChargedContent = false;
+ lastAttemptChargedRate = false;
if (!isMultiDevice.getAsBoolean() || isRetry) {
return Decision.Allowed.INSTANCE;
}
@@ -56,9 +60,11 @@ public final class StorageSyncLoopDetector {
if (chargeContent) {
contentBucket.use(now);
+ lastAttemptChargedContent = true;
}
if (fetchedRemoteManifest) {
rateBucket.use(now);
+ lastAttemptChargedRate = true;
}
if (fingerprint != null) {
remember(fingerprint);
@@ -72,8 +78,14 @@ public final class StorageSyncLoopDetector {
}
synchronized void onWriteFailed(final long now) {
- contentBucket.refund(now);
- rateBucket.refund(now);
+ if (lastAttemptChargedContent) {
+ contentBucket.refund(now);
+ lastAttemptChargedContent = false;
+ }
+ if (lastAttemptChargedRate) {
+ rateBucket.refund(now);
+ lastAttemptChargedRate = false;
+ }
}
public synchronized void onConverged() {
diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncModels.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncModels.java
index e721d540..d1a77361 100644
--- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncModels.java
+++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncModels.java
@@ -100,7 +100,8 @@ public final class StorageSyncModels {
final var builder = SignalContactRecord.Companion.newBuilder(recipient.getStorageRecord())
.e164(address.number().orElse(""))
.username(address.username().orElse(""))
- .pniSignatureVerified(recipient.isPniSignatureVerified())
+ .pniSignatureVerified(address.pni().map(PNI::isValid).orElse(false)
+ && recipient.isPniSignatureVerified())
.profileKey(recipient.getProfileKey() == null
? ByteString.EMPTY
: ByteString.of(recipient.getProfileKey().serialize()));
diff --git a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java
index 74693ac3..8d2329b8 100644
--- a/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java
+++ b/lib/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java
@@ -1,5 +1,6 @@
package org.asamk.signal.manager.util;
+import org.asamk.signal.manager.api.Message.AttachmentDimensions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
@@ -27,16 +28,21 @@ public class AttachmentUtils {
StreamDetails streamDetails,
Optional name,
boolean voiceNote,
+ AttachmentDimensions dimensions,
+ String blurHash,
ResumableUploadSpec resumableUploadSpec
) throws ResumeLocationInvalidException, IOException {
final var uploadTimestamp = System.currentTimeMillis();
- final var probedStream = probeImageDimensions(streamDetails);
+ final var probedStream = dimensions != null
+ ? new ProbedStream(streamDetails.getStream(), dimensions.width(), dimensions.height())
+ : probeImageDimensions(streamDetails);
return SignalServiceAttachmentStream.newStreamBuilder()
.withStream(probedStream.inputStream())
.withContentType(streamDetails.getContentType())
.withLength(streamDetails.getLength())
.withFileName(name.orElse(null))
.withVoiceNote(voiceNote)
+ .withBlurHash(blurHash)
.withWidth(probedStream.width())
.withHeight(probedStream.height())
.withUploadTimestamp(uploadTimestamp)
@@ -50,7 +56,7 @@ public class AttachmentUtils {
Optional name,
ResumableUploadSpec resumableUploadSpec
) throws ResumeLocationInvalidException, IOException {
- return createAttachmentStream(streamDetails, name, false, resumableUploadSpec);
+ return createAttachmentStream(streamDetails, name, false, null, null, resumableUploadSpec);
}
/**
diff --git a/lib/src/main/java/org/asamk/signal/manager/util/StickerUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/StickerUtils.java
index e1696da5..d017f0a5 100644
--- a/lib/src/main/java/org/asamk/signal/manager/util/StickerUtils.java
+++ b/lib/src/main/java/org/asamk/signal/manager/util/StickerUtils.java
@@ -78,10 +78,8 @@ public class StickerUtils {
throw new StickerPackInvalidException("Could not find find " + pack.cover().file());
}
- var contentType = pack.cover().contentType() != null && !pack.cover().contentType().isEmpty()
- ? pack.cover()
- .contentType()
- : getContentType(rootPath, zip, pack.cover().file());
+ var contentType = pack.cover().contentType() != null && !pack.cover().contentType().isEmpty() ? pack.cover()
+ .contentType() : getContentType(rootPath, zip, pack.cover().file());
cover = new SignalServiceStickerManifestUpload.StickerInfo(data.first(),
data.second(),
Optional.ofNullable(pack.cover().emoji()).orElse(""),
diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java
index 27ba298b..674b0ab1 100644
--- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java
+++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java
@@ -1,5 +1,6 @@
package org.asamk.signal.manager.util;
+import org.asamk.signal.manager.api.BadRequestException;
import org.asamk.signal.manager.api.Pair;
import org.signal.core.models.ServiceId;
import org.signal.libsignal.net.BadRequestError;
@@ -194,8 +195,8 @@ public class Utils {
}
} else if (result instanceof RequestResult.RetryableNetworkError e) {
throw e.getNetworkError();
- } else if (result instanceof RequestResult.NonSuccess) {
- throw new AssertionError();
+ } else if (result instanceof RequestResult.NonSuccess> e) {
+ throw new BadRequestException(e.getError());
}
throw new IllegalStateException("Unexpected value: " + result);
}
diff --git a/lib/src/test/java/org/asamk/signal/manager/helper/StorageHelperTest.java b/lib/src/test/java/org/asamk/signal/manager/helper/StorageHelperTest.java
new file mode 100644
index 00000000..01a79ef8
--- /dev/null
+++ b/lib/src/test/java/org/asamk/signal/manager/helper/StorageHelperTest.java
@@ -0,0 +1,53 @@
+package org.asamk.signal.manager.helper;
+
+import org.asamk.signal.manager.syncStorage.WriteOperationResult;
+import org.junit.jupiter.api.Test;
+import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
+import org.whispersystems.signalservice.api.storage.StorageId;
+import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StorageHelperTest {
+
+ @Test
+ void removesKnownTypeLocalOnlyIdsStoredAsUnknown() {
+ final var staleStickerPackId = StorageId.forStickerPack(new byte[]{1});
+ final var localContactId = StorageId.forContact(new byte[]{2});
+
+ final var result = StorageHelper.findUnknownOnlyLocalStorageIds(List.of(staleStickerPackId, localContactId),
+ Set.of(staleStickerPackId));
+
+ assertEquals(List.of(staleStickerPackId), result);
+ }
+
+ @Test
+ void defersWritesContainingOnlyIdentityConflictsPendingRepair() {
+ final var pendingId = StorageId.forContact(new byte[]{1});
+ final var otherId = StorageId.forContact(new byte[]{2});
+ final var pendingRecord = record(pendingId);
+ final var otherRecord = record(otherId);
+
+ assertTrue(StorageHelper.containsOnlyIdentityConflictsPendingRepair(write(List.of(pendingRecord)),
+ Set.of(pendingId)));
+ assertFalse(StorageHelper.containsOnlyIdentityConflictsPendingRepair(write(List.of(pendingRecord, otherRecord)),
+ Set.of(pendingId)));
+ assertFalse(StorageHelper.containsOnlyIdentityConflictsPendingRepair(write(List.of()), Set.of(pendingId)));
+ assertFalse(StorageHelper.containsOnlyIdentityConflictsPendingRepair(new WriteOperationResult(null,
+ List.of(pendingRecord),
+ List.of(new byte[]{3})), Set.of(pendingId)));
+ }
+
+ private static SignalStorageRecord record(final StorageId id) {
+ return new SignalStorageRecord(id, new StorageRecord.Builder().build());
+ }
+
+ private static WriteOperationResult write(final List inserts) {
+ return new WriteOperationResult(null, inserts, List.of());
+ }
+}
diff --git a/lib/src/test/java/org/asamk/signal/manager/syncStorage/StorageRecordProcessorTest.java b/lib/src/test/java/org/asamk/signal/manager/syncStorage/StorageRecordProcessorTest.java
index 2a0c6b75..1ce68a07 100644
--- a/lib/src/test/java/org/asamk/signal/manager/syncStorage/StorageRecordProcessorTest.java
+++ b/lib/src/test/java/org/asamk/signal/manager/syncStorage/StorageRecordProcessorTest.java
@@ -23,6 +23,13 @@ class StorageRecordProcessorTest {
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, null, "+12025550123"));
}
+ @Test
+ void linkedDeviceUsesRemoteIdentityKeyForUnrepairableConflict() {
+ assertTrue(ContactRecordProcessor.shouldUseRemoteIdentityKey(false, false, 33, 33, 0, true));
+ assertFalse(ContactRecordProcessor.shouldUseRemoteIdentityKey(true, false, 33, 33, 0, true));
+ assertFalse(ContactRecordProcessor.shouldUseRemoteIdentityKey(false, false, 33, 33, 0, false));
+ }
+
@Test
void keepsOlderLocalStickerDeletion() {
assertTrue(StickerPackRecordProcessor.shouldKeepLocalDeletion(200, 100));
diff --git a/lib/src/test/java/org/asamk/signal/manager/syncStorage/StorageSyncLoopDetectorTest.java b/lib/src/test/java/org/asamk/signal/manager/syncStorage/StorageSyncLoopDetectorTest.java
index f9d4f98c..4e436ce7 100644
--- a/lib/src/test/java/org/asamk/signal/manager/syncStorage/StorageSyncLoopDetectorTest.java
+++ b/lib/src/test/java/org/asamk/signal/manager/syncStorage/StorageSyncLoopDetectorTest.java
@@ -65,6 +65,29 @@ class StorageSyncLoopDetectorTest {
}
}
+ @Test
+ void failedWriteRefundsOnlyBucketsChargedByLatestAttempt() {
+ final var contentDetector = new StorageSyncLoopDetector(() -> true);
+ final var repeatedWrite = writeWithInsert(1);
+ for (var index = 0; index < 4; index++) {
+ assertAllowed(contentDetector.onWriteAttempt(repeatedWrite, true, false, NOW));
+ }
+ final var deleteOnlyWrite = new WriteOperationResult(null, List.of(), List.of(new byte[]{1}));
+ assertAllowed(contentDetector.onWriteAttempt(deleteOnlyWrite, true, false, NOW));
+ contentDetector.onWriteFailed(NOW);
+ assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.REPEATED_PAYLOAD, 3),
+ contentDetector.onWriteAttempt(repeatedWrite, true, false, NOW));
+
+ final var rateDetector = new StorageSyncLoopDetector(() -> true);
+ for (var index = 0; index < 100; index++) {
+ assertAllowed(rateDetector.onWriteAttempt(deleteOnlyWrite, true, false, NOW));
+ }
+ assertAllowed(rateDetector.onWriteAttempt(deleteOnlyWrite, false, false, NOW));
+ rateDetector.onWriteFailed(NOW);
+ assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.WRITE_RATE, 100),
+ rateDetector.onWriteAttempt(deleteOnlyWrite, true, false, NOW));
+ }
+
@Test
void rateBucketLimitsDeleteOnlyWrites() {
final var detector = new StorageSyncLoopDetector(() -> true);
diff --git a/lib/src/test/java/org/asamk/signal/manager/util/AttachmentUtilsTest.java b/lib/src/test/java/org/asamk/signal/manager/util/AttachmentUtilsTest.java
index dfc393cb..123f04a6 100644
--- a/lib/src/test/java/org/asamk/signal/manager/util/AttachmentUtilsTest.java
+++ b/lib/src/test/java/org/asamk/signal/manager/util/AttachmentUtilsTest.java
@@ -1,5 +1,6 @@
package org.asamk.signal.manager.util;
+import org.asamk.signal.manager.api.Message.AttachmentDimensions;
import org.junit.jupiter.api.Test;
import org.whispersystems.signalservice.api.util.StreamDetails;
@@ -43,6 +44,34 @@ class AttachmentUtilsTest {
assertArrayEquals(bytes, attachment.getInputStream().readAllBytes());
}
+ @Test
+ public void createAttachmentStream_setsSuppliedDimensionsAndBlurHash() throws Exception {
+ final var bytes = "opaque video bytes".getBytes();
+ final var blurHash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
+ final var details = new StreamDetails(new ByteArrayInputStream(bytes), "video/mp4", bytes.length);
+ final var attachment = AttachmentUtils.createAttachmentStream(details,
+ Optional.of("clip.mp4"), false, new AttachmentDimensions(1080, 1920), blurHash, null);
+
+ assertEquals(1080, attachment.getWidth());
+ assertEquals(1920, attachment.getHeight());
+ assertEquals(Optional.of(blurHash), attachment.getBlurHash());
+ assertArrayEquals(bytes, attachment.getInputStream().readAllBytes());
+ }
+
+ @Test
+ public void createAttachmentStream_skipsProbingWhenDimensionsSupplied() throws Exception {
+ final var imageBytes = pngBytes(37, 21);
+ final var stream = new ByteArrayInputStream(imageBytes);
+ final var details = new StreamDetails(stream, "image/png", imageBytes.length);
+ final var attachment = AttachmentUtils.createAttachmentStream(details,
+ Optional.of("meme.png"), false, new AttachmentDimensions(100, 200), null, null);
+
+ assertEquals(imageBytes.length, stream.available());
+ assertEquals(100, attachment.getWidth());
+ assertEquals(200, attachment.getHeight());
+ assertArrayEquals(imageBytes, attachment.getInputStream().readAllBytes());
+ }
+
private static byte[] pngBytes(final int width, final int height) throws Exception {
final var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
final var out = new ByteArrayOutputStream();
diff --git a/libsignal-version b/libsignal-version
index 626c498f..5bd38158 100644
--- a/libsignal-version
+++ b/libsignal-version
@@ -1 +1 @@
-0.99.1
+0.102.1
diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc
index 6fcdfee1..9e4d922e 100644
--- a/man/signal-cli.1.adoc
+++ b/man/signal-cli.1.adoc
@@ -247,7 +247,8 @@ Remove the registration lock pin.
Link to an existing device, instead of registering a new number.
This shows a "sgnl://linkdevice?uuid=..." URI.
If you want to connect to another signal-cli instance, you can just use this URI.
-If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app.
+If you want to link to an Android/iOS device, scan the QR code that signal-cli prints in the terminal with the Signal app.
+If signal-cli is not running in a terminal, only the URI is printed and you can create a QR code from it yourself (e.g. with qrencode).
*-n* NAME, *--name* NAME::
Optionally specify a name to describe this new device.
@@ -344,6 +345,15 @@ Data URI encoded attachments must follow the RFC 2397.
Additionally a file name can be added:
e.g.: `data:;filename=;base64,`
+*--attachment-dimensions* [WIDTHxHEIGHT [WIDTHxHEIGHT ...]]::
+Specify the displayed dimensions (after rotation) of the attachment at the same position in `--attachment`.
+Use an empty string to skip one; image dimensions are otherwise detected automatically.
+
+*--attachment-blurhash* [BLURHASH [BLURHASH ...]]::
+Specify a BlurHash (https://blurha.sh) for the attachment at the same position in `--attachment`, which clients show while it's downloading.
+Use an empty string to skip one.
+e.g.: `--attachment a.jpg clip.mp4 --attachment-dimensions '' 1080x1920 --attachment-blurhash '' 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'`
+
*--view-once*::
Send the message as a view once message.
A conformant client will only allow the receiver to view the message once.
diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java
index 8b818897..0be50fd8 100644
--- a/src/main/java/org/asamk/signal/BaseConfig.java
+++ b/src/main/java/org/asamk/signal/BaseConfig.java
@@ -8,7 +8,7 @@ public class BaseConfig {
public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion();
static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT"))
- .orElse("Signal-Android/8.21.1");
+ .orElse("Signal-Android/8.27.0");
static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null
? "signal-cli"
: PROJECT_NAME + "/" + PROJECT_VERSION;
diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java
index c6ef5077..dc6be9a4 100644
--- a/src/main/java/org/asamk/signal/commands/SendCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendCommand.java
@@ -36,6 +36,7 @@ import static org.asamk.signal.util.SendMessageResultUtils.outputResult;
public class SendCommand implements JsonRpcLocalCommand {
private static final Logger logger = LoggerFactory.getLogger(SendCommand.class);
+ private static final String BLURHASH_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
@Override
public String getName() {
@@ -112,6 +113,13 @@ public class SendCommand implements JsonRpcLocalCommand {
subparser.addArgument("--voice-note")
.action(Arguments.storeTrue())
.help("Mark audio attachments as voice notes. Voice notes are displayed inline in Signal clients.");
+ subparser.addArgument("--attachment-dimensions")
+ .nargs("*")
+ .help("Specify displayed WIDTHxHEIGHT for each attachment in order. Use an empty string to skip one.");
+ subparser.addArgument("--attachment-blurhash")
+ .nargs("*")
+ .help("Specify a BlurHash (https://blurha.sh) for each attachment in order, which clients show "
+ + "while it's downloading. Use an empty string to skip one.");
}
@Override
@@ -175,6 +183,13 @@ public class SendCommand implements JsonRpcLocalCommand {
final var viewOnce = Boolean.TRUE.equals(ns.getBoolean("view-once"));
final var voiceNote = Boolean.TRUE.equals(ns.getBoolean("voice-note"));
+ final var dimensionStrings = ns.getList("attachment-dimensions");
+ final var attachmentDimensions = dimensionStrings == null
+ ? List.of()
+ : parseAttachmentDimensions(dimensionStrings);
+ final var blurHashes = ns.getList("attachment-blurhash");
+ final var attachmentBlurHashes = blurHashes == null ? List.of() : parseAttachmentBlurHashes(blurHashes);
+
final var selfNumber = m.getSelfNumber();
final var mentionStrings = ns.getList("mention");
@@ -249,6 +264,8 @@ public class SendCommand implements JsonRpcLocalCommand {
try {
final var message = new Message(messageText,
attachments,
+ attachmentDimensions,
+ attachmentBlurHashes,
viewOnce,
voiceNote,
mentions,
@@ -280,6 +297,46 @@ public class SendCommand implements JsonRpcLocalCommand {
}
}
+ private List parseAttachmentDimensions(
+ final List dimensionStrings
+ ) throws UserErrorException {
+ final var dimensionPattern = Pattern.compile("([1-9]\\d*)x([1-9]\\d*)");
+ final var dimensions = new ArrayList();
+ for (final var dimension : dimensionStrings) {
+ if (dimension.isEmpty()) {
+ dimensions.add(null);
+ continue;
+ }
+ final var matcher = dimensionPattern.matcher(dimension);
+ if (!matcher.matches()) {
+ throw new UserErrorException("Invalid attachment dimensions syntax ("
+ + dimension
+ + ") expected 'WIDTHxHEIGHT'");
+ }
+ dimensions.add(new Message.AttachmentDimensions(Integer.parseInt(matcher.group(1)),
+ Integer.parseInt(matcher.group(2))));
+ }
+ return dimensions;
+ }
+
+ private List parseAttachmentBlurHashes(final List blurHashes) throws UserErrorException {
+ for (final var blurHash : blurHashes) {
+ if (!blurHash.isEmpty() && !isValidBlurHash(blurHash)) {
+ throw new UserErrorException("Invalid attachment BlurHash (" + blurHash + ")");
+ }
+ }
+ return blurHashes;
+ }
+
+ // Same check the BlurHash decoders make: the first digit fixes the length.
+ private static boolean isValidBlurHash(final String blurHash) {
+ if (blurHash.length() < 6) {
+ return false;
+ }
+ final var sizeFlag = BLURHASH_DIGITS.indexOf(blurHash.charAt(0));
+ return blurHash.length() == 4 + 2 * (sizeFlag % 9 + 1) * (sizeFlag / 9 + 1);
+ }
+
private List parseMentions(
final String selfNumber,
final List mentionStrings
diff --git a/src/main/java/org/asamk/signal/commands/SendPollVoteCommand.java b/src/main/java/org/asamk/signal/commands/SendPollVoteCommand.java
index 4b36da2e..87ecde55 100644
--- a/src/main/java/org/asamk/signal/commands/SendPollVoteCommand.java
+++ b/src/main/java/org/asamk/signal/commands/SendPollVoteCommand.java
@@ -41,7 +41,9 @@ public class SendPollVoteCommand implements JsonRpcLocalCommand {
.help("If self is part of recipients/groups send a normal message, not a sync message.")
.action(Arguments.storeTrue());
- subparser.addArgument("--poll-author").help("Specify the number of the author of the poll message.");
+ subparser.addArgument("--poll-author")
+ .help("Specify the number of the author of the poll message.")
+ .required(true);
subparser.addArgument("--poll-timestamp")
.type(long.class)
.help("Specify the timestamp of the original poll message.")
diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
index 1d53fe7d..30d7e4a0 100644
--- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
+++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
@@ -242,6 +242,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try {
final var message = new Message(messageText,
attachments,
+ List.of(),
+ List.of(),
false,
false,
List.of(),
@@ -409,6 +411,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try {
final var message = new Message(messageText,
attachments,
+ List.of(),
+ List.of(),
false,
false,
List.of(),
@@ -456,6 +460,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
try {
final var message = new Message(messageText,
attachments,
+ List.of(),
+ List.of(),
false,
false,
List.of(),
diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json
index 3925fcff..60ff4c40 100644
--- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json
+++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json
@@ -3239,6 +3239,9 @@
"allDeclaredMethods": true,
"allDeclaredConstructors": true
},
+ {
+ "type": "org.asamk.signal.json.JsonGroupCallUpdate"
+ },
{
"type": "org.asamk.signal.json.JsonGroupInfo",
"allDeclaredFields": true,
@@ -6190,6 +6193,22 @@
}
]
},
+ {
+ "type": "org.signal.libsignal.internal.LinkedDeviceInternal_ReturnConverter",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "fromNative",
+ "parameterTypes": [
+ "java.lang.Object",
+ "java.lang.Object",
+ "java.lang.Object",
+ "java.lang.Object",
+ "java.lang.Object"
+ ]
+ }
+ ]
+ },
{
"type": "org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner",
"jniAccessible": true,
@@ -6234,6 +6253,18 @@
}
]
},
+ {
+ "type": "org.signal.libsignal.net.ChallengeOption",
+ "jniAccessible": true,
+ "fields": [
+ {
+ "name": "CAPTCHA"
+ },
+ {
+ "name": "PUSH_CHALLENGE"
+ }
+ ]
+ },
{
"type": "org.signal.libsignal.net.ChatConnection$ListenerBridge",
"jniAccessible": true,
@@ -6357,6 +6388,46 @@
}
]
},
+ {
+ "type": "org.signal.libsignal.net.DeviceIdNotFoundException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ }
+ ]
+ },
+ {
+ "type": "org.signal.libsignal.net.MismatchedDeviceException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String",
+ "org.signal.libsignal.net.MismatchedDeviceException$Entry[]"
+ ]
+ }
+ ]
+ },
+ {
+ "type": "org.signal.libsignal.net.MismatchedDeviceException$Entry",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "byte[]",
+ "int[]",
+ "int[]",
+ "int[]"
+ ]
+ }
+ ]
+ },
{
"type": "org.signal.libsignal.net.NetworkException",
"jniAccessible": true,
@@ -6369,6 +6440,33 @@
}
]
},
+ {
+ "type": "org.signal.libsignal.net.RateLimitChallengeException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String",
+ "java.lang.String",
+ "org.signal.libsignal.net.ChallengeOption[]",
+ "long"
+ ]
+ }
+ ]
+ },
+ {
+ "type": "org.signal.libsignal.net.RequestUnauthorizedException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ }
+ ]
+ },
{
"type": "org.signal.libsignal.net.RetryLaterException",
"jniAccessible": true,
@@ -6381,6 +6479,42 @@
}
]
},
+ {
+ "type": "org.signal.libsignal.net.ServerSideErrorException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ }
+ ]
+ },
+ {
+ "type": "org.signal.libsignal.net.ServiceIdNotFoundException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ }
+ ]
+ },
+ {
+ "type": "org.signal.libsignal.net.TimeoutException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ }
+ ]
+ },
{
"type": "org.signal.libsignal.net.TransportFailureException",
"jniAccessible": true,
@@ -6393,6 +6527,18 @@
}
]
},
+ {
+ "type": "org.signal.libsignal.net.UnexpectedResponseException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ }
+ ]
+ },
{
"type": "org.signal.libsignal.net.UploadForm",
"jniAccessible": true,
@@ -6408,6 +6554,18 @@
}
]
},
+ {
+ "type": "org.signal.libsignal.net.UploadTooLargeException",
+ "jniAccessible": true,
+ "methods": [
+ {
+ "name": "",
+ "parameterTypes": [
+ "java.lang.String"
+ ]
+ }
+ ]
+ },
{
"type": "org.signal.libsignal.net.internal.BridgeChatListener",
"jniAccessible": true
@@ -6492,6 +6650,13 @@
"parameterTypes": [
"java.lang.String"
]
+ },
+ {
+ "name": "",
+ "parameterTypes": [
+ "org.signal.libsignal.protocol.SignalProtocolAddress",
+ "java.lang.String"
+ ]
}
]
},
@@ -9779,6 +9944,9 @@
}
]
},
+ {
+ "type": "org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UnreadBadgeType"
+ },
{
"type": "org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink",
"allDeclaredFields": true
@@ -10968,6 +11136,12 @@
{
"glob": "net/sourceforge/argparse4j/internal/ArgumentParserImpl_*.properties"
},
+ {
+ "glob": "net/sourceforge/argparse4j/internal/ArgumentParserImpl_de.properties"
+ },
+ {
+ "glob": "net/sourceforge/argparse4j/internal/ArgumentParserImpl_de_DE.properties"
+ },
{
"glob": "net/sourceforge/argparse4j/internal/ArgumentParserImpl_en.properties"
},
diff --git a/src/test/java/org/asamk/signal/commands/SendCommandParsingTest.java b/src/test/java/org/asamk/signal/commands/SendCommandParsingTest.java
new file mode 100644
index 00000000..f56b00e2
--- /dev/null
+++ b/src/test/java/org/asamk/signal/commands/SendCommandParsingTest.java
@@ -0,0 +1,117 @@
+package org.asamk.signal.commands;
+
+import net.sourceforge.argparse4j.ArgumentParsers;
+import net.sourceforge.argparse4j.DefaultSettings;
+import net.sourceforge.argparse4j.inf.Namespace;
+
+import org.asamk.signal.commands.exceptions.UserErrorException;
+import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.api.Message;
+import org.asamk.signal.manager.api.SendMessageResults;
+import org.asamk.signal.output.JsonWriter;
+import org.asamk.signal.testutil.ManagerMock;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class SendCommandParsingTest {
+
+ private static final List ATTACHMENTS = List.of("photo.jpg", "portrait.mp4", "other.mp4");
+ private static final String BLUR_HASH = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
+
+ @Test
+ void cliPreservesEmptyAndMissingDimensionPositions() throws Exception {
+ final var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS)
+ .includeArgumentNamesAsKeysInResult(true).build();
+ new SendCommand().attachToSubparser(parser.addSubparsers().addParser("send"));
+ final var message = send(parser.parseArgs(new String[]{"send", "--note-to-self",
+ "-a", "photo.jpg", "portrait.mp4", "other.mp4", "--attachment-dimensions", "", "1080x1920"}));
+
+ assertEquals(ATTACHMENTS, message.attachments());
+ assertEquals(Arrays.asList(null, new Message.AttachmentDimensions(1080, 1920)),
+ message.attachmentDimensions());
+ }
+
+ @Test
+ void jsonRpcPassesDimensionsIntoTheSentMessage() throws Exception {
+ final var message = send(namespace(List.of("", "1920x1080")));
+ assertEquals(ATTACHMENTS, message.attachments());
+ assertEquals(Arrays.asList(null, new Message.AttachmentDimensions(1920, 1080)),
+ message.attachmentDimensions());
+ }
+
+ @Test
+ void cliPreservesBlurHashPositions() throws Exception {
+ final var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS)
+ .includeArgumentNamesAsKeysInResult(true).build();
+ new SendCommand().attachToSubparser(parser.addSubparsers().addParser("send"));
+ final var message = send(parser.parseArgs(new String[]{"send", "--note-to-self",
+ "-a", "photo.jpg", "portrait.mp4", "other.mp4", "--attachment-blurhash", "", BLUR_HASH}));
+
+ assertEquals(List.of("", BLUR_HASH), message.attachmentBlurHashes());
+ }
+
+ @Test
+ void jsonRpcPassesBlurHashesWithDimensions() throws Exception {
+ final var message = send(new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS,
+ "attachmentDimensions", List.of("", "1920x1080"), "attachmentBlurhash", List.of("", BLUR_HASH))));
+
+ assertEquals(Arrays.asList(null, new Message.AttachmentDimensions(1920, 1080)),
+ message.attachmentDimensions());
+ assertEquals(List.of("", BLUR_HASH), message.attachmentBlurHashes());
+ }
+
+ @Test
+ void omittedDimensionsKeepExistingBehavior() throws Exception {
+ final var message = send(new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS)));
+ assertEquals(List.of(), message.attachmentDimensions());
+ assertEquals(List.of(), message.attachmentBlurHashes());
+ }
+
+ @Test
+ void rejectsMalformedDimensionsBeforeSending() {
+ for (final var value : List.of("0x1080", "1920x0", "-1x2", "1.5x2", "1920")) {
+ assertThrows(UserErrorException.class, () -> send(namespace(List.of(value))), value);
+ }
+ }
+
+ @Test
+ void rejectsMalformedBlurHashesBeforeSending() {
+ // Wrong length for its size digit, an unknown size digit, too short.
+ for (final var value : List.of(BLUR_HASH + "0", "!" + BLUR_HASH.substring(1), "00000")) {
+ assertThrows(UserErrorException.class, () -> send(blurHashNamespace(List.of(value))), value);
+ }
+ }
+
+ private JsonRpcNamespace blurHashNamespace(List blurHashes) {
+ return new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS,
+ "attachmentBlurhash", blurHashes));
+ }
+
+ private JsonRpcNamespace namespace(List dimensions) {
+ return new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS,
+ "attachmentDimensions", dimensions));
+ }
+
+ private Message send(Namespace namespace) throws Exception {
+ final var captured = new AtomicReference();
+ final var delegate = ManagerMock.create("+15551234567");
+ final var manager = (Manager) Proxy.newProxyInstance(Manager.class.getClassLoader(),
+ new Class>[]{Manager.class}, (proxy, method, args) -> {
+ if (method.getName().equals("sendMessage")) {
+ captured.set((Message) args[0]);
+ return new SendMessageResults(1, Map.of());
+ }
+ return method.invoke(delegate, args);
+ });
+ new SendCommand().handleCommand(namespace, manager, (JsonWriter) ignored -> {});
+ return captured.get();
+ }
+}