X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/e532a24cf8cba6aede416b1b21aa72e95519c383..32150b1aaa32888c5179d664a9a497a13d3f4bfa:/lib/src/main/java/org/asamk/signal/manager/Manager.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index ac7b571f..87b89913 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,6 +16,7 @@ */ package org.asamk.signal.manager; +import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -26,27 +27,26 @@ import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; -import org.asamk.signal.manager.groups.GroupUtils; 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.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; +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.jobs.Job; -import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; -import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.CachedMessage; @@ -55,21 +55,14 @@ import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; 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.IOUtils; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; -import org.signal.libsignal.metadata.ProtocolInvalidMessageException; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; -import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; @@ -79,33 +72,16 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; -import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; -import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; +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; @@ -113,25 +89,18 @@ import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableExcept 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.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; import java.io.Closeable; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.security.SignatureException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; @@ -165,20 +134,13 @@ public class Manager implements Closeable { private final ProfileHelper profileHelper; private final PinHelper pinHelper; 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 AvatarStore avatarStore; - private final AttachmentStore attachmentStore; - private final StickerPackStore stickerPackStore; - private final SignalSessionLock sessionLock = new SignalSessionLock() { - private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); - - @Override - public Lock acquire() { - LEGACY_LOCK.lock(); - return LEGACY_LOCK::unlock; - } - }; + private final Context context; Manager( SignalAccount account, @@ -193,6 +155,15 @@ public class Manager implements Closeable { account.getUsername(), account.getPassword(), account.getDeviceId()); + final var sessionLock = new SignalSessionLock() { + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } + }; this.dependencies = new SignalDependencies(account.getSelfAddress(), serviceEnvironmentConfig, userAgent, @@ -200,10 +171,11 @@ public class Manager implements Closeable { account.getSignalProtocolStore(), executor, sessionLock); - this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); + final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, @@ -227,27 +199,51 @@ public class Manager implements Closeable { dependencies, unidentifiedAccessHelper, this::resolveSignalServiceAddress, - this::resolveRecipient, + account.getRecipientStore(), this::handleIdentityFailure, this::getGroup, this::refreshRegisteredUser); this.groupHelper = new GroupHelper(account, dependencies, + attachmentHelper, sendHelper, groupV2Helper, avatarStore, this::resolveSignalServiceAddress, - this::resolveRecipient); + account.getRecipientStore()); + this.contactHelper = new ContactHelper(account); + this.syncHelper = new SyncHelper(account, + attachmentHelper, + sendHelper, + groupHelper, + avatarStore, + this::resolveSignalServiceAddress); + + this.context = new Context(account, + dependencies.getAccountManager(), + dependencies.getMessageReceiver(), + stickerPackStore, + sendHelper, + groupHelper, + syncHelper, + profileHelper); + var jobExecutor = new JobExecutor(context); + + this.incomingMessageHandler = new IncomingMessageHandler(account, + dependencies, + account.getRecipientStore(), + this::resolveSignalServiceAddress, + groupHelper, + contactHelper, + attachmentHelper, + syncHelper, + jobExecutor); } public String getUsername() { return account.getUsername(); } - private SignalServiceAddress getSelfAddress() { - return account.getSelfAddress(); - } - public RecipientId getSelfRecipientId() { return account.getSelfRecipientId(); } @@ -331,21 +327,21 @@ public class Manager implements Closeable { public Map> areUsersRegistered(Set numbers) throws IOException { Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { try { - return canonicalizePhoneNumber(n); + return PhoneNumberFormatter.formatNumber(n, account.getUsername()); } catch (InvalidNumberException e) { return ""; } })); - // Note "contactDetails" has no optionals. It only gives us info on users who are registered - var contactDetails = getRegisteredUsers(canonicalizedNumbers.values() + // Note "registeredUsers" has no optionals. It only gives us info on users who are registered + var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values() .stream() .filter(s -> !s.isEmpty()) .collect(Collectors.toSet())); return numbers.stream().collect(Collectors.toMap(n -> n, n -> { final var number = canonicalizedNumbers.get(n); - final var uuid = contactDetails.get(number); + final var uuid = registeredUsers.get(number); return new Pair<>(number.isEmpty() ? null : number, uuid); })); } @@ -376,12 +372,7 @@ public class Manager implements Closeable { String givenName, final String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException { profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); - - sendSyncFetchProfileMessage(); - } - - private void sendSyncFetchProfileMessage() throws IOException { - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); + syncHelper.sendSyncFetchProfileMessage(); } public void unregister() throws IOException { @@ -491,19 +482,6 @@ public class Manager implements Closeable { return profileHelper.getRecipientProfile(recipientId); } - public void refreshRecipientProfile(RecipientId recipientId) { - profileHelper.refreshRecipientProfile(recipientId); - } - - private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { - final var streamDetails = avatarStore.retrieveContactAvatar(address); - if (streamDetails == null) { - return Optional.absent(); - } - - return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); - } - public List getGroups() { return account.getGroupStore().getGroups(); } @@ -511,19 +489,18 @@ public class Manager implements Closeable { public SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - final var newAdmins = getRecipientIds(groupAdmins); + final var newAdmins = resolveRecipients(groupAdmins); return groupHelper.quitGroup(groupId, newAdmins); } public void deleteGroup(GroupId groupId) throws IOException { - account.getGroupStore().deleteGroup(groupId); - avatarStore.deleteGroupAvatar(groupId); + groupHelper.deleteGroup(groupId); } public Pair createGroup( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { - return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); + return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); } public SendGroupMessageResults updateGroup( @@ -545,10 +522,10 @@ public class Manager implements Closeable { return groupHelper.updateGroup(groupId, name, description, - members == null ? null : getRecipientIds(members), - removeMembers == null ? null : getRecipientIds(removeMembers), - admins == null ? null : getRecipientIds(admins), - removeAdmins == null ? null : getRecipientIds(removeAdmins), + members == null ? null : resolveRecipients(members), + removeMembers == null ? null : resolveRecipients(removeMembers), + admins == null ? null : resolveRecipients(admins), + removeAdmins == null ? null : resolveRecipients(removeAdmins), resetGroupLink, groupLinkState, addMemberPermission, @@ -604,20 +581,6 @@ public class Manager implements Closeable { } } - SendGroupMessageResults sendGroupInfoMessage( - GroupIdV1 groupId, SignalServiceAddress recipient - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { - final var recipientId = resolveRecipient(recipient); - return groupHelper.sendGroupInfoMessage(groupId, recipientId); - } - - SendGroupMessageResults sendGroupInfoRequest( - GroupIdV1 groupId, SignalServiceAddress recipient - ) throws IOException { - final var recipientId = resolveRecipient(recipient); - return groupHelper.sendGroupInfoRequest(groupId, recipientId); - } - public void sendReadReceipt( RecipientIdentifier.Single sender, List messageIds ) throws IOException, UntrustedIdentityException { @@ -638,16 +601,6 @@ public class Manager implements Closeable { sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } - void sendDeliveryReceipt( - SignalServiceAddress remoteAddress, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, - messageIds, - System.currentTimeMillis()); - - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); - } - public SendMessageResults sendMessage( Message message, Set recipients ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { @@ -660,21 +613,9 @@ public class Manager implements Closeable { final SignalServiceDataMessage.Builder messageBuilder, final Message message ) throws AttachmentInvalidException, IOException { messageBuilder.withBody(message.getMessageText()); - if (message.getAttachments() != null) { - var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(message.getAttachments()); - - // Upload attachments here, so we only upload once even for multiple recipients - var messageSender = dependencies.getMessageSender(); - var attachmentPointers = new ArrayList(attachmentStreams.size()); - for (var attachment : attachmentStreams) { - if (attachment.isStream()) { - attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream())); - } else if (attachment.isPointer()) { - attachmentPointers.add(attachment.asPointer()); - } - } - - messageBuilder.withAttachments(attachmentPointers); + final var attachments = message.getAttachments(); + if (attachments != null) { + messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); } } @@ -712,56 +653,38 @@ public class Manager implements Closeable { throw new AssertionError(e); } finally { for (var recipient : recipients) { - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - handleEndSession(recipientId); + final var recipientId = resolveRecipient(recipient); + account.getSessionStore().deleteAllSessions(recipientId); } } } - void renewSession(RecipientId recipientId) throws IOException { - account.getSessionStore().archiveSessions(recipientId); - if (!recipientId.equals(getSelfRecipientId())) { - sendHelper.sendNullMessage(recipientId); - } - } - public void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException { + ) throws NotMasterDeviceException, UnregisteredUserException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - final var recipientId = resolveRecipient(recipient); - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore().storeContact(recipientId, builder.withName(name).build()); + contactHelper.setContactName(resolveRecipient(recipient), name); } public void setContactBlocked( RecipientIdentifier.Single recipient, boolean blocked - ) throws NotMasterDeviceException { + ) throws NotMasterDeviceException, IOException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - setContactBlocked(resolveRecipient(recipient), blocked); - } - - private void setContactBlocked(RecipientId recipientId, boolean blocked) { - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); // TODO cycle our profile key - account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); + syncHelper.sendBlockedList(); } - public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { - var group = getGroup(groupId); - if (group == null) { - throw new GroupNotFoundException(groupId); - } - - group.setBlocked(blocked); + public void setGroupBlocked( + final GroupId groupId, final boolean blocked + ) throws GroupNotFoundException, IOException { + groupHelper.setGroupBlocked(groupId, blocked); // TODO cycle our profile key - account.getGroupStore().updateGroup(group); + syncHelper.sendBlockedList(); } /** @@ -771,7 +694,7 @@ public class Manager implements Closeable { RecipientIdentifier.Single recipient, int messageExpirationTimer ) throws IOException { var recipientId = resolveRecipient(recipient); - setExpirationTimer(recipientId, messageExpirationTimer); + contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { sendMessage(messageBuilder, Set.of(recipient)); @@ -780,16 +703,6 @@ public class Manager implements Closeable { } } - private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { - var contact = account.getContactStore().getContact(recipientId); - if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { - return; - } - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore() - .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); - } - /** * Upload the sticker pack from path. * @@ -822,51 +735,7 @@ public class Manager implements Closeable { } public void requestAllSyncData() throws IOException { - requestSyncGroups(); - requestSyncContacts(); - requestSyncBlocked(); - requestSyncConfiguration(); - requestSyncKeys(); - } - - private void requestSyncGroups() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncContacts() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncBlocked() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncConfiguration() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncKeys() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); + syncHelper.requestAllSyncData(); } private byte[] getSenderCertificate() { @@ -885,53 +754,28 @@ public class Manager implements Closeable { return certificate; } - private Set getRecipientIds(Collection recipients) { - final var signalServiceAddresses = new HashSet(recipients.size()); - final var addressesMissingUuid = new HashSet(); - - for (var number : recipients) { - final var resolvedAddress = resolveSignalServiceAddress(resolveRecipient(number)); - if (resolvedAddress.getUuid().isPresent()) { - signalServiceAddresses.add(resolvedAddress); - } else { - addressesMissingUuid.add(resolvedAddress); - } - } - - final var numbersMissingUuid = addressesMissingUuid.stream() - .map(a -> a.getNumber().get()) - .collect(Collectors.toSet()); - Map registeredUsers; - try { - registeredUsers = getRegisteredUsers(numbersMissingUuid); - } catch (IOException e) { - logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage()); - registeredUsers = Map.of(); - } - - for (var address : addressesMissingUuid) { - final var number = address.getNumber().get(); - if (registeredUsers.containsKey(number)) { - final var newAddress = resolveSignalServiceAddress(resolveRecipientTrusted(new SignalServiceAddress( - registeredUsers.get(number), - number))); - signalServiceAddresses.add(newAddress); - } else { - signalServiceAddresses.add(address); - } - } - - return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet()); - } - 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 uuidMap = getRegisteredUsers(Set.of(number)); - return resolveRecipientTrusted(new SignalServiceAddress(uuidMap.getOrDefault(number, null), number)); + final var uuid = getRegisteredUser(number); + return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); + } + + private UUID getRegisteredUser(final String number) throws IOException { + final Map uuidMap; + try { + uuidMap = getRegisteredUsers(Set.of(number)); + } catch (NumberFormatException e) { + throw new UnregisteredUserException(number, e); + } + final var uuid = uuidMap.get(number); + if (uuid == null) { + throw new UnregisteredUserException(number, null); + } + return uuid; } private Map getRegisteredUsers(final Set numbers) throws IOException { @@ -957,162 +801,6 @@ public class Manager implements Closeable { sendTypingMessage(action.toSignalService(), recipients); } - private void handleEndSession(RecipientId recipientId) { - account.getSessionStore().deleteAllSessions(recipientId); - } - - private List handleSignalServiceDataMessage( - SignalServiceDataMessage message, - boolean isSync, - SignalServiceAddress source, - SignalServiceAddress destination, - boolean ignoreAttachments - ) { - var actions = new ArrayList(); - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - var groupId = GroupId.v1(groupInfo.getGroupId()); - var group = getGroup(groupId); - if (group == null || group instanceof GroupInfoV1) { - var groupV1 = (GroupInfoV1) group; - switch (groupInfo.getType()) { - case UPDATE: { - if (groupV1 == null) { - groupV1 = new GroupInfoV1(groupId); - } - - if (groupInfo.getAvatar().isPresent()) { - var avatar = groupInfo.getAvatar().get(); - downloadGroupAvatar(groupV1.getGroupId(), avatar); - } - - if (groupInfo.getName().isPresent()) { - groupV1.name = groupInfo.getName().get(); - } - - if (groupInfo.getMembers().isPresent()) { - groupV1.addMembers(groupInfo.getMembers() - .get() - .stream() - .map(this::resolveRecipient) - .collect(Collectors.toSet())); - } - - account.getGroupStore().updateGroup(groupV1); - break; - } - case DELIVER: - if (groupV1 == null && !isSync) { - actions.add(new SendGroupInfoRequestAction(source, groupId)); - } - break; - case QUIT: { - if (groupV1 != null) { - groupV1.removeMember(resolveRecipient(source)); - account.getGroupStore().updateGroup(groupV1); - } - break; - } - case REQUEST_INFO: - if (groupV1 != null && !isSync) { - actions.add(new SendGroupInfoAction(source, groupV1.getGroupId())); - } - break; - } - } else { - // Received a group v1 message for a v2 group - } - } - if (message.getGroupContext().get().getGroupV2().isPresent()) { - final var groupContext = message.getGroupContext().get().getGroupV2().get(); - final var groupMasterKey = groupContext.getMasterKey(); - - groupHelper.getOrMigrateGroup(groupMasterKey, - groupContext.getRevision(), - groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); - } - } - - final var conversationPartnerAddress = isSync ? destination : source; - if (conversationPartnerAddress != null && message.isEndSession()) { - handleEndSession(resolveRecipient(conversationPartnerAddress)); - } - if (message.isExpirationUpdate() || message.getBody().isPresent()) { - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); - if (group != null) { - if (group.messageExpirationTime != message.getExpiresInSeconds()) { - group.messageExpirationTime = message.getExpiresInSeconds(); - account.getGroupStore().updateGroup(group); - } - } - } else if (message.getGroupContext().get().getGroupV2().isPresent()) { - // disappearing message timer already stored in the DecryptedGroup - } - } else if (conversationPartnerAddress != null) { - setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds()); - } - } - if (!ignoreAttachments) { - if (message.getAttachments().isPresent()) { - for (var attachment : message.getAttachments().get()) { - downloadAttachment(attachment); - } - } - if (message.getSharedContacts().isPresent()) { - for (var contact : message.getSharedContacts().get()) { - if (contact.getAvatar().isPresent()) { - downloadAttachment(contact.getAvatar().get().getAttachment()); - } - } - } - } - if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { - final ProfileKey profileKey; - try { - profileKey = new ProfileKey(message.getProfileKey().get()); - } catch (InvalidInputException e) { - throw new AssertionError(e); - } - if (source.matches(account.getSelfAddress())) { - this.account.setProfileKey(profileKey); - } - this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); - } - if (message.getPreviews().isPresent()) { - final var previews = message.getPreviews().get(); - for (var preview : previews) { - if (preview.getImage().isPresent()) { - downloadAttachment(preview.getImage().get()); - } - } - } - if (message.getQuote().isPresent()) { - final var quote = message.getQuote().get(); - - for (var quotedAttachment : quote.getAttachments()) { - final var thumbnail = quotedAttachment.getThumbnail(); - if (thumbnail != null) { - downloadAttachment(thumbnail); - } - } - } - if (message.getSticker().isPresent()) { - final var messageSticker = message.getSticker().get(); - final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); - var sticker = account.getStickerStore().getSticker(stickerPackId); - if (sticker == null) { - sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); - account.getStickerStore().updateSticker(sticker); - } - enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); - } - return actions; - } - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { @@ -1129,32 +817,33 @@ public class Manager implements Closeable { ) { var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { + cachedMessage.delete(); return null; } - SignalServiceContent content = null; - List actions = null; - if (!envelope.isReceipt()) { - try { - content = dependencies.getCipher().decrypt(envelope); - } catch (ProtocolUntrustedIdentityException e) { - if (!envelope.hasSource()) { - final var identifier = e.getSender(); - final var recipientId = 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; - } catch (Exception er) { - // All other errors are not recoverable, so delete the cached message + + 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; } - actions = handleMessage(envelope, content, ignoreAttachments); + 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; } - handler.handleMessage(envelope, content, null); + + // If successful and for all other errors that are not recoverable, delete the cached message cachedMessage.delete(); return actions; } @@ -1177,15 +866,13 @@ public class Manager implements Closeable { while (!Thread.interrupted()) { SignalServiceEnvelope envelope; - SignalServiceContent content = null; - Exception exception = null; 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.hasSource() - ? resolveRecipient(envelope1.getSourceIdentifier()) + 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); @@ -1219,59 +906,18 @@ public class Manager implements Closeable { continue; } - if (envelope.hasSource()) { - // Store uuid if we don't have it already - // address/uuid in envelope is sent by server - resolveRecipientTrusted(envelope.getSourceAddress()); - } - if (!envelope.isReceipt()) { - try { - content = dependencies.getCipher().decrypt(envelope); - } catch (Exception e) { - exception = e; - } - if (!envelope.hasSource() && content != null) { - // Store uuid if we don't have it already - // address/uuid is validated by unidentified sender certificate - resolveRecipientTrusted(content.getSender()); - } - var actions = handleMessage(envelope, content, ignoreAttachments); - if (exception instanceof ProtocolInvalidMessageException) { - final var sender = resolveRecipient(((ProtocolInvalidMessageException) exception).getSender()); - logger.debug("Received invalid message, queuing renew session action."); - actions.add(new RenewSessionAction(sender)); - } - if (hasCaughtUpWithOldMessages) { - for (var action : actions) { - try { - action.execute(this); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Message action failed.", e); - } - } - } else { - queuedActions.addAll(actions); - } - } - final var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content); - if (isMessageBlocked(envelope, content)) { - logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); - } else if (notAllowedToSendToGroup) { - logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", - (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), - envelope.getTimestamp()); - } else { - handler.handleMessage(envelope, content, exception); + final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); + queuedActions.addAll(result.first()); + final var exception = result.second(); + + if (hasCaughtUpWithOldMessages) { + handleQueuedActions(queuedActions); } if (cachedMessage[0] != null) { - if (exception instanceof ProtocolUntrustedIdentityException) { - final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender(); - final var recipientId = resolveRecipient(identifier); - queuedActions.add(new RetrieveProfileAction(recipientId)); - if (!envelope.hasSource()) { + if (exception instanceof UntrustedIdentityException) { + 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) { @@ -1287,10 +933,10 @@ public class Manager implements Closeable { handleQueuedActions(queuedActions); } - private void handleQueuedActions(final Set queuedActions) { + private void handleQueuedActions(final Collection queuedActions) { for (var action : queuedActions) { try { - action.execute(this); + action.execute(context); } catch (Throwable e) { if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { Thread.currentThread().interrupt(); @@ -1300,574 +946,22 @@ public class Manager implements Closeable { } } - private boolean isMessageBlocked( - SignalServiceEnvelope envelope, SignalServiceContent content - ) { - SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - source = envelope.getSourceAddress(); - } else if (content != null) { - source = content.getSender(); - } else { - return false; - } - final var recipientId = resolveRecipient(source); - if (isContactBlocked(recipientId)) { - return true; - } - - if (content != null && content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); - if (message.getGroupContext().isPresent()) { - var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - var group = getGroup(groupId); - if (group != null && group.isBlocked()) { - return true; - } - } - } - return false; - } - public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { - final var recipientId = resolveRecipient(recipient); - return isContactBlocked(recipientId); - } - - private boolean isContactBlocked(final RecipientId recipientId) { - var sourceContact = account.getContactStore().getContact(recipientId); - return sourceContact != null && sourceContact.isBlocked(); - } - - private boolean isNotAllowedToSendToGroup( - SignalServiceEnvelope envelope, SignalServiceContent content - ) { - SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - source = envelope.getSourceAddress(); - } else if (content != null) { - source = content.getSender(); - } else { - return false; - } - - if (content == null || !content.getDataMessage().isPresent()) { - return false; - } - - var message = content.getDataMessage().get(); - if (!message.getGroupContext().isPresent()) { - return false; - } - - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { - return false; - } - } - - var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - var group = getGroup(groupId); - if (group == null) { - return false; - } - - final var recipientId = resolveRecipient(source); - if (!group.isMember(recipientId)) { - return true; - } - - if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { - return message.getBody().isPresent() - || message.getAttachments().isPresent() - || message.getQuote() - .isPresent() - || message.getPreviews().isPresent() - || message.getMentions().isPresent() - || message.getSticker().isPresent(); - } - return false; - } - - private List handleMessage( - SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments - ) { - var actions = new ArrayList(); - if (content != null) { - final SignalServiceAddress sender; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - sender = envelope.getSourceAddress(); - } else { - sender = content.getSender(); - } - - if (content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); - - if (content.isNeedsReceipt()) { - actions.add(new SendReceiptAction(sender, message.getTimestamp())); - } - - actions.addAll(handleSignalServiceDataMessage(message, - false, - sender, - account.getSelfAddress(), - ignoreAttachments)); - } - if (content.getSyncMessage().isPresent()) { - account.setMultiDevice(true); - var syncMessage = content.getSyncMessage().get(); - if (syncMessage.getSent().isPresent()) { - var message = syncMessage.getSent().get(); - final var destination = message.getDestination().orNull(); - actions.addAll(handleSignalServiceDataMessage(message.getMessage(), - true, - sender, - destination, - ignoreAttachments)); - } - if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { - var rm = syncMessage.getRequest().get(); - if (rm.isContactsRequest()) { - actions.add(SendSyncContactsAction.create()); - } - if (rm.isGroupsRequest()) { - actions.add(SendSyncGroupsAction.create()); - } - if (rm.isBlockedListRequest()) { - actions.add(SendSyncBlockedListAction.create()); - } - // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); - } - if (syncMessage.getGroups().isPresent()) { - File tmpFile = null; - try { - tmpFile = IOUtils.createTempFile(); - final var groupsMessage = syncMessage.getGroups().get(); - try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) { - var s = new DeviceGroupsInputStream(attachmentAsStream); - DeviceGroup g; - while (true) { - try { - g = s.read(); - } catch (IOException e) { - logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); - continue; - } - if (g == null) { - break; - } - var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); - if (syncGroup != null) { - if (g.getName().isPresent()) { - syncGroup.name = g.getName().get(); - } - syncGroup.addMembers(g.getMembers() - .stream() - .map(this::resolveRecipient) - .collect(Collectors.toSet())); - if (!g.isActive()) { - syncGroup.removeMember(account.getSelfRecipientId()); - } else { - // Add ourself to the member set as it's marked as active - syncGroup.addMembers(List.of(account.getSelfRecipientId())); - } - syncGroup.blocked = g.isBlocked(); - if (g.getColor().isPresent()) { - syncGroup.color = g.getColor().get(); - } - - if (g.getAvatar().isPresent()) { - downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); - } - syncGroup.archived = g.isArchived(); - account.getGroupStore().updateGroup(syncGroup); - } - } - } - } catch (Exception e) { - logger.warn("Failed to handle received sync groups “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } finally { - if (tmpFile != null) { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - } - if (syncMessage.getBlockedList().isPresent()) { - final var blockedListMessage = syncMessage.getBlockedList().get(); - for (var address : blockedListMessage.getAddresses()) { - setContactBlocked(resolveRecipient(address), true); - } - for (var groupId : blockedListMessage.getGroupIds() - .stream() - .map(GroupId::unknownVersion) - .collect(Collectors.toSet())) { - try { - setGroupBlocked(groupId, true); - } catch (GroupNotFoundException e) { - logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", - groupId.toBase64()); - } - } - } - if (syncMessage.getContacts().isPresent()) { - File tmpFile = null; - try { - tmpFile = IOUtils.createTempFile(); - final var contactsMessage = syncMessage.getContacts().get(); - try (var attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream() - .asPointer(), tmpFile)) { - var s = new DeviceContactsInputStream(attachmentAsStream); - DeviceContact c; - while (true) { - try { - c = s.read(); - } catch (IOException e) { - logger.warn("Sync contacts contained invalid contact, ignoring: {}", - e.getMessage()); - continue; - } - if (c == null) { - break; - } - if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { - account.setProfileKey(c.getProfileKey().get()); - } - final var recipientId = resolveRecipientTrusted(c.getAddress()); - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null - ? Contact.newBuilder() - : Contact.newBuilder(contact); - if (c.getName().isPresent()) { - builder.withName(c.getName().get()); - } - if (c.getColor().isPresent()) { - builder.withColor(c.getColor().get()); - } - if (c.getProfileKey().isPresent()) { - account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); - } - if (c.getVerified().isPresent()) { - final var verifiedMessage = c.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } - if (c.getExpirationTimer().isPresent()) { - builder.withMessageExpirationTime(c.getExpirationTimer().get()); - } - builder.withBlocked(c.isBlocked()); - builder.withArchived(c.isArchived()); - account.getContactStore().storeContact(recipientId, builder.build()); - - if (c.getAvatar().isPresent()) { - downloadContactAvatar(c.getAvatar().get(), c.getAddress()); - } - } - } - } catch (Exception e) { - logger.warn("Failed to handle received sync contacts “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } finally { - if (tmpFile != null) { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - } - if (syncMessage.getVerified().isPresent()) { - final var verifiedMessage = syncMessage.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } - if (syncMessage.getStickerPackOperations().isPresent()) { - final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); - for (var m : stickerPackOperationMessages) { - if (!m.getPackId().isPresent()) { - continue; - } - final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); - final var installed = !m.getType().isPresent() - || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; - - var sticker = account.getStickerStore().getSticker(stickerPackId); - if (m.getPackKey().isPresent()) { - if (sticker == null) { - sticker = new Sticker(stickerPackId, m.getPackKey().get()); - } - if (installed) { - enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); - } - } - - if (sticker != null) { - sticker.setInstalled(installed); - account.getStickerStore().updateSticker(sticker); - } - } - } - if (syncMessage.getFetchType().isPresent()) { - switch (syncMessage.getFetchType().get()) { - case LOCAL_PROFILE: - actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); - case STORAGE_MANIFEST: - // TODO - } - } - if (syncMessage.getKeys().isPresent()) { - final var keysMessage = syncMessage.getKeys().get(); - if (keysMessage.getStorageService().isPresent()) { - final var storageKey = keysMessage.getStorageService().get(); - account.setStorageKey(storageKey); - } - } - if (syncMessage.getConfiguration().isPresent()) { - // TODO - } - } - } - return actions; - } - - private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) { - try { - avatarStore.storeContactAvatar(address, outputStream -> retrieveAttachment(avatar, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage()); - } - } - - private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { + final RecipientId recipientId; try { - avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; } + return contactHelper.isContactBlocked(recipientId); } public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return attachmentStore.getAttachmentFile(attachmentId); - } - - private void downloadAttachment(final SignalServiceAttachment attachment) { - if (!attachment.isPointer()) { - logger.warn("Invalid state, can't store an attachment stream."); - } - - var pointer = attachment.asPointer(); - if (pointer.getPreview().isPresent()) { - final var preview = pointer.getPreview().get(); - try { - attachmentStore.storeAttachmentPreview(pointer.getRemoteId(), - outputStream -> outputStream.write(preview, 0, preview.length)); - } catch (IOException e) { - logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage()); - } - } - - try { - attachmentStore.storeAttachment(pointer.getRemoteId(), - outputStream -> retrieveAttachmentPointer(pointer, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage()); - } - } - - private void retrieveAttachment( - final SignalServiceAttachment attachment, final OutputStream outputStream - ) throws IOException { - if (attachment.isPointer()) { - var pointer = attachment.asPointer(); - retrieveAttachmentPointer(pointer, outputStream); - } else { - var stream = attachment.asStream(); - IOUtils.copyStream(stream.getInputStream(), outputStream); - } - } - - private void retrieveAttachmentPointer( - SignalServiceAttachmentPointer pointer, OutputStream outputStream - ) throws IOException { - var tmpFile = IOUtils.createTempFile(); - try (var input = retrieveAttachmentAsStream(pointer, tmpFile)) { - IOUtils.copyStream(input, outputStream); - } catch (MissingConfigurationException | InvalidMessageException e) { - throw new IOException(e); - } finally { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - - private InputStream retrieveAttachmentAsStream( - SignalServiceAttachmentPointer pointer, File tmpFile - ) throws IOException, InvalidMessageException, MissingConfigurationException { - return dependencies.getMessageReceiver() - .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); - } - - void sendGroups() throws IOException { - var groupsFile = IOUtils.createTempFile(); - - try { - try (OutputStream fos = new FileOutputStream(groupsFile)) { - var out = new DeviceGroupsOutputStream(fos); - for (var record : getGroups()) { - if (record instanceof GroupInfoV1) { - var groupInfo = (GroupInfoV1) record; - out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), - Optional.fromNullable(groupInfo.name), - groupInfo.getMembers() - .stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()), - groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), - groupInfo.isMember(account.getSelfRecipientId()), - Optional.of(groupInfo.messageExpirationTime), - Optional.fromNullable(groupInfo.color), - groupInfo.blocked, - Optional.absent(), - groupInfo.archived)); - } - } - } - - if (groupsFile.exists() && groupsFile.length() > 0) { - try (var groupsFileStream = new FileInputStream(groupsFile)) { - var attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(groupsFileStream) - .withContentType("application/octet-stream") - .withLength(groupsFile.length()) - .build(); - - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); - } - } - } finally { - try { - Files.delete(groupsFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage()); - } - } + return attachmentHelper.getAttachmentFile(attachmentId); } public void sendContacts() throws IOException { - var contactsFile = IOUtils.createTempFile(); - - try { - try (OutputStream fos = new FileOutputStream(contactsFile)) { - var out = new DeviceContactsOutputStream(fos); - for (var contactPair : account.getContactStore().getContacts()) { - final var recipientId = contactPair.first(); - final var contact = contactPair.second(); - final var address = resolveSignalServiceAddress(recipientId); - - var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId); - VerifiedMessage verifiedMessage = null; - if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(address, - currentIdentity.getIdentityKey(), - currentIdentity.getTrustLevel().toVerifiedState(), - currentIdentity.getDateAdded().getTime()); - } - - var profileKey = account.getProfileStore().getProfileKey(recipientId); - out.write(new DeviceContact(address, - Optional.fromNullable(contact.getName()), - createContactAvatarAttachment(address), - Optional.fromNullable(contact.getColor()), - Optional.fromNullable(verifiedMessage), - Optional.fromNullable(profileKey), - contact.isBlocked(), - Optional.of(contact.getMessageExpirationTime()), - Optional.absent(), - contact.isArchived())); - } - - if (account.getProfileKey() != null) { - // Send our own profile key as well - out.write(new DeviceContact(account.getSelfAddress(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.of(account.getProfileKey()), - false, - Optional.absent(), - Optional.absent(), - false)); - } - } - - if (contactsFile.exists() && contactsFile.length() > 0) { - try (var contactsFileStream = new FileInputStream(contactsFile)) { - var attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(contactsFileStream) - .withContentType("application/octet-stream") - .withLength(contactsFile.length()) - .build(); - - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, - true))); - } - } - } finally { - try { - Files.delete(contactsFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage()); - } - } - } - - void sendBlockedList() throws IOException { - var addresses = new ArrayList(); - for (var record : account.getContactStore().getContacts()) { - if (record.second().isBlocked()) { - addresses.add(resolveSignalServiceAddress(record.first())); - } - } - var groupIds = new ArrayList(); - for (var record : getGroups()) { - if (record.isBlocked()) { - groupIds.add(record.getGroupId().serialize()); - } - } - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); - } - - private void sendVerifiedMessage( - SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel - ) throws IOException { - var verifiedMessage = new VerifiedMessage(destination, - identityKey, - trustLevel.toVerifiedState(), - System.currentTimeMillis()); - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + syncHelper.sendContacts(); } public List> getContacts() { @@ -1875,9 +969,14 @@ public class Manager implements Closeable { } public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { - final var recipientId = resolveRecipient(recipientIdentifier); + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipientIdentifier); + } catch (UnregisteredUserException e) { + return null; + } - final var contact = account.getRecipientStore().getContact(recipientId); + final var contact = account.getContactStore().getContact(recipientId); if (contact != null && !Util.isEmpty(contact.getName())) { return contact.getName(); } @@ -1899,7 +998,12 @@ public class Manager implements Closeable { } public List getIdentities(RecipientIdentifier.Single recipient) { - final var identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + IdentityInfo identity; + try { + identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + } catch (UnregisteredUserException e) { + identity = null; + } return identity == null ? List.of() : List.of(identity); } @@ -1910,7 +1014,12 @@ public class Manager implements Closeable { * @param fingerprint Fingerprint */ public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { - var recipientId = resolveRecipient(recipient); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } return trustIdentity(recipientId, identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), TrustLevel.TRUSTED_VERIFIED); @@ -1923,8 +1032,13 @@ public class Manager implements Closeable { * @param safetyNumber Safety number */ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { - var recipientId = resolveRecipient(recipient); - var address = account.getRecipientStore().resolveServiceAddress(recipientId); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), TrustLevel.TRUSTED_VERIFIED); @@ -1937,8 +1051,13 @@ public class Manager implements Closeable { * @param safetyNumber Scannable safety number */ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { - var recipientId = resolveRecipient(recipient); - var address = account.getRecipientStore().resolveServiceAddress(recipientId); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> { final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); try { @@ -1955,7 +1074,12 @@ public class Manager implements Closeable { * @param recipient username of the identity */ public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { - var recipientId = resolveRecipient(recipient); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); } @@ -1973,8 +1097,8 @@ public class Manager implements Closeable { account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); try { - var address = account.getRecipientStore().resolveServiceAddress(recipientId); - sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); + var address = resolveSignalServiceAddress(recipientId); + syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); } catch (IOException e) { logger.warn("Failed to send verification sync message: {}", e.getMessage()); } @@ -1993,7 +1117,7 @@ public class Manager implements Closeable { } } else { // Retrieve profile to get the current identity key from the server - refreshRecipientProfile(recipientId); + profileHelper.refreshRecipientProfile(recipientId); } } @@ -2017,48 +1141,61 @@ public class Manager implements Closeable { theirIdentityKey); } - @Deprecated - public SignalServiceAddress resolveSignalServiceAddress(String identifier) { - var address = Utils.getSignalServiceAddressFromIdentifier(identifier); - - return resolveSignalServiceAddress(address); - } - - @Deprecated public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { if (address.matches(account.getSelfAddress())) { return account.getSelfAddress(); } - return account.getRecipientStore().resolveServiceAddress(address); + return resolveSignalServiceAddress(resolveRecipient(address)); } - public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { - return account.getRecipientStore().resolveServiceAddress(recipientId); + public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); } - private String canonicalizePhoneNumber(final String number) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, account.getUsername()); - } + public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); + if (address.getUuid().isPresent()) { + return address.toSignalServiceAddress(); + } - private RecipientId resolveRecipient(final String identifier) { - var address = Utils.getSignalServiceAddressFromIdentifier(identifier); + // 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(); + try { + return resolveSignalServiceAddress(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 resolveRecipient(address); + private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { + final var recipientIds = new HashSet(recipients.size()); + for (var number : recipients) { + final var recipientId = resolveRecipient(number); + recipientIds.add(recipientId); + } + return recipientIds; } - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) { - final SignalServiceAddress address; + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { if (recipient instanceof RecipientIdentifier.Uuid) { - address = new SignalServiceAddress(((RecipientIdentifier.Uuid) recipient).uuid, null); + return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); } else { - address = new SignalServiceAddress(null, ((RecipientIdentifier.Number) recipient).number); + final var number = ((RecipientIdentifier.Number) recipient).number; + return account.getRecipientStore().resolveRecipient(number, () -> { + try { + return getRegisteredUser(number); + } catch (IOException e) { + return null; + } + }); } - - return resolveRecipient(address); } - public RecipientId resolveRecipient(SignalServiceAddress address) { + private RecipientId resolveRecipient(SignalServiceAddress address) { return account.getRecipientStore().resolveRecipient(address); } @@ -2066,20 +1203,12 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveRecipientTrusted(address); } - private void enqueueJob(Job job) { - var context = new Context(account, - dependencies.getAccountManager(), - dependencies.getMessageReceiver(), - stickerPackStore); - job.run(context); - } - @Override public void close() throws IOException { close(true); } - void close(boolean closeAccount) throws IOException { + private void close(boolean closeAccount) throws IOException { executor.shutdown(); dependencies.getSignalWebSocket().disconnect();