diff --git a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index ddb4f47e86..3b26f01955 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -40,24 +40,48 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { private static final String TAG = PushTextSendJob.class.getSimpleName(); + private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id"; private static final String KEY_MESSAGE_ID = "message_id"; + private static final String KEY_DESTINATION = "destination"; + private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request"; + private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message"; @Inject SignalServiceMessageSender messageSender; - private long messageId; + private long messageId; // The message id + private long templateMessageId; // The message id of the message to template this send job from - public PushTextSendJob(long messageId, Address destination) { - this(constructParameters(destination), messageId); + // Loki - Multi-device + + private Address destination = null; // Destination to check whether this is another device we're sending to + private boolean isFriendRequest = false; // Whether this is a friend request message + private String customFriendRequestMessage = null; // If this isn't set then we use the message body + + public PushTextSendJob(long messageId, Address destination) { this(messageId, messageId, destination); } + public PushTextSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null); } + public PushTextSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) { + this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage); } - private PushTextSendJob(@NonNull Job.Parameters parameters, long messageId) { + private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) { super(parameters); + this.templateMessageId = templateMessageId; this.messageId = messageId; + this.destination = destination; + this.isFriendRequest = isFriendRequest; + this.customFriendRequestMessage = customFriendRequestMessage; } @Override public @NonNull Data serialize() { - return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId).build(); + Data.Builder builder = new Data.Builder() + .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) + .putLong(KEY_MESSAGE_ID, messageId) + .putString(KEY_DESTINATION, destination.serialize()) + .putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest); + + if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); } + return builder.build(); } @Override @@ -67,31 +91,38 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { @Override public void onAdded() { - DatabaseFactory.getSmsDatabase(context).markAsSending(messageId); + if (messageId >= 0) { + DatabaseFactory.getSmsDatabase(context).markAsSending(messageId); + } } @Override public void onPushSend() throws NoSuchMessageException, RetryLaterException { ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); SmsDatabase database = DatabaseFactory.getSmsDatabase(context); - SmsMessageRecord record = database.getMessage(messageId); + SmsMessageRecord record = database.getMessage(templateMessageId); - if (!record.isPending() && !record.isFailed()) { - warn(TAG, "Message " + messageId + " was already sent. Ignoring."); + Recipient recordRecipient = record.getRecipient().resolve(); + boolean hasSameDestination = destination.equals(recordRecipient.getAddress()); + + if (hasSameDestination && !record.isPending() && !record.isFailed()) { + warn(TAG, "Message " + templateMessageId + " was already sent. Ignoring."); return; } try { - log(TAG, "Sending message: " + messageId); + log(TAG, "Sending message: " + templateMessageId + (hasSameDestination ? "" : "to another device.")); - Recipient recipient = record.getRecipient().resolve(); + Recipient recipient = Recipient.from(context, destination, false); byte[] profileKey = recipient.getProfileKey(); UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode(); - boolean unidentified = deliver(record); + boolean unidentified = deliver(messageId, recipient, record); - database.markAsSent(messageId, true); - database.markUnidentified(messageId, unidentified); + if (messageId >= 0) { + database.markAsSent(messageId, true); + database.markUnidentified(messageId, unidentified); + } if (recipient.isLocalNumber()) { SyncMessageId id = new SyncMessageId(recipient.getAddress(), record.getDateSent()); @@ -112,12 +143,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { } } - if (record.getExpiresIn() > 0) { + if (record.getExpiresIn() > 0 && messageId >= 0) { database.markExpireStarted(messageId); expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn()); } - log(TAG, "Sent message: " + messageId); + log(TAG, "Sent message: " + templateMessageId + (hasSameDestination ? "" : "to another device.")); } catch (InsecureFallbackApprovalException e) { warn(TAG, "Failure", e); @@ -142,43 +173,45 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { @Override public void onCanceled() { - DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); + if (messageId >= 0) { + DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); - long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); - Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); + long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); + Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); - if (threadId != -1 && recipient != null) { - MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); + if (threadId != -1 && recipient != null) { + MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); + } } } - private boolean deliver(SmsMessageRecord message) + private boolean deliver(long messageId, Recipient recipient, SmsMessageRecord message) throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException { try { // rotateSenderCertificateIfNecessary(); - - SignalServiceAddress address = getPushAddress(message.getIndividualRecipient().getAddress()); - Optional profileKey = getProfileKey(message.getIndividualRecipient()); - Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, message.getIndividualRecipient()); + SignalServiceAddress address = getPushAddress(recipient.getAddress()); + Optional profileKey = getProfileKey(recipient); + Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent()); // Loki - Include a pre key bundle if the message is a friend request or an end session message PreKeyBundle preKeyBundle; - if (message.isFriendRequest() || message.isEndSession()) { + if (isFriendRequest|| message.isEndSession()) { preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber()); } else { preKeyBundle = null; } + String body = (isFriendRequest && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody(); SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getDateSent()) - .withBody(message.getBody()) + .withBody(body) .withExpiration((int)(message.getExpiresIn() / 1000)) .withProfileKey(profileKey.orNull()) .asEndSessionMessage(message.isEndSession()) - .asFriendRequest(message.isFriendRequest()) + .asFriendRequest(isFriendRequest) .withPreKeyBundle(preKeyBundle) .build(); @@ -203,7 +236,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { public static class Factory implements Job.Factory { @Override public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new PushTextSendJob(parameters, data.getLong(KEY_MESSAGE_ID)); + long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID); + long messageID = data.getLong(KEY_MESSAGE_ID); + Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION)); + boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST); + String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null; + return new PushTextSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage); } } } diff --git a/src/org/thoughtcrime/securesms/loki/Utilities.kt b/src/org/thoughtcrime/securesms/loki/Utilities.kt index dc32ecce4d..4735a74af3 100644 --- a/src/org/thoughtcrime/securesms/loki/Utilities.kt +++ b/src/org/thoughtcrime/securesms/loki/Utilities.kt @@ -17,10 +17,28 @@ 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.push.SignalServiceAddress +import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI import org.whispersystems.signalservice.loki.api.LokiPairingAuthorisation import org.whispersystems.signalservice.loki.api.LokiStorageAPI import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus +fun isGroupChat(pubKey: String): Boolean { + return (LokiGroupChatAPI.publicChatServer == pubKey) +} + +fun getFriends(context: Context, devices: Set): Set { + val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context) + + return devices.mapNotNull { device -> + val address = Address.fromSerialized(device) + val recipient = Recipient.from(context, address, false) + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient) + if (threadID < 0) { return@mapNotNull null } + + if (lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) device else null + }.toSet() +} + fun shouldAutomaticallyBecomeFriendsWithDevice(pubKey: String, context: Context): Promise { val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context) val storageAPI = LokiStorageAPI.shared ?: return Promise.ofSuccess(false) diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index a6349f2e63..3fe3c4a5a4 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.UtilitiesKt; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.push.AccountManagerFactory; @@ -52,9 +53,15 @@ import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.ContactTokenDetails; +import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import kotlin.Unit; public class MessageSender { @@ -203,8 +210,47 @@ public class MessageSender { } private static void sendTextPush(Context context, Recipient recipient, long messageId) { + LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared(); JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - jobManager.add(new PushTextSendJob(messageId, recipient.getAddress())); + + // Just send the message normally if the job manager failed or if it's a group message + String recipientPubKey = recipient.getAddress().serialize(); + if (storageAPI == null || UtilitiesKt.isGroupChat(recipientPubKey)) { + jobManager.add(new PushTextSendJob(messageId, recipient.getAddress())); + return; + } + + String ourPubKey = TextSecurePreferences.getLocalNumber(context); + + // Get all the devices and run our logic on them + storageAPI.getAllDevices(recipientPubKey).success(devices -> { + // Remove our self if we intended this message to go to another recipient + if (!recipientPubKey.equals(ourPubKey)) { devices.remove(ourPubKey); } + + Set friends = UtilitiesKt.getFriends(context, devices); + Set nonFriends = new HashSet<>(devices); + nonFriends.removeAll(friends); + + // Send a normal message to our friends + for (String friend : friends) { + // If this message had the same recipient then point it to the correct message id otherwise point it to a non-existing message + long messageIdToUse = recipientPubKey.equals(friend) ? messageId : -1L; + jobManager.add(new PushTextSendJob(messageId, messageIdToUse, Address.fromSerialized(friend))); + } + + // Send friend requests to non friends + for (String stranger : nonFriends) { + // If this message had the same recipient then point it to the correct message id otherwise point it to a non-existing message + long messageIdToUse = recipientPubKey.equals(stranger) ? messageId : -1L; + + // If we're friends with one of the devices then send out a default friend request message + boolean isFriendsWithAny = friends.size() > 0; + String defaultFriendRequestMessage = isFriendsWithAny ? "This is a friend request for devices linked to " + recipientPubKey : null; + jobManager.add(new PushTextSendJob(messageId, messageIdToUse, Address.fromSerialized(stranger), true, defaultFriendRequestMessage)); + } + + return Unit.INSTANCE; + }); } private static void sendMediaPush(Context context, Recipient recipient, long messageId) {