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>) {
|
override fun setServerCapabilities(server: String, capabilities: List<String>) {
|
||||||
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
|
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
|
||||||
}
|
}
|
||||||
|
@ -1042,6 +1042,111 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
storage.insertGroupInfoChange(message, groupId)
|
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) {
|
private fun BatchResponse.requireAllRequestsSuccessful(errorMessage: String) {
|
||||||
val firstError = this.results.firstOrNull { it.code != 200 }
|
val firstError = this.results.firstOrNull { it.code != 200 }
|
||||||
|
@ -242,10 +242,10 @@ class DefaultConversationRepository @Inject constructor(
|
|||||||
|
|
||||||
if (recipient.isClosedGroupV2Recipient) {
|
if (recipient.isClosedGroupV2Recipient) {
|
||||||
// admin check internally, assume either admin or all belong to user
|
// admin check internally, assume either admin or all belong to user
|
||||||
storage.sendGroupUpdateDeleteMessage(
|
groupManager.requestMessageDeletion(
|
||||||
groupSessionId = recipient.address.serialize(),
|
groupId = AccountId(publicKey),
|
||||||
messageHashes = listOf(serverHash)
|
messageHashes = listOf(serverHash)
|
||||||
).await()
|
)
|
||||||
} else {
|
} else {
|
||||||
SnodeAPI.deleteMessage(
|
SnodeAPI.deleteMessage(
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
|
@ -177,8 +177,6 @@ interface StorageProtocol {
|
|||||||
fun insertGroupInfoLeaving(closedGroup: AccountId): Long?
|
fun insertGroupInfoLeaving(closedGroup: AccountId): Long?
|
||||||
fun insertGroupInviteControlMessage(sentTimestamp: Long, senderPublicKey: String, closedGroup: AccountId, groupName: String): Long?
|
fun insertGroupInviteControlMessage(sentTimestamp: Long, senderPublicKey: String, closedGroup: AccountId, groupName: String): Long?
|
||||||
fun updateGroupInfoChange(messageId: Long, newType: UpdateMessageData.Kind)
|
fun updateGroupInfoChange(messageId: Long, newType: UpdateMessageData.Kind)
|
||||||
fun handleMemberLeftNotification(message: GroupUpdated, closedGroupId: AccountId)
|
|
||||||
fun sendGroupUpdateDeleteMessage(groupSessionId: String, messageHashes: List<String>): Promise<Unit, Exception>
|
|
||||||
|
|
||||||
// Groups
|
// Groups
|
||||||
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
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.contacts.Contact
|
||||||
import org.session.libsession.messaging.messages.control.GroupUpdated
|
import org.session.libsession.messaging.messages.control.GroupUpdated
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
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
|
import org.session.libsignal.utilities.AccountId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,4 +59,13 @@ interface GroupManagerV2 {
|
|||||||
suspend fun handleKicked(groupId: AccountId)
|
suspend fun handleKicked(groupId: AccountId)
|
||||||
|
|
||||||
suspend fun setName(groupId: AccountId, newName: String)
|
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 kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import network.loki.messenger.libsession_util.util.Sodium
|
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.avatars.AvatarHelper
|
||||||
import org.session.libsession.database.userAuth
|
import org.session.libsession.database.userAuth
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
@ -585,45 +586,31 @@ private fun MessageReceiver.handleGroupUpdated(message: GroupUpdated, closedGrou
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: AccountId) {
|
private fun handleDeleteMemberContent(message: GroupUpdated, closedGroup: AccountId) {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val deleteMemberContent = message.inner.deleteMemberContent
|
val deleteMemberContent = message.inner.deleteMemberContent
|
||||||
val adminSig = if (deleteMemberContent.hasAdminSignature()) deleteMemberContent.adminSignature.toByteArray()!! else byteArrayOf()
|
val adminSig = if (deleteMemberContent.hasAdminSignature()) deleteMemberContent.adminSignature.toByteArray()!! else byteArrayOf()
|
||||||
|
|
||||||
val memberIds = deleteMemberContent.memberSessionIdsList
|
val hasValidAdminSignature = adminSig.isNotEmpty() && runCatching {
|
||||||
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
|
|
||||||
verifyAdminSignature(
|
|
||||||
closedGroup,
|
|
||||||
adminSig,
|
|
||||||
messageToValidate
|
|
||||||
)
|
|
||||||
// storage delete
|
|
||||||
storage.deleteMessagesByHash(threadId, hashes)
|
|
||||||
}
|
|
||||||
} else if (memberIds.isNotEmpty()) {
|
|
||||||
// Delete all from member Ids, and require admin sig?
|
|
||||||
verifyAdminSignature(
|
verifyAdminSignature(
|
||||||
closedGroup,
|
closedGroup,
|
||||||
adminSig,
|
adminSig,
|
||||||
messageToValidate
|
buildDeleteMemberContentSignature(
|
||||||
|
memberIds = deleteMemberContent.memberSessionIdsList.asSequence().map(::AccountId).asIterable(),
|
||||||
|
messageHashes = deleteMemberContent.messageHashesList,
|
||||||
|
timestamp = message.sentTimestamp!!,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for (member in memberIds) {
|
}.isSuccess
|
||||||
storage.deleteMessagesByUser(threadId, member)
|
|
||||||
|
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) {
|
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) {
|
private fun handleGroupInfoChange(message: GroupUpdated, closedGroup: AccountId) {
|
||||||
|
Loading…
Reference in New Issue
Block a user