diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ad107d..0ab8b90f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +### Fixed + +- Sending to large groups is no longer slowed down by members that are already known to be unregistered; they are skipped instead of being retried via the legacy 1:1 send path on every send. + +## [0.14.5] - 2026-06-11 + +### Changed + +- Disable host validation when binding on 0.0.0.0 +- Use new SVR2 enclave for PINs + +### Fixed + +- Receiving unidentified sender messages after signal server change + ## [0.14.4] - 2026-05-23 ### Added diff --git a/build.gradle.kts b/build.gradle.kts index d4f97eb6..26fdc7f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,12 +5,12 @@ plugins { application eclipse `check-lib-versions` - id("org.graalvm.buildtools.native") version "1.0.0" + id("org.graalvm.buildtools.native") version "1.1.3" } allprojects { group = "org.asamk" - version = "0.14.5-SNAPSHOT" + version = "0.14.6-SNAPSHOT" } java { diff --git a/data/org.asamk.SignalCli.metainfo.xml b/data/org.asamk.SignalCli.metainfo.xml index 231492be..2af90518 100644 --- a/data/org.asamk.SignalCli.metainfo.xml +++ b/data/org.asamk.SignalCli.metainfo.xml @@ -45,6 +45,9 @@ intense + + https://github.com/AsamK/signal-cli/releases/tag/v0.14.5 + https://github.com/AsamK/signal-cli/releases/tag/v0.14.4 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef39a50d..231496cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ slf4j = "2.0.18" junit = "6.1.0" micronaut-json-schema = "2.0.1" micronaut-core = "5.0.0" -signal-service = "2.15.3_unofficial_147" +signal-service = "2.15.3_unofficial_148" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 548c584b..14239a56 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -30,7 +30,9 @@ class LiveConfig { private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder() .decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6"); private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; - private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb"; + private static final String SVR2_MRENCLAVE = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4"; + private static final String SVR2_MRENCLAVE_LEGACY = "1240acbd4aa26974184844c8a46b1022d3957ac8a76c1fd8f5b1a15141ee0708"; + private static final String SVR2_MRENCLAVE_OLD_LEGACY = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb"; private static final String URL = "https://chat.signal.org"; private static final String CDN_URL = "https://cdn.signal.org"; @@ -93,7 +95,7 @@ class LiveConfig { createDefaultServiceConfiguration(interceptors), getUnidentifiedSenderTrustRoots(), CDSI_MRENCLAVE, - List.of(SVR2_MRENCLAVE)); + List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_OLD_LEGACY)); } private LiveConfig() { diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index c2109f86..9b71f6fa 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -31,7 +31,8 @@ public class ServiceConfig { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { final var attachmentBackfill = !isPrimaryDevice; final var spqr = true; - return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr); + final var usernameSyncChangeMessage = !isPrimaryDevice; + return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr, usernameSyncChangeMessage); } public static ServiceEnvironmentConfig getServiceEnvironmentConfig( diff --git a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java index b5f1658b..6bec93af 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java @@ -30,7 +30,9 @@ class StagingConfig { private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder() .decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm"); private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; - private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036"; + private static final String SVR2_MRENCLAVE = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302"; + private static final String SVR2_MRENCLAVE_LEGACY = "97f151f6ed078edbbfd72fa9cae694dcc08353f1f5e8d9ccd79a971b10ffc535"; + private static final String SVR2_MRENCLAVE_OLD_LEGACY = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036"; private static final String URL = "https://chat.staging.signal.org"; private static final String CDN_URL = "https://cdn-staging.signal.org"; @@ -93,7 +95,7 @@ class StagingConfig { createDefaultServiceConfiguration(interceptors), getUnidentifiedSenderTrustRoots(), CDSI_MRENCLAVE, - List.of(SVR2_MRENCLAVE)); + List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_OLD_LEGACY)); } private StagingConfig() { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java index ac0a2a49..3d8efb72 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java @@ -649,7 +649,7 @@ public class CallManager implements AutoCloseable { case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY; default -> HangupMessage.Type.NORMAL; }; - var hangupMessage = new HangupMessage(state.callId, type, state.deviceId); + var hangupMessage = new HangupMessage(state.callId, type, 0); var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId); final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId); logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId)); @@ -711,7 +711,7 @@ public class CallManager implements AutoCloseable { && !"rejected".equals(reason) && !"remote_busy".equals(reason) && !"ringrtc_hangup".equals(reason)) { - var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, state.deviceId); + var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, 0); var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null); final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId); if (!result.isSuccess()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 64e3a764..308102a0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -17,6 +17,7 @@ import org.asamk.signal.manager.util.PaymentUtils; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.Utils; import org.jetbrains.annotations.Nullable; +import org.signal.core.util.ExpiringProfileCredentialUtil; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; @@ -32,7 +33,6 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.services.ProfileService; -import org.whispersystems.signalservice.api.util.ExpiringProfileCredentialUtil; import java.io.IOException; import java.io.OutputStream; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index d4f54945..87e67b7a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -507,9 +507,32 @@ public class SendHelper { ) throws IOException { long startTime = System.currentTimeMillis(); - final var addressesMap = recipientIds.stream() + // Recipients that are already known to be unregistered are skipped here. + // Otherwise every group send re-attempts them via the slow legacy 1:1 + // fan-out, which on large groups can take tens of seconds (and time out). + // The unregistered flag is maintained independently by profile/CDS + // discovery, which clears it once a recipient registers again, so they + // are re-included automatically. An unregisteredFailure result is still + // returned for each skipped recipient, so callers see them unchanged. + final var skippedResults = new ArrayList(); + final Set targetRecipientIds; + final var unregisteredRecipientIds = account.getRecipientStore().getUnregisteredRecipientIds(recipientIds); + if (unregisteredRecipientIds.isEmpty()) { + targetRecipientIds = recipientIds; + } else { + logger.debug("Skipping {} known-unregistered recipient(s) in group send.", + unregisteredRecipientIds.size()); + targetRecipientIds = new HashSet<>(recipientIds); + targetRecipientIds.removeAll(unregisteredRecipientIds); + for (final var recipientId : unregisteredRecipientIds) { + skippedResults.add(SendMessageResult.unregisteredFailure(context.getRecipientHelper() + .resolveSignalServiceAddress(recipientId))); + } + } + + final var addressesMap = targetRecipientIds.stream() .collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress)); - final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds); + final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(targetRecipientIds); final var groupSendEndorsementsResult = getGroupSendEndorsements(groupInfo); final var groupSecretParams = groupInfo instanceof GroupInfoV2 gv2 ? GroupSecretParams.deriveFromMasterKey((gv2.getMasterKey())) @@ -523,7 +546,7 @@ public class SendHelper { : groupSendEndorsementsResult.first(); Set senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null ? Set.of() - : recipientIds.stream() + : targetRecipientIds.stream() .filter(s -> this.isSenderKeyCapable(s, addressesMap.get(s), unidentifiedAccessesMap.get(s), @@ -533,7 +556,7 @@ public class SendHelper { logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size()); senderKeyTargets = Set.of(); } else { - logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), recipientIds.size()); + logger.debug("Can use sender key for {}/{} recipients.", senderKeyTargets.size(), targetRecipientIds.size()); } final var allResults = new ArrayList(recipientIds.size()); @@ -569,9 +592,9 @@ public class SendHelper { } } - final var legacyTargets = new HashSet<>(recipientIds); + final var legacyTargets = new HashSet<>(targetRecipientIds); legacyTargets.removeAll(senderKeyTargets); - final boolean onlyTargetIsSelfWithLinkedDevice = recipientIds.isEmpty() && account.isMultiDevice(); + final boolean onlyTargetIsSelfWithLinkedDevice = targetRecipientIds.isEmpty() && account.isMultiDevice(); if (!legacyTargets.isEmpty() || onlyTargetIsSelfWithLinkedDevice) { if (!legacyTargets.isEmpty()) { @@ -605,6 +628,7 @@ public class SendHelper { isRecipientUpdate || !allResults.isEmpty()); allResults.addAll(results); } + allResults.addAll(skippedResults); final var duration = Duration.ofMillis(System.currentTimeMillis() - startTime); logger.debug("Sending took {}", duration.toString()); return allResults; diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 55795714..6eccb005 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.internal; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.util.Utils; +import org.signal.core.util.UptimeSleepTimer; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.SignalProtocolAddress; @@ -40,7 +41,6 @@ import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.storage.StorageServiceApi; import org.whispersystems.signalservice.api.svr.SecureValueRecovery; import org.whispersystems.signalservice.api.util.CredentialsProvider; -import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.websocket.LibSignalChatConnection; diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index 656b6646..10f7a2a9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -1,10 +1,10 @@ package org.asamk.signal.manager.internal; import org.jetbrains.annotations.NotNull; +import org.signal.core.util.SleepTimer; import org.signal.network.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.websocket.HealthMonitor; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index c80b3454..90ea1baa 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -463,6 +463,40 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } } + /** + * Returns the subset of the given recipients that are currently known to be + * unregistered (i.e. have an unregistered timestamp set). + *

