Merge branch 'AsamK:master' into android-sqlite-classifier-native

This commit is contained in:
dvrdnz 2026-09-20 11:27:55 +02:00 committed by GitHub
commit a2ff271c5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 979 additions and 302 deletions

View File

@ -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

View File

@ -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 {

363
client/Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -45,6 +45,9 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="0.14.8" date="2026-09-10">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.8</url>
</release>
<release version="0.14.7" date="2026-08-01">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.7</url>
</release>

View File

@ -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" }

Binary file not shown.

View File

@ -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

View File

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

View File

@ -6,6 +6,8 @@ import java.util.Optional;
public record Message(
String messageText,
List<String> attachments,
List<AttachmentDimensions> attachmentDimensions,
List<String> attachmentBlurHashes,
boolean viewOnce,
boolean voiceNote,
List<Mention> 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(

View File

@ -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> receipt;

View File

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

View File

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

View File

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

View File

@ -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<SignalServiceAttachment> uploadAttachments(
final List<String> attachments,
final List<AttachmentDimensions> dimensions,
final List<String> 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<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
return uploadAttachments(attachments, false);
return uploadAttachments(attachments, List.of(), List.of(), false);
}
private List<SignalServiceAttachmentStream> createAttachmentStreams(
List<String> attachments,
List<AttachmentDimensions> dimensions,
List<String> blurHashes,
boolean voiceNote
) throws AttachmentInvalidException, IOException {
if (attachments == null) {
return null;
}
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
for (var attachment : attachments) {
final var attachmentStream = getAttachmentStream(attachment, voiceNote);
signalServiceAttachments.add(attachmentStream);
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);
}

View File

@ -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,

View File

@ -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() {

View File

@ -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<RecipientId> 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)

View File

@ -637,11 +637,11 @@ public class SendHelper {
Set<RecipientId> 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,

View File

@ -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<StorageId> 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<StorageId> 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<StorageId> 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<StorageId> 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<StorageId> findUnknownOnlyLocalStorageIds(
final List<StorageId> localOnlyStorageIds,
final Set<StorageId> unknownStorageIds
) {
return localOnlyStorageIds.stream().filter(unknownStorageIds::contains).toList();
}
static boolean containsOnlyIdentityConflictsPendingRepair(
final WriteOperationResult writeOperation,
final Set<StorageId> 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<StorageId>, List<StorageId>> processKnownRecords(
final Connection connection,
List<SignalStorageRecord> records
List<SignalStorageRecord> records,
final Set<StorageId> identityConflictsPendingRepair
) throws SQLException {
final var unknownRecords = new ArrayList<StorageId>();
final var processedRecords = new ArrayList<StorageId>();
@ -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()

View File

@ -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<SignalServicePreview>(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(),

View File

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

View File

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

View File

@ -884,9 +884,9 @@ public class GroupStore {
final var members = membersString == null
? Set.<RecipientId>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");

View File

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

View File

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

View File

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

View File

@ -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<Signal
private final SignalAccount account;
private final Connection connection;
private final JobExecutor jobExecutor;
private final Set<StorageId> identityConflictsPendingRepair;
public ContactRecordProcessor(SignalAccount account, Connection connection, final JobExecutor jobExecutor) {
public ContactRecordProcessor(
SignalAccount account,
Connection connection,
final JobExecutor jobExecutor,
final Set<StorageId> 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<Signal
return unregisteredAtTimestamp > 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<Signal
final var remoteIdentityKeySize = remote.identityKey.size();
final var localIdentityKeySize = local.identityKey.size();
final var statesDiffer = remote.identityState != local.identityState;
final var identityKeysExistAndConflict = remoteIdentityKeySize > 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<Signal
e164 = firstNonEmpty(remote.e164, local.e164);
}
if (identityKeysExistAndConflict) {
if (conflictAci != null) {
logger.debug("Identity keys conflict for {}. Enqueueing a profile fetch.", conflictAci);
jobExecutor.enqueueJob(new DownloadProfileJob(new RecipientAddress(conflictAci, pni, e164), true));
} else {
logger.debug("Identity keys conflict for {}. No ACI, so no profile fetch is possible.", localPni);
}
} else if (identityKey.size() > 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<Signal
.hideStory(remote.hideStory)
.unregisteredAtTimestamp(remote.unregisteredAtTimestamp)
.hidden(remote.hidden)
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
.pniSignatureVerified((remote.pniSignatureVerified || local.pniSignatureVerified)
&& pni != null
&& pni.isValid())
.nickname(remote.nickname)
.note(remote.note)
.avatarColor(remote.avatarColor);
@ -272,6 +313,9 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
final var matchesLocal = doProtosMatch(merged, local);
if (matchesLocal) {
if (identityKeysExistAndConflict && conflictAci != null) {
identityConflictsPendingRepair.add(localRecord.getId());
}
return localRecord;
}

View File

@ -84,8 +84,15 @@ abstract class DefaultStorageRecordProcessor<E extends SignalRecord<?>> 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);
}
/**

View File

@ -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() {

View File

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

View File

@ -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<String> 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<String> name,
ResumableUploadSpec resumableUploadSpec
) throws ResumeLocationInvalidException, IOException {
return createAttachmentStream(streamDetails, name, false, resumableUploadSpec);
return createAttachmentStream(streamDetails, name, false, null, null, resumableUploadSpec);
}
/**

View File

@ -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(""),

View File

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

View File

@ -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<SignalStorageRecord> inserts) {
return new WriteOperationResult(null, inserts, List.of());
}
}

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
0.99.1
0.102.1

View File

@ -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:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>`
*--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.

View File

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

View File

@ -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.<String>getList("attachment-dimensions");
final var attachmentDimensions = dimensionStrings == null
? List.<Message.AttachmentDimensions>of()
: parseAttachmentDimensions(dimensionStrings);
final var blurHashes = ns.<String>getList("attachment-blurhash");
final var attachmentBlurHashes = blurHashes == null ? List.<String>of() : parseAttachmentBlurHashes(blurHashes);
final var selfNumber = m.getSelfNumber();
final var mentionStrings = ns.<String>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<Message.AttachmentDimensions> parseAttachmentDimensions(
final List<String> dimensionStrings
) throws UserErrorException {
final var dimensionPattern = Pattern.compile("([1-9]\\d*)x([1-9]\\d*)");
final var dimensions = new ArrayList<Message.AttachmentDimensions>();
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<String> parseAttachmentBlurHashes(final List<String> 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<Message.Mention> parseMentions(
final String selfNumber,
final List<String> mentionStrings

View File

@ -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.")

View File

@ -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(),

View File

@ -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": "<init>",
"parameterTypes": [
"java.lang.String"
]
}
]
},
{
"type": "org.signal.libsignal.net.MismatchedDeviceException",
"jniAccessible": true,
"methods": [
{
"name": "<init>",
"parameterTypes": [
"java.lang.String",
"org.signal.libsignal.net.MismatchedDeviceException$Entry[]"
]
}
]
},
{
"type": "org.signal.libsignal.net.MismatchedDeviceException$Entry",
"jniAccessible": true,
"methods": [
{
"name": "<init>",
"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": "<init>",
"parameterTypes": [
"java.lang.String",
"java.lang.String",
"org.signal.libsignal.net.ChallengeOption[]",
"long"
]
}
]
},
{
"type": "org.signal.libsignal.net.RequestUnauthorizedException",
"jniAccessible": true,
"methods": [
{
"name": "<init>",
"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": "<init>",
"parameterTypes": [
"java.lang.String"
]
}
]
},
{
"type": "org.signal.libsignal.net.ServiceIdNotFoundException",
"jniAccessible": true,
"methods": [
{
"name": "<init>",
"parameterTypes": [
"java.lang.String"
]
}
]
},
{
"type": "org.signal.libsignal.net.TimeoutException",
"jniAccessible": true,
"methods": [
{
"name": "<init>",
"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": "<init>",
"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": "<init>",
"parameterTypes": [
"java.lang.String"
]
}
]
},
{
"type": "org.signal.libsignal.net.internal.BridgeChatListener",
"jniAccessible": true
@ -6492,6 +6650,13 @@
"parameterTypes": [
"java.lang.String"
]
},
{
"name": "<init>",
"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"
},

View File

@ -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<String> 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<String> blurHashes) {
return new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS,
"attachmentBlurhash", blurHashes));
}
private JsonRpcNamespace namespace(List<String> dimensions) {
return new JsonRpcNamespace(Map.of("noteToSelf", true, "attachments", ATTACHMENTS,
"attachmentDimensions", dimensions));
}
private Message send(Namespace namespace) throws Exception {
final var captured = new AtomicReference<Message>();
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();
}
}