Compare commits

..

19 Commits

Author SHA1 Message Date
Stefan Meinecke
2cbff3a4c1 Add missing unidentified keep-alive methods to test stub
Implement addUnidentifiedKeepAlive and removeUnidentifiedKeepAlive in SseInitialFlushTest's Manager stub to match the updated interface.
2026-05-17 14:32:37 +02:00
Stefan Meinecke
540cfbd977 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-05-17 14:16:42 +02:00
Stefan Meinecke
97f77b1f69 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-05-17 14:16:01 +02:00
AsamK
4601e60118 Adapt containerfile to older apt versions 2026-05-16 11:14:17 +02:00
AsamK
fcf82b9318 Adapt containerfile to older apt versions 2026-05-16 10:48:12 +02:00
AsamK
9c8137fafa Update dependencies 2026-05-15 17:06:21 +02:00
AsamK
0a1531dcce Improve destination/source checks for incoming messages 2026-05-13 18:29:26 +02:00
AsamK
c10f618a3e Update gradle 2026-05-13 18:26:12 +02:00
AsamK
4a3d9d90a6 Update libsignal-service 2026-05-13 18:25:35 +02:00
AsamK
b4275414e1 Pass correct serviceId to SignalServiceCipher
Fixes #2036
2026-05-13 17:39:44 +02:00
Connor Lanigan
5f94b7b6d1
fix: Attempted immutable list modification causes runtime exception (#2038) 2026-05-05 10:15:21 +02:00
legacycode
dc43e44020
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.
2026-04-29 22:52:34 +02:00
AsamK
251bd2d87a Refactor profile key extraction 2026-04-27 16:36:22 +02:00
AsamK
a3fcda7598 Update kotlin jvm version for buildSrc 2026-04-27 16:27:15 +02:00
Patrick Dattilio
c9e2504349
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
2026-04-27 16:25:47 +02:00
dependabot[bot]
9b09df5f17
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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 19:55:35 +02:00
AsamK
5fe94ff44a Temporarily disable 26 build due to container issue 2026-04-23 21:58:10 +02:00
AsamK
6286a054eb Update libsignal-service 2026-04-23 20:37:46 +02:00
AsamK
e6635d1bb0 Prepare next release 2026-04-23 20:37:46 +02:00
25 changed files with 694 additions and 123 deletions

View File

@ -1,5 +1,7 @@
# 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.3" version = "0.14.4-SNAPSHOT"
} }
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_24) compilerOptions.jvmTarget.set(JvmTarget.JVM_25)
} }
java { java {
targetCompatibility = JavaVersion.VERSION_24 targetCompatibility = JavaVersion.VERSION_25
} }
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.12" version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",

View File