+ * These can be skipped when sending group messages; otherwise every send + * re-attempts them via the slow legacy 1:1 fan-out. The unregistered flag is + * maintained independently by profile/CDS discovery and cleared again once a + * recipient registers, so they are re-included automatically. + */ + public Set getUnregisteredRecipientIds(final Set recipientIds) { + if (recipientIds.isEmpty()) { + return Set.of(); + } + final var recipientIdsCommaSeparated = recipientIds.stream() + .map(recipientId -> String.valueOf(recipientId.id())) + .collect(Collectors.joining(",")); + final var sql = ( + """ + SELECT r._id + FROM %s r + WHERE r.unregistered_timestamp IS NOT NULL AND r._id IN (%s) + """ + ).formatted(TABLE_RECIPIENT, recipientIdsCommaSeparated); + try (final var connection = database.getConnection()) { + try (final var statement = connection.prepareStatement(sql)) { + try (var result = Utils.executeQueryForStream(statement, this::getRecipientIdFromResultSet)) { + return result.collect(Collectors.toSet()); + } + } + } catch (SQLException e) { + throw new RuntimeException("Failed read from recipient store", e); + } + } + public Set getAllNumbers() { final var sql = ( """ diff --git a/libsignal-version b/libsignal-version index 6bc8ee7b..0a4eab77 100644 --- a/libsignal-version +++ b/libsignal-version @@ -1 +1 @@ -0.94.1 +0.94.4 diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index da2e5422..c6d9b40f 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -8,7 +8,7 @@ public class BaseConfig { public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT")) - .orElse("Signal-Android/8.12.1"); + .orElse("Signal-Android/8.15.0"); static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index dcaf06f3..d179aed0 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -310,6 +310,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (callMessage.hangup().isPresent()) { var hangupMessage = callMessage.hangup().get(); writer.println("Hangup message: {}", callIdUnsigned(hangupMessage.id())); + writer.println("- type: {}", hangupMessage.type()); + writer.println("- device id: {}", hangupMessage.deviceId()); } if (!callMessage.iceUpdate().isEmpty()) { writer.println("Ice update messages:"); diff --git a/src/main/java/org/asamk/signal/http/HttpServerHandler.java b/src/main/java/org/asamk/signal/http/HttpServerHandler.java index 551c5b04..a6fc9f9e 100644 --- a/src/main/java/org/asamk/signal/http/HttpServerHandler.java +++ b/src/main/java/org/asamk/signal/http/HttpServerHandler.java @@ -73,7 +73,13 @@ public class HttpServerHandler implements AutoCloseable { server.start(); logger.info("Started HTTP server on {}", address); - logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts); + // If we're listening on any local address (0.0.0.0 or ::), skip Host header validation + final var addr = address == null ? null : address.getAddress(); + if (addr != null && addr.isAnyLocalAddress()) { + logger.warn("HTTP server has no authentication; Host header validation DISABLED because listening on {}", address); + } else { + logger.warn("HTTP server has no authentication; Host header is pinned to {}", allowedHosts); + } } @Override @@ -306,6 +312,18 @@ public class HttpServerHandler implements AutoCloseable { } private boolean isHostAllowed(final HttpExchange httpExchange) { + // If the server is bound to any local address (0.0.0.0 or ::), skip host header validation + if (address != null) { + final var addr = address.getAddress(); + if (addr != null && addr.isAnyLocalAddress()) { + return true; + } + final var hostStr = address.getHostString(); + if ("0.0.0.0".equals(hostStr) || "::".equals(hostStr)) { + return true; + } + } + final var hostHeader = httpExchange.getRequestHeaders().getFirst("Host"); if (hostHeader == null || hostHeader.isEmpty()) { return false; diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index dff20afe..e69e2df6 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -776,6 +776,20 @@ "type": "java.lang.Class", "jniAccessible": true, "methods": [ + { + "name": "forName", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "forName", + "parameterTypes": [ + "java.lang.String", + "boolean", + "java.lang.ClassLoader" + ] + }, { "name": "getCanonicalName", "parameterTypes": [] @@ -784,6 +798,10 @@ "name": "getClassLoader", "parameterTypes": [] }, + { + "name": "getName", + "parameterTypes": [] + }, { "name": "getPermittedSubclasses", "parameterTypes": [] @@ -819,7 +837,27 @@ ] }, { - "type": "java.lang.ClassNotFoundException" + "type": "java.lang.ClassNotFoundException", + "jniAccessible": true, + "methods": [ + { + "name": "", + "parameterTypes": [] + }, + { + "name": "", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "", + "parameterTypes": [ + "java.lang.String", + "java.lang.Throwable" + ] + } + ] }, { "type": "java.lang.ClassValue" @@ -959,6 +997,18 @@ "name": "currentThread", "parameterTypes": [] }, + { + "name": "getContextClassLoader", + "parameterTypes": [] + }, + { + "name": "getId", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, { "name": "getStackTrace", "parameterTypes": [] @@ -966,6 +1016,18 @@ { "name": "isVirtual", "parameterTypes": [] + }, + { + "name": "setContextClassLoader", + "parameterTypes": [ + "java.lang.ClassLoader" + ] + }, + { + "name": "setName", + "parameterTypes": [ + "java.lang.String" + ] } ] }, @@ -979,10 +1041,18 @@ "java.lang.Throwable" ] }, + { + "name": "getCause", + "parameterTypes": [] + }, { "name": "getMessage", "parameterTypes": [] }, + { + "name": "getStackTrace", + "parameterTypes": [] + }, { "name": "getSuppressed", "parameterTypes": [] @@ -1586,7 +1656,42 @@ }, { "type": "kotlin.reflect.jvm.internal.impl.resolve.scopes.DescriptorKindFilter", - "allPublicFields": true + "allPublicFields": true, + "fields": [ + { + "name": "ALL" + }, + { + "name": "CALLABLES" + }, + { + "name": "CLASSIFIERS" + }, + { + "name": "Companion" + }, + { + "name": "FUNCTIONS" + }, + { + "name": "NON_SINGLETON_CLASSIFIERS" + }, + { + "name": "PACKAGES" + }, + { + "name": "SINGLETON_CLASSIFIERS" + }, + { + "name": "TYPE_ALIASES" + }, + { + "name": "VALUES" + }, + { + "name": "VARIABLES" + } + ] }, { "type": "kotlinx.coroutines.AwaitAll"