Compare commits

..

5 Commits

Author SHA1 Message Date
Stefan Meinecke
67105e14c1
Merge branch 'master' into fix/unauthenticated-socket-keepalive 2026-04-23 10:23:51 +00:00
Stefan Meinecke
f27eb524de Merge branch 'master' of https://github.com/AsamK/signal-cli into fix/unauthenticated-socket-keepalive 2026-04-17 11:49:13 +02:00
Stefan Meinecke
0006fd0dc0 Tie unauthenticated WebSocket keep-alive to active client connections
Keep the unidentified socket alive while a JSON-RPC connection is open (including stdio mode) and while a D-Bus object is exported, instead of for the lifetime of the receive loop. The receive loop does not use the unauthenticated socket, so keeping it alive there was semantically wrong.

This also covers --receive-mode=manual, where no receive loop runs butclients still send messages.
2026-04-17 11:49:05 +02:00
Stefan Meinecke
72332750a8 Merge branch 'master' of https://github.com/AsamK/signal-cli into fix/unauthenticated-socket-keepalive 2026-04-15 11:42:43 +02:00
Stefan Meinecke
8310b16895 Keep unauthenticated WebSocket alive during daemon receive loop
The unauthenticated (sealed sender) socket had no keep-alive token
registered, causing SignalWebSocket's DelayedDisconnectThread to tear
down the connection ~10s after each send. Every subsequent group message
then had to re-establish a fresh TLS connection (~6s delay).

The authenticated socket avoids this by registering a "receive" keep-alive
token for the lifetime of the receive loop. Apply the same pattern to the
unauthenticated socket: register the token alongside the authenticated one
and remove it in the same finally block.

This keeps the unidentified connection alive in daemon mode, matching the
behaviour of Signal mobile clients.
2026-04-14 19:59:42 +02:00
25 changed files with 123 additions and 694 deletions

View File

@ -1,7 +1,5 @@
# Changelog # Changelog
## [Unreleased]
## [0.14.3] - 2026-04-22 ## [0.14.3] - 2026-04-22
### Fixed ### Fixed

View File

