Handle message deletion

This commit is contained in:
SessionHero01 2024-10-30 11:40:19 +11:00
parent b06aee7a20
commit 22b4479019
No known key found for this signature in database
11 changed files with 157 additions and 68 deletions

View File

@ -44,7 +44,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.thoughtcrime.securesms.configs.ConfigToDatabaseSync;
import org.thoughtcrime.securesms.configs.ConfigUploader;
import org.session.libsession.messaging.groups.GroupManagerV2;
import org.session.libsession.messaging.groups.RemoveGroupMemberHandler;
import org.thoughtcrime.securesms.groups.handler.RemoveGroupMemberHandler;
import org.session.libsession.messaging.notifications.TokenFetcher;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.sending_receiving.pollers.LegacyClosedGroupPollerV2;
@ -85,10 +85,8 @@ import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.PushRegistrationHandler;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService;

View File

@ -7,7 +7,6 @@ import org.greenrobot.eventbus.EventBus
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.MarkAsDeletedMessage
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
@ -249,6 +248,51 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
}
}
override fun markMessagesAsDeleted(
threadId: Long,
serverHashes: List<String>,
displayedMessage: String
) {
val sendersForHashes = DatabaseComponent.get(context).lokiMessageDatabase()
.getSendersForHashes(threadId, serverHashes.toSet())
val smsMessages = sendersForHashes.asSequence()
.filter { it.isSms }
.map { msg -> MarkAsDeletedMessage(messageId = msg.messageId, isOutgoing = msg.isOutgoing) }
.toList()
val mmsMessages = sendersForHashes.asSequence()
.filter { !it.isSms }
.map { msg -> MarkAsDeletedMessage(messageId = msg.messageId, isOutgoing = msg.isOutgoing) }
.toList()
markMessagesAsDeleted(smsMessages, isSms = true, displayedMessage)
markMessagesAsDeleted(mmsMessages, isSms = false, displayedMessage)
}
override fun markUserMessagesAsDeleted(
threadId: Long,
until: Long,
sender: String,
displayedMessage: String
) {
val mmsMessages = mutableListOf<MarkAsDeletedMessage>()
val smsMessages = mutableListOf<MarkAsDeletedMessage>()
DatabaseComponent.get(context).mmsSmsDatabase().getUserMessages(threadId, sender)
.filter { it.timestamp <= until }
.forEach { record ->
if (record.isMms) {
mmsMessages.add(MarkAsDeletedMessage(record.id, record.isOutgoing))
} else {
smsMessages.add(MarkAsDeletedMessage(record.id, record.isOutgoing))
}
}
markMessagesAsDeleted(smsMessages, isSms = true, displayedMessage)
markMessagesAsDeleted(mmsMessages, isSms = false, displayedMessage)
}
override fun getServerHashForMessage(messageID: Long, mms: Boolean): String? =
DatabaseComponent.get(context).lokiMessageDatabase().getMessageServerHash(messageID, mms)

View File

@ -310,25 +310,6 @@ public class MmsSmsDatabase extends Database {
return identifiedMessages;
}
// Version of the above `getAllMessageRecordsFromSenderInThread` method that returns the message
// Ids rather than the set of MessageRecords - currently unused by potentially useful in the future.
public Set<Long> getAllMessageIdsFromSenderInThread(long threadId, String serializedAuthor) {
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.ADDRESS + " = \"" + serializedAuthor + "\"";
Set<Long> identifiedMessages = new HashSet<Long>();
// Try everything with resources so that they auto-close on end of scope
try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
try (MmsSmsDatabase.Reader reader = readerFor(cursor)) {
MessageRecord messageRecord;
while ((messageRecord = reader.getNext()) != null) {
identifiedMessages.add(messageRecord.id);
}
}
}
return identifiedMessages;
}
public long getLastOutgoingTimestamp(long threadId) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;

View File

@ -30,7 +30,6 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.ConfigFactory
const val MAX_GROUP_NAME_LENGTH = 100

