From bf1376d74da494d687d4ee60abc20d288ab4fa40 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 10 Jun 2026 22:58:59 +0200 Subject: [PATCH 01/11] Update libsignal-service --- gradle/libs.versions.toml | 2 +- .../signal/manager/config/ServiceConfig.java | 3 +- .../signal/manager/helper/ProfileHelper.java | 2 +- .../manager/internal/SignalDependencies.java | 2 +- .../SignalWebSocketHealthMonitor.java | 2 +- .../signal-cli/reachability-metadata.json | 109 +++++++++++++++++- 6 files changed, 113 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef39a50d..231496cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ slf4j = "2.0.18" junit = "6.1.0" micronaut-json-schema = "2.0.1" micronaut-core = "5.0.0" -signal-service = "2.15.3_unofficial_147" +signal-service = "2.15.3_unofficial_148" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index c2109f86..9b71f6fa 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -31,7 +31,8 @@ public class ServiceConfig { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { final var attachmentBackfill = !isPrimaryDevice; final var spqr = true; - return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr); + final var usernameSyncChangeMessage = !isPrimaryDevice; + return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr, usernameSyncChangeMessage); } public static ServiceEnvironmentConfig getServiceEnvironmentConfig( 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 64e3a764..308102a0 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 @@ -17,6 +17,7 @@ import org.asamk.signal.manager.util.PaymentUtils; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.Utils; import org.jetbrains.annotations.Nullable; +import org.signal.core.util.ExpiringProfileCredentialUtil; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; @@ -32,7 +33,6 @@ 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.services.ProfileService; -import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; import java.io.IOException; import java.io.OutputStream; 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 55795714..6eccb005 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 @@ -3,6 +3,7 @@ package org.asamk.signal.manager.internal; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.util.Utils; +import org.signal.core.util.UptimeSleepTimer; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.SignalProtocolAddress; @@ -40,7 +41,6 @@ import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.storage.StorageServiceApi; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; import org.whispersystems.signalservice.api.util.CredentialsProvider; -import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection; 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 656b6646..10f7a2a9 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,10 +1,10 @@ package org.asamk.signal.manager.internal; import org.jetbrains.annotations.NotNull; +import org.signal.core.util.SleepTimer; import org.signal.network.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.websocket.HealthMonitor; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; 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 dff20afe..e69e2df6 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 @@ -776,6 +776,20 @@ "type": "java.lang.Class", "jniAccessible": true, "methods": [ + { + "name": "forName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "forName", + "parameterTypes": [ + "java.lang.String", + "boolean", + "java.lang.ClassLoader" + ] + }, { "name": "getCanonicalName", "parameterTypes": [] @@ -784,6 +798,10 @@ "name": "getClassLoader", "parameterTypes": [] }, + { + "name": "getName", + "parameterTypes": [] + }, { "name": "getPermittedSubclasses", "parameterTypes": [] @@ -819,7 +837,27 @@ ] }, { - "type": "java.lang.ClassNotFoundException" + "type": "java.lang.ClassNotFoundException", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "", + "parameterTypes": [ + "java.lang.String", + "java.lang.Throwable" + ] + } + ] }, { "type": "java.lang.ClassValue" @@ -959,6 +997,18 @@ "name": "currentThread", "parameterTypes": [] }, + { + "name": "getContextClassLoader", + "parameterTypes": [] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, { "name": "getStackTrace", "parameterTypes": [] @@ -966,6 +1016,18 @@ { "name": "isVirtual", "parameterTypes": [] + }, + { + "name": "setContextClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] } ] }, @@ -979,10 +1041,18 @@ "java.lang.Throwable" ] }, + { + "name": "getCause", + "parameterTypes": [] + }, { "name": "getMessage", "parameterTypes": [] }, + { + "name": "getStackTrace", + "parameterTypes": [] + }, { "name": "getSuppressed", "parameterTypes": [] @@ -1586,7 +1656,42 @@ }, { "type": "kotlin.reflect.jvm.internal.impl.resolve.scopes.DescriptorKindFilter", - "allPublicFields": true + "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" + } + ] }, { "type": "kotlinx.coroutines.AwaitAll" From b3c1b6a4f64c36ec9e8bb93622f01da024111462 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 10 Jun 2026 23:05:39 +0200 Subject: [PATCH 02/11] Update svr2 mrenclave --- .../java/org/asamk/signal/manager/config/LiveConfig.java | 6 ++++-- .../java/org/asamk/signal/manager/config/StagingConfig.java | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 548c584b..14239a56 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -30,7 +30,9 @@ class LiveConfig { private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder() .decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6"); private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; - private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb"; + private static final String SVR2_MRENCLAVE = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4"; + private static final String SVR2_MRENCLAVE_LEGACY = "1240acbd4aa26974184844c8a46b1022d3957ac8a76c1fd8f5b1a15141ee0708"; + private static final String SVR2_MRENCLAVE_OLD_LEGACY = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb"; private static final String URL = "https://chat.signal.org"; private static final String CDN_URL = "https://cdn.signal.org"; @@ -93,7 +95,7 @@ class LiveConfig { createDefaultServiceConfiguration(interceptors), getUnidentifiedSenderTrustRoots(), CDSI_MRENCLAVE, - List.of(SVR2_MRENCLAVE)); + List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_OLD_LEGACY)); } private LiveConfig() { diff --git a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java index b5f1658b..6bec93af 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java @@ -30,7 +30,9 @@ class StagingConfig { private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder() .decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm"); private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; - private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036"; + private static final String SVR2_MRENCLAVE = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302"; + private static final String SVR2_MRENCLAVE_LEGACY = "97f151f6ed078edbbfd72fa9cae694dcc08353f1f5e8d9ccd79a971b10ffc535"; + private static final String SVR2_MRENCLAVE_OLD_LEGACY = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036"; private static final String URL = "https://chat.staging.signal.org"; private static final String CDN_URL = "https://cdn-staging.signal.org"; @@ -93,7 +95,7 @@ class StagingConfig { createDefaultServiceConfiguration(interceptors), getUnidentifiedSenderTrustRoots(), CDSI_MRENCLAVE, - List.of(SVR2_MRENCLAVE)); + List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_OLD_LEGACY)); } private StagingConfig() { From 9f0676d563174eb65fddf049c1e4dad7cc76eaf9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 11 Jun 2026 11:43:49 +0200 Subject: [PATCH 03/11] Disable host validation when binding on 0.0.0.0 Fixes #2056 --- .../asamk/signal/http/HttpServerHandler.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java index 551c5b04..a6fc9f9e 100644 --- a/src/main/java/org/asamk/signal/http/HttpServerHandler.java +++ b/src/main/java/org/asamk/signal/http/HttpServerHandler.java @@ -73,7 +73,13 @@ 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); + // If we're listening on any local address (0.0.0.0 or ::), skip Host header validation + final var addr = address == null ? null : address.getAddress(); + if (addr != null && addr.isAnyLocalAddress()) { + logger.warn("HTTP server has no authentication; Host header validation DISABLED because listening on {}", address); + } else { + logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts); + } } @Override @@ -306,6 +312,18 @@ public class HttpServerHandler implements AutoCloseable { } private boolean isHostAllowed(final HttpExchange httpExchange) { + // If the server is bound to any local address (0.0.0.0 or ::), skip host header validation + if (address != null) { + final var addr = address.getAddress(); + if (addr != null && addr.isAnyLocalAddress()) { + return true; + } + final var hostStr = address.getHostString(); + if ("0.0.0.0".equals(hostStr) || "::".equals(hostStr)) { + return true; + } + } + final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host"); if (hostHeader == null || hostHeader.isEmpty()) { return false; From afe8c24665f773b5e87ae38cb2b3770b8a01975f Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 11 Jun 2026 12:00:14 +0200 Subject: [PATCH 04/11] Update user agent --- src/main/java/org/asamk/signal/BaseConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index da2e5422..c6d9b40f 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.12.1"); + .orElse("Signal-Android/8.15.0"); static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; From 443b4da8f8075538477a3a70e1a037f472a58461 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 11 Jun 2026 12:34:00 +0200 Subject: [PATCH 05/11] Update graalvm build tools --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d4f97eb6..7fbe75c2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { application eclipse `check-lib-versions` - id("org.graalvm.buildtools.native") version "1.0.0" + id("org.graalvm.buildtools.native") version "1.1.2" } allprojects { From 6bef205b3f1698ae62e7fc6fe155d14eddb725ac Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 11 Jun 2026 12:27:59 +0200 Subject: [PATCH 06/11] Bump version 0.14.5 --- CHANGELOG.md | 11 ++++++++++- build.gradle.kts | 2 +- data/org.asamk.SignalCli.metainfo.xml | 3 +++ libsignal-version | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ad107d..399cadd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog -## [Unreleased] +## [0.14.5] - 2026-06-11 + +### Changed + +- Disable host validation when binding on 0.0.0.0 +- Use new SVR2 enclave for PINs + +### Fixed + +- Receiving unidentified sender messages after signal server change ## [0.14.4] - 2026-05-23 diff --git a/build.gradle.kts b/build.gradle.kts index 7fbe75c2..30ef6d96 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.5-SNAPSHOT" + version = "0.14.5" } java { diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml index 231492be..2af90518 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.5 + https://github.com/AsamK/signal-cli/releases/tag/v0.14.4 diff --git a/libsignal-version b/libsignal-version index 6bc8ee7b..0a4eab77 100644 --- a/libsignal-version +++ b/libsignal-version @@ -1 +1 @@ -0.94.1 +0.94.4 From debb8a20e6218b6b4e7a2760bf5e90547535b67a Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 11 Jun 2026 20:12:34 +0200 Subject: [PATCH 07/11] Send HangupMessage with deviceId 0 like Signal-Android Closes #2068 --- .../java/org/asamk/signal/manager/helper/CallManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 ac0a2a49..3d8efb72 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 @@ -649,7 +649,7 @@ public class CallManager implements AutoCloseable { case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY; default -> HangupMessage.Type.NORMAL; }; - var hangupMessage = new HangupMessage(state.callId, type, state.deviceId); + var hangupMessage = new HangupMessage(state.callId, type, 0); var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId); final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId); logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId)); @@ -711,7 +711,7 @@ public class CallManager implements AutoCloseable { && !"rejected".equals(reason) && !"remote_busy".equals(reason) && !"ringrtc_hangup".equals(reason)) { - var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, state.deviceId); + var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, 0); var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null); final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId); if (!result.isSuccess()) { From 5ac06a5ccde5d6dcd4eaba677dbb2bdcbdda0645 Mon Sep 17 00:00:00 2001 From: KritBlade <256668062+KritBlade@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:56:22 +0100 Subject: [PATCH 08/11] Skip known-unregistered recipients when sending to groups (#2077) Group sends built the recipient list from the full group membership, so members already known to be unregistered were retried on every send via the legacy 1:1 fan-out. On large groups this made a single send take tens of seconds and could time out, leaving the message undelivered. Filter out recipients whose unregistered timestamp is set before sending, returning an unregisteredFailure result for each (so callers and CLI output are unchanged) without the network attempt. The flag is maintained independently by profile/CDS discovery and is cleared when a recipient registers again, so skipped recipients are re-included automatically. Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 ++++ .../signal/manager/helper/SendHelper.java | 36 +++++++++++++++---- .../storage/recipients/RecipientStore.java | 34 ++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 399cadd9..0ab8b90f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- Sending to large groups is no longer slowed down by members that are already known to be unregistered; they are skipped instead of being retried via the legacy 1:1 send path on every send. + ## [0.14.5] - 2026-06-11 ### Changed diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index d4f54945..87e67b7a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -507,9 +507,32 @@ public class SendHelper { ) throws IOException { long startTime = System.currentTimeMillis(); - final var addressesMap = recipientIds.stream() + // Recipients that are already known to be unregistered are skipped here. + // Otherwise every group send re-attempts them via the slow legacy 1:1 + // fan-out, which on large groups can take tens of seconds (and time out). + // The unregistered flag is maintained independently by profile/CDS + // discovery, which clears it once a recipient registers again, so they + // are re-included automatically. An unregisteredFailure result is still + // returned for each skipped recipient, so callers see them unchanged. + final var skippedResults = new ArrayList(); + final Set targetRecipientIds; + final var unregisteredRecipientIds = account.getRecipientStore().getUnregisteredRecipientIds(recipientIds); + if (unregisteredRecipientIds.isEmpty()) { + targetRecipientIds = recipientIds; + } else { + logger.debug("Skipping {} known-unregistered recipient(s) in group send.", + unregisteredRecipientIds.size()); + targetRecipientIds = new HashSet<>(recipientIds); + targetRecipientIds.removeAll(unregisteredRecipientIds); + for (final var recipientId : unregisteredRecipientIds) { + skippedResults.add(SendMessageResult.unregisteredFailure(context.getRecipientHelper() + .resolveSignalServiceAddress(recipientId))); + } + } + + final var addressesMap = targetRecipientIds.stream() .collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress)); - final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds); + final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(targetRecipientIds); final var groupSendEndorsementsResult = getGroupSendEndorsements(groupInfo); final var groupSecretParams = groupInfo instanceof GroupInfoV2 gv2 ? GroupSecretParams.deriveFromMasterKey((gv2.getMasterKey())) @@ -523,7 +546,7 @@ public class SendHelper { : groupSendEndorsementsResult.first(); Set senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null ? Set.of() - : recipientIds.stream() + : targetRecipientIds.stream() .filter(s -> this.isSenderKeyCapable(s, addressesMap.get(s), unidentifiedAccessesMap.get(s), @@ -533,7 +556,7 @@ public class SendHelper { logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size()); senderKeyTargets = Set.of(); } else { - logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size()); + logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), targetRecipientIds.size()); } final var allResults = new ArrayList(recipientIds.size()); @@ -569,9 +592,9 @@ public class SendHelper { } } - final var legacyTargets = new HashSet<>(recipientIds); + final var legacyTargets = new HashSet<>(targetRecipientIds); legacyTargets.removeAll(senderKeyTargets); - final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice(); + final boolean onlyTargetIsSelfWithLinkedDevice = targetRecipientIds.isEmpty() && account.isMultiDevice(); if (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice) { if (!legacyTargets.isEmpty()) { @@ -605,6 +628,7 @@ public class SendHelper { isRecipientUpdate || !allResults.isEmpty()); allResults.addAll(results); } + allResults.addAll(skippedResults); final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime); logger.debug("Sending took {}", duration.toString()); return allResults; 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 c80b3454..90ea1baa 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 @@ -463,6 +463,40 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } } + /** + * Returns the subset of the given recipients that are currently known to be + * unregistered (i.e. have an unregistered timestamp set). + *

+ * These can be skipped when sending group messages; otherwise every send + * re-attempts them via the slow legacy 1:1 fan-out. The unregistered flag is + * maintained independently by profile/CDS discovery and cleared again once a + * recipient registers, so they are re-included automatically. + */ + public Set getUnregisteredRecipientIds(final Set recipientIds) { + if (recipientIds.isEmpty()) { + return Set.of(); + } + final var recipientIdsCommaSeparated = recipientIds.stream() + .map(recipientId -> String.valueOf(recipientId.id())) + .collect(Collectors.joining(",")); + final var sql = ( + """ + SELECT r._id + FROM %s r + WHERE r.unregistered_timestamp IS NOT NULL AND r._id IN (%s) + """ + ).formatted(TABLE_RECIPIENT, recipientIdsCommaSeparated); + try (final var connection = database.getConnection()) { + try (final var statement = connection.prepareStatement(sql)) { + try (var result = Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet)) { + return result.collect(Collectors.toSet()); + } + } + } catch (SQLException e) { + throw new RuntimeException("Failed read from recipient store", e); + } + } + public Set getAllNumbers() { final var sql = ( """ From bb433ebe5928dfc1c86d63f22940c40433ca79c2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 2 Jul 2026 10:29:51 +0200 Subject: [PATCH 09/11] Print hangup message details --- src/main/java/org/asamk/signal/ReceiveMessageHandler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index dcaf06f3..d179aed0 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -310,6 +310,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (callMessage.hangup().isPresent()) { var hangupMessage = callMessage.hangup().get(); writer.println("Hangup message: {}", callIdUnsigned(hangupMessage.id())); + writer.println("- type: {}", hangupMessage.type()); + writer.println("- device id: {}", hangupMessage.deviceId()); } if (!callMessage.iceUpdate().isEmpty()) { writer.println("Ice update messages:"); From 254efba64371dbd1674572d885ca9f756cfac1e0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 May 2026 00:26:25 +0200 Subject: [PATCH 10/11] Prepare next release --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 30ef6d96..82ff4a01 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ plugins { allprojects { group = "org.asamk" - version = "0.14.5" + version = "0.14.6-SNAPSHOT" } java { From 98d9960e66707fc88a2812b5a85685a83c003063 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 2 Jul 2026 10:31:14 +0200 Subject: [PATCH 11/11] Update graalvm build tools --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 82ff4a01..26fdc7f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { application eclipse `check-lib-versions` - id("org.graalvm.buildtools.native") version "1.1.2" + id("org.graalvm.buildtools.native") version "1.1.3" } allprojects {