@ -10,7 +10,7 @@ plugins {
allprojects { allprojects {
group = "org.asamk" group = "org.asamk"
version = "0.14.4-SNAPSHOT" version = "0.14.3"
} }
java { java {

View File

@ -7,11 +7,11 @@ plugins {
} }
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure { tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
compilerOptions.jvmTarget.set(JvmTarget.JVM_25) compilerOptions.jvmTarget.set(JvmTarget.JVM_24)
} }
java { java {
targetCompatibility = JavaVersion.VERSION_25 targetCompatibility = JavaVersion.VERSION_24
} }
repositories { repositories {

4
client/Cargo.lock generated
View File

@ -912,9 +912,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.13" version = "0.103.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",

View File

@ -1,9 +1,8 @@
[versions] [versions]
slf4j = "2.0.18" slf4j = "2.0.17"
junit = "6.0.3" junit = "6.0.3"
micronaut-json-schema = "2.0.0" micronaut-json-schema = "2.0.0-M8"
micronaut-core = "5.0.0" micronaut-core = "4.9.3"
signal-service = "2.15.3_unofficial_146"
[libraries] [libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
@ -19,8 +18,8 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
logback = "ch.qos.logback:logback-classic:1.5.32" logback = "ch.qos.logback:logback-classic:1.5.32"
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" } signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_144"
sqlite = "org.xerial:sqlite-jdbc:3.53.1.0" sqlite = "org.xerial:sqlite-jdbc:3.53.0.0"
hikari = "com.zaxxer:HikariCP:7.0.2" hikari = "com.zaxxer:HikariCP:7.0.2"
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }

Binary file not shown.

View File

@ -1,9 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

31
gradlew.bat vendored
View File

@ -23,8 +23,8 @@
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled @rem Set local scope for the variables with windows NT shell
setlocal EnableExtensions if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 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 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% 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 Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
@ -73,10 +73,21 @@ echo location of your Java installation. 1>&2
@rem Execute Gradle @rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
@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
:exitWithErrorLevel :end
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts @rem End local scope for the variables with windows NT shell
"%COMSPEC%" /c exit %ERRORLEVEL% 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

View File

@ -18,9 +18,9 @@ val libsignalClientPath = project.findProperty("libsignal_client_path")?.toStrin
dependencies { dependencies {
if (libsignalClientPath == null) { if (libsignalClientPath == null) {
implementation(libs.signalnetwork) implementation(libs.signalservice)
} else { } else {
implementation(libs.signalnetwork) { implementation(libs.signalservice) {
exclude(group = "org.signal", module = "libsignal-client") exclude(group = "org.signal", module = "libsignal-client")
} }
implementation(files(libsignalClientPath)) implementation(files(libsignalClientPath))

View File

@ -259,7 +259,7 @@ public interface Manager extends Closeable {
RecipientIdentifier.Single recipient RecipientIdentifier.Single recipient
) throws IOException; ) throws IOException;
void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException; SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
SendMessageResults sendMessageRequestResponse( SendMessageResults sendMessageRequestResponse(
MessageEnvelope.Sync.MessageRequestResponse.Type type, MessageEnvelope.Sync.MessageRequestResponse.Type type,

View File

@ -157,7 +157,7 @@ public record MessageEnvelope(
dataMessage.getExpiresInSeconds(), dataMessage.getExpiresInSeconds(),
dataMessage.isExpirationUpdate(), dataMessage.isExpirationUpdate(),
dataMessage.isViewOnce(), dataMessage.isViewOnce(),
false, dataMessage.isEndSession(),
dataMessage.isProfileKeyUpdate(), dataMessage.isProfileKeyUpdate(),
dataMessage.getProfileKey().isPresent(), dataMessage.getProfileKey().isPresent(),
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)), dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
@ -1028,7 +1028,7 @@ public record MessageEnvelope(
final AttachmentFileProvider fileProvider, final AttachmentFileProvider fileProvider,
Exception exception Exception exception
) { ) {
final var serviceId = envelope.getSourceServiceId(); final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
final var source = !envelope.isUnidentifiedSender() && serviceId != null final var source = !envelope.isUnidentifiedSender() && serviceId != null
? recipientResolver.resolveRecipient(serviceId) ? recipientResolver.resolveRecipient(serviceId)
: envelope.isUnidentifiedSender() && content != null : envelope.isUnidentifiedSender() && content != null

View File

@ -558,24 +558,16 @@ public class GroupHelper {
private void storeProfileKeysFromMembers(final DecryptedGroup group) { private void storeProfileKeysFromMembers(final DecryptedGroup group) {
for (var member : group.members) { for (var member : group.members) {
final var serviceId = ServiceId.parseOrThrow(member.aciBytes); final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray()); final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId);
} final var profileStore = account.getProfileStore();
for (var member : group.requestingMembers) { if (profileStore.getProfileKey(recipientId) != null) {
final var serviceId = ServiceId.parseOrThrow(member.aciBytes); // We already have a profile key, not updating it from a non-authoritative source
storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray()); continue;
} }
} try {
profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray()));
private void storeProfileKeyIfMissing(final ServiceId serviceId, final byte[] profileKeyBytes) { } catch (InvalidInputException ignored) {
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(profileKeyBytes));
} catch (InvalidInputException ignored) {
} }
} }

View File

@ -109,8 +109,8 @@ public final class IncomingMessageHandler {
SignalServiceContent content = null; SignalServiceContent content = null;
if (!envelope.isReceipt()) { if (!envelope.isReceipt()) {
account.getIdentityKeyStore().setRetryingDecryption(true); account.getIdentityKeyStore().setRetryingDecryption(true);
final var destination = getDestination(envelope).serviceId();
try { try {
final var destination = getDestination(envelope).serviceId();
final var cipherResult = dependencies.getCipher(destination == null final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
@ -140,30 +140,15 @@ public final class IncomingMessageHandler {
final Manager.ReceiveMessageHandler handler final Manager.ReceiveMessageHandler handler
) { ) {
final var actions = new ArrayList<HandleAction>(); final var actions = new ArrayList<HandleAction>();
if (envelope.isPreKeySignalMessage()) {
actions.add(RefreshPreKeysAction.create());
}
SignalServiceContent content = null; SignalServiceContent content = null;
Exception exception = null; Exception exception = null;
if (envelope.getSourceServiceId() != null) { envelope.getSourceServiceId().map(ServiceId::parseOrNull)
// Store uuid if we don't have it already // Store uuid if we don't have it already
// uuid in envelope is sent by server // uuid in envelope is sent by server
account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId()); .ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId));
}
if (!envelope.isReceipt()) { if (!envelope.isReceipt()) {
final var destination = getDestination(envelope).serviceId();
try { 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 final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
@ -188,13 +173,7 @@ public final class IncomingMessageHandler {
logger.debug("Received invalid message from blocked contact, ignoring."); logger.debug("Received invalid message from blocked contact, ignoring.");
} else { } else {
var serviceId = ServiceId.parseOrNull(e.getSender()); var serviceId = ServiceId.parseOrNull(e.getSender());
ServiceId destination; if (serviceId != null) {
try {
destination = getDestination(envelope).serviceId();
} catch (InvalidMessageException ex) {
destination = null;
}
if (serviceId != null && destination != null) {
final var isSelf = sender.equals(account.getSelfRecipientId()) final var isSelf = sender.equals(account.getSelfRecipientId())
&& e.getSenderDevice() == account.getDeviceId(); && e.getSenderDevice() == account.getDeviceId();
logger.debug("Received invalid message, queuing renew session action."); logger.debug("Received invalid message, queuing renew session action.");
@ -332,12 +311,7 @@ public final class IncomingMessageHandler {
final var sender = senderDeviceAddress.recipientId(); final var sender = senderDeviceAddress.recipientId();
final var senderServiceId = senderDeviceAddress.serviceId(); final var senderServiceId = senderDeviceAddress.serviceId();
final var senderDeviceId = senderDeviceAddress.deviceId(); final var senderDeviceId = senderDeviceAddress.deviceId();
final DeviceAddress destination; final var destination = getDestination(envelope);
try {
destination = getDestination(envelope);
} catch (InvalidMessageException e) {
throw new AssertionError(e);
}
if (account.getPni().equals(destination.serviceId)) { if (account.getPni().equals(destination.serviceId)) {
account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true); account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true);
@ -900,6 +874,11 @@ public final class IncomingMessageHandler {
final var selfAddress = isSync ? source : destination; final var selfAddress = isSync ? source : destination;
final var conversationPartnerAddress = isSync ? destination : source; 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.isExpirationUpdate() || message.getBody().isPresent()) {
if (message.getGroupContext().isPresent()) { if (message.getGroupContext().isPresent()) {
final var groupContext = message.getGroupContext().get(); final var groupContext = message.getGroupContext().get();
@ -1068,7 +1047,7 @@ public final class IncomingMessageHandler {
} }
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) { private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
final var serviceId = envelope.getSourceServiceId(); final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
if (!envelope.isUnidentifiedSender() && serviceId != null) { if (!envelope.isUnidentifiedSender() && serviceId != null) {
return new SignalServiceAddress(serviceId); return new SignalServiceAddress(serviceId);
} else if (content != null) { } else if (content != null) {
@ -1079,7 +1058,7 @@ public final class IncomingMessageHandler {
} }
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) { private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
final var serviceId = envelope.getSourceServiceId(); final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null);
if (!envelope.isUnidentifiedSender() && serviceId != null) { if (!envelope.isUnidentifiedSender() && serviceId != null) {
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId), return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId),
serviceId, serviceId,
@ -1091,13 +1070,10 @@ public final class IncomingMessageHandler {
} }
} }
private DeviceAddress getDestination(SignalServiceEnvelope envelope) throws InvalidMessageException { private DeviceAddress getDestination(SignalServiceEnvelope envelope) {
final var destination = envelope.getDestinationServiceId(); final var destination = envelope.getDestinationServiceId();
if (destination == null || destination.isUnknown()) { if (destination == null || destination.isUnknown()) {
throw new InvalidMessageException("Missing destination"); return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
}
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), return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
destination, destination,

View File

@ -9,6 +9,7 @@ import org.asamk.signal.manager.jobs.CleanOldPreKeysJob;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.signal.core.models.ServiceId;
import org.signal.core.models.ServiceId.ACI; import org.signal.core.models.ServiceId.ACI;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -149,10 +150,10 @@ public class ReceiveHelper {
for (final var it : batch) { for (final var it : batch) {
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(), SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
it.getServerDeliveredTimestamp()); it.getServerDeliveredTimestamp());
final var sourceServiceId = envelope1.getSourceServiceId(); final var recipientId = envelope1.getSourceServiceId()
final var recipientId = sourceServiceId == null .map(ServiceId::parseOrNull)
? null .map(s -> account.getRecipientResolver().resolveRecipient(s))
: account.getRecipientResolver().resolveRecipient(sourceServiceId); .orElse(null);
logger.trace("Storing new message from {}", recipientId); logger.trace("Storing new message from {}", recipientId);
// store message on disk, before acknowledging receipt to the server // store message on disk, before acknowledging receipt to the server
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
@ -237,7 +238,7 @@ public class ReceiveHelper {
if (exception instanceof UntrustedIdentityException) { if (exception instanceof UntrustedIdentityException) {
logger.debug("Keeping message with untrusted identity in message cache"); logger.debug("Keeping message with untrusted identity in message cache");
final var address = ((UntrustedIdentityException) exception).getSender(); final var address = ((UntrustedIdentityException) exception).getSender();
if (envelope.getSourceServiceId() == null && address.aci().isPresent()) { if (envelope.getSourceServiceId().isEmpty() && address.aci().isPresent()) {
final var recipientId = account.getRecipientResolver() final var recipientId = account.getRecipientResolver()
.resolveRecipient(ACI.parseOrThrow(address.aci().get())); .resolveRecipient(ACI.parseOrThrow(address.aci().get()));
try { try {
@ -291,7 +292,7 @@ public class ReceiveHelper {
cachedMessage.delete(); cachedMessage.delete();
return null; return null;
} }
if (envelope.getSourceServiceId() == null) { if (envelope.getSourceServiceId().isEmpty()) {
final var identifier = ((UntrustedIdentityException) exception).getSender(); final var identifier = ((UntrustedIdentityException) exception).getSender();
final var recipientId = account.getRecipientResolver() final var recipientId = account.getRecipientResolver()
.resolveRecipient(new RecipientAddress(identifier)); .resolveRecipient(new RecipientAddress(identifier));

View File

@ -17,9 +17,6 @@ import org.asamk.signal.manager.util.KeyUtils;
import org.signal.core.models.storageservice.StorageKey; import org.signal.core.models.storageservice.StorageKey;
import org.signal.core.util.SetUtil; import org.signal.core.util.SetUtil;
import org.signal.libsignal.protocol.InvalidKeyException; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
@ -28,6 +25,9 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt; 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.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord; import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
@ -504,7 +504,7 @@ public class StorageHelper {
final var result = dependencies.getStorageServiceRepository() final var result = dependencies.getStorageServiceRepository()
.readStorageRecords(storageKey, manifest.recordIkm, storageIds); .readStorageRecords(storageKey, manifest.recordIkm, storageIds);
return switch (result) { return switch (result) {
case StorageServiceService.StorageRecordResult.DecryptionError decryptionError -> { case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
if (decryptionError.getException() instanceof InvalidKeyException) { if (decryptionError.getException() instanceof InvalidKeyException) {
logger.warn("Failed to read storage records, ignoring."); logger.warn("Failed to read storage records, ignoring.");
yield List.of(); yield List.of();
@ -514,11 +514,11 @@ public class StorageHelper {
throw new IOException(decryptionError.getException()); throw new IOException(decryptionError.getException());
} }
} }
case StorageServiceService.StorageRecordResult.NetworkError networkError -> case StorageServiceRepository.StorageRecordResult.NetworkError networkError ->
throw networkError.getException(); throw networkError.getException();
case StorageServiceService.StorageRecordResult.StatusCodeError statusCodeError -> case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError ->
throw statusCodeError.getException(); throw statusCodeError.getException();
case StorageServiceService.StorageRecordResult.Success success -> success.getRecords(); case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords();
default -> throw new IllegalStateException("Unexpected value: " + result); default -> throw new IllegalStateException("Unexpected value: " + result);
}; };
} }

View File

@ -1091,26 +1091,30 @@ public class ManagerImpl implements Manager {
} }
@Override @Override
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException { public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException {
for (var recipient : recipients) { var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
final RecipientId recipientId;
try { try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient); return sendMessage(messageBuilder,
} catch (UnregisteredRecipientException e) { recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()),
continue; false);
} } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
final var recipientAddress = context.getAccount() throw new AssertionError(e);
.getRecipientAddressResolver() } finally {
.resolveRecipientAddress(recipientId); for (var recipient : recipients) {
final var aciSessionStore = account.getAccountData(ServiceIdType.ACI).getSessionStore(); final RecipientId recipientId;
final var pniSessionStore = account.getAccountData(ServiceIdType.PNI).getSessionStore(); try {
if (recipientAddress.aci().isPresent()) { recipientId = context.getRecipientHelper().resolveRecipient(recipient);
aciSessionStore.archiveSessions(recipientAddress.aci().get()); } catch (UnregisteredRecipientException e) {
pniSessionStore.archiveSessions(recipientAddress.aci().get()); continue;
} }
if (recipientAddress.pni().isPresent()) { final var serviceId = context.getAccount()
aciSessionStore.archiveSessions(recipientAddress.pni().get()); .getRecipientAddressResolver()
pniSessionStore.archiveSessions(recipientAddress.pni().get()); .resolveRecipientAddress(recipientId)
.serviceId();
if (serviceId.isPresent()) {
account.getAccountData(ServiceIdType.ACI).getSessionStore().deleteAllSessions(serviceId.get());
}
} }
} }
} }

View File

@ -6,13 +6,6 @@ import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.net.Network; import org.signal.libsignal.net.Network;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
@ -22,19 +15,26 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.account.AccountApi; import org.whispersystems.signalservice.api.account.AccountApi;
import org.whispersystems.signalservice.api.attachment.AttachmentApi; 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.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.keys.KeysApi; 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.message.MessageApi;
import org.whispersystems.signalservice.api.profiles.ProfileApi; import org.whispersystems.signalservice.api.profiles.ProfileApi;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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.registration.RegistrationApi;
import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.storage.StorageServiceApi; 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.svr.SecureValueRecovery;
import org.whispersystems.signalservice.api.username.UsernameApi;
import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
@ -243,8 +243,8 @@ public class SignalDependencies {
getPushServiceSocket())); getPushServiceSocket()));
} }
public StorageServiceService getStorageServiceRepository() { public StorageServiceRepository getStorageServiceRepository() {
return new StorageServiceService(getStorageServiceApi()); return new StorageServiceRepository(getStorageServiceApi());
} }
public CertificateApi getCertificateApi() { public CertificateApi getCertificateApi() {
@ -368,8 +368,7 @@ public class SignalDependencies {
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots()); final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots());
final var serviceId = serviceIdType == ServiceIdType.ACI ? credentialsProvider.getAci() : credentialsProvider.getPni(); final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164());
final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164());
final var deviceId = credentialsProvider.getDeviceId(); final var deviceId = credentialsProvider.getDeviceId();
return new SignalServiceCipher(address, return new SignalServiceCipher(address,
deviceId, deviceId,

View File

@ -6,7 +6,7 @@ ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV LC_CTYPE=en_US.UTF-8 ENV LC_CTYPE=en_US.UTF-8
RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \ RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \
&& 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 && 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 COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh
WORKDIR /signal-cli WORKDIR /signal-cli
ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ] ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ]

View File

@ -144,7 +144,8 @@ public class SendCommand implements JsonRpcLocalCommand {
} }
try { try {
m.sendEndSessionMessage(singleRecipients); final var results = m.sendEndSessionMessage(singleRecipients);
outputResult(outputWriter, results);
return; return;
} catch (IOException e) { } catch (IOException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()

View File

@ -543,8 +543,9 @@ public class DbusManagerImpl implements Manager {
} }
@Override @Override
public void sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException { public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList()); signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
return new SendMessageResults(0, Map.of());
} }
@Override @Override

View File

@ -432,7 +432,8 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
@Override @Override
public void sendEndSessionMessage(final List<String> recipients) { public void sendEndSessionMessage(final List<String> recipients) {
try { try {
m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber()));
checkSendMessageResults(results);
} catch (IOException e) { } catch (IOException e) {
throw new Error.Failure(e.getMessage()); throw new Error.Failure(e.getMessage());
} }

View File

@ -169,12 +169,6 @@ public class HttpServerHandler implements AutoCloseable {
httpExchange.sendResponseHeaders(200, 0); httpExchange.sendResponseHeaders(200, 0);
final var sender = new ServerSentEventSender(httpExchange.getResponseBody()); 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 shouldStop = new AtomicBoolean(false);
final var handlers = subscribeReceiveHandlers(managers, sender, () -> { final var handlers = subscribeReceiveHandlers(managers, sender, () -> {
shouldStop.set(true); shouldStop.set(true);

View File

@ -33,7 +33,6 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors;
public class SignalJsonRpcDispatcherHandler { public class SignalJsonRpcDispatcherHandler {
@ -103,9 +102,7 @@ public class SignalJsonRpcDispatcherHandler {
private int subscribeCallEvents(final Collection<Manager> managers) { private int subscribeCallEvents(final Collection<Manager> managers) {
final var subscriptionId = nextSubscriptionId.getAndIncrement(); final var subscriptionId = nextSubscriptionId.getAndIncrement();
final var listeners = managers.stream() final var listeners = managers.stream().map(m -> createCallEventHandler(m, subscriptionId)).toList();
.map(m -> createCallEventHandler(m, subscriptionId))
.collect(Collectors.toCollection(ArrayList::new));
callEventHandlers.put(subscriptionId, listeners); callEventHandlers.put(subscriptionId, listeners);
return subscriptionId; return subscriptionId;
} }
@ -160,7 +157,7 @@ public class SignalJsonRpcDispatcherHandler {
final var subscriptionId = nextSubscriptionId.getAndIncrement(); final var subscriptionId = nextSubscriptionId.getAndIncrement();
final var handlers = managers.stream() final var handlers = managers.stream()
.map(m -> createReceiveHandler(m, subscriptionId, internalSubscription)) .map(m -> createReceiveHandler(m, subscriptionId, internalSubscription))
.collect(Collectors.toCollection(ArrayList::new)); .toList();
receiveHandlers.put(subscriptionId, handlers); receiveHandlers.put(subscriptionId, handlers);
return subscriptionId; return subscriptionId;

View File

@ -1,544 +0,0 @@
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<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler) {
}
@Override
public void stopReceiveMessages() {
}
@Override
public void setReceiveConfig(ReceiveConfig receiveConfig) {
}
@Override
public Map<String, UserStatus> getUserStatus(Set<String> numbers) {
return Map.of();
}
@Override
public Map<String, UsernameStatus> getUsernameStatus(Set<String> 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<Device> 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<String> pin) {
}
@Override
public List<Group> getGroups() {
return List.of();
}
@Override
public List<Group> getGroups(Collection<GroupId> groupIds) {
return List.of();
}
@Override
public SendGroupMessageResults quitGroup(GroupId groupId, Set<RecipientIdentifier.Single> administrators) {
return null;
}
@Override
public void deleteGroup(GroupId groupId) {
}
@Override
public Pair<GroupId, SendGroupMessageResults> createGroup(String name, Set<RecipientIdentifier.Single> members, String avatarFile) {
return null;
}
@Override
public SendGroupMessageResults updateGroup(GroupId groupId, UpdateGroup updateGroup) {
return null;
}
@Override
public Pair<GroupId, SendGroupMessageResults> joinGroup(GroupInviteLinkUrl inviteLinkUrl) {
return null;
}
@Override
public SendMessageResults sendTypingMessage(TypingAction action, Set<RecipientIdentifier> recipients) {
return null;
}
@Override
public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
return null;
}
@Override
public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
return null;
}
@Override
public SendMessageResults sendMessage(Message message, Set<RecipientIdentifier> recipients, boolean notifySelf) {
return null;
}
@Override
public SendMessageResults sendEditMessage(Message message, Set<RecipientIdentifier> recipients, long targetSentTimestamp) {
return null;
}
@Override
public SendMessageResults sendRemoteDeleteMessage(long targetSentTimestamp, Set<RecipientIdentifier> recipients) {
return null;
}
@Override
public SendMessageResults sendMessageReaction(String emoji, boolean remove, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set<RecipientIdentifier> recipients, boolean notifySelf, boolean story) {
return null;
}
@Override
public SendMessageResults sendAdminDelete(RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set<RecipientIdentifier.Group> recipients, boolean notifySelf, boolean story) {
return null;
}
@Override
public SendMessageResults sendPinMessage(int duration, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set<RecipientIdentifier> recipients, boolean notifySelf, boolean story) {
return null;
}
@Override
public SendMessageResults sendUnpinMessage(RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set<RecipientIdentifier> recipients, boolean notifySelf, boolean story) {
return null;
}
@Override
public SendMessageResults sendPaymentNotificationMessage(byte[] receipt, String note, RecipientIdentifier.Single recipient) {
return null;
}
@Override
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) {
}
@Override
public SendMessageResults sendMessageRequestResponse(MessageEnvelope.Sync.MessageRequestResponse.Type type, Set<RecipientIdentifier> recipients) {
return null;
}
@Override
public SendMessageResults sendPollCreateMessage(String question, boolean multipleChoice, List<String> options, Set<RecipientIdentifier> recipients, boolean notifySelf) {
return null;
}
@Override
public SendMessageResults sendPollVoteMessage(RecipientIdentifier.Single author, long timestamp, List<Integer> optionIds, int version, Set<RecipientIdentifier> recipients, boolean notifySelf) {
return null;
}
@Override
public SendMessageResults sendPollTerminateMessage(long timestamp, Set<RecipientIdentifier> 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<RecipientIdentifier.Single> recipients, boolean blocked) {
}
@Override
public void setGroupsBlocked(Collection<GroupId> 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<StickerPack> getStickerPacks() {
return List.of();
}
@Override
public void requestAllSyncData() {
}
@Override
public boolean isContactBlocked(RecipientIdentifier.Single recipient) {
return false;
}
@Override
public void sendContacts() {
}
@Override
public List<Recipient> getRecipients(boolean onlyWithProfile, Optional<Boolean> blocked, Collection<RecipientIdentifier.Single> addresses, Optional<String> name) {
return List.of();
}
@Override
public String getContactOrProfileName(RecipientIdentifier.Single recipient) {
return null;
}
@Override
public Group getGroup(GroupId groupId) {
return null;
}
@Override
public List<Identity> getIdentities() {
return List.of();
}
@Override
public List<Identity> 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 void addUnidentifiedKeepAlive(String token) {
}
@Override
public void removeUnidentifiedKeepAlive(String token) {
}
@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<CallInfo> 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<byte[]> 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<TurnServer> getTurnServerInfo() {
return List.of();
}
@Override
public void close() {
}
@Override
public void addCallEventListener(CallEventListener listener) {
}
@Override
public void removeCallEventListener(CallEventListener listener) {
}
}
}

View File

@ -328,7 +328,8 @@ class SubscribeCallEventsTest {
} }
@Override @Override
public void sendEndSessionMessage(Set<RecipientIdentifier.Single> r) { public SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> r) {
return null;
} }
@Override @Override