mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 02:55:23 +00:00
Handle message deletion
This commit is contained in:
parent
b06aee7a20
commit
22b4479019
@ -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;
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
@ -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?
|
||||
|
@ -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,
|
||||
|
@ -93,6 +93,7 @@ interface GroupManagerV2 {
|
||||
suspend fun handleDeleteMemberContent(
|
||||
groupId: AccountId,
|
||||
deleteMemberContent: GroupUpdateDeleteMemberContentMessage,
|
||||
timestamp: Long,
|
||||
sender: AccountId,
|
||||
senderIsVerifiedAdmin: Boolean,
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user