Group message deletion

This commit is contained in:
SessionHero01 2024-10-25 17:14:12 +11:00
parent 86a9e07f31
commit ea714a60a2
No known key found for this signature in database
7 changed files with 102 additions and 66 deletions

View File

@ -610,13 +610,12 @@ class ConversationViewModel(
} }
private fun markAsDeletedForEveryoneGroupsV2(data: DeleteForEveryoneDialogData){ private fun markAsDeletedForEveryoneGroupsV2(data: DeleteForEveryoneDialogData){
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.Default) {
// show a loading indicator // show a loading indicator
_uiState.update { it.copy(showLoader = true) } _uiState.update { it.copy(showLoader = true) }
//todo GROUPS V2 - uncomment below and use Fanchao's method to delete a group V2
try { try {
//repository.callMethodFromFanchao(threadId, recipient, data.messages) repository.deleteGroupV2MessagesRemotely(recipient!!, data.messages)
// the repo will handle the internal logic (calling `/delete` on the swarm // the repo will handle the internal logic (calling `/delete` on the swarm
// and sending 'GroupUpdateDeleteMemberContentMessage' // and sending 'GroupUpdateDeleteMemberContentMessage'
@ -638,7 +637,7 @@ class ConversationViewModel(
).show() ).show()
} }
} catch (e: Exception) { } 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 // failed to delete - show a toast and get back on the modal
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText( Toast.makeText(

View File

@ -3,31 +3,34 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import net.zetetic.database.sqlcipher.SQLiteDatabase.CONFLICT_REPLACE 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.libsession.database.ServerHashToMessageId
import org.session.libsignal.database.LokiMessageDatabaseProtocol import org.session.libsignal.database.LokiMessageDatabaseProtocol
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.util.asSequence
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol { class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
companion object { companion object {
private val messageIDTable = "loki_message_friend_request_database" private const val messageIDTable = "loki_message_friend_request_database"
private val messageThreadMappingTable = "loki_message_thread_mapping_database" private const val messageThreadMappingTable = "loki_message_thread_mapping_database"
private val errorMessageTable = "loki_error_message_database" private const val errorMessageTable = "loki_error_message_database"
private val messageHashTable = "loki_message_hash_database" private const val messageHashTable = "loki_message_hash_database"
private val smsHashTable = "loki_sms_hash_database" private const val smsHashTable = "loki_sms_hash_database"
private val mmsHashTable = "loki_mms_hash_database" private const val mmsHashTable = "loki_mms_hash_database"
const val groupInviteTable = "loki_group_invites" 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 const val messageID = "message_id"
private val serverID = "server_id" private const val serverID = "server_id"
private val friendRequestStatus = "friend_request_status" private const val friendRequestStatus = "friend_request_status"
private val threadID = "thread_id" private const val threadID = "thread_id"
private val errorMessage = "error_message" private const val errorMessage = "error_message"
private val messageType = "message_type" private const val messageType = "message_type"
private val serverHash = "server_hash" private const val serverHash = "server_hash"
const val invitingSessionId = "inviting_session_id" const val invitingSessionId = "inviting_session_id"
@JvmStatic @JvmStatic
@ -236,46 +239,52 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
} }
fun getSendersForHashes(threadId: Long, hashes: Set<String>): List<ServerHashToMessageId> { fun getSendersForHashes(threadId: Long, hashes: Set<String>): List<ServerHashToMessageId> {
val smsQuery = "SELECT ${SmsDatabase.TABLE_NAME}.${MmsSmsColumns.ADDRESS}, $smsHashTable.$serverHash, " + @Language("RoomSql")
"${SmsDatabase.TABLE_NAME}.${MmsSmsColumns.ID} FROM $smsHashTable LEFT OUTER JOIN ${SmsDatabase.TABLE_NAME} " + val query = """
"ON ${SmsDatabase.TABLE_NAME}.${MmsSmsColumns.ID} = $smsHashTable.$messageID WHERE ${SmsDatabase.TABLE_NAME}.${MmsSmsColumns.THREAD_ID} = ?;" WITH
val mmsQuery = "SELECT ${MmsDatabase.TABLE_NAME}.${MmsSmsColumns.ADDRESS}, $mmsHashTable.$serverHash, " + sender_hash_mapping AS (
"${MmsDatabase.TABLE_NAME}.${MmsSmsColumns.ID} FROM $mmsHashTable LEFT OUTER JOIN ${MmsDatabase.TABLE_NAME} " + SELECT
"ON ${MmsDatabase.TABLE_NAME}.${MmsSmsColumns.ID} = $mmsHashTable.$messageID WHERE ${MmsDatabase.TABLE_NAME}.${MmsSmsColumns.THREAD_ID} = ?;" sms_hash_table.$serverHash AS hash,
val smsCursor = databaseHelper.readableDatabase.query(smsQuery, arrayOf(threadId)) sms.${MmsSmsColumns.ID} AS message_id,
val mmsCursor = databaseHelper.readableDatabase.query(mmsQuery, arrayOf(threadId)) 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
val serverHashToMessageIds = mutableListOf<ServerHashToMessageId>() UNION ALL
smsCursor.use { cursor -> SELECT
while (cursor.moveToNext()) { mms_hash_table.$serverHash,
val hash = cursor.getString(1) mms.${MmsSmsColumns.ID},
if (hash in hashes) { mms.${MmsSmsColumns.ADDRESS},
serverHashToMessageIds += ServerHashToMessageId( mms.${MmsDatabase.MESSAGE_TYPE},
serverHash = hash, false
isSms = true, FROM $mmsHashTable mms_hash_table
sender = cursor.getString(0), LEFT OUTER JOIN ${MmsDatabase.TABLE_NAME} mms ON mms_hash_table.${messageID} = mms.${MmsSmsColumns.ID}
messageId = cursor.getLong(2) WHERE mms.${MmsSmsColumns.THREAD_ID} = :threadId
)
SELECT * FROM sender_hash_mapping
WHERE hash IN (SELECT value FROM json_each(:hashes))
""".trimIndent()
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 -> return result
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
} }
fun getMessageServerHash(messageID: Long, mms: Boolean): String? = getMessageTables(mms).firstNotNullOfOrNull { fun getMessageServerHash(messageID: Long, mms: Boolean): String? = getMessageTables(mms).firstNotNullOfOrNull {

View File

@ -296,15 +296,21 @@ open class Storage @Inject constructor(
closedGroupId: String closedGroupId: String
): Boolean { ): Boolean {
val threadId = getThreadId(fromSerialized(closedGroupId))!! val threadId = getThreadId(fromSerialized(closedGroupId))!!
val senderIsMe = sender == getUserPublicKey()
val info = lokiMessageDatabase.getSendersForHashes(threadId, hashes) val info = lokiMessageDatabase.getSendersForHashes(threadId, hashes)
if (senderIsMe) {
return info.all { it.isOutgoing }
} else {
return info.all { it.sender == sender } return info.all { it.sender == sender }
} }
}
override fun deleteMessagesByHash(threadId: Long, hashes: List<String>) { override fun deleteMessagesByHash(threadId: Long, hashes: List<String>) {
val info = lokiMessageDatabase.getSendersForHashes(threadId, hashes.toSet()) for (info in lokiMessageDatabase.getSendersForHashes(threadId, hashes.toSet())) {
for ((serverHash, sender, messageIdToDelete, isSms) in info) { messageDataProvider.deleteMessage(info.messageId, info.isSms)
messageDataProvider.deleteMessage(messageIdToDelete, isSms) if (!info.isOutgoing) {
if (!messageDataProvider.isOutgoingMessage(messageIdToDelete)) {
notificationManager.updateNotification(context) notificationManager.updateNotification(context)
} }
} }

View File

@ -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_FAILED
import network.loki.messenger.libsession_util.util.INVITE_STATUS_SENT import network.loki.messenger.libsession_util.util.INVITE_STATUS_SENT
import network.loki.messenger.libsession_util.util.UserPic 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.StorageProtocol
import org.session.libsession.database.userAuth import org.session.libsession.database.userAuth
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
@ -74,6 +75,7 @@ class GroupManagerV2Impl @Inject constructor(
private val profileManager: SSKEnvironment.ProfileManagerProtocol, private val profileManager: SSKEnvironment.ProfileManagerProtocol,
@ApplicationContext val application: Context, @ApplicationContext val application: Context,
private val clock: SnodeClock, private val clock: SnodeClock,
private val messageDataProvider: MessageDataProvider,
) : GroupManagerV2 { ) : GroupManagerV2 {
private val dispatcher = Dispatchers.Default private val dispatcher = Dispatchers.Default
@ -865,7 +867,7 @@ class GroupManagerV2Impl @Inject constructor(
override suspend fun requestMessageDeletion( override suspend fun requestMessageDeletion(
groupId: AccountId, groupId: AccountId,
messageHashes: List<String> messageHashes: Set<String>
): Unit = withContext(dispatcher) { ): Unit = withContext(dispatcher) {
// To delete messages from a group, there are a few considerations: // 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 // 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( check(
group.hasAdminKey() || group.hasAdminKey() ||
storage.ensureMessageHashesAreSender( storage.ensureMessageHashesAreSender(
messageHashes.toSet(), messageHashes,
userPubKey, userPubKey,
groupId.hexString groupId.hexString
) )
@ -896,7 +898,7 @@ class GroupManagerV2Impl @Inject constructor(
SnodeAPI.deleteMessage( SnodeAPI.deleteMessage(
publicKey = groupId.hexString, publicKey = groupId.hexString,
swarmAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), swarmAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey),
serverHashes = messageHashes serverHashes = messageHashes.toList()
) )
} }
@ -958,8 +960,8 @@ class GroupManagerV2Impl @Inject constructor(
groupId.hexString groupId.hexString
) )
) { ) {
// ensure that all message hashes belong to user // For deleting message by hashes, we'll likely only need to mark
// storage delete // them as deleted
storage.deleteMessagesByHash(threadId, hashes) storage.deleteMessagesByHash(threadId, hashes)
} }
} }

View File

@ -77,6 +77,8 @@ interface ConversationRepository {
messages: Set<MessageRecord> messages: Set<MessageRecord>
) )
suspend fun deleteGroupV2MessagesRemotely(recipient: Recipient, messages: Set<MessageRecord>)
suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit> suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit> suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun deleteThread(threadId: Long): Result<Unit> suspend fun deleteThread(threadId: Long): Result<Unit>
@ -315,6 +317,20 @@ class DefaultConversationRepository @Inject constructor(
} }
} }
override suspend fun deleteGroupV2MessagesRemotely(
recipient: Recipient,
messages: Set<MessageRecord>
) {
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( override suspend fun deleteNoteToSelfMessagesRemotely(
threadId: Long, threadId: Long,
recipient: Recipient, recipient: Recipient,

View File

@ -2,7 +2,11 @@ package org.session.libsession.database
data class ServerHashToMessageId( data class ServerHashToMessageId(
val serverHash: String, val serverHash: String,
/**
* This will only be the "sender" when the message is incoming.
*/
val sender: String, val sender: String,
val messageId: Long, val messageId: Long,
val isSms: Boolean, val isSms: Boolean,
val isOutgoing: Boolean,
) )

View File

@ -80,7 +80,7 @@ interface GroupManagerV2 {
* It can be called by a regular member who wishes to delete their own messages. * 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. * It can also called by an admin, who can delete any messages from any member.
*/ */
suspend fun requestMessageDeletion(groupId: AccountId, messageHashes: List<String>) suspend fun requestMessageDeletion(groupId: AccountId, messageHashes: Set<String>)
/** /**
* Handle a request to delete a member's content from the group. This is called when we receive * Handle a request to delete a member's content from the group. This is called when we receive