slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
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"
hikari = "com.zaxxer:HikariCP:6.2.1"
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.12.0"
package org.asamk.signal.manager;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+
import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.CaptchaRejectedException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.Closeable;
import java.io.File;
public interface Manager extends Closeable {
static boolean isValidNumber(final String e164Number, final String countryCode) {
- return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
+ return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
}
static boolean isSignalClientAvailable() {
public class InvalidNumberException extends Exception {
- InvalidNumberException(String message) {
+ public InvalidNumberException(String message) {
super(message);
}
package org.asamk.signal.manager.api;
+import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID;
sealed interface Single extends RecipientIdentifier {
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
- try {
- if (UuidUtil.isUuid(identifier)) {
- return new Uuid(UUID.fromString(identifier));
- }
+ if (UuidUtil.isUuid(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("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));
- }
+ 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);
+ 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) {
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
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.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
-import java.util.Optional;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
import okio.ByteString;
context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> {
final var registrationApi = dependencies.getRegistrationApi();
+ final var accountApi = dependencies.getAccountApi();
try {
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
} catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number
}
- return handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
+ return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
null,
newNumber,
registrationLock,
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());
if (hashIndex == -1) {
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
logger.debug("[reserveUsername] Successfully reserved username.");
final var username = candidates.get(hashIndex);
- final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
+ final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsername(username.getUsername());
account.setUsernameLink(linkComponents);
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
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 {
final var localUsername = account.getUsername();
if (localUsername == null) {
final var usernameLink = account.getUsernameLink();
if (usernameLink == null) {
- dependencies.getAccountManager()
- .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
+ handleResponseException(dependencies.getAccountApi()
+ .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
logger.debug("[reserveUsername] Successfully reserved existing username.");
- final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
+ final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully confirmed existing username.");
} else {
- final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink);
+ final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
}
private void tryToSetUsernameLink(Username username) {
for (var i = 1; i < 4; i++) {
try {
- final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
+ final var linkComponents = createUsernameLink(username);
account.setUsernameLink(linkComponents);
break;
} catch (IOException e) {
}
public void deleteUsername() throws IOException {
- dependencies.getAccountManager().deleteUsernameLink();
+ handleResponseException(dependencies.getAccountApi().deleteUsername());
account.setUsernameLink(null);
- dependencies.getAccountManager().deleteUsername();
account.setUsername(null);
logger.debug("[deleteUsername] Successfully deleted the username.");
}
}
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 removeLinkedDevices(int deviceId) throws IOException {
- dependencies.getAccountManager().removeDevice(deviceId);
- var devices = dependencies.getAccountManager().getDevices();
+ handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
+ var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1);
}
var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
- dependencies.getAccountManager().enableRegistrationLock(masterKey);
+ handleResponseException(dependencies.getAccountApi()
+ .enableRegistrationLock(masterKey.deriveRegistrationLock()));
}
public void setRegistrationPin(String pin) throws IOException {
var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
- dependencies.getAccountManager().enableRegistrationLock(masterKey);
+ handleResponseException(dependencies.getAccountApi()
+ .enableRegistrationLock(masterKey.deriveRegistrationLock()));
account.setRegistrationLockPin(pin);
updateAccountAttributes();
public void removeRegistrationPin() throws IOException {
// Remove KBS Pin
context.getPinHelper().removeRegistrationLockPin();
- dependencies.getAccountManager().disableRegistrationLock();
+ handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
account.setRegistrationLockPin(null);
}
// 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 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);
unregisteredListener.call();
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId;
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.WebSocketUnavailableException;
import java.util.Set;
import java.util.concurrent.TimeoutException;
-import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class ReceiveHelper {
// Use a Map here because java Set doesn't have a get method ...
Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
- final var signalWebSocket = dependencies.getSignalWebSocket();
- final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(),
- signalWebSocket.getWebSocketState())
+ final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket();
+ final var webSocketStateDisposable = signalWebSocket.getState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
}
private void receiveMessagesInternal(
- final SignalWebSocket signalWebSocket,
+ final SignalWebSocket.AuthenticatedWebSocket signalWebSocket,
Duration timeout,
boolean returnOnTimeout,
Integer maxMessages,
import org.signal.libsignal.usernames.Username;
import org.slf4j.Logger;
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.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
-import org.whispersystems.signalservice.api.services.CdsiV2Service;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
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 {
}
if (forceRefresh) {
try {
- final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername);
+ final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
} catch (IOException e) {
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
}
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
try {
- return dependencies.getAccountManager().getAciByUsername(finalUsername);
+ return handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
} catch (Exception e) {
return null;
}
try {
final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
final var components = usernameLinkUrl.getComponents();
- final var encryptedUsername = dependencies.getAccountManager()
- .getEncryptedUsernameFromLinkServerId(components.getServerId());
+ final var encryptedUsername = handleResponseException(dependencies.getUsernameApi()
+ .getEncryptedUsernameFromLinkServerId(components.getServerId()));
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
return Username.fromLink(link);
final CdsiV2Service.Response response;
try {
- response = dependencies.getAccountManager()
- .getRegisteredUsersWithCdsi(token.isEmpty() ? Set.of() : previousNumbers,
+ response = handleResponseException(dependencies.getCdsApi()
+ .getRegisteredUsers(token.isEmpty() ? Set.of() : previousNumbers,
newNumbers,
account.getRecipientStore().getServiceIdToProfileKeyMap(),
token,
null,
dependencies.getLibSignalNetwork(),
+ false,
newToken -> {
if (isPartialRefresh) {
account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
account.setCdsiToken(newToken);
account.setLastRecipientsRefresh(System.currentTimeMillis());
}
- });
+ }));
} catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) {
account.setCdsiToken(null);
account.getCdsiStore().clearAll();
import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.IncorrectPinException;
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.InvalidUsernameException;
import org.asamk.signal.manager.api.LastGroupAdminException;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.MimeUtils;
+import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.asamk.signal.manager.util.StickerUtils;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
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.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import okio.Utf8;
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;
public class ManagerImpl implements Manager {
) {
this.account = account;
- final var sessionLock = new SignalSessionLock() {
- private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
-
- @Override
- public Lock acquire() {
- LEGACY_LOCK.lock();
- return LEGACY_LOCK::unlock;
- }
- };
+ final var sessionLock = new ReentrantSignalSessionLock();
this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
userAgent,
account.getCredentialsProvider(),
String challenge,
String captcha
) throws IOException, CaptchaRejectedException {
- captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
+ captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", "");
try {
- dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
+ handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha));
} catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) {
throw new CaptchaRejectedException();
}
@Override
public List<Device> getLinkedDevices() throws IOException {
- var devices = dependencies.getAccountManager().getDevices();
+ var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
account.setMultiDevice(devices.size() > 1);
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
return devices.stream().map(d -> {
context.close();
executor.close();
- dependencies.getSignalWebSocket().disconnect();
+ dependencies.getAuthenticatedSignalWebSocket().disconnect();
+ dependencies.getUnauthenticatedSignalWebSocket().disconnect();
dependencies.getPushServiceSocket().close();
disposable.dispose();
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.SignalServiceAccountManager;
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.SignalServiceAddress;
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.internal.push.ProvisioningSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
private final Consumer<Manager> newManagerListener;
private final AccountsStore accountsStore;
- private final SignalServiceAccountManager accountManager;
+ private final ProvisioningApi provisioningApi;
private final IdentityKeyPair tempIdentityKey;
private final String password;
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
password = KeyUtils.createPassword();
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
- final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE);
final var credentialsProvider = new DynamicCredentialsProvider(null,
null,
null,
userAgent,
clientZkOperations.getProfileOperations(),
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
- accountManager = new SignalServiceAccountManager(pushServiceSocket,
- new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent),
- groupsV2Operations);
+ final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
+ userAgent);
+ this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
}
@Override
public URI getDeviceLinkUri() throws TimeoutException, IOException {
- var deviceUuid = accountManager.getNewDeviceUuid();
+ var deviceUuid = provisioningApi.getNewDeviceUuid();
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
}
@Override
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
- var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
+ var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber();
var aci = ret.getAci();
var pni = ret.getPni();
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
logger.debug("Finishing new device registration");
- var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
+ var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
account.getAccountAttributes(null),
aciPreKeys,
pniPreKeys);
--- /dev/null
+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;
+ }
+}
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils;
-import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
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.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
-import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException;
import java.util.function.Consumer;
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
+import static org.asamk.signal.manager.util.Utils.handleResponseException;
public class RegistrationManagerImpl implements RegistrationManager {
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
- final var response = Utils.handleResponseException(registrationApi.registerAccount(null,
+ final var response = handleResponseException(registrationApi.registerAccount(null,
recoveryPassword,
account.getAccountAttributes(null),
aciPreKeys,
private boolean attemptReactivateAccount() {
try {
- final var accountManager = createAuthenticatedSignalServiceAccountManager();
- accountManager.setAccountAttributes(account.getAccountAttributes(null));
+ final var dependencies = new SignalDependencies(serviceEnvironmentConfig,
+ userAgent,
+ account.getCredentialsProvider(),
+ account.getSignalServiceDataStore(),
+ null,
+ new ReentrantSignalSessionLock());
+ handleResponseException(dependencies.getAccountApi()
+ .setAccountAttributes(account.getAccountAttributes(null)));
account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) {
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(
final String sessionId,
final String verificationCode,
) throws IOException {
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
try {
- Utils.handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
+ handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
} catch (AlreadyVerifiedException e) {
// Already verified so can continue registering
}
- return Utils.handleResponseException(registrationApi.registerAccount(sessionId,
+ return handleResponseException(registrationApi.registerAccount(sessionId,
null,
account.getAccountAttributes(registrationLock),
aciPreKeys,
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
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.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
import org.whispersystems.signalservice.api.push.ServiceIdType;
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.services.ProfileService;
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
+import org.whispersystems.signalservice.api.username.UsernameApi;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
-import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
-import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
+import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
-import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.io.IOException;
import java.net.InetSocketAddress;
private boolean allowStories = true;
private SignalServiceAccountManager accountManager;
+ private AccountApi accountApi;
+ private RateLimitChallengeApi rateLimitChallengeApi;
+ private CdsApi cdsApi;
+ private UsernameApi usernameApi;
private GroupsV2Api groupsV2Api;
private RegistrationApi registrationApi;
private LinkDeviceApi linkDeviceApi;
private ClientZkOperations clientZkOperations;
private PushServiceSocket pushServiceSocket;
- private ProvisioningSocket provisioningSocket;
private Network libSignalNetwork;
- private SignalWebSocket signalWebSocket;
+ private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket;
+ private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket;
private SignalServiceMessageReceiver messageReceiver;
private SignalServiceMessageSender messageSender;
this.registrationApi = null;
this.secureValueRecovery = null;
}
- getSignalWebSocket().forceNewWebSockets();
+ if (this.authenticatedSignalWebSocket != null) {
+ this.authenticatedSignalWebSocket.forceNewWebSocket();
+ }
+ if (this.unauthenticatedSignalWebSocket != null) {
+ this.unauthenticatedSignalWebSocket.forceNewWebSocket();
+ }
}
/**
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
}
- public ProvisioningSocket getProvisioningSocket() {
- return getOrCreate(() -> provisioningSocket,
- () -> provisioningSocket = new ProvisioningSocket(getServiceEnvironmentConfig().signalServiceConfiguration(),
- userAgent));
- }
-
public Network getLibSignalNetwork() {
return getOrCreate(() -> libSignalNetwork, () -> {
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
public SignalServiceAccountManager getAccountManager() {
return getOrCreate(() -> accountManager,
- () -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(),
- getProvisioningSocket(),
+ () -> accountManager = new SignalServiceAccountManager(getAccountApi(),
+ getPushServiceSocket(),
getGroupsV2Operations()));
}
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() {
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
}
}
public LinkDeviceApi getLinkDeviceApi() {
- return getOrCreate(() -> linkDeviceApi, () -> linkDeviceApi = new LinkDeviceApi(getPushServiceSocket()));
+ return getOrCreate(() -> linkDeviceApi,
+ () -> linkDeviceApi = new LinkDeviceApi(getAuthenticatedSignalWebSocket()));
}
private StorageServiceApi getStorageServiceApi() {
return getOrCreate(() -> storageServiceApi,
- () -> storageServiceApi = new StorageServiceApi(getPushServiceSocket()));
+ () -> storageServiceApi = new StorageServiceApi(getAuthenticatedSignalWebSocket(),
+ getPushServiceSocket()));
}
public StorageServiceRepository getStorageServiceRepository() {
return clientZkOperations.getProfileOperations();
}
- public SignalWebSocket getSignalWebSocket() {
- return getOrCreate(() -> signalWebSocket, () -> {
+ public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() {
+ return getOrCreate(() -> authenticatedSignalWebSocket, () -> {
final var timer = new UptimeSleepTimer();
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
- public WebSocketConnection createUnidentifiedWebSocket() {
- return new OkHttpWebSocketConnection("unidentified",
- serviceEnvironmentConfig.signalServiceConfiguration(),
- Optional.empty(),
- userAgent,
- healthMonitor,
- allowStories);
- }
- };
- signalWebSocket = new SignalWebSocket(webSocketFactory);
- healthMonitor.monitor(signalWebSocket);
+ authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
+ "normal",
+ serviceEnvironmentConfig.signalServiceConfiguration(),
+ Optional.of(credentialsProvider),
+ userAgent,
+ healthMonitor,
+ allowStories));
+ healthMonitor.monitor(authenticatedSignalWebSocket);
+ });
+ }
+
+ 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);
});
}
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
dataStore,
sessionLock,
- getSignalWebSocket(),
+ getAuthenticatedSignalWebSocket(),
+ getUnauthenticatedSignalWebSocket(),
Optional.empty(),
executor,
ServiceConfig.MAX_ENVELOPE_SIZE));
return getOrCreate(() -> profileService,
() -> profileService = new ProfileService(getClientZkProfileOperations(),
getMessageReceiver(),
- getSignalWebSocket()));
+ getAuthenticatedSignalWebSocket(),
+ getUnauthenticatedSignalWebSocket()));
}
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.util.Preconditions;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
+import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
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 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 {
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 MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3;
-
- private SignalWebSocket signalWebSocket;
- private final SleepTimer sleepTimer;
- private volatile KeepAliveSender keepAliveSender;
+ /**
+ * 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 HealthState identified = new HealthState();
- private final HealthState unidentified = new HealthState();
+ private final Executor executor = Executors.newSingleThreadExecutor();
+ private final SleepTimer sleepTimer;
+ private SignalWebSocket webSocket = null;
+ private volatile KeepAliveSender keepAliveSender = null;
+ private boolean needsKeepAlive = false;
+ private long lastKeepAliveReceived = 0;
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
this.sleepTimer = sleepTimer;
}
- public void monitor(SignalWebSocket signalWebSocket) {
- Preconditions.checkNotNull(signalWebSocket);
- Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once");
-
- this.signalWebSocket = signalWebSocket;
-
- //noinspection ResultOfMethodCallIgnored
- signalWebSocket.getWebSocketState()
- .subscribeOn(Schedulers.computation())
- .observeOn(Schedulers.computation())
- .distinctUntilChanged()
- .subscribe(s -> onStateChange(s, identified));
-
- //noinspection ResultOfMethodCallIgnored
- signalWebSocket.getUnidentifiedWebSocketState()
- .subscribeOn(Schedulers.computation())
- .observeOn(Schedulers.computation())
- .distinctUntilChanged()
- .subscribe(s -> onStateChange(s, unidentified));
- }
+ void monitor(SignalWebSocket webSocket) {
+ Preconditions.checkNotNull(webSocket);
+ Preconditions.checkArgument(this.webSocket == null, "monitor can only be called once");
- private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) {
- switch (connectionState) {
- case CONNECTED -> logger.debug("WebSocket is now connected");
- case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed");
- case FAILED -> logger.debug("WebSocket connection failed");
- }
+ executor.execute(() -> {
- healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
+ this.webSocket = webSocket;
- if (keepAliveSender == null && isKeepAliveNecessary()) {
- keepAliveSender = new KeepAliveSender();
- keepAliveSender.start();
- } else if (keepAliveSender != null && !isKeepAliveNecessary()) {
- keepAliveSender.shutdown();
- keepAliveSender = null;
- }
+ webSocket.getState()
+ .subscribeOn(Schedulers.computation())
+ .observeOn(Schedulers.computation())
+ .distinctUntilChanged()
+ .subscribe(this::onStateChanged);
+
+ webSocket.setKeepAliveChangedListener(this::updateKeepAliveSenderStatus);
+ });
+ }
+
+ private void onStateChanged(WebSocketConnectionState connectionState) {
+ executor.execute(() -> {
+ needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
+
+ updateKeepAliveSenderStatus();
+ });
}
@Override
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
- if (isIdentifiedWebSocket) {
- identified.lastKeepAliveReceived = System.currentTimeMillis();
- } else {
- unidentified.lastKeepAliveReceived = System.currentTimeMillis();
- }
+ final var keepAliveTime = System.currentTimeMillis();
+ executor.execute(() -> lastKeepAliveReceived = keepAliveTime);
}
@Override
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
- if (status == 409) {
- HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified);
- if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
- logger.warn("Received too many mismatch device errors, forcing new websockets.");
- signalWebSocket.forceNewWebSockets();
- signalWebSocket.connect();
- }
- }
}
- private boolean isKeepAliveNecessary() {
- return identified.needsKeepAlive || unidentified.needsKeepAlive;
+ private Unit updateKeepAliveSenderStatus() {
+ if (keepAliveSender == null && sendKeepAlives()) {
+ keepAliveSender = new KeepAliveSender();
+ keepAliveSender.start();
+ } else if (keepAliveSender != null && !sendKeepAlives()) {
+ keepAliveSender.shutdown();
+ keepAliveSender = null;
+ }
+ return Unit.INSTANCE;
}
- private static class HealthState {
-
- private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1));
-
- private volatile boolean needsKeepAlive;
- private volatile long lastKeepAliveReceived;
+ private boolean sendKeepAlives() {
+ return needsKeepAlive && webSocket != null && webSocket.getShouldSendKeepAlives();
}
/**
- * Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If
- * either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated.
+ * Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
+ * 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;
+ @Override
public void run() {
- identified.lastKeepAliveReceived = System.currentTimeMillis();
- unidentified.lastKeepAliveReceived = System.currentTimeMillis();
+ logger.debug("[KeepAliveSender({})] started", this.threadId());
+ lastKeepAliveReceived = System.currentTimeMillis();
- while (shouldKeepRunning && isKeepAliveNecessary()) {
+ var keepAliveSendTime = System.currentTimeMillis();
+ while (shouldKeepRunning && sendKeepAlives()) {
try {
- sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE);
-
- if (shouldKeepRunning && isKeepAliveNecessary()) {
- long keepAliveRequiredSinceTime = System.currentTimeMillis()
- - MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE;
-
- if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime
- || unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) {
- logger.warn("Missed keep alives, identified last: "
- + identified.lastKeepAliveReceived
- + " unidentified last: "
- + unidentified.lastKeepAliveReceived
- + " needed by: "
- + keepAliveRequiredSinceTime);
- signalWebSocket.forceNewWebSockets();
- signalWebSocket.connect();
- } else {
- signalWebSocket.sendKeepAlive();
+ final var nextKeepAliveSendTime = keepAliveSendTime + KEEP_ALIVE_SEND_CADENCE;
+ sleepUntil(nextKeepAliveSendTime);
+
+ if (shouldKeepRunning && sendKeepAlives()) {
+ keepAliveSendTime = System.currentTimeMillis();
+ webSocket.sendKeepAlive();
+ }
+
+ final var responseRequiredTime = keepAliveSendTime + KEEP_ALIVE_TIMEOUT;
+ sleepUntil(responseRequiredTime);
+
+ if (shouldKeepRunning && sendKeepAlives()) {
+ if (lastKeepAliveReceived < keepAliveSendTime) {
+ logger.debug("Missed keep alive, last: {} needed by: {}",
+ lastKeepAliveReceived,
+ responseRequiredTime);
+ webSocket.forceNewWebSocket();
}
}
} catch (Throwable e) {
- logger.warn("Error occurred in KeepAliveSender, ignoring ...", e);
+ logger.warn("Keep alive sender failed", e);
}
}
+ logger.debug("[KeepAliveSender({})] ended", threadId());
}
- public void shutdown() {
- 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;
+ 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);
+ }
}
}
+ }
- timestamps[minIndex] = now;
-
- if (count >= timestamps.length) {
- Arrays.fill(timestamps, 0);
- return true;
- }
- return false;
+ void shutdown() {
+ shouldKeepRunning = false;
}
}
}
+
package org.asamk.signal.manager.storage.accounts;
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.ServiceEnvironment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
-import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
return Arrays.stream(files)
.filter(File::isFile)
.map(File::getName)
- .filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
+ .filter(file -> PhoneNumberUtil.getInstance().isPossibleNumber(file, null))
.collect(Collectors.toSet());
}
.hidden(remote.hidden)
.pniSignatureVerified(remote.pniSignatureVerified || local.pniSignatureVerified)
.nickname(remote.nickname)
- .note(remote.note);
+ .note(remote.note)
+ .avatarColor(remote.avatarColor);
final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote);
.mutedUntilTimestamp(remote.mutedUntilTimestamp)
.dontNotifyForMentionsIfMuted(remote.dontNotifyForMentionsIfMuted)
.hideStory(remote.hideStory)
- .storySendMode(remote.storySendMode);
+ .storySendMode(remote.storySendMode)
+ .avatarColor(remote.avatarColor);
final var merged = mergedBuilder.build();
final var matchesRemote = doProtosMatch(merged, remote);
--- /dev/null
+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);
+ }
+ }
+}
throw new IOException(throwableOptional);
}
}
- return response.successOrThrow();
+ try {
+ return response.successOrThrow();
+ } catch (Throwable e) {
+ throw new AssertionError(e);
+ }
}
public static ByteString firstNonEmpty(ByteString... strings) {