@file:JvmName("MultiDeviceUtilities") package org.thoughtcrime.securesms.loki import android.content.Context import android.os.Handler import nl.komponents.kovenant.* import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.logging.Log import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.libsignal.util.guava.Optional import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.loki.api.LokiStorageAPI import org.whispersystems.signalservice.loki.api.PairingAuthorisation import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.utilities.recover import org.whispersystems.signalservice.loki.utilities.retryIfNeeded fun getAllDeviceFriendRequestStatuses(context: Context, hexEncodedPublicKey: String): Promise, Exception> { val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context) return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys -> val map = mutableMapOf() for (devicePublicKey in keys) { val device = Recipient.from(context, Address.fromSerialized(devicePublicKey), false) val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(device) val friendRequestStatus = if (threadID < 0) LokiThreadFriendRequestStatus.NONE else lokiThreadDatabase.getFriendRequestStatus(threadID) map[devicePublicKey] = friendRequestStatus } map }.recover { mutableMapOf() } } fun getAllDevicePublicKeysWithFriendStatus(context: Context, hexEncodedPublicKey: String): Promise, Unit> { val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys -> val devices = keys.toMutableSet() if (hexEncodedPublicKey != userHexEncodedPublicKey) { devices.remove(userHexEncodedPublicKey) } val friends = getFriendPublicKeys(context, devices) val friendMap = mutableMapOf() for (device in devices) { friendMap[device] = friends.contains(device) } friendMap }.toFailVoid() } fun getFriendCount(context: Context, devices: Set): Int { return getFriendPublicKeys(context, devices).count() } fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Context): Promise { // If this public key is our primary device then we should become friends if (publicKey == TextSecurePreferences.getMasterHexEncodedPublicKey(context)) { return Promise.of(true) } return LokiStorageAPI.shared.getPrimaryDevicePublicKey(publicKey).bind { primaryDevicePublicKey -> // If the public key doesn't have any other devices then go through regular friend request logic if (primaryDevicePublicKey == null) { return@bind Promise.of(false) } // If the primary device public key matches our primary device then we should become friends since this is our other device if (primaryDevicePublicKey == TextSecurePreferences.getMasterHexEncodedPublicKey(context)) { return@bind Promise.of(true) } // If we are friends with any of the other devices then we should become friends isFriendsWithAnyLinkedDevice(context, Address.fromSerialized(primaryDevicePublicKey)) } } fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise { val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() val address = SignalServiceAddress(contactHexEncodedPublicKey) val message = SignalServiceDataMessage.newBuilder().withBody("").withPairingAuthorisation(authorisation) // A REQUEST should always act as a friend request. A GRANT should always be replying back as a normal message. if (authorisation.type == PairingAuthorisation.Type.REQUEST) { val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number) message.asFriendRequest(true).withPreKeyBundle(preKeyBundle) } return try { Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.") val result = messageSender.sendMessage(0, address, Optional.absent(), message.build()) if (result.success == null) { val exception = when { result.isNetworkFailure -> "Failed to send authorisation message due to a network error." else -> "Failed to send authorisation message." } throw Exception(exception) } Promise.ofSuccess(Unit) } catch (e: Exception) { Log.d("Loki", "Failed to send authorisation message to: $contactHexEncodedPublicKey.") Promise.ofFail(e) } } fun signAndSendPairingAuthorisationMessage(context: Context, pairingAuthorisation: PairingAuthorisation) { val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() val signedPairingAuthorisation = pairingAuthorisation.sign(PairingAuthorisation.Type.GRANT, userPrivateKey) if (signedPairingAuthorisation == null || signedPairingAuthorisation.type != PairingAuthorisation.Type.GRANT) { Log.d("Loki", "Failed to sign pairing authorization.") return } retryIfNeeded(8) { sendPairingAuthorisationMessage(context, pairingAuthorisation.secondaryDevicePublicKey, signedPairingAuthorisation) }.fail { Log.d("Loki", "Failed to send pairing authorization message to ${pairingAuthorisation.secondaryDevicePublicKey}.") } DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(signedPairingAuthorisation) // Call function after a short delay Handler().postDelayed({ LokiStorageAPI.shared.updateUserDeviceMappings().fail { Log.w("Loki", "Failed to update device mapping") } }, 100) } fun isOneOfOurDevices(context: Context, address: Address): Promise { if (address.isGroup || address.isEmail || address.isMmsGroup) { return Promise.of(false) } val ourPublicKey = TextSecurePreferences.getLocalNumber(context) return LokiStorageAPI.shared.getAllDevicePublicKeys(ourPublicKey).map { devices -> devices.contains(address.serialize()) } } fun isFriendsWithAnyLinkedDevice(context: Context, recipient: Recipient): Promise { return isFriendsWithAnyLinkedDevice(context, recipient.address) } fun isFriendsWithAnyLinkedDevice(context: Context, address: Address): Promise { if (!address.isPhone) { return Promise.of(true) } return getAllDeviceFriendRequestStatuses(context, address.serialize()).map { map -> for (status in map.values) { if (status == LokiThreadFriendRequestStatus.FRIENDS) { return@map true } } false } } fun hasPendingFriendRequestWithAnyLinkedDevice(context: Context, recipient: Recipient): Promise { if (recipient.isGroupRecipient) { return Promise.of(false) } return getAllDeviceFriendRequestStatuses(context, recipient.address.serialize()).map { map -> for (status in map.values) { if (status == LokiThreadFriendRequestStatus.REQUEST_SENDING || status == LokiThreadFriendRequestStatus.REQUEST_SENT || status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) { return@map true } } false } } fun shouldEnableUserInput(context: Context, recipient: Recipient): Promise { // Input should be enabled if we don't have any pending requests OR we're friends with any linked device return hasPendingFriendRequestWithAnyLinkedDevice(context, recipient).bind { hasPendingFriendRequest -> if (!hasPendingFriendRequest) Promise.of(true) else isFriendsWithAnyLinkedDevice(context, recipient) }.recover { true } }