From: AsamK Date: Sat, 21 Aug 2021 13:02:14 +0000 (+0200) Subject: Refactor message sending X-Git-Tag: v0.9.0~65 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/893b7f7f9daf698d5424f5f9b1a14c383457b431?ds=sidebyside Refactor message sending --- 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 a2152fac..c4b69665 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -33,6 +33,7 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupV2Helper; 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.UnidentifiedAccessHelper; import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.jobs.Job; @@ -86,7 +87,6 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.InvalidMessageStructureException; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; @@ -111,7 +111,6 @@ 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.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; @@ -120,7 +119,6 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.ConflictException; 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; @@ -179,10 +177,11 @@ public class Manager implements Closeable { private final ExecutorService executor = Executors.newCachedThreadPool(); - private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final ProfileHelper profileHelper; private final GroupV2Helper groupV2Helper; private final PinHelper pinHelper; + private final SendHelper sendHelper; + private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; private final StickerPackStore stickerPackStore; @@ -218,7 +217,7 @@ public class Manager implements Closeable { sessionLock); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); - this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, + final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, this::getRecipientProfile, this::getSenderCertificate); @@ -237,6 +236,14 @@ public class Manager implements Closeable { this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + this.sendHelper = new SendHelper(account, + dependencies, + unidentifiedAccessHelper, + this::resolveSignalServiceAddress, + this::resolveRecipient, + this::handleIdentityFailure, + this::getGroup, + this::refreshRegisteredUser); } public String getUsername() { @@ -396,10 +403,7 @@ public class Manager implements Closeable { } account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); - try { - sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); - } catch (UntrustedIdentityException ignored) { - } + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); } public void unregister() throws IOException { @@ -653,17 +657,6 @@ public class Manager implements Closeable { return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); } - private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - var g = getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } - if (!g.isMember(account.getSelfRecipientId())) { - throw new NotAGroupMemberException(groupId, g.getTitle()); - } - return g; - } - private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { var g = getGroup(groupId); if (g == null) { @@ -682,12 +675,12 @@ public class Manager implements Closeable { public Pair> sendGroupMessage( String messageText, List attachments, GroupId groupId ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); } - return sendGroupMessage(messageBuilder, groupId); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } public Pair> sendGroupMessageReaction( @@ -698,20 +691,9 @@ public class Manager implements Closeable { remove, resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); + final var messageBuilder = createMessageBuilder().withReaction(reaction); - return sendGroupMessage(messageBuilder, groupId); - } - - public Pair> sendGroupMessage( - SignalServiceDataMessage.Builder messageBuilder, GroupId groupId - ) throws IOException, GroupNotFoundException, NotAGroupMemberException { - final var g = getGroupForSending(groupId); - - GroupUtils.setGroupContext(messageBuilder, g); - messageBuilder.withExpiration(g.getMessageExpirationTime()); - - return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } public Pair> sendQuitGroupMessage( @@ -722,7 +704,7 @@ public class Manager implements Closeable { return quitGroupV1((GroupInfoV1) group); } - final var newAdmins = getSignalServiceAddresses(groupAdmins); + final var newAdmins = getRecipientIds(groupAdmins); try { return quitGroupV2((GroupInfoV2) group, newAdmins); } catch (ConflictException e) { @@ -737,10 +719,11 @@ public class Manager implements Closeable { .withId(groupInfoV1.getGroupId().serialize()) .build(); - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); + var messageBuilder = createMessageBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfRecipientId()); account.getGroupStore().updateGroup(groupInfoV1); - return sendMessage(messageBuilder, groupInfoV1.getMembersWithout(account.getSelfRecipientId())); + return sendHelper.sendGroupMessage(messageBuilder.build(), + groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } private Pair> quitGroupV2( @@ -760,7 +743,8 @@ public class Manager implements Closeable { groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); account.getGroupStore().updateGroup(groupInfoV2); - return sendMessage(messageBuilder, groupInfoV2.getMembersWithout(account.getSelfRecipientId())); + return sendHelper.sendGroupMessage(messageBuilder.build(), + groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } public void deleteGroup(GroupId groupId) throws IOException { @@ -771,7 +755,7 @@ public class Manager implements Closeable { public Pair> createGroup( String name, List members, File avatarFile ) throws IOException, AttachmentInvalidException, InvalidNumberException { - return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile); + return createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); } private Pair> createGroup( @@ -807,7 +791,8 @@ public class Manager implements Closeable { messageBuilder = getGroupUpdateMessageBuilder(gv2, null); account.getGroupStore().updateGroup(gv2); - final var result = sendMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + final var result = sendHelper.sendGroupMessage(messageBuilder.build(), + gv2.getMembersIncludingPendingWithout(selfRecipientId)); return new Pair<>(gv2.getGroupId(), result.second()); } @@ -829,10 +814,10 @@ public class Manager implements Closeable { return updateGroup(groupId, name, description, - members == null ? null : getSignalServiceAddresses(members), - removeMembers == null ? null : getSignalServiceAddresses(removeMembers), - admins == null ? null : getSignalServiceAddresses(admins), - removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins), + members == null ? null : getRecipientIds(members), + removeMembers == null ? null : getRecipientIds(removeMembers), + admins == null ? null : getRecipientIds(admins), + removeAdmins == null ? null : getRecipientIds(removeAdmins), resetGroupLink, groupLinkState, addMemberPermission, @@ -908,7 +893,8 @@ public class Manager implements Closeable { account.getGroupStore().updateGroup(gv1); - return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + return sendHelper.sendGroupMessage(messageBuilder.build(), + gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } private void updateGroupV1Details( @@ -1092,7 +1078,7 @@ public class Manager implements Closeable { final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); account.getGroupStore().updateGroup(group); - return sendMessage(messageBuilder, members); + return sendHelper.sendGroupMessage(messageBuilder.build(), members); } private static int currentTimeDays() { @@ -1122,9 +1108,9 @@ public class Manager implements Closeable { GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; - var group = getGroupForSending(groupId); + var group = getGroupForUpdating(groupId); if (!(group instanceof GroupInfoV1)) { - throw new RuntimeException("Received an invalid group request for a v2 group!"); + throw new IOException("Received an invalid group request for a v2 group!"); } g = (GroupInfoV1) group; @@ -1136,7 +1122,7 @@ public class Manager implements Closeable { var messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendMessage(messageBuilder, Set.of(recipientId)); + return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(recipientId)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { @@ -1157,18 +1143,14 @@ public class Manager implements Closeable { throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); } - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) .withRevision(g.getGroup().getRevision()) .withSignedGroupChange(signedGroupChange); - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); } Pair> sendGroupInfoRequest( @@ -1176,10 +1158,10 @@ public class Manager implements Closeable { ) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); + var messageBuilder = createMessageBuilder().asGroupMessage(group.build()); // Send group info request message to the recipient who sent us a message with this groupId - return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient))); + return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(resolveRecipient(recipient))); } void sendReceipt( @@ -1189,16 +1171,13 @@ public class Manager implements Closeable { List.of(messageId), System.currentTimeMillis()); - dependencies.getMessageSender() - .sendReceipt(remoteAddress, - unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)), - receiptMessage); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); } public Pair> sendMessage( String messageText, List attachments, List recipients ) throws IOException, AttachmentInvalidException, InvalidNumberException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); @@ -1215,33 +1194,33 @@ public class Manager implements Closeable { messageBuilder.withAttachments(attachmentPointers); } - return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } public Pair sendSelfMessage( String messageText, List attachments ) throws IOException, AttachmentInvalidException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); } - return sendSelfMessage(messageBuilder); + return sendHelper.sendSelfMessage(messageBuilder); } public Pair> sendRemoteDeleteMessage( long targetSentTimestamp, List recipients ) throws IOException, InvalidNumberException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); - return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); + final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } public Pair> sendGroupRemoteDeleteMessage( long targetSentTimestamp, GroupId groupId ) throws IOException, NotAGroupMemberException, GroupNotFoundException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); - return sendGroupMessage(messageBuilder, groupId); + final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } public Pair> sendMessageReaction( @@ -1252,28 +1231,27 @@ public class Manager implements Closeable { remove, resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); - return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); + final var messageBuilder = createMessageBuilder().withReaction(reaction); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } public Pair> sendEndSessionMessage(List recipients) throws IOException, InvalidNumberException { - var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); + var messageBuilder = createMessageBuilder().asEndSessionMessage(); - final var signalServiceAddresses = getSignalServiceAddresses(recipients); + final var recipientIds = getRecipientIds(recipients); try { - return sendMessage(messageBuilder, signalServiceAddresses); - } catch (Exception e) { - for (var address : signalServiceAddresses) { - handleEndSession(address); + return sendHelper.sendMessage(messageBuilder, recipientIds); + } finally { + for (var recipientId : recipientIds) { + handleEndSession(recipientId); } - throw e; } } void renewSession(RecipientId recipientId) throws IOException { account.getSessionStore().archiveSessions(recipientId); if (!recipientId.equals(getSelfRecipientId())) { - sendNullMessage(recipientId); + sendHelper.sendNullMessage(recipientId); } } @@ -1323,8 +1301,8 @@ public class Manager implements Closeable { } private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - sendMessage(messageBuilder, Set.of(recipientId)); + final var messageBuilder = createMessageBuilder().asExpirationUpdate(); + sendHelper.sendMessage(messageBuilder, Set.of(recipientId)); } /** @@ -1350,8 +1328,8 @@ public class Manager implements Closeable { } private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - sendGroupMessage(messageBuilder, groupId); + final var messageBuilder = createMessageBuilder().asExpirationUpdate(); + sendHelper.sendAsGroupMessage(messageBuilder, groupId); } /** @@ -1398,11 +1376,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private void requestSyncContacts() throws IOException { @@ -1410,11 +1384,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private void requestSyncBlocked() throws IOException { @@ -1422,11 +1392,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private void requestSyncConfiguration() throws IOException { @@ -1434,11 +1400,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private void requestSyncKeys() throws IOException { @@ -1446,11 +1408,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private byte[] getSenderCertificate() { @@ -1469,12 +1427,7 @@ public class Manager implements Closeable { return certificate; } - private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { - var messageSender = dependencies.getMessageSender(); - messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync()); - } - - private Set getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { + private Set getRecipientIds(Collection numbers) throws InvalidNumberException { final var signalServiceAddresses = new HashSet(numbers.size()); final var addressesMissingUuid = new HashSet(); @@ -1537,188 +1490,27 @@ public class Manager implements Closeable { public void sendTypingMessage( TypingAction action, Set recipients ) throws IOException, UntrustedIdentityException, InvalidNumberException { - sendTypingMessageInternal(action, getSignalServiceAddresses(recipients)); - } - - private void sendTypingMessageInternal( - TypingAction action, Set recipientIds - ) throws IOException, UntrustedIdentityException { final var timestamp = System.currentTimeMillis(); var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent()); - var messageSender = dependencies.getMessageSender(); - for (var recipientId : recipientIds) { - final var address = resolveSignalServiceAddress(recipientId); - messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); - } + sendHelper.sendTypingMessage(message, getRecipientIds(recipients)); } public void sendGroupTypingMessage( TypingAction action, GroupId groupId ) throws IOException, NotAGroupMemberException, GroupNotFoundException { final var timestamp = System.currentTimeMillis(); - final var g = getGroupForSending(groupId); final var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.of(groupId.serialize())); - final var messageSender = dependencies.getMessageSender(); - final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId())); - final var addresses = recipientIdList.stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()); - messageSender.sendTyping(addresses, unidentifiedAccessHelper.getAccessFor(recipientIdList), message, null); + sendHelper.sendGroupTypingMessage(message, groupId); } - private Pair> sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Set recipientIds - ) throws IOException { + private SignalServiceDataMessage.Builder createMessageBuilder() { final var timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - - SignalServiceDataMessage message = null; - try { - message = messageBuilder.build(); - if (message.getGroupContext().isPresent()) { - try { - var messageSender = dependencies.getMessageSender(); - final var isRecipientUpdate = false; - final var recipientIdList = new ArrayList<>(recipientIds); - final var addresses = recipientIdList.stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()); - var result = messageSender.sendDataMessage(addresses, - unidentifiedAccessHelper.getAccessFor(recipientIdList), - isRecipientUpdate, - ContentHint.DEFAULT, - message, - sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), - () -> false); - - for (var r : result) { - handlePossibleIdentityFailure(r); - } - - return new Pair<>(timestamp, result); - } catch (UntrustedIdentityException e) { - return new Pair<>(timestamp, List.of()); - } - } else { - // Send to all individually, so sync messages are sent correctly - messageBuilder.withProfileKey(account.getProfileKey().serialize()); - var results = new ArrayList(recipientIds.size()); - for (var recipientId : recipientIds) { - final var contact = account.getContactStore().getContact(recipientId); - final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; - messageBuilder.withExpiration(expirationTime); - message = messageBuilder.build(); - final var result = sendMessage(recipientId, message); - handlePossibleIdentityFailure(result); - results.add(result); - } - return new Pair<>(timestamp, results); - } - } finally { - if (message != null && message.isEndSession()) { - for (var recipient : recipientIds) { - handleEndSession(recipient); - } - } - } - } - - private void handlePossibleIdentityFailure(final SendMessageResult r) { - if (r.getIdentityFailure() != null) { - final var recipientId = resolveRecipient(r.getAddress()); - final var identityKey = r.getIdentityFailure().getIdentityKey(); - if (identityKey != null) { - final var newIdentity = account.getIdentityKeyStore() - .saveIdentity(recipientId, identityKey, new Date()); - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } else { - // Retrieve profile to get the current identity key from the server - retrieveEncryptedProfile(recipientId); - } - } - } - private Pair sendSelfMessage( - SignalServiceDataMessage.Builder messageBuilder - ) throws IOException { - final var timestamp = System.currentTimeMillis(); + var messageBuilder = SignalServiceDataMessage.newBuilder(); messageBuilder.withTimestamp(timestamp); - final var recipientId = account.getSelfRecipientId(); - - final var contact = account.getContactStore().getContact(recipientId); - final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; - messageBuilder.withExpiration(expirationTime); - - var message = messageBuilder.build(); - final var result = sendSelfMessage(message); - return new Pair<>(timestamp, result); - } - - private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { - var messageSender = dependencies.getMessageSender(); - - var recipientId = account.getSelfRecipientId(); - - final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipientId); - var recipient = resolveSignalServiceAddress(recipientId); - var transcript = new SentTranscriptMessage(Optional.of(recipient), - message.getTimestamp(), - message, - message.getExpiresInSeconds(), - Map.of(recipient, unidentifiedAccess.isPresent()), - false); - var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); - - try { - return messageSender.sendSyncMessage(syncMessage, unidentifiedAccess); - } catch (UntrustedIdentityException e) { - return SendMessageResult.identityFailure(recipient, e.getIdentityKey()); - } - } - - private SendMessageResult sendMessage( - RecipientId recipientId, SignalServiceDataMessage message - ) throws IOException { - var messageSender = dependencies.getMessageSender(); - - final var address = resolveSignalServiceAddress(recipientId); - try { - try { - return messageSender.sendDataMessage(address, - unidentifiedAccessHelper.getAccessFor(recipientId), - ContentHint.DEFAULT, - message); - } catch (UnregisteredUserException e) { - final var newRecipientId = refreshRegisteredUser(recipientId); - return messageSender.sendDataMessage(resolveSignalServiceAddress(newRecipientId), - unidentifiedAccessHelper.getAccessFor(newRecipientId), - ContentHint.DEFAULT, - message); - } - } catch (UntrustedIdentityException e) { - return SendMessageResult.identityFailure(address, e.getIdentityKey()); - } - } - - private SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { - var messageSender = dependencies.getMessageSender(); - - final var address = resolveSignalServiceAddress(recipientId); - try { - try { - return messageSender.sendNullMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId)); - } catch (UnregisteredUserException e) { - final var newRecipientId = refreshRegisteredUser(recipientId); - final var newAddress = resolveSignalServiceAddress(newRecipientId); - return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId)); - } - } catch (UntrustedIdentityException e) { - return SendMessageResult.identityFailure(address, e.getIdentityKey()); - } + return messageBuilder; } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { @@ -2611,7 +2403,7 @@ public class Manager implements Closeable { .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } - void sendGroups() throws IOException, UntrustedIdentityException { + void sendGroups() throws IOException { var groupsFile = IOUtils.createTempFile(); try { @@ -2645,7 +2437,7 @@ public class Manager implements Closeable { .withLength(groupsFile.length()) .build(); - sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); } } } finally { @@ -2657,7 +2449,7 @@ public class Manager implements Closeable { } } - public void sendContacts() throws IOException, UntrustedIdentityException { + public void sendContacts() throws IOException { var contactsFile = IOUtils.createTempFile(); try { @@ -2713,7 +2505,8 @@ public class Manager implements Closeable { .withLength(contactsFile.length()) .build(); - sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, true))); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, + true))); } } } finally { @@ -2725,7 +2518,7 @@ public class Manager implements Closeable { } } - void sendBlockedList() throws IOException, UntrustedIdentityException { + void sendBlockedList() throws IOException { var addresses = new ArrayList(); for (var record : account.getContactStore().getContacts()) { if (record.second().isBlocked()) { @@ -2738,17 +2531,17 @@ public class Manager implements Closeable { groupIds.add(record.getGroupId().serialize()); } } - sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); } private void sendVerifiedMessage( SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel - ) throws IOException, UntrustedIdentityException { + ) throws IOException { var verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis()); - sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } public List> getContacts() { @@ -2849,13 +2642,28 @@ public class Manager implements Closeable { try { var address = account.getRecipientStore().resolveServiceAddress(recipientId); sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); - } catch (IOException | UntrustedIdentityException e) { + } catch (IOException e) { logger.warn("Failed to send verification sync message: {}", e.getMessage()); } return true; } + private void handleIdentityFailure( + final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure + ) { + final var identityKey = identityFailure.getIdentityKey(); + if (identityKey != null) { + final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } else { + // Retrieve profile to get the current identity key from the server + retrieveEncryptedProfile(recipientId); + } + } + public String computeSafetyNumber( SignalServiceAddress theirAddress, IdentityKey theirIdentityKey ) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupProvider.java new file mode 100644 index 00000000..25b86786 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupProvider.java @@ -0,0 +1,9 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.groups.GroupInfo; + +public interface GroupProvider { + + GroupInfo getGroup(GroupId groupId); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityFailureHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityFailureHandler.java new file mode 100644 index 00000000..1cd31409 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityFailureHandler.java @@ -0,0 +1,9 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.whispersystems.signalservice.api.messages.SendMessageResult; + +public interface IdentityFailureHandler { + + void handleIdentityFailure(RecipientId recipientId, SendMessageResult.IdentityFailure identityFailure); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientRegistrationRefresher.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientRegistrationRefresher.java new file mode 100644 index 00000000..836d18d7 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientRegistrationRefresher.java @@ -0,0 +1,10 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.recipients.RecipientId; + +import java.io.IOException; + +public interface RecipientRegistrationRefresher { + + RecipientId refreshRecipientRegistration(RecipientId recipientId) throws IOException; +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java new file mode 100644 index 00000000..bc8800cf --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -0,0 +1,277 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.crypto.ContentHint; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class SendHelper { + + private final static Logger logger = LoggerFactory.getLogger(SendHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final UnidentifiedAccessHelper unidentifiedAccessHelper; + private final SignalServiceAddressResolver addressResolver; + private final RecipientResolver recipientResolver; + private final IdentityFailureHandler identityFailureHandler; + private final GroupProvider groupProvider; + private final RecipientRegistrationRefresher recipientRegistrationRefresher; + + public SendHelper( + final SignalAccount account, + final SignalDependencies dependencies, + final UnidentifiedAccessHelper unidentifiedAccessHelper, + final SignalServiceAddressResolver addressResolver, + final RecipientResolver recipientResolver, + final IdentityFailureHandler identityFailureHandler, + final GroupProvider groupProvider, + final RecipientRegistrationRefresher recipientRegistrationRefresher + ) { + this.account = account; + this.dependencies = dependencies; + this.unidentifiedAccessHelper = unidentifiedAccessHelper; + this.addressResolver = addressResolver; + this.recipientResolver = recipientResolver; + this.identityFailureHandler = identityFailureHandler; + this.groupProvider = groupProvider; + this.recipientRegistrationRefresher = recipientRegistrationRefresher; + } + + /** + * Send a single message to one or multiple recipients. + * The message is extended with the current expiration timer for each recipient. + */ + public Pair> sendMessage( + SignalServiceDataMessage.Builder messageBuilder, Set recipientIds + ) throws IOException { + // Send to all individually, so sync messages are sent correctly + messageBuilder.withProfileKey(account.getProfileKey().serialize()); + var results = new ArrayList(recipientIds.size()); + long timestamp = 0; + for (var recipientId : recipientIds) { + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + messageBuilder.withExpiration(expirationTime); + + final var singleMessage = messageBuilder.build(); + timestamp = singleMessage.getTimestamp(); + final var result = sendMessage(singleMessage, recipientId); + handlePossibleIdentityFailure(result); + + results.add(result); + } + return new Pair<>(timestamp, results); + } + + /** + * Send a group message to the given group + * The message is extended with the current expiration timer for the group and the group context. + */ + public Pair> sendAsGroupMessage( + SignalServiceDataMessage.Builder messageBuilder, GroupId groupId + ) throws IOException, GroupNotFoundException, NotAGroupMemberException { + final var g = getGroupForSending(groupId); + GroupUtils.setGroupContext(messageBuilder, g); + messageBuilder.withExpiration(g.getMessageExpirationTime()); + + final var recipients = g.getMembersWithout(account.getSelfRecipientId()); + return sendGroupMessage(messageBuilder.build(), recipients); + } + + /** + * Send a complete group message to the given recipients (should be current/old/new members) + * This method should only be used for create/update/quit group messages. + */ + public Pair> sendGroupMessage( + final SignalServiceDataMessage message, final Set recipientIds + ) throws IOException { + List result = sendGroupMessageInternal(message, recipientIds); + + for (var r : result) { + handlePossibleIdentityFailure(r); + } + + return new Pair<>(message.getTimestamp(), result); + } + + public void sendReceiptMessage( + final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId + ) throws IOException, UntrustedIdentityException { + final var messageSender = dependencies.getMessageSender(); + messageSender.sendReceipt(addressResolver.resolveSignalServiceAddress(recipientId), + unidentifiedAccessHelper.getAccessFor(recipientId), + receiptMessage); + } + + public SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { + var messageSender = dependencies.getMessageSender(); + + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + try { + return messageSender.sendNullMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId)); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); + return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId)); + } + } catch (UntrustedIdentityException e) { + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + + public Pair sendSelfMessage( + SignalServiceDataMessage.Builder messageBuilder + ) throws IOException { + final var recipientId = account.getSelfRecipientId(); + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + messageBuilder.withExpiration(expirationTime); + + var message = messageBuilder.build(); + final var result = sendSelfMessage(message); + return new Pair<>(message.getTimestamp(), result); + } + + public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) throws IOException { + var messageSender = dependencies.getMessageSender(); + try { + return messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync()); + } catch (UntrustedIdentityException e) { + var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId()); + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + + public void sendTypingMessage( + SignalServiceTypingMessage message, Set recipientIds + ) throws IOException, UntrustedIdentityException { + var messageSender = dependencies.getMessageSender(); + for (var recipientId : recipientIds) { + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); + messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); + } + } + } + + public void sendGroupTypingMessage( + SignalServiceTypingMessage message, GroupId groupId + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var g = getGroupForSending(groupId); + final var messageSender = dependencies.getMessageSender(); + final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId())); + final var addresses = recipientIdList.stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList()); + messageSender.sendTyping(addresses, unidentifiedAccessHelper.getAccessFor(recipientIdList), message, null); + } + + private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { + var g = groupProvider.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + if (!g.isMember(account.getSelfRecipientId())) { + throw new NotAGroupMemberException(groupId, g.getTitle()); + } + return g; + } + + private List sendGroupMessageInternal( + final SignalServiceDataMessage message, final Set recipientIds + ) throws IOException { + try { + var messageSender = dependencies.getMessageSender(); + // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false. + final var isRecipientUpdate = false; + final var recipientIdList = new ArrayList<>(recipientIds); + final var addresses = recipientIdList.stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList()); + return messageSender.sendDataMessage(addresses, + unidentifiedAccessHelper.getAccessFor(recipientIdList), + isRecipientUpdate, + ContentHint.DEFAULT, + message, + sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), + () -> false); + } catch (UntrustedIdentityException e) { + return List.of(); + } + } + + private SendMessageResult sendMessage( + SignalServiceDataMessage message, RecipientId recipientId + ) throws IOException { + var messageSender = dependencies.getMessageSender(); + + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + try { + return messageSender.sendDataMessage(address, + unidentifiedAccessHelper.getAccessFor(recipientId), + ContentHint.DEFAULT, + message); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId), + unidentifiedAccessHelper.getAccessFor(newRecipientId), + ContentHint.DEFAULT, + message); + } + } catch (UntrustedIdentityException e) { + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + + private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { + var address = account.getSelfAddress(); + var transcript = new SentTranscriptMessage(Optional.of(address), + message.getTimestamp(), + message, + message.getExpiresInSeconds(), + Map.of(address, true), + false); + var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); + + return sendSyncMessage(syncMessage); + } + + private void handlePossibleIdentityFailure(final SendMessageResult r) { + if (r.getIdentityFailure() != null) { + final var recipientId = recipientResolver.resolveRecipient(r.getAddress()); + identityFailureHandler.handleIdentityFailure(recipientId, r.getIdentityFailure()); + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java index 0fcae322..07dca322 100644 --- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java @@ -6,9 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; -import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.manager.Manager; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import java.io.IOException; @@ -30,8 +28,6 @@ public class SendContactsCommand implements JsonRpcLocalCommand { ) throws CommandException { try { m.sendContacts(); - } catch (UntrustedIdentityException e) { - throw new UntrustedKeyErrorException("SendContacts error: " + e.getMessage()); } catch (IOException e) { throw new IOErrorException("SendContacts error: " + e.getMessage()); }