X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/1e87240db8a7d476d66e296701202413d94061c9..c7a7d00da56f8f90fe8d6a11042fa2cd7cb3b985:/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 7712b01b..1af99ff1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -17,16 +17,21 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.actions.HandleAction; +import org.asamk.signal.manager.api.Configuration; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Identity; +import org.asamk.signal.manager.api.InactiveGroupLinkException; +import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.api.SendMessageResult; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UpdateGroup; -import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; @@ -34,20 +39,7 @@ import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.helper.AttachmentHelper; -import org.asamk.signal.manager.helper.ContactHelper; -import org.asamk.signal.manager.helper.GroupHelper; -import org.asamk.signal.manager.helper.GroupV2Helper; -import org.asamk.signal.manager.helper.IdentityHelper; -import org.asamk.signal.manager.helper.IncomingMessageHandler; -import org.asamk.signal.manager.helper.PinHelper; -import org.asamk.signal.manager.helper.PreKeyHelper; -import org.asamk.signal.manager.helper.ProfileHelper; -import org.asamk.signal.manager.helper.SendHelper; -import org.asamk.signal.manager.helper.StorageHelper; -import org.asamk.signal.manager.helper.SyncHelper; -import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; -import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.helper.Context; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo; @@ -64,25 +56,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.messages.SendMessageResult; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; 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.websocket.WebSocketConnectionState; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; -import org.whispersystems.signalservice.internal.contacts.crypto.Quote; -import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; -import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; @@ -93,7 +79,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.security.SignatureException; +import java.time.Duration; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -107,6 +94,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.schedulers.Schedulers; import static org.asamk.signal.manager.config.ServiceConfig.capabilities; @@ -114,32 +105,23 @@ public class ManagerImpl implements Manager { private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); - private final ServiceEnvironmentConfig serviceEnvironmentConfig; private final SignalDependencies dependencies; private SignalAccount account; private final ExecutorService executor = Executors.newCachedThreadPool(); - private final ProfileHelper profileHelper; - private final PinHelper pinHelper; - private final StorageHelper storageHelper; - private final SendHelper sendHelper; - private final SyncHelper syncHelper; - private final AttachmentHelper attachmentHelper; - private final GroupHelper groupHelper; - private final ContactHelper contactHelper; - private final IncomingMessageHandler incomingMessageHandler; - private final PreKeyHelper preKeyHelper; - private final IdentityHelper identityHelper; - private final Context context; + private boolean hasCaughtUpWithOldMessages = false; private boolean ignoreAttachments = false; private Thread receiveThread; + private final Set weakHandlers = new HashSet<>(); private final Set messageHandlers = new HashSet<>(); + private final List closedListeners = new ArrayList<>(); private boolean isReceivingSynchronous; + private boolean needsToRetryFailedMessages = false; ManagerImpl( SignalAccount account, @@ -148,10 +130,9 @@ public class ManagerImpl implements Manager { String userAgent ) { this.account = account; - this.serviceEnvironmentConfig = serviceEnvironmentConfig; - final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(), - account.getUsername(), + final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(), + account.getAccount(), account.getPassword(), account.getDeviceId()); final var sessionLock = new SignalSessionLock() { @@ -173,80 +154,12 @@ public class ManagerImpl implements Manager { final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath()); final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath()); - this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); - this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); - final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account, - dependencies, - account::getProfileKey, - this::getRecipientProfile); - this.profileHelper = new ProfileHelper(account, - dependencies, - avatarStore, - unidentifiedAccessHelper::getAccessFor, - this::resolveSignalServiceAddress); - final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, - this::getRecipientProfile, - account::getSelfRecipientId, - dependencies.getGroupsV2Operations(), - dependencies.getGroupsV2Api(), - this::resolveSignalServiceAddress); - this.sendHelper = new SendHelper(account, - dependencies, - unidentifiedAccessHelper, - this::resolveSignalServiceAddress, - account.getRecipientStore(), - this::handleIdentityFailure, - this::getGroupInfo, - this::refreshRegisteredUser); - this.groupHelper = new GroupHelper(account, - dependencies, - attachmentHelper, - sendHelper, - groupV2Helper, - avatarStore, - this::resolveSignalServiceAddress, - account.getRecipientStore()); - this.storageHelper = new StorageHelper(account, dependencies, groupHelper, profileHelper); - this.contactHelper = new ContactHelper(account); - this.syncHelper = new SyncHelper(account, - attachmentHelper, - sendHelper, - groupHelper, - avatarStore, - this::resolveSignalServiceAddress); - preKeyHelper = new PreKeyHelper(account, dependencies); - - this.context = new Context(account, - dependencies, - stickerPackStore, - sendHelper, - groupHelper, - syncHelper, - profileHelper, - storageHelper, - preKeyHelper); - var jobExecutor = new JobExecutor(context); - - this.incomingMessageHandler = new IncomingMessageHandler(account, - dependencies, - account.getRecipientStore(), - this::resolveSignalServiceAddress, - groupHelper, - contactHelper, - attachmentHelper, - syncHelper, - this::getRecipientProfile, - jobExecutor); - this.identityHelper = new IdentityHelper(account, - dependencies, - this::resolveSignalServiceAddress, - syncHelper, - profileHelper); + this.context = new Context(account, dependencies, avatarStore, attachmentStore, stickerPackStore); } @Override public String getSelfNumber() { - return account.getUsername(); + return account.getAccount(); } @Override @@ -262,11 +175,16 @@ public class ManagerImpl implements Manager { days); } } - preKeyHelper.refreshPreKeysIfNecessary(); - if (account.getUuid() == null) { - account.setUuid(dependencies.getAccountManager().getOwnUuid()); + try { + context.getPreKeyHelper().refreshPreKeysIfNecessary(); + if (account.getAci() == null) { + account.setAci(ACI.parseOrNull(dependencies.getAccountManager().getWhoAmI().getAci())); + } + updateAccountAttributes(null); + } catch (AuthorizationFailedException e) { + account.setRegistered(false); + throw e; } - updateAccountAttributes(null); } /** @@ -274,28 +192,33 @@ public class ManagerImpl implements Manager { * * @param numbers The set of phone number in question * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. - * @throws IOException if its unable to get the contacts to check if they're registered + * @throws IOException if it's unable to get the contacts to check if they're registered */ @Override public Map> areUsersRegistered(Set numbers) throws IOException { - Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { + final var canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { try { - return PhoneNumberFormatter.formatNumber(n, account.getUsername()); + final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getAccount()); + if (!canonicalizedNumber.equals(n)) { + logger.debug("Normalized number {} to {}.", n, canonicalizedNumber); + } + return canonicalizedNumber; } catch (InvalidNumberException e) { return ""; } })); // Note "registeredUsers" has no optionals. It only gives us info on users who are registered - var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values() + final var canonicalizedNumbersSet = canonicalizedNumbers.values() .stream() .filter(s -> !s.isEmpty()) - .collect(Collectors.toSet())); + .collect(Collectors.toSet()); + final var registeredUsers = context.getRecipientHelper().getRegisteredUsers(canonicalizedNumbersSet); return numbers.stream().collect(Collectors.toMap(n -> n, n -> { final var number = canonicalizedNumbers.get(n); - final var uuid = registeredUsers.get(number); - return new Pair<>(number.isEmpty() ? null : number, uuid); + final var aci = registeredUsers.get(number); + return new Pair<>(number.isEmpty() ? null : number, aci == null ? null : aci.uuid()); })); } @@ -322,31 +245,37 @@ public class ManagerImpl implements Manager { account.isDiscoverableByPhoneNumber()); } + @Override + public Configuration getConfiguration() { + final var configurationStore = account.getConfigurationStore(); + return new Configuration(java.util.Optional.ofNullable(configurationStore.getReadReceipts()), + java.util.Optional.ofNullable(configurationStore.getUnidentifiedDeliveryIndicators()), + java.util.Optional.ofNullable(configurationStore.getTypingIndicators()), + java.util.Optional.ofNullable(configurationStore.getLinkPreviews())); + } + @Override public void updateConfiguration( - final Boolean readReceipts, - final Boolean unidentifiedDeliveryIndicators, - final Boolean typingIndicators, - final Boolean linkPreviews + Configuration configuration ) throws IOException, NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } final var configurationStore = account.getConfigurationStore(); - if (readReceipts != null) { - configurationStore.setReadReceipts(readReceipts); + if (configuration.readReceipts().isPresent()) { + configurationStore.setReadReceipts(configuration.readReceipts().get()); } - if (unidentifiedDeliveryIndicators != null) { - configurationStore.setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators); + if (configuration.unidentifiedDeliveryIndicators().isPresent()) { + configurationStore.setUnidentifiedDeliveryIndicators(configuration.unidentifiedDeliveryIndicators().get()); } - if (typingIndicators != null) { - configurationStore.setTypingIndicators(typingIndicators); + if (configuration.typingIndicators().isPresent()) { + configurationStore.setTypingIndicators(configuration.typingIndicators().get()); } - if (linkPreviews != null) { - configurationStore.setLinkPreviews(linkPreviews); + if (configuration.linkPreviews().isPresent()) { + configurationStore.setLinkPreviews(configuration.linkPreviews().get()); } - syncHelper.sendConfigurationMessage(); + context.getSyncHelper().sendConfigurationMessage(); } /** @@ -360,12 +289,13 @@ public class ManagerImpl implements Manager { public void setProfile( String givenName, final String familyName, String about, String aboutEmoji, java.util.Optional avatar ) throws IOException { - profileHelper.setProfile(givenName, - familyName, - about, - aboutEmoji, - avatar == null ? null : Optional.fromNullable(avatar.orElse(null))); - syncHelper.sendSyncFetchProfileMessage(); + context.getProfileHelper() + .setProfile(givenName, + familyName, + about, + aboutEmoji, + avatar == null ? null : Optional.fromNullable(avatar.orElse(null))); + context.getSyncHelper().sendSyncFetchProfileMessage(); } @Override @@ -376,13 +306,14 @@ public class ManagerImpl implements Manager { dependencies.getAccountManager().setGcmId(Optional.absent()); account.setRegistered(false); + close(); } @Override public void deleteAccount() throws IOException { try { - pinHelper.removeRegistrationLockPin(); - } catch (UnauthenticatedResponseException e) { + context.getPinHelper().removeRegistrationLockPin(); + } catch (IOException e) { logger.warn("Failed to remove registration lock pin"); } account.setRegistrationLockPin(null, null); @@ -390,10 +321,13 @@ public class ManagerImpl implements Manager { dependencies.getAccountManager().deleteAccount(); account.setRegistered(false); + close(); } @Override public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { + captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); + dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); } @@ -416,7 +350,7 @@ public class ManagerImpl implements Manager { d.getCreated(), d.getLastSeen(), d.getId() == account.getDeviceId()); - }).collect(Collectors.toList()); + }).toList(); } @Override @@ -427,27 +361,33 @@ public class ManagerImpl implements Manager { } @Override - public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { + public void addDeviceLink(URI linkUri) throws IOException, InvalidDeviceLinkException { var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); addDevice(info.deviceIdentifier(), info.deviceKey()); } - private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + private void addDevice( + String deviceIdentifier, ECPublicKey deviceKey + ) throws IOException, InvalidDeviceLinkException { var identityKeyPair = account.getIdentityKeyPair(); var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); - dependencies.getAccountManager() - .addDevice(deviceIdentifier, - deviceKey, - identityKeyPair, - Optional.of(account.getProfileKey().serialize()), - verificationCode); + try { + dependencies.getAccountManager() + .addDevice(deviceIdentifier, + deviceKey, + identityKeyPair, + Optional.of(account.getProfileKey().serialize()), + verificationCode); + } catch (InvalidKeyException e) { + throw new InvalidDeviceLinkException("Invalid device link", e); + } account.setMultiDevice(true); } @Override - public void setRegistrationLockPin(java.util.Optional pin) throws IOException, UnauthenticatedResponseException { + public void setRegistrationLockPin(java.util.Optional pin) throws IOException { if (!account.isMasterDevice()) { throw new RuntimeException("Only master device can set a PIN"); } @@ -456,33 +396,29 @@ public class ManagerImpl implements Manager { ? account.getPinMasterKey() : KeyUtils.createMasterKey(); - pinHelper.setRegistrationLockPin(pin.get(), masterKey); + context.getPinHelper().setRegistrationLockPin(pin.get(), masterKey); account.setRegistrationLockPin(pin.get(), masterKey); } else { // Remove KBS Pin - pinHelper.removeRegistrationLockPin(); + context.getPinHelper().removeRegistrationLockPin(); account.setRegistrationLockPin(null, null); } } void refreshPreKeys() throws IOException { - preKeyHelper.refreshPreKeys(); + context.getPreKeyHelper().refreshPreKeys(); } @Override - public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException { - return profileHelper.getRecipientProfile(resolveRecipient(recipient)); - } - - private Profile getRecipientProfile(RecipientId recipientId) { - return profileHelper.getRecipientProfile(recipientId); + public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException { + return context.getProfileHelper().getRecipientProfile(context.getRecipientHelper().resolveRecipient(recipient)); } @Override public List getGroups() { - return account.getGroupStore().getGroups().stream().map(this::toGroup).collect(Collectors.toList()); + return account.getGroupStore().getGroups().stream().map(this::toGroup).toList(); } private Group toGroup(final GroupInfo groupInfo) { @@ -522,48 +458,60 @@ public class ManagerImpl implements Manager { @Override public SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins - ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - final var newAdmins = resolveRecipients(groupAdmins); - return groupHelper.quitGroup(groupId, newAdmins); + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException { + final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins); + return context.getGroupHelper().quitGroup(groupId, newAdmins); } @Override public void deleteGroup(GroupId groupId) throws IOException { - groupHelper.deleteGroup(groupId); + context.getGroupHelper().deleteGroup(groupId); } @Override public Pair createGroup( String name, Set members, File avatarFile - ) throws IOException, AttachmentInvalidException { - return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); + ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException { + return context.getGroupHelper() + .createGroup(name, + members == null ? null : context.getRecipientHelper().resolveRecipients(members), + avatarFile); } @Override public SendGroupMessageResults updateGroup( final GroupId groupId, final UpdateGroup updateGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { - return groupHelper.updateGroup(groupId, - updateGroup.getName(), - updateGroup.getDescription(), - updateGroup.getMembers() == null ? null : resolveRecipients(updateGroup.getMembers()), - updateGroup.getRemoveMembers() == null ? null : resolveRecipients(updateGroup.getRemoveMembers()), - updateGroup.getAdmins() == null ? null : resolveRecipients(updateGroup.getAdmins()), - updateGroup.getRemoveAdmins() == null ? null : resolveRecipients(updateGroup.getRemoveAdmins()), - updateGroup.isResetGroupLink(), - updateGroup.getGroupLinkState(), - updateGroup.getAddMemberPermission(), - updateGroup.getEditDetailsPermission(), - updateGroup.getAvatarFile(), - updateGroup.getExpirationTimer(), - updateGroup.getIsAnnouncementGroup()); + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException { + return context.getGroupHelper() + .updateGroup(groupId, + updateGroup.getName(), + updateGroup.getDescription(), + updateGroup.getMembers() == null + ? null + : context.getRecipientHelper().resolveRecipients(updateGroup.getMembers()), + updateGroup.getRemoveMembers() == null + ? null + : context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveMembers()), + updateGroup.getAdmins() == null + ? null + : context.getRecipientHelper().resolveRecipients(updateGroup.getAdmins()), + updateGroup.getRemoveAdmins() == null + ? null + : context.getRecipientHelper().resolveRecipients(updateGroup.getRemoveAdmins()), + updateGroup.isResetGroupLink(), + updateGroup.getGroupLinkState(), + updateGroup.getAddMemberPermission(), + updateGroup.getEditDetailsPermission(), + updateGroup.getAvatarFile(), + updateGroup.getExpirationTimer(), + updateGroup.getIsAnnouncementGroup()); } @Override public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl - ) throws IOException, GroupLinkNotActiveException { - return groupHelper.joinGroup(inviteLinkUrl); + ) throws IOException, InactiveGroupLinkException { + return context.getGroupHelper().joinGroup(inviteLinkUrl); } private SendMessageResults sendMessage( @@ -574,70 +522,124 @@ public class ManagerImpl implements Manager { messageBuilder.withTimestamp(timestamp); for (final var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single single) { - final var recipientId = resolveRecipient(single); - final var result = sendHelper.sendMessage(messageBuilder, recipientId); - results.put(recipient, List.of(result)); + try { + final var recipientId = context.getRecipientHelper().resolveRecipient(single); + final var result = context.getSendHelper().sendMessage(messageBuilder, recipientId); + results.put(recipient, + List.of(SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress))); + } catch (UnregisteredRecipientException e) { + results.put(recipient, + List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress()))); + } } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { - final var result = sendHelper.sendSelfMessage(messageBuilder); - results.put(recipient, List.of(result)); + final var result = context.getSendHelper().sendSelfMessage(messageBuilder); + results.put(recipient, + List.of(SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress))); } else if (recipient instanceof RecipientIdentifier.Group group) { - final var result = sendHelper.sendAsGroupMessage(messageBuilder, group.groupId); - results.put(recipient, result); + final var result = context.getSendHelper().sendAsGroupMessage(messageBuilder, group.groupId()); + results.put(recipient, + result.stream() + .map(sendMessageResult -> SendMessageResult.from(sendMessageResult, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress)) + .toList()); } } return new SendMessageResults(timestamp, results); } - private void sendTypingMessage( + private SendMessageResults sendTypingMessage( SignalServiceTypingMessage.Action action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + var results = new HashMap>(); final var timestamp = System.currentTimeMillis(); for (var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single) { + if (recipient instanceof RecipientIdentifier.Single single) { final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - sendHelper.sendTypingMessage(message, recipientId); + try { + final var recipientId = context.getRecipientHelper().resolveRecipient(single); + final var result = context.getSendHelper().sendTypingMessage(message, recipientId); + results.put(recipient, + List.of(SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress))); + } catch (UnregisteredRecipientException e) { + results.put(recipient, + List.of(SendMessageResult.unregisteredFailure(single.toPartialRecipientAddress()))); + } } else if (recipient instanceof RecipientIdentifier.Group) { - final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var groupId = ((RecipientIdentifier.Group) recipient).groupId(); final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); - sendHelper.sendGroupTypingMessage(message, groupId); + final var result = context.getSendHelper().sendGroupTypingMessage(message, groupId); + results.put(recipient, + result.stream() + .map(r -> SendMessageResult.from(r, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress)) + .toList()); } } + return new SendMessageResults(timestamp, results); } @Override - public void sendTypingMessage( + public SendMessageResults sendTypingMessage( TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - sendTypingMessage(action.toSignalService(), recipients); + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return sendTypingMessage(action.toSignalService(), recipients); } @Override - public void sendReadReceipt( + public SendMessageResults sendReadReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException, UntrustedIdentityException { + ) throws IOException { + final var timestamp = System.currentTimeMillis(); var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, - System.currentTimeMillis()); + timestamp); - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + return sendReceiptMessage(sender, timestamp, receiptMessage); } @Override - public void sendViewedReceipt( + public SendMessageResults sendViewedReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException, UntrustedIdentityException { + ) throws IOException { + final var timestamp = System.currentTimeMillis(); var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, messageIds, - System.currentTimeMillis()); + timestamp); + + return sendReceiptMessage(sender, timestamp, receiptMessage); + } - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + private SendMessageResults sendReceiptMessage( + final RecipientIdentifier.Single sender, + final long timestamp, + final SignalServiceReceiptMessage receiptMessage + ) throws IOException { + try { + final var result = context.getSendHelper() + .sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender)); + return new SendMessageResults(timestamp, + Map.of(sender, + List.of(SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress)))); + } catch (UnregisteredRecipientException e) { + return new SendMessageResults(timestamp, + Map.of(sender, List.of(SendMessageResult.unregisteredFailure(sender.toPartialRecipientAddress())))); + } } @Override public SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); @@ -645,12 +647,35 @@ public class ManagerImpl implements Manager { private void applyMessage( final SignalServiceDataMessage.Builder messageBuilder, final Message message - ) throws AttachmentInvalidException, IOException { + ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException { messageBuilder.withBody(message.messageText()); final var attachments = message.attachments(); if (attachments != null) { - messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); + messageBuilder.withAttachments(context.getAttachmentHelper().uploadAttachments(attachments)); + } + if (message.mentions().size() > 0) { + messageBuilder.withMentions(resolveMentions(message.mentions())); + } + if (message.quote().isPresent()) { + final var quote = message.quote().get(); + messageBuilder.withQuote(new SignalServiceDataMessage.Quote(quote.timestamp(), + context.getRecipientHelper() + .resolveSignalServiceAddress(context.getRecipientHelper().resolveRecipient(quote.author())), + quote.message(), + List.of(), + resolveMentions(quote.mentions()))); + } + } + + private ArrayList resolveMentions(final List mentionList) throws IOException, UnregisteredRecipientException { + final var mentions = new ArrayList(); + for (final var m : mentionList) { + final var recipientId = context.getRecipientHelper().resolveRecipient(m.recipient()); + mentions.add(new SignalServiceDataMessage.Mention(context.getRecipientHelper() + .resolveSignalServiceAddress(recipientId) + .getAci(), m.start(), m.length())); } + return mentions; } @Override @@ -669,11 +694,11 @@ public class ManagerImpl implements Manager { RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - var targetAuthorRecipientId = resolveRecipient(targetAuthor); + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException { + var targetAuthorRecipientId = context.getRecipientHelper().resolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, - resolveSignalServiceAddress(targetAuthorRecipientId), + context.getRecipientHelper().resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); return sendMessage(messageBuilder, recipients); @@ -690,32 +715,48 @@ public class ManagerImpl implements Manager { throw new AssertionError(e); } finally { for (var recipient : recipients) { - final var recipientId = resolveRecipient(recipient); + final RecipientId recipientId; + try { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (UnregisteredRecipientException e) { + continue; + } account.getSessionStore().deleteAllSessions(recipientId); } } } + @Override + public void deleteRecipient(final RecipientIdentifier.Single recipient) { + account.removeRecipient(account.getRecipientStore().resolveRecipient(recipient.toPartialRecipientAddress())); + } + + @Override + public void deleteContact(final RecipientIdentifier.Single recipient) { + account.getContactStore() + .deleteContact(account.getRecipientStore().resolveRecipient(recipient.toPartialRecipientAddress())); + } + @Override public void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException, UnregisteredUserException { + ) throws NotMasterDeviceException, IOException, UnregisteredRecipientException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - contactHelper.setContactName(resolveRecipient(recipient), name); + context.getContactHelper().setContactName(context.getRecipientHelper().resolveRecipient(recipient), name); } @Override public void setContactBlocked( RecipientIdentifier.Single recipient, boolean blocked - ) throws NotMasterDeviceException, IOException { + ) throws NotMasterDeviceException, IOException, UnregisteredRecipientException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); - // TODO cycle our profile key - syncHelper.sendBlockedList(); + context.getContactHelper().setContactBlocked(context.getRecipientHelper().resolveRecipient(recipient), blocked); + // TODO cycle our profile key, if we're not together in a group with recipient + context.getSyncHelper().sendBlockedList(); } @Override @@ -725,9 +766,9 @@ public class ManagerImpl implements Manager { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - groupHelper.setGroupBlocked(groupId, blocked); + context.getGroupHelper().setGroupBlocked(groupId, blocked); // TODO cycle our profile key - syncHelper.sendBlockedList(); + context.getSyncHelper().sendBlockedList(); } /** @@ -736,9 +777,9 @@ public class ManagerImpl implements Manager { @Override public void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer - ) throws IOException { - var recipientId = resolveRecipient(recipient); - contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); + ) throws IOException, UnregisteredRecipientException { + var recipientId = context.getRecipientHelper().resolveRecipient(recipient); + context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer); final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { sendMessage(messageBuilder, Set.of(recipient)); @@ -781,55 +822,14 @@ public class ManagerImpl implements Manager { @Override public void requestAllSyncData() throws IOException { - syncHelper.requestAllSyncData(); + context.getSyncHelper().requestAllSyncData(); retrieveRemoteStorage(); } void retrieveRemoteStorage() throws IOException { if (account.getStorageKey() != null) { - storageHelper.readDataFromStorage(); - } - } - - private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { - final var address = resolveSignalServiceAddress(recipientId); - if (!address.getNumber().isPresent()) { - return recipientId; - } - final var number = address.getNumber().get(); - final var uuid = getRegisteredUser(number); - return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); - } - - private UUID getRegisteredUser(final String number) throws IOException { - final Map uuidMap; - try { - uuidMap = getRegisteredUsers(Set.of(number)); - } catch (NumberFormatException e) { - throw new UnregisteredUserException(number, e); - } - final var uuid = uuidMap.get(number); - if (uuid == null) { - throw new UnregisteredUserException(number, null); + context.getStorageHelper().readDataFromStorage(); } - return uuid; - } - - private Map getRegisteredUsers(final Set numbers) throws IOException { - final Map registeredUsers; - try { - registeredUsers = dependencies.getAccountManager() - .getRegisteredUsers(ServiceConfig.getIasKeyStore(), - numbers, - serviceEnvironmentConfig.getCdsMrenclave()); - } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { - throw new IOException(e); - } - - // Store numbers as recipients so we have the number/uuid association - registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - - return registeredUsers; } private void retryFailedReceivedMessages(ReceiveMessageHandler handler) { @@ -852,7 +852,8 @@ public class ManagerImpl implements Manager { return null; } - final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); + final var result = context.getIncomingMessageHandler() + .handleRetryEnvelope(envelope, ignoreAttachments, handler); final var actions = result.first(); final var exception = result.second(); @@ -880,14 +881,17 @@ public class ManagerImpl implements Manager { } @Override - public void addReceiveHandler(final ReceiveMessageHandler handler) { + public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) { if (isReceivingSynchronous) { throw new IllegalStateException("Already receiving message synchronously."); } synchronized (messageHandlers) { - messageHandlers.add(handler); - - startReceiveThreadIfRequired(); + if (isWeakListener) { + weakHandlers.add(handler); + } else { + messageHandlers.add(handler); + startReceiveThreadIfRequired(); + } } } @@ -896,17 +900,18 @@ public class ManagerImpl implements Manager { return; } receiveThread = new Thread(() -> { + logger.debug("Starting receiving messages"); while (!Thread.interrupted()) { try { - receiveMessagesInternal(1L, TimeUnit.HOURS, false, (envelope, decryptedContent, e) -> { + receiveMessagesInternal(Duration.ofMinutes(1), false, (envelope, e) -> { synchronized (messageHandlers) { - for (ReceiveMessageHandler h : messageHandlers) { + Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> { try { - h.handleMessage(envelope, decryptedContent, e); + h.handleMessage(envelope, e); } catch (Exception ex) { logger.warn("Message handler failed, ignoring", ex); } - } + }); } }); break; @@ -914,12 +919,14 @@ public class ManagerImpl implements Manager { logger.warn("Receiving messages failed, retrying", e); } } + logger.debug("Finished receiving messages"); hasCaughtUpWithOldMessages = false; synchronized (messageHandlers) { receiveThread = null; // Check if in the meantime another handler has been registered if (!messageHandlers.isEmpty()) { + logger.debug("Another handler has been registered, starting receive thread again"); startReceiveThreadIfRequired(); } } @@ -932,12 +939,13 @@ public class ManagerImpl implements Manager { public void removeReceiveHandler(final ReceiveMessageHandler handler) { final Thread thread; synchronized (messageHandlers) { - thread = receiveThread; - receiveThread = null; + weakHandlers.remove(handler); messageHandlers.remove(handler); - if (!messageHandlers.isEmpty() || isReceivingSynchronous) { + if (!messageHandlers.isEmpty() || receiveThread == null || isReceivingSynchronous) { return; } + thread = receiveThread; + receiveThread = null; } stopReceiveThread(thread); @@ -962,17 +970,17 @@ public class ManagerImpl implements Manager { } @Override - public void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException { - receiveMessages(timeout, unit, true, handler); + public void receiveMessages(Duration timeout, ReceiveMessageHandler handler) throws IOException { + receiveMessages(timeout, true, handler); } @Override public void receiveMessages(ReceiveMessageHandler handler) throws IOException { - receiveMessages(1L, TimeUnit.HOURS, false, handler); + receiveMessages(Duration.ofMinutes(1), false, handler); } private void receiveMessages( - long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler + Duration timeout, boolean returnOnTimeout, ReceiveMessageHandler handler ) throws IOException { if (isReceiving()) { throw new IllegalStateException("Already receiving message."); @@ -980,7 +988,7 @@ public class ManagerImpl implements Manager { isReceivingSynchronous = true; receiveThread = Thread.currentThread(); try { - receiveMessagesInternal(timeout, unit, returnOnTimeout, handler); + receiveMessagesInternal(timeout, returnOnTimeout, handler); } finally { receiveThread = null; hasCaughtUpWithOldMessages = false; @@ -989,13 +997,20 @@ public class ManagerImpl implements Manager { } private void receiveMessagesInternal( - long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler + Duration timeout, boolean returnOnTimeout, ReceiveMessageHandler handler ) throws IOException { - retryFailedReceivedMessages(handler); + needsToRetryFailedMessages = true; - Set queuedActions = new HashSet<>(); + // Use a Map here because java Set doesn't have a get method ... + Map queuedActions = new HashMap<>(); final var signalWebSocket = dependencies.getSignalWebSocket(); + final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(), + signalWebSocket.getWebSocketState()) + .subscribeOn(Schedulers.computation()) + .observeOn(Schedulers.computation()) + .distinctUntilChanged() + .subscribe(this::onWebSocketStateChange); signalWebSocket.connect(); hasCaughtUpWithOldMessages = false; @@ -1003,15 +1018,21 @@ public class ManagerImpl implements Manager { final var MAX_BACKOFF_COUNTER = 9; while (!Thread.interrupted()) { + if (needsToRetryFailedMessages) { + retryFailedReceivedMessages(handler); + needsToRetryFailedMessages = false; + } SignalServiceEnvelope envelope; final CachedMessage[] cachedMessage = {null}; - account.setLastReceiveTimestamp(System.currentTimeMillis()); + final var nowMillis = System.currentTimeMillis(); + if (nowMillis - account.getLastReceiveTimestamp() > 60000) { + account.setLastReceiveTimestamp(nowMillis); + } logger.debug("Checking for new message from server"); try { - var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { - final var recipientId = envelope1.hasSourceUuid() - ? resolveRecipient(envelope1.getSourceAddress()) - : null; + var result = signalWebSocket.readOrEmpty(timeout.toMillis(), envelope1 -> { + final var recipientId = envelope1.hasSourceUuid() ? account.getRecipientStore() + .resolveRecipient(envelope1.getSourceAddress()) : null; // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); }); @@ -1022,7 +1043,7 @@ public class ManagerImpl implements Manager { logger.debug("New message received from server"); } else { logger.debug("Received indicator that server queue is empty"); - handleQueuedActions(queuedActions); + handleQueuedActions(queuedActions.keySet()); queuedActions.clear(); hasCaughtUpWithOldMessages = true; @@ -1062,19 +1083,26 @@ public class ManagerImpl implements Manager { continue; } - final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); - queuedActions.addAll(result.first()); + final var result = context.getIncomingMessageHandler().handleEnvelope(envelope, ignoreAttachments, handler); + for (final var h : result.first()) { + final var existingAction = queuedActions.get(h); + if (existingAction == null) { + queuedActions.put(h, h); + } else { + existingAction.mergeOther(h); + } + } final var exception = result.second(); if (hasCaughtUpWithOldMessages) { - handleQueuedActions(queuedActions); + handleQueuedActions(queuedActions.keySet()); queuedActions.clear(); } if (cachedMessage[0] != null) { if (exception instanceof UntrustedIdentityException) { logger.debug("Keeping message with untrusted identity in message cache"); final var address = ((UntrustedIdentityException) exception).getSender(); - final var recipientId = resolveRecipient(address); + final var recipientId = account.getRecipientStore().resolveRecipient(address); if (!envelope.hasSourceUuid()) { try { cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); @@ -1088,8 +1116,21 @@ public class ManagerImpl implements Manager { } } } - handleQueuedActions(queuedActions); + handleQueuedActions(queuedActions.keySet()); queuedActions.clear(); + dependencies.getSignalWebSocket().disconnect(); + webSocketStateDisposable.dispose(); + } + + private void onWebSocketStateChange(final WebSocketConnectionState s) { + if (s.equals(WebSocketConnectionState.AUTHENTICATION_FAILED)) { + account.setRegistered(false); + try { + close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } @Override @@ -1106,6 +1147,7 @@ public class ManagerImpl implements Manager { logger.debug("Handling message actions"); var interrupted = false; for (var action : queuedActions) { + logger.debug("Executing action {}", action.getClass().getSimpleName()); try { action.execute(context); } catch (Throwable e) { @@ -1126,21 +1168,16 @@ public class ManagerImpl implements Manager { public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { final RecipientId recipientId; try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (IOException | UnregisteredRecipientException e) { return false; } - return contactHelper.isContactBlocked(recipientId); - } - - @Override - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return attachmentHelper.getAttachmentFile(attachmentId); + return context.getContactHelper().isContactBlocked(recipientId); } @Override public void sendContacts() throws IOException { - syncHelper.sendContacts(); + context.getSyncHelper().sendContacts(); } @Override @@ -1149,15 +1186,15 @@ public class ManagerImpl implements Manager { .getContacts() .stream() .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second())) - .collect(Collectors.toList()); + .toList(); } @Override public String getContactOrProfileName(RecipientIdentifier.Single recipient) { final RecipientId recipientId; try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (IOException | UnregisteredRecipientException e) { return null; } @@ -1166,7 +1203,7 @@ public class ManagerImpl implements Manager { return contact.getName(); } - final var profile = getRecipientProfile(recipientId); + final var profile = context.getProfileHelper().getRecipientProfile(recipientId); if (profile != null) { return profile.getDisplayName(); } @@ -1176,20 +1213,12 @@ public class ManagerImpl implements Manager { @Override public Group getGroup(GroupId groupId) { - return toGroup(groupHelper.getGroup(groupId)); - } - - private GroupInfo getGroupInfo(GroupId groupId) { - return groupHelper.getGroup(groupId); + return toGroup(context.getGroupHelper().getGroup(groupId)); } @Override public List getIdentities() { - return account.getIdentityKeyStore() - .getIdentities() - .stream() - .map(this::toIdentity) - .collect(Collectors.toList()); + return account.getIdentityKeyStore().getIdentities().stream().map(this::toIdentity).toList(); } private Identity toIdentity(final IdentityInfo identityInfo) { @@ -1198,11 +1227,12 @@ public class ManagerImpl implements Manager { } final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); - final var scannableFingerprint = identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), - identityInfo.getIdentityKey()); + final var scannableFingerprint = context.getIdentityHelper() + .computeSafetyNumberForScanning(identityInfo.getRecipientId(), identityInfo.getIdentityKey()); return new Identity(address, identityInfo.getIdentityKey(), - identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), + context.getIdentityHelper() + .computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), scannableFingerprint == null ? null : scannableFingerprint.getSerialized(), identityInfo.getTrustLevel(), identityInfo.getDateAdded()); @@ -1212,8 +1242,9 @@ public class ManagerImpl implements Manager { public List getIdentities(RecipientIdentifier.Single recipient) { IdentityInfo identity; try { - identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); - } catch (UnregisteredUserException e) { + identity = account.getIdentityKeyStore() + .getIdentity(context.getRecipientHelper().resolveRecipient(recipient)); + } catch (IOException | UnregisteredRecipientException e) { identity = null; } return identity == null ? List.of() : List.of(toIdentity(identity)); @@ -1222,141 +1253,104 @@ public class ManagerImpl implements Manager { /** * Trust this the identity with this fingerprint * - * @param recipient username of the identity + * @param recipient account of the identity * @param fingerprint Fingerprint */ @Override - public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { + public boolean trustIdentityVerified( + RecipientIdentifier.Single recipient, byte[] fingerprint + ) throws UnregisteredRecipientException { RecipientId recipientId; try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (IOException e) { return false; } - return identityHelper.trustIdentityVerified(recipientId, fingerprint); + final var updated = context.getIdentityHelper().trustIdentityVerified(recipientId, fingerprint); + if (updated && this.isReceiving()) { + needsToRetryFailedMessages = true; + } + return updated; } /** * Trust this the identity with this safety number * - * @param recipient username of the identity + * @param recipient account of the identity * @param safetyNumber Safety number */ @Override - public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { + public boolean trustIdentityVerifiedSafetyNumber( + RecipientIdentifier.Single recipient, String safetyNumber + ) throws UnregisteredRecipientException { RecipientId recipientId; try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (IOException e) { return false; } - return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); + final var updated = context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); + if (updated && this.isReceiving()) { + needsToRetryFailedMessages = true; + } + return updated; } /** * Trust this the identity with this scannable safety number * - * @param recipient username of the identity + * @param recipient account of the identity * @param safetyNumber Scannable safety number */ @Override - public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { + public boolean trustIdentityVerifiedSafetyNumber( + RecipientIdentifier.Single recipient, byte[] safetyNumber + ) throws UnregisteredRecipientException { RecipientId recipientId; try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (IOException e) { return false; } - return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); + final var updated = context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); + if (updated && this.isReceiving()) { + needsToRetryFailedMessages = true; + } + return updated; } /** * Trust all keys of this identity without verification * - * @param recipient username of the identity + * @param recipient account of the identity */ @Override - public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { RecipientId recipientId; try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (IOException e) { return false; } - return identityHelper.trustIdentityAllKeys(recipientId); - } - - private void handleIdentityFailure( - final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure - ) { - this.identityHelper.handleIdentityFailure(recipientId, identityFailure); - } - - @Override - public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { - return resolveSignalServiceAddress(resolveRecipient(address)); - } - - private SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { - final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); - if (address.getUuid().isPresent()) { - return address.toSignalServiceAddress(); - } - - // Address in recipient store doesn't have a uuid, this shouldn't happen - // Try to retrieve the uuid from the server - final var number = address.getNumber().get(); - final UUID uuid; - try { - uuid = getRegisteredUser(number); - } catch (IOException e) { - logger.warn("Failed to get uuid for e164 number: {}", number, e); - // Return SignalServiceAddress with unknown UUID - return address.toSignalServiceAddress(); + final var updated = context.getIdentityHelper().trustIdentityAllKeys(recipientId); + if (updated && this.isReceiving()) { + needsToRetryFailedMessages = true; } - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); + return updated; } - private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { - final var recipientIds = new HashSet(recipients.size()); - for (var number : recipients) { - final var recipientId = resolveRecipient(number); - recipientIds.add(recipientId); - } - return recipientIds; - } - - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { - if (recipient instanceof RecipientIdentifier.Uuid) { - return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); - } else { - final var number = ((RecipientIdentifier.Number) recipient).number; - return account.getRecipientStore().resolveRecipient(number, () -> { - try { - return getRegisteredUser(number); - } catch (IOException e) { - return null; - } - }); + @Override + public void addClosedListener(final Runnable listener) { + synchronized (closedListeners) { + closedListeners.add(listener); } } - private RecipientId resolveRecipient(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipient(address); - } - - private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipientTrusted(address); - } - @Override public void close() throws IOException { - close(true); - } - - private void close(boolean closeAccount) throws IOException { Thread thread; synchronized (messageHandlers) { + weakHandlers.clear(); messageHandlers.clear(); thread = receiveThread; receiveThread = null; @@ -1368,7 +1362,12 @@ public class ManagerImpl implements Manager { dependencies.getSignalWebSocket().disconnect(); - if (closeAccount && account != null) { + synchronized (closedListeners) { + closedListeners.forEach(Runnable::run); + closedListeners.clear(); + } + + if (account != null) { account.close(); } account = null;