From 0caeb3a1091c0bbcec700890f18c58b509c7249d Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 6 Dec 2019 11:35:10 +1100 Subject: [PATCH] Handle session restoration. --- .../conversation/ConversationActivity.java | 5 +- .../securesms/jobs/PushDecryptJob.java | 50 +++++++++++++---- .../securesms/loki/DebouncerCache.kt | 18 ++++++ .../securesms/loki/FriendRequestHandler.kt | 9 +++ .../loki/PushBackgroundMessageSendJob.kt | 56 +++++++++++++------ .../securesms/sms/MessageSender.java | 4 ++ 6 files changed, 110 insertions(+), 32 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/DebouncerCache.kt diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 46bb52d7b2..28ce95d447 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -3086,8 +3086,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void updateItemButtonPressed(@NonNull MessageRecord messageRecord) { // Loki - User clicked restore session - if (messageRecord.isNoRemoteSession() && !messageRecord.isLokiSessionRestoreSent()) { - // TODO: Send a message with `SESSION_RESTORE` flag + Recipient recipient = messageRecord.getRecipient(); + if (!recipient.isGroupRecipient() && messageRecord.isNoRemoteSession() && !messageRecord.isLokiSessionRestoreSent()) { + MessageSender.sendRestoreSessionMessage(this, recipient.getAddress().serialize()); DatabaseFactory.getSmsDatabase(this).markAsLokiSessionRestoreSent(messageRecord.id); TextSecurePreferences.setShowingSessionRestorePrompt(this, messageRecord.getIndividualRecipient().getAddress().serialize(), false); } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 1270fe6909..74a3ed7fce 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -74,6 +74,7 @@ import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase; import org.thoughtcrime.securesms.loki.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.MultiDeviceUtilities; +import org.thoughtcrime.securesms.loki.DebouncerCache; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; @@ -94,6 +95,7 @@ import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; +import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.IdentityUtil; @@ -337,6 +339,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType { MultiDeviceUtilities.checkForRevocation(context); } } else { + // Loki - We shouldn't process session restore message any further + if (message.isSessionRestore()) { return; } if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); else if (message.isExpirationUpdate()) @@ -1187,7 +1191,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private void storePreKeyBundleIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) { - if (content.lokiServiceMessage.isPresent()) { + Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false); + if (!sender.isGroupRecipient() && content.lokiServiceMessage.isPresent()) { LokiServiceMessage lokiMessage = content.lokiServiceMessage.get(); if (lokiMessage.getPreKeyBundleMessage() != null) { int registrationID = TextSecurePreferences.getLocalRegistrationId(context); @@ -1203,10 +1208,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { // If we got a friend request and we were friends with this user then we need to reset our session if (envelope.isFriendRequest()) { - Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false); long threadID = threadDatabase.getThreadIdIfExistsFor(sender); if (lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) { resetSession(content.getSender(), threadID); + // Let our other devices know that we have reset session + MessageSender.syncContact(context, sender.getAddress()); } } } @@ -1398,23 +1404,43 @@ public class PushDecryptJob extends BaseJob implements InjectableType { private void handleNoSessionMessage(@NonNull String sender, int senderDevice, long timestamp, @NonNull Optional smsMessageId) { - SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); + Recipient recipient = Recipient.from(context, Address.fromSerialized(sender), false); + if (recipient.isGroupRecipient()) { return; } - if (!smsMessageId.isPresent()) { - if (!TextSecurePreferences.isShowingSessionRestorePrompt(context, sender)) { - Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); + long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient); + LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID); + /* + If we are friends with the user or we sent a friend request to them and we got a message back with no session then we want to try and restore the session automatically. + otherwise if we're not friends or our friend request expired then we need to prompt the user for action + */ + if (friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) { + autoRestoreSession(sender); + } else if (friendRequestStatus == LokiThreadFriendRequestStatus.NONE || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) { + SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); - if (insertResult.isPresent()) { - smsDatabase.markAsNoSession(insertResult.get().getMessageId()); - TextSecurePreferences.setShowingSessionRestorePrompt(context, sender, true); - //MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + if (!smsMessageId.isPresent()) { + if (!TextSecurePreferences.isShowingSessionRestorePrompt(context, sender)) { + Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); + + if (insertResult.isPresent()) { + smsDatabase.markAsNoSession(insertResult.get().getMessageId()); + TextSecurePreferences.setShowingSessionRestorePrompt(context, sender, true); + //MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + } } + } else { + smsDatabase.markAsNoSession(smsMessageId.get()); } - } else { - smsDatabase.markAsNoSession(smsMessageId.get()); } } + private void autoRestoreSession(@NonNull String sender) { + // We don't want to keep spamming the user for an auto restore + String key = "restore_session_" + sender; + Debouncer debouncer = DebouncerCache.getDebouncer(key, 10000); + debouncer.publish(() -> MessageSender.sendRestoreSessionMessage(context, sender)); + } + private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp, @NonNull Optional smsMessageId) { diff --git a/src/org/thoughtcrime/securesms/loki/DebouncerCache.kt b/src/org/thoughtcrime/securesms/loki/DebouncerCache.kt new file mode 100644 index 0000000000..d98231f65c --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/DebouncerCache.kt @@ -0,0 +1,18 @@ +package org.thoughtcrime.securesms.loki + +import org.thoughtcrime.securesms.util.Debouncer + +object DebouncerCache { + private val cache: HashMap = hashMapOf() + @JvmStatic + fun getDebouncer(key: String, threshold: Long): Debouncer { + val throttler = cache[key] ?: Debouncer(threshold) + cache[key] = throttler + return throttler + } + + @JvmStatic + fun remove(key: String) { + cache.remove(key) + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt index 0ecffedb55..b660ec67a0 100644 --- a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt +++ b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt @@ -27,6 +27,15 @@ object FriendRequestHandler { ActionType.Sent -> LokiThreadFriendRequestStatus.REQUEST_SENT } DatabaseFactory.getLokiThreadDatabase(context).setFriendRequestStatus(threadId, threadFriendStatus) + // If we sent a friend request then we need to hide the session restore prompt + if (type == ActionType.Sent) { + val smsDatabase = DatabaseFactory.getSmsDatabase(context) + smsDatabase.getMessages(threadId) + .filter { it.isNoRemoteSession && !it.isLokiSessionRestoreSent } + .forEach { + smsDatabase.markAsLokiSessionRestoreSent(it.id) + } + } } // Update message status diff --git a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt index d1a81005b8..73ac0a9144 100644 --- a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt @@ -13,30 +13,43 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.internal.util.JsonUtil import java.io.IOException +import java.lang.IllegalStateException import java.util.concurrent.TimeUnit -data class BackgroundMessage private constructor(val recipient: String, val body: String?, val friendRequest: Boolean, val unpairingRequest: Boolean) { +data class BackgroundMessage private constructor(val data: Map) { companion object { @JvmStatic - fun create(recipient: String) = BackgroundMessage(recipient, null, false, false) + fun create(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient)) @JvmStatic - fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(recipient, messageBody, true, false) + fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(mapOf( + "recipient" to recipient, + "body" to messageBody, + "friendRequest" to true + )) @JvmStatic - fun createUnpairingRequest(recipient: String) = BackgroundMessage(recipient, null, false, true) + fun createUnpairingRequest(recipient: String) = BackgroundMessage(mapOf( + "recipient" to recipient, + "unpairingRequest" to true + )) + @JvmStatic + fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf( + "recipient" to recipient, + "friendRequest" to true, + "sessionRestore" to true + )) internal fun parse(serialized: String): BackgroundMessage { - val node = JsonUtil.fromJson(serialized) - val recipient = node.get("recipient").asText() - val body = if (node.hasNonNull("body")) node.get("body").asText() else null - val friendRequest = node.get("friendRequest").asBoolean(false) - val unpairingRequest = node.get("unpairingRequest").asBoolean(false) - return BackgroundMessage(recipient, body, friendRequest, unpairingRequest) + val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map ?: throw AssertionError("JSON parsing failed") + return BackgroundMessage(data) } } + fun get(key: String, defaultValue: T): T { + return data[key] as? T ?: defaultValue + } + fun serialize(): String { - val map = mapOf("recipient" to recipient, "body" to body, "friendRequest" to friendRequest, "unpairingRequest" to unpairingRequest) - return JsonUtil.toJson(map) + return JsonUtil.toJson(data) } } @@ -71,24 +84,31 @@ class PushBackgroundMessageSendJob private constructor( } public override fun onRun() { + val recipient = message.get("recipient", null) ?: throw IllegalStateException() val dataMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(System.currentTimeMillis()) - .withBody(message.body) + .withBody(message.get("body", null)) - if (message.friendRequest) { - val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(message.recipient) + if (message.get("friendRequest", false)) { + val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient) dataMessage.withPreKeyBundle(bundle) .asFriendRequest(true) - } else if (message.unpairingRequest) { + } + + if (message.get("unpairingRequest", false)) { dataMessage.asUnpairingRequest(true) } + if (message.get("sessionRestore", false)) { + dataMessage.asSessionRestore(true) + } + val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() - val address = SignalServiceAddress(message.recipient) + val address = SignalServiceAddress(recipient) try { messageSender.sendMessage(-1, address, Optional.absent(), dataMessage.build()) // The message ID doesn't matter } catch (e: Exception) { - Log.d("Loki", "Failed to send background message to: ${message.recipient}.") + Log.d("Loki", "Failed to send background message to: ${recipient}.") throw e } } diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index c60bb40231..9bb30f7dc6 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -139,6 +139,10 @@ public class MessageSender { public static void sendUnpairRequest(Context context, String contactHexEncodedPublicKey) { ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createUnpairingRequest(contactHexEncodedPublicKey))); } + + public static void sendRestoreSessionMessage(Context context, String contactHexEncodedPublicKey) { + ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRestore(contactHexEncodedPublicKey))); + } // endregion public static long send(final Context context,