mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 11:05:25 +00:00
Handle message deletion
This commit is contained in:
parent
57c95ea7d6
commit
b13b7e647f
@ -1339,80 +1339,6 @@ open class Storage(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun handleMemberLeftNotification(message: GroupUpdated, closedGroupId: AccountId) {
|
||||
insertGroupInfoChange(message, closedGroupId)
|
||||
}
|
||||
|
||||
|
||||
override fun sendGroupUpdateDeleteMessage(groupSessionId: String, messageHashes: List<String>): Promise<Unit, Exception> {
|
||||
val closedGroup = configFactory.userGroups?.getClosedGroup(groupSessionId)
|
||||
?: return Promise.ofFail(NullPointerException("No group found"))
|
||||
|
||||
val keys = configFactory.getGroupKeysConfig(AccountId(groupSessionId))
|
||||
?: return Promise.ofFail(NullPointerException("No group keys found"))
|
||||
|
||||
val adminKey = if (closedGroup.hasAdminKey()) closedGroup.adminKey else null
|
||||
val authData = closedGroup.authData
|
||||
val auth = if (adminKey != null) {
|
||||
OwnedSwarmAuth.ofClosedGroup(AccountId(groupSessionId), adminKey)
|
||||
} else if (authData != null) {
|
||||
GroupSubAccountSwarmAuth(keys, AccountId(groupSessionId), authData)
|
||||
} else {
|
||||
return Promise.ofFail(IllegalStateException("No auth data nor admin key found"))
|
||||
}
|
||||
|
||||
val groupDestination = Destination.ClosedGroup(groupSessionId)
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(groupDestination)
|
||||
val timestamp = SnodeAPI.nowWithOffset
|
||||
val signature = adminKey?.let { key ->
|
||||
SodiumUtilities.sign(
|
||||
buildDeleteMemberContentSignature(memberIds = emptyList(), messageHashes, timestamp),
|
||||
key
|
||||
)
|
||||
}
|
||||
val message = GroupUpdated(
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setDeleteMemberContent(
|
||||
GroupUpdateDeleteMemberContentMessage.newBuilder()
|
||||
.addAllMessageHashes(messageHashes)
|
||||
.let {
|
||||
if (signature != null) it.setAdminSignature(ByteString.copyFrom(signature))
|
||||
else it
|
||||
}
|
||||
)
|
||||
.build()
|
||||
).apply {
|
||||
sentTimestamp = timestamp
|
||||
}
|
||||
|
||||
// Delete might need fake hash?
|
||||
val authenticatedDelete = if (adminKey == null) null else buildAuthenticatedDeleteBatchInfo(auth, messageHashes, required = true)
|
||||
val authenticatedStore = buildAuthenticatedStoreBatchInfo(
|
||||
namespace = Namespace.CLOSED_GROUP_MESSAGES(),
|
||||
message = MessageSender.buildWrappedMessageToSnode(Destination.ClosedGroup(groupSessionId), message, false),
|
||||
auth = auth
|
||||
)
|
||||
|
||||
keys.free()
|
||||
|
||||
// delete only present when admin
|
||||
val storeIndex = if (adminKey != null) 1 else 0
|
||||
return SnodeAPI.getSingleTargetSnode(groupSessionId).bind { snode ->
|
||||
SnodeAPI.getRawBatchResponse(
|
||||
snode,
|
||||
groupSessionId,
|
||||
listOfNotNull(authenticatedDelete, authenticatedStore),
|
||||
sequence = true
|
||||
)
|
||||
}.map { rawResponse ->
|
||||
val results = (rawResponse["results"] as ArrayList<Any>)[storeIndex] as Map<String,Any>
|
||||
val hash = results["hash"] as? String
|
||||
message.serverHash = hash
|
||||
MessageSender.handleSuccessfulMessageSend(message, groupDestination, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setServerCapabilities(server: String, capabilities: List<String>) {
|
||||
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
|
||||
}
|
||||
|
@ -1042,6 +1042,111 @@ class GroupManagerV2Impl @Inject constructor(
|
||||
storage.insertGroupInfoChange(message, groupId)
|
||||
}
|
||||
|
||||
override suspend fun requestMessageDeletion(
|
||||
groupId: AccountId,
|
||||
messageHashes: List<String>
|
||||
): 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
|
||||
// 2. Messages are also stored on the group swarm, only the group admin can delete them
|
||||
// So we will send a group message to ask members to delete the messages,
|
||||
// meanwhile, if we are admin we can just delete those messages from the group swarm, and otherwise
|
||||
// the admins can pick up the group message and delete the messages on our behalf.
|
||||
|
||||
val userGroups = requireNotNull(configFactory.userGroups) { "User groups config is not available" }
|
||||
val group = requireNotNull(userGroups.getClosedGroup(groupId.hexString)) {
|
||||
"Group doesn't exist"
|
||||
}
|
||||
val userPubKey = requireNotNull(storage.getUserPublicKey()) { "No current user available" }
|
||||
|
||||
// Check if we can actually delete these messages
|
||||
check(
|
||||
group.hasAdminKey() ||
|
||||
storage.ensureMessageHashesAreSender(messageHashes.toSet(), userPubKey, groupId.hexString)
|
||||
) {
|
||||
"Cannot delete messages that are not sent by us"
|
||||
}
|
||||
|
||||
// If we are admin, we can delete the messages from the group swarm
|
||||
group.adminKey?.let { adminKey ->
|
||||
deleteMessageFromGroupSwarm(groupId, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), messageHashes)
|
||||
}
|
||||
|
||||
// Construct a message to ask members to delete the messages, sign if we are admin, then send
|
||||
val timestamp = SnodeAPI.nowWithOffset
|
||||
val signature = group.adminKey?.let { key ->
|
||||
SodiumUtilities.sign(
|
||||
buildDeleteMemberContentSignature(memberIds = emptyList(), messageHashes, timestamp),
|
||||
key
|
||||
)
|
||||
}
|
||||
val message = GroupUpdated(
|
||||
GroupUpdateMessage.newBuilder()
|
||||
.setDeleteMemberContent(
|
||||
GroupUpdateDeleteMemberContentMessage.newBuilder()
|
||||
.addAllMessageHashes(messageHashes)
|
||||
.let {
|
||||
if (signature != null) it.setAdminSignature(ByteString.copyFrom(signature))
|
||||
else it
|
||||
}
|
||||
)
|
||||
.build()
|
||||
).apply {
|
||||
sentTimestamp = timestamp
|
||||
}
|
||||
|
||||
val groupAddress = Address.fromSerialized(groupId.hexString)
|
||||
MessageSender.sendNonDurably(message, groupAddress, false).await()
|
||||
}
|
||||
|
||||
override suspend fun handleDeleteMemberContent(
|
||||
groupId: AccountId,
|
||||
deleteMemberContent: GroupUpdateDeleteMemberContentMessage,
|
||||
sender: AccountId,
|
||||
senderIsVerifiedAdmin: Boolean,
|
||||
): Unit = withContext(dispatcher) {
|
||||
val threadId = requireNotNull(storage.getThreadId(Address.fromSerialized(groupId.hexString))) {
|
||||
"No thread ID found for the group"
|
||||
}
|
||||
|
||||
val hashes = deleteMemberContent.messageHashesList
|
||||
val memberIds = deleteMemberContent.memberSessionIdsList
|
||||
|
||||
if (hashes.isNotEmpty()) {
|
||||
if (senderIsVerifiedAdmin) {
|
||||
// We'll delete everything the admin says
|
||||
storage.deleteMessagesByHash(threadId, hashes)
|
||||
} else if (storage.ensureMessageHashesAreSender(hashes.toSet(), sender.hexString, groupId.hexString)) {
|
||||
// ensure that all message hashes belong to user
|
||||
// storage delete
|
||||
storage.deleteMessagesByHash(threadId, hashes)
|
||||
}
|
||||
}
|
||||
|
||||
if (memberIds.isNotEmpty() && senderIsVerifiedAdmin) {
|
||||
for (member in memberIds) {
|
||||
storage.deleteMessagesByUser(threadId, member)
|
||||
}
|
||||
}
|
||||
|
||||
val adminKey = configFactory.userGroups?.getClosedGroup(groupId.hexString)?.adminKey
|
||||
if (!senderIsVerifiedAdmin && adminKey != null) {
|
||||
// If the deletion request comes from a non-admin, and we as an admin, will also delete
|
||||
// the content from the swarm, provided that the messages are actually sent by that user
|
||||
if (storage.ensureMessageHashesAreSender(hashes.toSet(), sender.hexString, groupId.hexString)) {
|
||||
deleteMessageFromGroupSwarm(groupId, OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), hashes)
|
||||
}
|
||||
|
||||
// The non-admin user shouldn't be able to delete other user's messages so we will
|
||||
// ignore the memberIds in the message
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteMessageFromGroupSwarm(groupId: AccountId, auth: OwnedSwarmAuth, hashes: List<String>) {
|
||||
SnodeAPI.sendBatchRequest(
|
||||
groupId, SnodeAPI.buildAuthenticatedDeleteBatchInfo(auth, hashes)
|
||||
)
|
||||
}
|
||||
|
||||
private fun BatchResponse.requireAllRequestsSuccessful(errorMessage: String) {
|
||||
val firstError = this.results.firstOrNull { it.code != 200 }
|
||||
|
@ -242,10 +242,10 @@ class DefaultConversationRepository @Inject constructor(
|
||||
|
||||
if (recipient.isClosedGroupV2Recipient) {
|
||||
// admin check internally, assume either admin or all belong to user
|
||||
storage.sendGroupUpdateDeleteMessage(
|
||||
groupSessionId = recipient.address.serialize(),
|
||||
groupManager.requestMessageDeletion(
|
||||
groupId = AccountId(publicKey),
|
||||
messageHashes = listOf(serverHash)
|
||||
).await()
|
||||
)
|
||||
} else {
|
||||
SnodeAPI.deleteMessage(
|
||||
publicKey = publicKey,
|
||||
|
@ -177,8 +177,6 @@ interface StorageProtocol {
|
||||
fun insertGroupInfoLeaving(closedGroup: AccountId): Long?
|
||||
fun insertGroupInviteControlMessage(sentTimestamp: Long, senderPublicKey: String, closedGroup: AccountId, groupName: String): Long?
|
||||
fun updateGroupInfoChange(messageId: Long, newType: UpdateMessageData.Kind)
|
||||
fun handleMemberLeftNotification(message: GroupUpdated, closedGroupId: AccountId)
|
||||
fun sendGroupUpdateDeleteMessage(groupSessionId: String, messageHashes: List<String>): Promise<Unit, Exception>
|
||||
|
||||
// Groups
|
||||
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
||||
|
@ -3,6 +3,8 @@ package org.session.libsession.messaging.groups
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.messages.control.GroupUpdated
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
|
||||
import org.session.libsignal.utilities.AccountId
|
||||
|
||||
/**
|
||||
@ -57,4 +59,13 @@ interface GroupManagerV2 {
|
||||
suspend fun handleKicked(groupId: AccountId)
|
||||
|
||||
suspend fun setName(groupId: AccountId, newName: String)
|
||||
|
||||
suspend fun requestMessageDeletion(groupId: AccountId, messageHashes: List<String>)
|
||||
|
||||
suspend fun handleDeleteMemberContent(
|
||||
groupId: AccountId,
|
||||
deleteMemberContent: GroupUpdateDeleteMemberContentMessage,
|
||||
sender: AccountId,
|
||||
senderIsVerifiedAdmin: Boolean,
|
||||
)
|
||||
}
|
@ -6,6 +6,7 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import network.loki.messenger.libsession_util.util.Sodium
|
||||
import network.loki.messenger.libsession_util.util.afterSend
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.database.userAuth
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
@ -585,45 +586,31 @@ private fun MessageReceiver.handleGroupUpdated(message: GroupUpdated, closedGrou
|
||||
}
|
||||
|
||||
private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: AccountId) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val deleteMemberContent = message.inner.deleteMemberContent
|
||||
val adminSig = if (deleteMemberContent.hasAdminSignature()) deleteMemberContent.adminSignature.toByteArray()!! else byteArrayOf()
|
||||
|
||||
val memberIds = deleteMemberContent.memberSessionIdsList
|
||||
val hashes = deleteMemberContent.messageHashesList
|
||||
val threadId = storage.getThreadId(Address.fromSerialized(closedGroup.hexString))!!
|
||||
|
||||
val messageToValidate = buildDeleteMemberContentSignature(
|
||||
memberIds = memberIds.asSequence().map(::AccountId).asIterable(),
|
||||
messageHashes = hashes,
|
||||
timestamp = message.sentTimestamp!!
|
||||
)
|
||||
|
||||
if (hashes.isNotEmpty()) {
|
||||
// Delete all hashes conditionally
|
||||
if (storage.ensureMessageHashesAreSender(hashes.toSet(), message.sender!!, closedGroup.hexString)) {
|
||||
// ensure that all message hashes belong to user
|
||||
// storage delete
|
||||
storage.deleteMessagesByHash(threadId, hashes)
|
||||
} else {
|
||||
// otherwise assert a valid admin sig exists
|
||||
val hasValidAdminSignature = adminSig.isNotEmpty() && runCatching {
|
||||
verifyAdminSignature(
|
||||
closedGroup,
|
||||
adminSig,
|
||||
messageToValidate
|
||||
buildDeleteMemberContentSignature(
|
||||
memberIds = deleteMemberContent.memberSessionIdsList.asSequence().map(::AccountId).asIterable(),
|
||||
messageHashes = deleteMemberContent.messageHashesList,
|
||||
timestamp = message.sentTimestamp!!,
|
||||
)
|
||||
// storage delete
|
||||
storage.deleteMessagesByHash(threadId, hashes)
|
||||
}
|
||||
} else if (memberIds.isNotEmpty()) {
|
||||
// Delete all from member Ids, and require admin sig?
|
||||
verifyAdminSignature(
|
||||
closedGroup,
|
||||
adminSig,
|
||||
messageToValidate
|
||||
)
|
||||
for (member in memberIds) {
|
||||
storage.deleteMessagesByUser(threadId, member)
|
||||
}.isSuccess
|
||||
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
MessagingModuleConfiguration.shared.groupManagerV2.handleDeleteMemberContent(
|
||||
groupId = closedGroup,
|
||||
deleteMemberContent = deleteMemberContent,
|
||||
sender = AccountId(message.sender!!),
|
||||
senderIsVerifiedAdmin = hasValidAdminSignature
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("GroupUpdated", "Failed to handle delete member content", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -649,7 +636,7 @@ private fun handleMemberLeft(message: GroupUpdated, closedGroup: AccountId) {
|
||||
}
|
||||
|
||||
private fun handleMemberLeftNotification(message: GroupUpdated, closedGroup: AccountId) {
|
||||
MessagingModuleConfiguration.shared.storage.handleMemberLeftNotification(message, closedGroup)
|
||||
MessagingModuleConfiguration.shared.storage.insertGroupInfoChange(message, closedGroup)
|
||||
}
|
||||
|
||||
private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: AccountId) {
|
||||
|
Loading…
Reference in New Issue
Block a user