View File

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE
import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.GroupInfo
@ -939,6 +940,7 @@ class GroupManagerV2Impl @Inject constructor(
override suspend fun handleDeleteMemberContent(
groupId: AccountId,
deleteMemberContent: GroupUpdateDeleteMemberContentMessage,
timestamp: Long,
sender: AccountId,
senderIsVerifiedAdmin: Boolean,
): Unit = withContext(dispatcher) {
@ -951,24 +953,31 @@ class GroupManagerV2Impl @Inject constructor(
val memberIds = deleteMemberContent.memberSessionIdsList
if (hashes.isNotEmpty()) {
if (senderIsVerifiedAdmin) {
// We'll delete everything the admin says
storage.deleteMessagesByHash(threadId, hashes)
} else if (storage.ensureMessageHashesAreSender(
// If the sender is a verified admin, or the sender is the actual sender of the messages,
// we can mark them as deleted locally.
if (senderIsVerifiedAdmin ||
storage.ensureMessageHashesAreSender(
hashes.toSet(),
sender.hexString,
groupId.hexString
)) {
// We'll delete everything the admin says
messageDataProvider.markMessagesAsDeleted(
threadId = threadId,
serverHashes = hashes,
displayedMessage = application.getString(
R.string.deleteMessageDeletedGlobally
)
)
) {
// For deleting message by hashes, we'll likely only need to mark
// them as deleted
storage.deleteMessagesByHash(threadId, hashes)
}
}
// To be able to delete a user's messages, the sender must be a verified admin
if (memberIds.isNotEmpty() && senderIsVerifiedAdmin) {
for (member in memberIds) {
storage.deleteMessagesByUser(threadId, member)
messageDataProvider.markUserMessagesAsDeleted(threadId, timestamp, member, application.getString(
R.string.deleteMessageDeletedGlobally
))
}
}

View File

@ -303,6 +303,27 @@ private fun ConfirmRemovingMemberDialog(
groupName: String,
) {
val context = LocalContext.current
val buttons = buildList {
this += DialogButtonModel(
text = GetString(R.string.remove),
color = LocalColors.current.danger,
onClick = { onConfirmed(member.accountId, false) }
)
if (BuildConfig.DEBUG) {
this += DialogButtonModel(
text = GetString("Remove with messages"),
color = LocalColors.current.danger,
onClick = { onConfirmed(member.accountId, true) }
)
}
this += DialogButtonModel(
text = GetString(R.string.cancel),
onClick = onDismissRequest,
)
}
AlertDialog(
onDismissRequest = onDismissRequest,
text = Phrase.from(context, R.string.groupRemoveDescription)
@ -311,17 +332,7 @@ private fun ConfirmRemovingMemberDialog(
.format()
.toString(),
title = stringResource(R.string.remove),
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.remove),
color = LocalColors.current.danger,
onClick = { onConfirmed(member.accountId, false) }
),
DialogButtonModel(
text = GetString(R.string.cancel),
onClick = onDismissRequest,
)
)
buttons = buttons
)
}
@ -346,7 +357,7 @@ private fun MemberOptionsDialog(
)
}
if (false && member.canPromote) {
if (BuildConfig.DEBUG && member.canPromote) {
this += BottomOptionsDialogItem(
title = context.getString(R.string.adminPromoteToAdmin),
iconRes = R.drawable.ic_profile_default,

View File

@ -1,37 +1,45 @@
package org.session.libsession.messaging.groups
package org.thoughtcrime.securesms.groups.handler
import android.content.Context
import android.os.SystemClock
import kotlinx.coroutines.CoroutineScope
import com.google.protobuf.ByteString
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.libsession_util.ReadableGroupKeysConfig
import network.loki.messenger.libsession_util.util.GroupMember
import network.loki.messenger.libsession_util.util.Sodium
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.MessageAuthentication
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.OwnedSwarmAuth
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeClock
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.waitUntilGroupConfigsPushed
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import javax.inject.Inject
import javax.inject.Singleton
private const val TAG = "RemoveGroupMemberHandler"
private const val MIN_PROCESS_INTERVAL_MILLS = 1_000L
/**
@ -39,10 +47,15 @@ private const val MIN_PROCESS_INTERVAL_MILLS = 1_000L
*
* It automatically does so by listening to the config updates changes and checking for any pending removals.
*/
@Singleton
class RemoveGroupMemberHandler @Inject constructor(
@ApplicationContext private val context: Context,
private val configFactory: ConfigFactoryProtocol,
private val textSecurePreferences: TextSecurePreferences,
private val groupManager: GroupManagerV2,
private val clock: SnodeClock,
private val messageDataProvider: MessageDataProvider,
private val storage: StorageProtocol,
) {
private var job: Job? = null
@ -121,7 +134,12 @@ class RemoveGroupMemberHandler @Inject constructor(
// Call No 2. Send a "kicked" message to the revoked namespace
calls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = Namespace.REVOKED_GROUP_MESSAGES(),
message = buildGroupKickMessage(groupAccountId.hexString, pendingRemovals, configs.groupKeys, adminKey),
message = buildGroupKickMessage(
groupAccountId.hexString,
pendingRemovals,
configs.groupKeys,
adminKey
),
auth = groupAuth,
)
@ -130,11 +148,12 @@ class RemoveGroupMemberHandler @Inject constructor(
calls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = Namespace.CLOSED_GROUP_MESSAGES(),
message = buildDeleteGroupMemberContentMessage(
adminKey = adminKey,
groupAccountId = groupAccountId.hexString,
memberSessionIDs = pendingRemovals
.asSequence()
.filter { it.shouldRemoveMessages }
.map { it.sessionId }
.map { it.sessionId },
),
auth = groupAuth,
)
@ -148,7 +167,8 @@ class RemoveGroupMemberHandler @Inject constructor(
}
val node = SnodeAPI.getSingleTargetSnode(groupAccountId.hexString).await()
val response = SnodeAPI.getBatchResponse(node, groupAccountId.hexString, batchCalls, sequence = true)
val response =
SnodeAPI.getBatchResponse(node, groupAccountId.hexString, batchCalls, sequence = true)
val firstError = response.results.firstOrNull { !it.isSuccessful }
check(firstError == null) {
@ -172,36 +192,58 @@ class RemoveGroupMemberHandler @Inject constructor(
// cases (a.k.a the GroupUpdateDeleteMemberContent message handling) and could be by different admins.
val deletingMessagesForMembers = pendingRemovals.filter { it.shouldRemoveMessages }
if (deletingMessagesForMembers.isNotEmpty()) {
try {
groupManager.removeMemberMessages(
groupAccountId,
deletingMessagesForMembers.map { AccountId(it.sessionId) }
)
} catch (e: Exception) {
Log.e(TAG, "Error deleting messages for removed members", e)
val threadId = storage.getThreadId(Address.fromSerialized(groupAccountId.hexString))
if (threadId != null) {
val until = clock.currentTimeMills()
for (member in deletingMessagesForMembers) {
try {
messageDataProvider.markUserMessagesAsDeleted(
threadId = threadId,
until = until,
sender = member.sessionId,
displayedMessage = context.getString(R.string.deleteMessageDeletedGlobally)
)
} catch (e: Exception) {
Log.e(TAG, "Error deleting messages for removed member", e)
}
}
}
}
}
private fun buildDeleteGroupMemberContentMessage(
adminKey: ByteArray,
groupAccountId: String,
memberSessionIDs: Sequence<String>
): SnodeMessage {
val timestamp = clock.currentTimeMills()
return MessageSender.buildWrappedMessageToSnode(
destination = Destination.ClosedGroup(groupAccountId),
message = GroupUpdated(
GroupUpdateMessage.newBuilder()
SignalServiceProtos.DataMessage.GroupUpdateMessage.newBuilder()
.setDeleteMemberContent(
SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
.newBuilder()
SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage.newBuilder()
.apply {
for (id in memberSessionIDs) {
addMemberSessionIds(id)
}
}
.setAdminSignature(
ByteString.copyFrom(
SodiumUtilities.sign(
MessageAuthentication.buildDeleteMemberContentSignature(
memberIds = memberSessionIDs.map { AccountId(it) }
.toList(),
messageHashes = emptyList(),
timestamp = timestamp,
), adminKey
)
)
)
)
.build()
),
).apply { sentTimestamp = timestamp },
isSyncMessage = false
)
}
@ -227,7 +269,6 @@ class RemoveGroupMemberHandler @Inject constructor(
)
),
ttl = SnodeMessage.DEFAULT_TTL,
timestamp = SnodeAPI.nowWithOffset
timestamp = clock.currentTimeMills()
)
}
}

View File

@ -26,6 +26,8 @@ interface MessageDataProvider {
fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean)
fun markMessageAsDeleted(timestamp: Long, author: String, displayedMessage: String)
fun markMessagesAsDeleted(messages: List<MarkAsDeletedMessage>, isSms: Boolean, displayedMessage: String)
fun markMessagesAsDeleted(threadId: Long, serverHashes: List<String>, displayedMessage: String)
fun markUserMessagesAsDeleted(threadId: Long, until: Long, sender: String, displayedMessage: String)
fun getServerHashForMessage(messageID: Long, mms: Boolean): String?
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?

View File

@ -3,7 +3,9 @@ package org.session.libsession.database
data class ServerHashToMessageId(
val serverHash: String,
/**
* This will only be the "sender" when the message is incoming.
* This will only be the "sender" when the message is incoming, when the message is outgoing,
* the value here could be the receiver of the message, it's better not to rely on opposite
* meaning of this field.
*/
val sender: String,
val messageId: Long,

View File

@ -93,6 +93,7 @@ interface GroupManagerV2 {
suspend fun handleDeleteMemberContent(
groupId: AccountId,
deleteMemberContent: GroupUpdateDeleteMemberContentMessage,
timestamp: Long,
sender: AccountId,
senderIsVerifiedAdmin: Boolean,
)

View File

@ -651,6 +651,7 @@ private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: Accoun
MessagingModuleConfiguration.shared.groupManagerV2.handleDeleteMemberContent(
groupId = closedGroup,
deleteMemberContent = deleteMemberContent,
timestamp = message.sentTimestamp!!,
sender = AccountId(message.sender!!),
senderIsVerifiedAdmin = hasValidAdminSignature
)