Deletion logic rework

Moving away from promises
pull/1518/head
ThomasSession 6 months ago
parent 28498c4ff2
commit 792a4ca74d

@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.ExpirationConfiguration
@ -371,26 +372,30 @@ class ConversationViewModel(
)
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
}
_dialogsState.update { it.copy(deleteEveryone = data) }
}
@ -418,26 +423,30 @@ class ConversationViewModel(
)
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
}
_dialogsState.update { it.copy(deleteEveryone = data) }
}
@ -470,25 +479,29 @@ class ConversationViewModel(
)
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(), data.messages.count()
),
Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(), data.messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
}
_dialogsState.update { it.copy(deleteAllDevices = data) }
}
@ -514,26 +527,30 @@ class ConversationViewModel(
)
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
}
_dialogsState.update { it.copy(deleteEveryone = data) }
}

@ -233,7 +233,7 @@ class DefaultConversationRepository @Inject constructor(
messages: Set<MessageRecord>
) {
val community = lokiThreadDb.getOpenGroupChat(threadId) ?:
throw Error("Not a Community")
throw Exception("Not a Community")
messages.forEach { message ->
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
@ -255,7 +255,7 @@ class DefaultConversationRepository @Inject constructor(
// delete from swarm
messageDataProvider.getServerHashForMessage(message.id, message.isMms)
?.let { serverHash ->
SnodeAPI.deleteMessage(publicKey, listOf(serverHash)).await()
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
}
// send an UnsendRequest to user's swarm

@ -1,6 +1,9 @@
package org.session.libsession.messaging.sending_receiving
import android.text.TextUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.R
import org.session.libsession.avatars.AvatarHelper
@ -256,12 +259,16 @@ fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? {
val author = message.author ?: return null
val (messageIdToDelete, mms) = storage.getMessageIdInDatabase(timestamp, author) ?: return null
messageDataProvider.getServerHashForMessage(messageIdToDelete, mms)?.let { serverHash ->
SnodeAPI.deleteMessage(author, listOf(serverHash))
GlobalScope.launch(Dispatchers.IO) { // using GlobalScope as we are slowly migrating to coroutines but we can't migrate everything at once
try {
SnodeAPI.deleteMessage(author, listOf(serverHash))
}catch (e: Exception){}
}
}
val deletedMessageId = messageDataProvider.markMessageAsDeleted(
timestamp = timestamp,
author = author,
displayedMessage = context.getString(R.string.deleteMessageDeletedLocally)
displayedMessage = context.getString(R.string.deleteMessageDeletedGlobally)
)
if (!messageDataProvider.isOutgoingMessage(timestamp)) {
SSKEnvironment.shared.notificationManager.updateNotification(context)

@ -9,6 +9,7 @@ import com.goterl.lazysodium.interfaces.SecretBox
import com.goterl.lazysodium.interfaces.Sign
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.coroutineScope
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.bind
@ -18,6 +19,7 @@ import nl.komponents.kovenant.unwrap
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.buildMutableMap
import org.session.libsession.utilities.mapValuesNotNull
import org.session.libsession.utilities.toByteArray
@ -35,6 +37,7 @@ import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.prettifiedDescription
import org.session.libsignal.utilities.retryIfNeeded
import org.session.libsignal.utilities.retryWithUniformInterval
import java.util.Locale
import kotlin.collections.component1
import kotlin.collections.component2
@ -557,50 +560,66 @@ object SnodeAPI {
}
}
fun deleteMessage(publicKey: String, serverHashes: List<String>): Promise<Map<String, Boolean>, Exception> =
retryIfNeeded(maxRetryCount) {
val userED25519KeyPair = getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
val userPublicKey = getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
getSingleTargetSnode(publicKey).bind { snode ->
retryIfNeeded(maxRetryCount) {
val verificationData = sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray()
val deleteMessageParams = buildMap {
this["pubkey"] = userPublicKey
this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString
this["messages"] = serverHashes
this["signature"] = signAndEncode(verificationData, userED25519KeyPair)
suspend fun deleteMessage(publicKey: String, serverHashes: List<String>) {
retryWithUniformInterval {
val userED25519KeyPair =
getUserED25519KeyPair() ?: throw (Error.NoKeyPair)
val userPublicKey =
getUserPublicKey() ?: throw (Error.NoKeyPair)
val snode = getSingleTargetSnode(publicKey).await()
val verificationData =
sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray()
val deleteMessageParams = buildMap {
this["pubkey"] = userPublicKey
this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString
this["messages"] = serverHashes
this["signature"] = signAndEncode(verificationData, userED25519KeyPair)
}
val rawResponse = invoke(
Snode.Method.DeleteMessage,
snode,
deleteMessageParams,
publicKey
).await()
// thie next step is to verify the nodes on our swarm and check that the message was deleted
// on at least one of them
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: throw (Error.Generic)
val deletedMessages = swarms.mapValuesNotNull { (hexSnodePublicKey, rawJSON) ->
(rawJSON as? Map<String, Any>)?.let { json ->
val isFailed = json["failed"] as? Boolean ?: false
val statusCode = json["code"] as? String
val reason = json["reason"] as? String
if (isFailed) {
Log.e(
"Loki",
"Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode)."
)
false
} else {
// Hashes of deleted messages
val hashes = json["deleted"] as List<String>
val signature = json["signature"] as String
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes)
.toByteArray()
sodium.cryptoSignVerifyDetached(
Base64.decode(signature),
message,
message.size,
snodePublicKey.asBytes
)
}
invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse ->
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
swarms.mapValuesNotNull { (hexSnodePublicKey, rawJSON) ->
(rawJSON as? Map<String, Any>)?.let { json ->
val isFailed = json["failed"] as? Boolean ?: false
val statusCode = json["code"] as? String
val reason = json["reason"] as? String
if (isFailed) {
Log.e("Loki", "Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
false
} else {
// Hashes of deleted messages
val hashes = json["deleted"] as List<String>
val signature = json["signature"] as String
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes).toByteArray()
sodium.cryptoSignVerifyDetached(
Base64.decode(signature),
message,
message.size,
snodePublicKey.asBytes
)
}
}
}
}.fail { e -> Log.e("Loki", "Failed to delete messages", e) }
}
}
// if all the nodes returned false (the message was not deleted) then we consider this a failed scenario
if (deletedMessages.entries.all { !it.value }) throw (Error.Generic)
}
}
// Parsing
private fun parseSnodes(rawResponse: Any): List<Snode> =

@ -0,0 +1,3 @@
package org.session.libsignal.exceptions
class NonRetryableException(message: String? = null, cause: Throwable? = null): RuntimeException(message, cause)

@ -1,8 +1,11 @@
package org.session.libsignal.utilities
import kotlinx.coroutines.delay
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.session.libsignal.exceptions.NonRetryableException
import java.util.*
import kotlin.coroutines.cancellation.CancellationException
fun <V, T : Promise<V, Exception>> retryIfNeeded(maxRetryCount: Int, retryInterval: Long = 1000L, body: () -> T): Promise<V, Exception> {
var retryCount = 0
@ -28,3 +31,23 @@ fun <V, T : Promise<V, Exception>> retryIfNeeded(maxRetryCount: Int, retryInterv
retryIfNeeded()
return deferred.promise
}
suspend fun <T> retryWithUniformInterval(maxRetryCount: Int = 3, retryIntervalMills: Long = 1000L, body: suspend () -> T): T {
var retryCount = 0
while (true) {
try {
return body()
} catch (e: CancellationException) {
throw e
} catch (e: NonRetryableException) {
throw e
} catch (e: Exception) {
if (retryCount == maxRetryCount) {
throw e
} else {
retryCount += 1
delay(retryIntervalMills)
}
}
}
}

Loading…
Cancel
Save