mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Update libsignal-service
This commit is contained in:
parent
2b150112ff
commit
f26a0d2891
18 changed files with 371 additions and 287 deletions
|
@ -10,7 +10,7 @@ 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.17"
|
logback = "ch.qos.logback:logback-classic:1.5.17"
|
||||||
|
|
||||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_118"
|
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_119"
|
||||||
sqlite = "org.xerial:sqlite-jdbc:3.49.1.0"
|
sqlite = "org.xerial:sqlite-jdbc:3.49.1.0"
|
||||||
hikari = "com.zaxxer:HikariCP:6.2.1"
|
hikari = "com.zaxxer:HikariCP:6.2.1"
|
||||||
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.12.0"
|
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.12.0"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.asamk.signal.manager;
|
package org.asamk.signal.manager;
|
||||||
|
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||||
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
||||||
|
@ -49,7 +51,6 @@ import org.asamk.signal.manager.api.UsernameStatus;
|
||||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -65,7 +66,7 @@ import java.util.Set;
|
||||||
public interface Manager extends Closeable {
|
public interface Manager extends Closeable {
|
||||||
|
|
||||||
static boolean isValidNumber(final String e164Number, final String countryCode) {
|
static boolean isValidNumber(final String e164Number, final String countryCode) {
|
||||||
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
|
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isSignalClientAvailable() {
|
static boolean isSignalClientAvailable() {
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
public class InvalidNumberException extends Exception {
|
public class InvalidNumberException extends Exception {
|
||||||
|
|
||||||
InvalidNumberException(String message) {
|
public InvalidNumberException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package org.asamk.signal.manager.api;
|
package org.asamk.signal.manager.api;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -24,32 +24,28 @@ public sealed interface RecipientIdentifier {
|
||||||
sealed interface Single extends RecipientIdentifier {
|
sealed interface Single extends RecipientIdentifier {
|
||||||
|
|
||||||
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
|
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
|
||||||
try {
|
if (UuidUtil.isUuid(identifier)) {
|
||||||
if (UuidUtil.isUuid(identifier)) {
|
return new Uuid(UUID.fromString(identifier));
|
||||||
return new Uuid(UUID.fromString(identifier));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier.startsWith("PNI:")) {
|
|
||||||
final var pni = identifier.substring(4);
|
|
||||||
if (!UuidUtil.isUuid(pni)) {
|
|
||||||
throw new InvalidNumberException("Invalid PNI");
|
|
||||||
}
|
|
||||||
return new Pni(UUID.fromString(pni));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier.startsWith("u:")) {
|
|
||||||
return new Username(identifier.substring(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
|
||||||
if (!normalizedNumber.equals(identifier)) {
|
|
||||||
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
|
||||||
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
|
|
||||||
}
|
|
||||||
return new Number(normalizedNumber);
|
|
||||||
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
|
|
||||||
throw new InvalidNumberException(e.getMessage(), e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (identifier.startsWith("PNI:")) {
|
||||||
|
final var pni = identifier.substring(4);
|
||||||
|
if (!UuidUtil.isUuid(pni)) {
|
||||||
|
throw new InvalidNumberException("Invalid PNI");
|
||||||
|
}
|
||||||
|
return new Pni(UUID.fromString(pni));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identifier.startsWith("u:")) {
|
||||||
|
return new Username(identifier.substring(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
||||||
|
if (!normalizedNumber.equals(identifier)) {
|
||||||
|
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
||||||
|
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
|
||||||
|
}
|
||||||
|
return new Number(normalizedNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Single fromAddress(RecipientAddress address) {
|
static Single fromAddress(RecipientAddress address) {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||||
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.push.SignedPreKeyEntity;
|
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||||
|
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
|
@ -50,7 +51,7 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import okio.ByteString;
|
import okio.ByteString;
|
||||||
|
@ -289,12 +290,13 @@ public class AccountHelper {
|
||||||
context.getPinHelper(),
|
context.getPinHelper(),
|
||||||
(sessionId1, verificationCode1, registrationLock) -> {
|
(sessionId1, verificationCode1, registrationLock) -> {
|
||||||
final var registrationApi = dependencies.getRegistrationApi();
|
final var registrationApi = dependencies.getRegistrationApi();
|
||||||
|
final var accountApi = dependencies.getAccountApi();
|
||||||
try {
|
try {
|
||||||
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
|
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
|
||||||
} catch (AlreadyVerifiedException e) {
|
} catch (AlreadyVerifiedException e) {
|
||||||
// Already verified so can continue changing number
|
// Already verified so can continue changing number
|
||||||
}
|
}
|
||||||
return handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
|
return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
|
||||||
null,
|
null,
|
||||||
newNumber,
|
newNumber,
|
||||||
registrationLock,
|
registrationLock,
|
||||||
|
@ -378,7 +380,7 @@ public class AccountHelper {
|
||||||
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
|
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
|
||||||
}
|
}
|
||||||
|
|
||||||
final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
|
final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||||
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
||||||
if (hashIndex == -1) {
|
if (hashIndex == -1) {
|
||||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||||
|
@ -388,7 +390,7 @@ public class AccountHelper {
|
||||||
logger.debug("[reserveUsername] Successfully reserved username.");
|
logger.debug("[reserveUsername] Successfully reserved username.");
|
||||||
final var username = candidates.get(hashIndex);
|
final var username = candidates.get(hashIndex);
|
||||||
|
|
||||||
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
|
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||||
account.setUsername(username.getUsername());
|
account.setUsername(username.getUsername());
|
||||||
account.setUsernameLink(linkComponents);
|
account.setUsernameLink(linkComponents);
|
||||||
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
||||||
|
@ -396,6 +398,40 @@ public class AccountHelper {
|
||||||
logger.debug("[confirmUsername] Successfully confirmed username.");
|
logger.debug("[confirmUsername] Successfully confirmed username.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
|
||||||
|
try {
|
||||||
|
Username.UsernameLink link = username.generateLink();
|
||||||
|
return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
|
||||||
|
} catch (BaseUsernameException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
|
||||||
|
try {
|
||||||
|
Username.UsernameLink link = username.generateLink();
|
||||||
|
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
|
||||||
|
|
||||||
|
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||||
|
} catch (BaseUsernameException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UsernameLinkComponents reclaimUsernameAndLink(
|
||||||
|
Username username,
|
||||||
|
UsernameLinkComponents linkComponents
|
||||||
|
) throws IOException {
|
||||||
|
try {
|
||||||
|
Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
|
||||||
|
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
|
||||||
|
|
||||||
|
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||||
|
} catch (BaseUsernameException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void refreshCurrentUsername() throws IOException, BaseUsernameException {
|
public void refreshCurrentUsername() throws IOException, BaseUsernameException {
|
||||||
final var localUsername = account.getUsername();
|
final var localUsername = account.getUsername();
|
||||||
if (localUsername == null) {
|
if (localUsername == null) {
|
||||||
|
@ -438,14 +474,14 @@ public class AccountHelper {
|
||||||
final var usernameLink = account.getUsernameLink();
|
final var usernameLink = account.getUsernameLink();
|
||||||
|
|
||||||
if (usernameLink == null) {
|
if (usernameLink == null) {
|
||||||
dependencies.getAccountManager()
|
handleResponseException(dependencies.getAccountApi()
|
||||||
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
|
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
|
||||||
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
||||||
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
|
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||||
account.setUsernameLink(linkComponents);
|
account.setUsernameLink(linkComponents);
|
||||||
logger.debug("[confirmUsername] Successfully confirmed existing username.");
|
logger.debug("[confirmUsername] Successfully confirmed existing username.");
|
||||||
} else {
|
} else {
|
||||||
final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink);
|
final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
|
||||||
account.setUsernameLink(linkComponents);
|
account.setUsernameLink(linkComponents);
|
||||||
logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
|
logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
|
||||||
}
|
}
|
||||||
|
@ -455,7 +491,7 @@ public class AccountHelper {
|
||||||
private void tryToSetUsernameLink(Username username) {
|
private void tryToSetUsernameLink(Username username) {
|
||||||
for (var i = 1; i < 4; i++) {
|
for (var i = 1; i < 4; i++) {
|
||||||
try {
|
try {
|
||||||
final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
|
final var linkComponents = createUsernameLink(username);
|
||||||
account.setUsernameLink(linkComponents);
|
account.setUsernameLink(linkComponents);
|
||||||
break;
|
break;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -465,9 +501,8 @@ public class AccountHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUsername() throws IOException {
|
public void deleteUsername() throws IOException {
|
||||||
dependencies.getAccountManager().deleteUsernameLink();
|
handleResponseException(dependencies.getAccountApi().deleteUsername());
|
||||||
account.setUsernameLink(null);
|
account.setUsernameLink(null);
|
||||||
dependencies.getAccountManager().deleteUsername();
|
|
||||||
account.setUsername(null);
|
account.setUsername(null);
|
||||||
logger.debug("[deleteUsername] Successfully deleted the username.");
|
logger.debug("[deleteUsername] Successfully deleted the username.");
|
||||||
}
|
}
|
||||||
|
@ -479,7 +514,7 @@ public class AccountHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateAccountAttributes() throws IOException {
|
public void updateAccountAttributes() throws IOException {
|
||||||
dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
|
handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
|
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
|
||||||
|
@ -510,8 +545,8 @@ public class AccountHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeLinkedDevices(int deviceId) throws IOException {
|
public void removeLinkedDevices(int deviceId) throws IOException {
|
||||||
dependencies.getAccountManager().removeDevice(deviceId);
|
handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
|
||||||
var devices = dependencies.getAccountManager().getDevices();
|
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -519,14 +554,16 @@ public class AccountHelper {
|
||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
||||||
dependencies.getAccountManager().enableRegistrationLock(masterKey);
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegistrationPin(String pin) throws IOException {
|
public void setRegistrationPin(String pin) throws IOException {
|
||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
||||||
dependencies.getAccountManager().enableRegistrationLock(masterKey);
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||||
|
|
||||||
account.setRegistrationLockPin(pin);
|
account.setRegistrationLockPin(pin);
|
||||||
updateAccountAttributes();
|
updateAccountAttributes();
|
||||||
|
@ -535,7 +572,7 @@ public class AccountHelper {
|
||||||
public void removeRegistrationPin() throws IOException {
|
public void removeRegistrationPin() throws IOException {
|
||||||
// Remove KBS Pin
|
// Remove KBS Pin
|
||||||
context.getPinHelper().removeRegistrationLockPin();
|
context.getPinHelper().removeRegistrationLockPin();
|
||||||
dependencies.getAccountManager().disableRegistrationLock();
|
handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
|
||||||
|
|
||||||
account.setRegistrationLockPin(null);
|
account.setRegistrationLockPin(null);
|
||||||
}
|
}
|
||||||
|
@ -544,7 +581,7 @@ public class AccountHelper {
|
||||||
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
||||||
// If this is the primary device, other users can't send messages to this number anymore.
|
// If this is the primary device, other users can't send messages to this number anymore.
|
||||||
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
||||||
dependencies.getAccountManager().setGcmId(Optional.empty());
|
handleResponseException(dependencies.getAccountApi().clearFcmToken());
|
||||||
|
|
||||||
account.setRegistered(false);
|
account.setRegistered(false);
|
||||||
unregisteredListener.call();
|
unregisteredListener.call();
|
||||||
|
|
|
@ -11,10 +11,10 @@ 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.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||||
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
|
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
|
||||||
public class ReceiveHelper {
|
public class ReceiveHelper {
|
||||||
|
@ -94,9 +93,8 @@ public class ReceiveHelper {
|
||||||
// Use a Map here because java Set doesn't have a get method ...
|
// Use a Map here because java Set doesn't have a get method ...
|
||||||
Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
|
Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
|
||||||
|
|
||||||
final var signalWebSocket = dependencies.getSignalWebSocket();
|
final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket();
|
||||||
final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(),
|
final var webSocketStateDisposable = signalWebSocket.getState()
|
||||||
signalWebSocket.getWebSocketState())
|
|
||||||
.subscribeOn(Schedulers.computation())
|
.subscribeOn(Schedulers.computation())
|
||||||
.observeOn(Schedulers.computation())
|
.observeOn(Schedulers.computation())
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
@ -116,7 +114,7 @@ public class ReceiveHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void receiveMessagesInternal(
|
private void receiveMessagesInternal(
|
||||||
final SignalWebSocket signalWebSocket,
|
final SignalWebSocket.AuthenticatedWebSocket signalWebSocket,
|
||||||
Duration timeout,
|
Duration timeout,
|
||||||
boolean returnOnTimeout,
|
boolean returnOnTimeout,
|
||||||
Integer maxMessages,
|
Integer maxMessages,
|
||||||
|
|
|
@ -10,13 +10,13 @@ import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.signal.libsignal.usernames.Username;
|
import org.signal.libsignal.usernames.Username;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
||||||
import org.whispersystems.signalservice.api.services.CdsiV2Service;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -27,6 +27,7 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
|
import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
|
|
||||||
public class RecipientHelper {
|
public class RecipientHelper {
|
||||||
|
|
||||||
|
@ -108,7 +109,7 @@ public class RecipientHelper {
|
||||||
}
|
}
|
||||||
if (forceRefresh) {
|
if (forceRefresh) {
|
||||||
try {
|
try {
|
||||||
final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername);
|
final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
|
||||||
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
|
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
||||||
|
@ -119,7 +120,7 @@ public class RecipientHelper {
|
||||||
}
|
}
|
||||||
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
|
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
|
||||||
try {
|
try {
|
||||||
return dependencies.getAccountManager().getAciByUsername(finalUsername);
|
return handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -130,8 +131,8 @@ public class RecipientHelper {
|
||||||
try {
|
try {
|
||||||
final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
|
final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
|
||||||
final var components = usernameLinkUrl.getComponents();
|
final var components = usernameLinkUrl.getComponents();
|
||||||
final var encryptedUsername = dependencies.getAccountManager()
|
final var encryptedUsername = handleResponseException(dependencies.getUsernameApi()
|
||||||
.getEncryptedUsernameFromLinkServerId(components.getServerId());
|
.getEncryptedUsernameFromLinkServerId(components.getServerId()));
|
||||||
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
|
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
|
||||||
|
|
||||||
return Username.fromLink(link);
|
return Username.fromLink(link);
|
||||||
|
@ -234,13 +235,14 @@ public class RecipientHelper {
|
||||||
|
|
||||||
final CdsiV2Service.Response response;
|
final CdsiV2Service.Response response;
|
||||||
try {
|
try {
|
||||||
response = dependencies.getAccountManager()
|
response = handleResponseException(dependencies.getCdsApi()
|
||||||
.getRegisteredUsersWithCdsi(token.isEmpty() ? Set.of() : previousNumbers,
|
.getRegisteredUsers(token.isEmpty() ? Set.of() : previousNumbers,
|
||||||
newNumbers,
|
newNumbers,
|
||||||
account.getRecipientStore().getServiceIdToProfileKeyMap(),
|
account.getRecipientStore().getServiceIdToProfileKeyMap(),
|
||||||
token,
|
token,
|
||||||
null,
|
null,
|
||||||
dependencies.getLibSignalNetwork(),
|
dependencies.getLibSignalNetwork(),
|
||||||
|
false,
|
||||||
newToken -> {
|
newToken -> {
|
||||||
if (isPartialRefresh) {
|
if (isPartialRefresh) {
|
||||||
account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
|
account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
|
||||||
|
@ -256,7 +258,7 @@ public class RecipientHelper {
|
||||||
account.setCdsiToken(newToken);
|
account.setCdsiToken(newToken);
|
||||||
account.setLastRecipientsRefresh(System.currentTimeMillis());
|
account.setLastRecipientsRefresh(System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
} catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) {
|
} catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) {
|
||||||
account.setCdsiToken(null);
|
account.setCdsiToken(null);
|
||||||
account.getCdsiStore().clearAll();
|
account.getCdsiStore().clearAll();
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.asamk.signal.manager.api.IdentityVerificationCode;
|
||||||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||||
|
import org.asamk.signal.manager.api.InvalidNumberException;
|
||||||
import org.asamk.signal.manager.api.InvalidStickerException;
|
import org.asamk.signal.manager.api.InvalidStickerException;
|
||||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||||
import org.asamk.signal.manager.api.LastGroupAdminException;
|
import org.asamk.signal.manager.api.LastGroupAdminException;
|
||||||
|
@ -87,12 +88,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPack;
|
||||||
import org.asamk.signal.manager.util.AttachmentUtils;
|
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.asamk.signal.manager.util.MimeUtils;
|
import org.asamk.signal.manager.util.MimeUtils;
|
||||||
|
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
||||||
import org.asamk.signal.manager.util.StickerUtils;
|
import org.asamk.signal.manager.util.StickerUtils;
|
||||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
||||||
|
@ -106,8 +107,6 @@ import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhauste
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
import org.whispersystems.signalservice.internal.util.Hex;
|
import org.whispersystems.signalservice.internal.util.Hex;
|
||||||
import org.whispersystems.signalservice.internal.util.Util;
|
import org.whispersystems.signalservice.internal.util.Util;
|
||||||
|
@ -132,7 +131,6 @@ import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -142,6 +140,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
import okio.Utf8;
|
import okio.Utf8;
|
||||||
|
|
||||||
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
|
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
|
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
|
||||||
|
|
||||||
public class ManagerImpl implements Manager {
|
public class ManagerImpl implements Manager {
|
||||||
|
@ -171,15 +170,7 @@ public class ManagerImpl implements Manager {
|
||||||
) {
|
) {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
|
|
||||||
final var sessionLock = new SignalSessionLock() {
|
final var sessionLock = new ReentrantSignalSessionLock();
|
||||||
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Lock acquire() {
|
|
||||||
LEGACY_LOCK.lock();
|
|
||||||
return LEGACY_LOCK::unlock;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
|
this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
|
||||||
userAgent,
|
userAgent,
|
||||||
account.getCredentialsProvider(),
|
account.getCredentialsProvider(),
|
||||||
|
@ -457,10 +448,10 @@ public class ManagerImpl implements Manager {
|
||||||
String challenge,
|
String challenge,
|
||||||
String captcha
|
String captcha
|
||||||
) throws IOException, CaptchaRejectedException {
|
) throws IOException, CaptchaRejectedException {
|
||||||
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
|
captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
|
handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha));
|
||||||
} catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) {
|
} catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) {
|
||||||
throw new CaptchaRejectedException();
|
throw new CaptchaRejectedException();
|
||||||
}
|
}
|
||||||
|
@ -468,7 +459,7 @@ public class ManagerImpl implements Manager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Device> getLinkedDevices() throws IOException {
|
public List<Device> getLinkedDevices() throws IOException {
|
||||||
var devices = dependencies.getAccountManager().getDevices();
|
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||||
account.setMultiDevice(devices.size() > 1);
|
account.setMultiDevice(devices.size() > 1);
|
||||||
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||||
return devices.stream().map(d -> {
|
return devices.stream().map(d -> {
|
||||||
|
@ -1594,7 +1585,8 @@ public class ManagerImpl implements Manager {
|
||||||
context.close();
|
context.close();
|
||||||
executor.close();
|
executor.close();
|
||||||
|
|
||||||
dependencies.getSignalWebSocket().disconnect();
|
dependencies.getAuthenticatedSignalWebSocket().disconnect();
|
||||||
|
dependencies.getUnauthenticatedSignalWebSocket().disconnect();
|
||||||
dependencies.getPushServiceSocket().close();
|
dependencies.getPushServiceSocket().close();
|
||||||
disposable.dispose();
|
disposable.dispose();
|
||||||
|
|
||||||
|
|
|
@ -29,12 +29,11 @@ import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
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.groupsv2.ClientZkOperations;
|
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
|
||||||
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.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
|
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
|
||||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||||
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
|
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
|
@ -58,7 +57,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
||||||
private final Consumer<Manager> newManagerListener;
|
private final Consumer<Manager> newManagerListener;
|
||||||
private final AccountsStore accountsStore;
|
private final AccountsStore accountsStore;
|
||||||
|
|
||||||
private final SignalServiceAccountManager accountManager;
|
private final ProvisioningApi provisioningApi;
|
||||||
private final IdentityKeyPair tempIdentityKey;
|
private final IdentityKeyPair tempIdentityKey;
|
||||||
private final String password;
|
private final String password;
|
||||||
|
|
||||||
|
@ -78,7 +77,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
||||||
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||||
password = KeyUtils.createPassword();
|
password = KeyUtils.createPassword();
|
||||||
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
|
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
|
||||||
final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE);
|
|
||||||
final var credentialsProvider = new DynamicCredentialsProvider(null,
|
final var credentialsProvider = new DynamicCredentialsProvider(null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
@ -89,21 +87,21 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
||||||
userAgent,
|
userAgent,
|
||||||
clientZkOperations.getProfileOperations(),
|
clientZkOperations.getProfileOperations(),
|
||||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||||
accountManager = new SignalServiceAccountManager(pushServiceSocket,
|
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent),
|
userAgent);
|
||||||
groupsV2Operations);
|
this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
||||||
var deviceUuid = accountManager.getNewDeviceUuid();
|
var deviceUuid = provisioningApi.getNewDeviceUuid();
|
||||||
|
|
||||||
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
||||||
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
|
var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
|
||||||
var number = ret.getNumber();
|
var number = ret.getNumber();
|
||||||
var aci = ret.getAci();
|
var aci = ret.getAci();
|
||||||
var pni = ret.getPni();
|
var pni = ret.getPni();
|
||||||
|
@ -160,7 +158,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
||||||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||||
|
|
||||||
logger.debug("Finishing new device registration");
|
logger.debug("Finishing new device registration");
|
||||||
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
||||||
account.getAccountAttributes(null),
|
account.getAccountAttributes(null),
|
||||||
aciPreKeys,
|
aciPreKeys,
|
||||||
pniPreKeys);
|
pniPreKeys);
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.asamk.signal.manager.internal;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
class ReentrantSignalSessionLock implements SignalSessionLock {
|
||||||
|
|
||||||
|
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Lock acquire() {
|
||||||
|
LEGACY_LOCK.lock();
|
||||||
|
return LEGACY_LOCK::unlock;
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,14 +32,11 @@ import org.asamk.signal.manager.helper.PinHelper;
|
||||||
import org.asamk.signal.manager.storage.SignalAccount;
|
import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.util.KeyUtils;
|
import org.asamk.signal.manager.util.KeyUtils;
|
||||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||||
import org.asamk.signal.manager.util.Utils;
|
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
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;
|
||||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
|
||||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||||
|
@ -48,13 +45,13 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
|
||||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
||||||
|
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||||
|
|
||||||
public class RegistrationManagerImpl implements RegistrationManager {
|
public class RegistrationManagerImpl implements RegistrationManager {
|
||||||
|
|
||||||
|
@ -199,7 +196,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||||
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||||
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
||||||
final var response = Utils.handleResponseException(registrationApi.registerAccount(null,
|
final var response = handleResponseException(registrationApi.registerAccount(null,
|
||||||
recoveryPassword,
|
recoveryPassword,
|
||||||
account.getAccountAttributes(null),
|
account.getAccountAttributes(null),
|
||||||
aciPreKeys,
|
aciPreKeys,
|
||||||
|
@ -221,8 +218,14 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||||
|
|
||||||
private boolean attemptReactivateAccount() {
|
private boolean attemptReactivateAccount() {
|
||||||
try {
|
try {
|
||||||
final var accountManager = createAuthenticatedSignalServiceAccountManager();
|
final var dependencies = new SignalDependencies(serviceEnvironmentConfig,
|
||||||
accountManager.setAccountAttributes(account.getAccountAttributes(null));
|
userAgent,
|
||||||
|
account.getCredentialsProvider(),
|
||||||
|
account.getSignalServiceDataStore(),
|
||||||
|
null,
|
||||||
|
new ReentrantSignalSessionLock());
|
||||||
|
handleResponseException(dependencies.getAccountApi()
|
||||||
|
.setAccountAttributes(account.getAccountAttributes(null)));
|
||||||
account.setRegistered(true);
|
account.setRegistered(true);
|
||||||
logger.info("Reactivated existing account, verify is not necessary.");
|
logger.info("Reactivated existing account, verify is not necessary.");
|
||||||
if (newManagerListener != null) {
|
if (newManagerListener != null) {
|
||||||
|
@ -241,17 +244,6 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignalServiceAccountManager createAuthenticatedSignalServiceAccountManager() {
|
|
||||||
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
|
|
||||||
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
|
||||||
account.getCredentialsProvider(),
|
|
||||||
userAgent,
|
|
||||||
clientZkOperations.getProfileOperations(),
|
|
||||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
|
||||||
final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE);
|
|
||||||
return new SignalServiceAccountManager(pushServiceSocket, null, groupsV2Operations);
|
|
||||||
}
|
|
||||||
|
|
||||||
private VerifyAccountResponse verifyAccountWithCode(
|
private VerifyAccountResponse verifyAccountWithCode(
|
||||||
final String sessionId,
|
final String sessionId,
|
||||||
final String verificationCode,
|
final String verificationCode,
|
||||||
|
@ -261,11 +253,11 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
||||||
try {
|
try {
|
||||||
Utils.handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
|
handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
|
||||||
} catch (AlreadyVerifiedException e) {
|
} catch (AlreadyVerifiedException e) {
|
||||||
// Already verified so can continue registering
|
// Already verified so can continue registering
|
||||||
}
|
}
|
||||||
return Utils.handleResponseException(registrationApi.registerAccount(sessionId,
|
return handleResponseException(registrationApi.registerAccount(sessionId,
|
||||||
null,
|
null,
|
||||||
account.getAccountAttributes(registrationLock),
|
account.getAccountAttributes(registrationLock),
|
||||||
aciPreKeys,
|
aciPreKeys,
|
||||||
|
|
|
@ -13,7 +13,8 @@ import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
import org.whispersystems.signalservice.api.account.AccountApi;
|
||||||
|
import org.whispersystems.signalservice.api.cds.CdsApi;
|
||||||
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;
|
||||||
|
@ -21,18 +22,18 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
||||||
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.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.WebSocketFactory;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
|
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||||
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
||||||
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
@ -58,6 +59,10 @@ public class SignalDependencies {
|
||||||
private boolean allowStories = true;
|
private boolean allowStories = true;
|
||||||
|
|
||||||
private SignalServiceAccountManager accountManager;
|
private SignalServiceAccountManager accountManager;
|
||||||
|
private AccountApi accountApi;
|
||||||
|
private RateLimitChallengeApi rateLimitChallengeApi;
|
||||||
|
private CdsApi cdsApi;
|
||||||
|
private UsernameApi usernameApi;
|
||||||
private GroupsV2Api groupsV2Api;
|
private GroupsV2Api groupsV2Api;
|
||||||
private RegistrationApi registrationApi;
|
private RegistrationApi registrationApi;
|
||||||
private LinkDeviceApi linkDeviceApi;
|
private LinkDeviceApi linkDeviceApi;
|
||||||
|
@ -66,9 +71,9 @@ public class SignalDependencies {
|
||||||
private ClientZkOperations clientZkOperations;
|
private ClientZkOperations clientZkOperations;
|
||||||
|
|
||||||
private PushServiceSocket pushServiceSocket;
|
private PushServiceSocket pushServiceSocket;
|
||||||
private ProvisioningSocket provisioningSocket;
|
|
||||||
private Network libSignalNetwork;
|
private Network libSignalNetwork;
|
||||||
private SignalWebSocket signalWebSocket;
|
private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket;
|
||||||
|
private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket;
|
||||||
private SignalServiceMessageReceiver messageReceiver;
|
private SignalServiceMessageReceiver messageReceiver;
|
||||||
private SignalServiceMessageSender messageSender;
|
private SignalServiceMessageSender messageSender;
|
||||||
|
|
||||||
|
@ -103,7 +108,12 @@ public class SignalDependencies {
|
||||||
this.registrationApi = null;
|
this.registrationApi = null;
|
||||||
this.secureValueRecovery = null;
|
this.secureValueRecovery = null;
|
||||||
}
|
}
|
||||||
getSignalWebSocket().forceNewWebSockets();
|
if (this.authenticatedSignalWebSocket != null) {
|
||||||
|
this.authenticatedSignalWebSocket.forceNewWebSocket();
|
||||||
|
}
|
||||||
|
if (this.unauthenticatedSignalWebSocket != null) {
|
||||||
|
this.unauthenticatedSignalWebSocket.forceNewWebSocket();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,12 +140,6 @@ public class SignalDependencies {
|
||||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
|
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProvisioningSocket getProvisioningSocket() {
|
|
||||||
return getOrCreate(() -> provisioningSocket,
|
|
||||||
() -> provisioningSocket = new ProvisioningSocket(getServiceEnvironmentConfig().signalServiceConfiguration(),
|
|
||||||
userAgent));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Network getLibSignalNetwork() {
|
public Network getLibSignalNetwork() {
|
||||||
return getOrCreate(() -> libSignalNetwork, () -> {
|
return getOrCreate(() -> libSignalNetwork, () -> {
|
||||||
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
|
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
|
||||||
|
@ -169,8 +173,8 @@ public class SignalDependencies {
|
||||||
|
|
||||||
public SignalServiceAccountManager getAccountManager() {
|
public SignalServiceAccountManager getAccountManager() {
|
||||||
return getOrCreate(() -> accountManager,
|
return getOrCreate(() -> accountManager,
|
||||||
() -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(),
|
() -> accountManager = new SignalServiceAccountManager(getAccountApi(),
|
||||||
getProvisioningSocket(),
|
getPushServiceSocket(),
|
||||||
getGroupsV2Operations()));
|
getGroupsV2Operations()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +190,23 @@ public class SignalDependencies {
|
||||||
ServiceConfig.GROUP_MAX_SIZE);
|
ServiceConfig.GROUP_MAX_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AccountApi getAccountApi() {
|
||||||
|
return getOrCreate(() -> accountApi, () -> accountApi = new AccountApi(getAuthenticatedSignalWebSocket()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RateLimitChallengeApi getRateLimitChallengeApi() {
|
||||||
|
return getOrCreate(() -> rateLimitChallengeApi,
|
||||||
|
() -> rateLimitChallengeApi = new RateLimitChallengeApi(getAuthenticatedSignalWebSocket()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CdsApi getCdsApi() {
|
||||||
|
return getOrCreate(() -> cdsApi, () -> cdsApi = new CdsApi(getAuthenticatedSignalWebSocket()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UsernameApi getUsernameApi() {
|
||||||
|
return getOrCreate(() -> usernameApi, () -> usernameApi = new UsernameApi(getUnauthenticatedSignalWebSocket()));
|
||||||
|
}
|
||||||
|
|
||||||
public GroupsV2Api getGroupsV2Api() {
|
public GroupsV2Api getGroupsV2Api() {
|
||||||
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
|
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
|
||||||
}
|
}
|
||||||
|
@ -195,12 +216,14 @@ public class SignalDependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkDeviceApi getLinkDeviceApi() {
|
public LinkDeviceApi getLinkDeviceApi() {
|
||||||
return getOrCreate(() -> linkDeviceApi, () -> linkDeviceApi = new LinkDeviceApi(getPushServiceSocket()));
|
return getOrCreate(() -> linkDeviceApi,
|
||||||
|
() -> linkDeviceApi = new LinkDeviceApi(getAuthenticatedSignalWebSocket()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private StorageServiceApi getStorageServiceApi() {
|
private StorageServiceApi getStorageServiceApi() {
|
||||||
return getOrCreate(() -> storageServiceApi,
|
return getOrCreate(() -> storageServiceApi,
|
||||||
() -> storageServiceApi = new StorageServiceApi(getPushServiceSocket()));
|
() -> storageServiceApi = new StorageServiceApi(getAuthenticatedSignalWebSocket(),
|
||||||
|
getPushServiceSocket()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public StorageServiceRepository getStorageServiceRepository() {
|
public StorageServiceRepository getStorageServiceRepository() {
|
||||||
|
@ -223,33 +246,35 @@ public class SignalDependencies {
|
||||||
return clientZkOperations.getProfileOperations();
|
return clientZkOperations.getProfileOperations();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalWebSocket getSignalWebSocket() {
|
public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() {
|
||||||
return getOrCreate(() -> signalWebSocket, () -> {
|
return getOrCreate(() -> authenticatedSignalWebSocket, () -> {
|
||||||
final var timer = new UptimeSleepTimer();
|
final var timer = new UptimeSleepTimer();
|
||||||
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
|
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
|
||||||
final var webSocketFactory = new WebSocketFactory() {
|
|
||||||
@Override
|
|
||||||
public WebSocketConnection createWebSocket() {
|
|
||||||
return new OkHttpWebSocketConnection("normal",
|
|
||||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
|
||||||
Optional.of(credentialsProvider),
|
|
||||||
userAgent,
|
|
||||||
healthMonitor,
|
|
||||||
allowStories);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
|
||||||
public WebSocketConnection createUnidentifiedWebSocket() {
|
"normal",
|
||||||
return new OkHttpWebSocketConnection("unidentified",
|
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
Optional.of(credentialsProvider),
|
||||||
Optional.empty(),
|
userAgent,
|
||||||
userAgent,
|
healthMonitor,
|
||||||
healthMonitor,
|
allowStories));
|
||||||
allowStories);
|
healthMonitor.monitor(authenticatedSignalWebSocket);
|
||||||
}
|
});
|
||||||
};
|
}
|
||||||
signalWebSocket = new SignalWebSocket(webSocketFactory);
|
|
||||||
healthMonitor.monitor(signalWebSocket);
|
public SignalWebSocket.UnauthenticatedWebSocket getUnauthenticatedSignalWebSocket() {
|
||||||
|
return getOrCreate(() -> unauthenticatedSignalWebSocket, () -> {
|
||||||
|
final var timer = new UptimeSleepTimer();
|
||||||
|
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
|
||||||
|
|
||||||
|
unauthenticatedSignalWebSocket = new SignalWebSocket.UnauthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
|
||||||
|
"unidentified",
|
||||||
|
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||||
|
Optional.empty(),
|
||||||
|
userAgent,
|
||||||
|
healthMonitor,
|
||||||
|
allowStories));
|
||||||
|
healthMonitor.monitor(unauthenticatedSignalWebSocket);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +288,8 @@ public class SignalDependencies {
|
||||||
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
|
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
|
||||||
dataStore,
|
dataStore,
|
||||||
sessionLock,
|
sessionLock,
|
||||||
getSignalWebSocket(),
|
getAuthenticatedSignalWebSocket(),
|
||||||
|
getUnauthenticatedSignalWebSocket(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
executor,
|
executor,
|
||||||
ServiceConfig.MAX_ENVELOPE_SIZE));
|
ServiceConfig.MAX_ENVELOPE_SIZE));
|
||||||
|
@ -281,7 +307,8 @@ public class SignalDependencies {
|
||||||
return getOrCreate(() -> profileService,
|
return getOrCreate(() -> profileService,
|
||||||
() -> profileService = new ProfileService(getClientZkProfileOperations(),
|
() -> profileService = new ProfileService(getClientZkProfileOperations(),
|
||||||
getMessageReceiver(),
|
getMessageReceiver(),
|
||||||
getSignalWebSocket()));
|
getAuthenticatedSignalWebSocket(),
|
||||||
|
getUnauthenticatedSignalWebSocket()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
|
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
|
||||||
|
|
|
@ -2,195 +2,155 @@ package org.asamk.signal.manager.internal;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
|
||||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||||
import org.whispersystems.signalservice.api.util.SleepTimer;
|
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||||
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
|
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
|
||||||
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||||
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||||
|
import kotlin.Unit;
|
||||||
|
|
||||||
/**
|
|
||||||
* Monitors the health of the identified and unidentified WebSockets. If either one appears to be
|
|
||||||
* unhealthy, will trigger restarting both.
|
|
||||||
* <p>
|
|
||||||
* The monitor is also responsible for sending heartbeats/keep-alive messages to prevent
|
|
||||||
* timeouts.
|
|
||||||
*/
|
|
||||||
final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class);
|
private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the amount of time in between sent keep alives. Must be greater than [KEEP_ALIVE_TIMEOUT]
|
||||||
|
*/
|
||||||
private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(OkHttpWebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS);
|
private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(OkHttpWebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS);
|
||||||
private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3;
|
|
||||||
|
|
||||||
private SignalWebSocket signalWebSocket;
|
/**
|
||||||
|
* This is the amount of time we will wait for a response to the keep alive before we consider the websockets dead.
|
||||||
|
* It is required that this value be less than [KEEP_ALIVE_SEND_CADENCE]
|
||||||
|
*/
|
||||||
|
private static final long KEEP_ALIVE_TIMEOUT = TimeUnit.SECONDS.toMillis(20);
|
||||||
|
|
||||||
|
private final Executor executor = Executors.newSingleThreadExecutor();
|
||||||
private final SleepTimer sleepTimer;
|
private final SleepTimer sleepTimer;
|
||||||
|
private SignalWebSocket webSocket = null;
|
||||||
private volatile KeepAliveSender keepAliveSender;
|
private volatile KeepAliveSender keepAliveSender = null;
|
||||||
|
private boolean needsKeepAlive = false;
|
||||||
private final HealthState identified = new HealthState();
|
private long lastKeepAliveReceived = 0;
|
||||||
private final HealthState unidentified = new HealthState();
|
|
||||||
|
|
||||||
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
|
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
|
||||||
this.sleepTimer = sleepTimer;
|
this.sleepTimer = sleepTimer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void monitor(SignalWebSocket signalWebSocket) {
|
void monitor(SignalWebSocket webSocket) {
|
||||||
Preconditions.checkNotNull(signalWebSocket);
|
Preconditions.checkNotNull(webSocket);
|
||||||
Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once");
|
Preconditions.checkArgument(this.webSocket == null, "monitor can only be called once");
|
||||||
|
|
||||||
this.signalWebSocket = signalWebSocket;
|
executor.execute(() -> {
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
this.webSocket = webSocket;
|
||||||
signalWebSocket.getWebSocketState()
|
|
||||||
.subscribeOn(Schedulers.computation())
|
|
||||||
.observeOn(Schedulers.computation())
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.subscribe(s -> onStateChange(s, identified));
|
|
||||||
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
webSocket.getState()
|
||||||
signalWebSocket.getUnidentifiedWebSocketState()
|
.subscribeOn(Schedulers.computation())
|
||||||
.subscribeOn(Schedulers.computation())
|
.observeOn(Schedulers.computation())
|
||||||
.observeOn(Schedulers.computation())
|
.distinctUntilChanged()
|
||||||
.distinctUntilChanged()
|
.subscribe(this::onStateChanged);
|
||||||
.subscribe(s -> onStateChange(s, unidentified));
|
|
||||||
|
webSocket.setKeepAliveChangedListener(this::updateKeepAliveSenderStatus);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) {
|
private void onStateChanged(WebSocketConnectionState connectionState) {
|
||||||
switch (connectionState) {
|
executor.execute(() -> {
|
||||||
case CONNECTED -> logger.debug("WebSocket is now connected");
|
needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
|
||||||
case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed");
|
|
||||||
case FAILED -> logger.debug("WebSocket connection failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
|
updateKeepAliveSenderStatus();
|
||||||
|
});
|
||||||
if (keepAliveSender == null && isKeepAliveNecessary()) {
|
|
||||||
keepAliveSender = new KeepAliveSender();
|
|
||||||
keepAliveSender.start();
|
|
||||||
} else if (keepAliveSender != null && !isKeepAliveNecessary()) {
|
|
||||||
keepAliveSender.shutdown();
|
|
||||||
keepAliveSender = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
|
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
|
||||||
if (isIdentifiedWebSocket) {
|
final var keepAliveTime = System.currentTimeMillis();
|
||||||
identified.lastKeepAliveReceived = System.currentTimeMillis();
|
executor.execute(() -> lastKeepAliveReceived = keepAliveTime);
|
||||||
} else {
|
|
||||||
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
|
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
|
||||||
if (status == 409) {
|
}
|
||||||
HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified);
|
|
||||||
if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
|
private Unit updateKeepAliveSenderStatus() {
|
||||||
logger.warn("Received too many mismatch device errors, forcing new websockets.");
|
if (keepAliveSender == null && sendKeepAlives()) {
|
||||||
signalWebSocket.forceNewWebSockets();
|
keepAliveSender = new KeepAliveSender();
|
||||||
signalWebSocket.connect();
|
keepAliveSender.start();
|
||||||
}
|
} else if (keepAliveSender != null && !sendKeepAlives()) {
|
||||||
|
keepAliveSender.shutdown();
|
||||||
|
keepAliveSender = null;
|
||||||
}
|
}
|
||||||
|
return Unit.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isKeepAliveNecessary() {
|
private boolean sendKeepAlives() {
|
||||||
return identified.needsKeepAlive || unidentified.needsKeepAlive;
|
return needsKeepAlive && webSocket != null && webSocket.getShouldSendKeepAlives();
|
||||||
}
|
|
||||||
|
|
||||||
private static class HealthState {
|
|
||||||
|
|
||||||
private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1));
|
|
||||||
|
|
||||||
private volatile boolean needsKeepAlive;
|
|
||||||
private volatile long lastKeepAliveReceived;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If
|
* Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
|
||||||
* either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated.
|
* the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
|
||||||
*/
|
*/
|
||||||
private class KeepAliveSender extends Thread {
|
private final class KeepAliveSender extends Thread {
|
||||||
|
|
||||||
private volatile boolean shouldKeepRunning = true;
|
private volatile boolean shouldKeepRunning = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
identified.lastKeepAliveReceived = System.currentTimeMillis();
|
logger.debug("[KeepAliveSender({})] started", this.threadId());
|
||||||
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
|
lastKeepAliveReceived = System.currentTimeMillis();
|
||||||
|
|
||||||
while (shouldKeepRunning && isKeepAliveNecessary()) {
|
var keepAliveSendTime = System.currentTimeMillis();
|
||||||
|
while (shouldKeepRunning && sendKeepAlives()) {
|
||||||
try {
|
try {
|
||||||
sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE);
|
final var nextKeepAliveSendTime = keepAliveSendTime + KEEP_ALIVE_SEND_CADENCE;
|
||||||
|
sleepUntil(nextKeepAliveSendTime);
|
||||||
|
|
||||||
if (shouldKeepRunning && isKeepAliveNecessary()) {
|
if (shouldKeepRunning && sendKeepAlives()) {
|
||||||
long keepAliveRequiredSinceTime = System.currentTimeMillis()
|
keepAliveSendTime = System.currentTimeMillis();
|
||||||
- MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE;
|
webSocket.sendKeepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime
|
final var responseRequiredTime = keepAliveSendTime + KEEP_ALIVE_TIMEOUT;
|
||||||
|| unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) {
|
sleepUntil(responseRequiredTime);
|
||||||
logger.warn("Missed keep alives, identified last: "
|
|
||||||
+ identified.lastKeepAliveReceived
|
if (shouldKeepRunning && sendKeepAlives()) {
|
||||||
+ " unidentified last: "
|
if (lastKeepAliveReceived < keepAliveSendTime) {
|
||||||
+ unidentified.lastKeepAliveReceived
|
logger.debug("Missed keep alive, last: {} needed by: {}",
|
||||||
+ " needed by: "
|
lastKeepAliveReceived,
|
||||||
+ keepAliveRequiredSinceTime);
|
responseRequiredTime);
|
||||||
signalWebSocket.forceNewWebSockets();
|
webSocket.forceNewWebSocket();
|
||||||
signalWebSocket.connect();
|
|
||||||
} else {
|
|
||||||
signalWebSocket.sendKeepAlive();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.warn("Error occurred in KeepAliveSender, ignoring ...", e);
|
logger.warn("Keep alive sender failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("[KeepAliveSender({})] ended", threadId());
|
||||||
|
}
|
||||||
|
|
||||||
|
void sleepUntil(long timeMillis) {
|
||||||
|
while (System.currentTimeMillis() < timeMillis) {
|
||||||
|
final var waitTime = timeMillis - System.currentTimeMillis();
|
||||||
|
if (waitTime > 0) {
|
||||||
|
try {
|
||||||
|
sleepTimer.sleep(waitTime);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.warn("WebSocket health monitor interrupted", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
void shutdown() {
|
||||||
shouldKeepRunning = false;
|
shouldKeepRunning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class HttpErrorTracker {
|
|
||||||
|
|
||||||
private final long[] timestamps;
|
|
||||||
private final long errorTimeRange;
|
|
||||||
|
|
||||||
public HttpErrorTracker(int samples, long errorTimeRange) {
|
|
||||||
this.timestamps = new long[samples];
|
|
||||||
this.errorTimeRange = errorTimeRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean addSample(long now) {
|
|
||||||
long errorsMustBeAfter = now - errorTimeRange;
|
|
||||||
int count = 1;
|
|
||||||
int minIndex = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < timestamps.length; i++) {
|
|
||||||
if (timestamps[i] < errorsMustBeAfter) {
|
|
||||||
timestamps[i] = 0;
|
|
||||||
} else if (timestamps[i] != 0) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timestamps[i] < timestamps[minIndex]) {
|
|
||||||
minIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timestamps[minIndex] = now;
|
|
||||||
|
|
||||||
if (count >= timestamps.length) {
|
|
||||||
Arrays.fill(timestamps, 0);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.asamk.signal.manager.storage.accounts;
|
package org.asamk.signal.manager.storage.accounts;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.Pair;
|
import org.asamk.signal.manager.api.Pair;
|
||||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||||
|
@ -10,7 +11,6 @@ import org.asamk.signal.manager.util.IOUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -181,7 +181,7 @@ public class AccountsStore {
|
||||||
return Arrays.stream(files)
|
return Arrays.stream(files)
|
||||||
.filter(File::isFile)
|
.filter(File::isFile)
|
||||||
.map(File::getName)
|
.map(File::getName)
|
||||||
.filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
|
.filter(file -> PhoneNumberUtil.getInstance().isPossibleNumber(file, null))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||||
.hidden(remote.hidden)
|
.hidden(remote.hidden)
|
||||||
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
|
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
|
||||||
.nickname(remote.nickname)
|
.nickname(remote.nickname)
|
||||||
.note(remote.note);
|
.note(remote.note)
|
||||||
|
.avatarColor(remote.avatarColor);
|
||||||
final var merged = mergedBuilder.build();
|
final var merged = mergedBuilder.build();
|
||||||
|
|
||||||
final var matchesRemote = doProtosMatch(merged, remote);
|
final var matchesRemote = doProtosMatch(merged, remote);
|
||||||
|
|
|
@ -62,7 +62,8 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
||||||
.mutedUntilTimestamp(remote.mutedUntilTimestamp)
|
.mutedUntilTimestamp(remote.mutedUntilTimestamp)
|
||||||
.dontNotifyForMentionsIfMuted(remote.dontNotifyForMentionsIfMuted)
|
.dontNotifyForMentionsIfMuted(remote.dontNotifyForMentionsIfMuted)
|
||||||
.hideStory(remote.hideStory)
|
.hideStory(remote.hideStory)
|
||||||
.storySendMode(remote.storySendMode);
|
.storySendMode(remote.storySendMode)
|
||||||
|
.avatarColor(remote.avatarColor);
|
||||||
final var merged = mergedBuilder.build();
|
final var merged = mergedBuilder.build();
|
||||||
|
|
||||||
final var matchesRemote = doProtosMatch(merged, remote);
|
final var matchesRemote = doProtosMatch(merged, remote);
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.asamk.signal.manager.util;
|
||||||
|
|
||||||
|
import com.google.i18n.phonenumbers.NumberParseException;
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
|
||||||
|
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.InvalidNumberException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class PhoneNumberFormatter {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PhoneNumberFormatter.class);
|
||||||
|
|
||||||
|
private static String impreciseFormatNumber(String number, String localNumber) {
|
||||||
|
number = number.replaceAll("[^0-9+]", "");
|
||||||
|
|
||||||
|
if (number.charAt(0) == '+') return number;
|
||||||
|
|
||||||
|
if (localNumber.charAt(0) == '+') localNumber = localNumber.substring(1);
|
||||||
|
|
||||||
|
if (localNumber.length() == number.length() || number.length() > localNumber.length()) return "+" + number;
|
||||||
|
|
||||||
|
int difference = localNumber.length() - number.length();
|
||||||
|
|
||||||
|
return "+" + localNumber.substring(0, difference) + number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatNumber(String number, String localNumber) throws InvalidNumberException {
|
||||||
|
if (number == null) {
|
||||||
|
throw new InvalidNumberException("Null String passed as number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (number.contains("@")) {
|
||||||
|
throw new InvalidNumberException("Possible attempt to use email address.");
|
||||||
|
}
|
||||||
|
|
||||||
|
number = number.replaceAll("[^0-9+]", "");
|
||||||
|
|
||||||
|
if (number.isEmpty()) {
|
||||||
|
throw new InvalidNumberException("No valid characters found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
|
||||||
|
PhoneNumber localNumberObject = util.parse(localNumber, null);
|
||||||
|
|
||||||
|
String localCountryCode = util.getRegionCodeForNumber(localNumberObject);
|
||||||
|
logger.trace("Got local CC: {}", localCountryCode);
|
||||||
|
|
||||||
|
PhoneNumber numberObject = util.parse(number, localCountryCode);
|
||||||
|
return util.format(numberObject, PhoneNumberFormat.E164);
|
||||||
|
} catch (NumberParseException e) {
|
||||||
|
logger.debug("{}: {}", e.getClass().getSimpleName(), e.getMessage());
|
||||||
|
return impreciseFormatNumber(number, localNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -162,7 +162,11 @@ public class Utils {
|
||||||
throw new IOException(throwableOptional);
|
throw new IOException(throwableOptional);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response.successOrThrow();
|
try {
|
||||||
|
return response.successOrThrow();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ByteString firstNonEmpty(ByteString... strings) {
|
public static ByteString firstNonEmpty(ByteString... strings) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue