X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/f094cd6806aae68fba9ebd0cf29eaf4eabf042d2..7e7e4150e19a65c646bb3f4ec33b5ec87224f8cf:/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 d2ffaaab..269edb3b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -17,12 +17,17 @@ 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.UpdateGroup; @@ -64,18 +69,14 @@ 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.ACI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -94,6 +95,7 @@ import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SignatureException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -107,6 +109,7 @@ 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 static org.asamk.signal.manager.config.ServiceConfig.capabilities; @@ -135,6 +138,13 @@ public class ManagerImpl implements Manager { 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; ManagerImpl( SignalAccount account, @@ -145,8 +155,8 @@ public class ManagerImpl implements Manager { 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() { @@ -164,9 +174,9 @@ public class ManagerImpl implements Manager { account.getSignalProtocolStore(), executor, sessionLock); - final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + final var avatarStore = new AvatarStore(pathConfig.avatarsPath()); + 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()); @@ -241,7 +251,7 @@ public class ManagerImpl implements Manager { @Override public String getSelfNumber() { - return account.getUsername(); + return account.getAccount(); } @Override @@ -258,8 +268,8 @@ public class ManagerImpl implements Manager { } } preKeyHelper.refreshPreKeysIfNecessary(); - if (account.getUuid() == null) { - account.setUuid(dependencies.getAccountManager().getOwnUuid()); + if (account.getAci() == null) { + account.setAci(dependencies.getAccountManager().getOwnAci()); } updateAccountAttributes(null); } @@ -269,13 +279,17 @@ 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 -> { 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 ""; } @@ -289,8 +303,8 @@ public class ManagerImpl implements Manager { 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()); })); } @@ -317,29 +331,35 @@ 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(); } @@ -353,9 +373,13 @@ public class ManagerImpl implements Manager { */ @Override public void setProfile( - String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + String givenName, final String familyName, String about, String aboutEmoji, java.util.Optional avatar ) throws IOException { - profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); + profileHelper.setProfile(givenName, + familyName, + about, + aboutEmoji, + avatar == null ? null : Optional.fromNullable(avatar.orElse(null))); syncHelper.sendSyncFetchProfileMessage(); } @@ -367,13 +391,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) { + } catch (IOException e) { logger.warn("Failed to remove registration lock pin"); } account.setRegistrationLockPin(null, null); @@ -381,10 +406,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); } @@ -418,27 +446,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); + 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(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"); } @@ -463,7 +497,7 @@ public class ManagerImpl implements Manager { } @Override - public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException { + public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException { return profileHelper.getRecipientProfile(resolveRecipient(recipient)); } @@ -553,7 +587,7 @@ public class ManagerImpl implements Manager { @Override public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl - ) throws IOException, GroupLinkNotActiveException { + ) throws IOException, InactiveGroupLinkException { return groupHelper.joinGroup(inviteLinkUrl); } @@ -564,66 +598,100 @@ public class ManagerImpl implements Manager { long timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); for (final var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single) { - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + if (recipient instanceof RecipientIdentifier.Single single) { + final var recipientId = resolveRecipient(single); final var result = sendHelper.sendMessage(messageBuilder, recipientId); - results.put(recipient, List.of(result)); + results.put(recipient, + List.of(SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress))); } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { final var result = sendHelper.sendSelfMessage(messageBuilder); - results.put(recipient, List.of(result)); - } else if (recipient instanceof RecipientIdentifier.Group) { - final var groupId = ((RecipientIdentifier.Group) recipient).groupId; - final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); - results.put(recipient, result); + 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.stream() + .map(sendMessageResult -> SendMessageResult.from(sendMessageResult, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress)) + .collect(Collectors.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) { final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - sendHelper.sendTypingMessage(message, recipientId); + final var result = sendHelper.sendTypingMessage(message, recipientId); + results.put(recipient, + List.of(SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress))); } 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 = sendHelper.sendGroupTypingMessage(message, groupId); + results.put(recipient, + result.stream() + .map(r -> SendMessageResult.from(r, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress)) + .collect(Collectors.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)); + final var result = sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + return new SendMessageResults(timestamp, + Map.of(sender, + List.of(SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress)))); } @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); - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + final var result = sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + return new SendMessageResults(timestamp, + Map.of(sender, + List.of(SendMessageResult.from(result, + account.getRecipientStore(), + account.getRecipientStore()::resolveRecipientAddress)))); } @Override @@ -638,11 +706,33 @@ public class ManagerImpl implements Manager { private void applyMessage( final SignalServiceDataMessage.Builder messageBuilder, final Message message ) throws AttachmentInvalidException, IOException { - messageBuilder.withBody(message.getMessageText()); - final var attachments = message.getAttachments(); + messageBuilder.withBody(message.messageText()); + final var attachments = message.attachments(); if (attachments != null) { messageBuilder.withAttachments(attachmentHelper.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(), + resolveSignalServiceAddress(resolveRecipient(quote.author())), + quote.message(), + List.of(), + resolveMentions(quote.mentions()))); + } + } + + private ArrayList resolveMentions(final List mentionList) throws IOException { + final var mentions = new ArrayList(); + for (final var m : mentionList) { + final var recipientId = resolveRecipient(m.recipient()); + mentions.add(new SignalServiceDataMessage.Mention(resolveSignalServiceAddress(recipientId).getAci(), + m.start(), + m.length())); + } + return mentions; } @Override @@ -688,10 +778,20 @@ public class ManagerImpl implements Manager { } } + @Override + public void deleteRecipient(final RecipientIdentifier.Single recipient) throws IOException { + account.removeRecipient(resolveRecipient(recipient)); + } + + @Override + public void deleteContact(final RecipientIdentifier.Single recipient) throws IOException { + account.getContactStore().deleteContact(resolveRecipient(recipient)); + } + @Override public void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException, UnregisteredUserException { + ) throws NotMasterDeviceException, IOException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } @@ -793,22 +893,22 @@ public class ManagerImpl implements Manager { return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); } - private UUID getRegisteredUser(final String number) throws IOException { - final Map uuidMap; + private ACI getRegisteredUser(final String number) throws IOException { + final Map aciMap; try { - uuidMap = getRegisteredUsers(Set.of(number)); + aciMap = getRegisteredUsers(Set.of(number)); } catch (NumberFormatException e) { - throw new UnregisteredUserException(number, e); + throw new IOException(number, e); } - final var uuid = uuidMap.get(number); + final var uuid = aciMap.get(number); if (uuid == null) { - throw new UnregisteredUserException(number, null); + throw new IOException(number, null); } return uuid; } - private Map getRegisteredUsers(final Set numbers) throws IOException { - final Map registeredUsers; + private Map getRegisteredUsers(final Set numbers) throws IOException { + final Map registeredUsers; try { registeredUsers = dependencies.getAccountManager() .getRegisteredUsers(ServiceConfig.getIasKeyStore(), @@ -818,16 +918,16 @@ public class ManagerImpl implements Manager { throw new IOException(e); } - // Store numbers as recipients so we have the number/uuid association - registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); + // 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, boolean ignoreAttachments) { + private void retryFailedReceivedMessages(ReceiveMessageHandler handler) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { - var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); + var actions = retryFailedReceivedMessage(handler, cachedMessage); if (actions != null) { queuedActions.addAll(actions); } @@ -836,7 +936,7 @@ public class ManagerImpl implements Manager { } private List retryFailedReceivedMessage( - final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage + final ReceiveMessageHandler handler, final CachedMessage cachedMessage ) { var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { @@ -872,26 +972,143 @@ public class ManagerImpl implements Manager { } @Override - public void receiveMessages( - long timeout, - TimeUnit unit, - boolean returnOnTimeout, - boolean ignoreAttachments, - ReceiveMessageHandler handler + public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) { + if (isReceivingSynchronous) { + throw new IllegalStateException("Already receiving message synchronously."); + } + synchronized (messageHandlers) { + if (isWeakListener) { + weakHandlers.add(handler); + } else { + messageHandlers.add(handler); + startReceiveThreadIfRequired(); + } + } + } + + 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) -> { + synchronized (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; + } catch (IOException e) { + 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(); + } + } + }); + + receiveThread.start(); + } + + @Override + public void removeReceiveHandler(final ReceiveMessageHandler handler) { + final Thread thread; + synchronized (messageHandlers) { + weakHandlers.remove(handler); + messageHandlers.remove(handler); + if (!messageHandlers.isEmpty() || receiveThread == null || isReceivingSynchronous) { + return; + } + thread = receiveThread; + receiveThread = null; + } + + stopReceiveThread(thread); + } + + private void stopReceiveThread(final Thread thread) { + thread.interrupt(); + try { + thread.join(); + } catch (InterruptedException ignored) { + } + } + + @Override + public boolean isReceiving() { + if (isReceivingSynchronous) { + return true; + } + synchronized (messageHandlers) { + return messageHandlers.size() > 0; + } + } + + @Override + public void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException { + receiveMessages(timeout, unit, true, handler); + } + + @Override + public void receiveMessages(ReceiveMessageHandler handler) throws IOException { + receiveMessages(1L, TimeUnit.HOURS, false, handler); + } + + private void receiveMessages( + long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler + ) throws IOException { + if (isReceiving()) { + throw new IllegalStateException("Already receiving message."); + } + isReceivingSynchronous = true; + receiveThread = Thread.currentThread(); + try { + receiveMessagesInternal(timeout, unit, returnOnTimeout, handler); + } finally { + receiveThread = null; + hasCaughtUpWithOldMessages = false; + isReceivingSynchronous = false; + } + } + + private void receiveMessagesInternal( + long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler ) throws IOException { - retryFailedReceivedMessages(handler, ignoreAttachments); + retryFailedReceivedMessages(handler); - 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(); 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()); + 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 -> { @@ -901,12 +1118,14 @@ public class ManagerImpl implements Manager { // 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); + handleQueuedActions(queuedActions.keySet()); queuedActions.clear(); hasCaughtUpWithOldMessages = true; @@ -924,24 +1143,46 @@ public class ManagerImpl implements Manager { } else { throw e; } - } catch (WebSocketUnavailableException e) { - logger.debug("Pipe unexpectedly unavailable, connecting"); - signalWebSocket.connect(); - continue; + } 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()); + 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); if (!envelope.hasSourceUuid()) { @@ -957,7 +1198,14 @@ public class ManagerImpl implements Manager { } } } - handleQueuedActions(queuedActions); + handleQueuedActions(queuedActions.keySet()); + queuedActions.clear(); + dependencies.getSignalWebSocket().disconnect(); + } + + @Override + public void setIgnoreAttachments(final boolean ignoreAttachments) { + this.ignoreAttachments = ignoreAttachments; } @Override @@ -966,6 +1214,7 @@ public class ManagerImpl implements Manager { } private void handleQueuedActions(final Collection queuedActions) { + logger.debug("Handling message actions"); var interrupted = false; for (var action : queuedActions) { try { @@ -989,17 +1238,12 @@ public class ManagerImpl implements Manager { final RecipientId recipientId; try { recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + } catch (IOException e) { return false; } return contactHelper.isContactBlocked(recipientId); } - @Override - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return attachmentHelper.getAttachmentFile(attachmentId); - } - @Override public void sendContacts() throws IOException { syncHelper.sendContacts(); @@ -1019,7 +1263,7 @@ public class ManagerImpl implements Manager { final RecipientId recipientId; try { recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + } catch (IOException e) { return null; } @@ -1060,11 +1304,12 @@ public class ManagerImpl implements Manager { } final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); + final var scannableFingerprint = identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), + identityInfo.getIdentityKey()); return new Identity(address, identityInfo.getIdentityKey(), identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), - identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), - identityInfo.getIdentityKey()).getSerialized(), + scannableFingerprint == null ? null : scannableFingerprint.getSerialized(), identityInfo.getTrustLevel(), identityInfo.getDateAdded()); } @@ -1074,7 +1319,7 @@ public class ManagerImpl implements Manager { IdentityInfo identity; try { identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); - } catch (UnregisteredUserException e) { + } catch (IOException e) { identity = null; } return identity == null ? List.of() : List.of(toIdentity(identity)); @@ -1083,7 +1328,7 @@ 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 @@ -1091,7 +1336,7 @@ public class ManagerImpl implements Manager { RecipientId recipientId; try { recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + } catch (IOException e) { return false; } return identityHelper.trustIdentityVerified(recipientId, fingerprint); @@ -1100,7 +1345,7 @@ public class ManagerImpl implements Manager { /** * 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 @@ -1108,7 +1353,7 @@ public class ManagerImpl implements Manager { RecipientId recipientId; try { recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + } catch (IOException e) { return false; } return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); @@ -1117,7 +1362,7 @@ public class ManagerImpl implements Manager { /** * 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 @@ -1125,7 +1370,7 @@ public class ManagerImpl implements Manager { RecipientId recipientId; try { recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + } catch (IOException e) { return false; } return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); @@ -1134,30 +1379,33 @@ public class ManagerImpl implements Manager { /** * 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) { RecipientId recipientId; try { recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { + } catch (IOException e) { return false; } return identityHelper.trustIdentityAllKeys(recipientId); } + @Override + public void addClosedListener(final Runnable listener) { + synchronized (closedListeners) { + closedListeners.add(listener); + } + } + private void handleIdentityFailure( - final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure + final RecipientId recipientId, + final org.whispersystems.signalservice.api.messages.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()) { @@ -1167,18 +1415,18 @@ public class ManagerImpl implements Manager { // 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; + final ACI aci; try { - uuid = getRegisteredUser(number); + aci = getRegisteredUser(number); } catch (IOException e) { logger.warn("Failed to get uuid for e164 number: {}", number, e); // Return SignalServiceAddress with unknown UUID return address.toSignalServiceAddress(); } - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(aci)); } - private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { + private Set resolveRecipients(Collection recipients) throws IOException { final var recipientIds = new HashSet(recipients.size()); for (var number : recipients) { final var recipientId = resolveRecipient(number); @@ -1187,11 +1435,11 @@ public class ManagerImpl implements Manager { return recipientIds; } - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { - if (recipient instanceof RecipientIdentifier.Uuid) { - return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); + 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; + final var number = ((RecipientIdentifier.Number) recipient).number(); return account.getRecipientStore().resolveRecipient(number, () -> { try { return getRegisteredUser(number); @@ -1202,6 +1450,10 @@ public class ManagerImpl implements Manager { } } + private RecipientId resolveRecipient(RecipientAddress address) { + return account.getRecipientStore().resolveRecipient(address); + } + private RecipientId resolveRecipient(SignalServiceAddress address) { return account.getRecipientStore().resolveRecipient(address); } @@ -1212,15 +1464,26 @@ public class ManagerImpl implements Manager { @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; + } + if (thread != null) { + stopReceiveThread(thread); + } executor.shutdown(); dependencies.getSignalWebSocket().disconnect(); - if (closeAccount && account != null) { + synchronized (closedListeners) { + closedListeners.forEach(Runnable::run); + closedListeners.clear(); + } + + if (account != null) { account.close(); } account = null;