Update libsignal-service

This commit is contained in:
AsamK 2025-03-16 22:06:58 +01:00
parent 2b150112ff
commit f26a0d2891
18 changed files with 371 additions and 287 deletions

View file

@ -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"

View file

@ -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() {

View file

@ -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);
} }

View file

@ -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) {

View file

@ -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();

View file

@ -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,

View file

@ -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();

View file

@ -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();

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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) {

View file

@ -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;
}
}
} }

View file

@ -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());
} }

View file

@ -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);

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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) {