@ -1,8 +1,9 @@
[versions] [versions]
slf4j = "2.0.17" slf4j = "2.0.18"
junit = "6.0.3" junit = "6.0.3"
micronaut-json-schema = "2.0.0-M8" micronaut-json-schema = "2.0.0"
micronaut-core = "4.9.3" micronaut-core = "5.0.0"
signal-service = "2.15.3_unofficial_146"
[libraries] [libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
@ -18,8 +19,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"
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" sqlite = "org.xerial:sqlite-jdbc:3.53.1.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,7 +1,9 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.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 with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
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
goto fail "%COMSPEC%" /c exit 1
: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
goto fail "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
@ -73,21 +73,10 @@ goto fail
@rem Execute Gradle @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 :exitWithErrorLevel
@rem End local scope for the variables with windows NT shell @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
if %ERRORLEVEL% equ 0 goto mainEnd "%COMSPEC%" /c exit %ERRORLEVEL%
: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.signalservice) implementation(libs.signalnetwork)
} else { } else {
implementation(libs.signalservice) { implementation(libs.signalnetwork) {
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;
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException; void 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(),
dataMessage.isEndSession(), false,
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().map(ServiceId::parseOrNull).orElse(null); final var serviceId = envelope.getSourceServiceId();
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,16 +558,24 @@ 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);
final var recipientId = account.getRecipientResolver().resolveRecipient(serviceId); storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
final var profileStore = account.getProfileStore(); }
if (profileStore.getProfileKey(recipientId) != null) { for (var member : group.requestingMembers) {
// We already have a profile key, not updating it from a non-authoritative source final var serviceId = ServiceId.parseOrThrow(member.aciBytes);
continue; storeProfileKeyIfMissing(serviceId, member.profileKey.toByteArray());
} }
try { }
profileStore.storeProfileKey(recipientId, new ProfileKey(member.profileKey.toByteArray()));
} catch (InvalidInputException ignored) { 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) {
// 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,15 +140,30 @@ 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;
envelope.getSourceServiceId().map(ServiceId::parseOrNull) if (envelope.getSourceServiceId() != null) {
// 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
.ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId)); account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId());
}
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());
@ -173,7 +188,13 @@ 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());
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()) 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.");
@ -311,7 +332,12 @@ 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 var destination = getDestination(envelope); final DeviceAddress destination;
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);
@ -874,11 +900,6 @@ 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();
@ -1047,7 +1068,7 @@ public final class IncomingMessageHandler {
} }
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) { 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) { if (!envelope.isUnidentifiedSender() && serviceId != null) {
return new SignalServiceAddress(serviceId); return new SignalServiceAddress(serviceId);
} else if (content != null) { } else if (content != null) {
@ -1058,7 +1079,7 @@ public final class IncomingMessageHandler {
} }
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) { 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) { if (!envelope.isUnidentifiedSender() && serviceId != null) {
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId), return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId),
serviceId, serviceId,
@ -1070,10 +1091,13 @@ public final class IncomingMessageHandler {
} }
} }
private DeviceAddress getDestination(SignalServiceEnvelope envelope) { private DeviceAddress getDestination(SignalServiceEnvelope envelope) throws InvalidMessageException {
final var destination = envelope.getDestinationServiceId(); final var destination = envelope.getDestinationServiceId();
if (destination == null || destination.isUnknown()) { 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), return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
destination, destination,

View File

@ -9,7 +9,6 @@ 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;
@ -150,10 +149,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 recipientId = envelope1.getSourceServiceId() final var sourceServiceId = envelope1.getSourceServiceId();
.map(ServiceId::parseOrNull) final var recipientId = sourceServiceId == null
.map(s -> account.getRecipientResolver().resolveRecipient(s)) ? null
.orElse(null); : account.getRecipientResolver().resolveRecipient(sourceServiceId);
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);
@ -238,7 +237,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().isEmpty() && address.aci().isPresent()) { if (envelope.getSourceServiceId() == null && 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 {
@ -292,7 +291,7 @@ public class ReceiveHelper {
cachedMessage.delete(); cachedMessage.delete();
return null; return null;
} }
if (envelope.getSourceServiceId().isEmpty()) { if (envelope.getSourceServiceId() == null) {
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,6 +17,9 @@ 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;
@ -25,9 +28,6 @@ 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 StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> { case StorageServiceService.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 StorageServiceRepository.StorageRecordResult.NetworkError networkError -> case StorageServiceService.StorageRecordResult.NetworkError networkError ->
throw networkError.getException(); throw networkError.getException();
case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError -> case StorageServiceService.StorageRecordResult.StatusCodeError statusCodeError ->
throw statusCodeError.getException(); throw statusCodeError.getException();
case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords(); case StorageServiceService.StorageRecordResult.Success success -> success.getRecords();
default -> throw new IllegalStateException("Unexpected value: " + result); default -> throw new IllegalStateException("Unexpected value: " + result);
}; };
} }

View File

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

View File

@ -6,6 +6,13 @@ 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;
@ -15,26 +22,19 @@ 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 StorageServiceRepository getStorageServiceRepository() { public StorageServiceService getStorageServiceRepository() {
return new StorageServiceRepository(getStorageServiceApi()); return new StorageServiceService(getStorageServiceApi());
} }
public CertificateApi getCertificateApi() { public CertificateApi getCertificateApi() {
@ -368,7 +368,8 @@ 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 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(); 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)" \
&& apt install -y make asciidoc-base --update --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 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,8 +144,7 @@ public class SendCommand implements JsonRpcLocalCommand {
} }
try { try {
final var results = m.sendEndSessionMessage(singleRecipients); 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,9 +543,8 @@ public class DbusManagerImpl implements Manager {
} }
@Override @Override
public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException { public void 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,8 +432,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
@Override @Override
public void sendEndSessionMessage(final List<String> recipients) { public void sendEndSessionMessage(final List<String> recipients) {
try { try {
final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); 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,6 +169,12 @@ 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,6 +33,7 @@ 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 {
@ -102,7 +103,9 @@ 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().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); callEventHandlers.put(subscriptionId, listeners);
return subscriptionId; return subscriptionId;
} }
@ -157,7 +160,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))
.toList(); .collect(Collectors.toCollection(ArrayList::new));
receiveHandlers.put(subscriptionId, handlers); receiveHandlers.put(subscriptionId, handlers);
return subscriptionId; return subscriptionId;

View File

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