diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 5062783dc2..2f8bd1096f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -134,7 +134,7 @@ import network.loki.messenger.libsession_util.UserProfile; * @author Moxie Marlinspike */ @HiltAndroidApp -public class ApplicationContext extends Application implements DefaultLifecycleObserver, ConfigFactoryUpdateListener, Toaster { +public class ApplicationContext extends Application implements DefaultLifecycleObserver, Toaster { public static final String PREFERENCES_NAME = "SecureSMS-Preferences"; @@ -214,15 +214,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO return this.persistentLogger; } - @Override - public void notifyUpdates(@NotNull Config forConfigObject, long messageTimestamp) { - // forward to the config factory / storage ig - if (forConfigObject instanceof UserProfile && !textSecurePreferences.getConfigurationMessageSynced()) { - textSecurePreferences.setConfigurationMessageSynced(true); - } - storage.notifyConfigUpdates(forConfigObject, messageTimestamp); - } - @Override public void toast(@StringRes int stringRes, int toastLength, @NonNull Map parameters) { Phrase builder = Phrase.from(this, stringRes); @@ -510,7 +501,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO Log.d("Loki", "Failed to delete database."); return false; } - configFactory.keyPairChanged(); + configFactory.clearAll(); return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt index 489d82a390..b16e4e37e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessages.kt @@ -4,8 +4,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import network.loki.messenger.R import network.loki.messenger.libsession_util.util.ExpiryMode -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.messages.Destination +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.sending_receiving.MessageSender @@ -20,8 +19,6 @@ import org.session.libsession.utilities.getExpirationTypeDisplayValue import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.ui.getSubbedCharSequence -import org.thoughtcrime.securesms.ui.getSubbedString -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -29,10 +26,11 @@ class DisappearingMessages @Inject constructor( @ApplicationContext private val context: Context, private val textSecurePreferences: TextSecurePreferences, private val messageExpirationManager: MessageExpirationManagerProtocol, + private val storage: StorageProtocol ) { fun set(threadId: Long, address: Address, mode: ExpiryMode, isGroup: Boolean) { val expiryChangeTimestampMs = SnodeAPI.nowWithOffset - MessagingModuleConfiguration.shared.storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs)) + storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs)) val message = ExpirationTimerUpdate(isGroup = isGroup).apply { expiryMode = mode @@ -44,11 +42,6 @@ class DisappearingMessages @Inject constructor( messageExpirationManager.insertExpirationTimerMessage(message) MessageSender.send(message, address) - if (address.isClosedGroupV2) { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(Destination.from(address)) - } else { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) - } } fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt index 21405b26c5..4dce23d177 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt @@ -11,17 +11,20 @@ import android.widget.Toast import androidx.fragment.app.DialogFragment import com.squareup.phrase.Phrase import network.loki.messenger.R -import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.groups.OpenGroupManager -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import javax.inject.Inject /** Shown upon tapping an open group invitation. */ class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() { + @Inject + lateinit var storage: StorageProtocol + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { title(resources.getString(R.string.communityJoin)) val explanation = Phrase.from(context, R.string.communityJoinDescription).put(COMMUNITY_NAME_KEY, name).format() @@ -43,8 +46,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : D ThreadUtils.queue { try { openGroup.apply { OpenGroupManager.add(server, room, serverPublicKey, activity) } - MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room) - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity) + storage.onOpenGroupAdded(openGroup.server, openGroup.room) } catch (e: Exception) { Toast.makeText(activity, R.string.communityErrorDescription, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index a7b89ae070..2c07ea03bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -93,7 +93,8 @@ object ConversationMenuHelper { // Groups v2 menu if (thread.isClosedGroupV2Recipient) { - if (configFactory.userGroups?.getClosedGroup(thread.address.serialize())?.hasAdminKey() == true) { + val hasAdminKey = configFactory.withUserConfigs { it.userGroups.getClosedGroup(thread.address.serialize())?.hasAdminKey() } + if (hasAdminKey == true) { inflater.inflate(R.menu.menu_conversation_groups_v2_admin, menu) } @@ -346,15 +347,15 @@ object ConversationMenuHelper { thread.isClosedGroupV2Recipient -> { val accountId = AccountId(thread.address.serialize()) - val group = configFactory.userGroups?.getClosedGroup(accountId.hexString) ?: return - val (name, isAdmin) = configFactory.getGroupInfoConfig(accountId)?.use { - it.getName() to group.hasAdminKey() - } ?: return + val group = configFactory.withUserConfigs { it.userGroups.getClosedGroup(accountId.hexString) } ?: return + val name = configFactory.withGroupConfigs(accountId) { + it.groupInfo.getName() + } confirmAndLeaveClosedGroup( context = context, groupName = name, - isAdmin = isAdmin, + isAdmin = group.hasAdminKey(), threadID = threadID, storage = storage, doLeave = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt index 71aca6c948..5006da99f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ConfigDatabase.kt @@ -9,6 +9,8 @@ import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +typealias ConfigVariant = String + class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) { companion object { @@ -25,12 +27,17 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?" private const val VARIANT_IN_AND_PUBKEY_WHERE = "$VARIANT in (?) AND $PUBKEY = ?" - val KEYS_VARIANT = SharedConfigMessage.Kind.ENCRYPTION_KEYS.name - val INFO_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_INFO.name - val MEMBER_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_MEMBERS.name + val CONTACTS_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CONTACTS.name + val USER_GROUPS_VARIANT: ConfigVariant = SharedConfigMessage.Kind.GROUPS.name + val USER_PROFILE_VARIANT: ConfigVariant = SharedConfigMessage.Kind.USER_PROFILE.name + val CONVO_INFO_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name + + val KEYS_VARIANT: ConfigVariant = SharedConfigMessage.Kind.ENCRYPTION_KEYS.name + val INFO_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CLOSED_GROUP_INFO.name + val MEMBER_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CLOSED_GROUP_MEMBERS.name } - fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) { + fun storeConfig(variant: ConfigVariant, publicKey: String, data: ByteArray, timestamp: Long) { val db = writableDatabase val contentValues = contentValuesOf( VARIANT to variant, @@ -84,7 +91,7 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co } } - fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? { + fun retrieveConfigAndHashes(variant: ConfigVariant, publicKey: String): ByteArray? { val db = readableDatabase val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) return query?.use { cursor -> @@ -94,7 +101,7 @@ class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(co } } - fun retrieveConfigLastUpdateTimestamp(variant: String, publicKey: String): Long { + fun retrieveConfigLastUpdateTimestamp(variant: ConfigVariant, publicKey: String): Long { val db = readableDatabase val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) if (cursor == null) return 0 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt index bbc3374072..e83c464c7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionJobDatabase.kt @@ -8,7 +8,6 @@ import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob -import org.session.libsession.messaging.jobs.InviteContactsJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageSendJob @@ -79,13 +78,6 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa return result.firstOrNull { job -> job.attachmentID == attachmentID } } - fun getGroupInviteJob(groupSessionId: String, memberSessionId: String): InviteContactsJob? { - val database = databaseHelper.readableDatabase - return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(InviteContactsJob.KEY)) { cursor -> - jobFromCursor(cursor) as? InviteContactsJob - }.firstOrNull { it != null && it.groupSessionId == groupSessionId && it.memberSessionIds.contains(memberSessionId) } - } - fun getMessageSendJob(messageSendJobID: String): MessageSendJob? { val database = databaseHelper.readableDatabase return database.get(sessionJobTable, "$jobID = ? AND $jobType = ?", arrayOf( messageSendJobID, MessageSendJob.KEY )) { cursor -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 6520b3c5c1..6523b4444e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -2,50 +2,39 @@ package org.thoughtcrime.securesms.database import android.content.Context import android.net.Uri -import com.google.protobuf.ByteString import com.goterl.lazysodium.utils.KeyPair -import network.loki.messenger.libsession_util.Config +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import java.security.MessageDigest import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE -import network.loki.messenger.libsession_util.Contacts -import network.loki.messenger.libsession_util.ConversationVolatileConfig -import network.loki.messenger.libsession_util.GroupInfoConfig -import network.loki.messenger.libsession_util.GroupKeysConfig -import network.loki.messenger.libsession_util.GroupMembersConfig -import network.loki.messenger.libsession_util.UserGroupsConfig -import network.loki.messenger.libsession_util.UserProfile +import network.loki.messenger.libsession_util.ReadableContacts +import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig +import network.loki.messenger.libsession_util.ReadableGroupInfoConfig +import network.loki.messenger.libsession_util.ReadableUserGroupsConfig +import network.loki.messenger.libsession_util.ReadableUserProfile import network.loki.messenger.libsession_util.util.BaseCommunityInfo import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupInfo -import network.loki.messenger.libsession_util.util.Sodium import network.loki.messenger.libsession_util.util.UserPic import network.loki.messenger.libsession_util.util.afterSend -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.functional.map import org.session.libsession.avatars.AvatarHelper import org.session.libsession.database.StorageProtocol -import org.session.libsession.database.userAuth import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.BackgroundGroupAddJob -import org.session.libsession.messaging.jobs.ConfigurationSyncJob -import org.session.libsession.messaging.jobs.ConfigurationSyncJob.Companion.messageInformation import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob -import org.session.libsession.messaging.jobs.InviteContactsJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob -import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ConfigurationMessage @@ -65,7 +54,6 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.GroupMember import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage @@ -75,17 +63,11 @@ import org.session.libsession.messaging.sending_receiving.pollers.LegacyClosedGr import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.UpdateMessageData -import org.session.libsession.snode.GroupSubAccountSwarmAuth import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.snode.OwnedSwarmAuth -import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.SnodeAPI.buildAuthenticatedDeleteBatchInfo -import org.session.libsession.snode.SnodeAPI.buildAuthenticatedStoreBatchInfo -import org.session.libsession.snode.SnodeMessage -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.ProfileKeyUtil @@ -94,31 +76,20 @@ 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.withGroupConfigsOrNull import org.session.libsignal.crypto.ecc.DjbECPrivateKey import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup -import org.session.libsignal.protos.SignalServiceProtos.DataMessage -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInfoChangeMessage -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteResponseMessage -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMemberChangeMessage -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.crypto.KeyPairUtilities -import org.session.libsession.messaging.utilities.MessageAuthentication.buildDeleteMemberContentSignature -import org.session.libsession.messaging.utilities.MessageAuthentication.buildInfoChangeVerifier -import org.session.libsession.messaging.utilities.MessageAuthentication.buildMemberChangeSignature import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord @@ -130,7 +101,6 @@ import org.thoughtcrime.securesms.groups.ClosedGroupManager import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.mms.PartAuthority -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.SessionMetaProtocol import network.loki.messenger.libsession_util.util.Contact as LibSessionContact import network.loki.messenger.libsession_util.util.GroupMember as LibSessionGroupMember @@ -145,33 +115,40 @@ open class Storage( ) : Database(context, helper), StorageProtocol, ThreadDatabase.ConversationThreadUpdateListener { + init { + observeConfigUpdates() + } + override fun threadCreated(address: Address, threadId: Long) { val localUserAddress = getUserPublicKey() ?: return if (!getRecipientApproved(address) && localUserAddress != address.serialize()) return // don't store unapproved / message requests - val volatile = configFactory.convoVolatile ?: return if (address.isGroup) { - val groups = configFactory.userGroups ?: return when { address.isLegacyClosedGroup -> { val accountId = GroupUtil.doubleDecodeGroupId(address.serialize()) val closedGroup = getGroup(address.toGroupString()) if (closedGroup != null && closedGroup.isActive) { - val legacyGroup = groups.getOrConstructLegacyGroupInfo(accountId) - groups.set(legacyGroup) - val newVolatileParams = volatile.getOrConstructLegacyGroup(accountId).copy( - lastRead = SnodeAPI.nowWithOffset, - ) - volatile.set(newVolatileParams) + configFactory.withMutableUserConfigs { configs -> + val legacyGroup = configs.userGroups.getOrConstructLegacyGroupInfo(accountId) + configs.userGroups.set(legacyGroup) + val newVolatileParams = configs.convoInfoVolatile.getOrConstructLegacyGroup(accountId).copy( + lastRead = SnodeAPI.nowWithOffset, + ) + configs.convoInfoVolatile.set(newVolatileParams) + } + } } address.isClosedGroupV2 -> { - val AccountId = address.serialize() - groups.getClosedGroup(AccountId) ?: return Log.d("Closed group doesn't exist locally", NullPointerException()) - val conversation = Conversation.ClosedGroup( - AccountId, 0, false - ) - volatile.set(conversation) + configFactory.withMutableUserConfigs { configs -> + val accountId = address.serialize() + configs.userGroups.getClosedGroup(accountId) + ?: return@withMutableUserConfigs Log.d("Closed group doesn't exist locally", NullPointerException()) + + configs.convoInfoVolatile.getOrConstructClosedGroup(accountId) + } + } address.isCommunity -> { // these should be added on the group join / group info fetch @@ -183,49 +160,53 @@ open class Storage( if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return // don't update our own address into the contacts DB if (getUserPublicKey() != address.serialize()) { - val contacts = configFactory.contacts ?: return - contacts.upsertContact(address.serialize()) { - priority = PRIORITY_VISIBLE + configFactory.withMutableUserConfigs { configs -> + configs.contacts.upsertContact(address.serialize()) { + priority = PRIORITY_VISIBLE + } } } else { - val userProfile = configFactory.user ?: return - userProfile.setNtsPriority(PRIORITY_VISIBLE) + configFactory.withMutableUserConfigs { configs -> + configs.userProfile.setNtsPriority(PRIORITY_VISIBLE) + } + DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true) } - val newVolatileParams = volatile.getOrConstructOneToOne(address.serialize()) - volatile.set(newVolatileParams) + + configFactory.withMutableUserConfigs { configs -> + configs.convoInfoVolatile.getOrConstructOneToOne(address.serialize()) + } } } override fun threadDeleted(address: Address, threadId: Long) { - val volatile = configFactory.convoVolatile ?: return - if (address.isGroup) { - val groups = configFactory.userGroups ?: return - if (address.isLegacyClosedGroup) { - val accountId = GroupUtil.doubleDecodeGroupId(address.serialize()) - volatile.eraseLegacyClosedGroup(accountId) - groups.eraseLegacyGroup(accountId) - } else if (address.isCommunity) { - // these should be removed in the group leave / handling new configs - Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere") - } else if (address.isClosedGroupV2) { - Log.w("Loki", "Thread delete called for closed group address, expecting to be handled elsewhere") - } - } else { - // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config - if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return - volatile.eraseOneToOne(address.serialize()) - if (getUserPublicKey() != address.serialize()) { - val contacts = configFactory.contacts ?: return - contacts.upsertContact(address.serialize()) { - priority = PRIORITY_HIDDEN + configFactory.withMutableUserConfigs { configs -> + if (address.isGroup) { + if (address.isLegacyClosedGroup) { + val accountId = GroupUtil.doubleDecodeGroupId(address.serialize()) + configs.convoInfoVolatile.eraseLegacyClosedGroup(accountId) + configs.userGroups.eraseLegacyGroup(accountId) + } else if (address.isCommunity) { + // these should be removed in the group leave / handling new configs + Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere") + } else if (address.isClosedGroupV2) { + Log.w("Loki", "Thread delete called for closed group address, expecting to be handled elsewhere") } } else { - val userProfile = configFactory.user ?: return - userProfile.setNtsPriority(PRIORITY_HIDDEN) + // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config + if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return@withMutableUserConfigs + configs.convoInfoVolatile.eraseOneToOne(address.serialize()) + if (getUserPublicKey() != address.serialize()) { + configs.contacts.upsertContact(address.serialize()) { + priority = PRIORITY_HIDDEN + } + } else { + configs.userProfile.setNtsPriority(PRIORITY_HIDDEN) + } } + + Unit } - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } override fun getUserPublicKey(): String? { @@ -347,22 +328,23 @@ open class Storage( // don't process configs for inbox recipients if (recipient.isOpenGroupInboxRecipient) return - configFactory.convoVolatile?.let { config -> + configFactory.withMutableUserConfigs { configs -> + val config = configs.convoInfoVolatile val convo = when { // recipient closed group recipient.isLegacyClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize())) recipient.isClosedGroupV2Recipient -> config.getOrConstructClosedGroup(recipient.address.serialize()) // recipient is open group recipient.isCommunityRecipient -> { - val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return + val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return@withMutableUserConfigs BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) -> config.getOrConstructCommunity(base, room, pubKey) - } ?: return + } ?: return@withMutableUserConfigs } // otherwise recipient is one to one recipient.isContactRecipient -> { // don't process non-standard account IDs though - if (AccountId(recipient.address.serialize()).prefix != IdPrefix.STANDARD) return + if (AccountId(recipient.address.serialize()).prefix != IdPrefix.STANDARD) return@withMutableUserConfigs config.getOrConstructOneToOne(recipient.address.serialize()) } else -> throw NullPointerException("Weren't expecting to have a convo with address ${recipient.address.serialize()}") @@ -373,7 +355,6 @@ open class Storage( notifyConversationListListeners() } config.set(convo) - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } } } @@ -522,12 +503,6 @@ open class Storage( return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room, imageId) } - override fun getConfigSyncJob(destination: Destination): Job? { - return DatabaseComponent.get(context).sessionJobDatabase().getAllJobs(ConfigurationSyncJob.KEY).values.firstOrNull { - (it as? ConfigurationSyncJob)?.destination == destination - } - } - override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) { val job = DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) ?: return JobQueue.shared.resumePendingSendMessage(job) @@ -547,10 +522,6 @@ open class Storage( return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id) } - override fun notifyConfigUpdates(forConfigObject: Config, messageTimestamp: Long) { - notifyUpdates(forConfigObject, messageTimestamp) - } - override fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean { return configFactory.conversationInConfig(publicKey, groupPublicKey, openGroupId, visibleOnly) } @@ -560,22 +531,36 @@ open class Storage( } override fun isCheckingCommunityRequests(): Boolean { - return configFactory.user?.getCommunityMessageRequests() == true + return configFactory.withUserConfigs { it.userProfile.getCommunityMessageRequests() } } - private fun notifyUpdates(forConfigObject: Config, messageTimestamp: Long) { - when (forConfigObject) { - is UserProfile -> updateUser(forConfigObject, messageTimestamp) - is Contacts -> updateContacts(forConfigObject, messageTimestamp) - is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject, messageTimestamp) - is UserGroupsConfig -> updateUserGroups(forConfigObject, messageTimestamp) - is GroupInfoConfig -> updateGroupInfo(forConfigObject, messageTimestamp) - is GroupKeysConfig -> updateGroupKeys(forConfigObject) - is GroupMembersConfig -> updateGroupMembers(forConfigObject) + private fun observeConfigUpdates() { + GlobalScope.launch { + configFactory.configUpdateNotifications + .collect { change -> + when (change) { + is ConfigUpdateNotification.GroupConfigsDeleted -> {} + is ConfigUpdateNotification.GroupConfigsUpdated -> { + configFactory.withGroupConfigs(change.groupId) { + updateGroup(it.groupInfo) + } + } + ConfigUpdateNotification.UserConfigs -> { + configFactory.withUserConfigs { + val messageTimestamp = SnodeAPI.nowWithOffset + + updateUser(it.userProfile, messageTimestamp) + updateContacts(it.contacts, messageTimestamp) + updateUserGroups(it.userGroups, messageTimestamp) + updateConvoVolatile(it.convoInfoVolatile) + } + } + } + } } } - private fun updateUser(userProfile: UserProfile, messageTimestamp: Long) { + private fun updateUser(userProfile: ReadableUserProfile, messageTimestamp: Long) { val userPublicKey = getUserPublicKey() ?: return // would love to get rid of recipient and context from this val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) @@ -588,7 +573,6 @@ open class Storage( name.takeUnless { it.isEmpty() }?.truncate(NAME_PADDED_LENGTH)?.let { TextSecurePreferences.setProfileName(context, it) profileManager.setName(context, recipient, it) - if (it != name) userProfile.setName(it) } // Update profile picture @@ -623,7 +607,7 @@ open class Storage( } } - private fun updateGroupInfo(groupInfoConfig: GroupInfoConfig, messageTimestamp: Long) { + private fun updateGroup(groupInfoConfig: ReadableGroupInfoConfig) { val threadId = getThreadId(fromSerialized(groupInfoConfig.id().hexString)) ?: return val recipient = getRecipientForThread(threadId) ?: return val db = DatabaseComponent.get(context).recipientDatabase() @@ -635,18 +619,9 @@ open class Storage( val mmsDb = DatabaseComponent.get(context).mmsDatabase() mmsDb.deleteMessagesInThreadBeforeDate(threadId, removeAttachmentsBefore, onlyMedia = true) } - // TODO: handle deleted group, handle delete attachment / message before a certain time } - private fun updateGroupKeys(groupKeys: GroupKeysConfig) { - // TODO: update something here? - } - - private fun updateGroupMembers(groupMembers: GroupMembersConfig) { - // TODO: maybe clear out some contacts or something? - } - - private fun updateContacts(contacts: Contacts, messageTimestamp: Long) { + private fun updateContacts(contacts: ReadableContacts, messageTimestamp: Long) { val extracted = contacts.all().toList() addLibSessionContacts(extracted, messageTimestamp) } @@ -665,10 +640,12 @@ open class Storage( TextSecurePreferences.setProfilePictureURL(context, null) Recipient.removeCached(fromSerialized(userPublicKey)) - configFactory.user?.setPic(UserPic.DEFAULT) + configFactory.withMutableUserConfigs { + it.userProfile.setPic(UserPic.DEFAULT) + } } - private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) { + private fun updateConvoVolatile(convos: ReadableConversationVolatileConfig) { val extracted = convos.all().filterNotNull() for (conversation in extracted) { val threadId = when (conversation) { @@ -686,7 +663,7 @@ open class Storage( } } - private fun updateUserGroups(userGroups: UserGroupsConfig, messageTimestamp: Long) { + private fun updateUserGroups(userGroups: ReadableUserGroupsConfig, messageTimestamp: Long) { val threadDb = DatabaseComponent.get(context).threadDatabase() val localUserPublicKey = getUserPublicKey() ?: return Log.w( "Loki", @@ -1080,25 +1057,29 @@ open class Storage( } override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair, expirationTimer: Int) { - val volatiles = configFactory.convoVolatile ?: return - val userGroups = configFactory.userGroups ?: return - if (volatiles.getLegacyClosedGroup(groupPublicKey) != null && userGroups.getLegacyGroupInfo(groupPublicKey) != null) return - val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey) - groupVolatileConfig.lastRead = formationTimestamp - volatiles.set(groupVolatileConfig) - val groupInfo = GroupInfo.LegacyGroupInfo( - accountId = groupPublicKey, - name = name, - members = members, - priority = PRIORITY_VISIBLE, - encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte - encSecKey = encryptionKeyPair.privateKey.serialize(), - disappearingTimer = expirationTimer.toLong(), - joinedAt = (formationTimestamp / 1000L) - ) - // shouldn't exist, don't use getOrConstruct + copy - userGroups.set(groupInfo) - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) + configFactory.withMutableUserConfigs { + val volatiles = it.convoInfoVolatile + val userGroups = it.userGroups + if (volatiles.getLegacyClosedGroup(groupPublicKey) != null && userGroups.getLegacyGroupInfo(groupPublicKey) != null) { + return@withMutableUserConfigs + } + + val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey) + groupVolatileConfig.lastRead = formationTimestamp + volatiles.set(groupVolatileConfig) + val groupInfo = GroupInfo.LegacyGroupInfo( + accountId = groupPublicKey, + name = name, + members = members, + priority = PRIORITY_VISIBLE, + encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte + encSecKey = encryptionKeyPair.privateKey.serialize(), + disappearingTimer = expirationTimer.toLong(), + joinedAt = (formationTimestamp / 1000L) + ) + // shouldn't exist, don't use getOrConstruct + copy + userGroups.set(groupInfo) + } } override fun updateGroupConfig(groupPublicKey: String) { @@ -1106,29 +1087,31 @@ open class Storage( val groupAddress = fromSerialized(groupID) val existingGroup = getGroup(groupID) ?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config") - val userGroups = configFactory.userGroups ?: return - if (!existingGroup.isActive) { - userGroups.eraseLegacyGroup(groupPublicKey) - return - } - val name = existingGroup.title - val admins = existingGroup.admins.map { it.serialize() } - val members = existingGroup.members.map { it.serialize() } - val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members) - val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey) - ?: return Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config") + configFactory.withMutableUserConfigs { + val userGroups = it.userGroups + if (!existingGroup.isActive) { + userGroups.eraseLegacyGroup(groupPublicKey) + return@withMutableUserConfigs + } + val name = existingGroup.title + val admins = existingGroup.admins.map { it.serialize() } + val members = existingGroup.members.map { it.serialize() } + val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members) + val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + ?: return@withMutableUserConfigs Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config") - val threadID = getThreadId(groupAddress) ?: return - val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy( - name = name, - members = membersMap, - encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte - encSecKey = latestKeyPair.privateKey.serialize(), - priority = if (isPinned(threadID)) PRIORITY_PINNED else PRIORITY_VISIBLE, - disappearingTimer = getExpirationConfiguration(threadID)?.expiryMode?.expirySeconds ?: 0L, - joinedAt = (existingGroup.formationTimestamp / 1000L) - ) - userGroups.set(groupInfo) + val threadID = getThreadId(groupAddress) ?: return@withMutableUserConfigs + val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy( + name = name, + members = membersMap, + encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte + encSecKey = latestKeyPair.privateKey.serialize(), + priority = if (isPinned(threadID)) PRIORITY_PINNED else PRIORITY_VISIBLE, + disappearingTimer = getExpirationConfiguration(threadID)?.expiryMode?.expirySeconds ?: 0L, + joinedAt = (existingGroup.formationTimestamp / 1000L) + ) + userGroups.set(groupInfo) + } } override fun isGroupActive(groupPublicKey: String): Boolean { @@ -1242,18 +1225,20 @@ open class Storage( * For new closed groups */ override fun getMembers(groupPublicKey: String): List = - configFactory.getGroupMemberConfig(AccountId(groupPublicKey))?.use { it.all() }?.toList() ?: emptyList() + configFactory.withGroupConfigs(AccountId(groupPublicKey)) { + it.groupMembers.all() + } - override fun getLibSessionClosedGroup(groupSessionId: String): GroupInfo.ClosedGroupInfo? { - return configFactory.userGroups?.getClosedGroup(groupSessionId) + override fun getLibSessionClosedGroup(groupAccountId: String): GroupInfo.ClosedGroupInfo? { + return configFactory.withUserConfigs { it.userGroups.getClosedGroup(groupAccountId) } } - override fun getClosedGroupDisplayInfo(groupSessionId: String): GroupDisplayInfo? { - val infoConfig = configFactory.getGroupInfoConfig(AccountId(groupSessionId)) ?: return null - val isAdmin = configFactory.userGroups?.getClosedGroup(groupSessionId)?.hasAdminKey() ?: return null + override fun getClosedGroupDisplayInfo(groupAccountId: String): GroupDisplayInfo? { + val groupIsAdmin = getLibSessionClosedGroup(groupAccountId)?.hasAdminKey() ?: return null - return infoConfig.use { info -> + return configFactory.withGroupConfigs(AccountId(groupAccountId)) { configs -> + val info = configs.groupInfo GroupDisplayInfo( id = info.id(), name = info.getName(), @@ -1262,7 +1247,7 @@ open class Storage( destroyed = false, created = info.getCreated(), description = info.getDescription(), - isUserAdmin = isAdmin + isUserAdmin = groupIsAdmin ) } } @@ -1270,7 +1255,7 @@ open class Storage( override fun insertGroupInfoChange(message: GroupUpdated, closedGroup: AccountId): Long? { val sentTimestamp = message.sentTimestamp ?: SnodeAPI.nowWithOffset val senderPublicKey = message.sender - val groupName = configFactory.getGroupInfoConfig(closedGroup)?.use { it.getName() }.orEmpty() + val groupName = configFactory.withGroupConfigs(closedGroup) { it.groupInfo.getName() } val updateData = UpdateMessageData.buildGroupUpdate(message, groupName) ?: return null @@ -1365,19 +1350,21 @@ open class Storage( override fun onOpenGroupAdded(server: String, room: String) { OpenGroupManager.restartPollerForServer(server.removeSuffix("/")) - val groups = configFactory.userGroups ?: return - val volatileConfig = configFactory.convoVolatile ?: return - val openGroup = getOpenGroup(room, server) ?: return - val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return - val pubKeyHex = Hex.toStringCondensed(pubKey) - val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex) - groups.set(communityInfo) - val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey) - if (volatile.lastRead != 0L) { - val threadId = getThreadId(openGroup) ?: return - markConversationAsRead(threadId, volatile.lastRead, force = true) + configFactory.withMutableUserConfigs { configs -> + val groups = configs.userGroups + val volatileConfig = configs.convoInfoVolatile + val openGroup = getOpenGroup(room, server) ?: return@withMutableUserConfigs + val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@withMutableUserConfigs + val pubKeyHex = Hex.toStringCondensed(pubKey) + val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex) + groups.set(communityInfo) + val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey) + if (volatile.lastRead != 0L) { + val threadId = getThreadId(openGroup) ?: return@withMutableUserConfigs + markConversationAsRead(threadId, volatile.lastRead, force = true) + } + volatileConfig.set(volatile) } - volatileConfig.set(volatile) } override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean { @@ -1606,41 +1593,44 @@ open class Storage( val threadDB = DatabaseComponent.get(context).threadDatabase() threadDB.setPinned(threadID, isPinned) val threadRecipient = getRecipientForThread(threadID) ?: return - if (threadRecipient.isLocalNumber) { - val user = configFactory.user ?: return - user.setNtsPriority(if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) - } else if (threadRecipient.isContactRecipient) { - val contacts = configFactory.contacts ?: return - contacts.upsertContact(threadRecipient.address.serialize()) { - priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE - } - } else if (threadRecipient.isGroupRecipient) { - val groups = configFactory.userGroups ?: return - when { - threadRecipient.isLegacyClosedGroupRecipient -> { - threadRecipient.address.serialize() - .let(GroupUtil::doubleDecodeGroupId) - .let(groups::getOrConstructLegacyGroupInfo) - .copy (priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) - .let(groups::set) + configFactory.withMutableUserConfigs { configs -> + if (threadRecipient.isLocalNumber) { + configs.userProfile.setNtsPriority(if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) + } else if (threadRecipient.isContactRecipient) { + configs.contacts.upsertContact(threadRecipient.address.serialize()) { + priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE } - threadRecipient.isClosedGroupV2Recipient -> { - val newGroupInfo = groups.getOrConstructClosedGroup(threadRecipient.address.serialize()).copy ( - priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE - ) - groups.set(newGroupInfo) - } - threadRecipient.isCommunityRecipient -> { - val openGroup = getOpenGroup(threadID) ?: return - val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return - val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy ( - priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE - ) - groups.set(newGroupInfo) + } else if (threadRecipient.isGroupRecipient) { + when { + threadRecipient.isLegacyClosedGroupRecipient -> { + threadRecipient.address.serialize() + .let(GroupUtil::doubleDecodeGroupId) + .let(configs.userGroups::getOrConstructLegacyGroupInfo) + .copy(priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) + .let(configs.userGroups::set) + } + + threadRecipient.isClosedGroupV2Recipient -> { + val newGroupInfo = configs.userGroups + .getOrConstructClosedGroup(threadRecipient.address.serialize()) + .copy(priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) + configs.userGroups.set(newGroupInfo) + } + + threadRecipient.isCommunityRecipient -> { + val openGroup = getOpenGroup(threadID) ?: return@withMutableUserConfigs + val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) + ?: return@withMutableUserConfigs + val newGroupInfo = configs.userGroups.getOrConstructCommunityInfo( + baseUrl, + room, + Hex.toStringCondensed(pubKeyHex) + ).copy(priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) + configs.userGroups.set(newGroupInfo) + } } } } - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } override fun isPinned(threadID: Long): Boolean { @@ -1676,17 +1666,19 @@ open class Storage( if (recipient.isContactRecipient || recipient.isCommunityRecipient) return // If we get here then this is a closed group conversation (i.e., recipient.isClosedGroupRecipient) - val volatile = configFactory.convoVolatile ?: return - val groups = configFactory.userGroups ?: return - val groupID = recipient.address.toGroupString() - val closedGroup = getGroup(groupID) - val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) - if (closedGroup != null) { - groupDB.delete(groupID) - volatile.eraseLegacyClosedGroup(groupPublicKey) - groups.eraseLegacyGroup(groupPublicKey) - } else { - Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}") + configFactory.withMutableUserConfigs { configs -> + val volatile = configs.convoInfoVolatile + val groups = configs.userGroups + val groupID = recipient.address.toGroupString() + val closedGroup = getGroup(groupID) + val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) + if (closedGroup != null) { + groupDB.delete(groupID) + volatile.eraseLegacyClosedGroup(groupPublicKey) + groups.eraseLegacyGroup(groupPublicKey) + } else { + Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}") + } } } @@ -1833,10 +1825,13 @@ open class Storage( setRecipientApprovedMe(sender, true) // Also update the config about this contact - configFactory.contacts?.upsertContact(sender.address.serialize()) { - approved = true - approvedMe = true + configFactory.withMutableUserConfigs { + it.contacts.upsertContact(sender.address.serialize()) { + approved = true + approvedMe = true + } } + val message = IncomingMediaMessage( sender.address, response.sentTimestamp!!, @@ -1894,16 +1889,20 @@ open class Storage( override fun setRecipientApproved(recipient: Recipient, approved: Boolean) { DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved) if (recipient.isLocalNumber || !recipient.isContactRecipient) return - configFactory.contacts?.upsertContact(recipient.address.serialize()) { - this.approved = approved + configFactory.withMutableUserConfigs { + it.contacts.upsertContact(recipient.address.serialize()) { + this.approved = approved + } } } override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) { DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe) if (recipient.isLocalNumber || !recipient.isContactRecipient) return - configFactory.contacts?.upsertContact(recipient.address.serialize()) { - this.approvedMe = approvedMe + configFactory.withMutableUserConfigs { + it.contacts.upsertContact(recipient.address.serialize()) { + this.approvedMe = approvedMe + } } } @@ -2040,15 +2039,13 @@ open class Storage( override fun setBlocked(recipients: Iterable, isBlocked: Boolean, fromConfigUpdate: Boolean) { val recipientDb = DatabaseComponent.get(context).recipientDatabase() recipientDb.setBlocked(recipients, isBlocked) - recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient -> - configFactory.contacts?.upsertContact(recipient.address.serialize()) { - this.blocked = isBlocked + configFactory.withMutableUserConfigs { configs -> + recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient -> + configs.contacts.upsertContact(recipient.address.serialize()) { + this.blocked = isBlocked + } } } - val contactsConfig = configFactory.contacts ?: return - if (contactsConfig.needsPush() && !fromConfigUpdate) { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) - } } override fun blockedContacts(): List { @@ -2060,21 +2057,23 @@ open class Storage( val recipient = getRecipientForThread(threadId) ?: return null val dbExpirationMetadata = DatabaseComponent.get(context).expirationConfigurationDatabase().getExpirationConfiguration(threadId) return when { - recipient.isLocalNumber -> configFactory.user?.getNtsExpiry() + recipient.isLocalNumber -> configFactory.withUserConfigs { it.userProfile.getNtsExpiry() } recipient.isContactRecipient -> { // read it from contacts config if exists recipient.address.serialize().takeIf { it.startsWith(IdPrefix.STANDARD.value) } - ?.let { configFactory.contacts?.get(it)?.expiryMode } + ?.let { configFactory.withUserConfigs { configs -> configs.contacts.get(it)?.expiryMode } } } recipient.isClosedGroupV2Recipient -> { - configFactory.getGroupInfoConfig(AccountId(recipient.address.serialize()))?.getExpiryTimer()?.let { + configFactory.withGroupConfigs(AccountId(recipient.address.serialize())) { configs -> + configs.groupInfo.getExpiryTimer() + }.let { if (it == 0L) ExpiryMode.NONE else ExpiryMode.AfterSend(it) } } recipient.isLegacyClosedGroupRecipient -> { // read it from group config if exists GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) - .let { configFactory.userGroups?.getLegacyGroupInfo(it) } + .let { id -> configFactory.withUserConfigs { it.userGroups.getLegacyGroupInfo(id) } } ?.run { disappearingTimer.takeIf { it != 0L }?.let(ExpiryMode::AfterSend) ?: ExpiryMode.NONE } } else -> null @@ -2100,24 +2099,28 @@ open class Storage( } if (recipient.isLegacyClosedGroupRecipient) { - val userGroups = configFactory.userGroups ?: return val groupPublicKey = GroupUtil.addressToGroupAccountId(recipient.address) - val groupInfo = userGroups.getLegacyGroupInfo(groupPublicKey) - ?.copy(disappearingTimer = expiryMode.expirySeconds) ?: return - userGroups.set(groupInfo) + + configFactory.withMutableUserConfigs { + val groupInfo = it.userGroups.getLegacyGroupInfo(groupPublicKey) + ?.copy(disappearingTimer = expiryMode.expirySeconds) ?: return@withMutableUserConfigs + it.userGroups.set(groupInfo) + } } else if (recipient.isClosedGroupV2Recipient) { val groupSessionId = AccountId(recipient.address.serialize()) - val groupInfo = configFactory.getGroupInfoConfig(groupSessionId) ?: return - groupInfo.setExpiryTimer(expiryMode.expirySeconds) - configFactory.persist(groupInfo, SnodeAPI.nowWithOffset, groupSessionId.hexString) - } else if (recipient.isLocalNumber) { - val user = configFactory.user ?: return - user.setNtsExpiry(expiryMode) - } else if (recipient.isContactRecipient) { - val contacts = configFactory.contacts ?: return + configFactory.withMutableGroupConfigs(groupSessionId) { configs -> + configs.groupInfo.setExpiryTimer(expiryMode.expirySeconds) + } - val contact = contacts.get(recipient.address.serialize())?.copy(expiryMode = expiryMode) ?: return - contacts.set(contact) + } else if (recipient.isLocalNumber) { + configFactory.withMutableUserConfigs { + it.userProfile.setNtsExpiry(expiryMode) + } + } else if (recipient.isContactRecipient) { + configFactory.withMutableUserConfigs { + val contact = it.contacts.get(recipient.address.serialize())?.copy(expiryMode = expiryMode) ?: return@withMutableUserConfigs + it.contacts.set(contact) + } } expirationDb.setExpirationConfiguration( config.run { copy(expiryMode = expiryMode) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index 7cfa1a6f9d..781676788e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -1,25 +1,17 @@ package org.thoughtcrime.securesms.debugmenu import android.app.Application -import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import network.loki.messenger.R -import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.Environment import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext -import org.session.libsession.utilities.Environment -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import javax.inject.Inject @HiltViewModel @@ -75,11 +67,6 @@ class DebugMenuViewModel @Inject constructor( // clear remote and local data, then restart the app viewModelScope.launch { - try { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application) - } catch (e: Exception) { - // we can ignore fails here as we might be switching environments before the user gets a public key - } ApplicationContext.getInstance(application).clearAllData().let { success -> if(success){ // save the environment diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index 058e276d8c..ab8b2dd2bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -1,40 +1,57 @@ package org.thoughtcrime.securesms.dependencies import android.content.Context -import android.os.Trace import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow -import network.loki.messenger.libsession_util.Config import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.GroupInfoConfig import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.GroupMembersConfig +import network.loki.messenger.libsession_util.MutableContacts +import network.loki.messenger.libsession_util.MutableConversationVolatileConfig +import network.loki.messenger.libsession_util.MutableUserGroupsConfig +import network.loki.messenger.libsession_util.MutableUserProfile import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserProfile +import network.loki.messenger.libsession_util.util.BaseCommunityInfo +import network.loki.messenger.libsession_util.util.Contact +import network.loki.messenger.libsession_util.util.ExpiryMode +import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.Sodium -import org.session.libsession.messaging.messages.Destination +import network.loki.messenger.libsession_util.util.UserPic +import org.session.libsession.database.StorageProtocol +import org.session.libsession.messaging.messages.control.ConfigurationMessage +import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SwarmAuth +import org.session.libsession.utilities.Address import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.ConfigFactoryUpdateListener -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage -import org.session.libsignal.utilities.IdPrefix -import org.session.libsignal.utilities.Log +import org.session.libsession.utilities.ConfigUpdateNotification +import org.session.libsession.utilities.GroupConfigs +import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.MutableGroupConfigs +import org.session.libsession.utilities.MutableUserConfigs +import org.session.libsession.utilities.UserConfigs +import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Hex +import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.database.ConfigDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.groups.GroupManager -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import java.util.concurrent.ConcurrentHashMap + class ConfigFactory( private val context: Context, private val configDatabase: ConfigDatabase, - /** */ - private val maybeGetUserInfo: () -> Pair? -) : - ConfigFactoryProtocol { + private val threadDb: ThreadDatabase, + private val storage: StorageProtocol, +) : ConfigFactoryProtocol { companion object { // This is a buffer period within which we will process messages which would result in a // config change, any message which would normally result in a config change which was sent @@ -43,351 +60,300 @@ class ConfigFactory( const val configChangeBufferPeriod: Long = (2 * 60 * 1000) } - fun keyPairChanged() { // this should only happen restoring or clearing datac - _userConfig?.free() - _contacts?.free() - _convoVolatileConfig?.free() - _userConfig = null - _contacts = null - _convoVolatileConfig = null + init { + System.loadLibrary("session_util") } - private val userLock = Object() - private var _userConfig: UserProfile? = null - private val contactsLock = Object() - private var _contacts: Contacts? = null - private val convoVolatileLock = Object() - private var _convoVolatileConfig: ConversationVolatileConfig? = null - private val userGroupsLock = Object() - private var _userGroups: UserGroupsConfig? = null + private class UserConfigsImpl( + userEd25519SecKey: ByteArray, + private val userAccountId: AccountId, + private val configDatabase: ConfigDatabase, + storage: StorageProtocol, + threadDb: ThreadDatabase, + contactsDump: ByteArray? = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.CONTACTS_VARIANT, + userAccountId.hexString + ), + userGroupsDump: ByteArray? = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.USER_GROUPS_VARIANT, + userAccountId.hexString + ), + userProfileDump: ByteArray? = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.USER_PROFILE_VARIANT, + userAccountId.hexString + ), + convoInfoDump: ByteArray? = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.CONVO_INFO_VARIANT, + userAccountId.hexString + ) + ) : MutableUserConfigs { + override val contacts = Contacts( + ed25519SecretKey = userEd25519SecKey, + initialDump = contactsDump, + ) - private val isConfigForcedOn by lazy { TextSecurePreferences.hasForcedNewConfig(context) } + override val userGroups = UserGroupsConfig( + ed25519SecretKey = userEd25519SecKey, + initialDump = userGroupsDump + ) + override val userProfile = UserProfile( + ed25519SecretKey = userEd25519SecKey, + initialDump = userProfileDump + ) + override val convoInfoVolatile = ConversationVolatileConfig( + ed25519SecretKey = userEd25519SecKey, + initialDump = convoInfoDump, + ) - private val listeners: MutableList = mutableListOf() + init { + if (contactsDump == null) { + contacts.initFrom(storage) + } - private val _configUpdateNotifications = MutableSharedFlow( + if (userGroupsDump == null) { + userGroups.initFrom(storage) + } + + if (userProfileDump == null) { + userProfile.initFrom(storage) + } + + if (convoInfoDump == null) { + convoInfoVolatile.initFrom(storage, threadDb) + } + } + + /** + * Persists the config if it is dirty and returns the list of classes that were persisted + */ + fun persistIfDirty(): Boolean { + return sequenceOf( + contacts to ConfigDatabase.CONTACTS_VARIANT, + userGroups to ConfigDatabase.USER_GROUPS_VARIANT, + userProfile to ConfigDatabase.USER_PROFILE_VARIANT, + convoInfoVolatile to ConfigDatabase.CONVO_INFO_VARIANT + ).fold(false) { acc, (config, variant) -> + if (config.needsDump()) { + configDatabase.storeConfig( + variant = variant, + publicKey = userAccountId.hexString, + data = config.dump(), + timestamp = SnodeAPI.nowWithOffset + ) + true + } else { + acc + } + } + } + } + + private class GroupConfigsImpl( + userEd25519SecKey: ByteArray, + private val groupAccountId: AccountId, + groupAdminKey: ByteArray?, + private val configDatabase: ConfigDatabase + ) : MutableGroupConfigs { + override val groupInfo = GroupInfoConfig( + groupPubKey = groupAccountId.pubKeyBytes, + groupAdminKey = groupAdminKey, + initialDump = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.INFO_VARIANT, + groupAccountId.hexString + ) + ) + override val groupMembers = GroupMembersConfig( + groupPubKey = groupAccountId.pubKeyBytes, + groupAdminKey = groupAdminKey, + initialDump = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.MEMBER_VARIANT, + groupAccountId.hexString + ) + ) + override val groupKeys = GroupKeysConfig( + userSecretKey = userEd25519SecKey, + groupPublicKey = groupAccountId.pubKeyBytes, + groupAdminKey = groupAdminKey, + initialDump = configDatabase.retrieveConfigAndHashes( + ConfigDatabase.KEYS_VARIANT, + groupAccountId.hexString + ), + info = groupInfo, + members = groupMembers + ) + + fun persistIfDirty(): Boolean { + if (groupInfo.needsDump() || groupMembers.needsDump() || groupKeys.needsDump()) { + configDatabase.storeGroupConfigs( + publicKey = groupAccountId.hexString, + keysConfig = groupKeys.dump(), + infoConfig = groupInfo.dump(), + memberConfig = groupMembers.dump(), + timestamp = SnodeAPI.nowWithOffset + ) + return true + } + + return false + } + + override fun loadKeys(message: ByteArray, hash: String, timestamp: Long): Boolean { + return groupKeys.loadKey(message, hash, timestamp, groupInfo.pointer, groupMembers.pointer) + } + + override fun rekeys() { + groupKeys.rekey(groupInfo.pointer, groupMembers.pointer) + } + } + + private val userConfigs = ConcurrentHashMap() + private val groupConfigs = ConcurrentHashMap() + + private val _configUpdateNotifications = MutableSharedFlow( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST ) override val configUpdateNotifications get() = _configUpdateNotifications - fun registerListener(listener: ConfigFactoryUpdateListener) { - listeners += listener - } + private fun requiresCurrentUserAccountId(): AccountId = + AccountId(requireNotNull(storage.getUserPublicKey()) { + "No logged in user" + }) - fun unregisterListener(listener: ConfigFactoryUpdateListener) { - listeners -= listener - } - - private inline fun synchronizedWithLog(lock: Any, body: () -> T): T { - Trace.beginSection("synchronizedWithLog") - val result = synchronized(lock) { - body() + private fun requiresCurrentUserED25519SecKey(): ByteArray = + requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) { + "No logged in user" + } + + override fun withUserConfigs(cb: (UserConfigs) -> T): T { + val userAccountId = requiresCurrentUserAccountId() + val configs = userConfigs.getOrPut(userAccountId) { + UserConfigsImpl( + requiresCurrentUserED25519SecKey(), + userAccountId, + threadDb = threadDb, + configDatabase = configDatabase, + storage = storage + ) + } + + return synchronized(configs) { + cb(configs) } - Trace.endSection() - return result } - override val user: UserProfile? - get() = synchronizedWithLog(userLock) { - if (_userConfig == null) { - val (secretKey, publicKey) = maybeGetUserInfo() ?: return null - val userDump = configDatabase.retrieveConfigAndHashes( - SharedConfigMessage.Kind.USER_PROFILE.name, - publicKey - ) - _userConfig = if (userDump != null) { - UserProfile.newInstance(secretKey, userDump) - } else { - ConfigurationMessageUtilities.generateUserProfileConfigDump()?.let { dump -> - UserProfile.newInstance(secretKey, dump) - } ?: UserProfile.newInstance(secretKey) - } + override fun withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T { + return withUserConfigs { configs -> + val result = cb(configs as UserConfigsImpl) + + if (configs.persistIfDirty()) { + _configUpdateNotifications.tryEmit(ConfigUpdateNotification.UserConfigs) } - _userConfig + + result + } + } + + override fun withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T { + val configs = groupConfigs.getOrPut(groupId) { + val groupAdminKey = requireNotNull(withUserConfigs { + it.userGroups.getClosedGroup(groupId.hexString) + }) { + "Group not found" + }.adminKey + + GroupConfigsImpl( + requiresCurrentUserED25519SecKey(), + groupId, + groupAdminKey, + configDatabase + ) } - override val contacts: Contacts? - get() = synchronizedWithLog(contactsLock) { - if (_contacts == null) { - val (secretKey, publicKey) = maybeGetUserInfo() ?: return null - val contactsDump = configDatabase.retrieveConfigAndHashes( - SharedConfigMessage.Kind.CONTACTS.name, - publicKey - ) - _contacts = if (contactsDump != null) { - Contacts.newInstance(secretKey, contactsDump) - } else { - ConfigurationMessageUtilities.generateContactConfigDump()?.let { dump -> - Contacts.newInstance(secretKey, dump) - } ?: Contacts.newInstance(secretKey) - } + return synchronized(configs) { + cb(configs) + } + } + + override fun withMutableGroupConfigs( + groupId: AccountId, + cb: (MutableGroupConfigs) -> T + ): T { + return withGroupConfigs(groupId) { configs -> + val result = cb(configs as GroupConfigsImpl) + + if (configs.persistIfDirty()) { + _configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsUpdated(groupId)) } - _contacts + + result } - - override val convoVolatile: ConversationVolatileConfig? - get() = synchronizedWithLog(convoVolatileLock) { - if (_convoVolatileConfig == null) { - val (secretKey, publicKey) = maybeGetUserInfo() ?: return null - val convoDump = configDatabase.retrieveConfigAndHashes( - SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name, - publicKey - ) - _convoVolatileConfig = if (convoDump != null) { - ConversationVolatileConfig.newInstance(secretKey, convoDump) - } else { - ConfigurationMessageUtilities.generateConversationVolatileDump(context) - ?.let { dump -> - ConversationVolatileConfig.newInstance(secretKey, dump) - } ?: ConversationVolatileConfig.newInstance(secretKey) - } - } - _convoVolatileConfig - } - - override val userGroups: UserGroupsConfig? - get() = synchronizedWithLog(userGroupsLock) { - if (_userGroups == null) { - val (secretKey, publicKey) = maybeGetUserInfo() ?: return null - val userGroupsDump = configDatabase.retrieveConfigAndHashes( - SharedConfigMessage.Kind.GROUPS.name, - publicKey - ) - _userGroups = if (userGroupsDump != null) { - UserGroupsConfig.Companion.newInstance(secretKey, userGroupsDump) - } else { - ConfigurationMessageUtilities.generateUserGroupDump(context)?.let { dump -> - UserGroupsConfig.Companion.newInstance(secretKey, dump) - } ?: UserGroupsConfig.newInstance(secretKey) - } - } - _userGroups - } - - private fun getGroupInfo(groupSessionId: AccountId) = userGroups?.getClosedGroup(groupSessionId.hexString) - - override fun getGroupInfoConfig(groupSessionId: AccountId): GroupInfoConfig? = getGroupInfo(groupSessionId)?.let { groupInfo -> - // get any potential initial dumps - val dump = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.INFO_VARIANT, - groupSessionId.hexString - ) ?: byteArrayOf() - - GroupInfoConfig.newInstance(groupSessionId.pubKeyBytes, groupInfo.adminKey, dump) } - override fun getGroupKeysConfig(groupSessionId: AccountId, - info: GroupInfoConfig?, - members: GroupMembersConfig?, - free: Boolean): GroupKeysConfig? = getGroupInfo(groupSessionId)?.let { groupInfo -> - // Get the user info or return early - val (userSk, _) = maybeGetUserInfo() ?: return@let null - - // Get the group info or return early - val usedInfo = info ?: getGroupInfoConfig(groupSessionId) ?: return@let null - - // Get the group members or return early - val usedMembers = members ?: getGroupMemberConfig(groupSessionId) ?: return@let null - - // Get the dump or empty - val dump = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.KEYS_VARIANT, - groupSessionId.hexString - ) ?: byteArrayOf() - - // Put it all together - val keys = GroupKeysConfig.newInstance( - userSk, - groupSessionId.pubKeyBytes, - groupInfo.adminKey, - dump, - usedInfo, - usedMembers - ) - if (free) { - info?.free() - members?.free() + override fun removeGroup(groupId: AccountId) { + withMutableUserConfigs { + it.userGroups.eraseClosedGroup(groupId.hexString) } - if (usedInfo !== info) usedInfo.free() - if (usedMembers !== members) usedMembers.free() - keys - } - override fun getGroupMemberConfig(groupSessionId: AccountId): GroupMembersConfig? = getGroupInfo(groupSessionId)?.let { groupInfo -> - // Get initial dump if we have one - val dump = configDatabase.retrieveConfigAndHashes( - ConfigDatabase.MEMBER_VARIANT, - groupSessionId.hexString - ) ?: byteArrayOf() - - GroupMembersConfig.newInstance( - groupSessionId.pubKeyBytes, - groupInfo.adminKey, - dump - ) - } - - override fun constructGroupKeysConfig( - groupSessionId: AccountId, - info: GroupInfoConfig, - members: GroupMembersConfig - ): GroupKeysConfig? = getGroupInfo(groupSessionId)?.let { groupInfo -> - val (userSk, _) = maybeGetUserInfo() ?: return null - GroupKeysConfig.newInstance( - userSk, - groupSessionId.pubKeyBytes, - groupInfo.adminKey, - info = info, - members = members - ) - } - - override fun userSessionId(): AccountId? { - return maybeGetUserInfo()?.second?.let(::AccountId) - } - - override fun maybeDecryptForUser(encoded: ByteArray, domain: String, closedGroupSessionId: AccountId): ByteArray? { - val secret = maybeGetUserInfo()?.first ?: run { - Log.e("ConfigFactory", "No user ed25519 secret key decrypting a message for us") - return null + if (groupConfigs.remove(groupId) != null) { + _configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsDeleted(groupId)) } + + configDatabase.deleteGroupConfigs(groupId) + } + + override fun maybeDecryptForUser( + encoded: ByteArray, + domain: String, + closedGroupSessionId: AccountId + ): ByteArray? { return Sodium.decryptForMultipleSimple( encoded = encoded, - ed25519SecretKey = secret, + ed25519SecretKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) { + "No logged in user" + }, domain = domain, senderPubKey = Sodium.ed25519PkToCurve25519(closedGroupSessionId.pubKeyBytes) ) } - override fun getUserConfigs(): List = - listOfNotNull(user, contacts, convoVolatile, userGroups) - - - private fun persistUserConfigDump(timestamp: Long) = synchronized(userLock) { - val dumped = user?.dump() ?: return - val (_, publicKey) = maybeGetUserInfo() ?: return - configDatabase.storeConfig( - SharedConfigMessage.Kind.USER_PROFILE.name, - publicKey, - dumped, - timestamp - ) - } - - private fun persistContactsConfigDump(timestamp: Long) = synchronized(contactsLock) { - val dumped = contacts?.dump() ?: return - val (_, publicKey) = maybeGetUserInfo() ?: return - configDatabase.storeConfig( - SharedConfigMessage.Kind.CONTACTS.name, - publicKey, - dumped, - timestamp - ) - } - - private fun persistConvoVolatileConfigDump(timestamp: Long) = synchronized(convoVolatileLock) { - val dumped = convoVolatile?.dump() ?: return - val (_, publicKey) = maybeGetUserInfo() ?: return - configDatabase.storeConfig( - SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name, - publicKey, - dumped, - timestamp - ) - } - - private fun persistUserGroupsConfigDump(timestamp: Long) = synchronized(userGroupsLock) { - val dumped = userGroups?.dump() ?: return - val (_, publicKey) = maybeGetUserInfo() ?: return - configDatabase.storeConfig( - SharedConfigMessage.Kind.GROUPS.name, - publicKey, - dumped, - timestamp - ) - } - - fun persistGroupConfigDump(forConfigObject: ConfigBase, groupSessionId: AccountId, timestamp: Long) = synchronized(userGroupsLock) { - val dumped = forConfigObject.dump() - val variant = when (forConfigObject) { - is GroupMembersConfig -> ConfigDatabase.MEMBER_VARIANT - is GroupInfoConfig -> ConfigDatabase.INFO_VARIANT - else -> throw Exception("Shouldn't be called") - } - configDatabase.storeConfig( - variant, - groupSessionId.hexString, - dumped, - timestamp - ) - _configUpdateNotifications.tryEmit(Unit) - } - - override fun persist(forConfigObject: Config, timestamp: Long, forPublicKey: String?) { - try { - if (forConfigObject is ConfigBase && !forConfigObject.needsDump() || forConfigObject is GroupKeysConfig && !forConfigObject.needsDump()) { - Log.d("ConfigFactory", "Don't need to persist ${forConfigObject.javaClass} for $forPublicKey pubkey") - return - } - - listeners.forEach { listener -> - listener.notifyUpdates(forConfigObject, timestamp) - } - - when (forConfigObject) { - is UserProfile -> persistUserConfigDump(timestamp) - is Contacts -> persistContactsConfigDump(timestamp) - is ConversationVolatileConfig -> persistConvoVolatileConfigDump(timestamp) - is UserGroupsConfig -> persistUserGroupsConfigDump(timestamp) - is GroupMembersConfig -> persistGroupConfigDump(forConfigObject, AccountId(forPublicKey!!), timestamp) - is GroupInfoConfig -> persistGroupConfigDump(forConfigObject, AccountId(forPublicKey!!), timestamp) - else -> throw UnsupportedOperationException("Can't support type of ${forConfigObject::class.simpleName} yet") - } - - _configUpdateNotifications.tryEmit(Unit) - } catch (e: Exception) { - Log.e("Loki", "failed to persist ${forConfigObject.javaClass.simpleName}", e) - } - } - override fun conversationInConfig( publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean ): Boolean { - val (_, userPublicKey) = maybeGetUserInfo() ?: return true + val userPublicKey = storage.getUserPublicKey() ?: return false if (openGroupId != null) { - val userGroups = userGroups ?: return false val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context) val openGroup = get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false // Not handling the `hidden` behaviour for communities so just indicate the existence - return (userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null) + return withUserConfigs { + it.userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null + } } else if (groupPublicKey != null) { - val userGroups = userGroups ?: return false - // Not handling the `hidden` behaviour for legacy groups so just indicate the existence - return if (groupPublicKey.startsWith(IdPrefix.GROUP.value)) { - userGroups.getClosedGroup(groupPublicKey) != null - } else { - userGroups.getLegacyGroupInfo(groupPublicKey) != null + return withUserConfigs { + if (groupPublicKey.startsWith(IdPrefix.GROUP.value)) { + it.userGroups.getClosedGroup(groupPublicKey) != null + } else { + it.userGroups.getLegacyGroupInfo(groupPublicKey) != null + } } } else if (publicKey == userPublicKey) { - val user = user ?: return false - - return (!visibleOnly || user.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN) + return withUserConfigs { + !visibleOnly || it.userProfile.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN + } } else if (publicKey != null) { - val contacts = contacts ?: return false - val targetContact = contacts.get(publicKey) ?: return false - - return (!visibleOnly || targetContact.priority != ConfigBase.PRIORITY_HIDDEN) + return withUserConfigs { + (!visibleOnly || it.contacts.get(publicKey)?.priority != ConfigBase.PRIORITY_HIDDEN) + } + } else { + return false } - - return false } override fun canPerformChange( @@ -402,32 +368,192 @@ class ConfigFactory( return (changeTimestampMs >= (lastUpdateTimestampMs - configChangeBufferPeriod)) } - override fun saveGroupConfigs( - groupKeys: GroupKeysConfig, - groupInfo: GroupInfoConfig, - groupMembers: GroupMembersConfig - ) { - val pubKey = groupInfo.id().hexString - val timestamp = SnodeAPI.nowWithOffset + override fun getGroupAuth(groupId: AccountId): SwarmAuth? { + val (adminKey, authData) = withUserConfigs { + val group = it.userGroups.getClosedGroup(groupId.hexString) + group?.adminKey to group?.authData + } - // this would be nicer with a .any iteration or something but the base types don't line up - val anyNeedDump = groupKeys.needsDump() || groupInfo.needsDump() || groupMembers.needsDump() - if (!anyNeedDump) return Log.d("ConfigFactory", "Group config doesn't need dump, skipping") - else Log.d("ConfigFactory", "Group config needs dump, storing and notifying") - - configDatabase.storeGroupConfigs(pubKey, groupKeys.dump(), groupInfo.dump(), groupMembers.dump(), timestamp) - _configUpdateNotifications.tryEmit(Unit) + return if (adminKey != null) { + OwnedSwarmAuth.ofClosedGroup(groupId, adminKey) + } else if (authData != null) { + GroupSubAccountSwarmAuth(groupId, this, authData) + } else { + null + } } - override fun removeGroup(closedGroupId: AccountId) { - val groups = userGroups ?: return - groups.eraseClosedGroup(closedGroupId.hexString) - persist(groups, SnodeAPI.nowWithOffset) - configDatabase.deleteGroupConfigs(closedGroupId) + fun clearAll() { + //TODO: clear all configsr } - override fun scheduleUpdate(destination: Destination) { - // there's probably a better way to do this - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(destination) + private class GroupSubAccountSwarmAuth( + override val accountId: AccountId, + val factory: ConfigFactory, + val authData: ByteArray, + ) : SwarmAuth { + override val ed25519PublicKeyHex: String? + get() = null + + override fun sign(data: ByteArray): Map { + return factory.withGroupConfigs(accountId) { + val auth = it.groupKeys.subAccountSign(data, authData) + buildMap { + put("subaccount", auth.subAccount) + put("subaccount_sig", auth.subAccountSig) + put("signature", auth.signature) + } + } + } + + override fun signForPushRegistry(data: ByteArray): Map { + return factory.withGroupConfigs(accountId) { + val auth = it.groupKeys.subAccountSign(data, authData) + buildMap { + put("subkey_tag", auth.subAccount) + put("signature", auth.signature) + } + } + } + } +} + +/** + * Sync group data from our local database + */ +private fun MutableUserGroupsConfig.initFrom(storage: StorageProtocol) { + storage + .getAllOpenGroups() + .values + .asSequence() + .mapNotNull { openGroup -> + val (baseUrl, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@mapNotNull null + val pubKeyHex = Hex.toStringCondensed(pubKey) + val baseInfo = BaseCommunityInfo(baseUrl, room, pubKeyHex) + val threadId = storage.getThreadId(openGroup) ?: return@mapNotNull null + val isPinned = storage.isPinned(threadId) + GroupInfo.CommunityGroupInfo(baseInfo, if (isPinned) 1 else 0) + } + .forEach(this::set) + + storage + .getAllGroups(includeInactive = false) + .asSequence().filter { it.isLegacyClosedGroup && it.isActive && it.members.size > 1 } + .mapNotNull { group -> + val groupAddress = Address.fromSerialized(group.encodedId) + val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupAddress.serialize()).toHexString() + val recipient = storage.getRecipientSettings(groupAddress) ?: return@mapNotNull null + val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return@mapNotNull null + val threadId = storage.getThreadId(group.encodedId) + val isPinned = threadId?.let { storage.isPinned(threadId) } ?: false + val admins = group.admins.associate { it.serialize() to true } + val members = group.members.filterNot { it.serialize() !in admins.keys }.associate { it.serialize() to false } + GroupInfo.LegacyGroupInfo( + accountId = groupPublicKey, + name = group.title, + members = admins + members, + priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, + encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte + encSecKey = encryptionKeyPair.privateKey.serialize(), + disappearingTimer = recipient.expireMessages.toLong(), + joinedAt = (group.formationTimestamp / 1000L) + ) + } + .forEach(this::set) +} + +private fun MutableConversationVolatileConfig.initFrom(storage: StorageProtocol, threadDb: ThreadDatabase) { + threadDb.approvedConversationList.use { cursor -> + val reader = threadDb.readerFor(cursor) + var current = reader.next + while (current != null) { + val recipient = current.recipient + val contact = when { + recipient.isCommunityRecipient -> { + val openGroup = storage.getOpenGroup(current.threadId) ?: continue + val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue + getOrConstructCommunity(base, room, pubKey) + } + recipient.isClosedGroupV2Recipient -> { + // It's probably safe to assume there will never be a case where new closed groups will ever be there before a dump is created... + // but just in case... + getOrConstructClosedGroup(recipient.address.serialize()) + } + recipient.isLegacyClosedGroupRecipient -> { + val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) + getOrConstructLegacyGroup(groupPublicKey) + } + recipient.isContactRecipient -> { + if (recipient.isLocalNumber) null // this is handled by the user profile NTS data + else if (recipient.isOpenGroupInboxRecipient) null // specifically exclude + else if (!recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) null + else getOrConstructOneToOne(recipient.address.serialize()) + } + else -> null + } + if (contact == null) { + current = reader.next + continue + } + contact.lastRead = current.lastSeen + contact.unread = false + set(contact) + current = reader.next + } + } +} + +private fun MutableUserProfile.initFrom(storage: StorageProtocol) { + val ownPublicKey = storage.getUserPublicKey() ?: return + val config = ConfigurationMessage.getCurrent(listOf()) ?: return + setName(config.displayName) + val picUrl = config.profilePicture + val picKey = config.profileKey + if (!picUrl.isNullOrEmpty() && picKey.isNotEmpty()) { + setPic(UserPic(picUrl, picKey)) + } + val ownThreadId = storage.getThreadId(Address.fromSerialized(ownPublicKey)) + setNtsPriority( + if (ownThreadId != null) + if (storage.isPinned(ownThreadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE + else ConfigBase.PRIORITY_HIDDEN + ) +} + +private fun MutableContacts.initFrom(storage: StorageProtocol) { + val localUserKey = storage.getUserPublicKey() ?: return + val contactsWithSettings = storage.getAllContacts().filter { recipient -> + recipient.accountID != localUserKey && recipient.accountID.startsWith(IdPrefix.STANDARD.value) + && storage.getThreadId(recipient.accountID) != null + }.map { contact -> + val address = Address.fromSerialized(contact.accountID) + val thread = storage.getThreadId(address) + val isPinned = if (thread != null) { + storage.isPinned(thread) + } else false + + Triple(contact, storage.getRecipientSettings(address)!!, isPinned) + } + for ((contact, settings, isPinned) in contactsWithSettings) { + val url = contact.profilePictureURL + val key = contact.profilePictureEncryptionKey + val userPic = if (url.isNullOrEmpty() || key?.isNotEmpty() != true) { + null + } else { + UserPic(url, key) + } + + val contactInfo = Contact( + id = contact.accountID, + name = contact.name.orEmpty(), + nickname = contact.nickname.orEmpty(), + blocked = settings.isBlocked, + approved = settings.isApproved, + approvedMe = settings.hasApprovedMe(), + profilePicture = userPic ?: UserPic.DEFAULT, + priority = if (isPinned) 1 else 0, + expiryMode = if (settings.expireMessages == 0) ExpiryMode.NONE else ExpiryMode.AfterRead(settings.expireMessages.toLong()) + ) + set(contactInfo) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PollerFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PollerFactory.kt index 996abdf47f..c5e420afab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/PollerFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/PollerFactory.kt @@ -3,9 +3,8 @@ package org.thoughtcrime.securesms.dependencies import dagger.Lazy import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.plus import network.loki.messenger.libsession_util.util.GroupInfo +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller import org.session.libsignal.utilities.AccountId @@ -16,23 +15,29 @@ class PollerFactory( private val executor: CoroutineDispatcher, private val configFactory: ConfigFactory, private val groupManagerV2: Lazy, + private val storage: StorageProtocol, ) { private val pollers = ConcurrentHashMap() fun pollerFor(sessionId: AccountId): ClosedGroupPoller? { // Check if the group is currently in our config and approved, don't start if it isn't - if (configFactory.userGroups?.getClosedGroup(sessionId.hexString)?.invited != false) return null + val invited = configFactory.withUserConfigs { + it.userGroups.getClosedGroup(sessionId.hexString)?.invited + } + + if (invited != false) return null return pollers.getOrPut(sessionId) { - ClosedGroupPoller(scope + SupervisorJob(), executor, sessionId, configFactory, groupManagerV2.get()) + ClosedGroupPoller(scope, executor, sessionId, configFactory, groupManagerV2.get(), storage) } } fun startAll() { - configFactory.userGroups?.allClosedGroupInfo()?.filterNot(GroupInfo.ClosedGroupInfo::invited)?.forEach { - pollerFor(it.groupAccountId)?.start() - } + configFactory + .withUserConfigs { it.userGroups.allClosedGroupInfo() } + .filterNot(GroupInfo.ClosedGroupInfo::invited) + .forEach { pollerFor(it.groupAccountId)?.start() } } fun stopAll() { @@ -42,7 +47,8 @@ class PollerFactory( } fun updatePollers() { - val currentGroups = configFactory.userGroups?.allClosedGroupInfo()?.filterNot(GroupInfo.ClosedGroupInfo::invited) ?: return + val currentGroups = configFactory + .withUserConfigs { it.userGroups.allClosedGroupInfo() }.filterNot(GroupInfo.ClosedGroupInfo::invited) val toRemove = pollers.filter { (id, _) -> id !in currentGroups.map { it.groupAccountId } } toRemove.forEach { (id, _) -> pollers.remove(id)?.stop() diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt index 045727968e..d040f87b04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SessionUtilModule.kt @@ -12,40 +12,32 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 -import org.session.libsession.utilities.ConfigFactoryUpdateListener -import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.database.ConfigDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import javax.inject.Named import javax.inject.Singleton +@Suppress("OPT_IN_USAGE") @Module @InstallIn(SingletonComponent::class) object SessionUtilModule { - const val POLLER_SCOPE = "poller_coroutine_scope" - - private fun maybeUserEdSecretKey(context: Context): ByteArray? { - val edKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return null - return edKey.secretKey.asBytes - } + private const val POLLER_SCOPE = "poller_coroutine_scope" @Provides @Singleton - fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory = - ConfigFactory(context, configDatabase) { - val localUserPublicKey = TextSecurePreferences.getLocalNumber(context) - val secretKey = maybeUserEdSecretKey(context) - if (localUserPublicKey == null || secretKey == null) null - else secretKey to localUserPublicKey - }.apply { - registerListener(context as ConfigFactoryUpdateListener) - } + fun provideConfigFactory( + @ApplicationContext context: Context, + configDatabase: ConfigDatabase, + storageProtocol: StorageProtocol, + threadDatabase: ThreadDatabase, + ): ConfigFactory = ConfigFactory(context, configDatabase, threadDatabase, storageProtocol) @Provides @Named(POLLER_SCOPE) - fun providePollerScope(@ApplicationContext applicationContext: Context): CoroutineScope = GlobalScope + fun providePollerScope(): CoroutineScope = GlobalScope @OptIn(ExperimentalCoroutinesApi::class) @Provides @@ -57,6 +49,12 @@ object SessionUtilModule { fun providePollerFactory(@Named(POLLER_SCOPE) coroutineScope: CoroutineScope, @Named(POLLER_SCOPE) dispatcher: CoroutineDispatcher, configFactory: ConfigFactory, - groupManagerV2: Lazy) = PollerFactory(coroutineScope, dispatcher, configFactory, groupManagerV2) - + storage: StorageProtocol, + groupManagerV2: Lazy) = PollerFactory( + scope = coroutineScope, + executor = dispatcher, + configFactory = configFactory, + groupManagerV2 = groupManagerV2, + storage = storage + ) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt index 96c0c7c882..e6acdd506e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupManager.kt @@ -34,22 +34,25 @@ object ClosedGroupManager { } fun ConfigFactory.updateLegacyGroup(group: GroupRecord) { - val groups = userGroups ?: return if (!group.isLegacyClosedGroup) return val storage = MessagingModuleConfiguration.shared.storage val threadId = storage.getThreadId(group.encodedId) ?: return val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId()) val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return - val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey) - val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize)) - val toSet = legacyInfo.copy( - members = latestMemberMap, - name = group.title, - priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, - encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte - encSecKey = latestKeyPair.privateKey.serialize() - ) - groups.set(toSet) - } + withMutableUserConfigs { + val groups = it.userGroups + + val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey) + val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize)) + val toSet = legacyInfo.copy( + members = latestMemberMap, + name = group.title, + priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, + encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte + encSecKey = latestKeyPair.privateKey.serialize() + ) + groups.set(toSet) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt index 482d22f004..111d0f2156 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt @@ -21,7 +21,6 @@ import network.loki.messenger.libsession_util.util.GroupMember import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.groups.GroupManagerV2 -import org.session.libsession.messaging.jobs.InviteContactsJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.dependencies.ConfigFactory diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index e381db8fed..2e3028ceca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -85,8 +85,7 @@ class GroupManagerV2Impl @Inject constructor( */ private fun requireAdminAccess(group: AccountId): ByteArray { return checkNotNull(configFactory - .userGroups - ?.getClosedGroup(group.hexString) + .withUserConfigs { it.userGroups.getClosedGroup(group.hexString) } ?.adminKey ?.takeIf { it.isNotEmpty() }) { "Only admin is allowed to invite members" } } @@ -96,10 +95,6 @@ class GroupManagerV2Impl @Inject constructor( groupDescription: String, members: Set ): Recipient = withContext(dispatcher) { - val userGroupsConfig = - requireNotNull(configFactory.userGroups) { "User groups config is not available" } - val convoVolatileConfig = - requireNotNull(configFactory.convoVolatile) { "Conversation volatile config is not available" } val ourAccountId = requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" } val ourKeys = @@ -109,25 +104,23 @@ class GroupManagerV2Impl @Inject constructor( val groupCreationTimestamp = SnodeAPI.nowWithOffset // Create a group in the user groups config - val group = userGroupsConfig.createGroup() + val group = configFactory.withMutableUserConfigs { configs -> + configs.userGroups.createGroup().also(configs.userGroups::set) + } + val adminKey = checkNotNull(group.adminKey) { "Admin key is null for new group creation." } - userGroupsConfig.set(group) val groupId = group.groupAccountId val groupAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey) try { - withNewGroupConfigs( - groupId = groupId, - userSecretKey = ourKeys.secretKey.asBytes, - groupAdminKey = adminKey - ) { infoConfig, membersConfig, keysConfig -> + configFactory.withMutableGroupConfigs(groupId) { configs -> // Update group's information - infoConfig.setName(groupName) - infoConfig.setDescription(groupDescription) + configs.groupInfo.setName(groupName) + configs.groupInfo.setDescription(groupDescription) // Add members for (member in members) { - membersConfig.set( + configs.groupMembers.set( GroupMember( sessionId = member.accountID, name = member.name, @@ -138,7 +131,7 @@ class GroupManagerV2Impl @Inject constructor( } // Add ourselves as admin - membersConfig.set( + configs.groupMembers.set( GroupMember( sessionId = ourAccountId, name = ourProfile.displayName, @@ -148,151 +141,48 @@ class GroupManagerV2Impl @Inject constructor( ) // Manually re-key to prevent issue with linked admin devices - keysConfig.rekey(infoConfig, membersConfig) + configs.rekeys() + } - - val configTtl = 14 * 24 * 60 * 60 * 1000L // 14 days - - // Push keys - val pendingKey = requireNotNull(keysConfig.pendingConfig()) { - "Expect pending keys data to push but got none" - } - - val pushKeys = async { - SnodeAPI.sendBatchRequest( - groupId, - SnodeAPI.buildAuthenticatedStoreBatchInfo( - namespace = keysConfig.namespace(), - message = SnodeMessage( - recipient = groupId.hexString, - data = Base64.encodeBytes(pendingKey), - ttl = configTtl, - timestamp = groupCreationTimestamp - ), - auth = groupAuth - ), - StoreMessageResponse::class.java - ) - } - - // Push info - val pushInfo = async { - val (infoPush, infoSeqNo) = infoConfig.push() - - infoSeqNo to SnodeAPI.sendBatchRequest( - groupId, - SnodeAPI.buildAuthenticatedStoreBatchInfo( - namespace = infoConfig.namespace(), - message = SnodeMessage( - recipient = groupId.hexString, - data = Base64.encodeBytes(infoPush), - ttl = configTtl, - timestamp = groupCreationTimestamp - ), - auth = groupAuth - ), - StoreMessageResponse::class.java - ) - } - - // Members push - val pushMembers = async { - val (membersPush, membersSeqNo) = membersConfig.push() - - membersSeqNo to SnodeAPI.sendBatchRequest( - groupId, - SnodeAPI.buildAuthenticatedStoreBatchInfo( - namespace = membersConfig.namespace(), - message = SnodeMessage( - recipient = groupId.hexString, - data = Base64.encodeBytes(membersPush), - ttl = configTtl, - timestamp = groupCreationTimestamp - ), - auth = groupAuth - ), - StoreMessageResponse::class.java - ) - } - - - // Wait for all the push requests to finish then update the configs - val (keyHash, keyTimestamp) = pushKeys.await() - val (infoSeqNo, infoHash) = pushInfo.await() - val (membersSeqNo, membersHash) = pushMembers.await() - - keysConfig.loadKey(pendingKey, keyHash, keyTimestamp, infoConfig, membersConfig) - infoConfig.confirmPushed(infoSeqNo, infoHash.hash) - membersConfig.confirmPushed(membersSeqNo, membersHash.hash) - - configFactory.saveGroupConfigs(keysConfig, infoConfig, membersConfig) - - // Add a new conversation into the volatile convo config and sync - convoVolatileConfig.set( + configFactory.withMutableUserConfigs { + it.convoInfoVolatile.set( Conversation.ClosedGroup( groupId.hexString, groupCreationTimestamp, false ) ) - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application) - - val recipient = - Recipient.from(application, Address.fromSerialized(groupId.hexString), false) - - // Apply various data locally - profileManager.setName(application, recipient, groupName) - storage.setRecipientApprovedMe(recipient, true) - storage.setRecipientApproved(recipient, true) - pollerFactory.updatePollers() - - // Invite members - JobQueue.shared.add( - InviteContactsJob( - groupSessionId = groupId.hexString, - memberSessionIds = members.map { it.accountID }.toTypedArray() - ) - ) - - recipient } + + val recipient = + Recipient.from(application, Address.fromSerialized(groupId.hexString), false) + + // Apply various data locally + profileManager.setName(application, recipient, groupName) + storage.setRecipientApprovedMe(recipient, true) + storage.setRecipientApproved(recipient, true) + pollerFactory.updatePollers() + + // Invite members + JobQueue.shared.add( + InviteContactsJob( + groupSessionId = groupId.hexString, + memberSessionIds = members.map { it.accountID }.toTypedArray() + ) + ) + + recipient } catch (e: Exception) { Log.e(TAG, "Failed to create group", e) // Remove the group from the user groups config is sufficient as a "rollback" - userGroupsConfig.erase(group) + configFactory.withMutableUserConfigs { + it.userGroups.eraseClosedGroup(groupId.hexString) + } throw e } } - private suspend fun withNewGroupConfigs( - groupId: AccountId, - userSecretKey: ByteArray, - groupAdminKey: ByteArray, - block: suspend CoroutineScope.(GroupInfoConfig, GroupMembersConfig, GroupKeysConfig) -> T - ): T { - return GroupInfoConfig.newInstance( - pubKey = groupId.pubKeyBytes, - secretKey = groupAdminKey - ).use { infoConfig -> - GroupMembersConfig.newInstance( - pubKey = groupId.pubKeyBytes, - secretKey = groupAdminKey - ).use { membersConfig -> - GroupKeysConfig.newInstance( - userSecretKey = userSecretKey, - groupPublicKey = groupId.pubKeyBytes, - groupSecretKey = groupAdminKey, - info = infoConfig, - members = membersConfig - ).use { keysConfig -> - coroutineScope { - this.block(infoConfig, membersConfig, keysConfig) - } - } - } - } - } override suspend fun inviteMembers( group: AccountId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt index bcf12b3920..8590372cf4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinCommunityFragment.kt @@ -116,9 +116,6 @@ class JoinCommunityFragment : Fragment() { GroupManager.getOpenGroupThreadID(openGroupID, requireContext()) val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded( - requireContext() - ) withContext(Dispatchers.Main) { val recipient = Recipient.from( requireContext(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index b8f3ba8012..095a505dd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -14,7 +14,6 @@ import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPolle import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities object OpenGroupManager { private val executorService = Executors.newScheduledThreadPool(4) @@ -131,8 +130,10 @@ object OpenGroupManager { pollers.remove(server) } } - configFactory.userGroups?.eraseCommunity(server, room) - configFactory.convoVolatile?.eraseCommunity(server, room) + configFactory.withMutableUserConfigs { + it.userGroups.eraseCommunity(server, room) + it.convoInfoVolatile.eraseCommunity(server, room) + } // Delete storage.removeLastDeletionServerID(room, server) storage.removeLastMessageServerID(room, server) @@ -142,7 +143,6 @@ object OpenGroupManager { lokiThreadDB.removeOpenGroupChat(threadID) storage.deleteConversation(threadID) // Must be invoked on a background thread GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } catch (e: Exception) { Log.e("Loki", "Failed to leave (delete) community", e) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt index a4c00f5845..f6b177de6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt @@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding import org.session.libsession.utilities.GroupRecord -import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.getConversationUnread @@ -119,7 +118,8 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto binding.leaveTextView.isVisible = recipient.isGroupRecipient && isCurrentUserInGroup binding.leaveTextView.setOnClickListener(this) - binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 || configFactory.convoVolatile?.getConversationUnread(thread) == true + binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 || + configFactory.withUserConfigs { it.convoInfoVolatile.getConversationUnread(thread) } binding.markAllAsReadTextView.setOnClickListener(this) binding.pinTextView.isVisible = !thread.isPinned binding.unpinTextView.isVisible = thread.isPinned diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 66e121fc50..2243aaea91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -97,7 +97,7 @@ class ConversationView : LinearLayout { val textSize = if (unreadCount < 1000) 12.0f else 10.0f binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) - || (configFactory.convoVolatile?.getConversationUnread(thread) == true) + || (configFactory.withUserConfigs { it.convoInfoVolatile.getConversationUnread(thread) }) binding.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup) val senderDisplayName = getTitle(thread.recipient) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index c6a4e92272..93be4d7d90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -294,9 +294,12 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), .request(Manifest.permission.POST_NOTIFICATIONS) .execute() } - configFactory.user - ?.takeUnless { it.isBlockCommunityMessageRequestsSet() } - ?.setCommunityMessageRequests(false) + + configFactory.withMutableUserConfigs { + if (!it.userProfile.isBlockCommunityMessageRequestsSet()) { + it.userProfile.setCommunityMessageRequests(false) + } + } } } @@ -378,11 +381,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } updateLegacyConfigView() - - // Sync config changes if there are any - lifecycleScope.launch(Dispatchers.IO) { - ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity) - } } override fun onPause() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt index 89f02ee21a..33471195ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDiffUtil.kt @@ -46,7 +46,7 @@ class HomeDiffUtil( oldItem.isSent == newItem.isSent && oldItem.isPending == newItem.isPending && oldItem.lastSeen == newItem.lastSeen && - configFactory.convoVolatile?.getConversationUnread(newItem) != true && + !configFactory.withUserConfigs { it.convoInfoVolatile.getConversationUnread(newItem) } && old.typingThreadIDs.contains(oldItem.threadId) == new.typingThreadIDs.contains(newItem.threadId) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt index 0e9865c01e..341831472f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsActivity.kt @@ -105,9 +105,6 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat fun doDecline() { viewModel.deleteMessageRequest(thread) LoaderManager.getInstance(this).restartLoader(0, null, this) - lifecycleScope.launch(Dispatchers.IO) { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity) - } } showSessionDialog { @@ -132,9 +129,6 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat fun doDeleteAllAndBlock() { viewModel.clearAllMessageRequests(false) LoaderManager.getInstance(this).restartLoader(0, null, this) - lifecycleScope.launch(Dispatchers.IO) { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity) - } } showSessionDialog { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index d592836440..acb41e3383 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -10,7 +10,6 @@ import com.goterl.lazysodium.utils.Key import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.serialization.json.Json import network.loki.messenger.R -import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters @@ -19,13 +18,9 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities.sodium -import org.session.libsession.snode.GroupSubAccountSwarmAuth -import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.utilities.bencode.Bencode import org.session.libsession.utilities.bencode.BencodeList import org.session.libsession.utilities.bencode.BencodeString -import org.session.libsession.utilities.withGroupConfigsOrNull -import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos.Envelope import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 @@ -92,17 +87,15 @@ class PushReceiver @Inject constructor( } private fun tryDecryptGroupMessage(groupId: AccountId, data: ByteArray): Envelope? { - return configFactory.withGroupConfigsOrNull(groupId) { _, _, keys -> - val (envelopBytes, sender) = checkNotNull(keys.decrypt(data)) { - "Failed to decrypt group message" - } - - Log.d(TAG, "Successfully decrypted group message from ${sender.hexString}") - Envelope.parseFrom(envelopBytes) - .toBuilder() - .setSource(sender.hexString) - .build() + val (envelopBytes, sender) = checkNotNull(configFactory.withGroupConfigs(groupId) { it.groupKeys.decrypt(data) }) { + "Failed to decrypt group message" } + + Log.d(TAG, "Successfully decrypted group message from ${sender.hexString}") + return Envelope.parseFrom(envelopBytes) + .toBuilder() + .setSource(sender.hexString) + .build() } private fun onPush() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistrationHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistrationHandler.kt index 052d285ff4..a8382477de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistrationHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistrationHandler.kt @@ -20,11 +20,9 @@ import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.GroupMembersConfig import org.session.libsession.database.userAuth import org.session.libsession.messaging.notifications.TokenFetcher -import org.session.libsession.snode.GroupSubAccountSwarmAuth import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.snode.SwarmAuth import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.withGroupConfigsOrNull import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Namespace diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt index 40bf577255..d6f8c99256 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt @@ -43,9 +43,9 @@ class CreateAccountManager @Inject constructor( prefs.setLocalNumber(userHexEncodedPublicKey) prefs.setRestorationTime(0) - // we'll rely on the config syncing in the homeActivity resume - configFactory.keyPairChanged() - configFactory.user?.setName(displayName) + configFactory.withMutableUserConfigs { + it.userProfile.setName(displayName) + } versionDataFetcher.startTimedVersionCheck() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt index 51d1b24609..ad56c93922 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt @@ -19,7 +19,6 @@ import javax.inject.Singleton @Singleton class LoadAccountManager @Inject constructor( @dagger.hilt.android.qualifiers.ApplicationContext private val context: Context, - private val configFactory: ConfigFactory, private val prefs: TextSecurePreferences, private val versionDataFetcher: VersionDataFetcher ) { @@ -44,7 +43,6 @@ class LoadAccountManager @Inject constructor( val keyPairGenerationResult = KeyPairUtilities.generate(seed) val x25519KeyPair = keyPairGenerationResult.x25519KeyPair KeyPairUtilities.store(context, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) - configFactory.keyPairChanged() val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey val registrationID = org.session.libsignal.utilities.KeyHelper.generateRegistrationId(false) prefs.apply { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt index d17c6f602c..9bb14d41d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt @@ -49,9 +49,9 @@ internal class PickDisplayNameViewModel( viewModelScope.launch(Dispatchers.IO) { if (loadFailed) { prefs.setProfileName(displayName) - // we'll rely on the config syncing in the homeActivity resume - configFactory.user?.setName(displayName) - + configFactory.withMutableUserConfigs { + it.userProfile.setName(displayName) + } _events.emit(Event.LoadAccountComplete) } else _events.emit(Event.CreateAccount(displayName)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt index fe2f94e093..4600c0a4a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt @@ -8,7 +8,6 @@ import android.widget.Toast import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.DividerItemDecoration import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Main @@ -24,7 +23,6 @@ import org.session.libsession.snode.SnodeAPI import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.createSessionDialog -import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import javax.inject.Inject @@ -124,15 +122,6 @@ class ClearAllDataDialog : DialogFragment() { } private suspend fun performDeleteLocalDataOnlyStep() { - try { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext()) - } catch (e: Exception) { - Log.e(TAG, "Failed to force sync when deleting data", e) - withContext(Main) { - Toast.makeText(ApplicationContext.getInstance(requireContext()), R.string.errorUnknown, Toast.LENGTH_LONG).show() - } - return - } ApplicationContext.getInstance(context).clearAllDataAndRestart().let { success -> withContext(Main) { if (success) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt index a2170e2cf7..8b163c43fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/PrivacySettingsPreferenceFragment.kt @@ -38,31 +38,32 @@ class PrivacySettingsPreferenceFragment : CorrectedPreferenceFragment() { findPreference(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!! .onPreferenceChangeListener = CallToggleListener(this) { setCall(it) } findPreference(getString(R.string.sessionMessageRequests))?.let { category -> - when (val user = configFactory.user) { - null -> category.isVisible = false - else -> SwitchPreferenceCompat(requireContext()).apply { - key = TextSecurePreferences.ALLOW_MESSAGE_REQUESTS - preferenceDataStore = object : PreferenceDataStore() { + SwitchPreferenceCompat(requireContext()).apply { + key = TextSecurePreferences.ALLOW_MESSAGE_REQUESTS + preferenceDataStore = object : PreferenceDataStore() { - override fun getBoolean(key: String?, defValue: Boolean): Boolean { - if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) { - return user.getCommunityMessageRequests() + override fun getBoolean(key: String?, defValue: Boolean): Boolean { + if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) { + return configFactory.withMutableUserConfigs { + it.userProfile.getCommunityMessageRequests() } - return super.getBoolean(key, defValue) - } - - override fun putBoolean(key: String?, value: Boolean) { - if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) { - user.setCommunityMessageRequests(value) - return - } - super.putBoolean(key, value) } + return super.getBoolean(key, defValue) } - title = getString(R.string.messageRequestsCommunities) - summary = getString(R.string.messageRequestsCommunitiesDescription) - }.let(category::addPreference) - } + + override fun putBoolean(key: String?, value: Boolean) { + if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) { + configFactory.withMutableUserConfigs { + it.userProfile.setCommunityMessageRequests(value) + } + return + } + super.putBoolean(key, value) + } + } + title = getString(R.string.messageRequestsCommunities) + summary = getString(R.string.messageRequestsCommunitiesDescription) + }.let(category::addPreference) } initializeVisibility() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 51a634187a..0d9ccb3b37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -295,16 +295,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } else { // if we have a network connection then attempt to update the display name TextSecurePreferences.setProfileName(this, displayName) - val user = viewModel.getUser() - if (user == null) { - Log.w(TAG, "Cannot update display name - missing user details from configFactory.") - } else { - user.setName(displayName) - // sync remote config - ConfigurationMessageUtilities.syncConfigurationIfNeeded(this) - binding.btnGroupNameDisplay.text = displayName - updateWasSuccessful = true - } + viewModel.updateName(displayName) + binding.btnGroupNameDisplay.text = displayName + updateWasSuccessful = true } // Inform the user if we failed to update the display name diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt index bedc913109..91326608e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt @@ -20,7 +20,6 @@ import network.loki.messenger.R import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.avatars.AvatarHelper import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.ProfileKeyUtil import org.session.libsession.utilities.ProfilePictureUtilities @@ -35,7 +34,6 @@ import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogStat import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapUtil -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.NetworkUtils import java.io.File import java.io.IOException @@ -91,8 +89,6 @@ class SettingsViewModel @Inject constructor( fun getTempFile() = tempFile - fun getUser() = configFactory.user - fun onAvatarPicked(result: CropImageView.CropResult) { when { result.isSuccessful -> { @@ -181,7 +177,6 @@ class SettingsViewModel @Inject constructor( ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, context) // If the online portion of the update succeeded then update the local state - val userConfig = configFactory.user AvatarHelper.setAvatar( context, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)!!), @@ -204,18 +199,15 @@ class SettingsViewModel @Inject constructor( // If we have a URL and a profile key then set the user's profile picture if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) { - userConfig?.setPic(UserPic(url, profileKey)) + configFactory.withMutableUserConfigs { + it.userProfile.setPic(UserPic(url, profileKey)) + } } // update dialog state _avatarDialogState.value = AvatarDialogState.UserAvatar(userAddress) } - if (userConfig != null && userConfig.needsDump()) { - configFactory.persist(userConfig, SnodeAPI.nowWithOffset) - } - - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } catch (e: Exception){ // If the sync failed then inform the user Log.d(TAG, "Error syncing avatar: $e") withContext(Dispatchers.Main) { @@ -230,6 +222,12 @@ class SettingsViewModel @Inject constructor( } } + fun updateName(displayName: String) { + configFactory.withMutableUserConfigs { + it.userProfile.setName(displayName) + } + } + sealed class AvatarDialogState() { object NoAvatar : AvatarDialogState() data class UserAvatar(val address: Address) : AvatarDialogState() diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index bbcdb88b8b..a22b827e07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -161,8 +161,9 @@ class DefaultConversationRepository @Inject constructor( return false } - return configFactory.userGroups - ?.getClosedGroup(recipient.address.serialize())?.kicked == true + return configFactory.withUserConfigs { + it.userGroups.getClosedGroup(recipient.address.serialize())?.kicked == true + } } // This assumes that recipient.isContactRecipient is true diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt index 3a6a337a28..4cff0f87c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt @@ -13,7 +13,6 @@ import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import javax.inject.Inject import javax.inject.Singleton @@ -97,25 +96,24 @@ class ProfileManager @Inject constructor( } override fun contactUpdatedInternal(contact: Contact): String? { - val contactConfig = configFactory.contacts ?: return null if (contact.accountID == TextSecurePreferences.getLocalNumber(context)) return null val accountId = AccountId(contact.accountID) if (accountId.prefix != IdPrefix.STANDARD) return null // only internally store standard account IDs - contactConfig.upsertContact(contact.accountID) { - this.name = contact.name.orEmpty() - this.nickname = contact.nickname.orEmpty() - val url = contact.profilePictureURL - val key = contact.profilePictureEncryptionKey - if (!url.isNullOrEmpty() && key != null && key.size == 32) { - this.profilePicture = UserPic(url, key) - } else if (url.isNullOrEmpty() && key == null) { - this.profilePicture = UserPic.DEFAULT + return configFactory.withMutableUserConfigs { + val contactConfig = it.contacts + contactConfig.upsertContact(contact.accountID) { + this.name = contact.name.orEmpty() + this.nickname = contact.nickname.orEmpty() + val url = contact.profilePictureURL + val key = contact.profilePictureEncryptionKey + if (!url.isNullOrEmpty() && key != null && key.size == 32) { + this.profilePicture = UserPic(url, key) + } else if (url.isNullOrEmpty() && key == null) { + this.profilePicture = UserPic.DEFAULT + } } + contactConfig.get(contact.accountID)?.hashCode()?.toString() } - if (contactConfig.needsPush()) { - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) - } - return contactConfig.get(contact.accountID)?.hashCode()?.toString() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt index 9b3b8ce627..19f95c6374 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt @@ -1,254 +1,10 @@ package org.thoughtcrime.securesms.util -import android.content.Context -import network.loki.messenger.libsession_util.ConfigBase -import network.loki.messenger.libsession_util.Contacts -import network.loki.messenger.libsession_util.ConversationVolatileConfig -import network.loki.messenger.libsession_util.UserGroupsConfig -import network.loki.messenger.libsession_util.UserProfile -import network.loki.messenger.libsession_util.util.BaseCommunityInfo -import network.loki.messenger.libsession_util.util.Contact -import network.loki.messenger.libsession_util.util.ExpiryMode -import network.loki.messenger.libsession_util.util.GroupInfo -import network.loki.messenger.libsession_util.util.UserPic -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.jobs.ConfigurationSyncJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.messages.Destination -import org.session.libsession.messaging.messages.control.ConfigurationMessage -import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.WindowDebouncer -import org.session.libsignal.crypto.ecc.DjbECPublicKey -import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.IdPrefix -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import java.util.Timer -import java.util.concurrent.ConcurrentLinkedDeque object ConfigurationMessageUtilities { - private const val TAG = "ConfigMessageUtils" - - private val debouncer = WindowDebouncer(3000, Timer()) - private val destinationUpdater = Any() - private val pendingDestinations = ConcurrentLinkedDeque() - - private fun scheduleConfigSync(destination: Destination) { - synchronized(destinationUpdater) { - pendingDestinations.add(destination) - } - debouncer.publish { - // don't schedule job if we already have one - val storage = MessagingModuleConfiguration.shared.storage - val configFactory = MessagingModuleConfiguration.shared.configFactory - val destinations = synchronized(destinationUpdater) { - val objects = pendingDestinations.toList() - pendingDestinations.clear() - objects - } - destinations.forEach { destination -> - if (destination is Destination.ClosedGroup) { - // ensure we have the appropriate admin keys, skip this destination otherwise - val group = configFactory.userGroups?.getClosedGroup(destination.publicKey) ?: return@forEach - if (group.adminKey == null) return@forEach Log.w("ConfigurationSync", "Trying to schedule config sync for group we aren't an admin of") - } - val currentStorageJob = storage.getConfigSyncJob(destination) - if (currentStorageJob != null) { - (currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true) - return@publish - } - val newConfigSyncJob = ConfigurationSyncJob(destination) - JobQueue.shared.add(newConfigSyncJob) - } - } - } - - @JvmStatic - fun syncConfigurationIfNeeded(context: Context) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return - scheduleConfigSync(Destination.Contact(userPublicKey)) - } - - fun forceSyncConfigurationNowIfNeeded(destination: Destination) { - scheduleConfigSync(destination) - } - - - fun forceSyncConfigurationNowIfNeeded(context: Context) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Log.e("Loki", NullPointerException("User Public Key is null")) - // Schedule a new job if one doesn't already exist (only) - scheduleConfigSync(Destination.Contact(userPublicKey)) - } - - private fun maybeUserSecretKey() = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()?.secretKey?.asBytes - - fun generateUserProfileConfigDump(): ByteArray? { - val storage = MessagingModuleConfiguration.shared.storage - val ownPublicKey = storage.getUserPublicKey() ?: return null - val config = ConfigurationMessage.getCurrent(listOf()) ?: return null - val secretKey = maybeUserSecretKey() ?: return null - val profile = UserProfile.newInstance(secretKey) - profile.setName(config.displayName) - val picUrl = config.profilePicture - val picKey = config.profileKey - if (!picUrl.isNullOrEmpty() && picKey.isNotEmpty()) { - profile.setPic(UserPic(picUrl, picKey)) - } - val ownThreadId = storage.getThreadId(Address.fromSerialized(ownPublicKey)) - profile.setNtsPriority( - if (ownThreadId != null) - if (storage.isPinned(ownThreadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE - else ConfigBase.PRIORITY_HIDDEN - ) - val dump = profile.dump() - profile.free() - return dump - } - - fun generateContactConfigDump(): ByteArray? { - val secretKey = maybeUserSecretKey() ?: return null - val storage = MessagingModuleConfiguration.shared.storage - val localUserKey = storage.getUserPublicKey() ?: return null - val contactsWithSettings = storage.getAllContacts().filter { recipient -> - recipient.accountID != localUserKey && recipient.accountID.startsWith(IdPrefix.STANDARD.value) - && storage.getThreadId(recipient.accountID) != null - }.map { contact -> - val address = Address.fromSerialized(contact.accountID) - val thread = storage.getThreadId(address) - val isPinned = if (thread != null) { - storage.isPinned(thread) - } else false - - Triple(contact, storage.getRecipientSettings(address)!!, isPinned) - } - val contactConfig = Contacts.newInstance(secretKey) - for ((contact, settings, isPinned) in contactsWithSettings) { - val url = contact.profilePictureURL - val key = contact.profilePictureEncryptionKey - val userPic = if (url.isNullOrEmpty() || key?.isNotEmpty() != true) { - null - } else { - UserPic(url, key) - } - - val contactInfo = Contact( - id = contact.accountID, - name = contact.name.orEmpty(), - nickname = contact.nickname.orEmpty(), - blocked = settings.isBlocked, - approved = settings.isApproved, - approvedMe = settings.hasApprovedMe(), - profilePicture = userPic ?: UserPic.DEFAULT, - priority = if (isPinned) 1 else 0, - expiryMode = if (settings.expireMessages == 0) ExpiryMode.NONE else ExpiryMode.AfterRead(settings.expireMessages.toLong()) - ) - contactConfig.set(contactInfo) - } - val dump = contactConfig.dump() - contactConfig.free() - if (dump.isEmpty()) return null - return dump - } - - fun generateConversationVolatileDump(context: Context): ByteArray? { - val secretKey = maybeUserSecretKey() ?: return null - val storage = MessagingModuleConfiguration.shared.storage - val convoConfig = ConversationVolatileConfig.newInstance(secretKey) - val threadDb = DatabaseComponent.get(context).threadDatabase() - threadDb.approvedConversationList.use { cursor -> - val reader = threadDb.readerFor(cursor) - var current = reader.next - while (current != null) { - val recipient = current.recipient - val contact = when { - recipient.isCommunityRecipient -> { - val openGroup = storage.getOpenGroup(current.threadId) ?: continue - val (base, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: continue - convoConfig.getOrConstructCommunity(base, room, pubKey) - } - recipient.isClosedGroupV2Recipient -> { - // It's probably safe to assume there will never be a case where new closed groups will ever be there before a dump is created... - // but just in case... - convoConfig.getOrConstructClosedGroup(recipient.address.serialize()) - } - recipient.isLegacyClosedGroupRecipient -> { - val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) - convoConfig.getOrConstructLegacyGroup(groupPublicKey) - } - recipient.isContactRecipient -> { - if (recipient.isLocalNumber) null // this is handled by the user profile NTS data - else if (recipient.isOpenGroupInboxRecipient) null // specifically exclude - else if (!recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) null - else convoConfig.getOrConstructOneToOne(recipient.address.serialize()) - } - else -> null - } - if (contact == null) { - current = reader.next - continue - } - contact.lastRead = current.lastSeen - contact.unread = false - convoConfig.set(contact) - current = reader.next - } - } - - val dump = convoConfig.dump() - convoConfig.free() - if (dump.isEmpty()) return null - return dump - } - - fun generateUserGroupDump(context: Context): ByteArray? { - val secretKey = maybeUserSecretKey() ?: return null - val storage = MessagingModuleConfiguration.shared.storage - val groupConfig = UserGroupsConfig.newInstance(secretKey) - val allOpenGroups = storage.getAllOpenGroups().values.mapNotNull { openGroup -> - val (baseUrl, room, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@mapNotNull null - val pubKeyHex = Hex.toStringCondensed(pubKey) - val baseInfo = BaseCommunityInfo(baseUrl, room, pubKeyHex) - val threadId = storage.getThreadId(openGroup) ?: return@mapNotNull null - val isPinned = storage.isPinned(threadId) - GroupInfo.CommunityGroupInfo(baseInfo, if (isPinned) 1 else 0) - } - - val allLgc = storage.getAllGroups(includeInactive = false).filter { - it.isLegacyClosedGroup && it.isActive && it.members.size > 1 - }.mapNotNull { group -> - val groupAddress = Address.fromSerialized(group.encodedId) - val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupAddress.serialize()).toHexString() - val recipient = storage.getRecipientSettings(groupAddress) ?: return@mapNotNull null - val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return@mapNotNull null - val threadId = storage.getThreadId(group.encodedId) - val isPinned = threadId?.let { storage.isPinned(threadId) } ?: false - val admins = group.admins.map { it.serialize() to true }.toMap() - val members = group.members.filterNot { it.serialize() !in admins.keys }.map { it.serialize() to false }.toMap() - GroupInfo.LegacyGroupInfo( - accountId = groupPublicKey, - name = group.title, - members = admins + members, - priority = if (isPinned) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE, - encPubKey = (encryptionKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte - encSecKey = encryptionKeyPair.privateKey.serialize(), - disappearingTimer = recipient.expireMessages.toLong(), - joinedAt = (group.formationTimestamp / 1000L) - ) - } - (allOpenGroups + allLgc).forEach { groupInfo -> - groupConfig.set(groupInfo) - } - val dump = groupConfig.dump() - groupConfig.free() - if (dump.isEmpty()) return null - return dump - } - @JvmField val DELETE_INACTIVE_GROUPS: String = """ DELETE FROM ${GroupDatabase.TABLE_NAME} WHERE ${GroupDatabase.GROUP_ID} IN (SELECT ${ThreadDatabase.ADDRESS} FROM ${ThreadDatabase.TABLE_NAME} WHERE ${ThreadDatabase.MESSAGE_COUNT} <= 0 AND ${ThreadDatabase.ADDRESS} LIKE '${GroupUtil.LEGACY_CLOSED_GROUP_PREFIX}%'); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt index 99c6ff6b79..3c65458207 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SharedConfigUtils.kt @@ -1,12 +1,13 @@ package org.thoughtcrime.securesms.util import network.loki.messenger.libsession_util.ConversationVolatileConfig +import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.utilities.GroupUtil import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.database.model.ThreadRecord -fun ConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Boolean { +fun ReadableConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Boolean { val recipient = thread.recipient if (recipient.isContactRecipient && recipient.isOpenGroupInboxRecipient diff --git a/libsession-util/src/main/cpp/CMakeLists.txt b/libsession-util/src/main/cpp/CMakeLists.txt index 72cbc387fa..2e2f8410a8 100644 --- a/libsession-util/src/main/cpp/CMakeLists.txt +++ b/libsession-util/src/main/cpp/CMakeLists.txt @@ -34,7 +34,9 @@ set(SOURCES util.cpp group_members.cpp group_keys.cpp - group_info.cpp) + group_info.cpp + config_common.cpp +) add_library( # Sets the name of the library. session_util diff --git a/libsession-util/src/main/cpp/config_common.cpp b/libsession-util/src/main/cpp/config_common.cpp new file mode 100644 index 0000000000..74e57305e6 --- /dev/null +++ b/libsession-util/src/main/cpp/config_common.cpp @@ -0,0 +1,39 @@ +#include +#include "util.h" +#include "jni_utils.h" + +#include +#include +#include +#include + +extern "C" +JNIEXPORT jlong JNICALL +Java_network_loki_messenger_libsession_1util_ConfigKt_createConfigObject( + JNIEnv *env, + jclass _clazz, + jstring java_config_name, + jbyteArray ed25519_secret_key, + jbyteArray initial_dump) { + return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { + auto config_name = util::string_from_jstring(env, java_config_name); + auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); + auto initial = initial_dump + ? std::optional(util::ustring_from_bytes(env, initial_dump)) + : std::nullopt; + + + std::lock_guard lock{util::util_mutex_}; + if (config_name == "Contacts") { + return reinterpret_cast(new session::config::Contacts(secret_key, initial)); + } else if (config_name == "UserProfile") { + return reinterpret_cast(new session::config::UserProfile(secret_key, initial)); + } else if (config_name == "UserGroups") { + return reinterpret_cast(new session::config::UserGroups(secret_key, initial)); + } else if (config_name == "ConversationVolatileConfig") { + return reinterpret_cast(new session::config::ConvoInfoVolatile(secret_key, initial)); + } else { + throw std::invalid_argument("Unknown config name: " + config_name); + } + }); +} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/contacts.cpp b/libsession-util/src/main/cpp/contacts.cpp index d064ccd683..ac756b7bed 100644 --- a/libsession-util/src/main/cpp/contacts.cpp +++ b/libsession-util/src/main/cpp/contacts.cpp @@ -62,46 +62,7 @@ Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject return result; }); } -extern "C" -#pragma clang diagnostic push -#pragma ide diagnostic ignored "bugprone-reserved-identifier" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env, - jobject thiz, - jbyteArray ed25519_secret_key) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); - auto *contacts = new session::config::Contacts(secret_key, std::nullopt); - jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts"); - jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V"); - jobject newConfig = env->NewObject(contactsClass, constructor, - reinterpret_cast(contacts)); - - return newConfig; - }); -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B( - JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); - auto initial = util::ustring_from_bytes(env, initial_dump); - - auto *contacts = new session::config::Contacts(secret_key, initial); - - jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts"); - jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V"); - jobject newConfig = env->NewObject(contactsClass, constructor, - reinterpret_cast(contacts)); - - return newConfig; - }); -} -#pragma clang diagnostic pop extern "C" JNIEXPORT jobject JNICALL Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) { @@ -118,4 +79,4 @@ Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject t } return our_stack; }); -} \ No newline at end of file +} diff --git a/libsession-util/src/main/cpp/conversation.cpp b/libsession-util/src/main/cpp/conversation.cpp index 64765c8f80..3fb322533b 100644 --- a/libsession-util/src/main/cpp/conversation.cpp +++ b/libsession-util/src/main/cpp/conversation.cpp @@ -1,41 +1,6 @@ #include #include "conversation.h" -#pragma clang diagnostic push - -extern "C" -#pragma ide diagnostic ignored "bugprone-reserved-identifier" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_00024Companion_newInstance___3B( - JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key) { - std::lock_guard lock{util::util_mutex_}; - auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); - auto* convo_info_volatile = new session::config::ConvoInfoVolatile(secret_key, std::nullopt); - - jclass convoClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig"); - jmethodID constructor = env->GetMethodID(convoClass, "", "(J)V"); - jobject newConfig = env->NewObject(convoClass, constructor, reinterpret_cast(convo_info_volatile)); - - return newConfig; -} -extern "C" -#pragma ide diagnostic ignored "bugprone-reserved-identifier" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_00024Companion_newInstance___3B_3B( - JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { - std::lock_guard lock{util::util_mutex_}; - auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); - auto initial = util::ustring_from_bytes(env, initial_dump); - auto* convo_info_volatile = new session::config::ConvoInfoVolatile(secret_key, initial); - - jclass convoClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig"); - jmethodID constructor = env->GetMethodID(convoClass, "", "(J)V"); - jobject newConfig = env->NewObject(convoClass, constructor, reinterpret_cast(convo_info_volatile)); - - return newConfig; -} - - extern "C" JNIEXPORT jint JNICALL @@ -46,7 +11,6 @@ Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeOneT return conversations->size_1to1(); } -#pragma clang diagnostic pop extern "C" JNIEXPORT jint JNICALL Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseAll(JNIEnv *env, @@ -406,4 +370,4 @@ Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseClo auto erased = config->erase_group(session_id_bytes); env->ReleaseStringUTFChars(session_id, session_id_bytes); return erased; -} \ No newline at end of file +} diff --git a/libsession-util/src/main/cpp/group_info.cpp b/libsession-util/src/main/cpp/group_info.cpp index 676178b245..c80db46d2a 100644 --- a/libsession-util/src/main/cpp/group_info.cpp +++ b/libsession-util/src/main/cpp/group_info.cpp @@ -3,7 +3,7 @@ #include "session/config/groups/info.hpp" extern "C" -JNIEXPORT jobject JNICALL +JNIEXPORT jlong JNICALL Java_network_loki_messenger_libsession_1util_GroupInfoConfig_00024Companion_newInstance(JNIEnv *env, jobject thiz, jbyteArray pub_key, @@ -17,18 +17,13 @@ Java_network_loki_messenger_libsession_1util_GroupInfoConfig_00024Companion_newI auto secret_key_bytes = util::ustring_from_bytes(env, secret_key); secret_key_optional = secret_key_bytes; } - if (env->GetArrayLength(initial_dump) > 0) { + if (initial_dump && env->GetArrayLength(initial_dump) > 0) { auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump); initial_dump_optional = initial_dump_bytes; } auto* group_info = new session::config::groups::Info(pub_key_bytes, secret_key_optional, initial_dump_optional); - - jclass groupInfoClass = env->FindClass("network/loki/messenger/libsession_util/GroupInfoConfig"); - jmethodID constructor = env->GetMethodID(groupInfoClass, "", "(J)V"); - jobject newConfig = env->NewObject(groupInfoClass, constructor, reinterpret_cast(group_info)); - - return newConfig; + return reinterpret_cast(group_info); } extern "C" diff --git a/libsession-util/src/main/cpp/group_keys.cpp b/libsession-util/src/main/cpp/group_keys.cpp index d2861c32a3..2c0ecd8231 100644 --- a/libsession-util/src/main/cpp/group_keys.cpp +++ b/libsession-util/src/main/cpp/group_keys.cpp @@ -10,15 +10,15 @@ JNIEXPORT jint JNICALL } extern "C" -JNIEXPORT jobject JNICALL +JNIEXPORT jlong JNICALL Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newInstance(JNIEnv *env, jobject thiz, jbyteArray user_secret_key, jbyteArray group_public_key, jbyteArray group_secret_key, jbyteArray initial_dump, - jobject info_jobject, - jobject members_jobject) { + jlong info_pointer, + jlong members_pointer) { std::lock_guard lock{util::util_mutex_}; auto user_key_bytes = util::ustring_from_bytes(env, user_secret_key); auto pub_key_bytes = util::ustring_from_bytes(env, group_public_key); @@ -35,8 +35,8 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newI initial_dump_optional = initial_dump_bytes; } - auto info = ptrToInfo(env, info_jobject); - auto members = ptrToMembers(env, members_jobject); + auto info = reinterpret_cast(info_pointer); + auto members = reinterpret_cast(members_pointer); auto* keys = new session::config::groups::Keys(user_key_bytes, pub_key_bytes, @@ -45,11 +45,7 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newI *info, *members); - jclass groupKeysConfig = env->FindClass("network/loki/messenger/libsession_util/GroupKeysConfig"); - jmethodID constructor = env->GetMethodID(groupKeysConfig, "", "(J)V"); - jobject newConfig = env->NewObject(groupKeysConfig, constructor, reinterpret_cast(keys)); - - return newConfig; + return reinterpret_cast(keys); } extern "C" @@ -75,14 +71,14 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_loadKey(JNIEnv *env jbyteArray message, jstring hash, jlong timestamp_ms, - jobject info_jobject, - jobject members_jobject) { + jlong info_ptr, + jlong members_ptr) { std::lock_guard lock{util::util_mutex_}; auto keys = ptrToKeys(env, thiz); auto message_bytes = util::ustring_from_bytes(env, message); auto hash_bytes = env->GetStringUTFChars(hash, nullptr); - auto info = ptrToInfo(env, info_jobject); - auto members = ptrToMembers(env, members_jobject); + auto info = reinterpret_cast(info_ptr); + auto members = reinterpret_cast(members_ptr); bool processed = keys->load_key_message(hash_bytes, message_bytes, timestamp_ms, *info, *members); env->ReleaseStringUTFChars(hash, hash_bytes); @@ -137,11 +133,11 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_pendingConfig(JNIEn extern "C" JNIEXPORT jbyteArray JNICALL Java_network_loki_messenger_libsession_1util_GroupKeysConfig_rekey(JNIEnv *env, jobject thiz, - jobject info_jobject, jobject members_jobject) { + jlong info_ptr, jlong members_ptr) { std::lock_guard lock{util::util_mutex_}; auto keys = ptrToKeys(env, thiz); - auto info = ptrToInfo(env, info_jobject); - auto members = ptrToMembers(env, members_jobject); + auto info = reinterpret_cast(info_ptr); + auto members = reinterpret_cast(members_ptr); auto rekey = keys->rekey(*info, *members); auto rekey_bytes = util::bytes_from_ustring(env, rekey.data()); return rekey_bytes; diff --git a/libsession-util/src/main/cpp/group_members.cpp b/libsession-util/src/main/cpp/group_members.cpp index a47b6068ab..1f469c47d5 100644 --- a/libsession-util/src/main/cpp/group_members.cpp +++ b/libsession-util/src/main/cpp/group_members.cpp @@ -1,7 +1,7 @@ #include "group_members.h" extern "C" -JNIEXPORT jobject JNICALL +JNIEXPORT jlong JNICALL Java_network_loki_messenger_libsession_1util_GroupMembersConfig_00024Companion_newInstance( JNIEnv *env, jobject thiz, jbyteArray pub_key, jbyteArray secret_key, jbyteArray initial_dump) { @@ -13,18 +13,13 @@ Java_network_loki_messenger_libsession_1util_GroupMembersConfig_00024Companion_n auto secret_key_bytes = util::ustring_from_bytes(env, secret_key); secret_key_optional = secret_key_bytes; } - if (env->GetArrayLength(initial_dump) > 0) { + if (initial_dump && env->GetArrayLength(initial_dump) > 0) { auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump); initial_dump_optional = initial_dump_bytes; } auto* group_members = new session::config::groups::Members(pub_key_bytes, secret_key_optional, initial_dump_optional); - - jclass groupMemberClass = env->FindClass("network/loki/messenger/libsession_util/GroupMembersConfig"); - jmethodID constructor = env->GetMethodID(groupMemberClass, "", "(J)V"); - jobject newConfig = env->NewObject(groupMemberClass, constructor, reinterpret_cast(group_members)); - - return newConfig; + return reinterpret_cast(group_members); } extern "C" diff --git a/libsession-util/src/main/cpp/user_groups.cpp b/libsession-util/src/main/cpp/user_groups.cpp index 3d63593e5c..4cf9f092ec 100644 --- a/libsession-util/src/main/cpp/user_groups.cpp +++ b/libsession-util/src/main/cpp/user_groups.cpp @@ -1,44 +1,6 @@ -#pragma clang diagnostic push -#pragma ide diagnostic ignored "bugprone-reserved-identifier" #include "user_groups.h" #include "oxenc/hex.h" -#pragma clang diagnostic push -#pragma ide diagnostic ignored "bugprone-reserved-identifier" -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_00024Companion_newInstance___3B( - JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key) { - std::lock_guard lock{util::util_mutex_}; - auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); - - auto* user_groups = new session::config::UserGroups(secret_key, std::nullopt); - - jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig"); - jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V"); - jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(user_groups)); - - return newConfig; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_00024Companion_newInstance___3B_3B( - JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { - std::lock_guard lock{util::util_mutex_}; - auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); - auto initial = util::ustring_from_bytes(env, initial_dump); - - auto* user_groups = new session::config::UserGroups(secret_key, initial); - - jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig"); - jmethodID constructor = env->GetMethodID(contactsClass, "", "(J)V"); - jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast(user_groups)); - - return newConfig; -} -#pragma clang diagnostic pop - extern "C" JNIEXPORT jint JNICALL Java_network_loki_messenger_libsession_1util_util_GroupInfo_00024LegacyGroupInfo_00024Companion_NAME_1MAX_1LENGTH( @@ -361,4 +323,4 @@ Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseClosedGroup(J bool return_value = config->erase_group(session_id_bytes); env->ReleaseStringUTFChars(session_id, session_id_bytes); return return_value; -} \ No newline at end of file +} diff --git a/libsession-util/src/main/cpp/user_profile.cpp b/libsession-util/src/main/cpp/user_profile.cpp index 519dcc8e2c..47def9b6b5 100644 --- a/libsession-util/src/main/cpp/user_profile.cpp +++ b/libsession-util/src/main/cpp/user_profile.cpp @@ -2,39 +2,6 @@ #include "util.h" extern "C" { -#pragma clang diagnostic push -#pragma ide diagnostic ignored "bugprone-reserved-identifier" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_00024Companion_newInstance___3B_3B( - JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) { - std::lock_guard lock{util::util_mutex_}; - auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); - auto initial = util::ustring_from_bytes(env, initial_dump); - auto* profile = new session::config::UserProfile(secret_key, std::optional(initial)); - - jclass userClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile"); - jmethodID constructor = env->GetMethodID(userClass, "", "(J)V"); - jobject newConfig = env->NewObject(userClass, constructor, reinterpret_cast(profile)); - - return newConfig; -} - -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_00024Companion_newInstance___3B( - JNIEnv* env, - jobject, - jbyteArray secretKey) { - std::lock_guard lock{util::util_mutex_}; - auto* profile = new session::config::UserProfile(util::ustring_from_bytes(env, secretKey), std::nullopt); - - jclass userClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile"); - jmethodID constructor = env->GetMethodID(userClass, "", "(J)V"); - jobject newConfig = env->NewObject(userClass, constructor, reinterpret_cast(profile)); - - return newConfig; -} -#pragma clang diagnostic pop - JNIEXPORT void JNICALL Java_network_loki_messenger_libsession_1util_UserProfile_setName( JNIEnv* env, @@ -149,4 +116,4 @@ Java_network_loki_messenger_libsession_1util_UserProfile_isBlockCommunityMessage std::lock_guard lock{util::util_mutex_}; auto profile = ptrToProfile(env, thiz); return profile->get_blinded_msgreqs().has_value(); -} \ No newline at end of file +} diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index 5bae7e50af..d09e7c8f93 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -16,15 +16,43 @@ import org.session.libsignal.utilities.Namespace import java.io.Closeable import java.util.Stack -sealed class Config(protected val pointer: Long): Closeable { +sealed class Config(initialPointer: Long): Closeable { + var pointer = initialPointer + private set + + init { + check(pointer != 0L) { "Pointer is null" } + } + abstract fun namespace(): Int - external fun free() - override fun close() { - free() + + private external fun free() + + final override fun close() { + if (pointer != 0L) { + free() + pointer = 0L + } } } -sealed class ConfigBase(pointer: Long): Config(pointer) { +interface ReadableConfig { + fun namespace(): Int + fun needsPush(): Boolean + fun needsDump(): Boolean + fun currentHashes(): List +} + +interface MutableConfig : ReadableConfig { + fun push(): ConfigPush + fun dump(): ByteArray + fun encryptionDomain(): String + fun confirmPushed(seqNo: Long, newHash: String) + fun merge(toMerge: Array>): Stack + fun dirty(): Boolean +} + +sealed class ConfigBase(pointer: Long): Config(pointer), MutableConfig { companion object { init { System.loadLibrary("session_util") @@ -46,42 +74,35 @@ sealed class ConfigBase(pointer: Long): Config(pointer) { } - external fun dirty(): Boolean - external fun needsPush(): Boolean - external fun needsDump(): Boolean - external fun push(): ConfigPush - external fun dump(): ByteArray - external fun encryptionDomain(): String - external fun confirmPushed(seqNo: Long, newHash: String) - external fun merge(toMerge: Array>): Stack - external fun currentHashes(): List + external override fun dirty(): Boolean + external override fun needsPush(): Boolean + external override fun needsDump(): Boolean + external override fun push(): ConfigPush + external override fun dump(): ByteArray + external override fun encryptionDomain(): String + external override fun confirmPushed(seqNo: Long, newHash: String) + external override fun merge(toMerge: Array>): Stack + external override fun currentHashes(): List // Singular merge external fun merge(toMerge: Pair): Stack - } -class Contacts(pointer: Long) : ConfigBase(pointer) { - companion object { - init { - System.loadLibrary("session_util") - } - external fun newInstance(ed25519SecretKey: ByteArray): Contacts - external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): Contacts - } - override fun namespace() = Namespace.CONTACTS() +interface ReadableContacts: ReadableConfig { + fun get(accountId: String): Contact? + fun all(): List +} - external fun get(accountId: String): Contact? - external fun getOrConstruct(accountId: String): Contact - external fun all(): List - external fun set(contact: Contact) - external fun erase(accountId: String): Boolean +interface MutableContacts : ReadableContacts, MutableConfig { + fun getOrConstruct(accountId: String): Contact + fun set(contact: Contact) + fun erase(accountId: String): Boolean /** * Similar to [updateIfExists], but will create the underlying contact if it doesn't exist before passing to [updateFunction] */ - fun upsertContact(accountId: String, updateFunction: Contact.()->Unit = {}) { + fun upsertContact(accountId: String, updateFunction: Contact.() -> Unit = {}) { when { accountId.startsWith(IdPrefix.BLINDED.value) -> Log.w("Loki", "Trying to create a contact with a blinded ID prefix") accountId.startsWith(IdPrefix.UN_BLINDED.value) -> Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix") @@ -92,251 +113,403 @@ class Contacts(pointer: Long) : ConfigBase(pointer) { } } } - - /** - * Updates the contact by accountId with a given [updateFunction], and applies to the underlying config. - * the [updateFunction] doesn't run if there is no contact - */ - private fun updateIfExists(accountId: String, updateFunction: Contact.()->Unit) { - when { - accountId.startsWith(IdPrefix.BLINDED.value) -> Log.w("Loki", "Trying to create a contact with a blinded ID prefix") - accountId.startsWith(IdPrefix.UN_BLINDED.value) -> Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix") - accountId.startsWith(IdPrefix.BLINDEDV2.value) -> Log.w("Loki", "Trying to create a contact with a blindedv2 ID prefix") - else -> get(accountId)?.let { - updateFunction(it) - set(it) - } - } - } } -class UserProfile(pointer: Long) : ConfigBase(pointer) { - companion object { - init { - System.loadLibrary("session_util") - } - external fun newInstance(ed25519SecretKey: ByteArray): UserProfile - external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserProfile - } +class Contacts private constructor(pointer: Long) : ConfigBase(pointer), MutableContacts { + constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this( + createConfigObject( + "Contacts", + ed25519SecretKey, + initialDump + ) + ) + + override fun namespace() = Namespace.CONTACTS() + + external override fun get(accountId: String): Contact? + external override fun getOrConstruct(accountId: String): Contact + external override fun all(): List + external override fun set(contact: Contact) + external override fun erase(accountId: String): Boolean +} + +interface ReadableUserProfile: ReadableConfig { + fun getName(): String? + fun getPic(): UserPic + fun getNtsPriority(): Long + fun getNtsExpiry(): ExpiryMode + fun getCommunityMessageRequests(): Boolean + fun isBlockCommunityMessageRequestsSet(): Boolean +} + +interface MutableUserProfile : ReadableUserProfile, MutableConfig { + fun setName(newName: String) + fun setPic(userPic: UserPic) + fun setNtsPriority(priority: Long) + fun setNtsExpiry(expiryMode: ExpiryMode) + fun setCommunityMessageRequests(blocks: Boolean) +} + +class UserProfile private constructor(pointer: Long) : ConfigBase(pointer), MutableUserProfile { + constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this( + createConfigObject( + "UserProfile", + ed25519SecretKey, + initialDump + ) + ) override fun namespace() = Namespace.USER_PROFILE() - external fun setName(newName: String) - external fun getName(): String? - external fun getPic(): UserPic - external fun setPic(userPic: UserPic) - external fun setNtsPriority(priority: Long) - external fun getNtsPriority(): Long - external fun setNtsExpiry(expiryMode: ExpiryMode) - external fun getNtsExpiry(): ExpiryMode - external fun getCommunityMessageRequests(): Boolean - external fun setCommunityMessageRequests(blocks: Boolean) - external fun isBlockCommunityMessageRequestsSet(): Boolean + external override fun setName(newName: String) + external override fun getName(): String? + external override fun getPic(): UserPic + external override fun setPic(userPic: UserPic) + external override fun setNtsPriority(priority: Long) + external override fun getNtsPriority(): Long + external override fun setNtsExpiry(expiryMode: ExpiryMode) + external override fun getNtsExpiry(): ExpiryMode + external override fun getCommunityMessageRequests(): Boolean + external override fun setCommunityMessageRequests(blocks: Boolean) + external override fun isBlockCommunityMessageRequestsSet(): Boolean } -class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { - companion object { - init { - System.loadLibrary("session_util") - } - external fun newInstance(ed25519SecretKey: ByteArray): ConversationVolatileConfig - external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): ConversationVolatileConfig - } +interface ReadableConversationVolatileConfig: ReadableConfig { + fun getOneToOne(pubKeyHex: String): Conversation.OneToOne? + fun getCommunity(baseUrl: String, room: String): Conversation.Community? + fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup? + fun getClosedGroup(sessionId: String): Conversation.ClosedGroup? + fun sizeOneToOnes(): Int + fun sizeCommunities(): Int + fun sizeLegacyClosedGroups(): Int + fun size(): Int + + fun empty(): Boolean + + fun allOneToOnes(): List + fun allCommunities(): List + fun allLegacyClosedGroups(): List + fun allClosedGroups(): List + fun all(): List +} + +interface MutableConversationVolatileConfig : ReadableConversationVolatileConfig, MutableConfig { + fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne + fun eraseOneToOne(pubKeyHex: String): Boolean + + fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community + fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community + fun eraseCommunity(community: Conversation.Community): Boolean + fun eraseCommunity(baseUrl: String, room: String): Boolean + + fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup + fun eraseLegacyClosedGroup(groupId: String): Boolean + + fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup + fun eraseClosedGroup(sessionId: String): Boolean + + fun erase(conversation: Conversation): Boolean + fun set(toStore: Conversation) + + fun eraseAll(predicate: (Conversation) -> Boolean): Int +} + + +class ConversationVolatileConfig private constructor(pointer: Long): ConfigBase(pointer), MutableConversationVolatileConfig { + constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this( + createConfigObject( + "ConvoInfoVolatile", + ed25519SecretKey, + initialDump + ) + ) override fun namespace() = Namespace.CONVO_INFO_VOLATILE() - external fun getOneToOne(pubKeyHex: String): Conversation.OneToOne? - external fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne - external fun eraseOneToOne(pubKeyHex: String): Boolean + external override fun getOneToOne(pubKeyHex: String): Conversation.OneToOne? + external override fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne + external override fun eraseOneToOne(pubKeyHex: String): Boolean - external fun getCommunity(baseUrl: String, room: String): Conversation.Community? - external fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community - external fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community - external fun eraseCommunity(community: Conversation.Community): Boolean - external fun eraseCommunity(baseUrl: String, room: String): Boolean + external override fun getCommunity(baseUrl: String, room: String): Conversation.Community? + external override fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community + external override fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community + external override fun eraseCommunity(community: Conversation.Community): Boolean + external override fun eraseCommunity(baseUrl: String, room: String): Boolean - external fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup? - external fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup - external fun eraseLegacyClosedGroup(groupId: String): Boolean + external override fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup? + external override fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup + external override fun eraseLegacyClosedGroup(groupId: String): Boolean - external fun getClosedGroup(sessionId: String): Conversation.ClosedGroup? - external fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup - external fun eraseClosedGroup(sessionId: String): Boolean + external override fun getClosedGroup(sessionId: String): Conversation.ClosedGroup? + external override fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup + external override fun eraseClosedGroup(sessionId: String): Boolean - external fun erase(conversation: Conversation): Boolean - external fun set(toStore: Conversation) + external override fun erase(conversation: Conversation): Boolean + external override fun set(toStore: Conversation) /** * Erase all conversations that do not satisfy the `predicate`, similar to [MutableList.removeAll] */ - external fun eraseAll(predicate: (Conversation) -> Boolean): Int + external override fun eraseAll(predicate: (Conversation) -> Boolean): Int - external fun sizeOneToOnes(): Int - external fun sizeCommunities(): Int - external fun sizeLegacyClosedGroups(): Int - external fun size(): Int + external override fun sizeOneToOnes(): Int + external override fun sizeCommunities(): Int + external override fun sizeLegacyClosedGroups(): Int + external override fun size(): Int - external fun empty(): Boolean - - external fun allOneToOnes(): List - external fun allCommunities(): List - external fun allLegacyClosedGroups(): List - external fun allClosedGroups(): List - external fun all(): List + external override fun empty(): Boolean + external override fun allOneToOnes(): List + external override fun allCommunities(): List + external override fun allLegacyClosedGroups(): List + external override fun allClosedGroups(): List + external override fun all(): List } -class UserGroupsConfig(pointer: Long): ConfigBase(pointer) { - companion object { - init { - System.loadLibrary("session_util") - } - external fun newInstance(ed25519SecretKey: ByteArray): UserGroupsConfig - external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserGroupsConfig - } +interface ReadableUserGroupsConfig : ReadableConfig { + fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo? + fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo? + fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo? + fun sizeCommunityInfo(): Long + fun sizeLegacyGroupInfo(): Long + fun sizeClosedGroup(): Long + fun size(): Long + fun all(): List + fun allCommunityInfo(): List + fun allLegacyGroupInfo(): List + fun allClosedGroupInfo(): List +} + +interface MutableUserGroupsConfig : ReadableUserGroupsConfig, MutableConfig { + fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo + fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo + fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo + fun set(groupInfo: GroupInfo) + fun erase(groupInfo: GroupInfo) + fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean + fun eraseCommunity(server: String, room: String): Boolean + fun eraseLegacyGroup(accountId: String): Boolean + fun eraseClosedGroup(accountId: String): Boolean + fun createGroup(): GroupInfo.ClosedGroupInfo +} + +class UserGroupsConfig private constructor(pointer: Long): ConfigBase(pointer), MutableUserGroupsConfig { + constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this( + createConfigObject( + "UserGroups", + ed25519SecretKey, + initialDump + ) + ) override fun namespace() = Namespace.GROUPS() - external fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo? - external fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo? - external fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo? - external fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo - external fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo - external fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo - external fun set(groupInfo: GroupInfo) - external fun erase(groupInfo: GroupInfo) - external fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean - external fun eraseCommunity(server: String, room: String): Boolean - external fun eraseLegacyGroup(accountId: String): Boolean - external fun eraseClosedGroup(accountId: String): Boolean - external fun sizeCommunityInfo(): Long - external fun sizeLegacyGroupInfo(): Long - external fun sizeClosedGroup(): Long - external fun size(): Long - external fun all(): List - external fun allCommunityInfo(): List - external fun allLegacyGroupInfo(): List - external fun allClosedGroupInfo(): List - external fun createGroup(): GroupInfo.ClosedGroupInfo + external override fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo? + external override fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo? + external override fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo? + external override fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo + external override fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo + external override fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo + external override fun set(groupInfo: GroupInfo) + external override fun erase(groupInfo: GroupInfo) + external override fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean + external override fun eraseCommunity(server: String, room: String): Boolean + external override fun eraseLegacyGroup(accountId: String): Boolean + external override fun eraseClosedGroup(accountId: String): Boolean + external override fun sizeCommunityInfo(): Long + external override fun sizeLegacyGroupInfo(): Long + external override fun sizeClosedGroup(): Long + external override fun size(): Long + external override fun all(): List + external override fun allCommunityInfo(): List + external override fun allLegacyGroupInfo(): List + external override fun allClosedGroupInfo(): List + external override fun createGroup(): GroupInfo.ClosedGroupInfo } -class GroupInfoConfig(pointer: Long): ConfigBase(pointer), Closeable { - companion object { - init { - System.loadLibrary("session_util") - } +interface ReadableGroupInfoConfig: ReadableConfig { + fun id(): AccountId + fun getDeleteAttachmentsBefore(): Long? + fun getDeleteBefore(): Long? + fun getExpiryTimer(): Long + fun getName(): String + fun getCreated(): Long? + fun getProfilePic(): UserPic + fun isDestroyed(): Boolean + fun getDescription(): String + fun storageNamespace(): Long +} - external fun newInstance( - pubKey: ByteArray?, - secretKey: ByteArray? = null, - initialDump: ByteArray = byteArrayOf() - ): GroupInfoConfig +interface MutableGroupInfoConfig : ReadableGroupInfoConfig, MutableConfig { + fun setCreated(createdAt: Long) + fun setDeleteAttachmentsBefore(deleteBefore: Long) + fun setDeleteBefore(deleteBefore: Long) + fun setExpiryTimer(expireSeconds: Long) + fun setName(newName: String) + fun setDescription(newDescription: String) + fun setProfilePic(newProfilePic: UserPic) + fun destroyGroup() +} + +class GroupInfoConfig private constructor(pointer: Long): ConfigBase(pointer), MutableGroupInfoConfig { + constructor(groupPubKey: ByteArray, groupAdminKey: ByteArray?, initialDump: ByteArray?) + : this(newInstance(groupPubKey, groupAdminKey, initialDump)) + + companion object { + private external fun newInstance( + pubKey: ByteArray, + secretKey: ByteArray?, + initialDump: ByteArray? + ): Long } override fun namespace() = Namespace.CLOSED_GROUP_INFO() - external fun id(): AccountId - external fun destroyGroup() - external fun getCreated(): Long? - external fun getDeleteAttachmentsBefore(): Long? - external fun getDeleteBefore(): Long? - external fun getExpiryTimer(): Long - external fun getName(): String - external fun getProfilePic(): UserPic - external fun isDestroyed(): Boolean - external fun setCreated(createdAt: Long) - external fun setDeleteAttachmentsBefore(deleteBefore: Long) - external fun setDeleteBefore(deleteBefore: Long) - external fun setExpiryTimer(expireSeconds: Long) - external fun setName(newName: String) - external fun getDescription(): String - external fun setDescription(newDescription: String) - external fun setProfilePic(newProfilePic: UserPic) - external fun storageNamespace(): Long - - override fun close() { - free() - } + external override fun id(): AccountId + external override fun destroyGroup() + external override fun getCreated(): Long? + external override fun getDeleteAttachmentsBefore(): Long? + external override fun getDeleteBefore(): Long? + external override fun getExpiryTimer(): Long + external override fun getName(): String + external override fun getProfilePic(): UserPic + external override fun isDestroyed(): Boolean + external override fun setCreated(createdAt: Long) + external override fun setDeleteAttachmentsBefore(deleteBefore: Long) + external override fun setDeleteBefore(deleteBefore: Long) + external override fun setExpiryTimer(expireSeconds: Long) + external override fun setName(newName: String) + external override fun getDescription(): String + external override fun setDescription(newDescription: String) + external override fun setProfilePic(newProfilePic: UserPic) + external override fun storageNamespace(): Long } -class GroupMembersConfig(pointer: Long): ConfigBase(pointer), Closeable { +interface ReadableGroupMembersConfig: ReadableConfig { + fun all(): List + fun get(pubKeyHex: String): GroupMember? +} + +interface MutableGroupMembersConfig : ReadableGroupMembersConfig, MutableConfig { + fun getOrConstruct(pubKeyHex: String): GroupMember + fun set(groupMember: GroupMember) + fun erase(groupMember: GroupMember): Boolean + fun erase(pubKeyHex: String): Boolean +} + +class GroupMembersConfig private constructor(pointer: Long): ConfigBase(pointer), MutableGroupMembersConfig { companion object { - init { - System.loadLibrary("session_util") - } - external fun newInstance( + private external fun newInstance( pubKey: ByteArray, - secretKey: ByteArray? = null, - initialDump: ByteArray = byteArrayOf() - ): GroupMembersConfig + secretKey: ByteArray?, + initialDump: ByteArray? + ): Long } + constructor(groupPubKey: ByteArray, groupAdminKey: ByteArray?, initialDump: ByteArray?) + : this(newInstance(groupPubKey, groupAdminKey, initialDump)) + override fun namespace() = Namespace.CLOSED_GROUP_MEMBERS() - external fun all(): Stack - external fun erase(groupMember: GroupMember): Boolean - external fun erase(pubKeyHex: String): Boolean - external fun get(pubKeyHex: String): GroupMember? - external fun getOrConstruct(pubKeyHex: String): GroupMember - external fun set(groupMember: GroupMember) - override fun close() { - free() - } + external override fun all(): Stack + external override fun erase(groupMember: GroupMember): Boolean + external override fun erase(pubKeyHex: String): Boolean + external override fun get(pubKeyHex: String): GroupMember? + external override fun getOrConstruct(pubKeyHex: String): GroupMember + external override fun set(groupMember: GroupMember) } sealed class ConfigSig(pointer: Long) : Config(pointer) -class GroupKeysConfig(pointer: Long): ConfigSig(pointer) { +interface ReadableGroupKeysConfig { + fun groupKeys(): Stack + fun needsDump(): Boolean + fun dump(): ByteArray + fun needsRekey(): Boolean + fun pendingKey(): ByteArray? + fun supplementFor(userSessionId: String): ByteArray + fun pendingConfig(): ByteArray? + fun currentHashes(): List + fun encrypt(plaintext: ByteArray): ByteArray + fun decrypt(ciphertext: ByteArray): Pair? + fun keys(): Stack + fun subAccountSign(message: ByteArray, signingValue: ByteArray): GroupKeysConfig.SwarmAuth + fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray + fun currentGeneration(): Int +} + +interface MutableGroupKeysConfig : ReadableGroupKeysConfig { + fun makeSubAccount(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray +} + +class GroupKeysConfig private constructor(pointer: Long): ConfigSig(pointer), MutableGroupKeysConfig { companion object { - init { - System.loadLibrary("session_util") - } - external fun newInstance( + private external fun newInstance( userSecretKey: ByteArray, groupPublicKey: ByteArray, groupSecretKey: ByteArray? = null, - initialDump: ByteArray = byteArrayOf(), - info: GroupInfoConfig, - members: GroupMembersConfig - ): GroupKeysConfig + initialDump: ByteArray?, + infoPtr: Long, + members: Long + ): Long } + constructor( + userSecretKey: ByteArray, + groupPublicKey: ByteArray, + groupAdminKey: ByteArray?, + initialDump: ByteArray?, + info: GroupInfoConfig, + members: GroupMembersConfig + ) : this( + newInstance( + userSecretKey, + groupPublicKey, + groupAdminKey, + initialDump, + info.pointer, + members.pointer + ) + ) + override fun namespace() = Namespace.ENCRYPTION_KEYS() - external fun groupKeys(): Stack - external fun needsDump(): Boolean - external fun dump(): ByteArray + external override fun groupKeys(): Stack + external override fun needsDump(): Boolean + external override fun dump(): ByteArray external fun loadKey(message: ByteArray, hash: String, timestampMs: Long, - info: GroupInfoConfig, - members: GroupMembersConfig): Boolean - external fun needsRekey(): Boolean - external fun pendingKey(): ByteArray? - external fun supplementFor(userSessionId: String): ByteArray - external fun pendingConfig(): ByteArray? - external fun currentHashes(): List - external fun rekey(info: GroupInfoConfig, members: GroupMembersConfig): ByteArray - override fun close() { - free() - } + infoPtr: Long, + membersPtr: Long): Boolean + external override fun needsRekey(): Boolean + external override fun pendingKey(): ByteArray? + external override fun supplementFor(userSessionId: String): ByteArray + external override fun pendingConfig(): ByteArray? + external override fun currentHashes(): List + external fun rekey(infoPtr: Long, membersPtr: Long): ByteArray - external fun encrypt(plaintext: ByteArray): ByteArray - external fun decrypt(ciphertext: ByteArray): Pair? + external override fun encrypt(plaintext: ByteArray): ByteArray + external override fun decrypt(ciphertext: ByteArray): Pair? - external fun keys(): Stack + external override fun keys(): Stack - external fun makeSubAccount(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray - external fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray + external override fun makeSubAccount(sessionId: AccountId, canWrite: Boolean, canDelete: Boolean): ByteArray + external override fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean, canDelete: Boolean): ByteArray - external fun subAccountSign(message: ByteArray, signingValue: ByteArray): SwarmAuth + external override fun subAccountSign(message: ByteArray, signingValue: ByteArray): SwarmAuth - external fun currentGeneration(): Int + external override fun currentGeneration(): Int data class SwarmAuth( val subAccount: String, val subAccountSig: String, val signature: String ) +} -} \ No newline at end of file +private external fun createConfigObject( + configName: String, + ed25519SecretKey: ByteArray, + initialDump: ByteArray? +): Long \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 732278b727..217e04864b 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -68,7 +68,6 @@ interface StorageProtocol { fun getMessageSendJob(messageSendJobID: String): MessageSendJob? fun getMessageReceiveJob(messageReceiveJobID: String): Job? fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job? - fun getConfigSyncJob(destination: Destination): Job? fun resumeMessageSendJobIfNeeded(messageSendJobID: String) fun isJobCanceled(job: Job): Boolean fun cancelPendingMessageSendJobs(threadID: Long) @@ -269,7 +268,6 @@ interface StorageProtocol { ) // Shared configs - fun notifyConfigUpdates(forConfigObject: Config, messageTimestamp: Long) fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean fun isCheckingCommunityRequests(): Boolean diff --git a/libsession/src/main/java/org/session/libsession/messaging/configs/ConfigSyncHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/configs/ConfigSyncHandler.kt new file mode 100644 index 0000000000..6c3fa303a2 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/configs/ConfigSyncHandler.kt @@ -0,0 +1,133 @@ +package org.session.libsession.messaging.configs + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import network.loki.messenger.libsession_util.MutableConfig +import network.loki.messenger.libsession_util.util.ConfigPush +import org.session.libsession.database.StorageProtocol +import org.session.libsession.database.userAuth +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeMessage +import org.session.libsession.snode.utilities.await +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.ConfigUpdateNotification +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.Log + +class ConfigSyncHandler( + private val configFactory: ConfigFactoryProtocol, + private val storageProtocol: StorageProtocol, + @Suppress("OPT_IN_USAGE") scope: CoroutineScope = GlobalScope, +) { + init { + scope.launch { + configFactory.configUpdateNotifications.collect { changes -> + try { + when (changes) { + is ConfigUpdateNotification.GroupConfigsDeleted -> {} + is ConfigUpdateNotification.GroupConfigsUpdated -> { + pushGroupConfigsChangesIfNeeded(changes.groupId) + } + ConfigUpdateNotification.UserConfigs -> pushUserConfigChangesIfNeeded() + } + } catch (e: Exception) { + Log.e("ConfigSyncHandler", "Error handling config update", e) + } + } + } + } + + private suspend fun pushGroupConfigsChangesIfNeeded(groupId: AccountId): Unit = coroutineScope { + + } + + private suspend fun pushUserConfigChangesIfNeeded(): Unit = coroutineScope { + val userAuth = requireNotNull(storageProtocol.userAuth) { + "Current user not available" + } + + data class PushInformation( + val namespace: Int, + val configClass: Class, + val push: ConfigPush, + ) + + // Gather all the user configs that need to be pushed + val pushes = configFactory.withMutableUserConfigs { configs -> + configs.allConfigs() + .filter { it.needsPush() } + .map { config -> + PushInformation( + namespace = config.namespace(), + configClass = config.javaClass, + push = config.push(), + ) + } + .toList() + } + + Log.d("ConfigSyncHandler", "Pushing ${pushes.size} configs") + + val snode = SnodeAPI.getSingleTargetSnode(userAuth.accountId.hexString).await() + + val pushTasks = pushes.map { info -> + val calls = buildList { + this += SnodeAPI.buildAuthenticatedStoreBatchInfo( + info.namespace, + SnodeMessage( + userAuth.accountId.hexString, + Base64.encodeBytes(info.push.config), + SnodeMessage.CONFIG_TTL, + SnodeAPI.nowWithOffset, + ), + userAuth + ) + + if (info.push.obsoleteHashes.isNotEmpty()) { + this += SnodeAPI.buildAuthenticatedDeleteBatchInfo( + messageHashes = info.push.obsoleteHashes, + auth = userAuth, + ) + } + } + + async { + val responses = SnodeAPI.getBatchResponse( + snode = snode, + publicKey = userAuth.accountId.hexString, + requests = calls, + sequence = true + ) + + val firstError = responses.results.firstOrNull { !it.isSuccessful } + check(firstError == null) { + "Failed to push config change due to error: ${firstError?.body}" + } + + val hash = responses.results.first().body.get("hash").asText() + require(hash.isNotEmpty()) { + "Missing server hash for pushed config" + } + + info to hash + } + } + + val pushResults = pushTasks.awaitAll().associateBy { it.first.configClass } + + Log.d("ConfigSyncHandler", "Pushed ${pushResults.size} configs") + + configFactory.withMutableUserConfigs { configs -> + configs.allConfigs() + .mapNotNull { config -> pushResults[config.javaClass]?.let { Triple(config, it.first, it.second) } } + .forEach { (config, info, hash) -> + config.confirmPushed(info.push.seqNo, hash) + } + } + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/groups/RemoveGroupMemberHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/groups/RemoveGroupMemberHandler.kt index 1803c0aea8..d4d7641991 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/groups/RemoveGroupMemberHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/groups/RemoveGroupMemberHandler.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import network.loki.messenger.libsession_util.GroupKeysConfig +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.messaging.messages.Destination @@ -17,8 +17,8 @@ import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeMessage +import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.withGroupConfigsOrNull import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage import org.session.libsignal.utilities.AccountId @@ -61,12 +61,8 @@ class RemoveGroupMemberHandler( } private suspend fun processPendingMemberRemoval() { - val userGroups = checkNotNull(configFactory.userGroups) { - "User groups config is null" - } - // Run the removal process for each group in parallel - val removalTasks = userGroups.allClosedGroupInfo() + val removalTasks = configFactory.withUserConfigs { it.userGroups.allClosedGroupInfo() } .asSequence() .filter { it.hasAdminKey() } .associate { group -> @@ -89,7 +85,7 @@ class RemoveGroupMemberHandler( } } - private fun processPendingRemovalsForGroup( + private suspend fun processPendingRemovalsForGroup( groupAccountId: AccountId, groupName: String, adminKey: ByteArray @@ -100,11 +96,11 @@ class RemoveGroupMemberHandler( ed25519PrivateKey = adminKey ) - configFactory.withGroupConfigsOrNull(groupAccountId) withConfig@ { info, members, keys -> - val pendingRemovals = members.all().filter { it.removed } + val batchCalls = configFactory.withGroupConfigs(groupAccountId) { configs -> + val pendingRemovals = configs.groupMembers.all().filter { it.removed } if (pendingRemovals.isEmpty()) { // Skip if there are no pending removals - return@withConfig + return@withGroupConfigs emptyList() } Log.d(TAG, "Processing ${pendingRemovals.size} pending removals for group $groupName") @@ -113,28 +109,28 @@ class RemoveGroupMemberHandler( // 1. Revoke the member's sub key (by adding the key to a "revoked list" under the hood) // 2. Send a message to a special namespace to inform the removed members they have been removed // 3. Conditionally, delete removed-members' messages from the group's message store, if that option is selected by the actioning admin - val seqCalls = ArrayList(3) + val calls = ArrayList(3) // Call No 1. Revoke sub-key. This call is crucial and must not fail for the rest of the operation to be successful. - seqCalls += checkNotNull( + calls += checkNotNull( SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest( groupAdminAuth = swarmAuth, subAccountTokens = pendingRemovals.map { - keys.getSubAccountToken(AccountId(it.sessionId)) + configs.groupKeys.getSubAccountToken(AccountId(it.sessionId)) } ) ) { "Fail to create a revoke request" } // Call No 2. Send a message to the removed members - seqCalls += SnodeAPI.buildAuthenticatedStoreBatchInfo( + calls += SnodeAPI.buildAuthenticatedStoreBatchInfo( namespace = Namespace.REVOKED_GROUP_MESSAGES(), - message = buildGroupKickMessage(groupAccountId.hexString, pendingRemovals, keys, adminKey), + message = buildGroupKickMessage(groupAccountId.hexString, pendingRemovals, configs.groupKeys, adminKey), auth = swarmAuth, ) // Call No 3. Conditionally remove the message from the group's message store if (pendingRemovals.any { it.shouldRemoveMessages }) { - seqCalls += SnodeAPI.buildAuthenticatedStoreBatchInfo( + calls += SnodeAPI.buildAuthenticatedStoreBatchInfo( namespace = Namespace.CLOSED_GROUP_MESSAGES(), message = buildDeleteGroupMemberContentMessage( groupAccountId = groupAccountId.hexString, @@ -147,9 +143,17 @@ class RemoveGroupMemberHandler( ) } - // Make the call: - SnodeAPI.getSingleTargetSnode(groupAccountId.hexString) + calls } + + if (batchCalls.isEmpty()) { + return + } + + val node = SnodeAPI.getSingleTargetSnode(groupAccountId.hexString).await() + SnodeAPI.getBatchResponse(node, groupAccountId.hexString, batchCalls, true) + + //TODO: Handle message removal } private fun buildDeleteGroupMemberContentMessage( @@ -178,7 +182,7 @@ class RemoveGroupMemberHandler( private fun buildGroupKickMessage( groupAccountId: String, pendingRemovals: List, - keys: GroupKeysConfig, + keys: ReadableGroupKeysConfig, adminKey: ByteArray ) = SnodeMessage( recipient = groupAccountId, diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt deleted file mode 100644 index 59f5149faf..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt +++ /dev/null @@ -1,338 +0,0 @@ -package org.session.libsession.messaging.jobs - -import network.loki.messenger.libsession_util.Config -import network.loki.messenger.libsession_util.ConfigBase -import network.loki.messenger.libsession_util.GroupKeysConfig -import nl.komponents.kovenant.functional.bind -import org.session.libsession.database.userAuth -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.messages.Destination -import org.session.libsession.messaging.utilities.Data -import org.session.libsession.snode.OwnedSwarmAuth -import org.session.libsession.snode.RawResponse -import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.SnodeAPI.SnodeBatchRequestInfo -import org.session.libsession.snode.SnodeMessage -import org.session.libsession.snode.SwarmAuth -import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.Log -import java.util.concurrent.atomic.AtomicBoolean - -class InvalidDestination : - Exception("Trying to push configs somewhere other than our swarm or a closed group") - -// only contact (self) and closed group destinations will be supported -data class ConfigurationSyncJob(val destination: Destination) : Job { - - override var delegate: JobDelegate? = null - override var id: String? = null - override var failureCount: Int = 0 - override val maxFailureCount: Int = 10 - - val shouldRunAgain = AtomicBoolean(false) - - data class ConfigMessageInformation( - val batch: SnodeBatchRequestInfo, - val config: Config, - val seqNo: Long? - ) // seqNo will be null for keys type - - data class SyncInformation( - val configs: List, - val toDelete: List - ) - - private fun destinationConfigs( - configFactoryProtocol: ConfigFactoryProtocol - ): SyncInformation { - val toDelete = mutableListOf() - val configsRequiringPush = - if (destination is Destination.ClosedGroup) { - // destination is a closed group, get all configs requiring push here - val groupId = AccountId(destination.publicKey) - - // Get the signing key for pushing configs - val signingKey = configFactoryProtocol - .userGroups?.getClosedGroup(destination.publicKey)?.adminKey - if (signingKey?.isNotEmpty() == true) { - val info = configFactoryProtocol.getGroupInfoConfig(groupId)!! - val members = configFactoryProtocol.getGroupMemberConfig(groupId)!! - val keys = configFactoryProtocol.getGroupKeysConfig( - groupId, - info, - members, - false - )!! - - val requiringPush = - listOf(keys, info, members).filter { - when (it) { - is GroupKeysConfig -> it.pendingConfig()?.isNotEmpty() == true - is ConfigBase -> it.needsPush() - else -> false - } - } - - // free the objects that were created but won't be used after this point - // in case any of the configs don't need pushing, they won't be freed later - (listOf(keys, info, members) subtract requiringPush).forEach(Config::free) - - val groupAuth = OwnedSwarmAuth.ofClosedGroup(groupId, signingKey) - - requiringPush.mapNotNull { config -> - if (config is GroupKeysConfig) { - config.messageInformation(groupAuth) - } else if (config is ConfigBase) { - config.messageInformation(toDelete, groupAuth) - } else { - Log.e("ConfigurationSyncJob", "Tried to create a message from an unknown config") - null - } - } - } else emptyList() - } else if (destination is Destination.Contact) { - val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth) { - "No user auth for syncing user config" - } - - // assume our own user as check already takes place in `execute` for our own key - // if contact - configFactoryProtocol.getUserConfigs().filter { it.needsPush() }.map { config -> - config.messageInformation(toDelete, userAuth) - } - } else throw InvalidDestination() - return SyncInformation(configsRequiringPush, toDelete) - } - - override suspend fun execute(dispatcherName: String) { - val storage = MessagingModuleConfiguration.shared.storage - - val userPublicKey = storage.getUserPublicKey() - val delegate = delegate ?: return Log.e("ConfigurationSyncJob", "No Delegate") - if (destination !is Destination.ClosedGroup && - (destination !is Destination.Contact || - destination.publicKey != userPublicKey) - ) { - return delegate.handleJobFailedPermanently(this, dispatcherName, InvalidDestination()) - } - - // configFactory singleton instance will come in handy for modifying hashes and fetching - // configs for namespace etc - val configFactory = MessagingModuleConfiguration.shared.configFactory - - // allow null results here so the list index matches configsRequiringPush - val (batchObjects, toDeleteHashes) = - destinationConfigs(configFactory) - - if (batchObjects.isEmpty()) return delegate.handleJobSucceeded(this, dispatcherName) - - val toDeleteRequest = - toDeleteHashes.let { toDeleteFromAllNamespaces -> - if (toDeleteFromAllNamespaces.isEmpty()) null - else if (destination is Destination.ClosedGroup) { - // Build sign callback for group's admin key - val signingKey = - configFactory.userGroups - ?.getClosedGroup(destination.publicKey) - ?.adminKey ?: return@let null - - - // Destination is a closed group swarm, build with signCallback - SnodeAPI.buildAuthenticatedDeleteBatchInfo( - OwnedSwarmAuth.ofClosedGroup( - groupAccountId = AccountId(destination.publicKey), - adminKey = signingKey, - ), - toDeleteFromAllNamespaces, - ) - } else { - // Destination is our own swarm - val userAuth = MessagingModuleConfiguration.shared.storage.userAuth - - if (userAuth == null) { - delegate.handleJobFailedPermanently( - this, - dispatcherName, - IllegalStateException("No user auth for syncing user config") - ) - return - } - - SnodeAPI.buildAuthenticatedDeleteBatchInfo( - auth = userAuth, - messageHashes = toDeleteFromAllNamespaces - ) - } - } - - val allRequests = mutableListOf() - allRequests += batchObjects.map { (request) -> request } - // add in the deletion if we have any hashes - if (toDeleteRequest != null) { - allRequests += toDeleteRequest - Log.d(TAG, "Including delete request for current hashes") - } - - val batchResponse = - SnodeAPI.getSingleTargetSnode(destination.destinationPublicKey()).bind { snode -> - SnodeAPI.getRawBatchResponse( - snode, - destination.destinationPublicKey(), - allRequests, - sequence = true - ) - } - - try { - val rawResponses = batchResponse.get() - @Suppress("UNCHECKED_CAST") - val responseList = (rawResponses["results"] as List) - - // at this point responseList index should line up with configsRequiringPush index - batchObjects.forEachIndexed { index, (message, config, seqNo) -> - val response = responseList[index] - val responseBody = response["body"] as? RawResponse - val insertHash = - responseBody?.get("hash") as? String - ?: run { - Log.w( - TAG, - "No hash returned for the configuration in namespace ${config.namespace()}" - ) - return@forEachIndexed - } - Log.d(TAG, "Hash ${insertHash.take(4)} returned from store request for new config") - - // confirm pushed seqno - if (config is ConfigBase) { - seqNo?.let { config.confirmPushed(it, insertHash) } - } - - Log.d( - TAG, - "Successfully removed the deleted hashes from ${config.javaClass.simpleName}" - ) - // dump and write config after successful - if (config is ConfigBase && config.needsDump()) { // usually this will be true? )) - val groupPubKey = if (destination is Destination.ClosedGroup) destination.publicKey else null - configFactory.persist(config, message.params["timestamp"] as Long, groupPubKey) - } else if (config is GroupKeysConfig && config.needsDump()) { - Log.d("Loki", "Should persist the GroupKeysConfig") - } - if (destination is Destination.ClosedGroup) { - config.free() // after they are used, free the temporary group configs - } - } - } catch (e: Exception) { - Log.e(TAG, "Error performing batch request", e) - return delegate.handleJobFailed(this, dispatcherName, e) - } - delegate.handleJobSucceeded(this, dispatcherName) - if (shouldRunAgain.get() && storage.getConfigSyncJob(destination) == null) { - // reschedule if something has updated since we started this job - JobQueue.shared.add(ConfigurationSyncJob(destination)) - } - } - - fun Destination.destinationPublicKey(): String = - when (this) { - is Destination.Contact -> publicKey - is Destination.ClosedGroup -> publicKey - else -> throw NullPointerException("Not public key for this destination") - } - - override fun serialize(): Data { - val (type, address) = - when (destination) { - is Destination.Contact -> CONTACT_TYPE to destination.publicKey - is Destination.ClosedGroup -> GROUP_TYPE to destination.publicKey - else -> return Data.EMPTY - } - return Data.Builder() - .putInt(DESTINATION_TYPE_KEY, type) - .putString(DESTINATION_ADDRESS_KEY, address) - .build() - } - - override fun getFactoryKey(): String = KEY - - companion object { - const val TAG = "ConfigSyncJob" - const val KEY = "ConfigSyncJob" - - // Keys used for DB storage - const val DESTINATION_ADDRESS_KEY = "destinationAddress" - const val DESTINATION_TYPE_KEY = "destinationType" - - // type mappings - const val CONTACT_TYPE = 1 - const val GROUP_TYPE = 2 - - fun ConfigBase.messageInformation(toDelete: MutableList, - auth: SwarmAuth): ConfigMessageInformation { - val sentTimestamp = SnodeAPI.nowWithOffset - val (push, seqNo, obsoleteHashes) = push() - toDelete.addAll(obsoleteHashes) - val message = - SnodeMessage( - auth.accountId.hexString, - Base64.encodeBytes(push), - SnodeMessage.CONFIG_TTL, - sentTimestamp - ) - - return ConfigMessageInformation( - SnodeAPI.buildAuthenticatedStoreBatchInfo( - namespace(), - message, - auth, - ), - this, - seqNo - ) - } - - fun GroupKeysConfig.messageInformation(auth: OwnedSwarmAuth): ConfigMessageInformation? { - val pending = pendingConfig() ?: return null - - val sentTimestamp = SnodeAPI.nowWithOffset - val message = - SnodeMessage( - auth.accountId.hexString, - Base64.encodeBytes(pending), - SnodeMessage.CONFIG_TTL, - sentTimestamp - ) - - return ConfigMessageInformation( - SnodeAPI.buildAuthenticatedStoreBatchInfo( - namespace(), - message, - auth, - ), - this, - 0 - ) - } - } - - class Factory : Job.Factory { - override fun create(data: Data): ConfigurationSyncJob? { - if (!data.hasInt(DESTINATION_TYPE_KEY) || !data.hasString(DESTINATION_ADDRESS_KEY)) - return null - - val address = data.getString(DESTINATION_ADDRESS_KEY) - val destination = - when (data.getInt(DESTINATION_TYPE_KEY)) { - CONTACT_TYPE -> Destination.Contact(address) - GROUP_TYPE -> Destination.ClosedGroup(address) - else -> return null - } - - return ConfigurationSyncJob(destination) - } - } -} - diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt deleted file mode 100644 index 0b4b068058..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt +++ /dev/null @@ -1,199 +0,0 @@ -package org.session.libsession.messaging.jobs - -import android.widget.Toast -import com.google.protobuf.ByteString -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.withContext -import org.session.libsession.R -import org.session.libsession.messaging.MessagingModuleConfiguration -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.Data -import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature -import org.session.libsession.messaging.utilities.SodiumUtilities -import org.session.libsession.snode.SnodeAPI -import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY -import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY -import org.session.libsession.utilities.StringSubstitutionConstants.OTHER_NAME_KEY -import org.session.libsession.utilities.truncateIdForDisplay -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage -import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage -import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.prettifiedDescription - -class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array) : Job { - - companion object { - const val KEY = "InviteContactJob" - private const val GROUP = "group" - private const val MEMBER = "member" - - } - - override var delegate: JobDelegate? = null - override var id: String? = null - override var failureCount: Int = 0 - override val maxFailureCount: Int = 1 - - override suspend fun execute(dispatcherName: String) { - val delegate = delegate ?: return - val configs = MessagingModuleConfiguration.shared.configFactory - val adminKey = configs.userGroups?.getClosedGroup(groupSessionId)?.adminKey - ?: return delegate.handleJobFailedPermanently( - this, - dispatcherName, - NullPointerException("No admin key") - ) - - withContext(Dispatchers.IO) { - val sessionId = AccountId(groupSessionId) - val members = configs.getGroupMemberConfig(sessionId) - val info = configs.getGroupInfoConfig(sessionId) - val keys = configs.getGroupKeysConfig(sessionId, info, members, free = false) - - if (members == null || info == null || keys == null) { - return@withContext delegate.handleJobFailedPermanently( - this@InviteContactsJob, - dispatcherName, - NullPointerException("One of the group configs was null") - ) - } - - val requests = memberSessionIds.map { memberSessionId -> - async { - // Make the request for this member - val member = members.get(memberSessionId) ?: return@async run { - InviteResult.failure( - memberSessionId, - NullPointerException("No group member ${memberSessionId.prettifiedDescription()} in members config") - ) - } - members.set(member.setInvited()) - configs.saveGroupConfigs(keys, info, members) - - val accountId = AccountId(memberSessionId) - val subAccount = keys.makeSubAccount(accountId) - - val timestamp = SnodeAPI.nowWithOffset - val signature = SodiumUtilities.sign( - buildGroupInviteSignature(accountId, timestamp), - adminKey - ) - - val groupInvite = GroupUpdateInviteMessage.newBuilder() - .setGroupSessionId(groupSessionId) - .setMemberAuthData(ByteString.copyFrom(subAccount)) - .setAdminSignature(ByteString.copyFrom(signature)) - .setName(info.getName()) - val message = GroupUpdateMessage.newBuilder() - .setInviteMessage(groupInvite) - .build() - val update = GroupUpdated(message).apply { - sentTimestamp = timestamp - } - try { - MessageSender.send(update, Destination.Contact(memberSessionId), false) - .get() - InviteResult.success(memberSessionId) - } catch (e: Exception) { - InviteResult.failure(memberSessionId, e) - } - } - } - val results = requests.awaitAll() - results.forEach { result -> - if (!result.success) { - // update invite failed - val toSet = members.get(result.memberSessionId) - ?.setInviteFailed() - ?: return@forEach - members.set(toSet) - } - } - val failures = results.filter { !it.success } - // if there are failed invites, display a message - // assume job "success" even if we fail, the state of invites is tracked outside of this job - if (failures.isNotEmpty()) { - // show the failure toast - val storage = MessagingModuleConfiguration.shared.storage - val toaster = MessagingModuleConfiguration.shared.toaster - when (failures.size) { - 1 -> { - val first = failures.first() - val firstString = first.memberSessionId.let { storage.getContactWithAccountID(it) }?.name - ?: truncateIdForDisplay(first.memberSessionId) - withContext(Dispatchers.Main) { - toaster.toast(R.string.groupInviteFailedUser, Toast.LENGTH_LONG, - mapOf( - NAME_KEY to firstString, - GROUP_NAME_KEY to info.getName() - ) - ) - } - } - 2 -> { - val (first, second) = failures - val firstString = first.memberSessionId.let { storage.getContactWithAccountID(it) }?.name - ?: truncateIdForDisplay(first.memberSessionId) - val secondString = second.memberSessionId.let { storage.getContactWithAccountID(it) }?.name - ?: truncateIdForDisplay(second.memberSessionId) - - withContext(Dispatchers.Main) { - toaster.toast(R.string.groupInviteFailedTwo, Toast.LENGTH_LONG, - mapOf( - NAME_KEY to firstString, - OTHER_NAME_KEY to secondString, - GROUP_NAME_KEY to info.getName() - ) - ) - } - } - else -> { - val first = failures.first() - val firstString = first.memberSessionId.let { storage.getContactWithAccountID(it) }?.name - ?: truncateIdForDisplay(first.memberSessionId) - val remaining = failures.size - 1 - withContext(Dispatchers.Main) { - toaster.toast(R.string.groupInviteFailedMultiple, Toast.LENGTH_LONG, - mapOf( - NAME_KEY to firstString, - OTHER_NAME_KEY to remaining.toString(), - GROUP_NAME_KEY to info.getName() - ) - ) - } - } - } - } - configs.saveGroupConfigs(keys, info, members) - keys.free() - info.free() - members.free() - } - } - - @Suppress("DataClassPrivateConstructor") - data class InviteResult private constructor( - val memberSessionId: String, - val success: Boolean, - val error: Exception? = null - ) { - companion object { - fun success(memberSessionId: String) = InviteResult(memberSessionId, success = true) - fun failure(memberSessionId: String, error: Exception) = - InviteResult(memberSessionId, success = false, error) - } - } - - override fun serialize(): Data = - Data.Builder() - .putString(GROUP, groupSessionId) - .putStringArray(MEMBER, memberSessionIds) - .build() - - override fun getFactoryKey(): String = KEY - -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 33839260e2..8e20044bfc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -29,7 +29,6 @@ class JobQueue : JobDelegate { private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher() private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - private val configDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob() private val queue = Channel(UNLIMITED) @@ -117,20 +116,14 @@ class JobQueue : JobDelegate { val txQueue = Channel(capacity = UNLIMITED) val mediaQueue = Channel(capacity = UNLIMITED) val openGroupQueue = Channel(capacity = UNLIMITED) - val configQueue = Channel(capacity = UNLIMITED) val receiveJob = processWithDispatcher(rxQueue, rxDispatcher, "rx", asynchronous = false) val txJob = processWithDispatcher(txQueue, txDispatcher, "tx") val mediaJob = processWithDispatcher(mediaQueue, rxMediaDispatcher, "media") val openGroupJob = processWithOpenGroupDispatcher(openGroupQueue, openGroupDispatcher, "openGroup") - val configJob = processWithDispatcher(configQueue, configDispatcher, "configDispatcher") while (isActive) { when (val job = queue.receive()) { - is InviteContactsJob, - is ConfigurationSyncJob -> { - configQueue.send(job) - } is NotifyPNServerJob, is AttachmentUploadJob, is GroupLeavingJob, @@ -167,7 +160,6 @@ class JobQueue : JobDelegate { txJob.cancel() mediaJob.cancel() openGroupJob.cancel() - configJob.cancel() } } @@ -239,8 +231,6 @@ class JobQueue : JobDelegate { BackgroundGroupAddJob.KEY, OpenGroupDeleteJob.KEY, RetrieveProfileAvatarJob.KEY, - ConfigurationSyncJob.KEY, - InviteContactsJob.KEY, GroupLeavingJob.KEY, LibSessionGroupLeavingJob.KEY ) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt index 46c87d5b90..cfe792274f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/SessionJobManagerFactories.kt @@ -16,7 +16,6 @@ class SessionJobManagerFactories { GroupAvatarDownloadJob.KEY to GroupAvatarDownloadJob.Factory(), BackgroundGroupAddJob.KEY to BackgroundGroupAddJob.Factory(), OpenGroupDeleteJob.KEY to OpenGroupDeleteJob.Factory(), - ConfigurationSyncJob.KEY to ConfigurationSyncJob.Factory() ) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 2f6707649d..b196cc93ac 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -25,7 +25,6 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability import org.session.libsession.messaging.open_groups.OpenGroupMessage import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.SodiumUtilities -import org.session.libsession.snode.GroupSubAccountSwarmAuth import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.snode.RawResponsePromise import org.session.libsession.snode.SnodeAPI @@ -184,13 +183,9 @@ object MessageSender { MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey) } is Destination.ClosedGroup -> { - val groupKeys = configFactory.getGroupKeysConfig(AccountId(destination.publicKey)) ?: throw Error.NoKeyPair val envelope = MessageWrapper.createEnvelope(kind, message.sentTimestamp!!, senderPublicKey, proto.build().toByteArray()) - groupKeys.use { keys -> - if (keys.keys().isEmpty()) { - throw Error.EncryptionFailed - } - keys.encrypt(envelope.toByteArray()) + configFactory.withGroupConfigs(AccountId(destination.publicKey)) { + it.groupKeys.encrypt(envelope.toByteArray()) } } else -> throw IllegalStateException("Destination should not be open group.") @@ -252,27 +247,13 @@ object MessageSender { namespaces.mapNotNull { namespace -> if (destination is Destination.ClosedGroup) { // possibly handle a failure for no user groups or no closed group signing key? - val group = configFactory.userGroups?.getClosedGroup(destination.publicKey) ?: return@mapNotNull null - val groupAuthData = group.authData - val groupAdminKey = group.adminKey - if (groupAuthData != null) { - configFactory.getGroupKeysConfig(AccountId(destination.publicKey))?.use { keys -> - SnodeAPI.sendMessage( - auth = GroupSubAccountSwarmAuth(keys, AccountId(destination.publicKey), groupAuthData), - message = snodeMessage, - namespace = namespace - ) - } - } else if (groupAdminKey != null) { - SnodeAPI.sendMessage( - auth = OwnedSwarmAuth(AccountId(destination.publicKey), null, groupAdminKey), - message = snodeMessage, - namespace = namespace - ) - } else { - Log.w("MessageSender", "No auth data for group") - null - } + val groupAuth = configFactory.getGroupAuth(AccountId(destination.publicKey)) ?: return@mapNotNull null + + SnodeAPI.sendMessage( + auth = groupAuth, + message = snodeMessage, + namespace = namespace + ) } else { SnodeAPI.sendMessage(snodeMessage, auth = null, namespace = namespace) } @@ -353,9 +334,9 @@ object MessageSender { message.sentTimestamp = nowWithOffset } // Attach the blocks message requests info - configFactory.user?.let { user -> + configFactory.withUserConfigs { configs -> if (message is VisibleMessage) { - message.blocksMessageRequests = !user.getCommunityMessageRequests() + message.blocksMessageRequests = !configs.userProfile.getCommunityMessageRequests() } } val userEdKeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()!! diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt index ae34349f97..b9b2ca62c3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt @@ -9,25 +9,18 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import network.loki.messenger.libsession_util.GroupInfoConfig -import network.loki.messenger.libsession_util.GroupKeysConfig -import network.loki.messenger.libsession_util.GroupMembersConfig import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.Sodium -import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters import org.session.libsession.messaging.messages.Destination -import org.session.libsession.snode.GroupSubAccountSwarmAuth -import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.model.BatchResponse import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.ConfigFactoryProtocol -import org.session.libsession.utilities.withGroupConfigsOrNull import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log @@ -41,7 +34,9 @@ class ClosedGroupPoller( private val executor: CoroutineDispatcher, private val closedGroupSessionId: AccountId, private val configFactoryProtocol: ConfigFactoryProtocol, - private val groupManagerV2: GroupManagerV2) { + private val groupManagerV2: GroupManagerV2, + private val storage: StorageProtocol, +) { data class ParsedRawMessage( val data: ByteArray, @@ -82,9 +77,8 @@ class ClosedGroupPoller( if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Starting closed group poller for ${closedGroupSessionId.hexString.take(4)}") job?.cancel() job = scope.launch(executor) { - val closedGroups = configFactoryProtocol.userGroups ?: return@launch while (isActive) { - val group = closedGroups.getClosedGroup(closedGroupSessionId.hexString) ?: break + val group = configFactoryProtocol.withUserConfigs { it.userGroups.getClosedGroup(closedGroupSessionId.hexString) } ?: break val nextPoll = runCatching { poll(group) } when { nextPoll.isFailure -> { @@ -114,156 +108,122 @@ class ClosedGroupPoller( private suspend fun poll(group: GroupInfo.ClosedGroupInfo): Long? = coroutineScope { val snode = SnodeAPI.getSingleTargetSnode(closedGroupSessionId.hexString).await() - configFactoryProtocol.withGroupConfigsOrNull(closedGroupSessionId) { info, members, keys -> - val hashesToExtend = mutableSetOf() + val groupAuth = configFactoryProtocol.getGroupAuth(closedGroupSessionId) ?: return@coroutineScope null + val configHashesToExtends = configFactoryProtocol.withGroupConfigs(closedGroupSessionId) { + buildSet { + addAll(it.groupKeys.currentHashes()) + addAll(it.groupInfo.currentHashes()) + addAll(it.groupMembers.currentHashes()) + } + } - hashesToExtend += info.currentHashes() - hashesToExtend += members.currentHashes() - hashesToExtend += keys.currentHashes() + val adminKey = requireNotNull(configFactoryProtocol.withUserConfigs { it.userGroups.getClosedGroup(closedGroupSessionId.hexString) }) { + "Group doesn't exist" + }.adminKey - val authData = group.authData - val adminKey = group.adminKey - val groupAccountId = group.groupAccountId - val auth = if (authData != null) { - GroupSubAccountSwarmAuth( - groupKeysConfig = keys, - accountId = groupAccountId, - authData = authData + val pollingTasks = mutableListOf>>() + + pollingTasks += "Poll revoked messages" to async { + handleRevoked( + SnodeAPI.sendBatchRequest( + snode, + closedGroupSessionId.hexString, + SnodeAPI.buildAuthenticatedRetrieveBatchRequest( + snode = snode, + auth = groupAuth, + namespace = Namespace.REVOKED_GROUP_MESSAGES(), + maxSize = null, + ), + Map::class.java ) - } else if (adminKey != null) { - OwnedSwarmAuth.ofClosedGroup( - groupAccountId = groupAccountId, - adminKey = adminKey - ) - } else { - Log.e("ClosedGroupPoller", "No auth data for group, polling is cancelled") - return@coroutineScope null - } - - val pollingTasks = mutableListOf>>() - - pollingTasks += "Poll revoked messages" to async { - handleRevoked( - body = SnodeAPI.sendBatchRequest( - groupAccountId, - SnodeAPI.buildAuthenticatedRetrieveBatchRequest( - snode = snode, - auth = auth, - namespace = Namespace.REVOKED_GROUP_MESSAGES(), - maxSize = null, - ), - Map::class.java), - keys = keys - ) - } - - pollingTasks += "Poll group messages" to async { - handleMessages( - body = SnodeAPI.sendBatchRequest( - groupAccountId, - SnodeAPI.buildAuthenticatedRetrieveBatchRequest( - snode = snode, - auth = auth, - namespace = Namespace.CLOSED_GROUP_MESSAGES(), - maxSize = null, - ), - Map::class.java), - snode = snode, - keysConfig = keys - ) - } - - pollingTasks += "Poll group keys config" to async { - handleKeyPoll( - response = SnodeAPI.sendBatchRequest( - groupAccountId, - SnodeAPI.buildAuthenticatedRetrieveBatchRequest( - snode = snode, - auth = auth, - namespace = keys.namespace(), - maxSize = null, - ), - Map::class.java), - keysConfig = keys, - infoConfig = info, - membersConfig = members - ) - } - - pollingTasks += "Poll group info config" to async { - handleInfo( - response = SnodeAPI.sendBatchRequest( - groupAccountId, - SnodeAPI.buildAuthenticatedRetrieveBatchRequest( - snode = snode, - auth = auth, - namespace = Namespace.CLOSED_GROUP_INFO(), - maxSize = null, - ), - Map::class.java), - infoConfig = info - ) - } - - pollingTasks += "Poll group members config" to async { - handleMembers( - response = SnodeAPI.sendBatchRequest( - groupAccountId, - SnodeAPI.buildAuthenticatedRetrieveBatchRequest( - snode = snode, - auth = auth, - namespace = Namespace.CLOSED_GROUP_MEMBERS(), - maxSize = null, - ), - Map::class.java), - membersConfig = members - ) - } - - if (hashesToExtend.isNotEmpty() && adminKey != null) { - pollingTasks += "Extend group config TTL" to async { - SnodeAPI.sendBatchRequest( - groupAccountId, - SnodeAPI.buildAuthenticatedAlterTtlBatchRequest( - messageHashes = hashesToExtend.toList(), - auth = auth, - newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, - extend = true - ), - ) - } - } - - val errors = pollingTasks.mapNotNull { (name, task) -> - runCatching { task.await() } - .exceptionOrNull() - ?.takeIf { it !is CancellationException } - ?.let { RuntimeException("Error executing: $name", it) } - } - - if (errors.isNotEmpty()) { - throw PollerException("Error polling closed group", errors) - } - - // If we no longer have a group, stop poller - if (configFactoryProtocol.userGroups?.getClosedGroup(closedGroupSessionId.hexString) == null) return@coroutineScope null - - // if poll result body is null here we don't have any things ig - if (ENABLE_LOGGING) Log.d( - "ClosedGroupPoller", - "Poll results @${SnodeAPI.nowWithOffset}:" ) + } - val requiresSync = - info.needsPush() || members.needsPush() || keys.needsRekey() || keys.pendingConfig() != null + pollingTasks += "Poll group messages" to async { + handleMessages( + body = SnodeAPI.sendBatchRequest( + snode, + closedGroupSessionId.hexString, + SnodeAPI.buildAuthenticatedRetrieveBatchRequest( + snode = snode, + auth = groupAuth, + namespace = Namespace.CLOSED_GROUP_MESSAGES(), + maxSize = null, + ), + Map::class.java), + snode = snode, + ) + } - if (info.needsDump() || members.needsDump() || keys.needsDump()) { - configFactoryProtocol.saveGroupConfigs(keys, info, members) + pollingTasks += "Poll group keys config" to async { + handleKeyPoll( + response = SnodeAPI.sendBatchRequest( + snode, + closedGroupSessionId.hexString, + SnodeAPI.buildAuthenticatedRetrieveBatchRequest( + snode = snode, + auth = groupAuth, + namespace = Namespace.ENCRYPTION_KEYS(), + maxSize = null, + ), + Map::class.java), + ) + } + + pollingTasks += "Poll group info config" to async { + handleInfo( + response = SnodeAPI.sendBatchRequest( + snode, + closedGroupSessionId.hexString, + SnodeAPI.buildAuthenticatedRetrieveBatchRequest( + snode = snode, + auth = groupAuth, + namespace = Namespace.CLOSED_GROUP_INFO(), + maxSize = null, + ), + Map::class.java), + ) + } + + pollingTasks += "Poll group members config" to async { + handleMembers( + SnodeAPI.sendBatchRequest( + snode, + closedGroupSessionId.hexString, + SnodeAPI.buildAuthenticatedRetrieveBatchRequest( + snode = snode, + auth = groupAuth, + namespace = Namespace.CLOSED_GROUP_MEMBERS(), + maxSize = null, + ), + Map::class.java), + ) + } + + if (configHashesToExtends.isNotEmpty() && adminKey != null) { + pollingTasks += "Extend group config TTL" to async { + SnodeAPI.sendBatchRequest( + snode, + closedGroupSessionId.hexString, + SnodeAPI.buildAuthenticatedAlterTtlBatchRequest( + messageHashes = configHashesToExtends.toList(), + auth = groupAuth, + newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, + extend = true + ), + ) } + } - if (requiresSync) { - configFactoryProtocol.scheduleUpdate(Destination.ClosedGroup(closedGroupSessionId.hexString)) - } + val errors = pollingTasks.mapNotNull { (name, task) -> + runCatching { task.await() } + .exceptionOrNull() + ?.takeIf { it !is CancellationException } + ?.let { RuntimeException("Error executing: $name", it) } + } + + if (errors.isNotEmpty()) { + throw PollerException("Error polling closed group", errors) } POLL_INTERVAL // this might change in future @@ -281,9 +241,8 @@ class ClosedGroupPoller( } } - private suspend fun handleRevoked(body: RawResponse, keys: GroupKeysConfig) { + private suspend fun handleRevoked(body: RawResponse) { // This shouldn't ever return null at this point - val userSessionId = configFactoryProtocol.userSessionId()!! val messages = body["messages"] as? List<*> ?: return Log.w("GroupPoller", "body didn't contain a list of messages") messages.forEach { messageMap -> @@ -305,7 +264,13 @@ class ClosedGroupPoller( val message = decoded.decodeToString() if (Sodium.KICKED_REGEX.matches(message)) { val (sessionId, generation) = message.split("-") - if (sessionId == userSessionId.hexString && generation.toInt() >= keys.currentGeneration()) { + val currentKeysGeneration by lazy { + configFactoryProtocol.withGroupConfigs(closedGroupSessionId) { + it.groupKeys.currentGeneration() + } + } + + if (sessionId == storage.getUserPublicKey() && generation.toInt() >= currentKeysGeneration) { try { groupManagerV2.handleKicked(closedGroupSessionId) } catch (e: Exception) { @@ -318,51 +283,51 @@ class ClosedGroupPoller( } } - private fun handleKeyPoll(response: RawResponse, - keysConfig: GroupKeysConfig, - infoConfig: GroupInfoConfig, - membersConfig: GroupMembersConfig) { + private fun handleKeyPoll(response: RawResponse) { // get all the data to hash objects and process them val allMessages = parseMessages(response) if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Total key messages this poll: ${allMessages.size}") var total = 0 allMessages.forEach { (message, hash, timestamp) -> - if (keysConfig.loadKey(message, hash, timestamp, infoConfig, membersConfig)) { - total++ + configFactoryProtocol.withMutableGroupConfigs(closedGroupSessionId) { configs -> + if (configs.loadKeys(message, hash, timestamp)) { + total++ + } } + if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for keys on ${closedGroupSessionId.hexString}") } if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Total key messages consumed: $total") } - private fun handleInfo(response: RawResponse, - infoConfig: GroupInfoConfig) { + private fun handleInfo(response: RawResponse) { val messages = parseMessages(response) messages.forEach { (message, hash, _) -> - infoConfig.merge(hash to message) + configFactoryProtocol.withMutableGroupConfigs(closedGroupSessionId) { configs -> + configs.groupInfo.merge(arrayOf(hash to message)) + } if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for info on ${closedGroupSessionId.hexString}") } - if (messages.isNotEmpty()) { - val lastTimestamp = messages.maxOf { it.timestamp } - MessagingModuleConfiguration.shared.storage.notifyConfigUpdates(infoConfig, lastTimestamp) - } } - private fun handleMembers(response: RawResponse, - membersConfig: GroupMembersConfig) { + private fun handleMembers(response: RawResponse) { parseMessages(response).forEach { (message, hash, _) -> - membersConfig.merge(hash to message) + configFactoryProtocol.withMutableGroupConfigs(closedGroupSessionId) { configs -> + configs.groupMembers.merge(arrayOf(hash to message)) + } if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for members on ${closedGroupSessionId.hexString}") } } - private fun handleMessages(body: RawResponse, snode: Snode, keysConfig: GroupKeysConfig) { - val messages = SnodeAPI.parseRawMessagesResponse( - rawResponse = body, - snode = snode, - publicKey = closedGroupSessionId.hexString, - decrypt = keysConfig::decrypt - ) + private fun handleMessages(body: RawResponse, snode: Snode) { + val messages = configFactoryProtocol.withGroupConfigs(closedGroupSessionId) { + SnodeAPI.parseRawMessagesResponse( + rawResponse = body, + snode = snode, + publicKey = closedGroupSessionId.hexString, + decrypt = it.groupKeys::decrypt, + ) + } val parameters = messages.map { (envelope, serverHash) -> MessageReceiveParameters( diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 09d0bdcf7a..b12c9065fa 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -3,10 +3,17 @@ package org.session.libsession.messaging.sending_receiving.pollers import android.util.SparseArray import androidx.core.util.valueIterator import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.ConversationVolatileConfig +import network.loki.messenger.libsession_util.MutableConfig +import network.loki.messenger.libsession_util.MutableContacts +import network.loki.messenger.libsession_util.MutableConversationVolatileConfig +import network.loki.messenger.libsession_util.MutableUserGroupsConfig +import network.loki.messenger.libsession_util.MutableUserProfile import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserProfile import nl.komponents.kovenant.Deferred @@ -24,6 +31,8 @@ import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeModule import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.Contact.Name +import org.session.libsession.utilities.MutableGroupConfigs import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Namespace @@ -135,9 +144,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { } } - private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfigObject: ConfigBase?) { - if (forConfigObject == null) return - + private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfig: Class) { val messages = rawMessages["messages"] as? List<*> val processed = if (!messages.isNullOrEmpty()) { SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace) @@ -152,20 +159,19 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { if (processed.isEmpty()) return - var latestMessageTimestamp: Long? = null - processed.forEach { (body, hash, timestamp) -> + processed.forEach { (body, hash, _) -> try { - forConfigObject.merge(hash to body) - latestMessageTimestamp = if (timestamp > (latestMessageTimestamp ?: 0L)) { timestamp } else { latestMessageTimestamp } + configFactory.withMutableUserConfigs { configs -> + configs + .allConfigs() + .filter { it.javaClass.isInstance(forConfig) } + .first() + .merge(arrayOf(hash to body)) + } } catch (e: Exception) { Log.e(TAG, e) } } - // process new results - // latestMessageTimestamp should always be non-null if the config object needs dump - if (forConfigObject.needsDump() && latestMessageTimestamp != null) { - configFactory.persist(forConfigObject, latestMessageTimestamp ?: SnodeAPI.nowWithOffset) - } } private fun poll(userProfileOnly: Boolean, snode: Snode, deferred: Deferred): Promise { @@ -181,7 +187,8 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { val hashesToExtend = mutableSetOf() val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth) - configFactory.user?.let { config -> + configFactory.withUserConfigs { + val config = it.userProfile hashesToExtend += config.currentHashes() SnodeAPI.buildAuthenticatedRetrieveBatchRequest( snode = snode, @@ -189,7 +196,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { namespace = config.namespace(), maxSize = -8 ) - }?.let { request -> + }.let { request -> requests += request } @@ -199,7 +206,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { auth = userAuth, newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, extend = true - )?.let { extensionRequest -> + ).let { extensionRequest -> requests += extensionRequest } } @@ -217,7 +224,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { if (body == null) { Log.e(TAG, "Batch sub-request didn't contain a body") } else { - processConfig(snode, body, configFactory.user!!.namespace(), configFactory.user) + processConfig(snode, body, Namespace.USER_PROFILE(), MutableUserProfile::class.java) } } } @@ -230,6 +237,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { } } + private fun poll(snode: Snode, deferred: Deferred): Promise { if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) } return task { @@ -244,17 +252,19 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { } // get the latest convo info volatile val hashesToExtend = mutableSetOf() - configFactory.getUserConfigs().map { config -> - hashesToExtend += config.currentHashes() - SnodeAPI.buildAuthenticatedRetrieveBatchRequest( - snode = snode, - auth = userAuth, - namespace = config.namespace(), - maxSize = -8 - ) - }.forEach { request -> + configFactory.withUserConfigs { + it.allConfigs().map { config -> + hashesToExtend += config.currentHashes() + config.namespace() to SnodeAPI.buildAuthenticatedRetrieveBatchRequest( + snode = snode, + auth = userAuth, + namespace = config.namespace(), + maxSize = -8 + ) + } + }.forEach { (namespace, request) -> // namespaces here should always be set - requestSparseArray[request.namespace!!] = request + requestSparseArray[namespace] = request } val requests = @@ -266,7 +276,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { auth = userAuth, newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, extend = true - )?.let { extensionRequest -> + ).let { extensionRequest -> requests += extensionRequest } } @@ -280,14 +290,15 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { val responseList = (rawResponses["results"] as List) // in case we had null configs, the array won't be fully populated // index of the sparse array key iterator should be the request index, with the key being the namespace - listOfNotNull( - configFactory.user?.namespace(), - configFactory.contacts?.namespace(), - configFactory.userGroups?.namespace(), - configFactory.convoVolatile?.namespace() - ).map { - it to requestSparseArray.indexOfKey(it) - }.filter { (_, i) -> i >= 0 }.forEach { (key, requestIndex) -> + sequenceOf( + Namespace.USER_PROFILE() to MutableUserProfile::class.java, + Namespace.CONTACTS() to MutableContacts::class.java, + Namespace.GROUPS() to MutableUserGroupsConfig::class.java, + Namespace.CONVO_INFO_VOLATILE() to MutableConversationVolatileConfig::class.java + ).map { (namespace, configClass) -> + Triple(namespace, configClass, requestSparseArray.indexOfKey(namespace)) + }.filter { (_, _, i) -> i >= 0 } + .forEach { (namespace, configClass, requestIndex) -> responseList.getOrNull(requestIndex)?.let { rawResponse -> if (rawResponse["code"] as? Int != 200) { Log.e(TAG, "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}") @@ -298,16 +309,8 @@ class Poller(private val configFactory: ConfigFactoryProtocol) { Log.e(TAG, "Batch sub-request didn't contain a body") return@forEach } - if (key == Namespace.DEFAULT()) { - return@forEach // continue, skip default namespace - } else { - when (ConfigBase.kindFor(key)) { - UserProfile::class.java -> processConfig(snode, body, key, configFactory.user) - Contacts::class.java -> processConfig(snode, body, key, configFactory.contacts) - ConversationVolatileConfig::class.java -> processConfig(snode, body, key, configFactory.convoVolatile) - UserGroupsConfig::class.java -> processConfig(snode, body, key, configFactory.userGroups) - } - } + + processConfig(snode, body, namespace, configClass) } } diff --git a/libsession/src/main/java/org/session/libsession/snode/GroupSubAccountSwarmAuth.kt b/libsession/src/main/java/org/session/libsession/snode/GroupSubAccountSwarmAuth.kt deleted file mode 100644 index d4beb08de7..0000000000 --- a/libsession/src/main/java/org/session/libsession/snode/GroupSubAccountSwarmAuth.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.session.libsession.snode - -import network.loki.messenger.libsession_util.GroupKeysConfig -import org.session.libsignal.utilities.AccountId - -/** - * A [SwarmAuth] that signs message using a group's subaccount. This should be used for non-admin - * users of a group signing their messages. - */ -class GroupSubAccountSwarmAuth( - private val groupKeysConfig: GroupKeysConfig, - override val accountId: AccountId, - private val authData: ByteArray -) : SwarmAuth { - override val ed25519PublicKeyHex: String? get() = null - - init { - check(authData.size == 100) { - "Invalid auth data size, expecting 100 but got ${authData.size}" - } - } - - override fun sign(data: ByteArray): Map { - val auth = groupKeysConfig.subAccountSign(data, authData) - return buildMap { - put("subaccount", auth.subAccount) - put("subaccount_sig", auth.subAccountSig) - put("signature", auth.signature) - } - } - - override fun signForPushRegistry(data: ByteArray): Map { - val auth = groupKeysConfig.subAccountSign(data, authData) - return buildMap { - put("subkey_tag", auth.subAccount) - put("signature", auth.signature) - } - } -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index c26f7ebd02..1cc83c66b3 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -579,7 +579,8 @@ object SnodeAPI { } private data class RequestInfo( - val accountId: AccountId, + val snode: Snode, + val publicKey: String, val request: SnodeBatchRequestInfo, val responseType: Class<*>, val callback: SendChannel>, @@ -594,15 +595,17 @@ object SnodeAPI { val batchWindowMills = 100L + data class BatchKey(val snodeAddress: String, val publicKey: String) + @Suppress("OPT_IN_USAGE") GlobalScope.launch { - val batches = hashMapOf>() + val batches = hashMapOf>() while (true) { val batch = select?> { // If we receive a request, add it to the batch batchRequests.onReceive { - batches.getOrPut(it.accountId) { mutableListOf() }.add(it) + batches.getOrPut(BatchKey(it.snode.address, it.publicKey)) { mutableListOf() }.add(it) null } @@ -622,11 +625,11 @@ object SnodeAPI { if (batch != null) { launch batch@{ - val accountId = batch.first().accountId + val snode = batch.first().snode val responses = try { getBatchResponse( - snode = getSingleTargetSnode(accountId.hexString).await(), - publicKey = accountId.hexString, + snode = snode, + publicKey = batch.first().publicKey, requests = batch.map { it.request }, sequence = false ) } catch (e: Exception) { @@ -660,21 +663,23 @@ object SnodeAPI { } suspend fun sendBatchRequest( - swarmAccount: AccountId, + snode: Snode, + publicKey: String, request: SnodeBatchRequestInfo, responseType: Class, ): T { val callback = Channel>() @Suppress("UNCHECKED_CAST") - batchedRequestsSender.send(RequestInfo(swarmAccount, request, responseType, callback as SendChannel)) + batchedRequestsSender.send(RequestInfo(snode, publicKey, request, responseType, callback as SendChannel)) return callback.receive().getOrThrow() } suspend fun sendBatchRequest( - swarmAccount: AccountId, + snode: Snode, + publicKey: String, request: SnodeBatchRequestInfo, ): JsonNode { - return sendBatchRequest(swarmAccount, request, JsonNode::class.java) + return sendBatchRequest(snode, publicKey, request, JsonNode::class.java) } suspend fun getBatchResponse( @@ -803,9 +808,9 @@ object SnodeAPI { } return scope.retrySuspendAsPromise(maxRetryCount) { - val destination = message.recipient sendBatchRequest( - swarmAccount = AccountId(destination), + snode = getSingleTargetSnode(message.recipient).await(), + publicKey = message.recipient, request = SnodeBatchRequestInfo( method = Snode.Method.SendMessage.rawValue, params = params, diff --git a/libsession/src/main/java/org/session/libsession/snode/model/BatchResponse.kt b/libsession/src/main/java/org/session/libsession/snode/model/BatchResponse.kt index fd022b5898..7bc4308417 100644 --- a/libsession/src/main/java/org/session/libsession/snode/model/BatchResponse.kt +++ b/libsession/src/main/java/org/session/libsession/snode/model/BatchResponse.kt @@ -10,5 +10,8 @@ data class BatchResponse @JsonCreator constructor( data class Item @JsonCreator constructor( @param:JsonProperty("code") val code: Int, @param:JsonProperty("body") val body: JsonNode, - ) + ) { + val isSuccessful: Boolean + get() = code in 200..299 + } } diff --git a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index 1839130f75..1ee3b58098 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -1,82 +1,109 @@ package org.session.libsession.utilities import kotlinx.coroutines.flow.Flow -import network.loki.messenger.libsession_util.Config -import network.loki.messenger.libsession_util.ConfigBase -import network.loki.messenger.libsession_util.Contacts -import network.loki.messenger.libsession_util.ConversationVolatileConfig -import network.loki.messenger.libsession_util.GroupInfoConfig -import network.loki.messenger.libsession_util.GroupKeysConfig -import network.loki.messenger.libsession_util.GroupMembersConfig -import network.loki.messenger.libsession_util.UserGroupsConfig -import network.loki.messenger.libsession_util.UserProfile -import org.session.libsession.messaging.messages.Destination +import network.loki.messenger.libsession_util.MutableConfig +import network.loki.messenger.libsession_util.MutableContacts +import network.loki.messenger.libsession_util.MutableConversationVolatileConfig +import network.loki.messenger.libsession_util.MutableGroupInfoConfig +import network.loki.messenger.libsession_util.MutableGroupKeysConfig +import network.loki.messenger.libsession_util.MutableGroupMembersConfig +import network.loki.messenger.libsession_util.MutableUserGroupsConfig +import network.loki.messenger.libsession_util.MutableUserProfile +import network.loki.messenger.libsession_util.ReadableConfig +import network.loki.messenger.libsession_util.ReadableContacts +import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig +import network.loki.messenger.libsession_util.ReadableGroupInfoConfig +import network.loki.messenger.libsession_util.ReadableGroupKeysConfig +import network.loki.messenger.libsession_util.ReadableGroupMembersConfig +import network.loki.messenger.libsession_util.ReadableUserGroupsConfig +import network.loki.messenger.libsession_util.ReadableUserProfile +import org.session.libsession.snode.SwarmAuth import org.session.libsignal.utilities.AccountId interface ConfigFactoryProtocol { + val configUpdateNotifications: Flow - val user: UserProfile? - val contacts: Contacts? - val convoVolatile: ConversationVolatileConfig? - val userGroups: UserGroupsConfig? + fun withUserConfigs(cb: (UserConfigs) -> T): T + fun withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T - val configUpdateNotifications: Flow - - fun getGroupInfoConfig(groupSessionId: AccountId): GroupInfoConfig? - fun getGroupMemberConfig(groupSessionId: AccountId): GroupMembersConfig? - fun getGroupKeysConfig(groupSessionId: AccountId, - info: GroupInfoConfig? = null, - members: GroupMembersConfig? = null, - free: Boolean = true): GroupKeysConfig? - - fun getUserConfigs(): List - fun persist(forConfigObject: Config, timestamp: Long, forPublicKey: String? = null) + fun withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T + fun withMutableGroupConfigs(groupId: AccountId, cb: (MutableGroupConfigs) -> T): T fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean - fun saveGroupConfigs( - groupKeys: GroupKeysConfig, - groupInfo: GroupInfoConfig, - groupMembers: GroupMembersConfig - ) - fun removeGroup(closedGroupId: AccountId) - fun scheduleUpdate(destination: Destination) - fun constructGroupKeysConfig( - groupSessionId: AccountId, - info: GroupInfoConfig, - members: GroupMembersConfig - ): GroupKeysConfig? + fun getGroupAuth(groupId: AccountId): SwarmAuth? + fun removeGroup(groupId: AccountId) fun maybeDecryptForUser(encoded: ByteArray, domain: String, closedGroupSessionId: AccountId): ByteArray? - fun userSessionId(): AccountId? - } -interface ConfigFactoryUpdateListener { - fun notifyUpdates(forConfigObject: Config, messageTimestamp: Long) + +interface UserConfigs { + val contacts: ReadableContacts + val userGroups: ReadableUserGroupsConfig + val userProfile: ReadableUserProfile + val convoInfoVolatile: ReadableConversationVolatileConfig + + fun allConfigs(): Sequence = sequenceOf(contacts, userGroups, userProfile, convoInfoVolatile) } -/** - * Access group configs if they exist, otherwise return null. - * - * Note: The config objects will be closed after the callback is executed. Any attempt - * to store the config objects will result in a native crash. - */ -inline fun ConfigFactoryProtocol.withGroupConfigsOrNull( - groupId: AccountId, - cb: (GroupInfoConfig, GroupMembersConfig, GroupKeysConfig) -> T -): T? { - getGroupInfoConfig(groupId)?.use { groupInfo -> - getGroupMemberConfig(groupId)?.use { groupMembers -> - getGroupKeysConfig(groupId, groupInfo, groupMembers)?.use { groupKeys -> - return cb(groupInfo, groupMembers, groupKeys) - } - } - } +interface MutableUserConfigs : UserConfigs { + override val contacts: MutableContacts + override val userGroups: MutableUserGroupsConfig + override val userProfile: MutableUserProfile + override val convoInfoVolatile: MutableConversationVolatileConfig - return null -} \ No newline at end of file + override fun allConfigs(): Sequence = sequenceOf(contacts, userGroups, userProfile, convoInfoVolatile) +} + +interface GroupConfigs { + val groupInfo: ReadableGroupInfoConfig + val groupMembers: ReadableGroupMembersConfig + val groupKeys: ReadableGroupKeysConfig +} + +interface MutableGroupConfigs : GroupConfigs { + override val groupInfo: MutableGroupInfoConfig + override val groupMembers: MutableGroupMembersConfig + override val groupKeys: MutableGroupKeysConfig + + fun loadKeys(message: ByteArray, hash: String, timestamp: Long): Boolean + fun rekeys() +} + +sealed interface ConfigUpdateNotification { + data object UserConfigs : ConfigUpdateNotification + data class GroupConfigsUpdated(val groupId: AccountId) : ConfigUpdateNotification + data class GroupConfigsDeleted(val groupId: AccountId) : ConfigUpdateNotification +} + +//interface ConfigFactoryUpdateListener { +// fun notifyUpdates(forConfigObject: Config, messageTimestamp: Long) +//} + + + +///** +// * Access group configs if they exist, otherwise return null. +// * +// * Note: The config objects will be closed after the callback is executed. Any attempt +// * to store the config objects will result in a native crash. +// */ +//inline fun ConfigFactoryProtocol.withGroupConfigsOrNull( +// groupId: AccountId, +// cb: (GroupInfoConfig, GroupMembersConfig, GroupKeysConfig) -> T +//): T? { +// getGroupInfoConfig(groupId)?.use { groupInfo -> +// getGroupMemberConfig(groupId)?.use { groupMembers -> +// getGroupKeysConfig(groupId, groupInfo, groupMembers)?.use { groupKeys -> +// return cb(groupInfo, groupMembers, groupKeys) +// } +// } +// } +// +// return null +//} \ No newline at end of file