From 7887ed408da289beb7f8c2e4a2f6a20f673abaf8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 16 Apr 2026 20:47:39 +0200 Subject: [PATCH 01/44] Update libsignal-service --- gradle/libs.versions.toml | 2 +- src/main/java/org/asamk/signal/BaseConfig.java | 2 +- .../org.asamk/signal-cli/reachability-metadata.json | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 697ab32a..05e161da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.32" -signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_143" +signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_144" sqlite = "org.xerial:sqlite-jdbc:3.51.2.0" hikari = "com.zaxxer:HikariCP:7.0.2" junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index 022a22f1..7a35165b 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -8,7 +8,7 @@ public class BaseConfig { public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT")) - .orElse("Signal-Android/8.6.1"); + .orElse("Signal-Android/8.8.0"); static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index f70e705d..c4c6469a 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1564,6 +1564,9 @@ } ] }, + { + "type": "kotlin.reflect.jvm.internal.impl.km.jvm.internal.JvmMetadataExtensions" + }, { "type": "kotlin.reflect.jvm.internal.impl.load.java.ErasedOverridabilityCondition" }, @@ -10115,6 +10118,9 @@ { "glob": "META-INF/services/java.util.spi.ResourceBundleControlProvider" }, + { + "glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.km.internal.extensions.MetadataExtensions" + }, { "glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition" }, From 740cd6f89b97ec5e8a3cee904ca1b9c168514e81 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 16 Apr 2026 20:53:16 +0200 Subject: [PATCH 02/44] Update dependencies --- client/Cargo.lock | 323 +++++++----------- gradle/libs.versions.toml | 8 +- .../signal-cli/reachability-metadata.json | 24 ++ 3 files changed, 160 insertions(+), 195 deletions(-) diff --git a/client/Cargo.lock b/client/Cargo.lock index bd3dfd02..36417919 100644 --- a/client/Cargo.lock +++ b/client/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -19,15 +19,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -83,9 +83,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bytes" @@ -95,9 +95,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -117,9 +117,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -140,9 +140,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -152,15 +152,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -377,9 +377,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -391,7 +391,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -399,16 +398,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "log", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -436,12 +434,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -449,9 +448,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -462,9 +461,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -476,15 +475,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -496,15 +495,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -538,9 +537,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -554,9 +553,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" @@ -567,7 +566,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -576,9 +575,31 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jsonrpsee" @@ -668,9 +689,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "linux-raw-sys" @@ -680,9 +701,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" @@ -698,9 +719,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -709,9 +730,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -757,26 +778,20 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -792,9 +807,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -815,9 +830,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" @@ -834,9 +849,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "log", "once_cell", @@ -897,9 +912,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "ring", "rustls-pki-types", @@ -917,9 +932,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -1026,12 +1041,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1082,12 +1097,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1132,9 +1147,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -1142,9 +1157,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" dependencies = [ "bytes", "libc", @@ -1157,9 +1172,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -1202,18 +1217,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "toml_datetime", @@ -1223,9 +1238,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] @@ -1360,14 +1375,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.6", + "webpki-root-certs 1.0.7", ] [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -1414,15 +1429,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -1456,30 +1462,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -1492,12 +1481,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -1510,12 +1493,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -1528,24 +1505,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -1558,12 +1523,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -1576,12 +1535,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -1594,12 +1547,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -1612,32 +1559,26 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -1646,9 +1587,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -1658,18 +1599,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -1685,9 +1626,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -1696,9 +1637,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -1707,9 +1648,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05e161da..ef0db840 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] slf4j = "2.0.17" -junit = "6.0.2" -micronaut-json-schema = "2.0.0-M6" +junit = "6.0.3" +micronaut-json-schema = "2.0.0-M8" micronaut-core = "4.9.3" [libraries] -bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.83" +bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.20.2" argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0" dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0" @@ -19,7 +19,7 @@ slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.32" signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_144" -sqlite = "org.xerial:sqlite-jdbc:3.51.2.0" +sqlite = "org.xerial:sqlite-jdbc:3.53.0.0" hikari = "com.zaxxer:HikariCP:7.0.2" junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index c4c6469a..4513328b 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1629,6 +1629,9 @@ } ] }, + { + "type": "kotlinx.coroutines.CancelledContinuation" + }, { "type": "kotlinx.coroutines.CompletedExceptionally", "fields": [ @@ -1684,6 +1687,9 @@ } ] }, + { + "type": "kotlinx.coroutines.internal.LimitedDispatcher" + }, { "type": "kotlinx.coroutines.internal.LockFreeLinkedListNode", "fields": [ @@ -1717,6 +1723,9 @@ } ] }, + { + "type": "kotlinx.coroutines.internal.ThreadSafeHeap" + }, { "type": "kotlinx.coroutines.scheduling.CoroutineScheduler", "fields": [ @@ -1731,6 +1740,12 @@ } ] }, + { + "type": "kotlinx.coroutines.scheduling.CoroutineScheduler$Worker" + }, + { + "type": "kotlinx.coroutines.scheduling.WorkQueue" + }, { "type": "libcore.io.Memory" }, @@ -4901,6 +4916,15 @@ } ] }, + { + "type": "org.bouncycastle.jcajce.provider.kdf.PBKDF2$Mappings", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, { "type": "org.bouncycastle.jcajce.provider.kdf.SCRYPT$Mappings", "methods": [ From 33b2b563b3530890b4bee2c8ba7e898d5310dfb8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 16 Apr 2026 20:54:19 +0200 Subject: [PATCH 03/44] Don't send busy call response to allow linked devices to accept call --- .../java/org/asamk/signal/manager/helper/CallManager.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java index cc2d7064..ad29c4b3 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java @@ -197,11 +197,6 @@ public class CallManager implements AutoCloseable { if (callEventListeners.isEmpty()) { logger.debug("Ignoring incoming offer for call {}: no call event listeners registered", callIdUnsigned(callId)); - - final var result = sendBusyMessage(callId, recipientId, deviceId); - if (!result.isSuccess()) { - logger.warn("Failed to send busy for unhandled call {}", callIdUnsigned(callId)); - } return; } From 417d2ce971c85f708fb255bd2773aba12afd19f5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 16 Apr 2026 21:04:20 +0200 Subject: [PATCH 04/44] Keep websocket connection alive during call --- .../java/org/asamk/signal/manager/helper/CallManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java index ad29c4b3..ac0a2a49 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java @@ -105,6 +105,8 @@ public class CallManager implements AutoCloseable { recipientAddress, recipientId); activeCalls.put(callId, state); + dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId); + dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId); fireCallEvent(state, null); // Spawn call tunnel binary and connect control channel @@ -696,6 +698,8 @@ public class CallManager implements AutoCloseable { private void endCall(final long callId, final String reason) { var state = activeCalls.remove(callId); + dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId); + dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId); if (state == null) return; state.state = CallInfo.State.ENDED; From b8d990b0f9efca7080eaa32c1551d4ce55b51097 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 22 Apr 2026 21:19:25 +0200 Subject: [PATCH 05/44] Adapt workflows to use previous naming scheme --- .github/workflows/build.yml | 8 ++++---- .github/workflows/release.yml | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b23ed3c..5a4cf4d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,22 +13,22 @@ jobs: build: strategy: matrix: - # The "reproducible" entry is used to build the project with the LTS Java version used in reproducible builds script. + # java="25" is the LTS Java version used in reproducible builds script (default in Containerfile). # More Java versions can be added to test compatibility, eg. "26". - java: ["reproducible", "26"] + java: ["25", "26"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Build run: | - if [ "${{ matrix.java }}" != "reproducible" ]; then + if [ "${{ matrix.java }}" != "25" ]; then export OVERRIDE_JAVA_VERSION="${{ matrix.java }}" fi ./reproducible-builds/build.sh - name: Upload build artifacts uses: actions/upload-artifact@v7 with: - name: signal-cli-${{ matrix.java }}-${{ github.job }} + name: signal-cli-archive-${{ matrix.java }} path: dist/* build-client: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a18b953f..621581f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,10 +12,11 @@ env: IMAGE_REGISTRY: ghcr.io/asamk REGISTRY_USER: ${{ github.actor }} REGISTRY_PASSWORD: ${{ github.token }} + ARCHIVE_JAVA_VERSION: 25 jobs: build: - uses: AsamK/signal-cli/.github/workflows/build.yml@master + uses: ./.github/workflows/build.yml release: needs: build @@ -31,7 +32,7 @@ jobs: - name: Get signal-cli version id: version run: | - mv ./signal-cli-reproducible-build/* . + mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* . echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT - name: Create release @@ -87,7 +88,7 @@ jobs: - name: Move archive file run: | - tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}.tar.gz + tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}.tar.gz mkdir -p build/install/ mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli @@ -127,7 +128,7 @@ jobs: - name: Move archive file run: | - tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz + tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz mkdir -p build/native/nativeCompile/ mv signal-cli build/native/nativeCompile/ chmod +x build/native/nativeCompile/signal-cli @@ -168,7 +169,7 @@ jobs: - name: Move archive file run: | - tar xf ./signal-cli-reproducible-build/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz + tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz mkdir -p client/target/release/ mv signal-cli-client client/target/release/ chmod +x client/target/release/signal-cli-client From 640565512746b968cbfd0bffd1873cd76ee1ee26 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 22 Apr 2026 21:43:26 +0200 Subject: [PATCH 06/44] Only build native/client containers if required --- reproducible-builds/build.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/reproducible-builds/build.sh b/reproducible-builds/build.sh index 8a3878ce..2a46edaa 100755 --- a/reproducible-builds/build.sh +++ b/reproducible-builds/build.sh @@ -18,11 +18,8 @@ fi VERSION=$(sed -n 's/\s*version\s*=\s*"\(.*\)".*/\1/p' build.gradle.kts | tail -n1) echo "$VERSION" >dist/VERSION -$ENGINE build -t signal-cli:build ${OVERRIDE_JAVA_VERSION:+--build-arg ZULU_TAG=$OVERRIDE_JAVA_VERSION} -f reproducible-builds/build.Containerfile . -$ENGINE build -t signal-cli:native -f reproducible-builds/native.Containerfile . -$ENGINE build -t signal-cli:client -f reproducible-builds/client.Containerfile . - # Build jar +$ENGINE build -t signal-cli:build ${OVERRIDE_JAVA_VERSION:+--build-arg ZULU_TAG=$OVERRIDE_JAVA_VERSION} -f reproducible-builds/build.Containerfile . git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**' # shellcheck disable=SC2086 $ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:build @@ -34,12 +31,14 @@ if [ -n "${OVERRIDE_JAVA_VERSION:-}" ]; then fi # Build native-image +$ENGINE build -t signal-cli:native -f reproducible-builds/native.Containerfile . git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**' # shellcheck disable=SC2086 $ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:native mv build/signal-cli-*-Linux-native.tar.gz dist/ # Build rust client +$ENGINE build -t signal-cli:client -f reproducible-builds/client.Containerfile . git clean -Xfd -e '!/dist/' -e '!/dist/**' -e '!/github/' -e '!/github/**' # shellcheck disable=SC2086 $ENGINE run --pull=never --rm -v "$(pwd)":/signal-cli:Z -e VERSION="$VERSION" $USER signal-cli:client From aa446619f23152f39cb07b796f0853fbb1e3c8ea Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 22 Apr 2026 22:08:10 +0200 Subject: [PATCH 07/44] Add script to update pinned container versions --- reproducible-builds/client.Containerfile | 4 +- reproducible-builds/native.Containerfile | 4 +- .../update-pinned-container-versions.sh | 45 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100755 reproducible-builds/update-pinned-container-versions.sh diff --git a/reproducible-builds/client.Containerfile b/reproducible-builds/client.Containerfile index 8a8dfa34..c65f1678 100644 --- a/reproducible-builds/client.Containerfile +++ b/reproducible-builds/client.Containerfile @@ -1,4 +1,6 @@ -FROM docker.io/rust:1.94.1-slim-trixie@sha256:c6a474d7164ea2455e09b60a759b1edca38db7373c5689c1dae31780de4e71ac +ARG RUST_TAG="1.94.1-slim-trixie@sha256:c6a474d7164ea2455e09b60a759b1edca38db7373c5689c1dae31780de4e71ac" + +FROM docker.io/rust:$RUST_TAG ENV SOURCE_DATE_EPOCH=1767225600 ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 diff --git a/reproducible-builds/native.Containerfile b/reproducible-builds/native.Containerfile index 47cd5ce3..326bb52b 100644 --- a/reproducible-builds/native.Containerfile +++ b/reproducible-builds/native.Containerfile @@ -1,4 +1,6 @@ -FROM container-registry.oracle.com/graalvm/native-image:25.0.2@sha256:4c0d5919f6840d89721274eb8cf81962faa2f870b816967e6732e2a151b150d8 +ARG GRAALVM_TAG="25.0.2@sha256:4c0d5919f6840d89721274eb8cf81962faa2f870b816967e6732e2a151b150d8" + +FROM container-registry.oracle.com/graalvm/native-image:$GRAALVM_TAG ENV SOURCE_DATE_EPOCH=1767225600 ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 diff --git a/reproducible-builds/update-pinned-container-versions.sh b/reproducible-builds/update-pinned-container-versions.sh new file mode 100755 index 00000000..46b903a7 --- /dev/null +++ b/reproducible-builds/update-pinned-container-versions.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../" +cd "$ROOT_DIR" + +if command -v podman >/dev/null; then + ENGINE=podman +elif command -v docker >/dev/null; then + ENGINE=docker +else + echo "error: neither podman nor docker is available" >&2 + exit 1 +fi + +resolve_digest() { + local image_ref="$1" + "$ENGINE" pull "$image_ref" >/dev/null + "$ENGINE" image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$image_ref" \ + | grep -m1 -E '@sha256:[0-9a-f]{64}$' \ + | sed -E 's|.*(@sha256:[0-9a-f]{64})$|\1|' +} + +update_arg_tag() { + local file="$1" + local arg_name="$2" + local image_prefix="$3" + local current + current="$(sed -n "s/^ARG ${arg_name}=\"\([^\"]*\)\"$/\\1/p" "$file")" + if [[ -z "$current" ]]; then + echo "error: could not find ARG ${arg_name} in $file" >&2 + exit 1 + fi + local tag + tag="${current%@*}" + local digest + digest="$(resolve_digest "${image_prefix}${tag}")" + sed -i -E "s|^ARG ${arg_name}=\"[^\"]+\"$|ARG ${arg_name}=\"${tag}${digest}\"|" "$file" + echo "updated $file -> ${tag}${digest}" +} + +update_arg_tag reproducible-builds/build.Containerfile ZULU_TAG docker.io/azul/zulu-openjdk: +update_arg_tag reproducible-builds/native.Containerfile GRAALVM_TAG container-registry.oracle.com/graalvm/native-image: +update_arg_tag reproducible-builds/client.Containerfile RUST_TAG docker.io/rust: From 47e50988b54b84142095b4776f654b9af25e07fb Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 22 Apr 2026 22:22:32 +0200 Subject: [PATCH 08/44] Make SOURCE_DATE_EPOCH configurable --- reproducible-builds/build.Containerfile | 9 ++++----- reproducible-builds/client.Containerfile | 3 ++- reproducible-builds/native.Containerfile | 3 ++- .../update-pinned-container-versions.sh | 12 ++++++++++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/reproducible-builds/build.Containerfile b/reproducible-builds/build.Containerfile index 0e73ddc5..f65f9639 100644 --- a/reproducible-builds/build.Containerfile +++ b/reproducible-builds/build.Containerfile @@ -1,13 +1,12 @@ ARG ZULU_TAG="25.0.2-jdk@sha256:9582df6c4415d9c770eb5ff8fce426ebba53631149c9eb083ee126568d32fab3" +ARG SOURCE_DATE_EPOCH="1767225600" FROM docker.io/azul/zulu-openjdk:$ZULU_TAG -ENV SOURCE_DATE_EPOCH=1767225600 +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 -ARG SNAPSHOT=20260101T000000Z -RUN echo "deb http://snapshot.ubuntu.com/ubuntu/${SNAPSHOT}/ jammy main" > /etc/apt/sources.list \ - && echo "deb http://snapshot.ubuntu.com/ubuntu/${SNAPSHOT}/ jammy universe" >> /etc/apt/sources.list -RUN apt update && apt install -y make asciidoc-base +RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \ + && apt install -y make asciidoc-base --update --snapshot "$SNAPSHOT" COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh WORKDIR /signal-cli ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ] diff --git a/reproducible-builds/client.Containerfile b/reproducible-builds/client.Containerfile index c65f1678..521a9181 100644 --- a/reproducible-builds/client.Containerfile +++ b/reproducible-builds/client.Containerfile @@ -1,7 +1,8 @@ ARG RUST_TAG="1.94.1-slim-trixie@sha256:c6a474d7164ea2455e09b60a759b1edca38db7373c5689c1dae31780de4e71ac" +ARG SOURCE_DATE_EPOCH="1767225600" FROM docker.io/rust:$RUST_TAG -ENV SOURCE_DATE_EPOCH=1767225600 +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/reproducible-builds/native.Containerfile b/reproducible-builds/native.Containerfile index 326bb52b..4cae2bcd 100644 --- a/reproducible-builds/native.Containerfile +++ b/reproducible-builds/native.Containerfile @@ -1,7 +1,8 @@ ARG GRAALVM_TAG="25.0.2@sha256:4c0d5919f6840d89721274eb8cf81962faa2f870b816967e6732e2a151b150d8" +ARG SOURCE_DATE_EPOCH="1767225600" FROM container-registry.oracle.com/graalvm/native-image:$GRAALVM_TAG -ENV SOURCE_DATE_EPOCH=1767225600 +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh diff --git a/reproducible-builds/update-pinned-container-versions.sh b/reproducible-builds/update-pinned-container-versions.sh index 46b903a7..f03c4333 100755 --- a/reproducible-builds/update-pinned-container-versions.sh +++ b/reproducible-builds/update-pinned-container-versions.sh @@ -40,6 +40,18 @@ update_arg_tag() { echo "updated $file -> ${tag}${digest}" } +update_source_date_epoch() { + local file="$1" + local current_timestamp + current_timestamp="$(date +%s)" + sed -i -E "s|^ARG SOURCE_DATE_EPOCH=\"[^\"]+\"$|ARG SOURCE_DATE_EPOCH=\"${current_timestamp}\"|" "$file" + echo "updated $file SOURCE_DATE_EPOCH -> ${current_timestamp}" +} + update_arg_tag reproducible-builds/build.Containerfile ZULU_TAG docker.io/azul/zulu-openjdk: update_arg_tag reproducible-builds/native.Containerfile GRAALVM_TAG container-registry.oracle.com/graalvm/native-image: update_arg_tag reproducible-builds/client.Containerfile RUST_TAG docker.io/rust: + +update_source_date_epoch reproducible-builds/build.Containerfile +update_source_date_epoch reproducible-builds/native.Containerfile +update_source_date_epoch reproducible-builds/client.Containerfile From d6edaf3be2cc203e5df81f19bc2fdbfdaf79907c Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 22 Apr 2026 22:18:42 +0200 Subject: [PATCH 09/44] Update reproducible build container images --- reproducible-builds/build.Containerfile | 4 ++-- reproducible-builds/client.Containerfile | 4 ++-- reproducible-builds/native.Containerfile | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/reproducible-builds/build.Containerfile b/reproducible-builds/build.Containerfile index f65f9639..4c1e8a1b 100644 --- a/reproducible-builds/build.Containerfile +++ b/reproducible-builds/build.Containerfile @@ -1,7 +1,7 @@ -ARG ZULU_TAG="25.0.2-jdk@sha256:9582df6c4415d9c770eb5ff8fce426ebba53631149c9eb083ee126568d32fab3" -ARG SOURCE_DATE_EPOCH="1767225600" +ARG ZULU_TAG="25-latest@sha256:8eca9375451a392bff01efe946f2e9263c50aa71a9d68423c068cc1061a41b7e" FROM docker.io/azul/zulu-openjdk:$ZULU_TAG +ARG SOURCE_DATE_EPOCH="1776889382" ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 diff --git a/reproducible-builds/client.Containerfile b/reproducible-builds/client.Containerfile index 521a9181..59d85db8 100644 --- a/reproducible-builds/client.Containerfile +++ b/reproducible-builds/client.Containerfile @@ -1,7 +1,7 @@ -ARG RUST_TAG="1.94.1-slim-trixie@sha256:c6a474d7164ea2455e09b60a759b1edca38db7373c5689c1dae31780de4e71ac" -ARG SOURCE_DATE_EPOCH="1767225600" +ARG RUST_TAG="1-slim@sha256:715efd1ccdc4a63bd6a6e2f54387fff73f904b70e610d41b4d9d74ff38e13ad3" FROM docker.io/rust:$RUST_TAG +ARG SOURCE_DATE_EPOCH="1776889382" ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 diff --git a/reproducible-builds/native.Containerfile b/reproducible-builds/native.Containerfile index 4cae2bcd..a242538d 100644 --- a/reproducible-builds/native.Containerfile +++ b/reproducible-builds/native.Containerfile @@ -1,7 +1,7 @@ -ARG GRAALVM_TAG="25.0.2@sha256:4c0d5919f6840d89721274eb8cf81962faa2f870b816967e6732e2a151b150d8" -ARG SOURCE_DATE_EPOCH="1767225600" +ARG GRAALVM_TAG="25@sha256:38f835ccb37d4a106c37376a98e8713999077a8c8173d9876505f77da438332c" FROM container-registry.oracle.com/graalvm/native-image:$GRAALVM_TAG +ARG SOURCE_DATE_EPOCH="1776889382" ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 From da214817be1b43c6c3da3642167189e47987e24c Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 22 Apr 2026 22:43:13 +0200 Subject: [PATCH 10/44] Reduce number of installed packages --- reproducible-builds/build.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reproducible-builds/build.Containerfile b/reproducible-builds/build.Containerfile index 4c1e8a1b..38626266 100644 --- a/reproducible-builds/build.Containerfile +++ b/reproducible-builds/build.Containerfile @@ -6,7 +6,7 @@ ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \ - && apt install -y make asciidoc-base --update --snapshot "$SNAPSHOT" + && apt install -y make asciidoc-base --update --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh WORKDIR /signal-cli ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ] From 056878fad7a74dc38a2ac74a056121e958d631a0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 22 Apr 2026 23:12:40 +0200 Subject: [PATCH 11/44] Bump version to 0.14.3 --- CHANGELOG.md | 14 +++++++++++++- build.gradle.kts | 2 +- data/org.asamk.SignalCli.metainfo.xml | 3 +++ libsignal-version | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44478806..90d02420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,22 @@ # Changelog -## [Unreleased] +## [0.14.3] - 2026-04-22 + +### Fixed + +- Fix sender key re-distribution on every group message (Thanks @meinecke) + +### Improved + +- Performance improvement when assigning admin role to multiple group members +- Increase disconnect timeout for websocket connections +- Release builds are now reproducible ### Changed - Send message results now surface server-advised retry time for plain rate-limit (HTTP 413) failures, not only for proof-required challenges. The `retryAfterSeconds` field in JSON-RPC `SendMessageResult` is populated whenever the server sends a `Retry-After` header. The canonical way to distinguish proof-required failures remains `token != null`. Text output includes "retry after N seconds" when known. +- Add distinct JSON-RPC error code (6) for captcha rejection (Thanks @tonycpsu) +- No longer sends busy call response to allow linked devices to accept call ## [0.14.2] - 2026-04-04 diff --git a/build.gradle.kts b/build.gradle.kts index f1e10f4d..64e29294 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.3-SNAPSHOT" + version = "0.14.3" } java { diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml index c1730192..3bf04d8c 100644 --- a/data/org.asamk.SignalCli.metainfo.xml +++ b/data/org.asamk.SignalCli.metainfo.xml @@ -45,6 +45,9 @@ intense + + https://github.com/AsamK/signal-cli/releases/tag/v0.14.3 + https://github.com/AsamK/signal-cli/releases/tag/v0.14.2 diff --git a/libsignal-version b/libsignal-version index ae02209b..da011ce4 100644 --- a/libsignal-version +++ b/libsignal-version @@ -1 +1 @@ -0.90.0 +0.92.1 From e6635d1bb0505d460d40d7cbdadfee3e833dccdc Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 23 Apr 2026 20:37:35 +0200 Subject: [PATCH 12/44] Prepare next release --- CHANGELOG.md | 2 ++ build.gradle.kts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d02420..9f046f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.14.3] - 2026-04-22 ### Fixed diff --git a/build.gradle.kts b/build.gradle.kts index 64e29294..5ed47048 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.3" + version = "0.14.4-SNAPSHOT" } java { From 6286a054eb6a5d2d45dcb5a79ebd417d8367c771 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 23 Apr 2026 20:31:43 +0200 Subject: [PATCH 13/44] Update libsignal-service --- gradle/libs.versions.toml | 3 ++- lib/build.gradle.kts | 4 ++-- .../signal/manager/helper/StorageHelper.java | 14 +++++++------- .../manager/internal/SignalDependencies.java | 18 +++++++++--------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef0db840..e952c6eb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ slf4j = "2.0.17" junit = "6.0.3" micronaut-json-schema = "2.0.0-M8" micronaut-core = "4.9.3" +signal-service = "2.15.3_unofficial_145" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" @@ -18,7 +19,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.32" -signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_144" +signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" } sqlite = "org.xerial:sqlite-jdbc:3.53.0.0" hikari = "com.zaxxer:HikariCP:7.0.2" junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 45237064..510482e8 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -18,9 +18,9 @@ val libsignalClientPath = project.findProperty("libsignal_client_path")?.toStrin dependencies { if (libsignalClientPath == null) { - implementation(libs.signalservice) + implementation(libs.signalnetwork) } else { - implementation(libs.signalservice) { + implementation(libs.signalnetwork) { exclude(group = "org.signal", module = "libsignal-client") } implementation(files(libsignalClientPath)) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index aeeb3ac5..6f60f734 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -17,6 +17,9 @@ import org.asamk.signal.manager.util.KeyUtils; import org.signal.core.models.storageservice.StorageKey; import org.signal.core.util.SetUtil; import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.network.service.StorageServiceService; +import org.signal.network.service.StorageServiceService.ManifestIfDifferentVersionResult; +import org.signal.network.service.StorageServiceService.WriteStorageRecordsResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; @@ -25,9 +28,6 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt; -import org.whispersystems.signalservice.api.storage.StorageServiceRepository; -import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult; -import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import org.whispersystems.signalservice.internal.storage.protos.StorageRecord; @@ -504,7 +504,7 @@ public class StorageHelper { final var result = dependencies.getStorageServiceRepository() .readStorageRecords(storageKey, manifest.recordIkm, storageIds); return switch (result) { - case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> { + case StorageServiceService.StorageRecordResult.DecryptionError decryptionError -> { if (decryptionError.getException() instanceof InvalidKeyException) { logger.warn("Failed to read storage records, ignoring."); yield List.of(); @@ -514,11 +514,11 @@ public class StorageHelper { throw new IOException(decryptionError.getException()); } } - case StorageServiceRepository.StorageRecordResult.NetworkError networkError -> + case StorageServiceService.StorageRecordResult.NetworkError networkError -> throw networkError.getException(); - case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError -> + case StorageServiceService.StorageRecordResult.StatusCodeError statusCodeError -> throw statusCodeError.getException(); - case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords(); + case StorageServiceService.StorageRecordResult.Success success -> success.getRecords(); default -> throw new IllegalStateException("Unexpected value: " + result); }; } diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 77068eaa..d81078d4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -6,6 +6,13 @@ import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.net.Network; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.network.api.CallingApi; +import org.signal.network.api.CdsApi; +import org.signal.network.api.CertificateApi; +import org.signal.network.api.LinkDeviceApi; +import org.signal.network.api.RateLimitChallengeApi; +import org.signal.network.api.UsernameApi; +import org.signal.network.service.StorageServiceService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.SignalServiceAccountManager; @@ -15,26 +22,19 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.account.AccountApi; import org.whispersystems.signalservice.api.attachment.AttachmentApi; -import org.whispersystems.signalservice.api.calling.CallingApi; -import org.whispersystems.signalservice.api.cds.CdsApi; -import org.whispersystems.signalservice.api.certificate.CertificateApi; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.keys.KeysApi; -import org.whispersystems.signalservice.api.link.LinkDeviceApi; import org.whispersystems.signalservice.api.message.MessageApi; import org.whispersystems.signalservice.api.profiles.ProfileApi; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.storage.StorageServiceApi; -import org.whispersystems.signalservice.api.storage.StorageServiceRepository; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; -import org.whispersystems.signalservice.api.username.UsernameApi; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; @@ -243,8 +243,8 @@ public class SignalDependencies { getPushServiceSocket())); } - public StorageServiceRepository getStorageServiceRepository() { - return new StorageServiceRepository(getStorageServiceApi()); + public StorageServiceService getStorageServiceRepository() { + return new StorageServiceService(getStorageServiceApi()); } public CertificateApi getCertificateApi() { From 5fe94ff44ac449ec13f12e7c2b1decff75e95261 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 23 Apr 2026 21:58:10 +0200 Subject: [PATCH 14/44] Temporarily disable 26 build due to container issue --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a4cf4d7..aaac4e07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: matrix: # java="25" is the LTS Java version used in reproducible builds script (default in Containerfile). # More Java versions can be added to test compatibility, eg. "26". - java: ["25", "26"] + java: ["25"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 From 9b09df5f17ad8f93c6868735f3b8c7e7a139e00b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:55:35 +0200 Subject: [PATCH 15/44] Bump rustls-webpki from 0.103.12 to 0.103.13 in /client (#2030) Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.12 to 0.103.13. - [Release notes](https://github.com/rustls/webpki/releases) - [Commits](https://github.com/rustls/webpki/compare/v/0.103.12...v/0.103.13) --- updated-dependencies: - dependency-name: rustls-webpki dependency-version: 0.103.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- client/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/Cargo.lock b/client/Cargo.lock index 36417919..954b400b 100644 --- a/client/Cargo.lock +++ b/client/Cargo.lock @@ -912,9 +912,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.12" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", From c9e25043491dbaf97f7820a2dc0d7a97d1fcdec6 Mon Sep 17 00:00:00 2001 From: Patrick Dattilio Date: Mon, 27 Apr 2026 10:25:47 -0400 Subject: [PATCH 16/44] Store profile keys from group requesting members (#2031) When filling or updating a V2 group, profile keys were copied from DecryptedGroup.members into the local profile store but not from requestingMembers. Admins who never had a prior session with a user in the join queue then lacked profile keys and could not decrypt profiles (e.g. for listContacts). Also process DecryptedRequestingMember entries the same way as full members, using DecryptedMember / DecryptedRequestingMember types so the lib module does not require a direct protobuf dependency. Made-with: Cursor --- .../signal/manager/helper/GroupHelper.java | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 024e84fc..7d475929 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -38,6 +38,8 @@ import org.signal.storageservice.storage.protos.groups.GroupChangeResponse; import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup; import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupJoinInfo; +import org.signal.storageservice.storage.protos.groups.local.DecryptedMember; +import org.signal.storageservice.storage.protos.groups.local.DecryptedRequestingMember; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog; @@ -557,17 +559,44 @@ public class GroupHelper { private void storeProfileKeysFromMembers(final DecryptedGroup group) { for (var member : group.members) { - final var serviceId = ServiceId.parseOrThrow(member.aciBytes); - final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); - final var profileStore = account.getProfileStore(); - if (profileStore.getProfileKey(recipientId) != null) { - // We already have a profile key, not updating it from a non-authoritative source - continue; - } - try { - profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray())); - } catch (InvalidInputException ignored) { - } + storeProfileKeyForDecryptedMemberIfMissing(member); + } + for (var member : group.requestingMembers) { + storeProfileKeyForDecryptedRequestingMemberIfMissing(member); + } + } + + private void storeProfileKeyForDecryptedMemberIfMissing(final DecryptedMember member) { + if (member == null) { + return; + } + final var serviceId = ServiceId.parseOrThrow(member.aciBytes); + final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); + final var profileStore = account.getProfileStore(); + if (profileStore.getProfileKey(recipientId) != null) { + // We already have a profile key, not updating it from a non-authoritative source + return; + } + try { + profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray())); + } catch (InvalidInputException ignored) { + } + } + + private void storeProfileKeyForDecryptedRequestingMemberIfMissing(final DecryptedRequestingMember member) { + if (member == null) { + return; + } + final var serviceId = ServiceId.parseOrThrow(member.aciBytes); + final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); + final var profileStore = account.getProfileStore(); + if (profileStore.getProfileKey(recipientId) != null) { + // We already have a profile key, not updating it from a non-authoritative source + return; + } + try { + profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray())); + } catch (InvalidInputException ignored) { } } From a3fcda7598cc4c9c46f9df48c0ba259dae703eb3 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 24 Apr 2026 19:52:50 +0200 Subject: [PATCH 17/44] Update kotlin jvm version for buildSrc --- buildSrc/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 53271fd0..b7c492e1 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -7,11 +7,11 @@ plugins { } tasks.named>("compileKotlin").configure { - compilerOptions.jvmTarget.set(JvmTarget.JVM_24) + compilerOptions.jvmTarget.set(JvmTarget.JVM_25) } java { - targetCompatibility = JavaVersion.VERSION_24 + targetCompatibility = JavaVersion.VERSION_25 } repositories { From 251bd2d87a664c163b0a18cdfcadc6dc9e68f261 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 27 Apr 2026 16:36:22 +0200 Subject: [PATCH 18/44] Refactor profile key extraction --- .../signal/manager/helper/GroupHelper.java | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 7d475929..1c750358 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -38,8 +38,6 @@ import org.signal.storageservice.storage.protos.groups.GroupChangeResponse; import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup; import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupJoinInfo; -import org.signal.storageservice.storage.protos.groups.local.DecryptedMember; -import org.signal.storageservice.storage.protos.groups.local.DecryptedRequestingMember; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog; @@ -559,18 +557,16 @@ public class GroupHelper { private void storeProfileKeysFromMembers(final DecryptedGroup group) { for (var member : group.members) { - storeProfileKeyForDecryptedMemberIfMissing(member); + final var serviceId = ServiceId.parseOrThrow(member.aciBytes); + storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray()); } for (var member : group.requestingMembers) { - storeProfileKeyForDecryptedRequestingMemberIfMissing(member); + final var serviceId = ServiceId.parseOrThrow(member.aciBytes); + storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray()); } } - private void storeProfileKeyForDecryptedMemberIfMissing(final DecryptedMember member) { - if (member == null) { - return; - } - final var serviceId = ServiceId.parseOrThrow(member.aciBytes); + private void storeProfileKeyIfMissing(final ServiceId serviceId, final byte[] profileKeyBytes) { final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); final var profileStore = account.getProfileStore(); if (profileStore.getProfileKey(recipientId) != null) { @@ -578,24 +574,7 @@ public class GroupHelper { return; } try { - profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray())); - } catch (InvalidInputException ignored) { - } - } - - private void storeProfileKeyForDecryptedRequestingMemberIfMissing(final DecryptedRequestingMember member) { - if (member == null) { - return; - } - final var serviceId = ServiceId.parseOrThrow(member.aciBytes); - final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); - final var profileStore = account.getProfileStore(); - if (profileStore.getProfileKey(recipientId) != null) { - // We already have a profile key, not updating it from a non-authoritative source - return; - } - try { - profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray())); + profileStore.storeProfileKey(recipientId, new ProfileKey(profileKeyBytes)); } catch (InvalidInputException ignored) { } } From dc43e4402050fc774ef2f420e1f7578da0290e17 Mon Sep 17 00:00:00 2001 From: legacycode Date: Wed, 29 Apr 2026 22:52:34 +0200 Subject: [PATCH 19/44] fix: flush SSE response headers immediately on connect (#2034) Without an initial flush(), the JVM HttpServer buffers all output until the first flush() in the 15-second keep-alive loop. Clients with shorter timeouts (e.g. 10 s) abort before receiving any data. Add a flush() call directly after creating ServerSentEventSender, before the wait loop, so the HTTP 200 response and headers reach the client immediately upon connection. Adds regression test SseInitialFlushTest that verifies at least one byte arrives within 2 seconds of connecting to GET /api/v1/events. --- .../asamk/signal/http/HttpServerHandler.java | 6 + .../signal/http/SseInitialFlushTest.java | 537 ++++++++++++++++++ 2 files changed, 543 insertions(+) create mode 100644 src/test/java/org/asamk/signal/http/SseInitialFlushTest.java diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java index 1e62c3de..325b0eec 100644 --- a/src/main/java/org/asamk/signal/http/HttpServerHandler.java +++ b/src/main/java/org/asamk/signal/http/HttpServerHandler.java @@ -169,6 +169,12 @@ public class HttpServerHandler implements AutoCloseable { httpExchange.sendResponseHeaders(200, 0); final var sender = new ServerSentEventSender(httpExchange.getResponseBody()); + // Flush HTTP response headers to the client immediately. + // Without this, the JVM HttpServer buffers everything until a later write + // in the keep-alive loop (15 s), causing clients with shorter timeouts + // (e.g. 10 s) to abort before receiving the initial response. + httpExchange.getResponseBody().flush(); + final var shouldStop = new AtomicBoolean(false); final var handlers = subscribeReceiveHandlers(managers, sender, () -> { shouldStop.set(true); diff --git a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java new file mode 100644 index 00000000..77e59dbd --- /dev/null +++ b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java @@ -0,0 +1,537 @@ +package org.asamk.signal.http; + +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.CallInfo; +import org.asamk.signal.manager.api.CallOffer; +import org.asamk.signal.manager.api.Configuration; +import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.DeviceLinkUrl; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.GroupId; +import org.asamk.signal.manager.api.GroupInviteLinkUrl; +import org.asamk.signal.manager.api.Identity; +import org.asamk.signal.manager.api.IdentityVerificationCode; +import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.MessageEnvelope; +import org.asamk.signal.manager.api.Pair; +import org.asamk.signal.manager.api.ReceiveConfig; +import org.asamk.signal.manager.api.Recipient; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.api.SendMessageResult; +import org.asamk.signal.manager.api.SendMessageResults; +import org.asamk.signal.manager.api.StickerPack; +import org.asamk.signal.manager.api.StickerPackId; +import org.asamk.signal.manager.api.StickerPackUrl; +import org.asamk.signal.manager.api.TurnServer; +import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; +import org.asamk.signal.manager.api.UpdateProfile; +import org.asamk.signal.manager.api.UserStatus; +import org.asamk.signal.manager.api.UsernameLinkUrl; +import org.asamk.signal.manager.api.UsernameStatus; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.URL; +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Regression test for the SSE initial-flush bug: + * HttpServerHandler used to flush the initial SSE response only after a later + * write in the 15-second keep-alive loop, meaning the HTTP response headers + * were not flushed to the client until then. + * Clients with a shorter connection timeout (e.g. 10 s) would time out before + * receiving the initial response. + * + * This test verifies that the endpoint returns HTTP 200 within 2 seconds of + * connecting to GET /api/v1/events. + */ +class SseInitialFlushTest { + + private HttpServerHandler handler; + private int port; + + /** Finds a free local port. */ + private static int freePort() throws Exception { + try (var ss = new ServerSocket(0)) { + ss.setReuseAddress(true); + return ss.getLocalPort(); + } + } + + @BeforeEach + void setUp() throws Exception { + port = freePort(); + handler = new HttpServerHandler(new InetSocketAddress("127.0.0.1", port), new MinimalStubManager()); + handler.init(); + } + + @AfterEach + void tearDown() { + if (handler != null) { + handler.close(); + } + } + + /** + * The SSE endpoint MUST flush the initial HTTP response immediately upon + * connection, before the 15-second keep-alive loop fires. A read timeout of + * 2 000 ms is used — well below the 15-second wait interval but generous + * enough to survive any CI scheduling jitter. + */ + @Test + void sseEndpointReturnsHeadersWithinTwoSeconds() { + assertDoesNotThrow(() -> { + var url = new URL("http://127.0.0.1:" + port + "/api/v1/events"); + var conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("Accept", "text/event-stream"); + conn.setReadTimeout(2_000); // 2 s — fails before fix (15 s flush), passes after + conn.setConnectTimeout(2_000); + try { + conn.connect(); + assertEquals(200, conn.getResponseCode()); + } finally { + conn.disconnect(); + } + }, "SSE endpoint did not return the initial response within 2 seconds"); + } + + // ------------------------------------------------------------------------- + // Minimal Manager stub — only receive-handler methods need real behaviour; + // everything else is a no-op stub. + // ------------------------------------------------------------------------- + + private static final class MinimalStubManager implements Manager { + + @Override + public String getSelfNumber() { + return "+10000000000"; + } + + @Override + public void addReceiveHandler(ReceiveMessageHandler handler, boolean isWeakListener) { + // no-op + } + + @Override + public void removeReceiveHandler(ReceiveMessageHandler handler) { + // no-op + } + + @Override + public boolean isReceiving() { + return false; + } + + @Override + public void receiveMessages(Optional timeout, Optional maxMessages, ReceiveMessageHandler handler) { + } + + @Override + public void stopReceiveMessages() { + } + + @Override + public void setReceiveConfig(ReceiveConfig receiveConfig) { + } + + @Override + public Map getUserStatus(Set numbers) { + return Map.of(); + } + + @Override + public Map getUsernameStatus(Set usernames) { + return Map.of(); + } + + @Override + public void updateAccountAttributes(String deviceName, Boolean unidentifiedDeliveryIndicators, Boolean discoverableByNumber, Boolean numberSharing) { + } + + @Override + public Configuration getConfiguration() { + return null; + } + + @Override + public void updateConfiguration(Configuration configuration) { + } + + @Override + public void updateProfile(UpdateProfile updateProfile) { + } + + @Override + public String getUsername() { + return null; + } + + @Override + public UsernameLinkUrl getUsernameLink() { + return null; + } + + @Override + public void setUsername(String username) { + } + + @Override + public void deleteUsername() { + } + + @Override + public void startChangeNumber(String newNumber, boolean voiceVerification, String captcha) { + } + + @Override + public void finishChangeNumber(String newNumber, String verificationCode, String pin) { + } + + @Override + public void unregister() { + } + + @Override + public void deleteAccount() { + } + + @Override + public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) { + } + + @Override + public List getLinkedDevices() { + return List.of(); + } + + @Override + public void updateLinkedDevice(int deviceId, String name) { + } + + @Override + public void removeLinkedDevices(int deviceId) { + } + + @Override + public void addDeviceLink(DeviceLinkUrl deviceLinkUrl) { + } + + @Override + public void setRegistrationLockPin(Optional pin) { + } + + @Override + public List getGroups() { + return List.of(); + } + + @Override + public List getGroups(Collection groupIds) { + return List.of(); + } + + @Override + public SendGroupMessageResults quitGroup(GroupId groupId, Set administrators) { + return null; + } + + @Override + public void deleteGroup(GroupId groupId) { + } + + @Override + public Pair createGroup(String name, Set members, String avatarFile) { + return null; + } + + @Override + public SendGroupMessageResults updateGroup(GroupId groupId, UpdateGroup updateGroup) { + return null; + } + + @Override + public Pair joinGroup(GroupInviteLinkUrl inviteLinkUrl) { + return null; + } + + @Override + public SendMessageResults sendTypingMessage(TypingAction action, Set recipients) { + return null; + } + + @Override + public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List messageIds) { + return null; + } + + @Override + public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List messageIds) { + return null; + } + + @Override + public SendMessageResults sendMessage(Message message, Set recipients, boolean notifySelf) { + return null; + } + + @Override + public SendMessageResults sendEditMessage(Message message, Set recipients, long targetSentTimestamp) { + return null; + } + + @Override + public SendMessageResults sendRemoteDeleteMessage(long targetSentTimestamp, Set recipients) { + return null; + } + + @Override + public SendMessageResults sendMessageReaction(String emoji, boolean remove, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients, boolean notifySelf, boolean story) { + return null; + } + + @Override + public SendMessageResults sendAdminDelete(RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients, boolean notifySelf, boolean story) { + return null; + } + + @Override + public SendMessageResults sendPinMessage(int duration, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients, boolean notifySelf, boolean story) { + return null; + } + + @Override + public SendMessageResults sendUnpinMessage(RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients, boolean notifySelf, boolean story) { + return null; + } + + @Override + public SendMessageResults sendPaymentNotificationMessage(byte[] receipt, String note, RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public SendMessageResults sendEndSessionMessage(Set recipients) { + return null; + } + + @Override + public SendMessageResults sendMessageRequestResponse(MessageEnvelope.Sync.MessageRequestResponse.Type type, Set recipients) { + return null; + } + + @Override + public SendMessageResults sendPollCreateMessage(String question, boolean multipleChoice, List options, Set recipients, boolean notifySelf) { + return null; + } + + @Override + public SendMessageResults sendPollVoteMessage(RecipientIdentifier.Single author, long timestamp, List optionIds, int version, Set recipients, boolean notifySelf) { + return null; + } + + @Override + public SendMessageResults sendPollTerminateMessage(long timestamp, Set recipients, boolean notifySelf) { + return null; + } + + @Override + public void hideRecipient(RecipientIdentifier.Single recipient) { + } + + @Override + public void deleteRecipient(RecipientIdentifier.Single recipient) { + } + + @Override + public void deleteContact(RecipientIdentifier.Single recipient) { + } + + @Override + public void setContactName(RecipientIdentifier.Single recipient, String givenName, String familyName, String newGivenName, String newFamilyName, String nick) { + } + + @Override + public void setContactsBlocked(Collection recipients, boolean blocked) { + } + + @Override + public void setGroupsBlocked(Collection groupIds, boolean blocked) { + } + + @Override + public void setExpirationTimer(RecipientIdentifier.Single recipient, int messageExpirationTimer) { + } + + @Override + public StickerPackUrl uploadStickerPack(File path) { + return null; + } + + @Override + public void installStickerPack(StickerPackUrl url) { + } + + @Override + public List getStickerPacks() { + return List.of(); + } + + @Override + public void requestAllSyncData() { + } + + @Override + public boolean isContactBlocked(RecipientIdentifier.Single recipient) { + return false; + } + + @Override + public void sendContacts() { + } + + @Override + public List getRecipients(boolean onlyWithProfile, Optional blocked, Collection addresses, Optional name) { + return List.of(); + } + + @Override + public String getContactOrProfileName(RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public Group getGroup(GroupId groupId) { + return null; + } + + @Override + public List getIdentities() { + return List.of(); + } + + @Override + public List getIdentities(RecipientIdentifier.Single recipient) { + return List.of(); + } + + @Override + public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode) { + return false; + } + + @Override + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + return false; + } + + @Override + public void addAddressChangedListener(Runnable listener) { + } + + @Override + public void addClosedListener(Runnable listener) { + } + + @Override + public InputStream retrieveAttachment(String id) { + return null; + } + + @Override + public InputStream retrieveContactAvatar(RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public InputStream retrieveProfileAvatar(RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public InputStream retrieveGroupAvatar(GroupId groupId) { + return null; + } + + @Override + public InputStream retrieveSticker(StickerPackId stickerPackId, int stickerId) { + return null; + } + + @Override + public CallInfo startCall(RecipientIdentifier.Single recipient) { + return null; + } + + @Override + public CallInfo acceptCall(long callId) { + return null; + } + + @Override + public void hangupCall(long callId) { + } + + @Override + public SendMessageResult rejectCall(long callId) { + return null; + } + + @Override + public List listActiveCalls() { + return List.of(); + } + + @Override + public void sendCallOffer(RecipientIdentifier.Single recipient, CallOffer callOffer) { + } + + @Override + public void sendCallAnswer(RecipientIdentifier.Single recipient, long callId, byte[] answer) { + } + + @Override + public void sendIceUpdate(RecipientIdentifier.Single recipient, long callId, List iceCandidates) { + } + + @Override + public void sendHangup(RecipientIdentifier.Single recipient, long callId, MessageEnvelope.Call.Hangup.Type type) { + } + + @Override + public void sendBusy(RecipientIdentifier.Single recipient, long callId) { + } + + @Override + public List getTurnServerInfo() { + return List.of(); + } + + @Override + public void close() { + } + + @Override + public void addCallEventListener(CallEventListener listener) { + } + + @Override + public void removeCallEventListener(CallEventListener listener) { + } + } +} From 5f94b7b6d1406f77d35ab8e50dcd02a8807899eb Mon Sep 17 00:00:00 2001 From: Connor Lanigan Date: Tue, 5 May 2026 10:15:21 +0200 Subject: [PATCH 20/44] fix: Attempted immutable list modification causes runtime exception (#2038) --- .../jsonrpc/SignalJsonRpcDispatcherHandler.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java b/src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java index 14ff0076..166c822a 100644 --- a/src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java +++ b/src/main/java/org/asamk/signal/jsonrpc/SignalJsonRpcDispatcherHandler.java @@ -25,12 +25,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import java.util.stream.Collectors; public class SignalJsonRpcDispatcherHandler { @@ -41,8 +43,8 @@ public class SignalJsonRpcDispatcherHandler { private final JsonRpcReader jsonRpcReader; private final boolean noReceiveOnStart; - private final Map>> receiveHandlers = new HashMap<>(); - private final Map>> callEventHandlers = new HashMap<>(); + private final Map>> receiveHandlers = new HashMap<>(); + private final Map>> callEventHandlers = new HashMap<>(); private SignalJsonRpcCommandHandler commandHandler; public SignalJsonRpcDispatcherHandler( @@ -91,7 +93,9 @@ public class SignalJsonRpcDispatcherHandler { private int subscribeCallEvents(final Collection managers) { final var subscriptionId = nextSubscriptionId.getAndIncrement(); - final var listeners = managers.stream().map(m -> createCallEventHandler(m, subscriptionId)).toList(); + final var listeners = managers.stream() + .map(m -> createCallEventHandler(m, subscriptionId)) + .collect(Collectors.toCollection(ArrayList::new)); callEventHandlers.put(subscriptionId, listeners); return subscriptionId; } @@ -146,7 +150,7 @@ public class SignalJsonRpcDispatcherHandler { final var subscriptionId = nextSubscriptionId.getAndIncrement(); final var handlers = managers.stream() .map(m -> createReceiveHandler(m, subscriptionId, internalSubscription)) - .toList(); + .collect(Collectors.toCollection(ArrayList::new)); receiveHandlers.put(subscriptionId, handlers); return subscriptionId; From b4275414e17d53f202c310854765a141f9c75170 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 13 May 2026 17:39:44 +0200 Subject: [PATCH 21/44] Pass correct serviceId to SignalServiceCipher Fixes #2036 --- .../org/asamk/signal/manager/internal/SignalDependencies.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index d81078d4..78d658f4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -368,7 +368,8 @@ public class SignalDependencies { public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots()); - final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164()); + final var serviceId = serviceIdType == ServiceIdType.ACI ? credentialsProvider.getAci() : credentialsProvider.getPni(); + final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164()); final var deviceId = credentialsProvider.getDeviceId(); return new SignalServiceCipher(address, deviceId, From 4a3d9d90a6eb14335620d08b36e7b6742798ecba Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 13 May 2026 18:25:35 +0200 Subject: [PATCH 22/44] Update libsignal-service --- gradle/libs.versions.toml | 2 +- .../org/asamk/signal/manager/Manager.java | 2 +- .../signal/manager/api/MessageEnvelope.java | 4 +- .../helper/IncomingMessageHandler.java | 18 +++----- .../signal/manager/helper/ReceiveHelper.java | 13 +++--- .../signal/manager/internal/ManagerImpl.java | 44 +++++++++---------- .../asamk/signal/commands/SendCommand.java | 3 +- .../asamk/signal/dbus/DbusManagerImpl.java | 3 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 3 +- .../signal/http/SseInitialFlushTest.java | 3 +- .../jsonrpc/SubscribeCallEventsTest.java | 3 +- 11 files changed, 42 insertions(+), 56 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e952c6eb..4f788d43 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ slf4j = "2.0.17" junit = "6.0.3" micronaut-json-schema = "2.0.0-M8" micronaut-core = "4.9.3" -signal-service = "2.15.3_unofficial_145" +signal-service = "2.15.3_unofficial_146" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index fda3a323..dac629e7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -259,7 +259,7 @@ public interface Manager extends Closeable { RecipientIdentifier.Single recipient ) throws IOException; - SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; + void sendEndSessionMessage(Set recipients) throws IOException; SendMessageResults sendMessageRequestResponse( MessageEnvelope.Sync.MessageRequestResponse.Type type, diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index c8de254b..975e1639 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -157,7 +157,7 @@ public record MessageEnvelope( dataMessage.getExpiresInSeconds(), dataMessage.isExpirationUpdate(), dataMessage.isViewOnce(), - dataMessage.isEndSession(), + false, dataMessage.isProfileKeyUpdate(), dataMessage.getProfileKey().isPresent(), dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)), @@ -1028,7 +1028,7 @@ public record MessageEnvelope( final AttachmentFileProvider fileProvider, Exception exception ) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); final var source = !envelope.isUnidentifiedSender() && serviceId != null ? recipientResolver.resolveRecipient(serviceId) : envelope.isUnidentifiedSender() && content != null diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 92014fa3..4714b568 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -142,10 +142,11 @@ public final class IncomingMessageHandler { final var actions = new ArrayList(); SignalServiceContent content = null; Exception exception = null; - envelope.getSourceServiceId().map(ServiceId::parseOrNull) - // Store uuid if we don't have it already - // uuid in envelope is sent by server - .ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId)); + if (envelope.getSourceServiceId() != null) { + // Store uuid if we don't have it already + // uuid in envelope is sent by server + account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId()); + } if (!envelope.isReceipt()) { final var destination = getDestination(envelope).serviceId(); try { @@ -874,11 +875,6 @@ public final class IncomingMessageHandler { final var selfAddress = isSync ? source : destination; final var conversationPartnerAddress = isSync ? destination : source; - if (conversationPartnerAddress != null && message.isEndSession()) { - account.getAccountData(selfAddress.serviceId()) - .getSessionStore() - .deleteAllSessions(conversationPartnerAddress.serviceId()); - } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupContext().isPresent()) { final var groupContext = message.getGroupContext().get(); @@ -1047,7 +1043,7 @@ public final class IncomingMessageHandler { } private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); if (!envelope.isUnidentifiedSender() && serviceId != null) { return new SignalServiceAddress(serviceId); } else if (content != null) { @@ -1058,7 +1054,7 @@ public final class IncomingMessageHandler { } private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); if (!envelope.isUnidentifiedSender() && serviceId != null) { return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId), serviceId, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java index 73291d9e..31f1c04d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java @@ -9,7 +9,6 @@ import org.asamk.signal.manager.jobs.CleanOldPreKeysJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.RecipientAddress; -import org.signal.core.models.ServiceId; import org.signal.core.models.ServiceId.ACI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -150,10 +149,10 @@ public class ReceiveHelper { for (final var it : batch) { SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(), it.getServerDeliveredTimestamp()); - final var recipientId = envelope1.getSourceServiceId() - .map(ServiceId::parseOrNull) - .map(s -> account.getRecipientResolver().resolveRecipient(s)) - .orElse(null); + final var sourceServiceId = envelope1.getSourceServiceId(); + final var recipientId = sourceServiceId == null + ? null + : account.getRecipientResolver().resolveRecipient(sourceServiceId); logger.trace("Storing new message from {}", recipientId); // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); @@ -238,7 +237,7 @@ public class ReceiveHelper { if (exception instanceof UntrustedIdentityException) { logger.debug("Keeping message with untrusted identity in message cache"); final var address = ((UntrustedIdentityException) exception).getSender(); - if (envelope.getSourceServiceId().isEmpty() && address.aci().isPresent()) { + if (envelope.getSourceServiceId() == null && address.aci().isPresent()) { final var recipientId = account.getRecipientResolver() .resolveRecipient(ACI.parseOrThrow(address.aci().get())); try { @@ -292,7 +291,7 @@ public class ReceiveHelper { cachedMessage.delete(); return null; } - if (envelope.getSourceServiceId().isEmpty()) { + if (envelope.getSourceServiceId() == null) { final var identifier = ((UntrustedIdentityException) exception).getSender(); final var recipientId = account.getRecipientResolver() .resolveRecipient(new RecipientAddress(identifier)); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index c2aba751..394fa894 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -1091,30 +1091,26 @@ public class ManagerImpl implements Manager { } @Override - public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { - var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); - - try { - return sendMessage(messageBuilder, - recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()), - false); - } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { - throw new AssertionError(e); - } finally { - for (var recipient : recipients) { - final RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (UnregisteredRecipientException e) { - continue; - } - final var serviceId = context.getAccount() - .getRecipientAddressResolver() - .resolveRecipientAddress(recipientId) - .serviceId(); - if (serviceId.isPresent()) { - account.getAccountData(ServiceIdType.ACI).getSessionStore().deleteAllSessions(serviceId.get()); - } + public void sendEndSessionMessage(Set recipients) throws IOException { + for (var recipient : recipients) { + final RecipientId recipientId; + try { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (UnregisteredRecipientException e) { + continue; + } + final var recipientAddress = context.getAccount() + .getRecipientAddressResolver() + .resolveRecipientAddress(recipientId); + final var aciSessionStore = account.getAccountData(ServiceIdType.ACI).getSessionStore(); + final var pniSessionStore = account.getAccountData(ServiceIdType.PNI).getSessionStore(); + if (recipientAddress.aci().isPresent()) { + aciSessionStore.archiveSessions(recipientAddress.aci().get()); + pniSessionStore.archiveSessions(recipientAddress.aci().get()); + } + if (recipientAddress.pni().isPresent()) { + aciSessionStore.archiveSessions(recipientAddress.pni().get()); + pniSessionStore.archiveSessions(recipientAddress.pni().get()); } } } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index aa4bb957..c6ef5077 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -144,8 +144,7 @@ public class SendCommand implements JsonRpcLocalCommand { } try { - final var results = m.sendEndSessionMessage(singleRecipients); - outputResult(outputWriter, results); + m.sendEndSessionMessage(singleRecipients); return; } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index faf099e4..45b937ae 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -543,9 +543,8 @@ public class DbusManagerImpl implements Manager { } @Override - public SendMessageResults sendEndSessionMessage(final Set recipients) throws IOException { + public void sendEndSessionMessage(final Set recipients) throws IOException { signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList()); - return new SendMessageResults(0, Map.of()); } @Override diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 20069b9a..a9983caa 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -430,8 +430,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { @Override public void sendEndSessionMessage(final List recipients) { try { - final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); - checkSendMessageResults(results); + m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } diff --git a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java index 77e59dbd..4796ae5d 100644 --- a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java +++ b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java @@ -326,8 +326,7 @@ class SseInitialFlushTest { } @Override - public SendMessageResults sendEndSessionMessage(Set recipients) { - return null; + public void sendEndSessionMessage(Set recipients) { } @Override diff --git a/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java b/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java index 37c00829..bfc56ed3 100644 --- a/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java +++ b/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java @@ -328,8 +328,7 @@ class SubscribeCallEventsTest { } @Override - public SendMessageResults sendEndSessionMessage(Set r) { - return null; + public void sendEndSessionMessage(Set r) { } @Override From c10f618a3ef4e818ca47e7a884289c826af5b45d Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 13 May 2026 18:26:12 +0200 Subject: [PATCH 23/44] Update gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 48966 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++- gradlew.bat | 31 ++++++++--------------- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d997cfc60f4cff0e7451d19d49a82fa986695d07..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39760 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 e(*aw}>J`&Ep$5$o2mVP)i30-M3~u@DBh0 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 delta 40115 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 e{Sg5G5zENPNVU~b5#<2@M#Os+0czL&{q{eZe&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% From 0a1531dcce0a24c3e74d3ed95dbca48b0788fa43 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 13 May 2026 18:29:26 +0200 Subject: [PATCH 24/44] Improve destination/source checks for incoming messages --- .../helper/IncomingMessageHandler.java | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 4714b568..5069749c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -109,8 +109,8 @@ public final class IncomingMessageHandler { SignalServiceContent content = null; if (!envelope.isReceipt()) { account.getIdentityKeyStore().setRetryingDecryption(true); - final var destination = getDestination(envelope).serviceId(); try { + final var destination = getDestination(envelope).serviceId(); final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); @@ -140,6 +140,9 @@ public final class IncomingMessageHandler { final Manager.ReceiveMessageHandler handler ) { final var actions = new ArrayList(); + if (envelope.isPreKeySignalMessage()) { + actions.add(RefreshPreKeysAction.create()); + } SignalServiceContent content = null; Exception exception = null; if (envelope.getSourceServiceId() != null) { @@ -148,8 +151,19 @@ public final class IncomingMessageHandler { account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId()); } if (!envelope.isReceipt()) { - final var destination = getDestination(envelope).serviceId(); try { + final var destination = getDestination(envelope).serviceId(); + + if (destination == account.getPni() && envelope.getSourceServiceId() == null) { + throw new InvalidMessageException( + "Got a sealed sender message to our PNI? Invalid message, ignoring."); + } + + if (envelope.getSourceServiceId() instanceof ServiceId.PNI + && envelope.getProto().type != Envelope.Type.SERVER_DELIVERY_RECEIPT) { + throw new InvalidMessageException("Got a message from a PNI that was not a SERVER_DELIVERY_RECEIPT."); + } + final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); @@ -174,7 +188,13 @@ public final class IncomingMessageHandler { logger.debug("Received invalid message from blocked contact, ignoring."); } else { var serviceId = ServiceId.parseOrNull(e.getSender()); - if (serviceId != null) { + ServiceId destination; + try { + destination = getDestination(envelope).serviceId(); + } catch (InvalidMessageException ex) { + destination = null; + } + if (serviceId != null && destination != null) { final var isSelf = sender.equals(account.getSelfRecipientId()) && e.getSenderDevice() == account.getDeviceId(); logger.debug("Received invalid message, queuing renew session action."); @@ -312,7 +332,12 @@ public final class IncomingMessageHandler { final var sender = senderDeviceAddress.recipientId(); final var senderServiceId = senderDeviceAddress.serviceId(); final var senderDeviceId = senderDeviceAddress.deviceId(); - final var destination = getDestination(envelope); + final DeviceAddress destination; + try { + destination = getDestination(envelope); + } catch (InvalidMessageException e) { + throw new AssertionError(e); + } if (account.getPni().equals(destination.serviceId)) { account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true); @@ -1066,10 +1091,13 @@ public final class IncomingMessageHandler { } } - private DeviceAddress getDestination(SignalServiceEnvelope envelope) { + private DeviceAddress getDestination(SignalServiceEnvelope envelope) throws InvalidMessageException { final var destination = envelope.getDestinationServiceId(); if (destination == null || destination.isUnknown()) { - return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId()); + throw new InvalidMessageException("Missing destination"); + } + if (!account.getAci().equals(destination) && !account.getPni().equals(destination)) { + throw new InvalidMessageException("Message not intended for this account"); } return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination), destination, From 9c8137fafa451177fd6702feaf31b1d71b94c555 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 May 2026 17:06:21 +0200 Subject: [PATCH 25/44] Update dependencies --- .github/workflows/build.yml | 2 +- gradle/libs.versions.toml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aaac4e07..5a4cf4d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: matrix: # java="25" is the LTS Java version used in reproducible builds script (default in Containerfile). # More Java versions can be added to test compatibility, eg. "26". - java: ["25"] + java: ["25", "26"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f788d43..e36c07bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -slf4j = "2.0.17" +slf4j = "2.0.18" junit = "6.0.3" -micronaut-json-schema = "2.0.0-M8" -micronaut-core = "4.9.3" +micronaut-json-schema = "2.0.0" +micronaut-core = "5.0.0" signal-service = "2.15.3_unofficial_146" [libraries] @@ -20,7 +20,7 @@ slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.32" signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" } -sqlite = "org.xerial:sqlite-jdbc:3.53.0.0" +sqlite = "org.xerial:sqlite-jdbc:3.53.1.0" hikari = "com.zaxxer:HikariCP:7.0.2" junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } From fcf82b9318b44bdd3bc8ccc2e3b2109d2ea620ee Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 May 2026 10:48:12 +0200 Subject: [PATCH 26/44] Adapt containerfile to older apt versions --- reproducible-builds/build.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reproducible-builds/build.Containerfile b/reproducible-builds/build.Containerfile index 38626266..620a0a35 100644 --- a/reproducible-builds/build.Containerfile +++ b/reproducible-builds/build.Containerfile @@ -6,7 +6,7 @@ ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \ - && apt install -y make asciidoc-base --update --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests + && apt update && apt install -y make asciidoc-base --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh WORKDIR /signal-cli ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ] From 4601e601182c466d60a6f8c88d4bbd0150b6bf9d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 May 2026 11:14:17 +0200 Subject: [PATCH 27/44] Adapt containerfile to older apt versions --- reproducible-builds/build.Containerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reproducible-builds/build.Containerfile b/reproducible-builds/build.Containerfile index 620a0a35..3ae4902c 100644 --- a/reproducible-builds/build.Containerfile +++ b/reproducible-builds/build.Containerfile @@ -6,7 +6,7 @@ ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \ - && apt update && apt install -y make asciidoc-base --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests + && sed -i 's/^deb /deb [snapshot=yes] /' /etc/apt/sources.list && apt update --snapshot "$SNAPSHOT" && apt install -y make asciidoc-base --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh WORKDIR /signal-cli ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ] From 6da5c375040efc21a3dc7ccf916d7690de028eb7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 13:50:06 +0200 Subject: [PATCH 28/44] Prevent attaching files from the signal-cli data directory --- .../signal/manager/helper/AttachmentHelper.java | 17 +++++++++++++++++ .../signal/manager/storage/SignalAccount.java | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index afafeb24..e863be05 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -34,8 +34,10 @@ public class AttachmentHelper { private final SignalDependencies dependencies; private final AttachmentStore attachmentStore; + private final Context context; public AttachmentHelper(final Context context) { + this.context = context; this.dependencies = context.getDependencies(); this.attachmentStore = context.getAttachmentStore(); } @@ -92,6 +94,21 @@ public class AttachmentHelper { final boolean voiceNote ) throws AttachmentInvalidException { try { + // Reject local files that point into the signal-cli data directory + if (attachment != null && !attachment.startsWith("data:")) { + try { + final var file = new File(attachment); + final var canonical = file.getCanonicalFile(); + final var dataPath = context.getAccount().getDataPath().getCanonicalFile(); + if (canonical.toPath().startsWith(dataPath.toPath())) { + throw new AttachmentInvalidException(attachment, + new IOException("Attaching files from the signal-cli data directory is not allowed")); + } + } catch (IOException e) { + throw new AttachmentInvalidException(attachment, e); + } + } + final var streamDetailsAndFileName = Utils.createStreamDetails(attachment); final var streamDetails = streamDetailsAndFileName.first(); final var uploadSpec = getResumableUploadSpec(streamDetails); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 35f600b6..c7a5c6d8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -192,6 +192,10 @@ public class SignalAccount implements Closeable { this.lock = lock; } + public File getDataPath() { + return dataPath; + } + public static SignalAccount load( File dataPath, String accountPath, From 46ce552589da95264175eecc17a816a447d0ef42 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 14:17:28 +0200 Subject: [PATCH 29/44] Normalize attachment ids --- .../signal/manager/storage/AttachmentStore.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java index d25f1a72..00a284dd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/AttachmentStore.java @@ -44,7 +44,8 @@ public class AttachmentStore { } public StreamDetails retrieveAttachment(final String id) throws IOException { - final var attachmentFile = new File(attachmentsPath, id); + final var safeId = sanitizeId(id); + final var attachmentFile = new File(attachmentsPath, safeId); return Utils.createStreamDetailsFromFile(attachmentFile); } @@ -61,7 +62,8 @@ public class AttachmentStore { Optional contentType ) { final var extension = getAttachmentExtension(filename, contentType); - return new File(attachmentsPath, attachmentId.toString() + extension + ".preview"); + final var safe = sanitizeId(attachmentId.toString()); + return new File(attachmentsPath, safe + extension + ".preview"); } private File getAttachmentFile( @@ -70,7 +72,15 @@ public class AttachmentStore { Optional contentType ) { final var extension = getAttachmentExtension(filename, contentType); - return new File(attachmentsPath, attachmentId.toString() + extension); + final var safe = sanitizeId(attachmentId.toString()); + return new File(attachmentsPath, safe + extension); + } + + private static String sanitizeId(final String id) { + if (id == null) { + return ""; + } + return id.replaceAll("[^A-Za-z0-9_.-]", "_"); } private static String getAttachmentExtension(final Optional filename, final Optional contentType) { From f34b5520545510b60262bceb3187c73b1338ccf2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 14:21:25 +0200 Subject: [PATCH 30/44] Validate host header for http daemon --- .../asamk/signal/http/HttpServerHandler.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java index 325b0eec..551c5b04 100644 --- a/src/main/java/org/asamk/signal/http/HttpServerHandler.java +++ b/src/main/java/org/asamk/signal/http/HttpServerHandler.java @@ -19,8 +19,11 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,12 +40,14 @@ public class HttpServerHandler implements AutoCloseable { private final Manager m; private HttpServer server; private final AtomicBoolean shutdown = new AtomicBoolean(false); + private final Set allowedHosts; public HttpServerHandler(final InetSocketAddress address, final Manager m) { this.address = address; commandHandler = new SignalJsonRpcCommandHandler(m, Commands::getCommand); this.c = null; this.m = m; + this.allowedHosts = buildAllowedHosts(address); } public HttpServerHandler(final InetSocketAddress address, final MultiAccountManager c) { @@ -50,6 +55,7 @@ public class HttpServerHandler implements AutoCloseable { commandHandler = new SignalJsonRpcCommandHandler(c, Commands::getCommand); this.c = c; this.m = null; + this.allowedHosts = buildAllowedHosts(address); } public void init() throws IOException { @@ -67,6 +73,7 @@ public class HttpServerHandler implements AutoCloseable { server.start(); logger.info("Started HTTP server on {}", address); + logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts); } @Override @@ -99,6 +106,12 @@ public class HttpServerHandler implements AutoCloseable { } private void handleRpcEndpoint(HttpExchange httpExchange) throws IOException { + if (!isHostAllowed(httpExchange)) { + logger.warn("Rejected RPC request with invalid Host header: {} from {}", + httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress()); + sendResponse(421, null, httpExchange); + return; + } if (!"/api/v1/rpc".equals(httpExchange.getRequestURI().getPath())) { sendResponse(404, null, httpExchange); return; @@ -146,6 +159,12 @@ public class HttpServerHandler implements AutoCloseable { } private void handleEventsEndpoint(HttpExchange httpExchange) throws IOException { + if (!isHostAllowed(httpExchange)) { + logger.warn("Rejected Events request with invalid Host header: {} from {}", + httpExchange.getRequestHeaders().getFirst("Host"), httpExchange.getRemoteAddress()); + sendResponse(421, null, httpExchange); + return; + } if (!"/api/v1/events".equals(httpExchange.getRequestURI().getPath())) { sendResponse(404, null, httpExchange); return; @@ -273,4 +292,59 @@ public class HttpServerHandler implements AutoCloseable { void call(); } + + private Set buildAllowedHosts(final InetSocketAddress address) { + final var s = new HashSet(); + final var host = address == null ? null : address.getHostString(); + if (host != null && !host.isEmpty()) { + s.add(host.toLowerCase(Locale.ROOT)); + } + s.add("localhost"); + s.add("127.0.0.1"); + s.add("::1"); + return s; + } + + private boolean isHostAllowed(final HttpExchange httpExchange) { + final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host"); + if (hostHeader == null || hostHeader.isEmpty()) { + return false; + } + + String hostPart = hostHeader; + String portPart = null; + if (hostHeader.startsWith("[")) { + final var idx = hostHeader.indexOf(']'); + if (idx == -1) return false; + hostPart = hostHeader.substring(1, idx); + if (hostHeader.length() > idx + 1 && hostHeader.charAt(idx + 1) == ':') { + portPart = hostHeader.substring(idx + 2); + } + } else { + final var colon = hostHeader.lastIndexOf(':'); + if (colon != -1) { + final var possiblePort = hostHeader.substring(colon + 1); + if (possiblePort.chars().allMatch(Character::isDigit)) { + hostPart = hostHeader.substring(0, colon); + portPart = possiblePort; + } + } + } + + hostPart = hostPart.toLowerCase(Locale.ROOT); + if (!allowedHosts.contains(hostPart)) { + return false; + } + + if (portPart != null) { + try { + final var port = Integer.parseInt(portPart); + if (port != address.getPort()) return false; + } catch (NumberFormatException e) { + return false; + } + } + + return true; + } } From 393e1efcd1633d31d450212bf26ebfae77dc1cf1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 14:30:33 +0200 Subject: [PATCH 31/44] Create temp file with limited permissions --- .../org/asamk/signal/manager/util/IOUtils.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java index 65d3b1d8..0061e0d9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java @@ -21,9 +21,19 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; public class IOUtils { public static File createTempFile() throws IOException { - final var tempFile = File.createTempFile("signal-cli_tmp_", ".tmp"); - tempFile.deleteOnExit(); - return tempFile; + final var prefix = "signal-cli_tmp_"; + final var suffix = ".tmp"; + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); + var path = Files.createTempFile(prefix, suffix, PosixFilePermissions.asFileAttribute(perms)); + var tempFile = path.toFile(); + tempFile.deleteOnExit(); + return tempFile; + } catch (UnsupportedOperationException e) { + final var tempFile = File.createTempFile(prefix, suffix); + tempFile.deleteOnExit(); + return tempFile; + } } public static byte[] readFully(InputStream in) throws IOException { From 44d54b32157742fda0c09bc56937f456fa1329fe Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 16:35:22 +0200 Subject: [PATCH 32/44] Add support for a global configuration file --- man/signal-cli-dbus.5.adoc | 2 +- man/signal-cli-jsonrpc.5.adoc | 4 +- man/signal-cli.1.adoc | 23 +++++- src/main/java/org/asamk/signal/App.java | 55 ++++++++----- .../java/org/asamk/signal/ConfigLoader.java | 81 +++++++++++++++++++ .../java/org/asamk/signal/GlobalConfig.java | 36 +++++++++ src/main/java/org/asamk/signal/Main.java | 64 ++++++++++----- .../java/org/asamk/signal/OutputType.java | 15 +++- .../asamk/signal/ServiceEnvironmentCli.java | 15 +++- .../org/asamk/signal/TrustNewIdentityCli.java | 16 +++- .../signal-cli/reachability-metadata.json | 55 +++++++++++++ 11 files changed, 315 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/asamk/signal/ConfigLoader.java create mode 100644 src/main/java/org/asamk/signal/GlobalConfig.java diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 732efe43..9246e9ed 100644 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -13,7 +13,7 @@ signal-cli-dbus - A commandline and dbus interface for the Signal messenger == Synopsis -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system] +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] [-o {plain-text,json}] daemon [--dbus] [--dbus-system] *dbus-send* [--system | --session] [--print-reply] --type=method_call --dest="org.asamk.Signal" /org/asamk/Signal[/_] org.asamk.Signal. [string:] [array::] diff --git a/man/signal-cli-jsonrpc.5.adoc b/man/signal-cli-jsonrpc.5.adoc index 1c035af8..b9abb8ce 100644 --- a/man/signal-cli-jsonrpc.5.adoc +++ b/man/signal-cli-jsonrpc.5.adoc @@ -13,9 +13,9 @@ signal-cli-jsonrpc - A commandline and dbus interface for the Signal messenger == Synopsis -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]] +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] daemon [--socket[=SOCKET_PATH]] [--tcp[=HOST:PORT]] [--http[=HOST:PORT]] -*signal-cli* [--verbose] [--config CONFIG] [-a ACCOUNT] jsonRpc +*signal-cli* [--verbose] [--data-dir DATA_DIR] [-a ACCOUNT] jsonRpc == Description diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index e349a2ad..f6ed732d 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -13,7 +13,7 @@ signal-cli - A commandline interface for the Signal messenger == Synopsis -*signal-cli* [--config CONFIG] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options] +*signal-cli* [--data-dir DATA_DIR] [-h | -v | -a ACCOUNT | --dbus | --dbus-system] command [command-options] == Description @@ -57,8 +57,8 @@ If `--verbose` is also given, the detailed logs will only be written to the log Scrub possibly sensitive information from the log, like phone numbers and UUIDs. Doesn't work reliably on dbus logs with very verbose logging (`-vvv`) -*--config* CONFIG:: -Set the path, where to store the config. +*-d* DATA_DIR, *--data-dir* DATA_DIR, *-c* CONFIG, *--config* CONFIG:: +Set the path where to store account data and local configuration. Make sure you have full read/write access to the given directory. (Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) @@ -1172,10 +1172,25 @@ signal-cli -a ACCOUNT trust -a RECIPIENT == Files -The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*: +The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--data-dir* (legacy *--config*): `$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`) +=== Configuration file + +signal-cli supports a JSON-based global configuration file that provides defaults for CLI options. +Keys use camelCase and generally match the long CLI parameter names (for example `dataDir`, `verbose`, `logFile`, `serviceEnvironment`, `trustNewIdentities`, `output`, `disableSendLog`, `account`). + +Configuration files are read and merged in this order; later files override earlier ones: + +- `/etc/signal-cli/config.json` (system-wide defaults) +- the path in the `SIGNAL_CLI_CONFIG` environment variable (if set) +- `$XDG_CONFIG_HOME/signal-cli/config.json` (per-user; defaults to `$HOME/.config/signal-cli/config.json`) + +When multiple configuration files are present their settings are merged; values from later files override earlier values. +Command-line options always take precedence over configuration file values. +Overall precedence (highest → lowest): command-line options → per-user config → system config → built-in defaults. + == Authors Maintained by AsamK , who is assisted by other open source contributors. diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index bcf3c605..d29cc2f8 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -46,7 +46,9 @@ public class App { private final Namespace ns; - static ArgumentParser buildArgumentParser() { + static ArgumentParser buildArgumentParser(GlobalConfig config) { + final var cfg = config == null ? GlobalConfig.DEFAULT : config; + var parser = ArgumentParsers.newFor("signal-cli", VERSION_0_9_0_DEFAULT_SETTINGS) .includeArgumentNamesAsKeysInResult(true) .build() @@ -57,47 +59,60 @@ public class App { parser.addArgument("--version").help("Show package version.").action(Arguments.version()); parser.addArgument("-v", "--verbose") .help("Raise log level and include lib signal logs. Specify multiple times for even more logs.") - .action(Arguments.count()); + .action(Arguments.count()) + .setDefault(cfg.verbose()); parser.addArgument("--log-file") .type(File.class) - .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file."); + .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file.") + .setDefault(cfg.logFile() == null ? null : new File(cfg.logFile())); parser.addArgument("--scrub-log") .action(Arguments.storeTrue()) - .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs."); - parser.addArgument("-c", "--config") - .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); + .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.") + .setDefault(cfg.scrubLog()); + parser.addArgument("-d", "--data-dir", "-c", "--config") + .help("Set the path where to store data (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).") + .setDefault(cfg.dataDir()); parser.addArgument("-a", "--account", "-u", "--username") .help("Specify your phone number, that will be your identifier."); var mut = parser.addMutuallyExclusiveGroup(); - mut.addArgument("--dbus").dest("global-dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); + mut.addArgument("--dbus") + .dest("global-dbus") + .help("Make request via user dbus.") + .action(Arguments.storeTrue()) + .setDefault(cfg.dbus()); mut.addArgument("--dbus-system") .dest("global-dbus-system") .help("Make request via system dbus.") - .action(Arguments.storeTrue()); + .action(Arguments.storeTrue()) + .setDefault(cfg.dbusSystem()); parser.addArgument("--bus-name") .dest("global-bus-name") - .setDefault(DbusConfig.getBusname()) + .setDefault(cfg.busName() != null ? cfg.busName() : DbusConfig.getBusname()) .help("Specify the D-Bus bus name to connect to."); parser.addArgument("-o", "--output") .help("Choose to output in plain text or JSON") - .type(Arguments.enumStringType(OutputType.class)); + .type(Arguments.enumStringType(OutputType.class)) + .setDefault(cfg.output() == null ? null : cfg.output()); parser.addArgument("--service-environment") .help("Choose the server environment to use.") .type(Arguments.enumStringType(ServiceEnvironmentCli.class)) - .setDefault(ServiceEnvironmentCli.LIVE); + .setDefault(cfg.serviceEnvironment() != null ? cfg.serviceEnvironment() : ServiceEnvironmentCli.LIVE); parser.addArgument("--trust-new-identities") .help("Choose when to trust new identities.") .type(Arguments.enumStringType(TrustNewIdentityCli.class)) - .setDefault(TrustNewIdentityCli.ON_FIRST_USE); + .setDefault(cfg.trustNewIdentities() != null + ? cfg.trustNewIdentities() + : TrustNewIdentityCli.ON_FIRST_USE); parser.addArgument("--disable-send-log") .help("Disable message send log (for resending messages that recipient couldn't decrypt)") - .action(Arguments.storeTrue()); + .action(Arguments.storeTrue()) + .setDefault(cfg.disableSendLog()); parser.epilog( "The global arguments are shown with 'signal-cli -h' and need to come before the subcommand, while the subcommand-specific arguments (shown with 'signal-cli SUBCOMMAND -h') need to be given after the subcommand."); @@ -219,12 +234,12 @@ public class App { } private SignalAccountFiles loadSignalAccountFiles() throws IOErrorException { - final File configPath; - final var config = ns.getString("config"); - if (config != null) { - configPath = new File(config); + final File dataPath; + final var dataDir = ns.getString("data-dir"); + if (dataDir != null) { + dataPath = new File(dataDir); } else { - configPath = getDefaultConfigPath(); + dataPath = getDefaultDataPath(); } final var serviceEnvironmentCli = ns.get("service-environment"); @@ -240,7 +255,7 @@ public class App { final var disableSendLog = Boolean.TRUE.equals(ns.getBoolean("disable-send-log")); try { - return new SignalAccountFiles(configPath, + return new SignalAccountFiles(dataPath, serviceEnvironment, BaseConfig.USER_AGENT, new Settings(trustNewIdentity, disableSendLog)); @@ -339,7 +354,7 @@ public class App { /** * @return the default data directory to be used by signal-cli. */ - private static File getDefaultConfigPath() { + private static File getDefaultDataPath() { return new File(IOUtils.getDataHomeDir(), "signal-cli"); } } diff --git a/src/main/java/org/asamk/signal/ConfigLoader.java b/src/main/java/org/asamk/signal/ConfigLoader.java new file mode 100644 index 00000000..861de2e0 --- /dev/null +++ b/src/main/java/org/asamk/signal/ConfigLoader.java @@ -0,0 +1,81 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.asamk.signal.commands.exceptions.UserErrorException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Loads and merges configuration files. Merge order (later files override earlier): + * - /etc/signal-cli/config.json + * - file pointed to by SIGNAL_CLI_CONFIG (if set) + * - $XDG_CONFIG_HOME/signal-cli/config.json or $HOME/.config/signal-cli/config.json + */ +public final class ConfigLoader { + + private ConfigLoader() { + } + + public static GlobalConfig load() throws UserErrorException { + final ObjectMapper mapper = new ObjectMapper(); + final ObjectNode merged = mapper.createObjectNode(); + + // System config + addIfExists(merged, mapper, Paths.get("/etc/signal-cli/config.json")); + + // User config via env (if set) else XDG or ~/.config + final String env = System.getenv("SIGNAL_CLI_CONFIG"); + if (env != null && !env.isEmpty()) { + addIfExists(merged, mapper, Paths.get(env)); + } else { + final String xdg = System.getenv("XDG_CONFIG_HOME"); + if (xdg != null && !xdg.isEmpty()) { + addIfExists(merged, mapper, Paths.get(xdg, "signal-cli", "config.json")); + } else { + addIfExists(merged, + mapper, + Paths.get(System.getProperty("user.home"), ".config", "signal-cli", "config.json")); + } + } + + try { + if (merged.isEmpty()) { + return GlobalConfig.DEFAULT; + } + return mapper.treeToValue(merged, GlobalConfig.class); + } catch (Exception e) { + throw new UserErrorException("Failed to parse configuration file(s): " + e.getMessage(), e); + } + } + + private static void addIfExists(ObjectNode merged, ObjectMapper mapper, Path p) throws UserErrorException { + if (p == null) return; + try { + if (Files.exists(p)) { + final JsonNode node = mapper.readTree(p.toFile()); + merge(merged, node); + } + } catch (IOException e) { + throw new UserErrorException("Failed to load config from " + p + ": " + e.getMessage(), e); + } + } + + private static void merge(ObjectNode target, JsonNode source) { + source.properties().forEach(entry -> { + final String name = entry.getKey(); + final JsonNode value = entry.getValue(); + final JsonNode existing = target.get(name); + if (existing != null && existing.isObject() && value.isObject()) { + merge((ObjectNode) existing, value); + } else { + target.set(name, value); + } + }); + } +} diff --git a/src/main/java/org/asamk/signal/GlobalConfig.java b/src/main/java/org/asamk/signal/GlobalConfig.java new file mode 100644 index 00000000..1205df7c --- /dev/null +++ b/src/main/java/org/asamk/signal/GlobalConfig.java @@ -0,0 +1,36 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record GlobalConfig( + @JsonProperty("verbose") Integer verbose, + @JsonProperty("logFile") String logFile, + @JsonProperty("scrubLog") Boolean scrubLog, + @JsonProperty("dataDir") String dataDir, + @JsonProperty("busName") String busName, + @JsonProperty("dbus") Boolean dbus, + @JsonProperty("dbusSystem") Boolean dbusSystem, + @JsonProperty("output") OutputType output, + @JsonProperty("serviceEnvironment") ServiceEnvironmentCli serviceEnvironment, + @JsonProperty("trustNewIdentities") TrustNewIdentityCli trustNewIdentities, + @JsonProperty("disableSendLog") Boolean disableSendLog, + @JsonProperty("account") String account +) { + + public static final GlobalConfig DEFAULT = new GlobalConfig(null, + null, + null, + null, + null, + null, + null, + null, + ServiceEnvironmentCli.LIVE, + TrustNewIdentityCli.ON_FIRST_USE, + null, + null); + + public static GlobalConfig empty() { + return new GlobalConfig(null, null, null, null, null, null, null, null, null, null, null, null); + } +} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 568f58c6..800b0ee6 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -40,27 +40,32 @@ import java.security.Security; public class Main { - public static void main(String[] args) { + static void main(String[] args) { // enable unlimited strength crypto via Policy, supported on relevant JREs Security.setProperty("crypto.policy", "unlimited"); installSecurityProviderWorkaround(); + // Load global config early so we can use its values as parser defaults + final GlobalConfig globalConfig; + try { + globalConfig = ConfigLoader.load(); + } catch (UserErrorException e) { + System.exit(handleCommandException(e, null)); + return; + } + // Configuring the logger needs to happen before any logger is initialized - final var loggingConfig = parseLoggingConfig(args); + final var loggingConfig = parseLoggingConfig(args, globalConfig); configureLogging(loggingConfig); - final var parser = App.buildArgumentParser(); + final var parser = App.buildArgumentParser(globalConfig); final var ns = parser.parseArgsOrFail(args); int status = 0; try { new App(ns).init(); } catch (CommandException e) { - System.err.println(e.getMessage()); - if (loggingConfig.verboseLevel > 0 && e.getCause() != null) { - e.getCause().printStackTrace(System.err); - } - status = getStatusForError(e); + status = handleCommandException(e, loggingConfig); } catch (Throwable e) { e.printStackTrace(System.err); status = 2; @@ -69,16 +74,27 @@ public class Main { System.exit(status); } + private static int handleCommandException(final CommandException e, final LoggingConfig loggingConfig) { + System.err.println(e.getMessage()); + if (loggingConfig != null && loggingConfig.verboseLevel > 0 && e.getCause() != null) { + e.getCause().printStackTrace(System.err); + } + return getStatusForError(e); + } + private static void installSecurityProviderWorkaround() { // Register our own security provider Security.insertProviderAt(new SecurityProvider(), 1); Security.addProvider(new BouncyCastleProvider()); } - private static LoggingConfig parseLoggingConfig(final String[] args) { - final var nsLog = parseArgs(args); + private static LoggingConfig parseLoggingConfig(final String[] args, final GlobalConfig config) { + final var nsLog = parseArgs(args, config); if (nsLog == null) { - return new LoggingConfig(0, null, false); + final var verbose = config != null && config.verbose() != null ? config.verbose() : 0; + final var logFile = config != null && config.logFile() != null ? new File(config.logFile()) : null; + final var scrubLog = config != null && Boolean.TRUE.equals(config.scrubLog()); + return new LoggingConfig(verbose, logFile, scrubLog); } final var verboseLevel = nsLog.getInt("verbose"); @@ -90,14 +106,20 @@ public class Main { /** * This method only parses commandline args relevant for logging configuration. */ - private static Namespace parseArgs(String[] args) { + private static Namespace parseArgs(String[] args, final GlobalConfig config) { var parser = ArgumentParsers.newFor("signal-cli", DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS) .includeArgumentNamesAsKeysInResult(true) .build() .defaultHelp(false); - parser.addArgument("-v", "--verbose").action(Arguments.count()); - parser.addArgument("--log-file").type(File.class); - parser.addArgument("--scrub-log").action(Arguments.storeTrue()); + parser.addArgument("-v", "--verbose") + .action(Arguments.count()) + .setDefault(config == null ? null : config.verbose()); + parser.addArgument("--log-file") + .type(File.class) + .setDefault(config == null || config.logFile() == null ? null : new File(config.logFile())); + parser.addArgument("--scrub-log") + .action(Arguments.storeTrue()) + .setDefault(config == null ? null : config.scrubLog()); try { return parser.parseKnownArgs(args, null); @@ -124,12 +146,12 @@ public class Main { private static int getStatusForError(final CommandException e) { return switch (e) { - case UserErrorException userErrorException -> 1; - case UnexpectedErrorException unexpectedErrorException -> 2; - case IOErrorException ioErrorException -> 3; - case UntrustedKeyErrorException untrustedKeyErrorException -> 4; - case RateLimitErrorException rateLimitErrorException -> 5; - case CaptchaRejectedErrorException captchaRejectedErrorException -> 6; + case UserErrorException _ -> 1; + case UnexpectedErrorException _ -> 2; + case IOErrorException _ -> 3; + case UntrustedKeyErrorException _ -> 4; + case RateLimitErrorException _ -> 5; + case CaptchaRejectedErrorException _ -> 6; case null -> 2; }; } diff --git a/src/main/java/org/asamk/signal/OutputType.java b/src/main/java/org/asamk/signal/OutputType.java index 383d635f..2da6b5db 100644 --- a/src/main/java/org/asamk/signal/OutputType.java +++ b/src/main/java/org/asamk/signal/OutputType.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum OutputType { PLAIN_TEXT { @Override @@ -12,5 +14,16 @@ public enum OutputType { public String toString() { return "json"; } - }, + }; + + @JsonCreator + public static OutputType fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", ""); + return switch (norm) { + case "plaintext" -> PLAIN_TEXT; + case "json" -> JSON; + default -> throw new IllegalArgumentException("Invalid output type: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java b/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java index 9010f3f8..f61a6833 100644 --- a/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java +++ b/src/main/java/org/asamk/signal/ServiceEnvironmentCli.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum ServiceEnvironmentCli { LIVE { @Override @@ -12,5 +14,16 @@ public enum ServiceEnvironmentCli { public String toString() { return "staging"; } - }, + }; + + @JsonCreator + public static ServiceEnvironmentCli fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase(); + return switch (norm) { + case "live" -> LIVE; + case "staging" -> STAGING; + default -> throw new IllegalArgumentException("Invalid service-environment: " + value); + }; + } } diff --git a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java index 5cc36bbd..665ab90b 100644 --- a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java +++ b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonCreator; + public enum TrustNewIdentityCli { ALWAYS { @Override @@ -18,5 +20,17 @@ public enum TrustNewIdentityCli { public String toString() { return "never"; } - }, + }; + + @JsonCreator + public static TrustNewIdentityCli fromString(String value) { + if (value == null) return null; + final var norm = value.trim().toLowerCase().replaceAll("[^a-z0-9]", ""); + return switch (norm) { + case "always" -> ALWAYS; + case "onfirstuse" -> ON_FIRST_USE; + case "never" -> NEVER; + default -> throw new IllegalArgumentException("Invalid trust-new-identities: " + value); + }; + } } diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index 4513328b..cb971b26 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1317,6 +1317,14 @@ } ] }, + { + "type": "java.util.concurrent.CopyOnWriteArrayList", + "fields": [ + { + "name": "lock" + } + ] + }, { "type": "java.util.concurrent.ForkJoinTask", "fields": [ @@ -1979,6 +1987,28 @@ { "type": "org.asamk.SignalControl.Error.InvalidNumber" }, + { + "type": "org.asamk.signal.GlobalConfig", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.Integer", + "java.lang.String", + "java.lang.Boolean", + "java.lang.String", + "java.lang.String", + "java.lang.Boolean", + "java.lang.Boolean", + "org.asamk.signal.OutputType", + "org.asamk.signal.ServiceEnvironmentCli", + "org.asamk.signal.TrustNewIdentityCli", + "java.lang.Boolean", + "java.lang.String" + ] + } + ] + }, { "type": "org.asamk.signal.Main", "jniAccessible": true, @@ -1995,6 +2025,31 @@ } ] }, + { + "type": "org.asamk.signal.OutputType" + }, + { + "type": "org.asamk.signal.ServiceEnvironmentCli", + "methods": [ + { + "name": "fromString", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, + { + "type": "org.asamk.signal.TrustNewIdentityCli", + "methods": [ + { + "name": "fromString", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, { "type": "org.asamk.signal.commands.AcceptCallCommand$JsonCallInfo", "allDeclaredFields": true, From ced95600402b819da4704272a196b5f190928f6f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 18:46:39 +0200 Subject: [PATCH 33/44] Update libsignal-service --- gradle/libs.versions.toml | 2 +- .../signal/manager/api/MessageEnvelope.java | 1 - .../signal/manager/helper/AccountHelper.java | 2 +- .../manager/helper/AttachmentHelper.java | 2 +- .../signal/manager/helper/GroupV2Helper.java | 2 +- .../signal/manager/helper/PreKeyHelper.java | 7 +-- .../signal/manager/helper/ProfileHelper.java | 2 +- .../manager/helper/RecipientHelper.java | 2 +- .../signal/manager/helper/SyncHelper.java | 2 +- .../signal/manager/internal/ManagerImpl.java | 3 +- .../internal/RegistrationManagerImpl.java | 1 + .../manager/internal/SignalDependencies.java | 46 ++++++++++++++++--- .../SignalWebSocketHealthMonitor.java | 8 +++- .../storage/sessions/SessionStore.java | 2 +- .../manager/util/NumberVerificationUtils.java | 2 +- .../org/asamk/signal/manager/util/Utils.java | 2 +- .../signal-cli/reachability-metadata.json | 27 +++++++++++ 17 files changed, 90 insertions(+), 23 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e36c07bb..972d5595 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ slf4j = "2.0.18" junit = "6.0.3" micronaut-json-schema = "2.0.0" micronaut-core = "5.0.0" -signal-service = "2.15.3_unofficial_146" +signal-service = "2.15.3_unofficial_147" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index 975e1639..34b2274d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -4,7 +4,6 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.helper.RecipientAddressResolver; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.MimeUtils; -import org.signal.core.models.ServiceId; import org.signal.libsignal.metadata.ProtocolException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java index 2396b36e..a4f037f1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -25,6 +25,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.util.KeyHelper; import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; @@ -37,7 +38,6 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException; import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index e863be05..1fefcb24 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -126,7 +126,7 @@ public class AttachmentHelper { final var streamLength = streamDetails.getLength(); final var ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize( streamLength)); - return dependencies.getMessageSender().getResumableUploadSpec(ciphertextLength); + return dependencies.getCdnService().getResumableUploadSpecBlocking(ciphertextLength); } public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index e2bc2a82..d4df661e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -21,6 +21,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.signal.storageservice.storage.protos.groups.AccessControl; import org.signal.storageservice.storage.protos.groups.GroupChange; import org.signal.storageservice.storage.protos.groups.GroupChangeResponse; @@ -43,7 +44,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException; import java.io.IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java index bf7ad580..18a7b165 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java @@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.InvalidKeyIdException; import org.signal.libsignal.protocol.state.KyberPreKeyRecord; import org.signal.libsignal.protocol.state.PreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.NetworkResultUtil; @@ -16,7 +17,6 @@ import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.IOException; import java.util.List; @@ -84,7 +84,8 @@ public class PreKeyHelper { ) throws IOException { OneTimePreKeyCounts preKeyCounts; try { - preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType)); + preKeyCounts = handleResponseException(dependencies.getKeysApi() + .getAvailablePreKeyCountsSync(serviceIdType)); } catch (AuthorizationFailedException e) { logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName()); preKeyCounts = new OneTimePreKeyCounts(0, 0); @@ -145,7 +146,7 @@ public class PreKeyHelper { kyberPreKeyRecords); var needsReset = false; try { - NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload)); + NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeysSync(preKeyUpload)); try { if (preKeyRecords != null) { account.addPreKeys(serviceIdType, preKeyRecords); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 7906fc86..64e3a764 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -21,6 +21,7 @@ import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.signal.libsignal.zkgroup.profiles.ProfileKey; +import org.signal.network.exceptions.PushNetworkException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.NetworkResultUtil; @@ -30,7 +31,6 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java index e84a3059..c865f38f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java @@ -11,13 +11,13 @@ import org.signal.core.models.ServiceId.ACI; import org.signal.core.models.ServiceId.PNI; import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.cds.CdsiV2Service; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException; import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.IOException; import java.util.Collection; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 53ab3fa4..ba2ccad1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -160,7 +160,7 @@ public class SyncHelper { try { try (OutputStream fos = new FileOutputStream(contactsFile)) { - var out = new DeviceContactsOutputStream(fos, true, true); + var out = new DeviceContactsOutputStream(fos); for (var contactPair : account.getContactStore().getContacts()) { final var recipientId = contactPair.first(); final var contact = contactPair.second(); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index 394fa894..18c261b5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -101,6 +101,7 @@ import org.signal.core.util.Base64; import org.signal.core.util.Hex; import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.usernames.BaseUsernameException; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -117,7 +118,6 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Util; @@ -190,6 +190,7 @@ public class ManagerImpl implements Manager { userAgent, account.getCredentialsProvider(), account.getSignalServiceDataStore(), + account.getDeviceId(), executor, sessionLock); final var avatarStore = new AvatarStore(pathConfig.avatarsPath()); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java index e90bb154..1fd86c26 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/RegistrationManagerImpl.java @@ -231,6 +231,7 @@ public class RegistrationManagerImpl implements RegistrationManager { userAgent, account.getCredentialsProvider(), account.getSignalServiceDataStore(), + 0, null, new ReentrantSignalSessionLock()); handleResponseException(dependencies.getAccountApi() diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 78d658f4..55795714 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -5,13 +5,17 @@ import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.net.Network; +import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; +import org.signal.network.api.AttachmentApi; import org.signal.network.api.CallingApi; import org.signal.network.api.CdsApi; import org.signal.network.api.CertificateApi; import org.signal.network.api.LinkDeviceApi; import org.signal.network.api.RateLimitChallengeApi; import org.signal.network.api.UsernameApi; +import org.signal.network.rest.SignalRestClient; +import org.signal.network.service.CdnService; import org.signal.network.service.StorageServiceService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,12 +25,12 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.account.AccountApi; -import org.whispersystems.signalservice.api.attachment.AttachmentApi; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.keys.KeysApi; +import org.whispersystems.signalservice.api.keys.PreKeyRepository; import org.whispersystems.signalservice.api.message.MessageApi; import org.whispersystems.signalservice.api.profiles.ProfileApi; import org.whispersystems.signalservice.api.push.ServiceIdType; @@ -61,6 +65,7 @@ public class SignalDependencies { private final String userAgent; private final CredentialsProvider credentialsProvider; private final SignalServiceDataStore dataStore; + private final int deviceId; private final ExecutorService executor; private final SignalSessionLock sessionLock; @@ -82,6 +87,11 @@ public class SignalDependencies { private KeysApi keysApi; private GroupsV2Operations groupsV2Operations; private ClientZkOperations clientZkOperations; + private ProfileService profileService; + private ProfileApi profileApi; + private CdnService cdnService; + private PreKeyRepository preKeyRepository; + private SignalRestClient signalRestClient; private PushServiceSocket pushServiceSocket; private Network libSignalNetwork; @@ -91,14 +101,13 @@ public class SignalDependencies { private SignalServiceMessageSender messageSender; private List secureValueRecovery; - private ProfileService profileService; - private ProfileApi profileApi; SignalDependencies( final ServiceEnvironmentConfig serviceEnvironmentConfig, final String userAgent, final CredentialsProvider credentialsProvider, final SignalServiceDataStore dataStore, + final int deviceId, final ExecutorService executor, final SignalSessionLock sessionLock ) { @@ -106,6 +115,7 @@ public class SignalDependencies { this.userAgent = userAgent; this.credentialsProvider = credentialsProvider; this.dataStore = dataStore; + this.deviceId = deviceId; this.executor = executor; this.sessionLock = sessionLock; } @@ -326,12 +336,33 @@ public class SignalDependencies { () -> messageReceiver = new SignalServiceMessageReceiver(getPushServiceSocket())); } + private SignalRestClient getSignalRestClient() { + return getOrCreate(() -> signalRestClient, + () -> signalRestClient = new SignalRestClient(serviceEnvironmentConfig.signalServiceConfiguration(), + userAgent, + credentialsProvider, + ServiceConfig.AUTOMATIC_NETWORK_RETRY)); + } + + public CdnService getCdnService() { + return getOrCreate(() -> cdnService, + () -> cdnService = new CdnService(getSignalRestClient(), getAttachmentApi())); + } + + public PreKeyRepository getPreKeyRepository() { + final SignalProtocolAddress localProtocolAddress = credentialsProvider.getAci().toProtocolAddress(deviceId); + return getOrCreate(() -> preKeyRepository, + () -> preKeyRepository = new PreKeyRepository(getKeysApi(), + dataStore.aci(), + localProtocolAddress, + Runnable::run)); + } + public SignalServiceMessageSender getMessageSender() { return getOrCreate(() -> messageSender, () -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(), dataStore, sessionLock, - getAttachmentApi(), getMessageApi(), getKeysApi(), Optional.empty(), @@ -339,8 +370,7 @@ public class SignalDependencies { ServiceConfig.MAX_ENVELOPE_SIZE, ServiceConfig.MAX_INCREMENTAL_MACS_PER_ENVELOPE, () -> true, - true, - true)); + getPreKeyRepository())); } public List getSecureValueRecovery() { @@ -368,7 +398,9 @@ public class SignalDependencies { public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots()); - final var serviceId = serviceIdType == ServiceIdType.ACI ? credentialsProvider.getAci() : credentialsProvider.getPni(); + final var serviceId = serviceIdType == ServiceIdType.ACI + ? credentialsProvider.getAci() + : credentialsProvider.getPni(); final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164()); final var deviceId = credentialsProvider.getDeviceId(); return new SignalServiceCipher(address, diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index 39a447ac..a63ab6d7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -1,8 +1,9 @@ package org.asamk.signal.manager.internal; +import org.jetbrains.annotations.NotNull; +import org.signal.network.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.websocket.HealthMonitor; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; @@ -94,6 +95,11 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor { return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives(); } + @Override + public void onReceivedAlerts(@NotNull final String[] strings, final boolean b) { + logger.info("Received alerts: {}", String.join(", ", strings)); + } + /** * Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If * the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated. diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index 85b28bd7..fdc59e0c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -403,7 +403,7 @@ public class SessionStore implements SignalServiceSessionStore { } private static boolean isActive(SessionRecord record) { - return record != null && record.hasSenderChain(); + return record != null && record.hasSenderChain(0.0); } record Key(String address, int deviceId) {} diff --git a/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java index 1908c8f5..8733deb5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java @@ -10,11 +10,11 @@ import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.VerificationMethodNotAvailableException; import org.asamk.signal.manager.helper.PinHelper; import org.signal.core.models.MasterKey; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException; import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.internal.push.LockedException; diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index aee133ab..1871798b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -7,9 +7,9 @@ import org.signal.libsignal.net.RequestResult; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.fingerprint.Fingerprint; import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator; +import org.signal.network.NetworkResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.NetworkResult; import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.api.util.StreamDetails; diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index cb971b26..cf17ea05 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -6543,6 +6543,33 @@ "type": "org.signal.libsignal.zkgroup.profiles.ProfileKey", "allDeclaredFields": true }, + { + "type": "org.signal.network.api.SenderCertificate", + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "setCertificate", + "parameterTypes": [ + "byte[]" + ] + } + ] + }, + { + "type": "org.signal.network.api.SenderCertificate$ByteArrayDeserializer", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "type": "org.signal.network.api.SenderCertificate$ByteArraySerializer" + }, { "type": "org.signal.storageservice.storage.protos.groups.AccessControl", "fields": [ From 2a827f1285dbd66f36e547dd812ad7a2e53360bc Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 21:34:33 +0200 Subject: [PATCH 34/44] Don't log empty alerts --- .../signal/manager/internal/SignalWebSocketHealthMonitor.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index a63ab6d7..656b6646 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -97,6 +97,9 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor { @Override public void onReceivedAlerts(@NotNull final String[] strings, final boolean b) { + if (strings.length == 0) { + return; + } logger.info("Received alerts: {}", String.join(", ", strings)); } From 40b19288443a1c896f17d066060fb82d736a3639 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 22:38:33 +0200 Subject: [PATCH 35/44] Fix removal of local only unregistered accounts in storage sync --- .../signal/manager/helper/StorageHelper.java | 48 +++++++++++-------- .../storage/recipients/RecipientStore.java | 2 +- .../DefaultStorageRecordProcessor.java | 9 ++++ 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index 6f60f734..d01bf44b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.api.GroupIdV1; import org.asamk.signal.manager.api.GroupIdV2; +import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Profile; import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.storage.SignalAccount; @@ -38,6 +39,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -211,20 +213,23 @@ public class StorageHelper { remoteOnlyRecords.size()); } - // This logic is wrong, records should only be deleted if they're deleted remotely, not if the remote record is updated -// if (!idDifference.localOnlyIds().isEmpty()) { -// final var updated = account.getRecipientStore() -// .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, -// idDifference.localOnlyIds()); -// -// if (updated > 0) { -// logger.warn( -// "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", -// updated); -// } -// } -// - final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords); + final var listListPair = processKnownRecords(connection, remoteOnlyRecords); + final var unknownInserts = listListPair.first(); + final var updatedStorageIds = listListPair.second(); + final var oldUnregisteredLocalOnlyIds = new HashSet<>(idDifference.localOnlyIds()); + updatedStorageIds.forEach(oldUnregisteredLocalOnlyIds::remove); + if (!idDifference.localOnlyIds().isEmpty()) { + final var updated = account.getRecipientStore() + .removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, + oldUnregisteredLocalOnlyIds); + + if (updated > 0) { + logger.warn( + "Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.", + updated); + } + } + final var unknownDeletes = idDifference.localOnlyIds() .stream() .filter(id -> !KNOWN_TYPES.contains(id.getType())) @@ -480,13 +485,13 @@ public class StorageHelper { private Map generateGroupV1StorageIds(List groupIds) { return groupIds.stream() .collect(Collectors.toMap(recipientId -> recipientId, - recipientId -> StorageId.forGroupV1(KeyUtils.createRawStorageId()))); + _ -> StorageId.forGroupV1(KeyUtils.createRawStorageId()))); } private Map generateGroupV2StorageIds(List groupIds) { return groupIds.stream() .collect(Collectors.toMap(recipientId -> recipientId, - recipientId -> StorageId.forGroupV2(KeyUtils.createRawStorageId()))); + _ -> StorageId.forGroupV2(KeyUtils.createRawStorageId()))); } private void storeManifestLocally( @@ -630,16 +635,17 @@ public class StorageHelper { return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch); } - private List processKnownRecords( + private Pair, List> processKnownRecords( final Connection connection, List records ) throws SQLException { final var unknownRecords = new ArrayList(); + final var processedRecords = new ArrayList(); final var accountRecordProcessor = new AccountRecordProcessor(account, connection, context.getJobExecutor()); - final var contactRecordProcessor = new ContactRecordProcessor(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()); for (final var record : records) { if (record.getProto().account != null) { @@ -662,8 +668,12 @@ public class StorageHelper { unknownRecords.add(record.getId()); } } + processedRecords.addAll(accountRecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(groupV1RecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(groupV2RecordProcessor.getUpdatedStorageIds()); + processedRecords.addAll(contactRecordProcessor.getUpdatedStorageIds()); - return unknownRecords; + return new Pair<>(unknownRecords, processedRecords); } /** diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 8836a1d3..c80b3454 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -878,7 +878,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re public int removeStorageIdsFromLocalOnlyUnregisteredRecipients( final Connection connection, - final List storageIds + final Collection storageIds ) throws SQLException { final var sql = ( """ diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java index 87acb991..83fd5b5f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java +++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/DefaultStorageRecordProcessor.java @@ -6,7 +6,9 @@ import org.whispersystems.signalservice.api.storage.SignalRecord; import org.whispersystems.signalservice.api.storage.StorageId; import java.sql.SQLException; +import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.TreeSet; @@ -24,6 +26,7 @@ abstract class DefaultStorageRecordProcessor> implemen private static final Logger logger = LoggerFactory.getLogger(DefaultStorageRecordProcessor.class); private final Set matchedRecords = new TreeSet<>(this); + private final Set updatedStorageIds = new HashSet<>(); /** * One type of invalid remote data this handles is two records mapping to the same local data. We @@ -50,6 +53,7 @@ abstract class DefaultStorageRecordProcessor> implemen if (local.isEmpty()) { debug(remote.getId(), remote, "[Local Insert] No matching local record. Inserting."); + updatedStorageIds.add(remote.getId()); insertLocal(remote); return; } @@ -64,6 +68,7 @@ abstract class DefaultStorageRecordProcessor> implemen matchedRecords.add(local.get()); final var merged = merge(remote, local.get()); + updatedStorageIds.add(merged.getId()); if (!merged.equals(remote)) { debug(remote.getId(), remote, "[Remote Update] " + merged.describeDiff(remote)); } @@ -75,6 +80,10 @@ abstract class DefaultStorageRecordProcessor> implemen } } + public Set getUpdatedStorageIds() { + return Collections.unmodifiableSet(updatedStorageIds); + } + private void debug(StorageId i, E record, String message) { logger.debug("[{}][{}] {}", i, record.getClass().getSimpleName(), message); } From f057c5031c41d7008caa9a62c407fb022656a998 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 22:55:54 +0200 Subject: [PATCH 36/44] Fix use of deprecated API --- src/test/java/org/asamk/signal/http/SseInitialFlushTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java index 4796ae5d..d62c5220 100644 --- a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java +++ b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java @@ -39,7 +39,7 @@ import java.io.InputStream; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.ServerSocket; -import java.net.URL; +import java.net.URI; import java.time.Duration; import java.util.Collection; import java.util.List; @@ -97,7 +97,7 @@ class SseInitialFlushTest { @Test void sseEndpointReturnsHeadersWithinTwoSeconds() { assertDoesNotThrow(() -> { - var url = new URL("http://127.0.0.1:" + port + "/api/v1/events"); + var url = new URI("http", null, "127.0.0.1", port, "/api/v1/events", null, null).toURL(); var conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty("Accept", "text/event-stream"); conn.setReadTimeout(2_000); // 2 s — fails before fix (15 s flush), passes after From 8d6264e02ee3be56520da21697aca34f3451b65f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 22:56:09 +0200 Subject: [PATCH 37/44] Update dependencies --- gradle/libs.versions.toml | 4 ++-- src/main/java/org/asamk/signal/BaseConfig.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 972d5595..ef39a50d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] slf4j = "2.0.18" -junit = "6.0.3" -micronaut-json-schema = "2.0.0" +junit = "6.1.0" +micronaut-json-schema = "2.0.1" micronaut-core = "5.0.0" signal-service = "2.15.3_unofficial_147" diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index 7a35165b..da2e5422 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -8,7 +8,7 @@ public class BaseConfig { public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT")) - .orElse("Signal-Android/8.8.0"); + .orElse("Signal-Android/8.12.1"); static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; From 46c61c5aac428cc7caa44c4deda87ca23fd57ae8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 May 2026 23:01:29 +0200 Subject: [PATCH 38/44] Bump version to 0.14.4 --- CHANGELOG.md | 16 +++++++++++++++- build.gradle.kts | 4 ++-- data/org.asamk.SignalCli.metainfo.xml | 3 +++ libsignal-version | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f046f4b..d781a4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog -## [Unreleased] +## [0.14.4] - 2026-05-23 + +### Added + +- Support for a global configuration file to set system-wide defaults + +### Fixed + +- Group admins can now see profile information for users requesting to join groups. +- Storage sync with unregistered contacts fixed +- Incoming messages are validated more accurately, fixing receiving messages from new contacts + +### Improved + +- Some security and stability improvements, including HTTP HOST header validation and safer temporary file handling. ## [0.14.3] - 2026-04-22 diff --git a/build.gradle.kts b/build.gradle.kts index 5ed47048..31b0d72c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.4-SNAPSHOT" + version = "0.14.4" } java { @@ -102,7 +102,7 @@ dependencies { implementation(libs.micronaut.json.schema.annotations) if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) { implementation(libs.micronaut.json.schema.generator) - } + } implementation(project(":libsignal-cli")) testImplementation(libs.junit.jupiter) diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml index 3bf04d8c..231492be 100644 --- a/data/org.asamk.SignalCli.metainfo.xml +++ b/data/org.asamk.SignalCli.metainfo.xml @@ -45,6 +45,9 @@ intense + + https://github.com/AsamK/signal-cli/releases/tag/v0.14.4 + https://github.com/AsamK/signal-cli/releases/tag/v0.14.3 diff --git a/libsignal-version b/libsignal-version index da011ce4..6bc8ee7b 100644 --- a/libsignal-version +++ b/libsignal-version @@ -1 +1 @@ -0.92.1 +0.94.1 From 9e3585dccef8ebbf165656b6b5103b8193b14d1b Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 May 2026 00:51:12 +0200 Subject: [PATCH 39/44] Fix cli default values --- src/main/java/org/asamk/signal/App.java | 10 +++++----- src/main/java/org/asamk/signal/Main.java | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index d29cc2f8..fb940602 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -60,7 +60,7 @@ public class App { parser.addArgument("-v", "--verbose") .help("Raise log level and include lib signal logs. Specify multiple times for even more logs.") .action(Arguments.count()) - .setDefault(cfg.verbose()); + .setDefault(cfg.verbose() == null ? 0 : cfg.verbose()); parser.addArgument("--log-file") .type(File.class) .help("Write log output to the given file. If --verbose is also given, the detailed logs will only be written to the log file.") @@ -68,7 +68,7 @@ public class App { parser.addArgument("--scrub-log") .action(Arguments.storeTrue()) .help("Scrub possibly sensitive information from the log, like phone numbers and UUIDs.") - .setDefault(cfg.scrubLog()); + .setDefault(cfg.scrubLog() == null ? false : cfg.scrubLog()); parser.addArgument("-d", "--data-dir", "-c", "--config") .help("Set the path where to store data (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).") .setDefault(cfg.dataDir()); @@ -81,12 +81,12 @@ public class App { .dest("global-dbus") .help("Make request via user dbus.") .action(Arguments.storeTrue()) - .setDefault(cfg.dbus()); + .setDefault(cfg.dbus() == null ? false : cfg.dbus()); mut.addArgument("--dbus-system") .dest("global-dbus-system") .help("Make request via system dbus.") .action(Arguments.storeTrue()) - .setDefault(cfg.dbusSystem()); + .setDefault(cfg.dbusSystem() == null ? false : cfg.dbusSystem()); parser.addArgument("--bus-name") .dest("global-bus-name") .setDefault(cfg.busName() != null ? cfg.busName() : DbusConfig.getBusname()) @@ -112,7 +112,7 @@ public class App { parser.addArgument("--disable-send-log") .help("Disable message send log (for resending messages that recipient couldn't decrypt)") .action(Arguments.storeTrue()) - .setDefault(cfg.disableSendLog()); + .setDefault(cfg.disableSendLog() != null ? cfg.disableSendLog() : false); parser.epilog( "The global arguments are shown with 'signal-cli -h' and need to come before the subcommand, while the subcommand-specific arguments (shown with 'signal-cli SUBCOMMAND -h') need to be given after the subcommand."); diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 800b0ee6..0c9bddc0 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -113,13 +113,13 @@ public class Main { .defaultHelp(false); parser.addArgument("-v", "--verbose") .action(Arguments.count()) - .setDefault(config == null ? null : config.verbose()); + .setDefault(config == null || config.verbose() == null ? 0 : config.verbose()); parser.addArgument("--log-file") .type(File.class) .setDefault(config == null || config.logFile() == null ? null : new File(config.logFile())); parser.addArgument("--scrub-log") .action(Arguments.storeTrue()) - .setDefault(config == null ? null : config.scrubLog()); + .setDefault(config == null || config.scrubLog() == null ? false : config.scrubLog()); try { return parser.parseKnownArgs(args, null); From 12ffc34967222e9eff4a51782882177e92c3c04a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 May 2026 00:51:21 +0200 Subject: [PATCH 40/44] Bump version to 0.14.4.1 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 31b0d72c..f2428371 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.4" + version = "0.14.4.1" } java { From bda4e7fc0fb5ae3ae27cad9ee1ae9d7c3191b3c1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 May 2026 00:26:25 +0200 Subject: [PATCH 41/44] Prepare next release --- CHANGELOG.md | 2 ++ build.gradle.kts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d781a4ca..14ad107d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.14.4] - 2026-05-23 ### Added diff --git a/build.gradle.kts b/build.gradle.kts index f2428371..d4f97eb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.4.1" + version = "0.14.5-SNAPSHOT" } java { From 60779c91c6144eb4e28bb838ec754dd139f71c01 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 25 May 2026 11:40:46 +0200 Subject: [PATCH 42/44] Add missing graalvm metadata Fixes #2051 --- .../org.asamk/signal-cli/reachability-metadata.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index cf17ea05..b34f3df4 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -6570,6 +6570,15 @@ { "type": "org.signal.network.api.SenderCertificate$ByteArraySerializer" }, + { + "type": "org.signal.network.util.JsonUtil$ServiceIdDeserializer", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, { "type": "org.signal.storageservice.storage.protos.groups.AccessControl", "fields": [ From 29e65a5c8e5b50d5130ae26307b1ce060fcc89f2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 25 May 2026 11:51:08 +0200 Subject: [PATCH 43/44] Add missing graalvm metadata Fixes #2050 --- .../org.asamk/signal-cli/reachability-metadata.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index b34f3df4..96fe5efa 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -6570,6 +6570,15 @@ { "type": "org.signal.network.api.SenderCertificate$ByteArraySerializer" }, + { + "type": "org.signal.network.util.JsonUtil$IdentityKeyDeserializer", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, { "type": "org.signal.network.util.JsonUtil$ServiceIdDeserializer", "methods": [ From de678596c63e1ae8f4e7446c84d9b852b5e4b723 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 25 May 2026 17:39:53 +0200 Subject: [PATCH 44/44] Update test script --- run_tests.sh | 37 +++++++++--- .../signal-cli/reachability-metadata.json | 60 ++++++++----------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index 81d2c007..1a1c11e3 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -38,6 +38,13 @@ else SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli" fi +# Prefer line-buffered output for external commands when available +if command -v stdbuf >/dev/null 2>&1; then + STD_BUF="stdbuf -oL -eL --" +else + STD_BUF="" +fi + run() { # To update graalvm config, set GRAALVM_HOME, e.g: # export GRAALVM_HOME=/usr/lib/jvm/java-25-graalvm @@ -48,11 +55,23 @@ run() { set -x if [ "$JSON_RPC" -eq 1 ]; then - "$SIGNAL_CLI" $@ + if [ -n "$STD_BUF" ]; then + $STD_BUF "$SIGNAL_CLI" $@ + else + "$SIGNAL_CLI" $@ + fi elif [ "$DBUS" -eq 1 ]; then - "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org' + if [ -n "$STD_BUF" ]; then + $STD_BUF "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' + else + "$SIGNAL_CLI" --dbus --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' + fi else - "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep -v 'Warning:' | grep -v 'at org' + if [ -n "$STD_BUF" ]; then + $STD_BUF "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' + else + "$SIGNAL_CLI" --service-environment="staging" --verbose --verbose $@ | grep --line-buffered -v 'Warning:' | grep --line-buffered -v 'at org' + fi fi set +x } @@ -98,9 +117,10 @@ link() { rm -f "$LINK_CODE_FILE" mkfifo "$LINK_CODE_FILE" run_linked link -n "test-device" >"$LINK_CODE_FILE" & - read LINK_CODE <"$LINK_CODE_FILE" + LINK_PID=$! + read -r LINK_CODE <"$LINK_CODE_FILE" run_main -a "$NUMBER" addDevice --uri "$LINK_CODE" - wait + wait $LINK_PID run_linked -a "$NUMBER" send --note-to-self -m hi run_main -a "$NUMBER" receive run_linked -a "$NUMBER" receive @@ -180,6 +200,7 @@ run_main -a "$NUMBER_2" updateContact "$NUMBER_1" -n NUMBER_1 -e 10 run_main -a "$NUMBER_2" block "$NUMBER_1" run_main -a "$NUMBER_2" unblock "$NUMBER_1" run_main -a "$NUMBER_2" listContacts +run_main -a "$NUMBER_2" listContacts "$NUMBER_1" run_main -a "$NUMBER_1" send "$NUMBER_2" -m hi run_main -a "$NUMBER_2" receive @@ -207,6 +228,7 @@ run_main -a "$NUMBER_1" updateGroup -g "$GROUP_ID" -m "$NUMBER_2" run_main -a "$NUMBER_1" listGroups -d run_main -a "$NUMBER_1" --output=json listGroups -d run_main -a "$NUMBER_2" receive +run_main -a "$NUMBER_2" listGroups -g "$GROUP_ID" run_main -a "$NUMBER_2" quitGroup -g "$GROUP_ID" run_main -a "$NUMBER_2" listGroups -d run_main -a "$NUMBER_2" --output=json listGroups -d @@ -228,6 +250,7 @@ for OUTPUT in "plain-text" "json"; do run_main -a "$NUMBER_2" --output="$OUTPUT" receive run_main -a "$NUMBER_1" --output="$OUTPUT" receive run_main -a "$NUMBER_1" --output="$OUTPUT" send -e "$NUMBER_2" + run_main -a "$NUMBER_1" --output="$OUTPUT" send "$NUMBER_2" -m test run_main -a "$NUMBER_2" --output="$OUTPUT" receive done @@ -235,8 +258,8 @@ done run_main -a "$NUMBER_1" updateProfile --given-name=GIVEN --family-name=FAMILY --about=ABOUT --about-emoji=EMOJI --avatar=LICENSE --mobile-coin-address="YWJjCg==" ## Provisioning -link "$NUMBER_1" -link "$NUMBER_2" +link "$NUMBER_1" || true +link "$NUMBER_2" || true run_main -a "$NUMBER_1" listDevices run_linked -a "$NUMBER_1" sendSyncRequest run_main -a "$NUMBER_1" sendContacts diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index 96fe5efa..dff20afe 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1586,42 +1586,13 @@ }, { "type": "kotlin.reflect.jvm.internal.impl.resolve.scopes.DescriptorKindFilter", - "allPublicFields": true, - "fields": [ - { - "name": "ALL" - }, - { - "name": "CALLABLES" - }, - { - "name": "CLASSIFIERS" - }, - { - "name": "Companion" - }, - { - "name": "FUNCTIONS" - }, - { - "name": "NON_SINGLETON_CLASSIFIERS" - }, - { - "name": "PACKAGES" - }, - { - "name": "SINGLETON_CLASSIFIERS" - }, - { - "name": "TYPE_ALIASES" - }, - { - "name": "VALUES" - }, - { - "name": "VARIABLES" - } - ] + "allPublicFields": true + }, + { + "type": "kotlinx.coroutines.AwaitAll" + }, + { + "type": "kotlinx.coroutines.AwaitAll$AwaitAllNode" }, { "type": "kotlinx.coroutines.CancellableContinuationImpl", @@ -6570,6 +6541,23 @@ { "type": "org.signal.network.api.SenderCertificate$ByteArraySerializer" }, + { + "type": "org.signal.network.api.SubmitRecaptchaChallengePayload", + "methods": [ + { + "name": "getCaptcha", + "parameterTypes": [] + }, + { + "name": "getToken", + "parameterTypes": [] + }, + { + "name": "getType", + "parameterTypes": [] + } + ] + }, { "type": "org.signal.network.util.JsonUtil$IdentityKeyDeserializer", "methods": [