From ea714a60a290a5f859d9e8e2214b84caf76bd891 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:14:12 +1100 Subject: [PATCH] Group message deletion --- .../conversation/v2/ConversationViewModel.kt | 7 +- .../securesms/database/LokiMessageDatabase.kt | 111 ++++++++++-------- .../securesms/database/Storage.kt | 16 ++- .../securesms/groups/GroupManagerV2Impl.kt | 12 +- .../repository/ConversationRepository.kt | 16 +++ .../database/ServerHashToMessageId.kt | 4 + .../messaging/groups/GroupManagerV2.kt | 2 +- 7 files changed, 102 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 4c7c775e90..ef619d3066 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -610,13 +610,12 @@ class ConversationViewModel( } private fun markAsDeletedForEveryoneGroupsV2(data: DeleteForEveryoneDialogData){ - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.Default) { // show a loading indicator _uiState.update { it.copy(showLoader = true) } - //todo GROUPS V2 - uncomment below and use Fanchao's method to delete a group V2 try { - //repository.callMethodFromFanchao(threadId, recipient, data.messages) + repository.deleteGroupV2MessagesRemotely(recipient!!, data.messages) // the repo will handle the internal logic (calling `/delete` on the swarm // and sending 'GroupUpdateDeleteMemberContentMessage' @@ -638,7 +637,7 @@ class ConversationViewModel( ).show() } } catch (e: Exception) { - Log.w("Loki", "FAILED TO delete messages ${data.messages} ") + Log.e("Loki", "FAILED TO delete messages ${data.messages}", e) // failed to delete - show a toast and get back on the modal withContext(Dispatchers.Main) { Toast.makeText( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt index 3fa1dc6093..e9c13ca52f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt @@ -3,31 +3,34 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE +import org.intellij.lang.annotations.Language +import org.json.JSONArray import org.session.libsession.database.ServerHashToMessageId import org.session.libsignal.database.LokiMessageDatabaseProtocol import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.util.asSequence class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol { companion object { - private val messageIDTable = "loki_message_friend_request_database" - private val messageThreadMappingTable = "loki_message_thread_mapping_database" - private val errorMessageTable = "loki_error_message_database" - private val messageHashTable = "loki_message_hash_database" - private val smsHashTable = "loki_sms_hash_database" - private val mmsHashTable = "loki_mms_hash_database" + private const val messageIDTable = "loki_message_friend_request_database" + private const val messageThreadMappingTable = "loki_message_thread_mapping_database" + private const val errorMessageTable = "loki_error_message_database" + private const val messageHashTable = "loki_message_hash_database" + private const val smsHashTable = "loki_sms_hash_database" + private const val mmsHashTable = "loki_mms_hash_database" const val groupInviteTable = "loki_group_invites" - private val groupInviteDeleteTrigger = "group_invite_delete_trigger" + private const val groupInviteDeleteTrigger = "group_invite_delete_trigger" - private val messageID = "message_id" - private val serverID = "server_id" - private val friendRequestStatus = "friend_request_status" - private val threadID = "thread_id" - private val errorMessage = "error_message" - private val messageType = "message_type" - private val serverHash = "server_hash" + private const val messageID = "message_id" + private const val serverID = "server_id" + private const val friendRequestStatus = "friend_request_status" + private const val threadID = "thread_id" + private const val errorMessage = "error_message" + private const val messageType = "message_type" + private const val serverHash = "server_hash" const val invitingSessionId = "inviting_session_id" @JvmStatic @@ -236,46 +239,52 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab } fun getSendersForHashes(threadId: Long, hashes: Set): List { - val smsQuery = "SELECT ${SmsDatabase.TABLE_NAME}.${MmsSmsColumns.ADDRESS}, $smsHashTable.$serverHash, " + - "${SmsDatabase.TABLE_NAME}.${MmsSmsColumns.ID} FROM $smsHashTable LEFT OUTER JOIN ${SmsDatabase.TABLE_NAME} " + - "ON ${SmsDatabase.TABLE_NAME}.${MmsSmsColumns.ID} = $smsHashTable.$messageID WHERE ${SmsDatabase.TABLE_NAME}.${MmsSmsColumns.THREAD_ID} = ?;" - val mmsQuery = "SELECT ${MmsDatabase.TABLE_NAME}.${MmsSmsColumns.ADDRESS}, $mmsHashTable.$serverHash, " + - "${MmsDatabase.TABLE_NAME}.${MmsSmsColumns.ID} FROM $mmsHashTable LEFT OUTER JOIN ${MmsDatabase.TABLE_NAME} " + - "ON ${MmsDatabase.TABLE_NAME}.${MmsSmsColumns.ID} = $mmsHashTable.$messageID WHERE ${MmsDatabase.TABLE_NAME}.${MmsSmsColumns.THREAD_ID} = ?;" - val smsCursor = databaseHelper.readableDatabase.query(smsQuery, arrayOf(threadId)) - val mmsCursor = databaseHelper.readableDatabase.query(mmsQuery, arrayOf(threadId)) + @Language("RoomSql") + val query = """ + WITH + sender_hash_mapping AS ( + SELECT + sms_hash_table.$serverHash AS hash, + sms.${MmsSmsColumns.ID} AS message_id, + sms.${MmsSmsColumns.ADDRESS} AS sender, + sms.${SmsDatabase.TYPE} AS type, + true AS is_sms + FROM $smsHashTable sms_hash_table + LEFT OUTER JOIN ${SmsDatabase.TABLE_NAME} sms ON sms_hash_table.${messageID} = sms.${MmsSmsColumns.ID} + WHERE sms.${MmsSmsColumns.THREAD_ID} = :threadId + + UNION ALL + + SELECT + mms_hash_table.$serverHash, + mms.${MmsSmsColumns.ID}, + mms.${MmsSmsColumns.ADDRESS}, + mms.${MmsDatabase.MESSAGE_TYPE}, + false + FROM $mmsHashTable mms_hash_table + LEFT OUTER JOIN ${MmsDatabase.TABLE_NAME} mms ON mms_hash_table.${messageID} = mms.${MmsSmsColumns.ID} + WHERE mms.${MmsSmsColumns.THREAD_ID} = :threadId + ) + SELECT * FROM sender_hash_mapping + WHERE hash IN (SELECT value FROM json_each(:hashes)) + """.trimIndent() - val serverHashToMessageIds = mutableListOf() - - smsCursor.use { cursor -> - while (cursor.moveToNext()) { - val hash = cursor.getString(1) - if (hash in hashes) { - serverHashToMessageIds += ServerHashToMessageId( - serverHash = hash, - isSms = true, - sender = cursor.getString(0), - messageId = cursor.getLong(2) - ) - } + val result = databaseHelper.readableDatabase.query(query, arrayOf(threadId, JSONArray(hashes).toString())) + .use { cursor -> + cursor.asSequence() + .map { + ServerHashToMessageId( + serverHash = cursor.getString(0), + messageId = cursor.getLong(1), + sender = cursor.getString(2), + isSms = cursor.getInt(4) == 1, + isOutgoing = MmsSmsColumns.Types.isOutgoingMessageType(cursor.getLong(3)) + ) + } + .toList() } - } - mmsCursor.use { cursor -> - while (cursor.moveToNext()) { - val hash = cursor.getString(1) - if (hash in hashes) { - serverHashToMessageIds += ServerHashToMessageId( - serverHash = hash, - isSms = false, - sender = cursor.getString(0), - messageId = cursor.getLong(2) - ) - } - } - } - - return serverHashToMessageIds + return result } fun getMessageServerHash(messageID: Long, mms: Boolean): String? = getMessageTables(mms).firstNotNullOfOrNull { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 3348853276..b51680fc32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -296,15 +296,21 @@ open class Storage @Inject constructor( closedGroupId: String ): Boolean { val threadId = getThreadId(fromSerialized(closedGroupId))!! + val senderIsMe = sender == getUserPublicKey() + val info = lokiMessageDatabase.getSendersForHashes(threadId, hashes) - return info.all { it.sender == sender } + + if (senderIsMe) { + return info.all { it.isOutgoing } + } else { + return info.all { it.sender == sender } + } } override fun deleteMessagesByHash(threadId: Long, hashes: List) { - val info = lokiMessageDatabase.getSendersForHashes(threadId, hashes.toSet()) - for ((serverHash, sender, messageIdToDelete, isSms) in info) { - messageDataProvider.deleteMessage(messageIdToDelete, isSms) - if (!messageDataProvider.isOutgoingMessage(messageIdToDelete)) { + for (info in lokiMessageDatabase.getSendersForHashes(threadId, hashes.toSet())) { + messageDataProvider.deleteMessage(info.messageId, info.isSms) + if (!info.isOutgoing) { notificationManager.updateNotification(context) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index 6095d7b496..a9df8d6cba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -19,6 +19,7 @@ import network.loki.messenger.libsession_util.util.GroupMember import network.loki.messenger.libsession_util.util.INVITE_STATUS_FAILED import network.loki.messenger.libsession_util.util.INVITE_STATUS_SENT import network.loki.messenger.libsession_util.util.UserPic +import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol import org.session.libsession.database.userAuth import org.session.libsession.messaging.groups.GroupManagerV2 @@ -74,6 +75,7 @@ class GroupManagerV2Impl @Inject constructor( private val profileManager: SSKEnvironment.ProfileManagerProtocol, @ApplicationContext val application: Context, private val clock: SnodeClock, + private val messageDataProvider: MessageDataProvider, ) : GroupManagerV2 { private val dispatcher = Dispatchers.Default @@ -865,7 +867,7 @@ class GroupManagerV2Impl @Inject constructor( override suspend fun requestMessageDeletion( groupId: AccountId, - messageHashes: List + messageHashes: Set ): Unit = withContext(dispatcher) { // To delete messages from a group, there are a few considerations: // 1. Messages are stored on every member's device, we need a way to ask them to delete their stored messages @@ -883,7 +885,7 @@ class GroupManagerV2Impl @Inject constructor( check( group.hasAdminKey() || storage.ensureMessageHashesAreSender( - messageHashes.toSet(), + messageHashes, userPubKey, groupId.hexString ) @@ -896,7 +898,7 @@ class GroupManagerV2Impl @Inject constructor( SnodeAPI.deleteMessage( publicKey = groupId.hexString, swarmAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), - serverHashes = messageHashes + serverHashes = messageHashes.toList() ) } @@ -958,8 +960,8 @@ class GroupManagerV2Impl @Inject constructor( groupId.hexString ) ) { - // ensure that all message hashes belong to user - // storage delete + // For deleting message by hashes, we'll likely only need to mark + // them as deleted storage.deleteMessagesByHash(threadId, hashes) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index 383562a17c..ff9d25565a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -77,6 +77,8 @@ interface ConversationRepository { messages: Set ) + suspend fun deleteGroupV2MessagesRemotely(recipient: Recipient, messages: Set) + suspend fun banUser(threadId: Long, recipient: Recipient): Result suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result suspend fun deleteThread(threadId: Long): Result @@ -315,6 +317,20 @@ class DefaultConversationRepository @Inject constructor( } } + override suspend fun deleteGroupV2MessagesRemotely( + recipient: Recipient, + messages: Set + ) { + require(recipient.isGroupV2Recipient) { "Recipient is not a group v2 recipient" } + + val groupId = AccountId(recipient.address.serialize()) + val hashes = messages.mapNotNullTo(mutableSetOf()) { msg -> + messageDataProvider.getServerHashForMessage(msg.id, msg.isMms) + } + + groupManager.requestMessageDeletion(groupId, hashes) + } + override suspend fun deleteNoteToSelfMessagesRemotely( threadId: Long, recipient: Recipient, diff --git a/libsession/src/main/java/org/session/libsession/database/ServerHashToMessageId.kt b/libsession/src/main/java/org/session/libsession/database/ServerHashToMessageId.kt index 704b884ea8..9df00bedec 100644 --- a/libsession/src/main/java/org/session/libsession/database/ServerHashToMessageId.kt +++ b/libsession/src/main/java/org/session/libsession/database/ServerHashToMessageId.kt @@ -2,7 +2,11 @@ package org.session.libsession.database data class ServerHashToMessageId( val serverHash: String, + /** + * This will only be the "sender" when the message is incoming. + */ val sender: String, val messageId: Long, val isSms: Boolean, + val isOutgoing: Boolean, ) \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt index d78aa87904..a593b0283f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt @@ -80,7 +80,7 @@ interface GroupManagerV2 { * It can be called by a regular member who wishes to delete their own messages. * It can also called by an admin, who can delete any messages from any member. */ - suspend fun requestMessageDeletion(groupId: AccountId, messageHashes: List) + suspend fun requestMessageDeletion(groupId: AccountId, messageHashes: Set) /** * Handle a request to delete a member's content from the group. This is called when we receive