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