More deletion logic

Hndling unsend request retrieval as per figma docs
pull/1518/head
ThomasSession 6 months ago
parent 792a4ca74d
commit 2e14b0e94d

@ -199,7 +199,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
}
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
@ -216,10 +215,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) }
}
override fun markMessageAsDeleted(timestamp: Long, author: String, displayedMessage: String): Long? {
override fun markMessageAsDeleted(timestamp: Long, author: String, displayedMessage: String) {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = Address.fromSerialized(author)
val message = database.getMessageFor(timestamp, address) ?: return null
val message = database.getMessageFor(timestamp, address) ?: return
markMessagesAsDeleted(
messages = listOf(MarkAsDeletedMessage(
@ -229,8 +228,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
isSms = !message.isMms,
displayedMessage = displayedMessage
)
return message.id
}
override fun markMessagesAsDeleted(

@ -31,6 +31,8 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsession.utilities.recipients.getType
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
@ -210,24 +212,26 @@ class ConversationViewModel(
viewModelScope.launch(Dispatchers.IO) {
val allSentByCurrentUser = messages.all { it.isOutgoing }
val conversationType = conversation.getType()
// hashes are required if wanting to delete messages from the 'storage server' - they are not required for communities
val canDeleteForEveryone = conversation.isCommunityRecipient || messages.all {
val canDeleteForEveryone = conversationType == MessageType.COMMUNITY || messages.all {
lokiMessageDb.getMessageServerHash(
it.id,
it.isMms
) != null
}
// Determining is the current user is an admin will depend on the kind of conversation we are in
val isAdmin = when {
//todo GROUPS V2 add logic where code is commented to determine if user is an admin - CAREFUL in the current old code:
// isClosedGroup refers to the existing legacy groups.
// With the groupsV2 changes, isClosedGroup refers to groupsV2 and isLegacyClosedGroup is a new property to refer to old groups
val isAdmin = when(conversationType) {
// for Groups V2
// conversation: check if it is a GroupsV2 conversation - then check if user is an admin
MessageType.GROUPS_V2 -> {
//todo GROUPS V2 add logic where code is commented to determine if user is an admin
false // FANCHAO - properly set up admin for groups v2 here
}
// for legacy groups, check if the user created the group
conversation.isClosedGroupRecipient -> { //todo GROUPS V2 this property will change for groups v2. Check for legacyGroup here
MessageType.LEGACY_GROUP -> {
// for legacy groups, we check if the current user is the one who created the group
run {
val localUserAddress =
@ -238,7 +242,7 @@ class ConversationViewModel(
}
// for communities the the `isUserModerator` field
conversation.isCommunityRecipient -> isUserCommunityManager()
MessageType.COMMUNITY -> isUserCommunityManager()
// false in other cases
else -> false
@ -250,7 +254,7 @@ class ConversationViewModel(
// 3- Delete on device only - Used otherwise
when {
// the conversation is a note to self
conversation.isLocalNumber -> {
conversationType == MessageType.NOTE_TO_SELF -> {
_dialogsState.update {
it.copy(deleteAllDevices = DeleteForEveryoneDialogData(
messages = messages,
@ -291,14 +295,6 @@ class ConversationViewModel(
}
}
/**
* This will delete these messages from the db
* Not to be confused with 'marking messages as deleted'
*/
fun deleteMessages(messages: Set<MessageRecord>, threadId: Long) {
repository.deleteMessages(messages, threadId)
}
/**
* This will mark the messages as deleted, locally only.
* Attachments and other related data will be removed from the db,
@ -306,7 +302,7 @@ class ConversationViewModel(
* Instead they will appear as a special type of message
* that says something like "This message was deleted"
*/
private fun markAsDeletedLocally(messages: Set<MessageRecord>) {
fun markAsDeletedLocally(messages: Set<MessageRecord>) {
// make sure to stop audio messages, if any
messages.filterIsInstance<MmsMessageRecord>()
.mapNotNull { it.slideDeck.audioSlide }
@ -354,22 +350,18 @@ class ConversationViewModel(
}
private fun markAsDeletedForEveryoneNoteToSelf(data: DeleteForEveryoneDialogData){
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
// delete remotely
try {
//todo DELETION need to delete remotely for note to self
repository.deleteCommunityMessagesRemotely(threadId, data.messages)
repository.deleteNoteToSelfMessagesRemotely(threadId, recipient!!, data.messages)
//todo DELETION send unsendRequest to own swarm
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = data.messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// When this is done we simply need to remove the message locally (leave nothing behind)
repository.deleteMessages(messages = data.messages, threadId = threadId)
// show confirmation toast
withContext(Dispatchers.Main) {
@ -457,9 +449,47 @@ class ConversationViewModel(
}
private fun markAsDeletedForEveryoneLegacyGroup(messages: Set<MessageRecord>){
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
}
viewModelScope.launch(Dispatchers.IO) {
// delete remotely
try {
repository.deleteLegacyGroupMessagesRemotely(recipient!!, messages)
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// show confirmation toast
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
messages.count(),
messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${messages} ")
// failed to delete - show a toast and get back on the modal
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
messages.size,
messages.size
), Toast.LENGTH_SHORT
).show()
}
}
}
}
private fun markAsDeletedForEveryoneGroupsV2(data: DeleteForEveryoneDialogData){
viewModelScope.launch(Dispatchers.IO) {

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import network.loki.messenger.R
import java.security.MessageDigest
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
@ -74,6 +73,8 @@ import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Co
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.Recipient.DisappearingState
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsession.utilities.recipients.getType
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
@ -758,6 +759,12 @@ open class Storage(
return database.getMessageFor(timestamp, address)?.run { getId() to isMms }
}
override fun getMessageType(timestamp: Long, author: String): MessageType? {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = fromSerialized(author)
return database.getMessageFor(timestamp, address)?.individualRecipient?.getType()
}
override fun updateSentTimestamp(
messageID: Long,
isMms: Boolean,

@ -69,6 +69,16 @@ interface ConversationRepository {
recipient: Recipient,
messages: Set<MessageRecord>
)
suspend fun deleteNoteToSelfMessagesRemotely(
threadId: Long,
recipient: Recipient,
messages: Set<MessageRecord>
)
suspend fun deleteLegacyGroupMessagesRemotely(
recipient: Recipient,
messages: Set<MessageRecord>
)
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit>
@ -270,6 +280,45 @@ class DefaultConversationRepository @Inject constructor(
}
}
override suspend fun deleteLegacyGroupMessagesRemotely(
recipient: Recipient,
messages: Set<MessageRecord>
) {
if (recipient.isClosedGroupRecipient) {
val publicKey = recipient.address
messages.forEach { message ->
// send an UnsendRequest to group's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, publicKey)
}
}
}
}
override suspend fun deleteNoteToSelfMessagesRemotely(
threadId: Long,
recipient: Recipient,
messages: Set<MessageRecord>
) {
// delete the messages remotely
val publicKey = recipient.address.serialize()
val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) }
messages.forEach { message ->
// delete from swarm
messageDataProvider.getServerHashForMessage(message.id, message.isMms)
?.let { serverHash ->
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
}
// send an UnsendRequest to user's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
userAddress?.let { MessageSender.send(unsendRequest, it) }
}
}
}
/* override suspend fun markAsDeletedForEveryone(
threadId: Long,
recipient: Recipient,
@ -279,35 +328,6 @@ class DefaultConversationRepository @Inject constructor(
MessageSender.send(unsendRequest, recipient.address)
}
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) {
val serverId = lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
continuation.resume(Result.success(Unit))
}.fail { error ->
Log.w("TAG", "Call to OpenGroupApi.deleteForEveryone failed - attempting to resume..")
continuation.resume(Result.failure(error))
}
}
// If the server ID is null then this message is stuck in limbo (it has likely been
// deleted remotely but that deletion did not occur locally) - so we'll delete the
// message locally to clean up.
if (serverId == null) {
Log.w("ConversationRepository","Found community message without a server ID - deleting locally.")
// Caution: The bool returned from `deleteMessage` is NOT "Was the message
// successfully deleted?" - it is "Was the thread itself also deleted because
// removing that message resulted in an empty thread?".
if (message.isMms) {
mmsDb.deleteMessage(message.id)
} else {
smsDb.deleteMessage(message.id)
}
}
}
else // If this thread is NOT in a Community
{
messageDataProvider.deleteMessage(message.id, !message.isMms)

@ -34,7 +34,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
private lateinit var messageRecord: MessageRecord
private val viewModel: ConversationViewModel by lazy {
ConversationViewModel(threadId, edKeyPair, repository, storage, mock())
ConversationViewModel(threadId, edKeyPair, mock(), repository, storage, mock(), mock(), mock())
}
@Before
@ -86,28 +86,6 @@ class ConversationViewModelTest: BaseViewModelTest() {
verify(repository).setBlocked(recipient, false)
}
@Test
fun `should delete locally`() {
val messages = mock<Set<MessageRecord>>()
viewModel.deleteMessages(messages, threadId)
verify(repository).deleteMessages(messages, threadId)
}
@Test
fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest {
val message = mock<MessageRecord>()
val error = Throwable()
whenever(repository.markAsDeletedForEveryone(anyLong(), any(), any()))
.thenReturn(Result.failure(error))
viewModel.markAsDeletedForEveryone(message)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
}
@Test
fun `should emit error message on ban user failure`() = runBlockingTest {
val error = Throwable()

@ -24,7 +24,7 @@ interface MessageDataProvider {
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>>
fun deleteMessage(messageID: Long, isSms: Boolean)
fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean)
fun markMessageAsDeleted(timestamp: Long, author: String, displayedMessage: String): Long?
fun markMessageAsDeleted(timestamp: Long, author: String, displayedMessage: String)
fun markMessagesAsDeleted(messages: List<MarkAsDeletedMessage>, isSms: Boolean, displayedMessage: String)
fun getServerHashForMessage(messageID: Long, mms: Boolean): String?
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?

@ -30,6 +30,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
@ -115,6 +116,7 @@ interface StorageProtocol {
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
fun getMessageIdInDatabase(timestamp: Long, author: String): Pair<Long, Boolean>? // TODO: This is a weird name
fun getMessageType(timestamp: Long, author: String): MessageType?
fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long)
fun markAsResyncing(timestamp: Long, author: String)
fun markAsSyncing(timestamp: Long, author: String)

@ -45,6 +45,7 @@ import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
@ -249,32 +250,63 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
}
fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? {
//todo DELETION modify unsend request validation
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
if (message.sender != message.author && (message.sender != userPublicKey && userPublicKey != null)) { return null }
val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage
val isLegacyGroupAdmin: Boolean = message.groupPublicKey?.let { key ->
var admin = false
val groupID = doubleEncodeGroupID(key)
val group = storage.getGroup(groupID)
if(group != null) {
admin = group.admins.map { it.toString() }.contains(message.sender)
}
admin
} ?: false
// First we need to determine the validity of the UnsendRequest
// It is valid if:
val requestIsValid = message.sender == message.author || // the sender is the author of the message
message.author == userPublicKey || // the sender is the current user
isLegacyGroupAdmin // sender is an admin of legacy group
if (!requestIsValid) { return null }
val context = MessagingModuleConfiguration.shared.context
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val timestamp = message.timestamp ?: return null
val author = message.author ?: return null
val (messageIdToDelete, mms) = storage.getMessageIdInDatabase(timestamp, author) ?: return null
messageDataProvider.getServerHashForMessage(messageIdToDelete, mms)?.let { serverHash ->
GlobalScope.launch(Dispatchers.IO) { // using GlobalScope as we are slowly migrating to coroutines but we can't migrate everything at once
try {
SnodeAPI.deleteMessage(author, listOf(serverHash))
}catch (e: Exception){}
val messageType = storage.getMessageType(timestamp, author) ?: return null
Log.d("", "*** message type: $messageType")
// send a /delete rquest for 1on1 messages
if(messageType == MessageType.ONE_ON_ONE) {
messageDataProvider.getServerHashForMessage(messageIdToDelete, mms)?.let { serverHash ->
GlobalScope.launch(Dispatchers.IO) { // using GlobalScope as we are slowly migrating to coroutines but we can't migrate everything at once
try {
SnodeAPI.deleteMessage(author, listOf(serverHash))
} catch (e: Exception) {
}
}
}
}
val deletedMessageId = messageDataProvider.markMessageAsDeleted(
// the message is marked as deleted locally
// except for 'note to self' where the message is completely deleted
if(messageType == MessageType.NOTE_TO_SELF){
messageDataProvider.deleteMessage(messageIdToDelete, !mms)
} else {
messageDataProvider.markMessageAsDeleted(
timestamp = timestamp,
author = author,
displayedMessage = context.getString(R.string.deleteMessageDeletedGlobally)
displayedMessage = context.getString(R.string.deleteMessageDeletedGlobally)
)
}
if (!messageDataProvider.isOutgoingMessage(timestamp)) {
SSKEnvironment.shared.notificationManager.updateNotification(context)
}
return deletedMessageId
return messageIdToDelete
}
fun handleMessageRequestResponse(message: MessageRequestResponse) {

@ -0,0 +1,14 @@
package org.session.libsession.utilities.recipients
enum class MessageType {
ONE_ON_ONE, LEGACY_GROUP, GROUPS_V2, NOTE_TO_SELF, COMMUNITY
}
fun Recipient.getType(): MessageType =
when{
isCommunityRecipient -> MessageType.COMMUNITY
isLocalNumber -> MessageType.NOTE_TO_SELF
isClosedGroupRecipient -> MessageType.LEGACY_GROUP //todo GROUPS V2 this property will change for groups v2. Check for legacyGroup here
//isXXXXX -> RecipientType.GROUPS_V2 //todo GROUPS V2 this property will change for groups v2. Check for legacyGroup here
else -> MessageType.ONE_ON_ONE
}
Loading…
Cancel
Save