X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/e495afcff5f20cc2207bddac4923d015263f3e36..5cccf521032954d7ad8e3f70e3cbef2ce1293e85:/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 9e81f206..c15f1612 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2021 AsamK and contributors + Copyright (C) 2015-2022 AsamK and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,22 +16,28 @@ */ package org.asamk.signal.manager; -import org.asamk.signal.manager.actions.HandleAction; +import org.asamk.signal.manager.api.AttachmentInvalidException; 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.InvalidStickerException; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.NotMasterDeviceException; 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.StickerPack; +import org.asamk.signal.manager.api.StickerPackId; +import org.asamk.signal.manager.api.StickerPackInvalidException; +import org.asamk.signal.manager.api.StickerPackUrl; 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; @@ -39,51 +45,30 @@ 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; -import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.stickerPacks.JsonStickerPack; +import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore; import org.asamk.signal.manager.storage.stickers.Sticker; -import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; 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.ACI; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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.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; @@ -91,11 +76,8 @@ import org.whispersystems.signalservice.internal.util.Util; import java.io.File; import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.SignatureException; -import java.util.Collection; +import java.time.Duration; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -104,43 +86,30 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +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 static org.asamk.signal.manager.config.ServiceConfig.capabilities; +import io.reactivex.rxjava3.disposables.CompositeDisposable; -public class ManagerImpl implements Manager { +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 SignalDependencies dependencies; + private final Context context; 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 messageHandlers = new HashSet<>(); private boolean isReceivingSynchronous; + private final Set weakHandlers = new HashSet<>(); + private final Set messageHandlers = new HashSet<>(); + private final List closedListeners = new ArrayList<>(); + private final CompositeDisposable disposable = new CompositeDisposable(); ManagerImpl( SignalAccount account, @@ -149,10 +118,9 @@ public class ManagerImpl implements Manager { String userAgent ) { this.account = account; - this.serviceEnvironmentConfig = serviceEnvironmentConfig; final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(), - account.getUsername(), + account.getAccount(), account.getPassword(), account.getDeviceId()); final var sessionLock = new SignalSessionLock() { @@ -174,124 +142,59 @@ 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); + this.context.getAccountHelper().setUnregisteredListener(this::close); + this.context.getReceiveHelper().setAuthenticationFailureListener(this::close); + this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> { + synchronized (this) { + this.notifyAll(); + } + }); + disposable.add(account.getIdentityKeyStore().getIdentityChanges().subscribe(recipientId -> { + logger.trace("Archiving old sessions for {}", recipientId); + account.getSessionStore().archiveSessions(recipientId); + account.getSenderKeyStore().deleteSharedWith(recipientId); + final var profile = account.getRecipientStore().getProfile(recipientId); + if (profile != null) { + account.getRecipientStore() + .storeProfile(recipientId, + Profile.newBuilder(profile) + .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN) + .withLastUpdateTimestamp(0) + .build()); + } + })); } @Override public String getSelfNumber() { - return account.getUsername(); + return account.getAccount(); } - @Override - public void checkAccountState() throws IOException { - if (account.getLastReceiveTimestamp() == 0) { - logger.info("The Signal protocol expects that incoming messages are regularly received."); - } else { - var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); - long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); - if (days > 7) { - logger.warn( - "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.", - days); - } - } - preKeyHelper.refreshPreKeysIfNecessary(); - if (account.getAci() == null) { - account.setAci(dependencies.getAccountManager().getOwnAci()); - } - updateAccountAttributes(null); + void checkAccountState() throws IOException { + context.getAccountHelper().checkAccountState(); } - /** - * This is used for checking a set of phone numbers for registration on Signal - * - * @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 - */ @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); @@ -302,40 +205,22 @@ public class ManagerImpl implements Manager { @Override public void updateAccountAttributes(String deviceName) throws IOException { - final String encryptedDeviceName; - if (deviceName == null) { - encryptedDeviceName = account.getEncryptedDeviceName(); - } else { - final var privateKey = account.getIdentityKeyPair().getPrivateKey(); - encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); - account.setEncryptedDeviceName(encryptedDeviceName); + if (deviceName != null) { + context.getAccountHelper().setDeviceName(deviceName); } - dependencies.getAccountManager() - .setAccountAttributes(encryptedDeviceName, - null, - account.getLocalRegistrationId(), - true, - null, - account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - capabilities, - account.isDiscoverableByPhoneNumber()); + context.getAccountHelper().updateAccountAttributes(); } @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())); + return Configuration.from(configurationStore); } @Override public void updateConfiguration( Configuration configuration - ) throws IOException, NotMasterDeviceException { + ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } @@ -353,54 +238,36 @@ public class ManagerImpl implements Manager { if (configuration.linkPreviews().isPresent()) { configurationStore.setLinkPreviews(configuration.linkPreviews().get()); } - syncHelper.sendConfigurationMessage(); + context.getSyncHelper().sendConfigurationMessage(); } - /** - * @param givenName if null, the previous givenName will be kept - * @param familyName if null, the previous familyName will be kept - * @param about if null, the previous about text will be kept - * @param aboutEmoji if null, the previous about emoji will be kept - * @param avatar if avatar is null the image from the local avatar store is used (if present), - */ @Override 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 public void unregister() throws IOException { - // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. - // If this is the master 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.absent()); - - account.setRegistered(false); + context.getAccountHelper().unregister(); } @Override public void deleteAccount() throws IOException { - try { - pinHelper.removeRegistrationLockPin(); - } catch (IOException e) { - logger.warn("Failed to remove registration lock pin"); - } - account.setRegistrationLockPin(null, null); - - dependencies.getAccountManager().deleteAccount(); - - account.setRegistered(false); + context.getAccountHelper().deleteAccount(); } @Override public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { + captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); + dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); } @@ -423,79 +290,44 @@ public class ManagerImpl implements Manager { d.getCreated(), d.getLastSeen(), d.getId() == account.getDeviceId()); - }).collect(Collectors.toList()); + }).toList(); } @Override - public void removeLinkedDevices(long deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); + public void removeLinkedDevices(int deviceId) throws IOException { + context.getAccountHelper().removeLinkedDevices(deviceId); } @Override 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, InvalidDeviceLinkException { - var identityKeyPair = account.getIdentityKeyPair(); - var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); - - 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); + var deviceLinkInfo = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + context.getAccountHelper().addDevice(deviceLinkInfo); } @Override - public void setRegistrationLockPin(java.util.Optional pin) throws IOException { + public void setRegistrationLockPin(java.util.Optional pin) throws IOException, NotMasterDeviceException { if (!account.isMasterDevice()) { - throw new RuntimeException("Only master device can set a PIN"); + throw new NotMasterDeviceException(); } if (pin.isPresent()) { - final var masterKey = account.getPinMasterKey() != null - ? account.getPinMasterKey() - : KeyUtils.createMasterKey(); - - pinHelper.setRegistrationLockPin(pin.get(), masterKey); - - account.setRegistrationLockPin(pin.get(), masterKey); + context.getAccountHelper().setRegistrationPin(pin.get()); } else { - // Remove KBS Pin - pinHelper.removeRegistrationLockPin(); - - account.setRegistrationLockPin(null, null); + context.getAccountHelper().removeRegistrationPin(); } } void refreshPreKeys() throws IOException { - preKeyHelper.refreshPreKeys(); + context.getPreKeyHelper().refreshPreKeys(); } @Override - public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException { - 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) { @@ -503,80 +335,68 @@ public class ManagerImpl implements Manager { return null; } - return new Group(groupInfo.getGroupId(), - groupInfo.getTitle(), - groupInfo.getDescription(), - groupInfo.getGroupInviteLink(), - groupInfo.getMembers() - .stream() - .map(account.getRecipientStore()::resolveRecipientAddress) - .collect(Collectors.toSet()), - groupInfo.getPendingMembers() - .stream() - .map(account.getRecipientStore()::resolveRecipientAddress) - .collect(Collectors.toSet()), - groupInfo.getRequestingMembers() - .stream() - .map(account.getRecipientStore()::resolveRecipientAddress) - .collect(Collectors.toSet()), - groupInfo.getAdminMembers() - .stream() - .map(account.getRecipientStore()::resolveRecipientAddress) - .collect(Collectors.toSet()), - groupInfo.isBlocked(), - groupInfo.getMessageExpirationTimer(), - groupInfo.getPermissionAddMember(), - groupInfo.getPermissionEditDetails(), - groupInfo.getPermissionSendMessage(), - groupInfo.isMember(account.getSelfRecipientId()), - groupInfo.isAdmin(account.getSelfRecipientId())); + return Group.from(groupInfo, + account.getRecipientStore()::resolveRecipientAddress, + account.getSelfRecipientId()); } @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, InactiveGroupLinkException { - return groupHelper.joinGroup(inviteLinkUrl); + return context.getGroupHelper().joinGroup(inviteLinkUrl); } private SendMessageResults sendMessage( @@ -587,81 +407,107 @@ 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(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress))); + try { + final var recipientId = context.getRecipientHelper().resolveRecipient(single); + final var result = context.getSendHelper().sendMessage(messageBuilder, recipientId); + results.put(recipient, List.of(toSendMessageResult(result))); + } 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(SendMessageResult.from(result, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress))); + final var result = context.getSendHelper().sendSelfMessage(messageBuilder); + results.put(recipient, List.of(toSendMessageResult(result))); } else if (recipient instanceof RecipientIdentifier.Group group) { - final var result = sendHelper.sendAsGroupMessage(messageBuilder, group.groupId()); - results.put(recipient, - result.stream() - .map(sendMessageResult -> SendMessageResult.from(sendMessageResult, - account.getRecipientStore(), - account.getRecipientStore()::resolveRecipientAddress)) - .collect(Collectors.toList())); + final var result = context.getSendHelper().sendAsGroupMessage(messageBuilder, group.groupId()); + results.put(recipient, result.stream().map(this::toSendMessageResult).toList()); } } return new SendMessageResults(timestamp, results); } - private void sendTypingMessage( + private SendMessageResult toSendMessageResult(final org.whispersystems.signalservice.api.messages.SendMessageResult result) { + return SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress); + } + + 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(toSendMessageResult(result))); + } 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 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(this::toSendMessageResult).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(toSendMessageResult(result)))); + } 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, InvalidStickerException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); @@ -669,20 +515,78 @@ public class ManagerImpl implements Manager { private void applyMessage( final SignalServiceDataMessage.Builder messageBuilder, final Message message - ) throws AttachmentInvalidException, IOException { + ) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException { 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()))); + } + if (message.sticker().isPresent()) { + final var sticker = message.sticker().get(); + final var packId = StickerPackId.deserialize(sticker.packId()); + final var stickerId = sticker.stickerId(); + + final var stickerPack = context.getAccount().getStickerStore().getStickerPack(packId); + if (stickerPack == null) { + throw new InvalidStickerException("Sticker pack not found"); + } + final var manifest = context.getStickerHelper().getOrRetrieveStickerPack(packId, stickerPack.getPackKey()); + if (manifest.stickers().size() <= stickerId) { + throw new InvalidStickerException("Sticker id not part of this pack"); + } + final var manifestSticker = manifest.stickers().get(stickerId); + final var streamDetails = context.getStickerPackStore().retrieveSticker(packId, stickerId); + if (streamDetails == null) { + throw new InvalidStickerException("Missing local sticker file"); + } + messageBuilder.withSticker(new SignalServiceDataMessage.Sticker(packId.serialize(), + stickerPack.getPackKey(), + stickerId, + manifestSticker.emoji(), + AttachmentUtils.createAttachment(streamDetails, Optional.absent()))); } } + 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 public SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single r) { + try { + final var recipientId = context.getRecipientHelper().resolveRecipient(r); + account.getMessageSendLogStore().deleteEntryForRecipientNonGroup(targetSentTimestamp, recipientId); + } catch (UnregisteredRecipientException ignored) { + } + } else if (recipient instanceof RecipientIdentifier.Group r) { + account.getMessageSendLogStore().deleteEntryForGroup(targetSentTimestamp, r.groupId()); + } + } return sendMessage(messageBuilder, recipients); } @@ -693,11 +597,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); @@ -714,55 +618,68 @@ 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, IOException { + ) 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 public void setGroupBlocked( final GroupId groupId, final boolean blocked - ) throws GroupNotFoundException, IOException, NotMasterDeviceException { + ) throws GroupNotFoundException, NotMasterDeviceException { 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(); } - /** - * Change the expiration timer for a contact - */ @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)); @@ -771,14 +688,8 @@ public class ManagerImpl implements Manager { } } - /** - * Upload the sticker pack from path. - * - * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file - * @return if successful, returns the URL to install the sticker pack in the signal app - */ @Override - public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { + public StickerPackUrl uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var messageSender = dependencies.getMessageSender(); @@ -790,147 +701,78 @@ public class ManagerImpl implements Manager { var sticker = new Sticker(packId, packKey); account.getStickerStore().updateSticker(sticker); - try { - return new URI("https", - "signal.art", - "/addstickers/", - "pack_id=" - + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) - + "&pack_key=" - + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); - } catch (URISyntaxException e) { - throw new AssertionError(e); - } + return new StickerPackUrl(packId, packKey); + } + + @Override + public List getStickerPacks() { + final var stickerPackStore = context.getStickerPackStore(); + return account.getStickerStore().getStickerPacks().stream().map(pack -> { + if (stickerPackStore.existsStickerPack(pack.getPackId())) { + try { + final var manifest = stickerPackStore.retrieveManifest(pack.getPackId()); + return new StickerPack(pack.getPackId(), + new StickerPackUrl(pack.getPackId(), pack.getPackKey()), + pack.isInstalled(), + manifest.title(), + manifest.author(), + java.util.Optional.ofNullable(manifest.cover() == null ? null : manifest.cover().toApi()), + manifest.stickers().stream().map(JsonStickerPack.JsonSticker::toApi).toList()); + } catch (Exception e) { + logger.warn("Failed to read local sticker pack manifest: {}", e.getMessage(), e); + } + } + + return new StickerPack(pack.getPackId(), pack.getPackKey(), pack.isInstalled()); + }).toList(); } @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 ACI getRegisteredUser(final String number) throws IOException { - final Map aciMap; - try { - aciMap = getRegisteredUsers(Set.of(number)); - } catch (NumberFormatException e) { - throw new IOException(number, e); - } - final var uuid = aciMap.get(number); - if (uuid == null) { - throw new IOException(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, aci) -> resolveRecipientTrusted(new SignalServiceAddress(aci, number))); - - return registeredUsers; - } - - private void retryFailedReceivedMessages(ReceiveMessageHandler handler) { - Set queuedActions = new HashSet<>(); - for (var cachedMessage : account.getMessageCache().getCachedMessages()) { - var actions = retryFailedReceivedMessage(handler, cachedMessage); - if (actions != null) { - queuedActions.addAll(actions); - } - } - handleQueuedActions(queuedActions); - } - - private List retryFailedReceivedMessage( - final ReceiveMessageHandler handler, final CachedMessage cachedMessage - ) { - var envelope = cachedMessage.loadEnvelope(); - if (envelope == null) { - cachedMessage.delete(); - return null; - } - - final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); - final var actions = result.first(); - final var exception = result.second(); - - if (exception instanceof UntrustedIdentityException) { - if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { - // Envelope is more than a month old, cleaning up. - cachedMessage.delete(); - return null; - } - if (!envelope.hasSourceUuid()) { - final var identifier = ((UntrustedIdentityException) exception).getSender(); - final var recipientId = account.getRecipientStore().resolveRecipient(identifier); - try { - account.getMessageCache().replaceSender(cachedMessage, recipientId); - } catch (IOException ioException) { - logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); - } - } - return null; - } - - // If successful and for all other errors that are not recoverable, delete the cached message - cachedMessage.delete(); - return actions; } @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(); + } } } + private static final AtomicInteger threadNumber = new AtomicInteger(0); + private void startReceiveThreadIfRequired() { if (receiveThread != null) { return; } receiveThread = new Thread(() -> { + logger.debug("Starting receiving messages"); while (!Thread.interrupted()) { try { - receiveMessagesInternal(1L, TimeUnit.HOURS, false, (envelope, e) -> { + context.getReceiveHelper().receiveMessages(Duration.ofMinutes(1), false, (envelope, e) -> { synchronized (messageHandlers) { - for (ReceiveMessageHandler h : messageHandlers) { + Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> { try { h.handleMessage(envelope, e); } catch (Exception ex) { logger.warn("Message handler failed, ignoring", ex); } - } + }); } }); break; @@ -938,16 +780,18 @@ public class ManagerImpl implements Manager { logger.warn("Receiving messages failed, retrying", e); } } - hasCaughtUpWithOldMessages = false; + logger.debug("Finished receiving messages"); 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(); } } }); + receiveThread.setName("receive-" + threadNumber.getAndIncrement()); receiveThread.start(); } @@ -956,8 +800,9 @@ public class ManagerImpl implements Manager { public void removeReceiveHandler(final ReceiveMessageHandler handler) { final Thread thread; synchronized (messageHandlers) { + weakHandlers.remove(handler); messageHandlers.remove(handler); - if (!messageHandlers.isEmpty() || isReceivingSynchronous) { + if (!messageHandlers.isEmpty() || receiveThread == null || isReceivingSynchronous) { return; } thread = receiveThread; @@ -986,17 +831,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."); @@ -1004,162 +849,37 @@ public class ManagerImpl implements Manager { isReceivingSynchronous = true; receiveThread = Thread.currentThread(); try { - receiveMessagesInternal(timeout, unit, returnOnTimeout, handler); + context.getReceiveHelper().receiveMessages(timeout, returnOnTimeout, handler); } finally { receiveThread = null; - hasCaughtUpWithOldMessages = false; isReceivingSynchronous = false; } } - private void receiveMessagesInternal( - long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler - ) throws IOException { - retryFailedReceivedMessages(handler); - - Set queuedActions = new HashSet<>(); - - final var signalWebSocket = dependencies.getSignalWebSocket(); - signalWebSocket.connect(); - - hasCaughtUpWithOldMessages = false; - var backOffCounter = 0; - final var MAX_BACKOFF_COUNTER = 9; - - while (!Thread.interrupted()) { - SignalServiceEnvelope envelope; - final CachedMessage[] cachedMessage = {null}; - account.setLastReceiveTimestamp(System.currentTimeMillis()); - 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; - // store message on disk, before acknowledging receipt to the server - cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); - }); - backOffCounter = 0; - - if (result.isPresent()) { - envelope = result.get(); - logger.debug("New message received from server"); - } else { - logger.debug("Received indicator that server queue is empty"); - handleQueuedActions(queuedActions); - queuedActions.clear(); - - hasCaughtUpWithOldMessages = true; - synchronized (this) { - this.notifyAll(); - } - - // Continue to wait another timeout for new messages - continue; - } - } catch (AssertionError e) { - if (e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - break; - } else { - throw e; - } - } catch (IOException e) { - logger.debug("Pipe unexpectedly unavailable: {}", e.getMessage()); - if (e instanceof WebSocketUnavailableException || "Connection closed!".equals(e.getMessage())) { - final var sleepMilliseconds = 100 * (long) Math.pow(2, backOffCounter); - backOffCounter = Math.min(backOffCounter + 1, MAX_BACKOFF_COUNTER); - logger.warn("Connection closed unexpectedly, reconnecting in {} ms", sleepMilliseconds); - try { - Thread.sleep(sleepMilliseconds); - } catch (InterruptedException interruptedException) { - return; - } - hasCaughtUpWithOldMessages = false; - signalWebSocket.connect(); - continue; - } - throw e; - } catch (TimeoutException e) { - backOffCounter = 0; - if (returnOnTimeout) return; - continue; - } - - final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); - queuedActions.addAll(result.first()); - final var exception = result.second(); - - if (hasCaughtUpWithOldMessages) { - handleQueuedActions(queuedActions); - 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); - if (!envelope.hasSourceUuid()) { - try { - cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); - } catch (IOException ioException) { - logger.warn("Failed to move cached message to recipient folder: {}", - ioException.getMessage()); - } - } - } else { - cachedMessage[0].delete(); - } - } - } - handleQueuedActions(queuedActions); - queuedActions.clear(); - } - @Override public void setIgnoreAttachments(final boolean ignoreAttachments) { - this.ignoreAttachments = ignoreAttachments; + context.getReceiveHelper().setIgnoreAttachments(ignoreAttachments); } @Override public boolean hasCaughtUpWithOldMessages() { - return hasCaughtUpWithOldMessages; - } - - private void handleQueuedActions(final Collection queuedActions) { - logger.debug("Handling message actions"); - var interrupted = false; - for (var action : queuedActions) { - try { - action.execute(context); - } catch (Throwable e) { - if ((e instanceof AssertionError || e instanceof RuntimeException) - && e.getCause() instanceof InterruptedException) { - interrupted = true; - continue; - } - logger.warn("Message action failed.", e); - } - } - if (interrupted) { - Thread.currentThread().interrupt(); - } + return context.getReceiveHelper().hasCaughtUpWithOldMessages(); } @Override public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { final RecipientId recipientId; try { - recipientId = resolveRecipient(recipient); - } catch (IOException e) { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (IOException | UnregisteredRecipientException e) { return false; } - return contactHelper.isContactBlocked(recipientId); + return context.getContactHelper().isContactBlocked(recipientId); } @Override public void sendContacts() throws IOException { - syncHelper.sendContacts(); + context.getSyncHelper().sendContacts(); } @Override @@ -1168,15 +888,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 (IOException e) { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (IOException | UnregisteredRecipientException e) { return null; } @@ -1185,7 +905,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(); } @@ -1195,20 +915,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) { @@ -1217,11 +929,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()); @@ -1231,151 +944,70 @@ public class ManagerImpl implements Manager { public List getIdentities(RecipientIdentifier.Single recipient) { IdentityInfo identity; try { - identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); - } catch (IOException e) { + identity = account.getIdentityKeyStore() + .getIdentity(context.getRecipientHelper().resolveRecipient(recipient)); + } catch (IOException | UnregisteredRecipientException e) { identity = null; } return identity == null ? List.of() : List.of(toIdentity(identity)); } - /** - * Trust this the identity with this fingerprint - * - * @param recipient username of the identity - * @param fingerprint Fingerprint - */ @Override - public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - return identityHelper.trustIdentityVerified(recipientId, fingerprint); + public boolean trustIdentityVerified( + RecipientIdentifier.Single recipient, byte[] fingerprint + ) throws UnregisteredRecipientException { + return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityVerified(r, fingerprint)); } - /** - * Trust this the identity with this safety number - * - * @param recipient username of the identity - * @param safetyNumber Safety number - */ @Override - public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); + public boolean trustIdentityVerifiedSafetyNumber( + RecipientIdentifier.Single recipient, String safetyNumber + ) throws UnregisteredRecipientException { + return trustIdentity(recipient, + r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber)); } - /** - * Trust this the identity with this scannable safety number - * - * @param recipient username of the identity - * @param safetyNumber Scannable safety number - */ @Override - public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); + public boolean trustIdentityVerifiedSafetyNumber( + RecipientIdentifier.Single recipient, byte[] safetyNumber + ) throws UnregisteredRecipientException { + return trustIdentity(recipient, + r -> context.getIdentityHelper().trustIdentityVerifiedSafetyNumber(r, safetyNumber)); } - /** - * Trust all keys of this identity without verification - * - * @param recipient username of the identity - */ @Override - public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (IOException e) { - return false; - } - return identityHelper.trustIdentityAllKeys(recipientId); + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { + return trustIdentity(recipient, r -> context.getIdentityHelper().trustIdentityAllKeys(r)); } - private void handleIdentityFailure( - final RecipientId recipientId, - final org.whispersystems.signalservice.api.messages.SendMessageResult.IdentityFailure identityFailure - ) { - this.identityHelper.handleIdentityFailure(recipientId, identityFailure); - } - - 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 ACI aci; + private boolean trustIdentity( + RecipientIdentifier.Single recipient, Function trustMethod + ) throws UnregisteredRecipientException { + RecipientId recipientId; try { - aci = getRegisteredUser(number); + recipientId = context.getRecipientHelper().resolveRecipient(recipient); } catch (IOException e) { - logger.warn("Failed to get uuid for e164 number: {}", number, e); - // Return SignalServiceAddress with unknown UUID - return address.toSignalServiceAddress(); + return false; } - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(aci)); - } - - private Set resolveRecipients(Collection recipients) throws IOException { - final var recipientIds = new HashSet(recipients.size()); - for (var number : recipients) { - final var recipientId = resolveRecipient(number); - recipientIds.add(recipientId); + final var updated = trustMethod.apply(recipientId); + if (updated && this.isReceiving()) { + context.getReceiveHelper().setNeedsToRetryFailedMessages(true); } - return recipientIds; + return updated; } - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws IOException { - if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) { - return account.getRecipientStore().resolveRecipient(ACI.from(uuidRecipient.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(RecipientAddress address) { - return account.getRecipientStore().resolveRecipient(address); - } - - 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 { + public void close() { Thread thread; synchronized (messageHandlers) { + weakHandlers.clear(); messageHandlers.clear(); thread = receiveThread; receiveThread = null; @@ -1386,8 +1018,14 @@ public class ManagerImpl implements Manager { executor.shutdown(); dependencies.getSignalWebSocket().disconnect(); + disposable.dispose(); + + synchronized (closedListeners) { + closedListeners.forEach(Runnable::run); + closedListeners.clear(); + } - if (closeAccount && account != null) { + if (account != null) { account.close(); } account = null;