Config revamp WIP

This commit is contained in:
SessionHero01 2024-09-26 15:43:45 +10:00
parent 7eb615f8dc
commit 771d63e902
No known key found for this signature in database
58 changed files with 1831 additions and 2532 deletions

View File

@ -134,7 +134,7 @@ import network.loki.messenger.libsession_util.UserProfile;
* @author Moxie Marlinspike * @author Moxie Marlinspike
*/ */
@HiltAndroidApp @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"; public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
@ -214,15 +214,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return this.persistentLogger; 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 @Override
public void toast(@StringRes int stringRes, int toastLength, @NonNull Map<String, String> parameters) { public void toast(@StringRes int stringRes, int toastLength, @NonNull Map<String, String> parameters) {
Phrase builder = Phrase.from(this, stringRes); Phrase builder = Phrase.from(this, stringRes);
@ -510,7 +501,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
Log.d("Loki", "Failed to delete database."); Log.d("Loki", "Failed to delete database.");
return false; return false;
} }
configFactory.keyPairChanged(); configFactory.clearAll();
return true; return true;
} }

View File

@ -4,8 +4,7 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.MessageSender 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.database.model.MessageRecord
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.getSubbedCharSequence import org.thoughtcrime.securesms.ui.getSubbedCharSequence
import org.thoughtcrime.securesms.ui.getSubbedString
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -29,10 +26,11 @@ class DisappearingMessages @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val textSecurePreferences: TextSecurePreferences, private val textSecurePreferences: TextSecurePreferences,
private val messageExpirationManager: MessageExpirationManagerProtocol, private val messageExpirationManager: MessageExpirationManagerProtocol,
private val storage: StorageProtocol
) { ) {
fun set(threadId: Long, address: Address, mode: ExpiryMode, isGroup: Boolean) { fun set(threadId: Long, address: Address, mode: ExpiryMode, isGroup: Boolean) {
val expiryChangeTimestampMs = SnodeAPI.nowWithOffset val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
MessagingModuleConfiguration.shared.storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs)) storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs))
val message = ExpirationTimerUpdate(isGroup = isGroup).apply { val message = ExpirationTimerUpdate(isGroup = isGroup).apply {
expiryMode = mode expiryMode = mode
@ -44,11 +42,6 @@ class DisappearingMessages @Inject constructor(
messageExpirationManager.insertExpirationTimerMessage(message) messageExpirationManager.insertExpirationTimerMessage(message)
MessageSender.send(message, address) MessageSender.send(message, address)
if (address.isClosedGroupV2) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(Destination.from(address))
} else {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
}
} }
fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog { fun showFollowSettingDialog(context: Context, message: MessageRecord) = context.showSessionDialog {

View File

@ -11,17 +11,20 @@ import android.widget.Toast
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import network.loki.messenger.R 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.OpenGroupUrlParser
import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import javax.inject.Inject
/** Shown upon tapping an open group invitation. */ /** Shown upon tapping an open group invitation. */
class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() { class JoinOpenGroupDialog(private val name: String, private val url: String) : DialogFragment() {
@Inject
lateinit var storage: StorageProtocol
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
title(resources.getString(R.string.communityJoin)) title(resources.getString(R.string.communityJoin))
val explanation = Phrase.from(context, R.string.communityJoinDescription).put(COMMUNITY_NAME_KEY, name).format() 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 { ThreadUtils.queue {
try { try {
openGroup.apply { OpenGroupManager.add(server, room, serverPublicKey, activity) } openGroup.apply { OpenGroupManager.add(server, room, serverPublicKey, activity) }
MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server, openGroup.room) storage.onOpenGroupAdded(openGroup.server, openGroup.room)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity)
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(activity, R.string.communityErrorDescription, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.communityErrorDescription, Toast.LENGTH_SHORT).show()
} }

View File

@ -93,7 +93,8 @@ object ConversationMenuHelper {
// Groups v2 menu // Groups v2 menu
if (thread.isClosedGroupV2Recipient) { 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) inflater.inflate(R.menu.menu_conversation_groups_v2_admin, menu)
} }
@ -346,15 +347,15 @@ object ConversationMenuHelper {
thread.isClosedGroupV2Recipient -> { thread.isClosedGroupV2Recipient -> {
val accountId = AccountId(thread.address.serialize()) val accountId = AccountId(thread.address.serialize())
val group = configFactory.userGroups?.getClosedGroup(accountId.hexString) ?: return val group = configFactory.withUserConfigs { it.userGroups.getClosedGroup(accountId.hexString) } ?: return
val (name, isAdmin) = configFactory.getGroupInfoConfig(accountId)?.use { val name = configFactory.withGroupConfigs(accountId) {
it.getName() to group.hasAdminKey() it.groupInfo.getName()
} ?: return }
confirmAndLeaveClosedGroup( confirmAndLeaveClosedGroup(
context = context, context = context,
groupName = name, groupName = name,
isAdmin = isAdmin, isAdmin = group.hasAdminKey(),
threadID = threadID, threadID = threadID,
storage = storage, storage = storage,
doLeave = { doLeave = {

View File

@ -9,6 +9,8 @@ import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
typealias ConfigVariant = String
class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) { class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) {
companion object { 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_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
private const val VARIANT_IN_AND_PUBKEY_WHERE = "$VARIANT in (?) AND $PUBKEY = ?" private const val VARIANT_IN_AND_PUBKEY_WHERE = "$VARIANT in (?) AND $PUBKEY = ?"
val KEYS_VARIANT = SharedConfigMessage.Kind.ENCRYPTION_KEYS.name val CONTACTS_VARIANT: ConfigVariant = SharedConfigMessage.Kind.CONTACTS.name
val INFO_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_INFO.name val USER_GROUPS_VARIANT: ConfigVariant = SharedConfigMessage.Kind.GROUPS.name
val MEMBER_VARIANT = SharedConfigMessage.Kind.CLOSED_GROUP_MEMBERS.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 db = writableDatabase
val contentValues = contentValuesOf( val contentValues = contentValuesOf(
VARIANT to variant, 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 db = readableDatabase
val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
return query?.use { cursor -> 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 db = readableDatabase
val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null) val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
if (cursor == null) return 0 if (cursor == null) return 0

View File

@ -8,7 +8,6 @@ import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.AttachmentUploadJob
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob 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.Job
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageSendJob 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 } 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? { fun getMessageSendJob(messageSendJobID: String): MessageSendJob? {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.get(sessionJobTable, "$jobID = ? AND $jobType = ?", arrayOf( messageSendJobID, MessageSendJob.KEY )) { cursor -> return database.get(sessionJobTable, "$jobID = ? AND $jobType = ?", arrayOf( messageSendJobID, MessageSendJob.KEY )) { cursor ->

View File

@ -2,50 +2,39 @@ package org.thoughtcrime.securesms.database
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.google.protobuf.ByteString
import com.goterl.lazysodium.utils.KeyPair 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 java.security.MessageDigest
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN 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_PINNED
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE
import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.ReadableContacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig
import network.loki.messenger.libsession_util.GroupInfoConfig import network.loki.messenger.libsession_util.ReadableGroupInfoConfig
import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.ReadableUserGroupsConfig
import network.loki.messenger.libsession_util.GroupMembersConfig import network.loki.messenger.libsession_util.ReadableUserProfile
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.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupDisplayInfo
import network.loki.messenger.libsession_util.util.GroupInfo 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.UserPic
import network.loki.messenger.libsession_util.util.afterSend 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.avatars.AvatarHelper
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.database.userAuth
import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.BlindedIdMapping
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.AttachmentUploadJob
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob 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.GroupAvatarDownloadJob
import org.session.libsession.messaging.jobs.InviteContactsJob
import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.jobs.RetrieveProfileAvatarJob 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.ExpirationConfiguration
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage 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.GroupMember
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi 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.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage 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.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.snode.GroupSubAccountSwarmAuth
import org.session.libsession.snode.OnionRequestAPI 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
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
import org.session.libsession.utilities.Address.Companion.fromSerialized 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.GroupRecord
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.ProfileKeyUtil 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.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.Recipient.DisappearingState 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.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup 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.Base64
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.toHexString import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.crypto.KeyPairUtilities 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.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord 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.GroupManager
import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.SessionMetaProtocol import org.thoughtcrime.securesms.util.SessionMetaProtocol
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
import network.loki.messenger.libsession_util.util.GroupMember as LibSessionGroupMember import network.loki.messenger.libsession_util.util.GroupMember as LibSessionGroupMember
@ -145,33 +115,40 @@ open class Storage(
) : Database(context, helper), StorageProtocol, ) : Database(context, helper), StorageProtocol,
ThreadDatabase.ConversationThreadUpdateListener { ThreadDatabase.ConversationThreadUpdateListener {
init {
observeConfigUpdates()
}
override fun threadCreated(address: Address, threadId: Long) { override fun threadCreated(address: Address, threadId: Long) {
val localUserAddress = getUserPublicKey() ?: return val localUserAddress = getUserPublicKey() ?: return
if (!getRecipientApproved(address) && localUserAddress != address.serialize()) return // don't store unapproved / message requests if (!getRecipientApproved(address) && localUserAddress != address.serialize()) return // don't store unapproved / message requests
val volatile = configFactory.convoVolatile ?: return
if (address.isGroup) { if (address.isGroup) {
val groups = configFactory.userGroups ?: return
when { when {
address.isLegacyClosedGroup -> { address.isLegacyClosedGroup -> {
val accountId = GroupUtil.doubleDecodeGroupId(address.serialize()) val accountId = GroupUtil.doubleDecodeGroupId(address.serialize())
val closedGroup = getGroup(address.toGroupString()) val closedGroup = getGroup(address.toGroupString())
if (closedGroup != null && closedGroup.isActive) { if (closedGroup != null && closedGroup.isActive) {
val legacyGroup = groups.getOrConstructLegacyGroupInfo(accountId) configFactory.withMutableUserConfigs { configs ->
groups.set(legacyGroup) val legacyGroup = configs.userGroups.getOrConstructLegacyGroupInfo(accountId)
val newVolatileParams = volatile.getOrConstructLegacyGroup(accountId).copy( configs.userGroups.set(legacyGroup)
val newVolatileParams = configs.convoInfoVolatile.getOrConstructLegacyGroup(accountId).copy(
lastRead = SnodeAPI.nowWithOffset, lastRead = SnodeAPI.nowWithOffset,
) )
volatile.set(newVolatileParams) configs.convoInfoVolatile.set(newVolatileParams)
}
} }
} }
address.isClosedGroupV2 -> { address.isClosedGroupV2 -> {
val AccountId = address.serialize() configFactory.withMutableUserConfigs { configs ->
groups.getClosedGroup(AccountId) ?: return Log.d("Closed group doesn't exist locally", NullPointerException()) val accountId = address.serialize()
val conversation = Conversation.ClosedGroup( configs.userGroups.getClosedGroup(accountId)
AccountId, 0, false ?: return@withMutableUserConfigs Log.d("Closed group doesn't exist locally", NullPointerException())
)
volatile.set(conversation) configs.convoInfoVolatile.getOrConstructClosedGroup(accountId)
}
} }
address.isCommunity -> { address.isCommunity -> {
// these should be added on the group join / group info fetch // these should be added on the group join / group info fetch
@ -183,28 +160,32 @@ open class Storage(
if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return
// don't update our own address into the contacts DB // don't update our own address into the contacts DB
if (getUserPublicKey() != address.serialize()) { if (getUserPublicKey() != address.serialize()) {
val contacts = configFactory.contacts ?: return configFactory.withMutableUserConfigs { configs ->
contacts.upsertContact(address.serialize()) { configs.contacts.upsertContact(address.serialize()) {
priority = PRIORITY_VISIBLE priority = PRIORITY_VISIBLE
} }
}
} else { } else {
val userProfile = configFactory.user ?: return configFactory.withMutableUserConfigs { configs ->
userProfile.setNtsPriority(PRIORITY_VISIBLE) configs.userProfile.setNtsPriority(PRIORITY_VISIBLE)
}
DatabaseComponent.get(context).threadDatabase().setHasSent(threadId, true) 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) { override fun threadDeleted(address: Address, threadId: Long) {
val volatile = configFactory.convoVolatile ?: return configFactory.withMutableUserConfigs { configs ->
if (address.isGroup) { if (address.isGroup) {
val groups = configFactory.userGroups ?: return
if (address.isLegacyClosedGroup) { if (address.isLegacyClosedGroup) {
val accountId = GroupUtil.doubleDecodeGroupId(address.serialize()) val accountId = GroupUtil.doubleDecodeGroupId(address.serialize())
volatile.eraseLegacyClosedGroup(accountId) configs.convoInfoVolatile.eraseLegacyClosedGroup(accountId)
groups.eraseLegacyGroup(accountId) configs.userGroups.eraseLegacyGroup(accountId)
} else if (address.isCommunity) { } else if (address.isCommunity) {
// these should be removed in the group leave / handling new configs // 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") Log.w("Loki", "Thread delete called for open group address, expecting to be handled elsewhere")
@ -213,19 +194,19 @@ open class Storage(
} }
} else { } else {
// non-standard contact prefixes: 15, 00 etc shouldn't be stored in config // non-standard contact prefixes: 15, 00 etc shouldn't be stored in config
if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return if (AccountId(address.serialize()).prefix != IdPrefix.STANDARD) return@withMutableUserConfigs
volatile.eraseOneToOne(address.serialize()) configs.convoInfoVolatile.eraseOneToOne(address.serialize())
if (getUserPublicKey() != address.serialize()) { if (getUserPublicKey() != address.serialize()) {
val contacts = configFactory.contacts ?: return configs.contacts.upsertContact(address.serialize()) {
contacts.upsertContact(address.serialize()) {
priority = PRIORITY_HIDDEN priority = PRIORITY_HIDDEN
} }
} else { } else {
val userProfile = configFactory.user ?: return configs.userProfile.setNtsPriority(PRIORITY_HIDDEN)
userProfile.setNtsPriority(PRIORITY_HIDDEN)
} }
} }
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
Unit
}
} }
override fun getUserPublicKey(): String? { override fun getUserPublicKey(): String? {
@ -347,22 +328,23 @@ open class Storage(
// don't process configs for inbox recipients // don't process configs for inbox recipients
if (recipient.isOpenGroupInboxRecipient) return if (recipient.isOpenGroupInboxRecipient) return
configFactory.convoVolatile?.let { config -> configFactory.withMutableUserConfigs { configs ->
val config = configs.convoInfoVolatile
val convo = when { val convo = when {
// recipient closed group // recipient closed group
recipient.isLegacyClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize())) recipient.isLegacyClosedGroupRecipient -> config.getOrConstructLegacyGroup(GroupUtil.doubleDecodeGroupId(recipient.address.serialize()))
recipient.isClosedGroupV2Recipient -> config.getOrConstructClosedGroup(recipient.address.serialize()) recipient.isClosedGroupV2Recipient -> config.getOrConstructClosedGroup(recipient.address.serialize())
// recipient is open group // recipient is open group
recipient.isCommunityRecipient -> { recipient.isCommunityRecipient -> {
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return@withMutableUserConfigs
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) -> BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
config.getOrConstructCommunity(base, room, pubKey) config.getOrConstructCommunity(base, room, pubKey)
} ?: return } ?: return@withMutableUserConfigs
} }
// otherwise recipient is one to one // otherwise recipient is one to one
recipient.isContactRecipient -> { recipient.isContactRecipient -> {
// don't process non-standard account IDs though // 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()) config.getOrConstructOneToOne(recipient.address.serialize())
} }
else -> throw NullPointerException("Weren't expecting to have a convo with address ${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() notifyConversationListListeners()
} }
config.set(convo) config.set(convo)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
} }
} }
} }
@ -522,12 +503,6 @@ open class Storage(
return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room, imageId) 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) { override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) {
val job = DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) ?: return val job = DatabaseComponent.get(context).sessionJobDatabase().getMessageSendJob(messageSendJobID) ?: return
JobQueue.shared.resumePendingSendMessage(job) JobQueue.shared.resumePendingSendMessage(job)
@ -547,10 +522,6 @@ open class Storage(
return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id) 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 { override fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean {
return configFactory.conversationInConfig(publicKey, groupPublicKey, openGroupId, visibleOnly) return configFactory.conversationInConfig(publicKey, groupPublicKey, openGroupId, visibleOnly)
} }
@ -560,22 +531,36 @@ open class Storage(
} }
override fun isCheckingCommunityRequests(): Boolean { override fun isCheckingCommunityRequests(): Boolean {
return configFactory.user?.getCommunityMessageRequests() == true return configFactory.withUserConfigs { it.userProfile.getCommunityMessageRequests() }
} }
private fun notifyUpdates(forConfigObject: Config, messageTimestamp: Long) { private fun observeConfigUpdates() {
when (forConfigObject) { GlobalScope.launch {
is UserProfile -> updateUser(forConfigObject, messageTimestamp) configFactory.configUpdateNotifications
is Contacts -> updateContacts(forConfigObject, messageTimestamp) .collect { change ->
is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject, messageTimestamp) when (change) {
is UserGroupsConfig -> updateUserGroups(forConfigObject, messageTimestamp) is ConfigUpdateNotification.GroupConfigsDeleted -> {}
is GroupInfoConfig -> updateGroupInfo(forConfigObject, messageTimestamp) is ConfigUpdateNotification.GroupConfigsUpdated -> {
is GroupKeysConfig -> updateGroupKeys(forConfigObject) configFactory.withGroupConfigs(change.groupId) {
is GroupMembersConfig -> updateGroupMembers(forConfigObject) 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 val userPublicKey = getUserPublicKey() ?: return
// would love to get rid of recipient and context from this // would love to get rid of recipient and context from this
val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) val recipient = Recipient.from(context, fromSerialized(userPublicKey), false)
@ -588,7 +573,6 @@ open class Storage(
name.takeUnless { it.isEmpty() }?.truncate(NAME_PADDED_LENGTH)?.let { name.takeUnless { it.isEmpty() }?.truncate(NAME_PADDED_LENGTH)?.let {
TextSecurePreferences.setProfileName(context, it) TextSecurePreferences.setProfileName(context, it)
profileManager.setName(context, recipient, it) profileManager.setName(context, recipient, it)
if (it != name) userProfile.setName(it)
} }
// Update profile picture // 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 threadId = getThreadId(fromSerialized(groupInfoConfig.id().hexString)) ?: return
val recipient = getRecipientForThread(threadId) ?: return val recipient = getRecipientForThread(threadId) ?: return
val db = DatabaseComponent.get(context).recipientDatabase() val db = DatabaseComponent.get(context).recipientDatabase()
@ -635,18 +619,9 @@ open class Storage(
val mmsDb = DatabaseComponent.get(context).mmsDatabase() val mmsDb = DatabaseComponent.get(context).mmsDatabase()
mmsDb.deleteMessagesInThreadBeforeDate(threadId, removeAttachmentsBefore, onlyMedia = true) mmsDb.deleteMessagesInThreadBeforeDate(threadId, removeAttachmentsBefore, onlyMedia = true)
} }
// TODO: handle deleted group, handle delete attachment / message before a certain time
} }
private fun updateGroupKeys(groupKeys: GroupKeysConfig) { private fun updateContacts(contacts: ReadableContacts, messageTimestamp: Long) {
// TODO: update something here?
}
private fun updateGroupMembers(groupMembers: GroupMembersConfig) {
// TODO: maybe clear out some contacts or something?
}
private fun updateContacts(contacts: Contacts, messageTimestamp: Long) {
val extracted = contacts.all().toList() val extracted = contacts.all().toList()
addLibSessionContacts(extracted, messageTimestamp) addLibSessionContacts(extracted, messageTimestamp)
} }
@ -665,10 +640,12 @@ open class Storage(
TextSecurePreferences.setProfilePictureURL(context, null) TextSecurePreferences.setProfilePictureURL(context, null)
Recipient.removeCached(fromSerialized(userPublicKey)) 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() val extracted = convos.all().filterNotNull()
for (conversation in extracted) { for (conversation in extracted) {
val threadId = when (conversation) { 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 threadDb = DatabaseComponent.get(context).threadDatabase()
val localUserPublicKey = getUserPublicKey() ?: return Log.w( val localUserPublicKey = getUserPublicKey() ?: return Log.w(
"Loki", "Loki",
@ -1080,9 +1057,13 @@ open class Storage(
} }
override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair, expirationTimer: Int) { override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair, expirationTimer: Int) {
val volatiles = configFactory.convoVolatile ?: return configFactory.withMutableUserConfigs {
val userGroups = configFactory.userGroups ?: return val volatiles = it.convoInfoVolatile
if (volatiles.getLegacyClosedGroup(groupPublicKey) != null && userGroups.getLegacyGroupInfo(groupPublicKey) != null) return val userGroups = it.userGroups
if (volatiles.getLegacyClosedGroup(groupPublicKey) != null && userGroups.getLegacyGroupInfo(groupPublicKey) != null) {
return@withMutableUserConfigs
}
val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey) val groupVolatileConfig = volatiles.getOrConstructLegacyGroup(groupPublicKey)
groupVolatileConfig.lastRead = formationTimestamp groupVolatileConfig.lastRead = formationTimestamp
volatiles.set(groupVolatileConfig) volatiles.set(groupVolatileConfig)
@ -1098,7 +1079,7 @@ open class Storage(
) )
// shouldn't exist, don't use getOrConstruct + copy // shouldn't exist, don't use getOrConstruct + copy
userGroups.set(groupInfo) userGroups.set(groupInfo)
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) }
} }
override fun updateGroupConfig(groupPublicKey: String) { override fun updateGroupConfig(groupPublicKey: String) {
@ -1106,19 +1087,20 @@ open class Storage(
val groupAddress = fromSerialized(groupID) val groupAddress = fromSerialized(groupID)
val existingGroup = getGroup(groupID) val existingGroup = getGroup(groupID)
?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config") ?: return Log.w("Loki-DBG", "No existing group for ${groupPublicKey.take(4)}} when updating group config")
val userGroups = configFactory.userGroups ?: return configFactory.withMutableUserConfigs {
val userGroups = it.userGroups
if (!existingGroup.isActive) { if (!existingGroup.isActive) {
userGroups.eraseLegacyGroup(groupPublicKey) userGroups.eraseLegacyGroup(groupPublicKey)
return return@withMutableUserConfigs
} }
val name = existingGroup.title val name = existingGroup.title
val admins = existingGroup.admins.map { it.serialize() } val admins = existingGroup.admins.map { it.serialize() }
val members = existingGroup.members.map { it.serialize() } val members = existingGroup.members.map { it.serialize() }
val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members) val membersMap = GroupUtil.createConfigMemberMap(admins = admins, members = members)
val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey) val latestKeyPair = getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
?: return Log.w("Loki-DBG", "No latest closed group encryption key pair for ${groupPublicKey.take(4)}} when updating group config") ?: 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 threadID = getThreadId(groupAddress) ?: return@withMutableUserConfigs
val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy( val groupInfo = userGroups.getOrConstructLegacyGroupInfo(groupPublicKey).copy(
name = name, name = name,
members = membersMap, members = membersMap,
@ -1130,6 +1112,7 @@ open class Storage(
) )
userGroups.set(groupInfo) userGroups.set(groupInfo)
} }
}
override fun isGroupActive(groupPublicKey: String): Boolean { override fun isGroupActive(groupPublicKey: String): Boolean {
return DatabaseComponent.get(context).groupDatabase().getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true return DatabaseComponent.get(context).groupDatabase().getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true
@ -1242,18 +1225,20 @@ open class Storage(
* For new closed groups * For new closed groups
*/ */
override fun getMembers(groupPublicKey: String): List<LibSessionGroupMember> = override fun getMembers(groupPublicKey: String): List<LibSessionGroupMember> =
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 getClosedGroupDisplayInfo(groupSessionId: String): GroupDisplayInfo? {
val infoConfig = configFactory.getGroupInfoConfig(AccountId(groupSessionId)) ?: return null
val isAdmin = configFactory.userGroups?.getClosedGroup(groupSessionId)?.hasAdminKey() ?: return null
return infoConfig.use { info -> override fun getLibSessionClosedGroup(groupAccountId: String): GroupInfo.ClosedGroupInfo? {
return configFactory.withUserConfigs { it.userGroups.getClosedGroup(groupAccountId) }
}
override fun getClosedGroupDisplayInfo(groupAccountId: String): GroupDisplayInfo? {
val groupIsAdmin = getLibSessionClosedGroup(groupAccountId)?.hasAdminKey() ?: return null
return configFactory.withGroupConfigs(AccountId(groupAccountId)) { configs ->
val info = configs.groupInfo
GroupDisplayInfo( GroupDisplayInfo(
id = info.id(), id = info.id(),
name = info.getName(), name = info.getName(),
@ -1262,7 +1247,7 @@ open class Storage(
destroyed = false, destroyed = false,
created = info.getCreated(), created = info.getCreated(),
description = info.getDescription(), description = info.getDescription(),
isUserAdmin = isAdmin isUserAdmin = groupIsAdmin
) )
} }
} }
@ -1270,7 +1255,7 @@ open class Storage(
override fun insertGroupInfoChange(message: GroupUpdated, closedGroup: AccountId): Long? { override fun insertGroupInfoChange(message: GroupUpdated, closedGroup: AccountId): Long? {
val sentTimestamp = message.sentTimestamp ?: SnodeAPI.nowWithOffset val sentTimestamp = message.sentTimestamp ?: SnodeAPI.nowWithOffset
val senderPublicKey = message.sender 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 val updateData = UpdateMessageData.buildGroupUpdate(message, groupName) ?: return null
@ -1365,20 +1350,22 @@ open class Storage(
override fun onOpenGroupAdded(server: String, room: String) { override fun onOpenGroupAdded(server: String, room: String) {
OpenGroupManager.restartPollerForServer(server.removeSuffix("/")) OpenGroupManager.restartPollerForServer(server.removeSuffix("/"))
val groups = configFactory.userGroups ?: return configFactory.withMutableUserConfigs { configs ->
val volatileConfig = configFactory.convoVolatile ?: return val groups = configs.userGroups
val openGroup = getOpenGroup(room, server) ?: return val volatileConfig = configs.convoInfoVolatile
val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return val openGroup = getOpenGroup(room, server) ?: return@withMutableUserConfigs
val (infoServer, infoRoom, pubKey) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return@withMutableUserConfigs
val pubKeyHex = Hex.toStringCondensed(pubKey) val pubKeyHex = Hex.toStringCondensed(pubKey)
val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex) val communityInfo = groups.getOrConstructCommunityInfo(infoServer, infoRoom, pubKeyHex)
groups.set(communityInfo) groups.set(communityInfo)
val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey) val volatile = volatileConfig.getOrConstructCommunity(infoServer, infoRoom, pubKey)
if (volatile.lastRead != 0L) { if (volatile.lastRead != 0L) {
val threadId = getThreadId(openGroup) ?: return val threadId = getThreadId(openGroup) ?: return@withMutableUserConfigs
markConversationAsRead(threadId, volatile.lastRead, force = true) markConversationAsRead(threadId, volatile.lastRead, force = true)
} }
volatileConfig.set(volatile) volatileConfig.set(volatile)
} }
}
override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean { override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean {
val jobDb = DatabaseComponent.get(context).sessionJobDatabase() val jobDb = DatabaseComponent.get(context).sessionJobDatabase()
@ -1606,41 +1593,44 @@ open class Storage(
val threadDB = DatabaseComponent.get(context).threadDatabase() val threadDB = DatabaseComponent.get(context).threadDatabase()
threadDB.setPinned(threadID, isPinned) threadDB.setPinned(threadID, isPinned)
val threadRecipient = getRecipientForThread(threadID) ?: return val threadRecipient = getRecipientForThread(threadID) ?: return
configFactory.withMutableUserConfigs { configs ->
if (threadRecipient.isLocalNumber) { if (threadRecipient.isLocalNumber) {
val user = configFactory.user ?: return configs.userProfile.setNtsPriority(if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
user.setNtsPriority(if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
} else if (threadRecipient.isContactRecipient) { } else if (threadRecipient.isContactRecipient) {
val contacts = configFactory.contacts ?: return configs.contacts.upsertContact(threadRecipient.address.serialize()) {
contacts.upsertContact(threadRecipient.address.serialize()) {
priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE
} }
} else if (threadRecipient.isGroupRecipient) { } else if (threadRecipient.isGroupRecipient) {
val groups = configFactory.userGroups ?: return
when { when {
threadRecipient.isLegacyClosedGroupRecipient -> { threadRecipient.isLegacyClosedGroupRecipient -> {
threadRecipient.address.serialize() threadRecipient.address.serialize()
.let(GroupUtil::doubleDecodeGroupId) .let(GroupUtil::doubleDecodeGroupId)
.let(groups::getOrConstructLegacyGroupInfo) .let(configs.userGroups::getOrConstructLegacyGroupInfo)
.copy(priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE) .copy(priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
.let(groups::set) .let(configs.userGroups::set)
} }
threadRecipient.isClosedGroupV2Recipient -> { threadRecipient.isClosedGroupV2Recipient -> {
val newGroupInfo = groups.getOrConstructClosedGroup(threadRecipient.address.serialize()).copy ( val newGroupInfo = configs.userGroups
priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE .getOrConstructClosedGroup(threadRecipient.address.serialize())
) .copy(priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE)
groups.set(newGroupInfo) configs.userGroups.set(newGroupInfo)
} }
threadRecipient.isCommunityRecipient -> { threadRecipient.isCommunityRecipient -> {
val openGroup = getOpenGroup(threadID) ?: return val openGroup = getOpenGroup(threadID) ?: return@withMutableUserConfigs
val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL) ?: return val (baseUrl, room, pubKeyHex) = BaseCommunityInfo.parseFullUrl(openGroup.joinURL)
val newGroupInfo = groups.getOrConstructCommunityInfo(baseUrl, room, Hex.toStringCondensed(pubKeyHex)).copy ( ?: return@withMutableUserConfigs
priority = if (isPinned) PRIORITY_PINNED else PRIORITY_VISIBLE val newGroupInfo = configs.userGroups.getOrConstructCommunityInfo(
) baseUrl,
groups.set(newGroupInfo) 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 { override fun isPinned(threadID: Long): Boolean {
@ -1676,8 +1666,9 @@ open class Storage(
if (recipient.isContactRecipient || recipient.isCommunityRecipient) return if (recipient.isContactRecipient || recipient.isCommunityRecipient) return
// If we get here then this is a closed group conversation (i.e., recipient.isClosedGroupRecipient) // If we get here then this is a closed group conversation (i.e., recipient.isClosedGroupRecipient)
val volatile = configFactory.convoVolatile ?: return configFactory.withMutableUserConfigs { configs ->
val groups = configFactory.userGroups ?: return val volatile = configs.convoInfoVolatile
val groups = configs.userGroups
val groupID = recipient.address.toGroupString() val groupID = recipient.address.toGroupString()
val closedGroup = getGroup(groupID) val closedGroup = getGroup(groupID)
val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) val groupPublicKey = GroupUtil.doubleDecodeGroupId(recipient.address.serialize())
@ -1689,6 +1680,7 @@ open class Storage(
Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}") Log.w("Loki-DBG", "Failed to find a closed group for ${groupPublicKey.take(4)}")
} }
} }
}
override fun clearMessages(threadID: Long, fromUser: Address?): Boolean { override fun clearMessages(threadID: Long, fromUser: Address?): Boolean {
val smsDb = DatabaseComponent.get(context).smsDatabase() val smsDb = DatabaseComponent.get(context).smsDatabase()
@ -1833,10 +1825,13 @@ open class Storage(
setRecipientApprovedMe(sender, true) setRecipientApprovedMe(sender, true)
// Also update the config about this contact // Also update the config about this contact
configFactory.contacts?.upsertContact(sender.address.serialize()) { configFactory.withMutableUserConfigs {
it.contacts.upsertContact(sender.address.serialize()) {
approved = true approved = true
approvedMe = true approvedMe = true
} }
}
val message = IncomingMediaMessage( val message = IncomingMediaMessage(
sender.address, sender.address,
response.sentTimestamp!!, response.sentTimestamp!!,
@ -1894,18 +1889,22 @@ open class Storage(
override fun setRecipientApproved(recipient: Recipient, approved: Boolean) { override fun setRecipientApproved(recipient: Recipient, approved: Boolean) {
DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved) DatabaseComponent.get(context).recipientDatabase().setApproved(recipient, approved)
if (recipient.isLocalNumber || !recipient.isContactRecipient) return if (recipient.isLocalNumber || !recipient.isContactRecipient) return
configFactory.contacts?.upsertContact(recipient.address.serialize()) { configFactory.withMutableUserConfigs {
it.contacts.upsertContact(recipient.address.serialize()) {
this.approved = approved this.approved = approved
} }
} }
}
override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) { override fun setRecipientApprovedMe(recipient: Recipient, approvedMe: Boolean) {
DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe) DatabaseComponent.get(context).recipientDatabase().setApprovedMe(recipient, approvedMe)
if (recipient.isLocalNumber || !recipient.isContactRecipient) return if (recipient.isLocalNumber || !recipient.isContactRecipient) return
configFactory.contacts?.upsertContact(recipient.address.serialize()) { configFactory.withMutableUserConfigs {
it.contacts.upsertContact(recipient.address.serialize()) {
this.approvedMe = approvedMe this.approvedMe = approvedMe
} }
} }
}
override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) { override fun insertCallMessage(senderPublicKey: String, callMessageType: CallMessageType, sentTimestamp: Long) {
val database = DatabaseComponent.get(context).smsDatabase() val database = DatabaseComponent.get(context).smsDatabase()
@ -2040,14 +2039,12 @@ open class Storage(
override fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean) { override fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase() val recipientDb = DatabaseComponent.get(context).recipientDatabase()
recipientDb.setBlocked(recipients, isBlocked) recipientDb.setBlocked(recipients, isBlocked)
configFactory.withMutableUserConfigs { configs ->
recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient -> recipients.filter { it.isContactRecipient && !it.isLocalNumber }.forEach { recipient ->
configFactory.contacts?.upsertContact(recipient.address.serialize()) { configs.contacts.upsertContact(recipient.address.serialize()) {
this.blocked = isBlocked this.blocked = isBlocked
} }
} }
val contactsConfig = configFactory.contacts ?: return
if (contactsConfig.needsPush() && !fromConfigUpdate) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
} }
} }
@ -2060,21 +2057,23 @@ open class Storage(
val recipient = getRecipientForThread(threadId) ?: return null val recipient = getRecipientForThread(threadId) ?: return null
val dbExpirationMetadata = DatabaseComponent.get(context).expirationConfigurationDatabase().getExpirationConfiguration(threadId) val dbExpirationMetadata = DatabaseComponent.get(context).expirationConfigurationDatabase().getExpirationConfiguration(threadId)
return when { return when {
recipient.isLocalNumber -> configFactory.user?.getNtsExpiry() recipient.isLocalNumber -> configFactory.withUserConfigs { it.userProfile.getNtsExpiry() }
recipient.isContactRecipient -> { recipient.isContactRecipient -> {
// read it from contacts config if exists // read it from contacts config if exists
recipient.address.serialize().takeIf { it.startsWith(IdPrefix.STANDARD.value) } 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 -> { 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) if (it == 0L) ExpiryMode.NONE else ExpiryMode.AfterSend(it)
} }
} }
recipient.isLegacyClosedGroupRecipient -> { recipient.isLegacyClosedGroupRecipient -> {
// read it from group config if exists // read it from group config if exists
GroupUtil.doubleDecodeGroupId(recipient.address.serialize()) 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 } ?.run { disappearingTimer.takeIf { it != 0L }?.let(ExpiryMode::AfterSend) ?: ExpiryMode.NONE }
} }
else -> null else -> null
@ -2100,24 +2099,28 @@ open class Storage(
} }
if (recipient.isLegacyClosedGroupRecipient) { if (recipient.isLegacyClosedGroupRecipient) {
val userGroups = configFactory.userGroups ?: return
val groupPublicKey = GroupUtil.addressToGroupAccountId(recipient.address) val groupPublicKey = GroupUtil.addressToGroupAccountId(recipient.address)
val groupInfo = userGroups.getLegacyGroupInfo(groupPublicKey)
?.copy(disappearingTimer = expiryMode.expirySeconds) ?: return configFactory.withMutableUserConfigs {
userGroups.set(groupInfo) val groupInfo = it.userGroups.getLegacyGroupInfo(groupPublicKey)
?.copy(disappearingTimer = expiryMode.expirySeconds) ?: return@withMutableUserConfigs
it.userGroups.set(groupInfo)
}
} else if (recipient.isClosedGroupV2Recipient) { } else if (recipient.isClosedGroupV2Recipient) {
val groupSessionId = AccountId(recipient.address.serialize()) val groupSessionId = AccountId(recipient.address.serialize())
val groupInfo = configFactory.getGroupInfoConfig(groupSessionId) ?: return configFactory.withMutableGroupConfigs(groupSessionId) { configs ->
groupInfo.setExpiryTimer(expiryMode.expirySeconds) configs.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
val contact = contacts.get(recipient.address.serialize())?.copy(expiryMode = expiryMode) ?: return } else if (recipient.isLocalNumber) {
contacts.set(contact) 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( expirationDb.setExpirationConfiguration(
config.run { copy(expiryMode = expiryMode) } config.run { copy(expiryMode = expiryMode) }

View File

@ -1,25 +1,17 @@
package org.thoughtcrime.securesms.debugmenu package org.thoughtcrime.securesms.debugmenu
import android.app.Application import android.app.Application
import android.widget.Toast
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import org.session.libsession.utilities.Environment
import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext 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 import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -75,11 +67,6 @@ class DebugMenuViewModel @Inject constructor(
// clear remote and local data, then restart the app // clear remote and local data, then restart the app
viewModelScope.launch { 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 -> ApplicationContext.getInstance(application).clearAllData().let { success ->
if(success){ if(success){
// save the environment // save the environment

View File

@ -1,40 +1,57 @@
package org.thoughtcrime.securesms.dependencies package org.thoughtcrime.securesms.dependencies
import android.content.Context import android.content.Context
import android.os.Trace
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow 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.ConfigBase
import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.GroupInfoConfig import network.loki.messenger.libsession_util.GroupInfoConfig
import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.GroupKeysConfig
import network.loki.messenger.libsession_util.GroupMembersConfig 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.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile 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 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.SnodeAPI
import org.session.libsession.snode.SwarmAuth
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ConfigFactoryUpdateListener import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.GroupConfigs
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.utilities.IdPrefix import org.session.libsession.utilities.MutableGroupConfigs
import org.session.libsignal.utilities.Log 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.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.ConfigDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import java.util.concurrent.ConcurrentHashMap
class ConfigFactory( class ConfigFactory(
private val context: Context, private val context: Context,
private val configDatabase: ConfigDatabase, private val configDatabase: ConfigDatabase,
/** <ed25519 secret key,33 byte prefixed public key (hex encoded)> */ private val threadDb: ThreadDatabase,
private val maybeGetUserInfo: () -> Pair<ByteArray, String>? private val storage: StorageProtocol,
) : ) : ConfigFactoryProtocol {
ConfigFactoryProtocol {
companion object { companion object {
// This is a buffer period within which we will process messages which would result in a // 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 // config change, any message which would normally result in a config change which was sent
@ -43,352 +60,301 @@ class ConfigFactory(
const val configChangeBufferPeriod: Long = (2 * 60 * 1000) const val configChangeBufferPeriod: Long = (2 * 60 * 1000)
} }
fun keyPairChanged() { // this should only happen restoring or clearing datac init {
_userConfig?.free() System.loadLibrary("session_util")
_contacts?.free()
_convoVolatileConfig?.free()
_userConfig = null
_contacts = null
_convoVolatileConfig = null
} }
private val userLock = Object() private class UserConfigsImpl(
private var _userConfig: UserProfile? = null userEd25519SecKey: ByteArray,
private val contactsLock = Object() private val userAccountId: AccountId,
private var _contacts: Contacts? = null private val configDatabase: ConfigDatabase,
private val convoVolatileLock = Object() storage: StorageProtocol,
private var _convoVolatileConfig: ConversationVolatileConfig? = null threadDb: ThreadDatabase,
private val userGroupsLock = Object() contactsDump: ByteArray? = configDatabase.retrieveConfigAndHashes(
private var _userGroups: UserGroupsConfig? = null 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<ConfigFactoryUpdateListener> = mutableListOf() init {
if (contactsDump == null) {
contacts.initFrom(storage)
}
private val _configUpdateNotifications = MutableSharedFlow<Unit>( 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<AccountId, UserConfigsImpl>()
private val groupConfigs = ConcurrentHashMap<AccountId, GroupConfigsImpl>()
private val _configUpdateNotifications = MutableSharedFlow<ConfigUpdateNotification>(
extraBufferCapacity = 1, extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
override val configUpdateNotifications get() = _configUpdateNotifications override val configUpdateNotifications get() = _configUpdateNotifications
fun registerListener(listener: ConfigFactoryUpdateListener) { private fun requiresCurrentUserAccountId(): AccountId =
listeners += listener AccountId(requireNotNull(storage.getUserPublicKey()) {
"No logged in user"
})
private fun requiresCurrentUserED25519SecKey(): ByteArray =
requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) {
"No logged in user"
} }
fun unregisterListener(listener: ConfigFactoryUpdateListener) { override fun <T> withUserConfigs(cb: (UserConfigs) -> T): T {
listeners -= listener val userAccountId = requiresCurrentUserAccountId()
} val configs = userConfigs.getOrPut(userAccountId) {
UserConfigsImpl(
private inline fun <T> synchronizedWithLog(lock: Any, body: () -> T): T { requiresCurrentUserED25519SecKey(),
Trace.beginSection("synchronizedWithLog") userAccountId,
val result = synchronized(lock) { threadDb = threadDb,
body() configDatabase = configDatabase,
} storage = storage
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)
}
}
_userConfig
}
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)
}
}
_contacts
}
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()
}
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( return synchronized(configs) {
groupSessionId: AccountId, cb(configs)
info: GroupInfoConfig, }
members: GroupMembersConfig }
): GroupKeysConfig? = getGroupInfo(groupSessionId)?.let { groupInfo ->
val (userSk, _) = maybeGetUserInfo() ?: return null override fun <T> withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T {
GroupKeysConfig.newInstance( return withUserConfigs { configs ->
userSk, val result = cb(configs as UserConfigsImpl)
groupSessionId.pubKeyBytes,
groupInfo.adminKey, if (configs.persistIfDirty()) {
info = info, _configUpdateNotifications.tryEmit(ConfigUpdateNotification.UserConfigs)
members = members }
result
}
}
override fun <T> 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 fun userSessionId(): AccountId? { return synchronized(configs) {
return maybeGetUserInfo()?.second?.let(::AccountId) cb(configs)
}
} }
override fun maybeDecryptForUser(encoded: ByteArray, domain: String, closedGroupSessionId: AccountId): ByteArray? { override fun <T> withMutableGroupConfigs(
val secret = maybeGetUserInfo()?.first ?: run { groupId: AccountId,
Log.e("ConfigFactory", "No user ed25519 secret key decrypting a message for us") cb: (MutableGroupConfigs) -> T
return null ): T {
return withGroupConfigs(groupId) { configs ->
val result = cb(configs as GroupConfigsImpl)
if (configs.persistIfDirty()) {
_configUpdateNotifications.tryEmit(ConfigUpdateNotification.GroupConfigsUpdated(groupId))
} }
result
}
}
override fun removeGroup(groupId: AccountId) {
withMutableUserConfigs {
it.userGroups.eraseClosedGroup(groupId.hexString)
}
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( return Sodium.decryptForMultipleSimple(
encoded = encoded, encoded = encoded,
ed25519SecretKey = secret, ed25519SecretKey = requireNotNull(storage.getUserED25519KeyPair()?.secretKey?.asBytes) {
"No logged in user"
},
domain = domain, domain = domain,
senderPubKey = Sodium.ed25519PkToCurve25519(closedGroupSessionId.pubKeyBytes) senderPubKey = Sodium.ed25519PkToCurve25519(closedGroupSessionId.pubKeyBytes)
) )
} }
override fun getUserConfigs(): List<ConfigBase> =
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( override fun conversationInConfig(
publicKey: String?, publicKey: String?,
groupPublicKey: String?, groupPublicKey: String?,
openGroupId: String?, openGroupId: String?,
visibleOnly: Boolean visibleOnly: Boolean
): Boolean { ): Boolean {
val (_, userPublicKey) = maybeGetUserInfo() ?: return true val userPublicKey = storage.getUserPublicKey() ?: return false
if (openGroupId != null) { if (openGroupId != null) {
val userGroups = userGroups ?: return false
val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context) val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context)
val openGroup = val openGroup =
get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false
// Not handling the `hidden` behaviour for communities so just indicate the existence // 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) { } else if (groupPublicKey != null) {
val userGroups = userGroups ?: return false
// Not handling the `hidden` behaviour for legacy groups so just indicate the existence // Not handling the `hidden` behaviour for legacy groups so just indicate the existence
return if (groupPublicKey.startsWith(IdPrefix.GROUP.value)) { return withUserConfigs {
userGroups.getClosedGroup(groupPublicKey) != null if (groupPublicKey.startsWith(IdPrefix.GROUP.value)) {
it.userGroups.getClosedGroup(groupPublicKey) != null
} else { } else {
userGroups.getLegacyGroupInfo(groupPublicKey) != null it.userGroups.getLegacyGroupInfo(groupPublicKey) != null
}
} }
} else if (publicKey == userPublicKey) { } else if (publicKey == userPublicKey) {
val user = user ?: return false return withUserConfigs {
!visibleOnly || it.userProfile.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN
return (!visibleOnly || user.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)
} }
} else if (publicKey != null) {
return withUserConfigs {
(!visibleOnly || it.contacts.get(publicKey)?.priority != ConfigBase.PRIORITY_HIDDEN)
}
} else {
return false return false
} }
}
override fun canPerformChange( override fun canPerformChange(
variant: String, variant: String,
@ -402,32 +368,192 @@ class ConfigFactory(
return (changeTimestampMs >= (lastUpdateTimestampMs - configChangeBufferPeriod)) return (changeTimestampMs >= (lastUpdateTimestampMs - configChangeBufferPeriod))
} }
override fun saveGroupConfigs( override fun getGroupAuth(groupId: AccountId): SwarmAuth? {
groupKeys: GroupKeysConfig, val (adminKey, authData) = withUserConfigs {
groupInfo: GroupInfoConfig, val group = it.userGroups.getClosedGroup(groupId.hexString)
groupMembers: GroupMembersConfig group?.adminKey to group?.authData
) {
val pubKey = groupInfo.id().hexString
val timestamp = SnodeAPI.nowWithOffset
// 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)
} }
override fun removeGroup(closedGroupId: AccountId) { return if (adminKey != null) {
val groups = userGroups ?: return OwnedSwarmAuth.ofClosedGroup(groupId, adminKey)
groups.eraseClosedGroup(closedGroupId.hexString) } else if (authData != null) {
persist(groups, SnodeAPI.nowWithOffset) GroupSubAccountSwarmAuth(groupId, this, authData)
configDatabase.deleteGroupConfigs(closedGroupId) } else {
null
}
} }
override fun scheduleUpdate(destination: Destination) { fun clearAll() {
// there's probably a better way to do this //TODO: clear all configsr
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<String, String> {
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<String, String> {
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)
} }
} }

View File

@ -3,9 +3,8 @@ package org.thoughtcrime.securesms.dependencies
import dagger.Lazy import dagger.Lazy
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.plus
import network.loki.messenger.libsession_util.util.GroupInfo 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.groups.GroupManagerV2
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
@ -16,23 +15,29 @@ class PollerFactory(
private val executor: CoroutineDispatcher, private val executor: CoroutineDispatcher,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
private val groupManagerV2: Lazy<GroupManagerV2>, private val groupManagerV2: Lazy<GroupManagerV2>,
private val storage: StorageProtocol,
) { ) {
private val pollers = ConcurrentHashMap<AccountId, ClosedGroupPoller>() private val pollers = ConcurrentHashMap<AccountId, ClosedGroupPoller>()
fun pollerFor(sessionId: AccountId): ClosedGroupPoller? { fun pollerFor(sessionId: AccountId): ClosedGroupPoller? {
// Check if the group is currently in our config and approved, don't start if it isn't // 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) { return pollers.getOrPut(sessionId) {
ClosedGroupPoller(scope + SupervisorJob(), executor, sessionId, configFactory, groupManagerV2.get()) ClosedGroupPoller(scope, executor, sessionId, configFactory, groupManagerV2.get(), storage)
} }
} }
fun startAll() { fun startAll() {
configFactory.userGroups?.allClosedGroupInfo()?.filterNot(GroupInfo.ClosedGroupInfo::invited)?.forEach { configFactory
pollerFor(it.groupAccountId)?.start() .withUserConfigs { it.userGroups.allClosedGroupInfo() }
} .filterNot(GroupInfo.ClosedGroupInfo::invited)
.forEach { pollerFor(it.groupAccountId)?.start() }
} }
fun stopAll() { fun stopAll() {
@ -42,7 +47,8 @@ class PollerFactory(
} }
fun updatePollers() { 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 } } val toRemove = pollers.filter { (id, _) -> id !in currentGroups.map { it.groupAccountId } }
toRemove.forEach { (id, _) -> toRemove.forEach { (id, _) ->
pollers.remove(id)?.stop() pollers.remove(id)?.stop()

View File

@ -12,40 +12,32 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2 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.ConfigDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import javax.inject.Named import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton
@Suppress("OPT_IN_USAGE")
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object SessionUtilModule { object SessionUtilModule {
const val POLLER_SCOPE = "poller_coroutine_scope" private const val POLLER_SCOPE = "poller_coroutine_scope"
private fun maybeUserEdSecretKey(context: Context): ByteArray? {
val edKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return null
return edKey.secretKey.asBytes
}
@Provides @Provides
@Singleton @Singleton
fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory = fun provideConfigFactory(
ConfigFactory(context, configDatabase) { @ApplicationContext context: Context,
val localUserPublicKey = TextSecurePreferences.getLocalNumber(context) configDatabase: ConfigDatabase,
val secretKey = maybeUserEdSecretKey(context) storageProtocol: StorageProtocol,
if (localUserPublicKey == null || secretKey == null) null threadDatabase: ThreadDatabase,
else secretKey to localUserPublicKey ): ConfigFactory = ConfigFactory(context, configDatabase, threadDatabase, storageProtocol)
}.apply {
registerListener(context as ConfigFactoryUpdateListener)
}
@Provides @Provides
@Named(POLLER_SCOPE) @Named(POLLER_SCOPE)
fun providePollerScope(@ApplicationContext applicationContext: Context): CoroutineScope = GlobalScope fun providePollerScope(): CoroutineScope = GlobalScope
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Provides @Provides
@ -57,6 +49,12 @@ object SessionUtilModule {
fun providePollerFactory(@Named(POLLER_SCOPE) coroutineScope: CoroutineScope, fun providePollerFactory(@Named(POLLER_SCOPE) coroutineScope: CoroutineScope,
@Named(POLLER_SCOPE) dispatcher: CoroutineDispatcher, @Named(POLLER_SCOPE) dispatcher: CoroutineDispatcher,
configFactory: ConfigFactory, configFactory: ConfigFactory,
groupManagerV2: Lazy<GroupManagerV2>) = PollerFactory(coroutineScope, dispatcher, configFactory, groupManagerV2) storage: StorageProtocol,
groupManagerV2: Lazy<GroupManagerV2>) = PollerFactory(
scope = coroutineScope,
executor = dispatcher,
configFactory = configFactory,
groupManagerV2 = groupManagerV2,
storage = storage
)
} }

View File

@ -34,12 +34,15 @@ object ClosedGroupManager {
} }
fun ConfigFactory.updateLegacyGroup(group: GroupRecord) { fun ConfigFactory.updateLegacyGroup(group: GroupRecord) {
val groups = userGroups ?: return
if (!group.isLegacyClosedGroup) return if (!group.isLegacyClosedGroup) return
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val threadId = storage.getThreadId(group.encodedId) ?: return val threadId = storage.getThreadId(group.encodedId) ?: return
val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId()) val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
withMutableUserConfigs {
val groups = it.userGroups
val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey) val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey)
val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize)) val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize))
val toSet = legacyInfo.copy( val toSet = legacyInfo.copy(
@ -51,5 +54,5 @@ object ClosedGroupManager {
) )
groups.set(toSet) groups.set(toSet)
} }
}
} }

View File

@ -21,7 +21,6 @@ import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.jobs.InviteContactsJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory

View File

@ -85,8 +85,7 @@ class GroupManagerV2Impl @Inject constructor(
*/ */
private fun requireAdminAccess(group: AccountId): ByteArray { private fun requireAdminAccess(group: AccountId): ByteArray {
return checkNotNull(configFactory return checkNotNull(configFactory
.userGroups .withUserConfigs { it.userGroups.getClosedGroup(group.hexString) }
?.getClosedGroup(group.hexString)
?.adminKey ?.adminKey
?.takeIf { it.isNotEmpty() }) { "Only admin is allowed to invite members" } ?.takeIf { it.isNotEmpty() }) { "Only admin is allowed to invite members" }
} }
@ -96,10 +95,6 @@ class GroupManagerV2Impl @Inject constructor(
groupDescription: String, groupDescription: String,
members: Set<Contact> members: Set<Contact>
): Recipient = withContext(dispatcher) { ): 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 = val ourAccountId =
requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" } requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" }
val ourKeys = val ourKeys =
@ -109,25 +104,23 @@ class GroupManagerV2Impl @Inject constructor(
val groupCreationTimestamp = SnodeAPI.nowWithOffset val groupCreationTimestamp = SnodeAPI.nowWithOffset
// Create a group in the user groups config // 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." } val adminKey = checkNotNull(group.adminKey) { "Admin key is null for new group creation." }
userGroupsConfig.set(group)
val groupId = group.groupAccountId val groupId = group.groupAccountId
val groupAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey) val groupAuth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey)
try { try {
withNewGroupConfigs( configFactory.withMutableGroupConfigs(groupId) { configs ->
groupId = groupId,
userSecretKey = ourKeys.secretKey.asBytes,
groupAdminKey = adminKey
) { infoConfig, membersConfig, keysConfig ->
// Update group's information // Update group's information
infoConfig.setName(groupName) configs.groupInfo.setName(groupName)
infoConfig.setDescription(groupDescription) configs.groupInfo.setDescription(groupDescription)
// Add members // Add members
for (member in members) { for (member in members) {
membersConfig.set( configs.groupMembers.set(
GroupMember( GroupMember(
sessionId = member.accountID, sessionId = member.accountID,
name = member.name, name = member.name,
@ -138,7 +131,7 @@ class GroupManagerV2Impl @Inject constructor(
} }
// Add ourselves as admin // Add ourselves as admin
membersConfig.set( configs.groupMembers.set(
GroupMember( GroupMember(
sessionId = ourAccountId, sessionId = ourAccountId,
name = ourProfile.displayName, name = ourProfile.displayName,
@ -148,94 +141,18 @@ class GroupManagerV2Impl @Inject constructor(
) )
// Manually re-key to prevent issue with linked admin devices // 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 { configFactory.withMutableUserConfigs {
SnodeAPI.sendBatchRequest( it.convoInfoVolatile.set(
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(
Conversation.ClosedGroup( Conversation.ClosedGroup(
groupId.hexString, groupId.hexString,
groupCreationTimestamp, groupCreationTimestamp,
false false
) )
) )
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application) }
val recipient = val recipient =
Recipient.from(application, Address.fromSerialized(groupId.hexString), false) Recipient.from(application, Address.fromSerialized(groupId.hexString), false)
@ -255,44 +172,17 @@ class GroupManagerV2Impl @Inject constructor(
) )
recipient recipient
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to create group", e) Log.e(TAG, "Failed to create group", e)
// Remove the group from the user groups config is sufficient as a "rollback" // 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 throw e
} }
} }
private suspend fun <T> 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( override suspend fun inviteMembers(
group: AccountId, group: AccountId,

View File

@ -116,9 +116,6 @@ class JoinCommunityFragment : Fragment() {
GroupManager.getOpenGroupThreadID(openGroupID, requireContext()) GroupManager.getOpenGroupThreadID(openGroupID, requireContext())
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(
requireContext()
)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val recipient = Recipient.from( val recipient = Recipient.from(
requireContext(), requireContext(),

View File

@ -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.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
object OpenGroupManager { object OpenGroupManager {
private val executorService = Executors.newScheduledThreadPool(4) private val executorService = Executors.newScheduledThreadPool(4)
@ -131,8 +130,10 @@ object OpenGroupManager {
pollers.remove(server) pollers.remove(server)
} }
} }
configFactory.userGroups?.eraseCommunity(server, room) configFactory.withMutableUserConfigs {
configFactory.convoVolatile?.eraseCommunity(server, room) it.userGroups.eraseCommunity(server, room)
it.convoInfoVolatile.eraseCommunity(server, room)
}
// Delete // Delete
storage.removeLastDeletionServerID(room, server) storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerID(room, server) storage.removeLastMessageServerID(room, server)
@ -142,7 +143,6 @@ object OpenGroupManager {
lokiThreadDB.removeOpenGroupChat(threadID) lokiThreadDB.removeOpenGroupChat(threadID)
storage.deleteConversation(threadID) // Must be invoked on a background thread storage.deleteConversation(threadID) // Must be invoked on a background thread
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
} }
catch (e: Exception) { catch (e: Exception) {
Log.e("Loki", "Failed to leave (delete) community", e) Log.e("Loki", "Failed to leave (delete) community", e)

View File

@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.getConversationUnread 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.isVisible = recipient.isGroupRecipient && isCurrentUserInGroup
binding.leaveTextView.setOnClickListener(this) 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.markAllAsReadTextView.setOnClickListener(this)
binding.pinTextView.isVisible = !thread.isPinned binding.pinTextView.isVisible = !thread.isPinned
binding.unpinTextView.isVisible = thread.isPinned binding.unpinTextView.isVisible = thread.isPinned

View File

@ -97,7 +97,7 @@ class ConversationView : LinearLayout {
val textSize = if (unreadCount < 1000) 12.0f else 10.0f val textSize = if (unreadCount < 1000) 12.0f else 10.0f
binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) 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.unreadMentionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup) binding.unreadMentionIndicator.isVisible = (thread.unreadMentionCount != 0 && thread.recipient.address.isGroup)
val senderDisplayName = getTitle(thread.recipient) val senderDisplayName = getTitle(thread.recipient)

View File

@ -294,9 +294,12 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
.request(Manifest.permission.POST_NOTIFICATIONS) .request(Manifest.permission.POST_NOTIFICATIONS)
.execute() .execute()
} }
configFactory.user
?.takeUnless { it.isBlockCommunityMessageRequestsSet() } configFactory.withMutableUserConfigs {
?.setCommunityMessageRequests(false) if (!it.userProfile.isBlockCommunityMessageRequestsSet()) {
it.userProfile.setCommunityMessageRequests(false)
}
}
} }
} }
@ -378,11 +381,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
updateLegacyConfigView() updateLegacyConfigView()
// Sync config changes if there are any
lifecycleScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
}
} }
override fun onPause() { override fun onPause() {

View File

@ -46,7 +46,7 @@ class HomeDiffUtil(
oldItem.isSent == newItem.isSent && oldItem.isSent == newItem.isSent &&
oldItem.isPending == newItem.isPending && oldItem.isPending == newItem.isPending &&
oldItem.lastSeen == newItem.lastSeen && 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) old.typingThreadIDs.contains(oldItem.threadId) == new.typingThreadIDs.contains(newItem.threadId)
) )
} }

View File

@ -105,9 +105,6 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
fun doDecline() { fun doDecline() {
viewModel.deleteMessageRequest(thread) viewModel.deleteMessageRequest(thread)
LoaderManager.getInstance(this).restartLoader(0, null, this) LoaderManager.getInstance(this).restartLoader(0, null, this)
lifecycleScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity)
}
} }
showSessionDialog { showSessionDialog {
@ -132,9 +129,6 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
fun doDeleteAllAndBlock() { fun doDeleteAllAndBlock() {
viewModel.clearAllMessageRequests(false) viewModel.clearAllMessageRequests(false)
LoaderManager.getInstance(this).restartLoader(0, null, this) LoaderManager.getInstance(this).restartLoader(0, null, this)
lifecycleScope.launch(Dispatchers.IO) {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@MessageRequestsActivity)
}
} }
showSessionDialog { showSessionDialog {

View File

@ -10,7 +10,6 @@ import com.goterl.lazysodium.utils.Key
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import network.loki.messenger.R 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.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters 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.MessageWrapper
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium 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.Bencode
import org.session.libsession.utilities.bencode.BencodeList import org.session.libsession.utilities.bencode.BencodeList
import org.session.libsession.utilities.bencode.BencodeString 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.protos.SignalServiceProtos.Envelope
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
@ -92,18 +87,16 @@ class PushReceiver @Inject constructor(
} }
private fun tryDecryptGroupMessage(groupId: AccountId, data: ByteArray): Envelope? { private fun tryDecryptGroupMessage(groupId: AccountId, data: ByteArray): Envelope? {
return configFactory.withGroupConfigsOrNull(groupId) { _, _, keys -> val (envelopBytes, sender) = checkNotNull(configFactory.withGroupConfigs(groupId) { it.groupKeys.decrypt(data) }) {
val (envelopBytes, sender) = checkNotNull(keys.decrypt(data)) {
"Failed to decrypt group message" "Failed to decrypt group message"
} }
Log.d(TAG, "Successfully decrypted group message from ${sender.hexString}") Log.d(TAG, "Successfully decrypted group message from ${sender.hexString}")
Envelope.parseFrom(envelopBytes) return Envelope.parseFrom(envelopBytes)
.toBuilder() .toBuilder()
.setSource(sender.hexString) .setSource(sender.hexString)
.build() .build()
} }
}
private fun onPush() { private fun onPush() {
Log.d(TAG, "Failed to decode data for message.") Log.d(TAG, "Failed to decode data for message.")

View File

@ -20,11 +20,9 @@ import network.loki.messenger.libsession_util.GroupKeysConfig
import network.loki.messenger.libsession_util.GroupMembersConfig import network.loki.messenger.libsession_util.GroupMembersConfig
import org.session.libsession.database.userAuth import org.session.libsession.database.userAuth
import org.session.libsession.messaging.notifications.TokenFetcher import org.session.libsession.messaging.notifications.TokenFetcher
import org.session.libsession.snode.GroupSubAccountSwarmAuth
import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.snode.OwnedSwarmAuth
import org.session.libsession.snode.SwarmAuth import org.session.libsession.snode.SwarmAuth
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.withGroupConfigsOrNull
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Namespace

View File

@ -43,9 +43,9 @@ class CreateAccountManager @Inject constructor(
prefs.setLocalNumber(userHexEncodedPublicKey) prefs.setLocalNumber(userHexEncodedPublicKey)
prefs.setRestorationTime(0) prefs.setRestorationTime(0)
// we'll rely on the config syncing in the homeActivity resume configFactory.withMutableUserConfigs {
configFactory.keyPairChanged() it.userProfile.setName(displayName)
configFactory.user?.setName(displayName) }
versionDataFetcher.startTimedVersionCheck() versionDataFetcher.startTimedVersionCheck()
} }

View File

@ -19,7 +19,6 @@ import javax.inject.Singleton
@Singleton @Singleton
class LoadAccountManager @Inject constructor( class LoadAccountManager @Inject constructor(
@dagger.hilt.android.qualifiers.ApplicationContext private val context: Context, @dagger.hilt.android.qualifiers.ApplicationContext private val context: Context,
private val configFactory: ConfigFactory,
private val prefs: TextSecurePreferences, private val prefs: TextSecurePreferences,
private val versionDataFetcher: VersionDataFetcher private val versionDataFetcher: VersionDataFetcher
) { ) {
@ -44,7 +43,6 @@ class LoadAccountManager @Inject constructor(
val keyPairGenerationResult = KeyPairUtilities.generate(seed) val keyPairGenerationResult = KeyPairUtilities.generate(seed)
val x25519KeyPair = keyPairGenerationResult.x25519KeyPair val x25519KeyPair = keyPairGenerationResult.x25519KeyPair
KeyPairUtilities.store(context, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) KeyPairUtilities.store(context, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair)
configFactory.keyPairChanged()
val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
val registrationID = org.session.libsignal.utilities.KeyHelper.generateRegistrationId(false) val registrationID = org.session.libsignal.utilities.KeyHelper.generateRegistrationId(false)
prefs.apply { prefs.apply {

View File

@ -49,9 +49,9 @@ internal class PickDisplayNameViewModel(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (loadFailed) { if (loadFailed) {
prefs.setProfileName(displayName) prefs.setProfileName(displayName)
// we'll rely on the config syncing in the homeActivity resume configFactory.withMutableUserConfigs {
configFactory.user?.setName(displayName) it.userProfile.setName(displayName)
}
_events.emit(Event.LoadAccountComplete) _events.emit(Event.LoadAccountComplete)
} else _events.emit(Event.CreateAccount(displayName)) } else _events.emit(Event.CreateAccount(displayName))
} }

View File

@ -8,7 +8,6 @@ import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -24,7 +23,6 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.createSessionDialog import org.thoughtcrime.securesms.createSessionDialog
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject import javax.inject.Inject
@ -124,15 +122,6 @@ class ClearAllDataDialog : DialogFragment() {
} }
private suspend fun performDeleteLocalDataOnlyStep() { 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 -> ApplicationContext.getInstance(context).clearAllDataAndRestart().let { success ->
withContext(Main) { withContext(Main) {
if (success) { if (success) {

View File

@ -38,22 +38,24 @@ class PrivacySettingsPreferenceFragment : CorrectedPreferenceFragment() {
findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!! findPreference<Preference>(TextSecurePreferences.CALL_NOTIFICATIONS_ENABLED)!!
.onPreferenceChangeListener = CallToggleListener(this) { setCall(it) } .onPreferenceChangeListener = CallToggleListener(this) { setCall(it) }
findPreference<PreferenceCategory>(getString(R.string.sessionMessageRequests))?.let { category -> findPreference<PreferenceCategory>(getString(R.string.sessionMessageRequests))?.let { category ->
when (val user = configFactory.user) { SwitchPreferenceCompat(requireContext()).apply {
null -> category.isVisible = false
else -> SwitchPreferenceCompat(requireContext()).apply {
key = TextSecurePreferences.ALLOW_MESSAGE_REQUESTS key = TextSecurePreferences.ALLOW_MESSAGE_REQUESTS
preferenceDataStore = object : PreferenceDataStore() { preferenceDataStore = object : PreferenceDataStore() {
override fun getBoolean(key: String?, defValue: Boolean): Boolean { override fun getBoolean(key: String?, defValue: Boolean): Boolean {
if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) { if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) {
return user.getCommunityMessageRequests() return configFactory.withMutableUserConfigs {
it.userProfile.getCommunityMessageRequests()
}
} }
return super.getBoolean(key, defValue) return super.getBoolean(key, defValue)
} }
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String?, value: Boolean) {
if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) { if (key == TextSecurePreferences.ALLOW_MESSAGE_REQUESTS) {
user.setCommunityMessageRequests(value) configFactory.withMutableUserConfigs {
it.userProfile.setCommunityMessageRequests(value)
}
return return
} }
super.putBoolean(key, value) super.putBoolean(key, value)
@ -63,7 +65,6 @@ class PrivacySettingsPreferenceFragment : CorrectedPreferenceFragment() {
summary = getString(R.string.messageRequestsCommunitiesDescription) summary = getString(R.string.messageRequestsCommunitiesDescription)
}.let(category::addPreference) }.let(category::addPreference)
} }
}
initializeVisibility() initializeVisibility()
} }

View File

@ -295,17 +295,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} else { } else {
// if we have a network connection then attempt to update the display name // if we have a network connection then attempt to update the display name
TextSecurePreferences.setProfileName(this, displayName) TextSecurePreferences.setProfileName(this, displayName)
val user = viewModel.getUser() viewModel.updateName(displayName)
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 binding.btnGroupNameDisplay.text = displayName
updateWasSuccessful = true updateWasSuccessful = true
} }
}
// Inform the user if we failed to update the display name // Inform the user if we failed to update the display name
if (!updateWasSuccessful) { if (!updateWasSuccessful) {

View File

@ -20,7 +20,6 @@ import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.UserPic import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.avatars.AvatarHelper import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ProfileKeyUtil import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.ProfilePictureUtilities 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.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.NetworkUtils import org.thoughtcrime.securesms.util.NetworkUtils
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -91,8 +89,6 @@ class SettingsViewModel @Inject constructor(
fun getTempFile() = tempFile fun getTempFile() = tempFile
fun getUser() = configFactory.user
fun onAvatarPicked(result: CropImageView.CropResult) { fun onAvatarPicked(result: CropImageView.CropResult) {
when { when {
result.isSuccessful -> { result.isSuccessful -> {
@ -181,7 +177,6 @@ class SettingsViewModel @Inject constructor(
ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, context) ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, context)
// If the online portion of the update succeeded then update the local state // If the online portion of the update succeeded then update the local state
val userConfig = configFactory.user
AvatarHelper.setAvatar( AvatarHelper.setAvatar(
context, context,
Address.fromSerialized(TextSecurePreferences.getLocalNumber(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 we have a URL and a profile key then set the user's profile picture
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) { if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
userConfig?.setPic(UserPic(url, profileKey)) configFactory.withMutableUserConfigs {
it.userProfile.setPic(UserPic(url, profileKey))
}
} }
// update dialog state // update dialog state
_avatarDialogState.value = AvatarDialogState.UserAvatar(userAddress) _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 } catch (e: Exception){ // If the sync failed then inform the user
Log.d(TAG, "Error syncing avatar: $e") Log.d(TAG, "Error syncing avatar: $e")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -230,6 +222,12 @@ class SettingsViewModel @Inject constructor(
} }
} }
fun updateName(displayName: String) {
configFactory.withMutableUserConfigs {
it.userProfile.setName(displayName)
}
}
sealed class AvatarDialogState() { sealed class AvatarDialogState() {
object NoAvatar : AvatarDialogState() object NoAvatar : AvatarDialogState()
data class UserAvatar(val address: Address) : AvatarDialogState() data class UserAvatar(val address: Address) : AvatarDialogState()

View File

@ -161,8 +161,9 @@ class DefaultConversationRepository @Inject constructor(
return false return false
} }
return configFactory.userGroups return configFactory.withUserConfigs {
?.getClosedGroup(recipient.address.serialize())?.kicked == true it.userGroups.getClosedGroup(recipient.address.serialize())?.kicked == true
}
} }
// This assumes that recipient.isContactRecipient is true // This assumes that recipient.isContactRecipient is true

View File

@ -13,7 +13,6 @@ import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -97,10 +96,11 @@ class ProfileManager @Inject constructor(
} }
override fun contactUpdatedInternal(contact: Contact): String? { override fun contactUpdatedInternal(contact: Contact): String? {
val contactConfig = configFactory.contacts ?: return null
if (contact.accountID == TextSecurePreferences.getLocalNumber(context)) return null if (contact.accountID == TextSecurePreferences.getLocalNumber(context)) return null
val accountId = AccountId(contact.accountID) val accountId = AccountId(contact.accountID)
if (accountId.prefix != IdPrefix.STANDARD) return null // only internally store standard account IDs if (accountId.prefix != IdPrefix.STANDARD) return null // only internally store standard account IDs
return configFactory.withMutableUserConfigs {
val contactConfig = it.contacts
contactConfig.upsertContact(contact.accountID) { contactConfig.upsertContact(contact.accountID) {
this.name = contact.name.orEmpty() this.name = contact.name.orEmpty()
this.nickname = contact.nickname.orEmpty() this.nickname = contact.nickname.orEmpty()
@ -112,10 +112,8 @@ class ProfileManager @Inject constructor(
this.profilePicture = UserPic.DEFAULT this.profilePicture = UserPic.DEFAULT
} }
} }
if (contactConfig.needsPush()) { contactConfig.get(contact.accountID)?.hashCode()?.toString()
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
} }
return contactConfig.get(contact.accountID)?.hashCode()?.toString()
} }
} }

View File

@ -1,254 +1,10 @@
package org.thoughtcrime.securesms.util 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.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.GroupDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import java.util.Timer
import java.util.concurrent.ConcurrentLinkedDeque
object ConfigurationMessageUtilities { object ConfigurationMessageUtilities {
private const val TAG = "ConfigMessageUtils"
private val debouncer = WindowDebouncer(3000, Timer())
private val destinationUpdater = Any()
private val pendingDestinations = ConcurrentLinkedDeque<Destination>()
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 @JvmField
val DELETE_INACTIVE_GROUPS: String = """ 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}%'); 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}%');

View File

@ -1,12 +1,13 @@
package org.thoughtcrime.securesms.util package org.thoughtcrime.securesms.util
import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
fun ConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Boolean { fun ReadableConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Boolean {
val recipient = thread.recipient val recipient = thread.recipient
if (recipient.isContactRecipient if (recipient.isContactRecipient
&& recipient.isOpenGroupInboxRecipient && recipient.isOpenGroupInboxRecipient

View File

@ -34,7 +34,9 @@ set(SOURCES
util.cpp util.cpp
group_members.cpp group_members.cpp
group_keys.cpp group_keys.cpp
group_info.cpp) group_info.cpp
config_common.cpp
)
add_library( # Sets the name of the library. add_library( # Sets the name of the library.
session_util session_util

View File

@ -0,0 +1,39 @@
#include <jni.h>
#include "util.h"
#include "jni_utils.h"
#include <session/config/contacts.hpp>
#include <session/config/user_groups.hpp>
#include <session/config/user_profile.hpp>
#include <session/config/convo_info_volatile.hpp>
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<jlong>(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<jlong>(new session::config::Contacts(secret_key, initial));
} else if (config_name == "UserProfile") {
return reinterpret_cast<jlong>(new session::config::UserProfile(secret_key, initial));
} else if (config_name == "UserGroups") {
return reinterpret_cast<jlong>(new session::config::UserGroups(secret_key, initial));
} else if (config_name == "ConversationVolatileConfig") {
return reinterpret_cast<jlong>(new session::config::ConvoInfoVolatile(secret_key, initial));
} else {
throw std::invalid_argument("Unknown config name: " + config_name);
}
});
}

View File

@ -62,46 +62,7 @@ Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject
return result; 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<jobject>(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, "<init>", "(J)V");
jobject newConfig = env->NewObject(contactsClass, constructor,
reinterpret_cast<jlong>(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<jobject>(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, "<init>", "(J)V");
jobject newConfig = env->NewObject(contactsClass, constructor,
reinterpret_cast<jlong>(contacts));
return newConfig;
});
}
#pragma clang diagnostic pop
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) { Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) {

View File

@ -1,41 +1,6 @@
#include <jni.h> #include <jni.h>
#include "conversation.h" #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, "<init>", "(J)V");
jobject newConfig = env->NewObject(convoClass, constructor, reinterpret_cast<jlong>(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, "<init>", "(J)V");
jobject newConfig = env->NewObject(convoClass, constructor, reinterpret_cast<jlong>(convo_info_volatile));
return newConfig;
}
extern "C" extern "C"
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
@ -46,7 +11,6 @@ Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeOneT
return conversations->size_1to1(); return conversations->size_1to1();
} }
#pragma clang diagnostic pop
extern "C" extern "C"
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseAll(JNIEnv *env, Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseAll(JNIEnv *env,

View File

@ -3,7 +3,7 @@
#include "session/config/groups/info.hpp" #include "session/config/groups/info.hpp"
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jlong JNICALL
Java_network_loki_messenger_libsession_1util_GroupInfoConfig_00024Companion_newInstance(JNIEnv *env, Java_network_loki_messenger_libsession_1util_GroupInfoConfig_00024Companion_newInstance(JNIEnv *env,
jobject thiz, jobject thiz,
jbyteArray pub_key, 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); auto secret_key_bytes = util::ustring_from_bytes(env, secret_key);
secret_key_optional = secret_key_bytes; 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); auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump);
initial_dump_optional = initial_dump_bytes; initial_dump_optional = initial_dump_bytes;
} }
auto* group_info = new session::config::groups::Info(pub_key_bytes, secret_key_optional, initial_dump_optional); auto* group_info = new session::config::groups::Info(pub_key_bytes, secret_key_optional, initial_dump_optional);
return reinterpret_cast<jlong>(group_info);
jclass groupInfoClass = env->FindClass("network/loki/messenger/libsession_util/GroupInfoConfig");
jmethodID constructor = env->GetMethodID(groupInfoClass, "<init>", "(J)V");
jobject newConfig = env->NewObject(groupInfoClass, constructor, reinterpret_cast<jlong>(group_info));
return newConfig;
} }
extern "C" extern "C"

View File

@ -10,15 +10,15 @@ JNIEXPORT jint JNICALL
} }
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jlong JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newInstance(JNIEnv *env, Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newInstance(JNIEnv *env,
jobject thiz, jobject thiz,
jbyteArray user_secret_key, jbyteArray user_secret_key,
jbyteArray group_public_key, jbyteArray group_public_key,
jbyteArray group_secret_key, jbyteArray group_secret_key,
jbyteArray initial_dump, jbyteArray initial_dump,
jobject info_jobject, jlong info_pointer,
jobject members_jobject) { jlong members_pointer) {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto user_key_bytes = util::ustring_from_bytes(env, user_secret_key); auto user_key_bytes = util::ustring_from_bytes(env, user_secret_key);
auto pub_key_bytes = util::ustring_from_bytes(env, group_public_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; initial_dump_optional = initial_dump_bytes;
} }
auto info = ptrToInfo(env, info_jobject); auto info = reinterpret_cast<session::config::groups::Info*>(info_pointer);
auto members = ptrToMembers(env, members_jobject); auto members = reinterpret_cast<session::config::groups::Members*>(members_pointer);
auto* keys = new session::config::groups::Keys(user_key_bytes, auto* keys = new session::config::groups::Keys(user_key_bytes,
pub_key_bytes, pub_key_bytes,
@ -45,11 +45,7 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newI
*info, *info,
*members); *members);
jclass groupKeysConfig = env->FindClass("network/loki/messenger/libsession_util/GroupKeysConfig"); return reinterpret_cast<jlong>(keys);
jmethodID constructor = env->GetMethodID(groupKeysConfig, "<init>", "(J)V");
jobject newConfig = env->NewObject(groupKeysConfig, constructor, reinterpret_cast<jlong>(keys));
return newConfig;
} }
extern "C" extern "C"
@ -75,14 +71,14 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_loadKey(JNIEnv *env
jbyteArray message, jbyteArray message,
jstring hash, jstring hash,
jlong timestamp_ms, jlong timestamp_ms,
jobject info_jobject, jlong info_ptr,
jobject members_jobject) { jlong members_ptr) {
std::lock_guard lock{util::util_mutex_}; std::lock_guard lock{util::util_mutex_};
auto keys = ptrToKeys(env, thiz); auto keys = ptrToKeys(env, thiz);
auto message_bytes = util::ustring_from_bytes(env, message); auto message_bytes = util::ustring_from_bytes(env, message);
auto hash_bytes = env->GetStringUTFChars(hash, nullptr); auto hash_bytes = env->GetStringUTFChars(hash, nullptr);
auto info = ptrToInfo(env, info_jobject); auto info = reinterpret_cast<session::config::groups::Info*>(info_ptr);
auto members = ptrToMembers(env, members_jobject); auto members = reinterpret_cast<session::config::groups::Members*>(members_ptr);
bool processed = keys->load_key_message(hash_bytes, message_bytes, timestamp_ms, *info, *members); bool processed = keys->load_key_message(hash_bytes, message_bytes, timestamp_ms, *info, *members);
env->ReleaseStringUTFChars(hash, hash_bytes); env->ReleaseStringUTFChars(hash, hash_bytes);
@ -137,11 +133,11 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_pendingConfig(JNIEn
extern "C" extern "C"
JNIEXPORT jbyteArray JNICALL JNIEXPORT jbyteArray JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_rekey(JNIEnv *env, jobject thiz, 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_}; std::lock_guard lock{util::util_mutex_};
auto keys = ptrToKeys(env, thiz); auto keys = ptrToKeys(env, thiz);
auto info = ptrToInfo(env, info_jobject); auto info = reinterpret_cast<session::config::groups::Info*>(info_ptr);
auto members = ptrToMembers(env, members_jobject); auto members = reinterpret_cast<session::config::groups::Members*>(members_ptr);
auto rekey = keys->rekey(*info, *members); auto rekey = keys->rekey(*info, *members);
auto rekey_bytes = util::bytes_from_ustring(env, rekey.data()); auto rekey_bytes = util::bytes_from_ustring(env, rekey.data());
return rekey_bytes; return rekey_bytes;

View File

@ -1,7 +1,7 @@
#include "group_members.h" #include "group_members.h"
extern "C" extern "C"
JNIEXPORT jobject JNICALL JNIEXPORT jlong JNICALL
Java_network_loki_messenger_libsession_1util_GroupMembersConfig_00024Companion_newInstance( Java_network_loki_messenger_libsession_1util_GroupMembersConfig_00024Companion_newInstance(
JNIEnv *env, jobject thiz, jbyteArray pub_key, jbyteArray secret_key, JNIEnv *env, jobject thiz, jbyteArray pub_key, jbyteArray secret_key,
jbyteArray initial_dump) { 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); auto secret_key_bytes = util::ustring_from_bytes(env, secret_key);
secret_key_optional = secret_key_bytes; 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); auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump);
initial_dump_optional = initial_dump_bytes; initial_dump_optional = initial_dump_bytes;
} }
auto* group_members = new session::config::groups::Members(pub_key_bytes, secret_key_optional, initial_dump_optional); auto* group_members = new session::config::groups::Members(pub_key_bytes, secret_key_optional, initial_dump_optional);
return reinterpret_cast<jlong>(group_members);
jclass groupMemberClass = env->FindClass("network/loki/messenger/libsession_util/GroupMembersConfig");
jmethodID constructor = env->GetMethodID(groupMemberClass, "<init>", "(J)V");
jobject newConfig = env->NewObject(groupMemberClass, constructor, reinterpret_cast<jlong>(group_members));
return newConfig;
} }
extern "C" extern "C"

View File

@ -1,44 +1,6 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "bugprone-reserved-identifier"
#include "user_groups.h" #include "user_groups.h"
#include "oxenc/hex.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, "<init>", "(J)V");
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(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, "<init>", "(J)V");
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(user_groups));
return newConfig;
}
#pragma clang diagnostic pop
extern "C" extern "C"
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_network_loki_messenger_libsession_1util_util_GroupInfo_00024LegacyGroupInfo_00024Companion_NAME_1MAX_1LENGTH( Java_network_loki_messenger_libsession_1util_util_GroupInfo_00024LegacyGroupInfo_00024Companion_NAME_1MAX_1LENGTH(

View File

@ -2,39 +2,6 @@
#include "util.h" #include "util.h"
extern "C" { 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, "<init>", "(J)V");
jobject newConfig = env->NewObject(userClass, constructor, reinterpret_cast<jlong>(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, "<init>", "(J)V");
jobject newConfig = env->NewObject(userClass, constructor, reinterpret_cast<jlong>(profile));
return newConfig;
}
#pragma clang diagnostic pop
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_setName( Java_network_loki_messenger_libsession_1util_UserProfile_setName(
JNIEnv* env, JNIEnv* env,

View File

@ -16,15 +16,43 @@ import org.session.libsignal.utilities.Namespace
import java.io.Closeable import java.io.Closeable
import java.util.Stack 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 abstract fun namespace(): Int
external fun free()
override fun close() { private external fun free()
final override fun close() {
if (pointer != 0L) {
free() free()
pointer = 0L
}
} }
} }
sealed class ConfigBase(pointer: Long): Config(pointer) { interface ReadableConfig {
fun namespace(): Int
fun needsPush(): Boolean
fun needsDump(): Boolean
fun currentHashes(): List<String>
}
interface MutableConfig : ReadableConfig {
fun push(): ConfigPush
fun dump(): ByteArray
fun encryptionDomain(): String
fun confirmPushed(seqNo: Long, newHash: String)
fun merge(toMerge: Array<Pair<String,ByteArray>>): Stack<String>
fun dirty(): Boolean
}
sealed class ConfigBase(pointer: Long): Config(pointer), MutableConfig {
companion object { companion object {
init { init {
System.loadLibrary("session_util") System.loadLibrary("session_util")
@ -46,37 +74,30 @@ sealed class ConfigBase(pointer: Long): Config(pointer) {
} }
external fun dirty(): Boolean external override fun dirty(): Boolean
external fun needsPush(): Boolean external override fun needsPush(): Boolean
external fun needsDump(): Boolean external override fun needsDump(): Boolean
external fun push(): ConfigPush external override fun push(): ConfigPush
external fun dump(): ByteArray external override fun dump(): ByteArray
external fun encryptionDomain(): String external override fun encryptionDomain(): String
external fun confirmPushed(seqNo: Long, newHash: String) external override fun confirmPushed(seqNo: Long, newHash: String)
external fun merge(toMerge: Array<Pair<String,ByteArray>>): Stack<String> external override fun merge(toMerge: Array<Pair<String,ByteArray>>): Stack<String>
external fun currentHashes(): List<String> external override fun currentHashes(): List<String>
// Singular merge // Singular merge
external fun merge(toMerge: Pair<String,ByteArray>): Stack<String> external fun merge(toMerge: Pair<String,ByteArray>): Stack<String>
} }
class Contacts(pointer: Long) : ConfigBase(pointer) {
companion object { interface ReadableContacts: ReadableConfig {
init { fun get(accountId: String): Contact?
System.loadLibrary("session_util") fun all(): List<Contact>
}
external fun newInstance(ed25519SecretKey: ByteArray): Contacts
external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): Contacts
} }
override fun namespace() = Namespace.CONTACTS() interface MutableContacts : ReadableContacts, MutableConfig {
fun getOrConstruct(accountId: String): Contact
external fun get(accountId: String): Contact? fun set(contact: Contact)
external fun getOrConstruct(accountId: String): Contact fun erase(accountId: String): Boolean
external fun all(): List<Contact>
external fun set(contact: Contact)
external fun erase(accountId: String): Boolean
/** /**
* Similar to [updateIfExists], but will create the underlying contact if it doesn't exist before passing to [updateFunction] * Similar to [updateIfExists], but will create the underlying contact if it doesn't exist before passing to [updateFunction]
@ -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) { class Contacts private constructor(pointer: Long) : ConfigBase(pointer), MutableContacts {
companion object { constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this(
init { createConfigObject(
System.loadLibrary("session_util") "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<Contact>
external override fun set(contact: Contact)
external override fun erase(accountId: String): Boolean
} }
external fun newInstance(ed25519SecretKey: ByteArray): UserProfile
external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserProfile 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() override fun namespace() = Namespace.USER_PROFILE()
external fun setName(newName: String) external override fun setName(newName: String)
external fun getName(): String? external override fun getName(): String?
external fun getPic(): UserPic external override fun getPic(): UserPic
external fun setPic(userPic: UserPic) external override fun setPic(userPic: UserPic)
external fun setNtsPriority(priority: Long) external override fun setNtsPriority(priority: Long)
external fun getNtsPriority(): Long external override fun getNtsPriority(): Long
external fun setNtsExpiry(expiryMode: ExpiryMode) external override fun setNtsExpiry(expiryMode: ExpiryMode)
external fun getNtsExpiry(): ExpiryMode external override fun getNtsExpiry(): ExpiryMode
external fun getCommunityMessageRequests(): Boolean external override fun getCommunityMessageRequests(): Boolean
external fun setCommunityMessageRequests(blocks: Boolean) external override fun setCommunityMessageRequests(blocks: Boolean)
external fun isBlockCommunityMessageRequestsSet(): Boolean external override fun isBlockCommunityMessageRequestsSet(): Boolean
} }
class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { interface ReadableConversationVolatileConfig: ReadableConfig {
companion object { fun getOneToOne(pubKeyHex: String): Conversation.OneToOne?
init { fun getCommunity(baseUrl: String, room: String): Conversation.Community?
System.loadLibrary("session_util") 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<Conversation.OneToOne>
fun allCommunities(): List<Conversation.Community>
fun allLegacyClosedGroups(): List<Conversation.LegacyGroup>
fun allClosedGroups(): List<Conversation.ClosedGroup>
fun all(): List<Conversation?>
} }
external fun newInstance(ed25519SecretKey: ByteArray): ConversationVolatileConfig
external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): ConversationVolatileConfig 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() override fun namespace() = Namespace.CONVO_INFO_VOLATILE()
external fun getOneToOne(pubKeyHex: String): Conversation.OneToOne? external override fun getOneToOne(pubKeyHex: String): Conversation.OneToOne?
external fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne external override fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne
external fun eraseOneToOne(pubKeyHex: String): Boolean external override fun eraseOneToOne(pubKeyHex: String): Boolean
external fun getCommunity(baseUrl: String, room: String): Conversation.Community? external override fun getCommunity(baseUrl: String, room: String): Conversation.Community?
external fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community external override fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community
external fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community external override fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community
external fun eraseCommunity(community: Conversation.Community): Boolean external override fun eraseCommunity(community: Conversation.Community): Boolean
external fun eraseCommunity(baseUrl: String, room: String): Boolean external override fun eraseCommunity(baseUrl: String, room: String): Boolean
external fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup? external override fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup?
external fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup external override fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup
external fun eraseLegacyClosedGroup(groupId: String): Boolean external override fun eraseLegacyClosedGroup(groupId: String): Boolean
external fun getClosedGroup(sessionId: String): Conversation.ClosedGroup? external override fun getClosedGroup(sessionId: String): Conversation.ClosedGroup?
external fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup external override fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup
external fun eraseClosedGroup(sessionId: String): Boolean external override fun eraseClosedGroup(sessionId: String): Boolean
external fun erase(conversation: Conversation): Boolean external override fun erase(conversation: Conversation): Boolean
external fun set(toStore: Conversation) external override fun set(toStore: Conversation)
/** /**
* Erase all conversations that do not satisfy the `predicate`, similar to [MutableList.removeAll] * 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 override fun sizeOneToOnes(): Int
external fun sizeCommunities(): Int external override fun sizeCommunities(): Int
external fun sizeLegacyClosedGroups(): Int external override fun sizeLegacyClosedGroups(): Int
external fun size(): Int external override fun size(): Int
external fun empty(): Boolean external override fun empty(): Boolean
external fun allOneToOnes(): List<Conversation.OneToOne>
external fun allCommunities(): List<Conversation.Community>
external fun allLegacyClosedGroups(): List<Conversation.LegacyGroup>
external fun allClosedGroups(): List<Conversation.ClosedGroup>
external fun all(): List<Conversation?>
external override fun allOneToOnes(): List<Conversation.OneToOne>
external override fun allCommunities(): List<Conversation.Community>
external override fun allLegacyClosedGroups(): List<Conversation.LegacyGroup>
external override fun allClosedGroups(): List<Conversation.ClosedGroup>
external override fun all(): List<Conversation?>
} }
class UserGroupsConfig(pointer: Long): ConfigBase(pointer) { interface ReadableUserGroupsConfig : ReadableConfig {
companion object { fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo?
init { fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo?
System.loadLibrary("session_util") fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo?
fun sizeCommunityInfo(): Long
fun sizeLegacyGroupInfo(): Long
fun sizeClosedGroup(): Long
fun size(): Long
fun all(): List<GroupInfo>
fun allCommunityInfo(): List<GroupInfo.CommunityGroupInfo>
fun allLegacyGroupInfo(): List<GroupInfo.LegacyGroupInfo>
fun allClosedGroupInfo(): List<GroupInfo.ClosedGroupInfo>
} }
external fun newInstance(ed25519SecretKey: ByteArray): UserGroupsConfig
external fun newInstance(ed25519SecretKey: ByteArray, initialDump: ByteArray): UserGroupsConfig 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() override fun namespace() = Namespace.GROUPS()
external fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo? external override fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo?
external fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo? external override fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo?
external fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo? external override fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo?
external fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo external override fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo
external fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo external override fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo
external fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo external override fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo
external fun set(groupInfo: GroupInfo) external override fun set(groupInfo: GroupInfo)
external fun erase(groupInfo: GroupInfo) external override fun erase(groupInfo: GroupInfo)
external fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean external override fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean
external fun eraseCommunity(server: String, room: String): Boolean external override fun eraseCommunity(server: String, room: String): Boolean
external fun eraseLegacyGroup(accountId: String): Boolean external override fun eraseLegacyGroup(accountId: String): Boolean
external fun eraseClosedGroup(accountId: String): Boolean external override fun eraseClosedGroup(accountId: String): Boolean
external fun sizeCommunityInfo(): Long external override fun sizeCommunityInfo(): Long
external fun sizeLegacyGroupInfo(): Long external override fun sizeLegacyGroupInfo(): Long
external fun sizeClosedGroup(): Long external override fun sizeClosedGroup(): Long
external fun size(): Long external override fun size(): Long
external fun all(): List<GroupInfo> external override fun all(): List<GroupInfo>
external fun allCommunityInfo(): List<GroupInfo.CommunityGroupInfo> external override fun allCommunityInfo(): List<GroupInfo.CommunityGroupInfo>
external fun allLegacyGroupInfo(): List<GroupInfo.LegacyGroupInfo> external override fun allLegacyGroupInfo(): List<GroupInfo.LegacyGroupInfo>
external fun allClosedGroupInfo(): List<GroupInfo.ClosedGroupInfo> external override fun allClosedGroupInfo(): List<GroupInfo.ClosedGroupInfo>
external fun createGroup(): GroupInfo.ClosedGroupInfo external override fun createGroup(): GroupInfo.ClosedGroupInfo
} }
class GroupInfoConfig(pointer: Long): ConfigBase(pointer), Closeable { 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
}
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 { companion object {
init { private external fun newInstance(
System.loadLibrary("session_util") pubKey: ByteArray,
} secretKey: ByteArray?,
initialDump: ByteArray?
external fun newInstance( ): Long
pubKey: ByteArray?,
secretKey: ByteArray? = null,
initialDump: ByteArray = byteArrayOf()
): GroupInfoConfig
} }
override fun namespace() = Namespace.CLOSED_GROUP_INFO() override fun namespace() = Namespace.CLOSED_GROUP_INFO()
external fun id(): AccountId external override fun id(): AccountId
external fun destroyGroup() external override fun destroyGroup()
external fun getCreated(): Long? external override fun getCreated(): Long?
external fun getDeleteAttachmentsBefore(): Long? external override fun getDeleteAttachmentsBefore(): Long?
external fun getDeleteBefore(): Long? external override fun getDeleteBefore(): Long?
external fun getExpiryTimer(): Long external override fun getExpiryTimer(): Long
external fun getName(): String external override fun getName(): String
external fun getProfilePic(): UserPic external override fun getProfilePic(): UserPic
external fun isDestroyed(): Boolean external override fun isDestroyed(): Boolean
external fun setCreated(createdAt: Long) external override fun setCreated(createdAt: Long)
external fun setDeleteAttachmentsBefore(deleteBefore: Long) external override fun setDeleteAttachmentsBefore(deleteBefore: Long)
external fun setDeleteBefore(deleteBefore: Long) external override fun setDeleteBefore(deleteBefore: Long)
external fun setExpiryTimer(expireSeconds: Long) external override fun setExpiryTimer(expireSeconds: Long)
external fun setName(newName: String) external override fun setName(newName: String)
external fun getDescription(): String external override fun getDescription(): String
external fun setDescription(newDescription: String) external override fun setDescription(newDescription: String)
external fun setProfilePic(newProfilePic: UserPic) external override fun setProfilePic(newProfilePic: UserPic)
external fun storageNamespace(): Long external override fun storageNamespace(): Long
override fun close() {
free()
}
} }
class GroupMembersConfig(pointer: Long): ConfigBase(pointer), Closeable { interface ReadableGroupMembersConfig: ReadableConfig {
fun all(): List<GroupMember>
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 { companion object {
init { private external fun newInstance(
System.loadLibrary("session_util")
}
external fun newInstance(
pubKey: ByteArray, pubKey: ByteArray,
secretKey: ByteArray? = null, secretKey: ByteArray?,
initialDump: ByteArray = byteArrayOf() initialDump: ByteArray?
): GroupMembersConfig ): Long
} }
constructor(groupPubKey: ByteArray, groupAdminKey: ByteArray?, initialDump: ByteArray?)
: this(newInstance(groupPubKey, groupAdminKey, initialDump))
override fun namespace() = Namespace.CLOSED_GROUP_MEMBERS() override fun namespace() = Namespace.CLOSED_GROUP_MEMBERS()
external fun all(): Stack<GroupMember> external override fun all(): Stack<GroupMember>
external fun erase(groupMember: GroupMember): Boolean external override fun erase(groupMember: GroupMember): Boolean
external fun erase(pubKeyHex: String): Boolean external override fun erase(pubKeyHex: String): Boolean
external fun get(pubKeyHex: String): GroupMember? external override fun get(pubKeyHex: String): GroupMember?
external fun getOrConstruct(pubKeyHex: String): GroupMember external override fun getOrConstruct(pubKeyHex: String): GroupMember
external fun set(groupMember: GroupMember) external override fun set(groupMember: GroupMember)
override fun close() {
free()
}
} }
sealed class ConfigSig(pointer: Long) : Config(pointer) sealed class ConfigSig(pointer: Long) : Config(pointer)
class GroupKeysConfig(pointer: Long): ConfigSig(pointer) { interface ReadableGroupKeysConfig {
companion object { fun groupKeys(): Stack<ByteArray>
init { fun needsDump(): Boolean
System.loadLibrary("session_util") fun dump(): ByteArray
fun needsRekey(): Boolean
fun pendingKey(): ByteArray?
fun supplementFor(userSessionId: String): ByteArray
fun pendingConfig(): ByteArray?
fun currentHashes(): List<String>
fun encrypt(plaintext: ByteArray): ByteArray
fun decrypt(ciphertext: ByteArray): Pair<ByteArray, AccountId>?
fun keys(): Stack<ByteArray>
fun subAccountSign(message: ByteArray, signingValue: ByteArray): GroupKeysConfig.SwarmAuth
fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray
fun currentGeneration(): Int
} }
external fun newInstance(
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 {
private external fun newInstance(
userSecretKey: ByteArray, userSecretKey: ByteArray,
groupPublicKey: ByteArray, groupPublicKey: ByteArray,
groupSecretKey: ByteArray? = null, groupSecretKey: ByteArray? = null,
initialDump: ByteArray = byteArrayOf(), initialDump: ByteArray?,
infoPtr: Long,
members: Long
): Long
}
constructor(
userSecretKey: ByteArray,
groupPublicKey: ByteArray,
groupAdminKey: ByteArray?,
initialDump: ByteArray?,
info: GroupInfoConfig, info: GroupInfoConfig,
members: GroupMembersConfig members: GroupMembersConfig
): GroupKeysConfig ) : this(
} newInstance(
userSecretKey,
groupPublicKey,
groupAdminKey,
initialDump,
info.pointer,
members.pointer
)
)
override fun namespace() = Namespace.ENCRYPTION_KEYS() override fun namespace() = Namespace.ENCRYPTION_KEYS()
external fun groupKeys(): Stack<ByteArray> external override fun groupKeys(): Stack<ByteArray>
external fun needsDump(): Boolean external override fun needsDump(): Boolean
external fun dump(): ByteArray external override fun dump(): ByteArray
external fun loadKey(message: ByteArray, external fun loadKey(message: ByteArray,
hash: String, hash: String,
timestampMs: Long, timestampMs: Long,
info: GroupInfoConfig, infoPtr: Long,
members: GroupMembersConfig): Boolean membersPtr: Long): Boolean
external fun needsRekey(): Boolean external override fun needsRekey(): Boolean
external fun pendingKey(): ByteArray? external override fun pendingKey(): ByteArray?
external fun supplementFor(userSessionId: String): ByteArray external override fun supplementFor(userSessionId: String): ByteArray
external fun pendingConfig(): ByteArray? external override fun pendingConfig(): ByteArray?
external fun currentHashes(): List<String> external override fun currentHashes(): List<String>
external fun rekey(info: GroupInfoConfig, members: GroupMembersConfig): ByteArray external fun rekey(infoPtr: Long, membersPtr: Long): ByteArray
override fun close() {
free()
}
external fun encrypt(plaintext: ByteArray): ByteArray external override fun encrypt(plaintext: ByteArray): ByteArray
external fun decrypt(ciphertext: ByteArray): Pair<ByteArray, AccountId>? external override fun decrypt(ciphertext: ByteArray): Pair<ByteArray, AccountId>?
external fun keys(): Stack<ByteArray> external override fun keys(): Stack<ByteArray>
external fun makeSubAccount(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray external override fun makeSubAccount(sessionId: AccountId, canWrite: Boolean, canDelete: Boolean): ByteArray
external fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): 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( data class SwarmAuth(
val subAccount: String, val subAccount: String,
val subAccountSig: String, val subAccountSig: String,
val signature: String val signature: String
) )
} }
private external fun createConfigObject(
configName: String,
ed25519SecretKey: ByteArray,
initialDump: ByteArray?
): Long

View File

@ -68,7 +68,6 @@ interface StorageProtocol {
fun getMessageSendJob(messageSendJobID: String): MessageSendJob? fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
fun getMessageReceiveJob(messageReceiveJobID: String): Job? fun getMessageReceiveJob(messageReceiveJobID: String): Job?
fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job? fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job?
fun getConfigSyncJob(destination: Destination): Job?
fun resumeMessageSendJobIfNeeded(messageSendJobID: String) fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
fun isJobCanceled(job: Job): Boolean fun isJobCanceled(job: Job): Boolean
fun cancelPendingMessageSendJobs(threadID: Long) fun cancelPendingMessageSendJobs(threadID: Long)
@ -269,7 +268,6 @@ interface StorageProtocol {
) )
// Shared configs // Shared configs
fun notifyConfigUpdates(forConfigObject: Config, messageTimestamp: Long)
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
fun isCheckingCommunityRequests(): Boolean fun isCheckingCommunityRequests(): Boolean

View File

@ -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<out MutableConfig>,
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)
}
}
}
}

View File

@ -8,7 +8,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch 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.GroupMember
import network.loki.messenger.libsession_util.util.Sodium import network.loki.messenger.libsession_util.util.Sodium
import org.session.libsession.messaging.messages.Destination 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.OwnedSwarmAuth
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.withGroupConfigsOrNull
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
@ -61,12 +61,8 @@ class RemoveGroupMemberHandler(
} }
private suspend fun processPendingMemberRemoval() { private suspend fun processPendingMemberRemoval() {
val userGroups = checkNotNull(configFactory.userGroups) {
"User groups config is null"
}
// Run the removal process for each group in parallel // Run the removal process for each group in parallel
val removalTasks = userGroups.allClosedGroupInfo() val removalTasks = configFactory.withUserConfigs { it.userGroups.allClosedGroupInfo() }
.asSequence() .asSequence()
.filter { it.hasAdminKey() } .filter { it.hasAdminKey() }
.associate { group -> .associate { group ->
@ -89,7 +85,7 @@ class RemoveGroupMemberHandler(
} }
} }
private fun processPendingRemovalsForGroup( private suspend fun processPendingRemovalsForGroup(
groupAccountId: AccountId, groupAccountId: AccountId,
groupName: String, groupName: String,
adminKey: ByteArray adminKey: ByteArray
@ -100,11 +96,11 @@ class RemoveGroupMemberHandler(
ed25519PrivateKey = adminKey ed25519PrivateKey = adminKey
) )
configFactory.withGroupConfigsOrNull(groupAccountId) withConfig@ { info, members, keys -> val batchCalls = configFactory.withGroupConfigs(groupAccountId) { configs ->
val pendingRemovals = members.all().filter { it.removed } val pendingRemovals = configs.groupMembers.all().filter { it.removed }
if (pendingRemovals.isEmpty()) { if (pendingRemovals.isEmpty()) {
// Skip if there are no pending removals // Skip if there are no pending removals
return@withConfig return@withGroupConfigs emptyList()
} }
Log.d(TAG, "Processing ${pendingRemovals.size} pending removals for group $groupName") 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) // 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 // 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 // 3. Conditionally, delete removed-members' messages from the group's message store, if that option is selected by the actioning admin
val seqCalls = ArrayList<SnodeAPI.SnodeBatchRequestInfo>(3) val calls = ArrayList<SnodeAPI.SnodeBatchRequestInfo>(3)
// Call No 1. Revoke sub-key. This call is crucial and must not fail for the rest of the operation to be successful. // 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( SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest(
groupAdminAuth = swarmAuth, groupAdminAuth = swarmAuth,
subAccountTokens = pendingRemovals.map { subAccountTokens = pendingRemovals.map {
keys.getSubAccountToken(AccountId(it.sessionId)) configs.groupKeys.getSubAccountToken(AccountId(it.sessionId))
} }
) )
) { "Fail to create a revoke request" } ) { "Fail to create a revoke request" }
// Call No 2. Send a message to the removed members // Call No 2. Send a message to the removed members
seqCalls += SnodeAPI.buildAuthenticatedStoreBatchInfo( calls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = Namespace.REVOKED_GROUP_MESSAGES(), namespace = Namespace.REVOKED_GROUP_MESSAGES(),
message = buildGroupKickMessage(groupAccountId.hexString, pendingRemovals, keys, adminKey), message = buildGroupKickMessage(groupAccountId.hexString, pendingRemovals, configs.groupKeys, adminKey),
auth = swarmAuth, auth = swarmAuth,
) )
// Call No 3. Conditionally remove the message from the group's message store // Call No 3. Conditionally remove the message from the group's message store
if (pendingRemovals.any { it.shouldRemoveMessages }) { if (pendingRemovals.any { it.shouldRemoveMessages }) {
seqCalls += SnodeAPI.buildAuthenticatedStoreBatchInfo( calls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = Namespace.CLOSED_GROUP_MESSAGES(), namespace = Namespace.CLOSED_GROUP_MESSAGES(),
message = buildDeleteGroupMemberContentMessage( message = buildDeleteGroupMemberContentMessage(
groupAccountId = groupAccountId.hexString, groupAccountId = groupAccountId.hexString,
@ -147,9 +143,17 @@ class RemoveGroupMemberHandler(
) )
} }
// Make the call: calls
SnodeAPI.getSingleTargetSnode(groupAccountId.hexString)
} }
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( private fun buildDeleteGroupMemberContentMessage(
@ -178,7 +182,7 @@ class RemoveGroupMemberHandler(
private fun buildGroupKickMessage( private fun buildGroupKickMessage(
groupAccountId: String, groupAccountId: String,
pendingRemovals: List<GroupMember>, pendingRemovals: List<GroupMember>,
keys: GroupKeysConfig, keys: ReadableGroupKeysConfig,
adminKey: ByteArray adminKey: ByteArray
) = SnodeMessage( ) = SnodeMessage(
recipient = groupAccountId, recipient = groupAccountId,

View File

@ -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<ConfigMessageInformation>,
val toDelete: List<String>
)
private fun destinationConfigs(
configFactoryProtocol: ConfigFactoryProtocol
): SyncInformation {
val toDelete = mutableListOf<String>()
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<SnodeBatchRequestInfo>()
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<RawResponse>)
// 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<String>,
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<ConfigurationSyncJob> {
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)
}
}
}

View File

@ -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<String>) : 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
}

View File

@ -29,7 +29,6 @@ class JobQueue : JobDelegate {
private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() private val rxMediaDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher() private val openGroupDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val configDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob() private val scope = CoroutineScope(Dispatchers.Default) + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED) private val queue = Channel<Job>(UNLIMITED)
@ -117,20 +116,14 @@ class JobQueue : JobDelegate {
val txQueue = Channel<Job>(capacity = UNLIMITED) val txQueue = Channel<Job>(capacity = UNLIMITED)
val mediaQueue = Channel<Job>(capacity = UNLIMITED) val mediaQueue = Channel<Job>(capacity = UNLIMITED)
val openGroupQueue = Channel<Job>(capacity = UNLIMITED) val openGroupQueue = Channel<Job>(capacity = UNLIMITED)
val configQueue = Channel<Job>(capacity = UNLIMITED)
val receiveJob = processWithDispatcher(rxQueue, rxDispatcher, "rx", asynchronous = false) val receiveJob = processWithDispatcher(rxQueue, rxDispatcher, "rx", asynchronous = false)
val txJob = processWithDispatcher(txQueue, txDispatcher, "tx") val txJob = processWithDispatcher(txQueue, txDispatcher, "tx")
val mediaJob = processWithDispatcher(mediaQueue, rxMediaDispatcher, "media") val mediaJob = processWithDispatcher(mediaQueue, rxMediaDispatcher, "media")
val openGroupJob = processWithOpenGroupDispatcher(openGroupQueue, openGroupDispatcher, "openGroup") val openGroupJob = processWithOpenGroupDispatcher(openGroupQueue, openGroupDispatcher, "openGroup")
val configJob = processWithDispatcher(configQueue, configDispatcher, "configDispatcher")
while (isActive) { while (isActive) {
when (val job = queue.receive()) { when (val job = queue.receive()) {
is InviteContactsJob,
is ConfigurationSyncJob -> {
configQueue.send(job)
}
is NotifyPNServerJob, is NotifyPNServerJob,
is AttachmentUploadJob, is AttachmentUploadJob,
is GroupLeavingJob, is GroupLeavingJob,
@ -167,7 +160,6 @@ class JobQueue : JobDelegate {
txJob.cancel() txJob.cancel()
mediaJob.cancel() mediaJob.cancel()
openGroupJob.cancel() openGroupJob.cancel()
configJob.cancel()
} }
} }
@ -239,8 +231,6 @@ class JobQueue : JobDelegate {
BackgroundGroupAddJob.KEY, BackgroundGroupAddJob.KEY,
OpenGroupDeleteJob.KEY, OpenGroupDeleteJob.KEY,
RetrieveProfileAvatarJob.KEY, RetrieveProfileAvatarJob.KEY,
ConfigurationSyncJob.KEY,
InviteContactsJob.KEY,
GroupLeavingJob.KEY, GroupLeavingJob.KEY,
LibSessionGroupLeavingJob.KEY LibSessionGroupLeavingJob.KEY
) )

View File

@ -16,7 +16,6 @@ class SessionJobManagerFactories {
GroupAvatarDownloadJob.KEY to GroupAvatarDownloadJob.Factory(), GroupAvatarDownloadJob.KEY to GroupAvatarDownloadJob.Factory(),
BackgroundGroupAddJob.KEY to BackgroundGroupAddJob.Factory(), BackgroundGroupAddJob.KEY to BackgroundGroupAddJob.Factory(),
OpenGroupDeleteJob.KEY to OpenGroupDeleteJob.Factory(), OpenGroupDeleteJob.KEY to OpenGroupDeleteJob.Factory(),
ConfigurationSyncJob.KEY to ConfigurationSyncJob.Factory()
) )
} }
} }

View File

@ -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.open_groups.OpenGroupMessage
import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.GroupSubAccountSwarmAuth
import org.session.libsession.snode.OwnedSwarmAuth import org.session.libsession.snode.OwnedSwarmAuth
import org.session.libsession.snode.RawResponsePromise import org.session.libsession.snode.RawResponsePromise
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
@ -184,13 +183,9 @@ object MessageSender {
MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey) MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
} }
is Destination.ClosedGroup -> { is Destination.ClosedGroup -> {
val groupKeys = configFactory.getGroupKeysConfig(AccountId(destination.publicKey)) ?: throw Error.NoKeyPair
val envelope = MessageWrapper.createEnvelope(kind, message.sentTimestamp!!, senderPublicKey, proto.build().toByteArray()) val envelope = MessageWrapper.createEnvelope(kind, message.sentTimestamp!!, senderPublicKey, proto.build().toByteArray())
groupKeys.use { keys -> configFactory.withGroupConfigs(AccountId(destination.publicKey)) {
if (keys.keys().isEmpty()) { it.groupKeys.encrypt(envelope.toByteArray())
throw Error.EncryptionFailed
}
keys.encrypt(envelope.toByteArray())
} }
} }
else -> throw IllegalStateException("Destination should not be open group.") else -> throw IllegalStateException("Destination should not be open group.")
@ -252,27 +247,13 @@ object MessageSender {
namespaces.mapNotNull { namespace -> namespaces.mapNotNull { namespace ->
if (destination is Destination.ClosedGroup) { if (destination is Destination.ClosedGroup) {
// possibly handle a failure for no user groups or no closed group signing key? // 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 groupAuth = configFactory.getGroupAuth(AccountId(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( SnodeAPI.sendMessage(
auth = GroupSubAccountSwarmAuth(keys, AccountId(destination.publicKey), groupAuthData), auth = groupAuth,
message = snodeMessage, message = snodeMessage,
namespace = namespace 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
}
} else { } else {
SnodeAPI.sendMessage(snodeMessage, auth = null, namespace = namespace) SnodeAPI.sendMessage(snodeMessage, auth = null, namespace = namespace)
} }
@ -353,9 +334,9 @@ object MessageSender {
message.sentTimestamp = nowWithOffset message.sentTimestamp = nowWithOffset
} }
// Attach the blocks message requests info // Attach the blocks message requests info
configFactory.user?.let { user -> configFactory.withUserConfigs { configs ->
if (message is VisibleMessage) { if (message is VisibleMessage) {
message.blocksMessageRequests = !user.getCommunityMessageRequests() message.blocksMessageRequests = !configs.userProfile.getCommunityMessageRequests()
} }
} }
val userEdKeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()!! val userEdKeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair()!!

View File

@ -9,25 +9,18 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch 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.GroupInfo
import network.loki.messenger.libsession_util.util.Sodium 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.groups.GroupManagerV2
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters import org.session.libsession.messaging.jobs.MessageReceiveParameters
import org.session.libsession.messaging.messages.Destination 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.RawResponse
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.model.BatchResponse
import org.session.libsession.snode.utilities.await import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.withGroupConfigsOrNull
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -41,7 +34,9 @@ class ClosedGroupPoller(
private val executor: CoroutineDispatcher, private val executor: CoroutineDispatcher,
private val closedGroupSessionId: AccountId, private val closedGroupSessionId: AccountId,
private val configFactoryProtocol: ConfigFactoryProtocol, private val configFactoryProtocol: ConfigFactoryProtocol,
private val groupManagerV2: GroupManagerV2) { private val groupManagerV2: GroupManagerV2,
private val storage: StorageProtocol,
) {
data class ParsedRawMessage( data class ParsedRawMessage(
val data: ByteArray, val data: ByteArray,
@ -82,9 +77,8 @@ class ClosedGroupPoller(
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Starting closed group poller for ${closedGroupSessionId.hexString.take(4)}") if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Starting closed group poller for ${closedGroupSessionId.hexString.take(4)}")
job?.cancel() job?.cancel()
job = scope.launch(executor) { job = scope.launch(executor) {
val closedGroups = configFactoryProtocol.userGroups ?: return@launch
while (isActive) { while (isActive) {
val group = closedGroups.getClosedGroup(closedGroupSessionId.hexString) ?: break val group = configFactoryProtocol.withUserConfigs { it.userGroups.getClosedGroup(closedGroupSessionId.hexString) } ?: break
val nextPoll = runCatching { poll(group) } val nextPoll = runCatching { poll(group) }
when { when {
nextPoll.isFailure -> { nextPoll.isFailure -> {
@ -114,119 +108,106 @@ class ClosedGroupPoller(
private suspend fun poll(group: GroupInfo.ClosedGroupInfo): Long? = coroutineScope { private suspend fun poll(group: GroupInfo.ClosedGroupInfo): Long? = coroutineScope {
val snode = SnodeAPI.getSingleTargetSnode(closedGroupSessionId.hexString).await() val snode = SnodeAPI.getSingleTargetSnode(closedGroupSessionId.hexString).await()
configFactoryProtocol.withGroupConfigsOrNull(closedGroupSessionId) { info, members, keys -> val groupAuth = configFactoryProtocol.getGroupAuth(closedGroupSessionId) ?: return@coroutineScope null
val hashesToExtend = mutableSetOf<String>() val configHashesToExtends = configFactoryProtocol.withGroupConfigs(closedGroupSessionId) {
buildSet {
hashesToExtend += info.currentHashes() addAll(it.groupKeys.currentHashes())
hashesToExtend += members.currentHashes() addAll(it.groupInfo.currentHashes())
hashesToExtend += keys.currentHashes() addAll(it.groupMembers.currentHashes())
val authData = group.authData
val adminKey = group.adminKey
val groupAccountId = group.groupAccountId
val auth = if (authData != null) {
GroupSubAccountSwarmAuth(
groupKeysConfig = keys,
accountId = groupAccountId,
authData = authData
)
} 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 adminKey = requireNotNull(configFactoryProtocol.withUserConfigs { it.userGroups.getClosedGroup(closedGroupSessionId.hexString) }) {
"Group doesn't exist"
}.adminKey
val pollingTasks = mutableListOf<Pair<String, Deferred<*>>>() val pollingTasks = mutableListOf<Pair<String, Deferred<*>>>()
pollingTasks += "Poll revoked messages" to async { pollingTasks += "Poll revoked messages" to async {
handleRevoked( handleRevoked(
body = SnodeAPI.sendBatchRequest( SnodeAPI.sendBatchRequest(
groupAccountId, snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest( SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, snode = snode,
auth = auth, auth = groupAuth,
namespace = Namespace.REVOKED_GROUP_MESSAGES(), namespace = Namespace.REVOKED_GROUP_MESSAGES(),
maxSize = null, maxSize = null,
), ),
Map::class.java), Map::class.java
keys = keys )
) )
} }
pollingTasks += "Poll group messages" to async { pollingTasks += "Poll group messages" to async {
handleMessages( handleMessages(
body = SnodeAPI.sendBatchRequest( body = SnodeAPI.sendBatchRequest(
groupAccountId, snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest( SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, snode = snode,
auth = auth, auth = groupAuth,
namespace = Namespace.CLOSED_GROUP_MESSAGES(), namespace = Namespace.CLOSED_GROUP_MESSAGES(),
maxSize = null, maxSize = null,
), ),
Map::class.java), Map::class.java),
snode = snode, snode = snode,
keysConfig = keys
) )
} }
pollingTasks += "Poll group keys config" to async { pollingTasks += "Poll group keys config" to async {
handleKeyPoll( handleKeyPoll(
response = SnodeAPI.sendBatchRequest( response = SnodeAPI.sendBatchRequest(
groupAccountId, snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest( SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, snode = snode,
auth = auth, auth = groupAuth,
namespace = keys.namespace(), namespace = Namespace.ENCRYPTION_KEYS(),
maxSize = null, maxSize = null,
), ),
Map::class.java), Map::class.java),
keysConfig = keys,
infoConfig = info,
membersConfig = members
) )
} }
pollingTasks += "Poll group info config" to async { pollingTasks += "Poll group info config" to async {
handleInfo( handleInfo(
response = SnodeAPI.sendBatchRequest( response = SnodeAPI.sendBatchRequest(
groupAccountId, snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest( SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, snode = snode,
auth = auth, auth = groupAuth,
namespace = Namespace.CLOSED_GROUP_INFO(), namespace = Namespace.CLOSED_GROUP_INFO(),
maxSize = null, maxSize = null,
), ),
Map::class.java), Map::class.java),
infoConfig = info
) )
} }
pollingTasks += "Poll group members config" to async { pollingTasks += "Poll group members config" to async {
handleMembers( handleMembers(
response = SnodeAPI.sendBatchRequest( SnodeAPI.sendBatchRequest(
groupAccountId, snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedRetrieveBatchRequest( SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, snode = snode,
auth = auth, auth = groupAuth,
namespace = Namespace.CLOSED_GROUP_MEMBERS(), namespace = Namespace.CLOSED_GROUP_MEMBERS(),
maxSize = null, maxSize = null,
), ),
Map::class.java), Map::class.java),
membersConfig = members
) )
} }
if (hashesToExtend.isNotEmpty() && adminKey != null) { if (configHashesToExtends.isNotEmpty() && adminKey != null) {
pollingTasks += "Extend group config TTL" to async { pollingTasks += "Extend group config TTL" to async {
SnodeAPI.sendBatchRequest( SnodeAPI.sendBatchRequest(
groupAccountId, snode,
closedGroupSessionId.hexString,
SnodeAPI.buildAuthenticatedAlterTtlBatchRequest( SnodeAPI.buildAuthenticatedAlterTtlBatchRequest(
messageHashes = hashesToExtend.toList(), messageHashes = configHashesToExtends.toList(),
auth = auth, auth = groupAuth,
newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds,
extend = true extend = true
), ),
@ -245,27 +226,6 @@ class ClosedGroupPoller(
throw PollerException("Error polling closed group", errors) 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
if (info.needsDump() || members.needsDump() || keys.needsDump()) {
configFactoryProtocol.saveGroupConfigs(keys, info, members)
}
if (requiresSync) {
configFactoryProtocol.scheduleUpdate(Destination.ClosedGroup(closedGroupSessionId.hexString))
}
}
POLL_INTERVAL // this might change in future 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 // This shouldn't ever return null at this point
val userSessionId = configFactoryProtocol.userSessionId()!!
val messages = body["messages"] as? List<*> val messages = body["messages"] as? List<*>
?: return Log.w("GroupPoller", "body didn't contain a list of messages") ?: return Log.w("GroupPoller", "body didn't contain a list of messages")
messages.forEach { messageMap -> messages.forEach { messageMap ->
@ -305,7 +264,13 @@ class ClosedGroupPoller(
val message = decoded.decodeToString() val message = decoded.decodeToString()
if (Sodium.KICKED_REGEX.matches(message)) { if (Sodium.KICKED_REGEX.matches(message)) {
val (sessionId, generation) = message.split("-") 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 { try {
groupManagerV2.handleKicked(closedGroupSessionId) groupManagerV2.handleKicked(closedGroupSessionId)
} catch (e: Exception) { } catch (e: Exception) {
@ -318,51 +283,51 @@ class ClosedGroupPoller(
} }
} }
private fun handleKeyPoll(response: RawResponse, private fun handleKeyPoll(response: RawResponse) {
keysConfig: GroupKeysConfig,
infoConfig: GroupInfoConfig,
membersConfig: GroupMembersConfig) {
// get all the data to hash objects and process them // get all the data to hash objects and process them
val allMessages = parseMessages(response) val allMessages = parseMessages(response)
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Total key messages this poll: ${allMessages.size}") if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Total key messages this poll: ${allMessages.size}")
var total = 0 var total = 0
allMessages.forEach { (message, hash, timestamp) -> allMessages.forEach { (message, hash, timestamp) ->
if (keysConfig.loadKey(message, hash, timestamp, infoConfig, membersConfig)) { configFactoryProtocol.withMutableGroupConfigs(closedGroupSessionId) { configs ->
if (configs.loadKeys(message, hash, timestamp)) {
total++ total++
} }
}
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for keys on ${closedGroupSessionId.hexString}") if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for keys on ${closedGroupSessionId.hexString}")
} }
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Total key messages consumed: $total") if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Total key messages consumed: $total")
} }
private fun handleInfo(response: RawResponse, private fun handleInfo(response: RawResponse) {
infoConfig: GroupInfoConfig) {
val messages = parseMessages(response) val messages = parseMessages(response)
messages.forEach { (message, hash, _) -> messages.forEach { (message, hash, _) ->
infoConfig.merge(hash to message) configFactoryProtocol.withMutableGroupConfigs(closedGroupSessionId) { configs ->
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for info on ${closedGroupSessionId.hexString}") configs.groupInfo.merge(arrayOf(hash to message))
} }
if (messages.isNotEmpty()) { if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for info on ${closedGroupSessionId.hexString}")
val lastTimestamp = messages.maxOf { it.timestamp }
MessagingModuleConfiguration.shared.storage.notifyConfigUpdates(infoConfig, lastTimestamp)
} }
} }
private fun handleMembers(response: RawResponse, private fun handleMembers(response: RawResponse) {
membersConfig: GroupMembersConfig) {
parseMessages(response).forEach { (message, hash, _) -> 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}") if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Merged $hash for members on ${closedGroupSessionId.hexString}")
} }
} }
private fun handleMessages(body: RawResponse, snode: Snode, keysConfig: GroupKeysConfig) { private fun handleMessages(body: RawResponse, snode: Snode) {
val messages = SnodeAPI.parseRawMessagesResponse( val messages = configFactoryProtocol.withGroupConfigs(closedGroupSessionId) {
SnodeAPI.parseRawMessagesResponse(
rawResponse = body, rawResponse = body,
snode = snode, snode = snode,
publicKey = closedGroupSessionId.hexString, publicKey = closedGroupSessionId.hexString,
decrypt = keysConfig::decrypt decrypt = it.groupKeys::decrypt,
) )
}
val parameters = messages.map { (envelope, serverHash) -> val parameters = messages.map { (envelope, serverHash) ->
MessageReceiveParameters( MessageReceiveParameters(

View File

@ -3,10 +3,17 @@ package org.session.libsession.messaging.sending_receiving.pollers
import android.util.SparseArray import android.util.SparseArray
import androidx.core.util.valueIterator import androidx.core.util.valueIterator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig 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.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile import network.loki.messenger.libsession_util.UserProfile
import nl.komponents.kovenant.Deferred 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.SnodeAPI
import org.session.libsession.snode.SnodeModule import org.session.libsession.snode.SnodeModule
import org.session.libsession.utilities.ConfigFactoryProtocol 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.Base64
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace 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?) { private fun processConfig(snode: Snode, rawMessages: RawResponse, namespace: Int, forConfig: Class<out MutableConfig>) {
if (forConfigObject == null) return
val messages = rawMessages["messages"] as? List<*> val messages = rawMessages["messages"] as? List<*>
val processed = if (!messages.isNullOrEmpty()) { val processed = if (!messages.isNullOrEmpty()) {
SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace) SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace)
@ -152,20 +159,19 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
if (processed.isEmpty()) return if (processed.isEmpty()) return
var latestMessageTimestamp: Long? = null processed.forEach { (body, hash, _) ->
processed.forEach { (body, hash, timestamp) ->
try { try {
forConfigObject.merge(hash to body) configFactory.withMutableUserConfigs { configs ->
latestMessageTimestamp = if (timestamp > (latestMessageTimestamp ?: 0L)) { timestamp } else { latestMessageTimestamp } configs
.allConfigs()
.filter { it.javaClass.isInstance(forConfig) }
.first()
.merge(arrayOf(hash to body))
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, e) 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<Unit, Exception>): Promise<Unit, Exception> { private fun poll(userProfileOnly: Boolean, snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {
@ -181,7 +187,8 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
val hashesToExtend = mutableSetOf<String>() val hashesToExtend = mutableSetOf<String>()
val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth) val userAuth = requireNotNull(MessagingModuleConfiguration.shared.storage.userAuth)
configFactory.user?.let { config -> configFactory.withUserConfigs {
val config = it.userProfile
hashesToExtend += config.currentHashes() hashesToExtend += config.currentHashes()
SnodeAPI.buildAuthenticatedRetrieveBatchRequest( SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, snode = snode,
@ -189,7 +196,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
namespace = config.namespace(), namespace = config.namespace(),
maxSize = -8 maxSize = -8
) )
}?.let { request -> }.let { request ->
requests += request requests += request
} }
@ -199,7 +206,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
auth = userAuth, auth = userAuth,
newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds,
extend = true extend = true
)?.let { extensionRequest -> ).let { extensionRequest ->
requests += extensionRequest requests += extensionRequest
} }
} }
@ -217,7 +224,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
if (body == null) { if (body == null) {
Log.e(TAG, "Batch sub-request didn't contain a body") Log.e(TAG, "Batch sub-request didn't contain a body")
} else { } 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<Unit, Exception>): Promise<Unit, Exception> { private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {
if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) } if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) }
return task { return task {
@ -244,17 +252,19 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
} }
// get the latest convo info volatile // get the latest convo info volatile
val hashesToExtend = mutableSetOf<String>() val hashesToExtend = mutableSetOf<String>()
configFactory.getUserConfigs().map { config -> configFactory.withUserConfigs {
it.allConfigs().map { config ->
hashesToExtend += config.currentHashes() hashesToExtend += config.currentHashes()
SnodeAPI.buildAuthenticatedRetrieveBatchRequest( config.namespace() to SnodeAPI.buildAuthenticatedRetrieveBatchRequest(
snode = snode, snode = snode,
auth = userAuth, auth = userAuth,
namespace = config.namespace(), namespace = config.namespace(),
maxSize = -8 maxSize = -8
) )
}.forEach { request -> }
}.forEach { (namespace, request) ->
// namespaces here should always be set // namespaces here should always be set
requestSparseArray[request.namespace!!] = request requestSparseArray[namespace] = request
} }
val requests = val requests =
@ -266,7 +276,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
auth = userAuth, auth = userAuth,
newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds, newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds,
extend = true extend = true
)?.let { extensionRequest -> ).let { extensionRequest ->
requests += extensionRequest requests += extensionRequest
} }
} }
@ -280,14 +290,15 @@ class Poller(private val configFactory: ConfigFactoryProtocol) {
val responseList = (rawResponses["results"] as List<RawResponse>) val responseList = (rawResponses["results"] as List<RawResponse>)
// in case we had null configs, the array won't be fully populated // 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 // index of the sparse array key iterator should be the request index, with the key being the namespace
listOfNotNull( sequenceOf(
configFactory.user?.namespace(), Namespace.USER_PROFILE() to MutableUserProfile::class.java,
configFactory.contacts?.namespace(), Namespace.CONTACTS() to MutableContacts::class.java,
configFactory.userGroups?.namespace(), Namespace.GROUPS() to MutableUserGroupsConfig::class.java,
configFactory.convoVolatile?.namespace() Namespace.CONVO_INFO_VOLATILE() to MutableConversationVolatileConfig::class.java
).map { ).map { (namespace, configClass) ->
it to requestSparseArray.indexOfKey(it) Triple(namespace, configClass, requestSparseArray.indexOfKey(namespace))
}.filter { (_, i) -> i >= 0 }.forEach { (key, requestIndex) -> }.filter { (_, _, i) -> i >= 0 }
.forEach { (namespace, configClass, requestIndex) ->
responseList.getOrNull(requestIndex)?.let { rawResponse -> responseList.getOrNull(requestIndex)?.let { rawResponse ->
if (rawResponse["code"] as? Int != 200) { if (rawResponse["code"] as? Int != 200) {
Log.e(TAG, "Batch sub-request had non-200 response code, returned code ${(rawResponse["code"] as? Int) ?: "[unknown]"}") 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") Log.e(TAG, "Batch sub-request didn't contain a body")
return@forEach return@forEach
} }
if (key == Namespace.DEFAULT()) {
return@forEach // continue, skip default namespace processConfig(snode, body, namespace, configClass)
} 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)
}
}
} }
} }

View File

@ -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<String, String> {
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<String, String> {
val auth = groupKeysConfig.subAccountSign(data, authData)
return buildMap {
put("subkey_tag", auth.subAccount)
put("signature", auth.signature)
}
}
}

View File

@ -579,7 +579,8 @@ object SnodeAPI {
} }
private data class RequestInfo( private data class RequestInfo(
val accountId: AccountId, val snode: Snode,
val publicKey: String,
val request: SnodeBatchRequestInfo, val request: SnodeBatchRequestInfo,
val responseType: Class<*>, val responseType: Class<*>,
val callback: SendChannel<Result<Any>>, val callback: SendChannel<Result<Any>>,
@ -594,15 +595,17 @@ object SnodeAPI {
val batchWindowMills = 100L val batchWindowMills = 100L
data class BatchKey(val snodeAddress: String, val publicKey: String)
@Suppress("OPT_IN_USAGE") @Suppress("OPT_IN_USAGE")
GlobalScope.launch { GlobalScope.launch {
val batches = hashMapOf<AccountId, MutableList<RequestInfo>>() val batches = hashMapOf<BatchKey, MutableList<RequestInfo>>()
while (true) { while (true) {
val batch = select<List<RequestInfo>?> { val batch = select<List<RequestInfo>?> {
// If we receive a request, add it to the batch // If we receive a request, add it to the batch
batchRequests.onReceive { batchRequests.onReceive {
batches.getOrPut(it.accountId) { mutableListOf() }.add(it) batches.getOrPut(BatchKey(it.snode.address, it.publicKey)) { mutableListOf() }.add(it)
null null
} }
@ -622,11 +625,11 @@ object SnodeAPI {
if (batch != null) { if (batch != null) {
launch batch@{ launch batch@{
val accountId = batch.first().accountId val snode = batch.first().snode
val responses = try { val responses = try {
getBatchResponse( getBatchResponse(
snode = getSingleTargetSnode(accountId.hexString).await(), snode = snode,
publicKey = accountId.hexString, publicKey = batch.first().publicKey,
requests = batch.map { it.request }, sequence = false requests = batch.map { it.request }, sequence = false
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -660,21 +663,23 @@ object SnodeAPI {
} }
suspend fun <T> sendBatchRequest( suspend fun <T> sendBatchRequest(
swarmAccount: AccountId, snode: Snode,
publicKey: String,
request: SnodeBatchRequestInfo, request: SnodeBatchRequestInfo,
responseType: Class<T>, responseType: Class<T>,
): T { ): T {
val callback = Channel<Result<T>>() val callback = Channel<Result<T>>()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
batchedRequestsSender.send(RequestInfo(swarmAccount, request, responseType, callback as SendChannel<Any>)) batchedRequestsSender.send(RequestInfo(snode, publicKey, request, responseType, callback as SendChannel<Any>))
return callback.receive().getOrThrow() return callback.receive().getOrThrow()
} }
suspend fun sendBatchRequest( suspend fun sendBatchRequest(
swarmAccount: AccountId, snode: Snode,
publicKey: String,
request: SnodeBatchRequestInfo, request: SnodeBatchRequestInfo,
): JsonNode { ): JsonNode {
return sendBatchRequest(swarmAccount, request, JsonNode::class.java) return sendBatchRequest(snode, publicKey, request, JsonNode::class.java)
} }
suspend fun getBatchResponse( suspend fun getBatchResponse(
@ -803,9 +808,9 @@ object SnodeAPI {
} }
return scope.retrySuspendAsPromise(maxRetryCount) { return scope.retrySuspendAsPromise(maxRetryCount) {
val destination = message.recipient
sendBatchRequest( sendBatchRequest(
swarmAccount = AccountId(destination), snode = getSingleTargetSnode(message.recipient).await(),
publicKey = message.recipient,
request = SnodeBatchRequestInfo( request = SnodeBatchRequestInfo(
method = Snode.Method.SendMessage.rawValue, method = Snode.Method.SendMessage.rawValue,
params = params, params = params,

View File

@ -10,5 +10,8 @@ data class BatchResponse @JsonCreator constructor(
data class Item @JsonCreator constructor( data class Item @JsonCreator constructor(
@param:JsonProperty("code") val code: Int, @param:JsonProperty("code") val code: Int,
@param:JsonProperty("body") val body: JsonNode, @param:JsonProperty("body") val body: JsonNode,
) ) {
val isSuccessful: Boolean
get() = code in 200..299
}
} }

View File

@ -1,82 +1,109 @@
package org.session.libsession.utilities package org.session.libsession.utilities
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import network.loki.messenger.libsession_util.Config import network.loki.messenger.libsession_util.MutableConfig
import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.MutableContacts
import network.loki.messenger.libsession_util.Contacts import network.loki.messenger.libsession_util.MutableConversationVolatileConfig
import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.MutableGroupInfoConfig
import network.loki.messenger.libsession_util.GroupInfoConfig import network.loki.messenger.libsession_util.MutableGroupKeysConfig
import network.loki.messenger.libsession_util.GroupKeysConfig import network.loki.messenger.libsession_util.MutableGroupMembersConfig
import network.loki.messenger.libsession_util.GroupMembersConfig import network.loki.messenger.libsession_util.MutableUserGroupsConfig
import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.MutableUserProfile
import network.loki.messenger.libsession_util.UserProfile import network.loki.messenger.libsession_util.ReadableConfig
import org.session.libsession.messaging.messages.Destination 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 import org.session.libsignal.utilities.AccountId
interface ConfigFactoryProtocol { interface ConfigFactoryProtocol {
val configUpdateNotifications: Flow<ConfigUpdateNotification>
val user: UserProfile? fun <T> withUserConfigs(cb: (UserConfigs) -> T): T
val contacts: Contacts? fun <T> withMutableUserConfigs(cb: (MutableUserConfigs) -> T): T
val convoVolatile: ConversationVolatileConfig?
val userGroups: UserGroupsConfig?
val configUpdateNotifications: Flow<Unit> fun <T> withGroupConfigs(groupId: AccountId, cb: (GroupConfigs) -> T): T
fun <T> withMutableGroupConfigs(groupId: AccountId, cb: (MutableGroupConfigs) -> T): T
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<ConfigBase>
fun persist(forConfigObject: Config, timestamp: Long, forPublicKey: String? = null)
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): 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 getGroupAuth(groupId: AccountId): SwarmAuth?
fun constructGroupKeysConfig( fun removeGroup(groupId: AccountId)
groupSessionId: AccountId,
info: GroupInfoConfig,
members: GroupMembersConfig
): GroupKeysConfig?
fun maybeDecryptForUser(encoded: ByteArray, fun maybeDecryptForUser(encoded: ByteArray,
domain: String, domain: String,
closedGroupSessionId: AccountId): ByteArray? 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<ReadableConfig> = sequenceOf(contacts, userGroups, userProfile, convoInfoVolatile)
} }
/** interface MutableUserConfigs : UserConfigs {
* Access group configs if they exist, otherwise return null. override val contacts: MutableContacts
* override val userGroups: MutableUserGroupsConfig
* Note: The config objects will be closed after the callback is executed. Any attempt override val userProfile: MutableUserProfile
* to store the config objects will result in a native crash. override val convoInfoVolatile: MutableConversationVolatileConfig
*/
inline fun <T: Any> ConfigFactoryProtocol.withGroupConfigsOrNull( override fun allConfigs(): Sequence<MutableConfig> = sequenceOf(contacts, userGroups, userProfile, convoInfoVolatile)
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 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 <T: Any> 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
//}