2020-05-13 10:24:20 +10:00
|
|
|
package org.thoughtcrime.securesms.loki.protocol
|
|
|
|
|
|
|
|
import android.content.Context
|
2020-05-13 12:29:31 +10:00
|
|
|
import android.util.Log
|
2020-05-13 10:24:20 +10:00
|
|
|
import org.thoughtcrime.securesms.ApplicationContext
|
2020-05-13 11:15:17 +10:00
|
|
|
import org.thoughtcrime.securesms.database.Address
|
|
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
|
|
import org.thoughtcrime.securesms.jobs.PushMediaSendJob
|
|
|
|
import org.thoughtcrime.securesms.jobs.PushSendJob
|
|
|
|
import org.thoughtcrime.securesms.jobs.PushTextSendJob
|
2020-05-13 12:29:31 +10:00
|
|
|
import org.thoughtcrime.securesms.loki.utilities.Broadcaster
|
2020-05-13 10:24:20 +10:00
|
|
|
import org.thoughtcrime.securesms.recipients.Recipient
|
2020-05-13 12:29:31 +10:00
|
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
|
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
2020-05-13 11:15:17 +10:00
|
|
|
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
|
|
|
|
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
|
2020-05-13 12:29:31 +10:00
|
|
|
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink
|
|
|
|
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession
|
2020-05-13 11:15:17 +10:00
|
|
|
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
|
|
|
|
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
|
|
|
|
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
|
2020-05-13 10:24:20 +10:00
|
|
|
|
|
|
|
object MultiDeviceProtocol {
|
|
|
|
|
2020-05-13 12:29:31 +10:00
|
|
|
// TODO: Closed groups
|
2020-05-13 10:24:20 +10:00
|
|
|
|
2020-05-13 11:15:17 +10:00
|
|
|
enum class MessageType { Text, Media }
|
|
|
|
|
2020-05-13 10:24:20 +10:00
|
|
|
@JvmStatic
|
|
|
|
fun sendTextPush(context: Context, recipient: Recipient, messageID: Long) {
|
2020-05-13 11:16:04 +10:00
|
|
|
sendMessagePush(context, recipient, messageID, MessageType.Text)
|
2020-05-13 10:24:20 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
@JvmStatic
|
|
|
|
fun sendMediaPush(context: Context, recipient: Recipient, messageID: Long) {
|
2020-05-13 11:16:04 +10:00
|
|
|
sendMessagePush(context, recipient, messageID, MessageType.Media)
|
2020-05-13 10:24:20 +10:00
|
|
|
}
|
|
|
|
|
2020-05-13 11:15:17 +10:00
|
|
|
private fun sendMessagePushToDevice(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType): PushSendJob {
|
|
|
|
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
|
|
|
|
val threadFRStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID)
|
|
|
|
val isContactFriend = (threadFRStatus == LokiThreadFriendRequestStatus.FRIENDS)
|
|
|
|
val messageFRStatus = DatabaseFactory.getLokiMessageDatabase(context).getFriendRequestStatus(messageID)
|
|
|
|
val isFRMessage = (messageFRStatus != LokiMessageFriendRequestStatus.NONE)
|
|
|
|
val hasVisibleContent = when (messageType) {
|
|
|
|
MessageType.Text -> DatabaseFactory.getSmsDatabase(context).getMessage(messageID).body.isNotBlank()
|
|
|
|
MessageType.Media -> {
|
|
|
|
val outgoingMediaMessage = DatabaseFactory.getMmsDatabase(context).getOutgoingMessage(messageID)
|
|
|
|
outgoingMediaMessage.body.isNotBlank() || outgoingMediaMessage.attachments.isNotEmpty()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val shouldSendAutoGeneratedFR = !isContactFriend && !isFRMessage
|
|
|
|
&& !SessionMetaProtocol.shared.isNoteToSelf(recipient.address.serialize()) && !recipient.address.isGroup // Group threads work through session requests
|
|
|
|
&& hasVisibleContent
|
|
|
|
if (!shouldSendAutoGeneratedFR) {
|
|
|
|
when (messageType) {
|
|
|
|
MessageType.Text -> return PushTextSendJob(messageID, recipient.address)
|
|
|
|
MessageType.Media -> return PushMediaSendJob(messageID, recipient.address)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
val autoGeneratedFRMessage = "Please accept to enable messages to be synced across devices"
|
|
|
|
when (messageType) {
|
|
|
|
MessageType.Text -> return PushTextSendJob(messageID, messageID, recipient.address, true, autoGeneratedFRMessage)
|
|
|
|
MessageType.Media -> return PushMediaSendJob(messageID, messageID, recipient.address, true, autoGeneratedFRMessage)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun sendMessagePush(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType) {
|
|
|
|
val jobManager = ApplicationContext.getInstance(context).jobManager
|
|
|
|
val isMultiDeviceRequired = !recipient.address.isOpenGroup
|
|
|
|
if (!isMultiDeviceRequired) {
|
|
|
|
when (messageType) {
|
|
|
|
MessageType.Text -> jobManager.add(PushTextSendJob(messageID, recipient.address))
|
|
|
|
MessageType.Media -> PushMediaSendJob.enqueue(context, jobManager, messageID, recipient.address)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val publicKey = recipient.address.serialize()
|
|
|
|
LokiFileServerAPI.shared.getDeviceLinks(publicKey).success {
|
|
|
|
val devices = MultiDeviceProtocol.shared.getAllLinkedDevices(publicKey)
|
|
|
|
val jobs = devices.map { sendMessagePushToDevice(context, Recipient.from(context, Address.fromSerialized(it), false), messageID, messageType) }
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
when (messageType) {
|
|
|
|
MessageType.Text -> jobManager.startChain(jobs).enqueue()
|
|
|
|
MessageType.Media -> PushMediaSendJob.enqueue(context, jobManager, jobs as List<PushMediaSendJob>)
|
|
|
|
}
|
|
|
|
}.fail { exception ->
|
|
|
|
// Proceed even if updating the recipient's device links failed, so that message sending
|
|
|
|
// is independent of whether the file server is online
|
|
|
|
when (messageType) {
|
|
|
|
MessageType.Text -> jobManager.add(PushTextSendJob(messageID, recipient.address))
|
|
|
|
MessageType.Media -> PushMediaSendJob.enqueue(context, jobManager, messageID, recipient.address)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-05-13 12:29:31 +10:00
|
|
|
|
|
|
|
@JvmStatic
|
|
|
|
fun handleDeviceLinkMessageIfNeeded(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) {
|
|
|
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
|
|
|
if (deviceLink.type == DeviceLink.Type.REQUEST) {
|
|
|
|
handleDeviceLinkRequestMessage(context, deviceLink, content)
|
|
|
|
} else if (deviceLink.slaveHexEncodedPublicKey == userPublicKey) {
|
|
|
|
handleDeviceLinkAuthorizedMessage(context, deviceLink, content)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun isValidDeviceLinkMessage(context: Context, deviceLink: DeviceLink): Boolean {
|
|
|
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
|
|
|
val isRequest = (deviceLink.type == DeviceLink.Type.REQUEST)
|
|
|
|
if (deviceLink.requestSignature == null) {
|
|
|
|
Log.d("Loki", "Ignoring device link without a request signature.")
|
|
|
|
return false
|
|
|
|
} else if (isRequest && TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null) {
|
|
|
|
Log.d("Loki", "Ignoring unexpected device link message (the device is a slave device).")
|
|
|
|
return false
|
|
|
|
} else if (isRequest && deviceLink.masterHexEncodedPublicKey != userPublicKey) {
|
|
|
|
Log.d("Loki", "Ignoring device linking message addressed to another user.")
|
|
|
|
return false
|
|
|
|
} else if (isRequest && deviceLink.slaveHexEncodedPublicKey == userPublicKey) {
|
|
|
|
Log.d("Loki", "Ignoring device linking request message from self.")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return deviceLink.verify()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleDeviceLinkRequestMessage(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) {
|
|
|
|
val linkingSession = DeviceLinkingSession.shared
|
|
|
|
if (!linkingSession.isListeningForLinkingRequests) {
|
|
|
|
return Broadcaster(context).broadcast("unexpectedDeviceLinkRequestReceived")
|
|
|
|
}
|
|
|
|
val isValid = isValidDeviceLinkMessage(context, deviceLink)
|
|
|
|
if (!isValid) { return }
|
|
|
|
SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(context, content)
|
|
|
|
linkingSession.processLinkingRequest(deviceLink)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleDeviceLinkAuthorizedMessage(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) {
|
|
|
|
val linkingSession = DeviceLinkingSession.shared
|
|
|
|
if (!linkingSession.isListeningForLinkingRequests) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
val isValid = isValidDeviceLinkMessage(context, deviceLink)
|
|
|
|
if (!isValid) { return }
|
|
|
|
SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(context, content)
|
|
|
|
linkingSession.processLinkingAuthorization(deviceLink)
|
|
|
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
|
|
|
DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(userPublicKey)
|
|
|
|
DatabaseFactory.getLokiAPIDatabase(context).addDeviceLink(deviceLink)
|
|
|
|
TextSecurePreferences.setMasterHexEncodedPublicKey(context, deviceLink.masterHexEncodedPublicKey)
|
|
|
|
TextSecurePreferences.setMultiDevice(context, true)
|
|
|
|
LokiFileServerAPI.shared.addDeviceLink(deviceLink)
|
|
|
|
org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content)
|
|
|
|
org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.handleProfileKeyUpdateIfNeeded(context, content)
|
|
|
|
}
|
|
|
|
|
|
|
|
@JvmStatic
|
|
|
|
fun handleUnlinkingRequestIfNeeded(context: Context, content: SignalServiceContent) {
|
|
|
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
|
|
|
// Check that the request was sent by the user's master device
|
|
|
|
val masterDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) ?: return
|
|
|
|
val wasSentByMasterDevice = (content.sender == masterDevicePublicKey)
|
|
|
|
if (!wasSentByMasterDevice) { return }
|
|
|
|
// Ignore the request if we don't know about the device link in question
|
|
|
|
val masterDeviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getDeviceLinks(masterDevicePublicKey)
|
|
|
|
if (masterDeviceLinks.none {
|
|
|
|
it.masterHexEncodedPublicKey == masterDevicePublicKey && it.slaveHexEncodedPublicKey == userPublicKey
|
|
|
|
}) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
LokiFileServerAPI.shared.getDeviceLinks(userPublicKey, true).success { slaveDeviceLinks ->
|
|
|
|
// Check that the device link IS present on the file server.
|
|
|
|
// Note that the device link as seen from the master device's perspective has been deleted at this point, but the
|
|
|
|
// device link as seen from the slave perspective hasn't.
|
|
|
|
if (slaveDeviceLinks.any {
|
|
|
|
it.masterHexEncodedPublicKey == masterDevicePublicKey && it.slaveHexEncodedPublicKey == userPublicKey
|
|
|
|
}) {
|
|
|
|
for (slaveDeviceLink in slaveDeviceLinks) { // In theory there should only be one
|
|
|
|
LokiFileServerAPI.shared.removeDeviceLink(slaveDeviceLink) // Attempt to clean up on the file server
|
|
|
|
}
|
|
|
|
TextSecurePreferences.setWasUnlinked(context, true)
|
|
|
|
ApplicationContext.getInstance(context).clearData()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-05-13 10:24:20 +10:00
|
|
|
}
|